Skip to content

Commit

Permalink
feat: add workos oauth provider
Browse files Browse the repository at this point in the history
  • Loading branch information
brianacdev authored Nov 15, 2024
1 parent 21bff83 commit bfa2a88
Show file tree
Hide file tree
Showing 10 changed files with 165 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ It can also be set using environment variables:
- TikTok
- Twitch
- VK
- WorkOS
- X (Twitter)
- XSUAA
- Yandex
Expand Down
5 changes: 5 additions & 0 deletions playground/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
6 changes: 6 additions & 0 deletions playground/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions playground/auth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ declare module '#auth-utils' {
yandex?: string
tiktok?: string
dropbox?: string
workos?: string
polar?: string
zitadel?: string
authentik?: string
Expand Down
15 changes: 15 additions & 0 deletions playground/server/routes/auth/workos.get.ts
Original file line number Diff line number Diff line change
@@ -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, '/')
},
})
8 changes: 8 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,14 @@ export default defineNuxtModule<ModuleOptions>({
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: '',
Expand Down
126 changes: 126 additions & 0 deletions src/runtime/server/lib/oauth/workos.ts
Original file line number Diff line number Diff line change
@@ -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<OAuthWorkOSConfig, OAuthWorkOSUser, OAuthWorkOSTokens>) {
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,
})
})
}
2 changes: 1 addition & 1 deletion src/runtime/server/lib/oauth/xsuaa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/runtime/server/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface RequestAccessTokenBody {
redirect_uri: string
client_id: string
client_secret?: string
[key: string]: string | undefined
}

interface RequestAccessTokenOptions {
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/types/oauth-config.ts
Original file line number Diff line number Diff line change
@@ -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> | void

Expand Down

0 comments on commit bfa2a88

Please sign in to comment.