Skip to content

Commit

Permalink
feat: add authentik provider
Browse files Browse the repository at this point in the history
* feat: add authentik provider

* Update src/runtime/server/lib/oauth/authentik.ts

---------

Co-authored-by: Sébastien Chopin <[email protected]>
  • Loading branch information
aoor9 and atinux authored Nov 11, 2024
1 parent 6072a74 commit 33686af
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ It can also be set using environment variables:
#### Supported OAuth Providers

- Auth0
- Authentik
- AWS Cognito
- Battle.net
- Discord
Expand Down
4 changes: 4 additions & 0 deletions playground/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,7 @@ NUXT_OAUTH_LINEAR_CLIENT_SECRET=
NUXT_OAUTH_ZITADEL_CLIENT_ID=
NUXT_OAUTH_ZITADEL_CLIENT_SECRET=
NUXT_OAUTH_ZITADEL_DOMAIN=
# Authentik
NUXT_OAUTH_AUTHENTIK_CLIENT_ID=
NUXT_OAUTH_AUTHENTIK_CLIENT_SECRET=
NUXT_OAUTH_AUTHENTIK_DOMAIN=
6 changes: 6 additions & 0 deletions playground/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,12 @@ const providers = computed(() =>
disabled: Boolean(user.value?.zitadel),
icon: 'i-gravity-ui-lock',
},
{
label: user.value?.authentik || 'Authentik',
to: '/auth/authentik',
disabled: Boolean(user.value?.authentik),
icon: 'i-simple-icons-authentik',
},
].map(p => ({
...p,
prefetch: false,
Expand Down
1 change: 1 addition & 0 deletions playground/auth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ declare module '#auth-utils' {
dropbox?: string
polar?: string
zitadel?: string
authentik?: string
}

interface UserSession {
Expand Down
12 changes: 12 additions & 0 deletions playground/server/routes/auth/authentik.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default defineOAuthAuthentikEventHandler({
async onSuccess(event, { user }) {
await setUserSession(event, {
user: {
authentik: user.preferred_username,
},
loggedInAt: Date.now(),
})

return sendRedirect(event, '/')
},
})
7 changes: 7 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,5 +315,12 @@ export default defineNuxtModule<ModuleOptions>({
domain: '',
redirectURL: '',
})
// Authentik OAuth
runtimeConfig.oauth.authentik = defu(runtimeConfig.oauth.authentik, {
clientId: '',
clientSecret: '',
domain: '',
redirectURL: '',
})
},
})
113 changes: 113 additions & 0 deletions src/runtime/server/lib/oauth/authentik.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import type { H3Event } from 'h3'
import { eventHandler, getQuery, sendRedirect } from 'h3'
import { withQuery } from 'ufo'
import { defu } from 'defu'
import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from '../utils'
import { useRuntimeConfig, createError } from '#imports'
import type { OAuthConfig } from '#auth-utils'

export interface OAuthAuthentikConfig {
/**
* Authentik OAuth Client ID
* @default process.env.NUXT_OAUTH_AUTHENTIK_CLIENT_ID
*/
clientId?: string
/**
* Authentik OAuth Client Secret
* @default process.env.NUXT_OAUTH_AUTHENTIK_CLIENT_SECRET
*/
clientSecret?: string
/**
* Authentik OAuth Domain
* @example https://<your-authentik-instance>
* @default process.env.NUXT_OAUTH_AUTHENTIK_DOMAIN
*/
domain?: string
/**
* Redirect URL to allow overriding for situations like prod failing to determine public hostname
* @default process.env.NUXT_OAUTH_AUTHENTIK_REDIRECT_URL or current URL
*/
redirectURL?: string
}

export function defineOAuthAuthentikEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthAuthentikConfig>) {
return eventHandler(async (event: H3Event) => {
config = defu(config, useRuntimeConfig(event).oauth?.authentik) as OAuthAuthentikConfig

const query = getQuery<{ code?: string, error?: string }>(event)

if (query.error) {
const error = createError({
statusCode: 401,
message: `Authentik login failed: ${query.error || 'Unknown error'}`,
data: query,
})
if (!onError) throw error
return onError(event, error)
}

if (!config.clientId || !config.clientSecret || !config.domain) {
return handleMissingConfiguration(event, 'authentik', ['clientId', 'clientSecret', 'domain'], onError)
}

const authorizationURL = `https://${config.domain}/application/o/authorize/`
const tokenURL = `https://${config.domain}/application/o/token/`
const redirectURL = config.redirectURL || getOAuthRedirectURL(event)

if (!query.code) {
// Redirect to Authentik OAuth page

return sendRedirect(
event,
withQuery(authorizationURL, {
response_type: 'code',
client_id: config.clientId,
redirect_uri: redirectURL,
scope: ['openid', 'profile', 'email'].join(' '),
}),
)
}

const tokens = await requestAccessToken(tokenURL, {
headers: {
'Authorization': `Basic ${Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64')}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: {
grant_type: 'authorization_code',
client_id: config.clientId,
redirect_uri: redirectURL,
code: query.code,
},
})

if (tokens.error) {
return handleAccessTokenErrorResponse(event, 'authentik', tokens, onError)
}

const accessToken = tokens.access_token
// Fetch user info
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const user: any = await $fetch(`https://${config.domain}/application/o/userinfo/`, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
})

if (!user) {
const error = createError({
statusCode: 500,
message: 'Could not get Authentik user',
data: tokens,
})
if (!onError) throw error
return onError(event, error)
}

return onSuccess(event, {
user,
tokens,
})
})
}
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' | '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' | 'x' | 'xsuaa' | 'yandex' | 'zitadel' | (string & {})

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

Expand Down

0 comments on commit 33686af

Please sign in to comment.