Skip to content

Commit 33686af

Browse files
aoor9atinux
andauthored
feat: add authentik provider
* feat: add authentik provider * Update src/runtime/server/lib/oauth/authentik.ts --------- Co-authored-by: Sébastien Chopin <seb@nuxtlabs.com>
1 parent 6072a74 commit 33686af

File tree

8 files changed

+145
-1
lines changed

8 files changed

+145
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ It can also be set using environment variables:
205205
#### Supported OAuth Providers
206206

207207
- Auth0
208+
- Authentik
208209
- AWS Cognito
209210
- Battle.net
210211
- Discord

playground/.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,7 @@ NUXT_OAUTH_LINEAR_CLIENT_SECRET=
8181
NUXT_OAUTH_ZITADEL_CLIENT_ID=
8282
NUXT_OAUTH_ZITADEL_CLIENT_SECRET=
8383
NUXT_OAUTH_ZITADEL_DOMAIN=
84+
# Authentik
85+
NUXT_OAUTH_AUTHENTIK_CLIENT_ID=
86+
NUXT_OAUTH_AUTHENTIK_CLIENT_SECRET=
87+
NUXT_OAUTH_AUTHENTIK_DOMAIN=

playground/app.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,12 @@ const providers = computed(() =>
153153
disabled: Boolean(user.value?.zitadel),
154154
icon: 'i-gravity-ui-lock',
155155
},
156+
{
157+
label: user.value?.authentik || 'Authentik',
158+
to: '/auth/authentik',
159+
disabled: Boolean(user.value?.authentik),
160+
icon: 'i-simple-icons-authentik',
161+
},
156162
].map(p => ({
157163
...p,
158164
prefetch: false,

playground/auth.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ declare module '#auth-utils' {
2828
dropbox?: string
2929
polar?: string
3030
zitadel?: string
31+
authentik?: string
3132
}
3233

3334
interface UserSession {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export default defineOAuthAuthentikEventHandler({
2+
async onSuccess(event, { user }) {
3+
await setUserSession(event, {
4+
user: {
5+
authentik: user.preferred_username,
6+
},
7+
loggedInAt: Date.now(),
8+
})
9+
10+
return sendRedirect(event, '/')
11+
},
12+
})

src/module.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,5 +315,12 @@ export default defineNuxtModule<ModuleOptions>({
315315
domain: '',
316316
redirectURL: '',
317317
})
318+
// Authentik OAuth
319+
runtimeConfig.oauth.authentik = defu(runtimeConfig.oauth.authentik, {
320+
clientId: '',
321+
clientSecret: '',
322+
domain: '',
323+
redirectURL: '',
324+
})
318325
},
319326
})
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import type { H3Event } from 'h3'
2+
import { eventHandler, getQuery, sendRedirect } from 'h3'
3+
import { withQuery } from 'ufo'
4+
import { defu } from 'defu'
5+
import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from '../utils'
6+
import { useRuntimeConfig, createError } from '#imports'
7+
import type { OAuthConfig } from '#auth-utils'
8+
9+
export interface OAuthAuthentikConfig {
10+
/**
11+
* Authentik OAuth Client ID
12+
* @default process.env.NUXT_OAUTH_AUTHENTIK_CLIENT_ID
13+
*/
14+
clientId?: string
15+
/**
16+
* Authentik OAuth Client Secret
17+
* @default process.env.NUXT_OAUTH_AUTHENTIK_CLIENT_SECRET
18+
*/
19+
clientSecret?: string
20+
/**
21+
* Authentik OAuth Domain
22+
* @example https://<your-authentik-instance>
23+
* @default process.env.NUXT_OAUTH_AUTHENTIK_DOMAIN
24+
*/
25+
domain?: string
26+
/**
27+
* Redirect URL to allow overriding for situations like prod failing to determine public hostname
28+
* @default process.env.NUXT_OAUTH_AUTHENTIK_REDIRECT_URL or current URL
29+
*/
30+
redirectURL?: string
31+
}
32+
33+
export function defineOAuthAuthentikEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthAuthentikConfig>) {
34+
return eventHandler(async (event: H3Event) => {
35+
config = defu(config, useRuntimeConfig(event).oauth?.authentik) as OAuthAuthentikConfig
36+
37+
const query = getQuery<{ code?: string, error?: string }>(event)
38+
39+
if (query.error) {
40+
const error = createError({
41+
statusCode: 401,
42+
message: `Authentik login failed: ${query.error || 'Unknown error'}`,
43+
data: query,
44+
})
45+
if (!onError) throw error
46+
return onError(event, error)
47+
}
48+
49+
if (!config.clientId || !config.clientSecret || !config.domain) {
50+
return handleMissingConfiguration(event, 'authentik', ['clientId', 'clientSecret', 'domain'], onError)
51+
}
52+
53+
const authorizationURL = `https://${config.domain}/application/o/authorize/`
54+
const tokenURL = `https://${config.domain}/application/o/token/`
55+
const redirectURL = config.redirectURL || getOAuthRedirectURL(event)
56+
57+
if (!query.code) {
58+
// Redirect to Authentik OAuth page
59+
60+
return sendRedirect(
61+
event,
62+
withQuery(authorizationURL, {
63+
response_type: 'code',
64+
client_id: config.clientId,
65+
redirect_uri: redirectURL,
66+
scope: ['openid', 'profile', 'email'].join(' '),
67+
}),
68+
)
69+
}
70+
71+
const tokens = await requestAccessToken(tokenURL, {
72+
headers: {
73+
'Authorization': `Basic ${Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64')}`,
74+
'Content-Type': 'application/x-www-form-urlencoded',
75+
},
76+
body: {
77+
grant_type: 'authorization_code',
78+
client_id: config.clientId,
79+
redirect_uri: redirectURL,
80+
code: query.code,
81+
},
82+
})
83+
84+
if (tokens.error) {
85+
return handleAccessTokenErrorResponse(event, 'authentik', tokens, onError)
86+
}
87+
88+
const accessToken = tokens.access_token
89+
// Fetch user info
90+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
91+
const user: any = await $fetch(`https://${config.domain}/application/o/userinfo/`, {
92+
headers: {
93+
Authorization: `Bearer ${accessToken}`,
94+
Accept: 'application/json',
95+
},
96+
})
97+
98+
if (!user) {
99+
const error = createError({
100+
statusCode: 500,
101+
message: 'Could not get Authentik user',
102+
data: tokens,
103+
})
104+
if (!onError) throw error
105+
return onError(event, error)
106+
}
107+
108+
return onSuccess(event, {
109+
user,
110+
tokens,
111+
})
112+
})
113+
}

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' | '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' | 'x' | 'xsuaa' | 'yandex' | 'zitadel' | (string & {})
44

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

0 commit comments

Comments
 (0)