diff --git a/.eslintrc.cjs b/.eslintrc.cjs
new file mode 100644
index 00000000..c2431ac4
--- /dev/null
+++ b/.eslintrc.cjs
@@ -0,0 +1,16 @@
+module.exports = {
+ root: true,
+ extends: [
+ '@nuxt/eslint-config'
+ ],
+ rules: {
+ // Global
+ semi: ['error', 'never'],
+ quotes: ['error', 'single'],
+ 'quote-props': ['error', 'as-needed'],
+ // Vue
+ 'vue/multi-word-component-names': 0,
+ 'vue/max-attributes-per-line': 'off',
+ 'vue/no-v-html': 0
+ }
+}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 7a3eb22b..8920b020 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,12 +34,7 @@ coverage
.nyc_output
# VSCode
-.vscode/*
-!.vscode/settings.json
-!.vscode/tasks.json
-!.vscode/launch.json
-!.vscode/extensions.json
-!.vscode/*.code-snippets
+.vscode
# Intellij idea
*.iml
diff --git a/README.md b/README.md
index 894becf0..515af1ed 100644
--- a/README.md
+++ b/README.md
@@ -148,6 +148,9 @@ It can also be set using environment variables:
Supported providers:
- GitHub
- Spotify
+- Google
+
+You can add your favorite provider by creating a new file in [src/runtime/server/lib/oauth/](./src/runtime/server/lib/oauth/).
### Example
diff --git a/package.json b/package.json
index f88a7090..115855d7 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
"dependencies": {
"@nuxt/kit": "^3.8.1",
"defu": "^6.1.3",
+ "ofetch": "^1.3.3",
"ohash": "^1.1.3"
},
"devDependencies": {
diff --git a/playground/.env.example b/playground/.env.example
index 5646a9ed..60e761b7 100644
--- a/playground/.env.example
+++ b/playground/.env.example
@@ -5,3 +5,6 @@ NUXT_OAUTH_GITHUB_CLIENT_SECRET=
# Spotify OAuth
NUXT_OAUTH_SPOTIFY_CLIENT_ID=
NUXT_OAUTH_SPOTIFY_CLIENT_SECRET=
+# Google OAuth
+NUXT_OAUTH_GOOGLE_CLIENT_ID=
+NUXT_OAUTH_GOOGLE_CLIENT_SECRET=
diff --git a/playground/app.vue b/playground/app.vue
index 71105b02..77ace8db 100644
--- a/playground/app.vue
+++ b/playground/app.vue
@@ -25,6 +25,16 @@ const { loggedIn, session, clear } = useUserSession()
>
Login with Spotify
+
+ Login with Google
+
({
meta: {
- name: 'auth-core',
+ name: 'auth-utils',
configKey: 'auth'
},
// Default configuration options of the Nuxt module
@@ -15,13 +15,11 @@ export default defineNuxtModule({
setup (options, nuxt) {
const resolver = createResolver(import.meta.url)
- if (!process.env.NUXT_SESSION_PASSWORD) {
+ if (!process.env.NUXT_SESSION_PASSWORD && !nuxt.options._prepare) {
const randomPassword = sha256(`${Date.now()}${Math.random()}`).slice(0, 32)
process.env.NUXT_SESSION_PASSWORD = randomPassword
- if (!nuxt.options._prepare) {
- console.warn('No session password set, using a random password, please set NUXT_SESSION_PASSWORD in your .env file with at least 32 chars')
- console.log(`NUXT_SESSION_PASSWORD=${randomPassword}`)
- }
+ console.warn('No session password set, using a random password, please set NUXT_SESSION_PASSWORD in your .env file with at least 32 chars')
+ console.log(`NUXT_SESSION_PASSWORD=${randomPassword}`)
}
nuxt.options.alias['#auth-utils'] = resolver.resolve('./runtime/types/auth-utils-session')
@@ -81,5 +79,10 @@ export default defineNuxtModule({
clientId: '',
clientSecret: ''
})
+
+ runtimeConfig.oauth.google = defu(runtimeConfig.oauth.google, {
+ clientId: '',
+ clientSecret: ''
+ })
}
})
diff --git a/src/runtime/server/lib/oauth/google.ts b/src/runtime/server/lib/oauth/google.ts
new file mode 100644
index 00000000..2ecea9c2
--- /dev/null
+++ b/src/runtime/server/lib/oauth/google.ts
@@ -0,0 +1,140 @@
+import type { H3Event, H3Error } from 'h3'
+import {
+ eventHandler,
+ createError,
+ getQuery,
+ getRequestURL,
+ sendRedirect,
+} from 'h3'
+import { withQuery, parsePath } from 'ufo'
+import { ofetch } from 'ofetch'
+import { defu } from 'defu'
+import { useRuntimeConfig } from '#imports'
+
+export interface OAuthGoogleConfig {
+ /**
+ * Google OAuth Client ID
+ * @default process.env.NUXT_OAUTH_GOOGLE_CLIENT_ID
+ */
+ clientId?: string;
+
+ /**
+ * Google OAuth Client Secret
+ * @default process.env.NUXT_OAUTH_GOOGLE_CLIENT_SECRET
+ */
+ clientSecret?: string;
+
+ /**
+ * Google OAuth Scope
+ * @default []
+ * @see https://developers.google.com/identity/protocols/oauth2/scopes#google-sign-in
+ * @example ['email', 'openid', 'profile']
+ */
+ scope?: string[];
+
+ /**
+ * Google OAuth Authorization URL
+ * @default 'https://accounts.google.com/o/oauth2/v2/auth'
+ */
+ authorizationURL?: string;
+
+ /**
+ * Google OAuth Token URL
+ * @default 'https://oauth2.googleapis.com/token'
+ */
+ tokenURL?: string;
+
+ /**
+ * Redirect URL post authenticating via google
+ * @default '/auth/google'
+ */
+ redirectUrl: '/auth/google';
+}
+
+interface OAuthConfig {
+ config?: OAuthGoogleConfig;
+ onSuccess: (
+ event: H3Event,
+ result: { user: any; tokens: any }
+ ) => Promise | void;
+ onError?: (event: H3Event, error: H3Error) => Promise | void;
+}
+
+export function googleEventHandler({
+ config,
+ onSuccess,
+ onError,
+}: OAuthConfig) {
+ return eventHandler(async (event: H3Event) => {
+ // @ts-ignore
+ config = defu(config, useRuntimeConfig(event).oauth?.google, {
+ authorizationURL: 'https://accounts.google.com/o/oauth2/v2/auth',
+ tokenURL: 'https://oauth2.googleapis.com/token',
+ }) as OAuthGoogleConfig
+ const { code } = getQuery(event)
+
+ if (!config.clientId) {
+ const error = createError({
+ statusCode: 500,
+ message: 'Missing NUXT_OAUTH_GOOGLE_CLIENT_ID env variables.',
+ })
+ if (!onError) throw error
+ return onError(event, error)
+ }
+
+ const redirectUrl = getRequestURL(event).href
+ if (!code) {
+ config.scope = config.scope || ['email', 'profile']
+ // Redirect to Google Oauth page
+ return sendRedirect(
+ event,
+ withQuery(config.authorizationURL as string, {
+ response_type: 'code',
+ client_id: config.clientId,
+ redirect_uri: redirectUrl,
+ scope: config.scope.join(' '),
+ })
+ )
+ }
+
+ const body: any = {
+ grant_type: 'authorization_code',
+ redirect_uri: parsePath(redirectUrl).pathname,
+ client_id: config.clientId,
+ client_secret: config.clientSecret,
+ code,
+ }
+ const tokens: any = await ofetch(config.tokenURL as string, {
+ method: 'POST',
+ body,
+ }).catch((error) => {
+ return { error }
+ })
+ if (tokens.error) {
+ const error = createError({
+ statusCode: 401,
+ message: `Google login failed: ${
+ tokens.error?.data?.error_description || 'Unknown error'
+ }`,
+ data: tokens,
+ })
+ if (!onError) throw error
+ return onError(event, error)
+ }
+
+ const accessToken = tokens.access_token
+ const user: any = await ofetch(
+ 'https://www.googleapis.com/oauth2/v3/userinfo',
+ {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ }
+ )
+
+ return onSuccess(event, {
+ tokens,
+ user,
+ })
+ })
+}
diff --git a/src/runtime/server/utils/oauth.ts b/src/runtime/server/utils/oauth.ts
index cd8389e6..6878bfa1 100644
--- a/src/runtime/server/utils/oauth.ts
+++ b/src/runtime/server/utils/oauth.ts
@@ -1,7 +1,9 @@
import { githubEventHandler } from '../lib/oauth/github'
+import { googleEventHandler } from '../lib/oauth/google'
import { spotifyEventHandler } from '../lib/oauth/spotify'
export const oauth = {
githubEventHandler,
- spotifyEventHandler
+ spotifyEventHandler,
+ googleEventHandler
}
diff --git a/test/basic.test.ts b/test/basic.test.ts
index c4735db8..5ce88ea5 100644
--- a/test/basic.test.ts
+++ b/test/basic.test.ts
@@ -10,6 +10,6 @@ describe('ssr', async () => {
it('renders the index page', async () => {
// Get response to a server-rendered page with `$fetch`.
const html = await $fetch('/')
- expect(html).toContain('basic
')
+ expect(html).toContain('Nuxt Auth Utils
')
})
})
diff --git a/test/fixtures/basic/app.vue b/test/fixtures/basic/app.vue
index 29a9c81f..b58e1169 100644
--- a/test/fixtures/basic/app.vue
+++ b/test/fixtures/basic/app.vue
@@ -1,5 +1,5 @@
- basic
+ Nuxt Auth Utils