Skip to content

Commit

Permalink
feat: add facebook OAuth provider
Browse files Browse the repository at this point in the history
* feat: Add Facebook oAuth provider

* feat: add Facebook provider to README

* fix: lint README

* fix: typo in README

* fix: lint app.vue

* fix: typo in README

* chore: use name for playground

---------

Co-authored-by: Sébastien Chopin <seb@nuxt.com>
  • Loading branch information
adam-hudak and Atinux committed Apr 19, 2024
1 parent b74d47b commit 777d8b2
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 31 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ It can also be set using environment variables:
- AWS Cognito
- Battle.net
- Discord
- Facebook
- GitHub
- Google
- Keycloak
Expand Down
5 changes: 4 additions & 1 deletion playground/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,7 @@ NUXT_OAUTH_LINKEDIN_CLIENT_SECRET=
NUXT_OAUTH_COGNITO_USER_POOL_ID=
NUXT_OAUTH_COGNITO_CLIENT_ID=
NUXT_OAUTH_COGNITO_CLIENT_SECRET=
NUXT_OAUTH_COGNITO_REGION=
NUXT_OAUTH_COGNITO_REGION=
# Facebook
NUXT_OAUTH_FACEBOOK_CLIENT_ID=
NUXT_OAUTH_FACEBOOK_CLIENT_SECRET=
66 changes: 36 additions & 30 deletions playground/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,54 @@
const { loggedIn, user, session, clear } = useUserSession()
const providers = computed(() => [
{
label: session.value.user?.google || 'Google',
to: '/auth/google',
disabled: Boolean(user.value?.google),
icon: 'i-simple-icons-google',
},
{
label: session.value.user?.facebook || 'Facebook',
to: '/auth/facebook',
disabled: Boolean(user.value?.facebook),
icon: 'i-simple-icons-facebook',
},
{
label: session.value.user?.github || 'GitHub',
to: '/auth/github',
disabled: Boolean(user.value?.github),
icon: 'i-simple-icons-github',
},
{
label: user.value?.linkedin || 'LinkedIn',
to: '/auth/linkedin',
disabled: Boolean(user.value?.linkedin),
icon: 'i-simple-icons-linkedin',
},
{
label: user.value?.microsoft || 'Microsoft',
to: '/auth/microsoft',
disabled: Boolean(user.value?.microsoft),
icon: 'i-simple-icons-microsoft',
},
{
label: user.value?.cognito || 'Cognito',
to: '/auth/cognito',
disabled: Boolean(user.value?.cognito),
icon: 'i-simple-icons-amazonaws',
},
{
label: user.value?.discord || 'Discord',
to: '/auth/discord',
disabled: Boolean(user.value?.discord),
icon: 'i-simple-icons-discord',
},
{
label: session.value.user?.spotify || 'Spotify',
to: '/auth/spotify',
disabled: Boolean(user.value?.spotify),
icon: 'i-simple-icons-spotify',
},
{
label: session.value.user?.google || 'Google',
to: '/auth/google',
disabled: Boolean(user.value?.google),
icon: 'i-simple-icons-google',
},
{
label: session.value.user?.twitch || 'Twitch',
to: '/auth/twitch',
Expand All @@ -32,42 +62,18 @@ const providers = computed(() => [
disabled: Boolean(user.value?.auth0),
icon: 'i-simple-icons-auth0',
},
{
label: user.value?.discord || 'Discord',
to: '/auth/discord',
disabled: Boolean(user.value?.discord),
icon: 'i-simple-icons-discord',
},
{
label: user.value?.battledotnet || 'Battle.net',
to: '/auth/battledotnet',
disabled: Boolean(user.value?.battledotnet),
icon: 'i-simple-icons-battledotnet',
},
{
label: user.value?.microsoft || 'Microsoft',
to: '/auth/microsoft',
disabled: Boolean(user.value?.microsoft),
icon: 'i-simple-icons-microsoft',
},
{
label: user.value?.keycloak || 'Keycloak',
to: '/auth/keycloak',
disabled: Boolean(user.value?.keycloak),
icon: 'i-simple-icons-redhat',
},
{
label: user.value?.linkedin || 'LinkedIn',
to: '/auth/linkedin',
disabled: Boolean(user.value?.linkedin),
icon: 'i-simple-icons-linkedin',
},
{
label: user.value?.cognito || 'Cognito',
to: '/auth/cognito',
disabled: Boolean(user.value?.cognito),
icon: 'i-simple-icons-amazonaws',
},
].map(p => ({
...p,
prefetch: false,
Expand Down
1 change: 1 addition & 0 deletions playground/auth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ declare module '#auth-utils' {
keycloak?: string
linkedin?: string
cognito?: string
facebook?: string
}

interface UserSession {
Expand Down
12 changes: 12 additions & 0 deletions playground/server/routes/auth/facebook.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default oauth.facebookEventHandler({
async onSuccess(event, { user }) {
await setUserSession(event, {
user: {
facebook: user.name,
},
loggedInAt: Date.now(),
})

return sendRedirect(event, '/')
},
})
5 changes: 5 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,5 +147,10 @@ export default defineNuxtModule<ModuleOptions>({
region: '',
userPoolId: '',
})
// Facebook OAuth
runtimeConfig.oauth.facebook = defu(runtimeConfig.oauth.facebook, {
clientId: '',
clientSecret: '',
})
},
})
138 changes: 138 additions & 0 deletions src/runtime/server/lib/oauth/facebook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import type { H3Event } from 'h3'
import {
eventHandler,
createError,
getQuery,
getRequestURL,
sendRedirect,
} from 'h3'
import { ofetch } from 'ofetch'
import { withQuery } from 'ufo'
import { defu } from 'defu'
import { useRuntimeConfig } from '#imports'
import type { OAuthConfig } from '#auth-utils'

export interface OAuthFacebookConfig {
/**
* Facebook OAuth Client ID
* @default process.env.NUXT_OAUTH_FACEBOOK_CLIENT_ID
*/
clientId?: string
/**
* Facebook OAuth Client Secret
* @default process.env.NUXT_OAUTH_FACEBOOK_CLIENT_SECRET
*/
clientSecret?: string
/**
* Facebook OAuth Scope
* @default []
* @see https://developers.facebook.com/docs/permissions
* @example [ 'email' ],
*/
scope?: string[]

/**
* Facebook OAuth Authorization URL
* @default 'https://www.facebook.com/v19.0/dialog/oauth'
*/
authorizationURL?: string

/**
* Facebook OAuth Token URL
* @default 'https://graph.facebook.com/v19.0/oauth/access_token'
*/
tokenURL?: string

/**
* Extra authorization parameters to provide to the authorization URL
* @see https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow/
*/
authorizationParams?: Record<string, string>
}

export function facebookEventHandler({
config,
onSuccess,
onError,
}: OAuthConfig<OAuthFacebookConfig>) {
return eventHandler(async (event: H3Event) => {
config = defu(config, useRuntimeConfig(event).oauth?.facebook, {
authorizationURL: 'https://www.facebook.com/v19.0/dialog/oauth',
tokenURL: 'https://graph.facebook.com/v19.0/oauth/access_token',
authorizationParams: {},
}) as OAuthFacebookConfig
const query = getQuery(event)

if (query.error) {
const error = createError({
statusCode: 401,
message: `Facebook login failed: ${query.error || 'Unknown error'}`,
data: query,
})
if (!onError) throw error
return onError(event, error)
}

if (!config.clientId) {
const error = createError({
statusCode: 500,
message:
'Missing NUXT_OAUTH_FACEBOOK_CLIENT_ID or NUXT_OAUTH_FACEBOOK_CLIENT_SECRET env variables.',
})
if (!onError) throw error
return onError(event, error)
}

const redirectUrl = getRequestURL(event).href

if (!query.code) {
config.scope = config.scope || []
// Redirect to Facebook Oauth page
return sendRedirect(
event,
withQuery(config.authorizationURL as string, {
client_id: config.clientId,
redirect_uri: redirectUrl,
scope: config.scope.join(' '),
}),
)
}

// TODO: improve typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const tokens: any = await $fetch(config.tokenURL as string, {
method: 'POST',
body: {
client_id: config.clientId,
client_secret: config.clientSecret,
redirect_uri: redirectUrl,
code: query.code,
},
})
if (tokens.error) {
const error = createError({
statusCode: 401,
message: `Facebook login failed: ${tokens.error || 'Unknown error'}`,
data: tokens,
})
if (!onError) throw error
return onError(event, error)
}

const accessToken = tokens.access_token
// TODO: improve typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const user: any = await ofetch(
`https://graph.facebook.com/v19.0/me?fields=id,name&access_token=${accessToken}`,
)

if (!user) {
throw new Error('Facebook login failed: no user found')
}

return onSuccess(event, {
user,
tokens,
})
})
}
2 changes: 2 additions & 0 deletions src/runtime/server/utils/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { battledotnetEventHandler } from '../lib/oauth/battledotnet'
import { keycloakEventHandler } from '../lib/oauth/keycloak'
import { linkedinEventHandler } from '../lib/oauth/linkedin'
import { cognitoEventHandler } from '../lib/oauth/cognito'
import { facebookEventHandler } from '../lib/oauth/facebook'

export const oauth = {
githubEventHandler,
Expand All @@ -22,4 +23,5 @@ export const oauth = {
keycloakEventHandler,
linkedinEventHandler,
cognitoEventHandler,
facebookEventHandler,
}

0 comments on commit 777d8b2

Please sign in to comment.