From bfa2a880445c7100d93b9d1db95484f13cb930b7 Mon Sep 17 00:00:00 2001 From: Brian Coleman Date: Fri, 15 Nov 2024 08:23:53 -0800 Subject: [PATCH] feat: add workos oauth provider --- README.md | 1 + playground/.env.example | 5 + playground/app.vue | 6 + playground/auth.d.ts | 1 + playground/server/routes/auth/workos.get.ts | 15 +++ src/module.ts | 8 ++ src/runtime/server/lib/oauth/workos.ts | 126 ++++++++++++++++++++ src/runtime/server/lib/oauth/xsuaa.ts | 2 +- src/runtime/server/lib/utils.ts | 1 + src/runtime/types/oauth-config.ts | 2 +- 10 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 playground/server/routes/auth/workos.get.ts create mode 100644 src/runtime/server/lib/oauth/workos.ts diff --git a/README.md b/README.md index 273368cd..c07effe5 100644 --- a/README.md +++ b/README.md @@ -226,6 +226,7 @@ It can also be set using environment variables: - TikTok - Twitch - VK +- WorkOS - X (Twitter) - XSUAA - Yandex diff --git a/playground/.env.example b/playground/.env.example index 247fc00a..61545847 100644 --- a/playground/.env.example +++ b/playground/.env.example @@ -74,6 +74,11 @@ NUXT_OAUTH_DROPBOX_CLIENT_SECRET= # Polar NUXT_OAUTH_POLAR_CLIENT_ID= NUXT_OAUTH_POLAR_CLIENT_SECRET= +# WorkOS +NUXT_OAUTH_WORKOS_CLIENT_ID= +NUXT_OAUTH_WORKOS_CLIENT_SECRET= +NUXT_OAUTH_WORKOS_CONNECTION_ID= +NUXT_OAUTH_WORKOS_REDIRECT_URL= # Linear NUXT_OAUTH_LINEAR_CLIENT_ID= NUXT_OAUTH_LINEAR_CLIENT_SECRET= diff --git a/playground/app.vue b/playground/app.vue index 4fc26ad1..c6b66717 100644 --- a/playground/app.vue +++ b/playground/app.vue @@ -147,6 +147,12 @@ const providers = computed(() => disabled: Boolean(user.value?.polar), icon: 'i-iconoir-polar-sh', }, + { + label: user.value?.workos || 'WorkOS', + to: '/auth/workos', + disabled: Boolean(user.value?.workos), + icon: 'i-logos-workos-icon', + }, { label: user.value?.zitadel || 'Zitadel', to: '/auth/zitadel', diff --git a/playground/auth.d.ts b/playground/auth.d.ts index 556aa0b7..6e558221 100644 --- a/playground/auth.d.ts +++ b/playground/auth.d.ts @@ -26,6 +26,7 @@ declare module '#auth-utils' { yandex?: string tiktok?: string dropbox?: string + workos?: string polar?: string zitadel?: string authentik?: string diff --git a/playground/server/routes/auth/workos.get.ts b/playground/server/routes/auth/workos.get.ts new file mode 100644 index 00000000..3519a18a --- /dev/null +++ b/playground/server/routes/auth/workos.get.ts @@ -0,0 +1,15 @@ +export default defineOAuthWorkOSEventHandler({ + config: { + screenHint: 'sign-up', + }, + async onSuccess(event, { user }) { + await setUserSession(event, { + user: { + workos: user.email, + }, + loggedInAt: Date.now(), + }) + + return sendRedirect(event, '/') + }, +}) diff --git a/src/module.ts b/src/module.ts index f3595837..6a9f2129 100644 --- a/src/module.ts +++ b/src/module.ts @@ -196,6 +196,14 @@ export default defineNuxtModule({ audience: '', redirectURL: '', }) + // WorkOS OAuth + runtimeConfig.oauth.workos = defu(runtimeConfig.oauth.workos, { + clientId: '', + clientSecret: '', + connectionId: '', + screenHint: '', + redirectURL: '', + }) // Microsoft OAuth runtimeConfig.oauth.microsoft = defu(runtimeConfig.oauth.microsoft, { clientId: '', diff --git a/src/runtime/server/lib/oauth/workos.ts b/src/runtime/server/lib/oauth/workos.ts new file mode 100644 index 00000000..b9a9883e --- /dev/null +++ b/src/runtime/server/lib/oauth/workos.ts @@ -0,0 +1,126 @@ +import type { H3Event } from 'h3' +import { eventHandler, getQuery, sendRedirect, getRequestIP, getRequestHeader } from 'h3' +import { withQuery } from 'ufo' +import { defu } from 'defu' +import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from '../utils' +import { useRuntimeConfig } from '#imports' +import type { OAuthConfig } from '#auth-utils' + +/** + * WorkOS OAuth Configuration + * @see https://workos.com/docs/reference/user-management/authentication + */ +export interface OAuthWorkOSConfig { + /** + * WorkOS OAuth Client ID + * @default process.env.NUXT_OAUTH_WORKOS_CLIENT_ID + */ + clientId?: string + /** + * WorkOS OAuth Client Secret (API Key) + * @default process.env.NUXT_OAUTH_WORKOS_CLIENT_SECRET + */ + clientSecret?: string + /** + * WorkOS OAuth Connection ID (Not required for WorkOS) + * @default process.env.NUXT_OAUTH_WORKOS_CONNECTION_ID + */ + connectionId?: string + /** + * WorkOS OAuth screen hint + * @default 'sign-in' + */ + screenHint?: 'sign-in' | 'sign-up' + /** + * Redirect URL to to allow overriding for situations like prod failing to determine public hostname + * @default process.env.NUXT_OAUTH_WORKOS_REDIRECT_URL or current URL + */ + redirectURL?: string +} +export interface OAuthWorkOSUser { + object: 'user' + id: string + email: string + first_name: string | null + last_name: string | null + email_verified: boolean + profile_picture_url: string | null + created_at: string + updated_at: string +} + +export type OAuthWorkOSAuthenticationMethod = 'SSO' | 'Password' | 'AppleOAuth' | 'GitHubOAuth' | 'GoogleOAuth' | 'MicrosoftOAuth' | 'MagicAuth' | 'Impersonation' + +export interface OAuthWorkOSAuthenticateResponse { + user: OAuthWorkOSUser + organization_id: string | null + access_token: string + refresh_token: string + error: string | null + error_description: string | null + authentication_method: OAuthWorkOSAuthenticationMethod +} + +export interface OAuthWorkOSTokens { + access_token: string + refresh_token: string +} + +export function defineOAuthWorkOSEventHandler({ config, onSuccess, onError }: OAuthConfig) { + return eventHandler(async (event: H3Event) => { + config = defu(config, useRuntimeConfig(event).oauth?.workos, { screen_hint: 'sign-in' }) as OAuthWorkOSConfig + + if (!config.clientId || !config.clientSecret) { + return handleMissingConfiguration(event, 'workos', ['clientId', 'clientSecret'], onError) + } + + const query = getQuery<{ code?: string, state?: string, error?: string, error_description?: string, returnURL?: string }>(event) + const redirectURL = config.redirectURL || getOAuthRedirectURL(event) + + if (query.error) { + return handleAccessTokenErrorResponse(event, 'workos', query, onError) + } + + if (!query.code) { + // Redirect to WorkOS Oauth page + return sendRedirect( + event, + withQuery('https://api.workos.com/user_management/authorize', { + response_type: 'code', + provider: 'authkit', + client_id: config.clientId, + redirect_uri: redirectURL, + connection_id: config.connectionId, + screen_hint: config.screenHint, + }), + ) + } + + const ip_address = getRequestIP(event) + const user_agent = getRequestHeader(event, 'user-agent') + + const authenticateResponse: OAuthWorkOSAuthenticateResponse = await requestAccessToken('https://api.workos.com/user_management/authenticate', { + headers: { + 'Content-Type': 'application/json', + }, + body: { + grant_type: 'authorization_code', + client_id: config.clientId, + client_secret: config.clientSecret, + redirect_uri: redirectURL, + ip_address, + user_agent, + code: query.code, + }, + }) + + if (authenticateResponse.error) { + return handleAccessTokenErrorResponse(event, 'workos', authenticateResponse, onError) + } + + return onSuccess(event, { + tokens: { access_token: authenticateResponse.access_token, refresh_token: authenticateResponse.refresh_token }, + user: authenticateResponse.user, + }) + }) +} diff --git a/src/runtime/server/lib/oauth/xsuaa.ts b/src/runtime/server/lib/oauth/xsuaa.ts index b997e96f..4a290545 100644 --- a/src/runtime/server/lib/oauth/xsuaa.ts +++ b/src/runtime/server/lib/oauth/xsuaa.ts @@ -75,7 +75,7 @@ export function defineOAuthXSUAAEventHandler({ config, onSuccess, onError }: OAu }) if (tokens.error) { - return handleAccessTokenErrorResponse(event, 'auth0', tokens, onError) + return handleAccessTokenErrorResponse(event, 'xsuaa', tokens, onError) } const tokenType = tokens.token_type diff --git a/src/runtime/server/lib/utils.ts b/src/runtime/server/lib/utils.ts index e290c5c9..f3a24dab 100644 --- a/src/runtime/server/lib/utils.ts +++ b/src/runtime/server/lib/utils.ts @@ -22,6 +22,7 @@ export interface RequestAccessTokenBody { redirect_uri: string client_id: string client_secret?: string + [key: string]: string | undefined } interface RequestAccessTokenOptions { diff --git a/src/runtime/types/oauth-config.ts b/src/runtime/types/oauth-config.ts index 70860647..fade91b5 100644 --- a/src/runtime/types/oauth-config.ts +++ b/src/runtime/types/oauth-config.ts @@ -1,6 +1,6 @@ import type { H3Event, H3Error } from 'h3' -export type OAuthProvider = 'auth0' | 'authentik' | 'battledotnet' | 'cognito' | 'discord' | 'dropbox' | 'facebook' | 'github' | 'gitlab' | 'google' | 'instagram' | 'keycloak' | 'linear' | 'linkedin' | 'microsoft' | 'paypal' | 'polar' | 'spotify' | 'steam' | 'tiktok' | 'twitch' | 'vk' | 'x' | 'xsuaa' | 'yandex' | 'zitadel' | (string & {}) +export type OAuthProvider = 'auth0' | 'authentik' | 'battledotnet' | 'cognito' | 'discord' | 'dropbox' | 'facebook' | 'github' | 'gitlab' | 'google' | 'instagram' | 'keycloak' | 'linear' | 'linkedin' | 'microsoft' | 'paypal' | 'polar' | 'spotify' | 'steam' | 'tiktok' | 'twitch' | 'vk' | 'workos' | 'x' | 'xsuaa' | 'yandex' | 'zitadel' | (string & {}) export type OnError = (event: H3Event, error: H3Error) => Promise | void