Skip to content

Commit

Permalink
feat: add x(formerly twitter) as supported oauth provider
Browse files Browse the repository at this point in the history
* Add x auth

* chore: add x auth and update .env.example

* fix: x login flow

* feat: update user fields

* fix: optimize user fields query parameters

* up

---------

Co-authored-by: Sébastien Chopin <[email protected]>
  • Loading branch information
stonega and atinux authored Jul 7, 2024
1 parent 57ea01e commit a0be1f2
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 0 deletions.
3 changes: 3 additions & 0 deletions playground/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,6 @@ NUXT_OAUTH_PAYPAL_CLIENT_ID=
NUXT_OAUTH_PAYPAL_CLIENT_SECRET=
# Steam
NUXT_OAUTH_STEAM_API_KEY=
# X
NUXT_OAUTH_X_CLIENT_ID=
NUXT_OAUTH_X_CLIENT_SECRET=
6 changes: 6 additions & 0 deletions playground/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ const providers = computed(() => [
disabled: Boolean(user.value?.steam),
icon: 'i-simple-icons-steam',
},
{
label: user.value?.x || 'X',
to: '/auth/x',
disabled: Boolean(user.value?.x),
icon: 'i-simple-icons-x',
},
].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 @@ -15,6 +15,7 @@ declare module '#auth-utils' {
facebook?: string
paypal?: string
steam?: string
x?: string
}

interface UserSession {
Expand Down
3 changes: 3 additions & 0 deletions playground/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export default defineNuxtConfig({
compatibilityDate: '2024-06-17',
devServer: {
host: '127.0.0.1',
},
extends: ['@nuxt/ui-pro'],
modules: [
'nuxt-auth-utils',
Expand Down
12 changes: 12 additions & 0 deletions playground/server/routes/auth/x.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default oauth.xEventHandler({
async onSuccess(event, { user }) {
await setUserSession(event, {
user: {
x: user.username,
},
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 @@ -141,5 +141,10 @@ export default defineNuxtModule<ModuleOptions>({
runtimeConfig.oauth.steam = defu(runtimeConfig.oauth.steam, {
apiKey: '',
})
// X OAuth
runtimeConfig.oauth.x = defu(runtimeConfig.oauth.x, {
clientId: '',
clientSecret: '',
})
},
})
158 changes: 158 additions & 0 deletions src/runtime/server/lib/oauth/x.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { randomUUID } from 'node:crypto'
import type { H3Event } from 'h3'
import {
eventHandler,
createError,
getQuery,
getRequestURL,
sendRedirect,
} from 'h3'
import { withQuery, parsePath } from 'ufo'
import { defu } from 'defu'
import { useRuntimeConfig } from '#imports'
import type { OAuthConfig } from '#auth-utils'

export interface OAuthXConfig {
/**
* X OAuth Client ID
* @default process.env.NUXT_OAUTH_X_CLIENT_ID
*/
clientId?: string
/**
* X OAuth Client Secret
* @default process.env.NUXT_OAUTH_X_CLIENT_SECRET
*/
clientSecret?: string
/**
* X OAuth Scope
* @default []
* @see https://developer.x.com/en/docs/authentication/oauth-2-0/user-access-token
* @example [ 'tweet.read','users.read','offline.access ],
*/
scope?: string[]

/**
* X OAuth Authorization URL
* @default 'https://twitter.com/i/oauth2/authorize'
*/
authorizationURL?: string

/**
* X OAuth Token URL
* @default 'https://api.twitter.com/2/oauth2/token'
*/
tokenURL?: string

/**
* X OAuth User URL
* @default 'https://api.twitter.com/2/users/me'
*/
userURL?: string

/**
* Extra authorization parameters to provide to the authorization URL
* @see https://developer.x.com/en/docs/authentication/oauth-2-0/user-access-token
*/
authorizationParams: Record<string, string>
}

export function xEventHandler({
config,
onSuccess,
onError,
}: OAuthConfig<OAuthXConfig>) {
return eventHandler(async (event: H3Event) => {
config = defu(config, useRuntimeConfig(event).oauth?.x, {
authorizationURL: 'https://twitter.com/i/oauth2/authorize',
tokenURL: 'https://api.twitter.com/2/oauth2/token',
userURL: 'https://api.twitter.com/2/users/me',
authorizationParams: {
state: randomUUID(),
code_challenge: randomUUID(),
},
}) as OAuthXConfig
const { code } = getQuery(event)

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

const redirectUrl = getRequestURL(event).href
if (!code) {
config.scope = config.scope || ['tweet.read', 'users.read', 'offline.access']
// Redirect to X Oauth page
return sendRedirect(
event,
withQuery(config.authorizationURL as string, {
response_type: 'code',
client_id: config.clientId,
code_challenge_method: 'plain',
redirect_uri: redirectUrl,
scope: config.scope.join(' '),
...config.authorizationParams,
}),
)
}

// TODO: improve typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const params: any = {
grant_type: 'authorization_code',
code_verifier: config.authorizationParams.code_challenge,
redirect_uri: parsePath(redirectUrl).pathname,
code,
}

const authCode = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64')
// TODO: improve typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const tokens: any = await $fetch(config.tokenURL as string, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${authCode}`,
},
params,
}).catch((error) => {
return { error }
})
if (tokens.error) {
const error = createError({
statusCode: 401,
message: `X login failed: ${
tokens.error?.data?.error_description || '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 $fetch(
config.userURL as string,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
query: {
'user.fields': 'description,id,name,profile_image_url,username,verified,verified_type',
},
},
).catch((error) => {
return error
})

return onSuccess(event, {
tokens,
user: user?.data,
})
})
}
2 changes: 2 additions & 0 deletions src/runtime/server/utils/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { cognitoEventHandler } from '../lib/oauth/cognito'
import { facebookEventHandler } from '../lib/oauth/facebook'
import { paypalEventHandler } from '../lib/oauth/paypal'
import { steamEventHandler } from '../lib/oauth/steam'
import { xEventHandler } from '../lib/oauth/x'

export const oauth = {
githubEventHandler,
Expand All @@ -28,4 +29,5 @@ export const oauth = {
facebookEventHandler,
paypalEventHandler,
steamEventHandler,
xEventHandler,
}

0 comments on commit a0be1f2

Please sign in to comment.