- Integrated Authentik OAuth/OIDC authentication - Added PWA functionality with offline support - Created protected and public API endpoints - Configured Docker deployment with Traefik - Added Gitea Actions CI/CD workflow - Included comprehensive setup documentation
124 lines
3.5 KiB
TypeScript
124 lines
3.5 KiB
TypeScript
/**
|
|
* Custom OAuth Provider for Authentik
|
|
*
|
|
* Este archivo extiende nuxt-auth-utils para soportar Authentik como provider OAuth
|
|
*/
|
|
import type { H3Event } from 'h3'
|
|
import { eventHandler, getQuery, sendRedirect } from 'h3'
|
|
import { withQuery } from 'ufo'
|
|
import { defu } from 'defu'
|
|
import { useRuntimeConfig } from '#imports'
|
|
|
|
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 Server URL
|
|
* @default process.env.NUXT_OAUTH_AUTHENTIK_SERVER_URL
|
|
*/
|
|
serverUrl?: string
|
|
/**
|
|
* Redirect URL
|
|
* @default process.env.NUXT_OAUTH_AUTHENTIK_REDIRECT_URL
|
|
*/
|
|
redirectURL?: string
|
|
/**
|
|
* Require email from user, adds the ['email'] scope if not present
|
|
* @default false
|
|
*/
|
|
emailRequired?: boolean
|
|
/**
|
|
* Authentik OAuth Scope
|
|
* @default ['openid', 'profile', 'email']
|
|
*/
|
|
scope?: string[]
|
|
}
|
|
|
|
export function oauthAuthentikEventHandler({
|
|
config,
|
|
onSuccess,
|
|
onError,
|
|
}: {
|
|
config?: OAuthAuthentikConfig
|
|
onSuccess: (event: H3Event, result: { user: any; tokens: any }) => Promise<void> | void
|
|
onError?: (event: H3Event, error: any) => Promise<void> | void
|
|
}) {
|
|
return eventHandler(async (event: H3Event) => {
|
|
const runtimeConfig = useRuntimeConfig(event)
|
|
|
|
// Merge config with defaults
|
|
const authentikConfig = defu(config, {
|
|
clientId: runtimeConfig.oauth.authentik.clientId,
|
|
clientSecret: runtimeConfig.oauth.authentik.clientSecret,
|
|
serverUrl: runtimeConfig.oauth.authentik.serverUrl,
|
|
redirectURL: runtimeConfig.oauth.authentik.redirectURL,
|
|
scope: ['openid', 'profile', 'email'],
|
|
emailRequired: false
|
|
}) as Required<OAuthAuthentikConfig>
|
|
|
|
const query = getQuery(event)
|
|
|
|
// Handle callback
|
|
if (query.code) {
|
|
try {
|
|
// Exchange code for tokens
|
|
const tokenUrl = `${authentikConfig.serverUrl}/application/o/token/`
|
|
const tokenResponse = await $fetch(tokenUrl, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: new URLSearchParams({
|
|
grant_type: 'authorization_code',
|
|
client_id: authentikConfig.clientId,
|
|
client_secret: authentikConfig.clientSecret,
|
|
code: query.code as string,
|
|
redirect_uri: authentikConfig.redirectURL,
|
|
}),
|
|
})
|
|
|
|
const tokens = tokenResponse as any
|
|
|
|
// Get user info
|
|
const userInfoUrl = `${authentikConfig.serverUrl}/application/o/userinfo/`
|
|
const user = await $fetch(userInfoUrl, {
|
|
headers: {
|
|
Authorization: `Bearer ${tokens.access_token}`,
|
|
},
|
|
})
|
|
|
|
return onSuccess(event, { user, tokens })
|
|
} catch (error: any) {
|
|
if (onError) return onError(event, error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// Initial redirect to Authentik
|
|
const authorizationUrl = withQuery(
|
|
`${authentikConfig.serverUrl}/application/o/authorize/`,
|
|
{
|
|
client_id: authentikConfig.clientId,
|
|
redirect_uri: authentikConfig.redirectURL,
|
|
response_type: 'code',
|
|
scope: authentikConfig.scope.join(' '),
|
|
}
|
|
)
|
|
|
|
return sendRedirect(event, authorizationUrl)
|
|
})
|
|
}
|
|
|
|
// Export for use in defineOAuthAuthentikEventHandler
|
|
export const oauth = {
|
|
authentikEventHandler: oauthAuthentikEventHandler
|
|
}
|