Skip to content

Commit bfa2a88

Browse files
authored
feat: add workos oauth provider
1 parent 21bff83 commit bfa2a88

File tree

10 files changed

+165
-2
lines changed

10 files changed

+165
-2
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ It can also be set using environment variables:
226226
- TikTok
227227
- Twitch
228228
- VK
229+
- WorkOS
229230
- X (Twitter)
230231
- XSUAA
231232
- Yandex

playground/.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ NUXT_OAUTH_DROPBOX_CLIENT_SECRET=
7474
# Polar
7575
NUXT_OAUTH_POLAR_CLIENT_ID=
7676
NUXT_OAUTH_POLAR_CLIENT_SECRET=
77+
# WorkOS
78+
NUXT_OAUTH_WORKOS_CLIENT_ID=
79+
NUXT_OAUTH_WORKOS_CLIENT_SECRET=
80+
NUXT_OAUTH_WORKOS_CONNECTION_ID=
81+
NUXT_OAUTH_WORKOS_REDIRECT_URL=
7782
# Linear
7883
NUXT_OAUTH_LINEAR_CLIENT_ID=
7984
NUXT_OAUTH_LINEAR_CLIENT_SECRET=

playground/app.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,12 @@ const providers = computed(() =>
147147
disabled: Boolean(user.value?.polar),
148148
icon: 'i-iconoir-polar-sh',
149149
},
150+
{
151+
label: user.value?.workos || 'WorkOS',
152+
to: '/auth/workos',
153+
disabled: Boolean(user.value?.workos),
154+
icon: 'i-logos-workos-icon',
155+
},
150156
{
151157
label: user.value?.zitadel || 'Zitadel',
152158
to: '/auth/zitadel',

playground/auth.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ declare module '#auth-utils' {
2626
yandex?: string
2727
tiktok?: string
2828
dropbox?: string
29+
workos?: string
2930
polar?: string
3031
zitadel?: string
3132
authentik?: string
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export default defineOAuthWorkOSEventHandler({
2+
config: {
3+
screenHint: 'sign-up',
4+
},
5+
async onSuccess(event, { user }) {
6+
await setUserSession(event, {
7+
user: {
8+
workos: user.email,
9+
},
10+
loggedInAt: Date.now(),
11+
})
12+
13+
return sendRedirect(event, '/')
14+
},
15+
})

src/module.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,14 @@ export default defineNuxtModule<ModuleOptions>({
196196
audience: '',
197197
redirectURL: '',
198198
})
199+
// WorkOS OAuth
200+
runtimeConfig.oauth.workos = defu(runtimeConfig.oauth.workos, {
201+
clientId: '',
202+
clientSecret: '',
203+
connectionId: '',
204+
screenHint: '',
205+
redirectURL: '',
206+
})
199207
// Microsoft OAuth
200208
runtimeConfig.oauth.microsoft = defu(runtimeConfig.oauth.microsoft, {
201209
clientId: '',
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import type { H3Event } from 'h3'
2+
import { eventHandler, getQuery, sendRedirect, getRequestIP, getRequestHeader } from 'h3'
3+
import { withQuery } from 'ufo'
4+
import { defu } from 'defu'
5+
import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from '../utils'
6+
import { useRuntimeConfig } from '#imports'
7+
import type { OAuthConfig } from '#auth-utils'
8+
9+
/**
10+
* WorkOS OAuth Configuration
11+
* @see https://workos.com/docs/reference/user-management/authentication
12+
*/
13+
export interface OAuthWorkOSConfig {
14+
/**
15+
* WorkOS OAuth Client ID
16+
* @default process.env.NUXT_OAUTH_WORKOS_CLIENT_ID
17+
*/
18+
clientId?: string
19+
/**
20+
* WorkOS OAuth Client Secret (API Key)
21+
* @default process.env.NUXT_OAUTH_WORKOS_CLIENT_SECRET
22+
*/
23+
clientSecret?: string
24+
/**
25+
* WorkOS OAuth Connection ID (Not required for WorkOS)
26+
* @default process.env.NUXT_OAUTH_WORKOS_CONNECTION_ID
27+
*/
28+
connectionId?: string
29+
/**
30+
* WorkOS OAuth screen hint
31+
* @default 'sign-in'
32+
*/
33+
screenHint?: 'sign-in' | 'sign-up'
34+
/**
35+
* Redirect URL to to allow overriding for situations like prod failing to determine public hostname
36+
* @default process.env.NUXT_OAUTH_WORKOS_REDIRECT_URL or current URL
37+
*/
38+
redirectURL?: string
39+
}
40+
export interface OAuthWorkOSUser {
41+
object: 'user'
42+
id: string
43+
email: string
44+
first_name: string | null
45+
last_name: string | null
46+
email_verified: boolean
47+
profile_picture_url: string | null
48+
created_at: string
49+
updated_at: string
50+
}
51+
52+
export type OAuthWorkOSAuthenticationMethod = 'SSO' | 'Password' | 'AppleOAuth' | 'GitHubOAuth' | 'GoogleOAuth' | 'MicrosoftOAuth' | 'MagicAuth' | 'Impersonation'
53+
54+
export interface OAuthWorkOSAuthenticateResponse {
55+
user: OAuthWorkOSUser
56+
organization_id: string | null
57+
access_token: string
58+
refresh_token: string
59+
error: string | null
60+
error_description: string | null
61+
authentication_method: OAuthWorkOSAuthenticationMethod
62+
}
63+
64+
export interface OAuthWorkOSTokens {
65+
access_token: string
66+
refresh_token: string
67+
}
68+
69+
export function defineOAuthWorkOSEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthWorkOSConfig, OAuthWorkOSUser, OAuthWorkOSTokens>) {
70+
return eventHandler(async (event: H3Event) => {
71+
config = defu(config, useRuntimeConfig(event).oauth?.workos, { screen_hint: 'sign-in' }) as OAuthWorkOSConfig
72+
73+
if (!config.clientId || !config.clientSecret) {
74+
return handleMissingConfiguration(event, 'workos', ['clientId', 'clientSecret'], onError)
75+
}
76+
77+
const query = getQuery<{ code?: string, state?: string, error?: string, error_description?: string, returnURL?: string }>(event)
78+
const redirectURL = config.redirectURL || getOAuthRedirectURL(event)
79+
80+
if (query.error) {
81+
return handleAccessTokenErrorResponse(event, 'workos', query, onError)
82+
}
83+
84+
if (!query.code) {
85+
// Redirect to WorkOS Oauth page
86+
return sendRedirect(
87+
event,
88+
withQuery('https://api.workos.com/user_management/authorize', {
89+
response_type: 'code',
90+
provider: 'authkit',
91+
client_id: config.clientId,
92+
redirect_uri: redirectURL,
93+
connection_id: config.connectionId,
94+
screen_hint: config.screenHint,
95+
}),
96+
)
97+
}
98+
99+
const ip_address = getRequestIP(event)
100+
const user_agent = getRequestHeader(event, 'user-agent')
101+
102+
const authenticateResponse: OAuthWorkOSAuthenticateResponse = await requestAccessToken('https://api.workos.com/user_management/authenticate', {
103+
headers: {
104+
'Content-Type': 'application/json',
105+
},
106+
body: {
107+
grant_type: 'authorization_code',
108+
client_id: config.clientId,
109+
client_secret: config.clientSecret,
110+
redirect_uri: redirectURL,
111+
ip_address,
112+
user_agent,
113+
code: query.code,
114+
},
115+
})
116+
117+
if (authenticateResponse.error) {
118+
return handleAccessTokenErrorResponse(event, 'workos', authenticateResponse, onError)
119+
}
120+
121+
return onSuccess(event, {
122+
tokens: { access_token: authenticateResponse.access_token, refresh_token: authenticateResponse.refresh_token },
123+
user: authenticateResponse.user,
124+
})
125+
})
126+
}

src/runtime/server/lib/oauth/xsuaa.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export function defineOAuthXSUAAEventHandler({ config, onSuccess, onError }: OAu
7575
})
7676

7777
if (tokens.error) {
78-
return handleAccessTokenErrorResponse(event, 'auth0', tokens, onError)
78+
return handleAccessTokenErrorResponse(event, 'xsuaa', tokens, onError)
7979
}
8080

8181
const tokenType = tokens.token_type

src/runtime/server/lib/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface RequestAccessTokenBody {
2222
redirect_uri: string
2323
client_id: string
2424
client_secret?: string
25+
[key: string]: string | undefined
2526
}
2627

2728
interface RequestAccessTokenOptions {

src/runtime/types/oauth-config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { H3Event, H3Error } from 'h3'
22

3-
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 & {})
3+
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 & {})
44

55
export type OnError = (event: H3Event, error: H3Error) => Promise<void> | void
66

0 commit comments

Comments
 (0)