Skip to content

Commit

Permalink
feat: add paypal as supported oauth provider
Browse files Browse the repository at this point in the history
Co-authored-by: Sébastien Chopin <seb@nuxt.com>
  • Loading branch information
Yizack and atinux committed Jul 7, 2024
1 parent c8b02d0 commit 57ea01e
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ It can also be set using environment variables:
- Keycloak
- LinkedIn
- Microsoft
- PayPal
- Spotify
- Steam
- Twitch
Expand Down
5 changes: 4 additions & 1 deletion playground/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,8 @@ NUXT_OAUTH_COGNITO_REGION=
# Facebook
NUXT_OAUTH_FACEBOOK_CLIENT_ID=
NUXT_OAUTH_FACEBOOK_CLIENT_SECRET=
# PayPal
NUXT_OAUTH_PAYPAL_CLIENT_ID=
NUXT_OAUTH_PAYPAL_CLIENT_SECRET=
# Steam
NUXT_OAUTH_STEAM_API_KEY=
NUXT_OAUTH_STEAM_API_KEY=
6 changes: 6 additions & 0 deletions playground/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ const providers = computed(() => [
disabled: Boolean(user.value?.keycloak),
icon: 'i-simple-icons-redhat',
},
{
label: session.value.user?.paypal || 'PayPal',
to: '/auth/paypal',
disabled: Boolean(user.value?.paypal),
icon: 'i-simple-icons-paypal',
},
{
label: user.value?.steam || 'Steam',
to: '/auth/steam',
Expand Down
1 change: 1 addition & 0 deletions playground/auth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ declare module '#auth-utils' {
linkedin?: string
cognito?: string
facebook?: string
paypal?: string
steam?: string
}

Expand Down
15 changes: 15 additions & 0 deletions playground/server/routes/auth/paypal.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default oauth.paypalEventHandler({
config: {
emailRequired: true,
},
async onSuccess(event, { user }) {
await setUserSession(event, {
user: {
paypal: user.email,
},
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 @@ -132,6 +132,11 @@ export default defineNuxtModule<ModuleOptions>({
clientId: '',
clientSecret: '',
})
// PayPal OAuth
runtimeConfig.oauth.paypal = defu(runtimeConfig.oauth.paypal, {
clientId: '',
clientSecret: '',
})
// Steam OAuth
runtimeConfig.oauth.steam = defu(runtimeConfig.oauth.steam, {
apiKey: '',
Expand Down
174 changes: 174 additions & 0 deletions src/runtime/server/lib/oauth/paypal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
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 OAuthPaypalConfig {
/**
* PayPal Client ID
* @default process.env.NUXT_OAUTH_PAYPAL_CLIENT_ID
*/
clientId?: string

/**
* PayPal OAuth Client Secret
* @default process.env.NUXT_OAUTH_PAYPAL_CLIENT_SECRET
*/
clientSecret?: string

/**
* PayPal OAuth Scope
* @default []
* @see https://developer.paypal.com/docs/log-in-with-paypal/integrate/reference/#scope-attributes
* @example ['email', 'profile']
*/
scope?: string[]

/**
* Require email from user, adds the ['email'] scope if not present
* @default false
*/
emailRequired?: boolean

/**
* Use PayPal sandbox environment
* @default import.meta.dev // true in development, false in production
*/
sandbox?: boolean

/**
* PayPal OAuth Authorization URL
* @default 'https://www.paypal.com/signin/authorize'
*/
authorizationURL?: string

/**
* PayPal OAuth Token URL
* @default 'https://api-m.paypal.com/v1/oauth2/token'
*/
tokenURL?: string

/**
* Extra authorization parameters to provide to the authorization URL
* @see https://developer.paypal.com/docs/log-in-with-paypal/integrate/build-button/#link-constructauthorizationendpoint
* @example { flowEntry: 'static' }
*/
authorizationParams?: Record<string, string>
}

export function paypalEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthPaypalConfig>) {
return eventHandler(async (event: H3Event) => {
config = defu(config, useRuntimeConfig(event).oauth?.paypal, {
sandbox: import.meta.dev,
authorizationURL: 'https://www.paypal.com/signin/authorize',
tokenURL: 'https://api-m.paypal.com/v1/oauth2/token',
authorizationParams: {},
}) as OAuthPaypalConfig
const { code } = getQuery(event)

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

let paypalAPI = 'api-m.paypal.com'

if (config.sandbox) {
paypalAPI = 'api-m.sandbox.paypal.com'
config.authorizationURL = 'https://www.sandbox.paypal.com/signin/authorize'
config.tokenURL = `https://${paypalAPI}/v1/oauth2/token`
}

const redirectUrl = getRequestURL(event).href
if (!code) {
config.scope = config.scope || []
if (!config.scope.includes('openid')) {
config.scope.push('openid')
}
if (config.emailRequired && !config.scope.includes('email')) {
config.scope.push('email')
}

// Redirect to PayPal Oauth page
return sendRedirect(
event,
withQuery(config.authorizationURL as string, {
response_type: 'code',
client_id: config.clientId,
redirect_uri: redirectUrl,
scope: config.scope.join(' '),
flowEntry: 'static',
...config.authorizationParams,
}),
)
}

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: {
'Authorization': `Basic ${authCode}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
params: {
grant_type: 'authorization_code',
redirect_uri: encodeURIComponent(parsePath(redirectUrl).pathname),
code,
},
},
).catch((error) => {
return { error }
})

if (tokens.error) {
const error = createError({
statusCode: 401,
message: `PayPal 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 users: any = await $fetch(`https://${paypalAPI}/v1/identity/openidconnect/userinfo`, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
params: {
schema: 'openid',
},
})

const user = users

if (!user) {
const error = createError({
statusCode: 500,
message: 'Could not get PayPal user',
data: tokens,
})
if (!onError) throw error
return onError(event, error)
}

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

export const oauth = {
Expand All @@ -25,5 +26,6 @@ export const oauth = {
linkedinEventHandler,
cognitoEventHandler,
facebookEventHandler,
paypalEventHandler,
steamEventHandler,
}

0 comments on commit 57ea01e

Please sign in to comment.