diff --git a/README.md b/README.md index 7db3d050..8fe3ae69 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,7 @@ It can also be set using environment variables: #### Supported OAuth Providers - Auth0 +- AWS Cognito - Battle.net - Discord - GitHub diff --git a/playground/.env.example b/playground/.env.example index eb15a448..d972c112 100644 --- a/playground/.env.example +++ b/playground/.env.example @@ -33,3 +33,8 @@ NUXT_OAUTH_KEYCLOAK_REALM= # LinkedIn NUXT_OAUTH_LINKEDIN_CLIENT_ID= NUXT_OAUTH_LINKEDIN_CLIENT_SECRET= +# Cognito +NUXT_OAUTH_COGNITO_USER_POOL_ID= +NUXT_OAUTH_COGNITO_CLIENT_ID= +NUXT_OAUTH_COGNITO_CLIENT_SECRET= +NUXT_OAUTH_COGNITO_REGION= \ No newline at end of file diff --git a/playground/app.vue b/playground/app.vue index b46e6d37..f92cf87e 100644 --- a/playground/app.vue +++ b/playground/app.vue @@ -61,6 +61,12 @@ const providers = computed(() => [ to: '/auth/linkedin', disabled: Boolean(user.value?.linkedin), icon: 'i-simple-icons-linkedin', + }, + { + label: user.value?.cognito?.email || 'Cognito', + to: '/auth/cognito', + disabled: Boolean(user.value?.cognito), + icon: 'i-simple-icons-amazonaws', } ].map(p => ({ ...p, diff --git a/playground/server/routes/auth/cognito.get.ts b/playground/server/routes/auth/cognito.get.ts new file mode 100644 index 00000000..1785d41c --- /dev/null +++ b/playground/server/routes/auth/cognito.get.ts @@ -0,0 +1,12 @@ +export default oauth.cognitoEventHandler({ + async onSuccess(event, { user }) { + await setUserSession(event, { + user: { + cognito: user, + }, + loggedInAt: Date.now() + }) + + return sendRedirect(event, '/') + } +}) diff --git a/src/module.ts b/src/module.ts index 609efc06..6366a3b3 100644 --- a/src/module.ts +++ b/src/module.ts @@ -132,5 +132,12 @@ export default defineNuxtModule({ clientId: '', clientSecret: '' }) + // Cognito OAuth + runtimeConfig.oauth.cognito = defu(runtimeConfig.oauth.cognito, { + clientId: '', + clientSecret: '', + region: '', + userPoolId: '' + }) } }) diff --git a/src/runtime/server/lib/oauth/cognito.ts b/src/runtime/server/lib/oauth/cognito.ts new file mode 100644 index 00000000..e238e5e1 --- /dev/null +++ b/src/runtime/server/lib/oauth/cognito.ts @@ -0,0 +1,106 @@ +import type { H3Event } 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' +import type { OAuthConfig } from '#auth-utils' + +export interface OAuthCognitoConfig { + /** + * AWS Cognito App Client ID + * @default process.env.NUXT_OAUTH_COGNITO_CLIENT_ID + */ + clientId?: string + /** + * AWS Cognito App Client Secret + * @default process.env.NUXT_OAUTH_COGNITO_CLIENT_SECRET + */ + clientSecret?: string + /** + * AWS Cognito User Pool ID + * @default process.env.NUXT_OAUTH_COGNITO_USER_POOL_ID + */ + userPoolId?: string + /** + * AWS Cognito Region + * @default process.env.NUXT_OAUTH_COGNITO_REGION + */ + region?: string + /** + * AWS Cognito Scope + * @default [] + */ + scope?: string[] +} + +export function cognitoEventHandler({ config, onSuccess, onError }: OAuthConfig) { + return eventHandler(async (event: H3Event) => { + // @ts-ignore + config = defu(config, useRuntimeConfig(event).oauth?.cognito) as OAuthCognitoConfig + const { code } = getQuery(event) + + if (!config.clientId || !config.clientSecret || !config.userPoolId || !config.region) { + const error = createError({ + statusCode: 500, + message: 'Missing NUXT_OAUTH_COGNITO_CLIENT_ID, NUXT_OAUTH_COGNITO_CLIENT_SECRET, NUXT_OAUTH_COGNITO_USER_POOL_ID, or NUXT_OAUTH_COGNITO_REGION env variables.' + }) + if (!onError) throw error + return onError(event, error) + } + + const authorizationURL = `https://${config.userPoolId}.auth.${config.region}.amazoncognito.com/oauth2/authorize` + const tokenURL = `https://${config.userPoolId}.auth.${config.region}.amazoncognito.com/oauth2/token` + + const redirectUrl = getRequestURL(event).href + if (!code) { + config.scope = config.scope || ['openid', 'profile'] + // Redirect to Cognito login page + return sendRedirect( + event, + withQuery(authorizationURL as string, { + client_id: config.clientId, + redirect_uri: redirectUrl, + response_type: 'code', + scope: config.scope.join(' '), + }) + ) + } + + const tokens: any = await ofetch( + tokenURL as string, + { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: `grant_type=authorization_code&client_id=${config.clientId}&client_secret=${config.clientSecret}&redirect_uri=${parsePath(redirectUrl).pathname}&code=${code}`, + } + ).catch(error => { + return { error } + }) + + if (tokens.error) { + const error = createError({ + statusCode: 401, + message: `Cognito login failed: ${tokens.error_description || 'Unknown error'}`, + data: tokens + }) + if (!onError) throw error + return onError(event, error) + } + + const tokenType = tokens.token_type + const accessToken = tokens.access_token + const user: any = await ofetch(`https://${config.userPoolId}.auth.${config.region}.amazoncognito.com/oauth2/userInfo`, { + headers: { + Authorization: `${tokenType} ${accessToken}` + } + }) + + return onSuccess(event, { + tokens, + user + }) + }) +} \ No newline at end of file diff --git a/src/runtime/server/utils/oauth.ts b/src/runtime/server/utils/oauth.ts index 5082447b..e22bab81 100644 --- a/src/runtime/server/utils/oauth.ts +++ b/src/runtime/server/utils/oauth.ts @@ -8,6 +8,7 @@ import { discordEventHandler } from '../lib/oauth/discord' import { battledotnetEventHandler } from '../lib/oauth/battledotnet' import { keycloakEventHandler } from '../lib/oauth/keycloak' import { linkedinEventHandler } from '../lib/oauth/linkedin' +import { cognitoEventHandler } from '../lib/oauth/cognito' export const oauth = { githubEventHandler, @@ -20,4 +21,5 @@ export const oauth = { battledotnetEventHandler, keycloakEventHandler, linkedinEventHandler, + cognitoEventHandler }