diff --git a/README.md b/README.md index 69a22e74..b7b8acd8 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Add Authentication to Nuxt applications with secured & sealed cookies sessions. ## Features - [Hybrid Rendering](#hybrid-rendering) support (SSR / CSR / SWR / Prerendering) -- [15 OAuth Providers](#supported-oauth-providers) +- [15+ OAuth Providers](#supported-oauth-providers) - [Vue composable](#vue-composable) - [Server utils](#server-utils) - [`` component](#authstate-component) @@ -165,6 +165,7 @@ It can also be set using environment variables: - Steam - Twitch - X (Twitter) +- XSUAA You can add your favorite provider by creating a new file in [src/runtime/server/lib/oauth/](./src/runtime/server/lib/oauth/). diff --git a/playground/.env.example b/playground/.env.example index 876c8720..072025b6 100644 --- a/playground/.env.example +++ b/playground/.env.example @@ -49,3 +49,7 @@ NUXT_OAUTH_STEAM_API_KEY= # X NUXT_OAUTH_X_CLIENT_ID= NUXT_OAUTH_X_CLIENT_SECRET= +# XSUAA +NUXT_OAUTH_XSUAA_CLIENT_ID= +NUXT_OAUTH_XSUAA_CLIENT_SECRET= +NUXT_OAUTH_XSUAA_DOMAIN= diff --git a/playground/app.vue b/playground/app.vue index a1a860e9..0a08dde7 100644 --- a/playground/app.vue +++ b/playground/app.vue @@ -119,6 +119,12 @@ const providers = computed(() => [ disabled: Boolean(user.value?.x), icon: 'i-simple-icons-x', }, + { + label: user.value?.xsuaa || 'XSUAA', + to: '/auth/xsuaa', + disabled: Boolean(user.value?.xsuaa), + icon: 'i-simple-icons-sap', + }, ].map(p => ({ ...p, prefetch: false, diff --git a/playground/auth.d.ts b/playground/auth.d.ts index 35f42489..97682234 100644 --- a/playground/auth.d.ts +++ b/playground/auth.d.ts @@ -16,6 +16,7 @@ declare module '#auth-utils' { paypal?: string steam?: string x?: string + xsuaa?: string } interface UserSession { diff --git a/playground/server/routes/auth/xsuaa.get.ts b/playground/server/routes/auth/xsuaa.get.ts new file mode 100644 index 00000000..0a5cb7c7 --- /dev/null +++ b/playground/server/routes/auth/xsuaa.get.ts @@ -0,0 +1,12 @@ +export default oauth.xsuaaEventHandler({ + async onSuccess(event, { user }) { + await setUserSession(event, { + user: { + xsuaa: user.email, + }, + loggedInAt: Date.now(), + }) + + return sendRedirect(event, '/') + }, +}) diff --git a/src/module.ts b/src/module.ts index 5226e1ef..ccc9f828 100644 --- a/src/module.ts +++ b/src/module.ts @@ -146,5 +146,11 @@ export default defineNuxtModule({ clientId: '', clientSecret: '', }) + // XSUAA OAuth + runtimeConfig.oauth.xsuaa = defu(runtimeConfig.oauth.xsuaa, { + clientId: '', + clientSecret: '', + domain: '', + }) }, }) diff --git a/src/runtime/server/lib/oauth/xsuaa.ts b/src/runtime/server/lib/oauth/xsuaa.ts new file mode 100644 index 00000000..c2cd6e20 --- /dev/null +++ b/src/runtime/server/lib/oauth/xsuaa.ts @@ -0,0 +1,110 @@ +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 OAuthXSUAAConfig { + /** + * XSUAA OAuth Client ID + * @default process.env.NUXT_OAUTH_XSUAA_CLIENT_ID + */ + clientId?: string + /** + * XSUAA OAuth Client Secret + * @default process.env.NUXT_OAUTH_XSUAA_CLIENT_SECRET + */ + clientSecret?: string + /** + * XSUAA OAuth Issuer + * @default process.env.NUXT_OAUTH_XSUAA_DOMAIN + */ + domain?: string + /** + * XSUAA OAuth Scope + * @default [] + * @see https://sap.github.io/cloud-sdk/docs/java/guides/cloud-foundry-xsuaa-service + * @example ['openid'] + */ + scope?: string[] +} + +export function xsuaaEventHandler({ config, onSuccess, onError }: OAuthConfig) { + return eventHandler(async (event: H3Event) => { + config = defu(config, useRuntimeConfig(event).oauth?.xsuaa) as OAuthXSUAAConfig + const { code } = getQuery(event) + + if (!config.clientId || !config.clientSecret || !config.domain) { + const error = createError({ + statusCode: 500, + message: 'Missing NUXT_OAUTH_XSUAA_CLIENT_ID or NUXT_OAUTH_XSUAA_CLIENT_SECRET or NUXT_OAUTH_XSUAA_DOMAIN env variables.', + }) + if (!onError) throw error + return onError(event, error) + } + const authorizationURL = `https://${config.domain}/oauth/authorize` + const tokenURL = `https://${config.domain}/oauth/token` + + const redirectUrl = getRequestURL(event).href + if (!code) { + config.scope = config.scope || [] + // Redirect to XSUAA Oauth page + return sendRedirect( + event, + withQuery(authorizationURL as string, { + response_type: 'code', + 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( + tokenURL as string, + { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + 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: `XSUAA login failed: ${tokens.error?.data?.error_description || 'Unknown error'}`, + data: tokens, + }) + if (!onError) throw error + return onError(event, error) + } + + const tokenType = tokens.token_type + const accessToken = tokens.access_token + + // TODO: improve typing + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const user: any = await $fetch(`https://${config.domain}/userinfo`, { + headers: { + Authorization: `${tokenType} ${accessToken}`, + }, + }) + + return onSuccess(event, { + tokens, + user, + }) + }) +} diff --git a/src/runtime/server/utils/oauth.ts b/src/runtime/server/utils/oauth.ts index 6161a23e..e94ec29f 100644 --- a/src/runtime/server/utils/oauth.ts +++ b/src/runtime/server/utils/oauth.ts @@ -13,6 +13,7 @@ import { facebookEventHandler } from '../lib/oauth/facebook' import { paypalEventHandler } from '../lib/oauth/paypal' import { steamEventHandler } from '../lib/oauth/steam' import { xEventHandler } from '../lib/oauth/x' +import { xsuaaEventHandler } from '../lib/oauth/xsuaa' export const oauth = { githubEventHandler, @@ -30,4 +31,5 @@ export const oauth = { paypalEventHandler, steamEventHandler, xEventHandler, + xsuaaEventHandler, }