Skip to content

Commit

Permalink
feat: added google as oauth provider (#3)
Browse files Browse the repository at this point in the history
* feat - added google login under provider

* feat - added google login under provider

* feat - added google as auth provider

* chore: simplify gitignore

* chore: various improvements

---------

Co-authored-by: Akshara Hegde <[email protected]>
Co-authored-by: Sébastien Chopin <[email protected]>
  • Loading branch information
3 people authored Nov 9, 2023
1 parent cbc8b7a commit af4e2d1
Show file tree
Hide file tree
Showing 15 changed files with 209 additions and 20 deletions.
16 changes: 16 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -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
}
}
7 changes: 1 addition & 6 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"dependencies": {
"@nuxt/kit": "^3.8.1",
"defu": "^6.1.3",
"ofetch": "^1.3.3",
"ohash": "^1.1.3"
},
"devDependencies": {
Expand Down
3 changes: 3 additions & 0 deletions playground/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
10 changes: 10 additions & 0 deletions playground/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ const { loggedIn, session, clear } = useUserSession()
>
Login with Spotify
</UButton>
<UButton
v-if="!loggedIn || !session.user.google"
to="/auth/google"
icon="i-simple-icons-google"
external
color="gray"
size="xs"
>
Login with Google
</UButton>
<UButton
v-if="loggedIn"
color="gray"
Expand Down
9 changes: 5 additions & 4 deletions playground/auth.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
declare module '#auth-utils' {
interface UserSession {
user: {
spotify?: any
github?: any
}
loggedInAt: number
spotify?: any;
github?: any;
google?: any;
};
loggedInAt: number;
}
}
2 changes: 1 addition & 1 deletion playground/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"private": true,
"name": "my-module-playground",
"name": "auth-utils-playground",
"type": "module",
"scripts": {
"dev": "nuxi dev",
Expand Down
12 changes: 12 additions & 0 deletions playground/server/routes/auth/google.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default oauth.googleEventHandler({
async onSuccess(event, { user }) {
await setUserSession(event, {
user: {
google: user,
},
loggedInAt: Date.now()
})

return sendRedirect(event, '/')
}
})
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 9 additions & 6 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,19 @@ export interface ModuleOptions {}

export default defineNuxtModule<ModuleOptions>({
meta: {
name: 'auth-core',
name: 'auth-utils',
configKey: 'auth'
},
// Default configuration options of the Nuxt module
defaults: {},
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')
Expand Down Expand Up @@ -81,5 +79,10 @@ export default defineNuxtModule<ModuleOptions>({
clientId: '',
clientSecret: ''
})

runtimeConfig.oauth.google = defu(runtimeConfig.oauth.google, {
clientId: '',
clientSecret: ''
})
}
})
140 changes: 140 additions & 0 deletions src/runtime/server/lib/oauth/google.ts
Original file line number Diff line number Diff line change
@@ -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> | void;
onError?: (event: H3Event, error: H3Error) => Promise<void> | 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,
})
})
}
4 changes: 3 additions & 1 deletion src/runtime/server/utils/oauth.ts
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 1 addition & 1 deletion test/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('<div>basic</div>')
expect(html).toContain('<div>Nuxt Auth Utils</div>')
})
})
2 changes: 1 addition & 1 deletion test/fixtures/basic/app.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div>basic</div>
<div>Nuxt Auth Utils</div>
</template>

<script setup>
Expand Down

0 comments on commit af4e2d1

Please sign in to comment.