-
Notifications
You must be signed in to change notification settings - Fork 1
feat(oidc): add Authentik OpenID Connect Provider #199
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,202 @@ | ||
| --- | ||
| title: Authentik | ||
| description: Add Authentik authorization provider to Aura Auth for authentication and authorization | ||
| --- | ||
|
|
||
| <Steps> | ||
|
|
||
| <Step> | ||
|
|
||
| ## Authentik OAuth App | ||
|
|
||
| ### Register the Application | ||
|
|
||
| The first step is to create and register an OAuth App on the Authentik Applications to obtain access to the user's resources. | ||
|
|
||
| 1. Navigate to your Authentik profile and go to **Applications > Applications > New Provider**. | ||
| 2. Click **New Application**. | ||
| 3. Select **OAuth2/OIDC** as Provider Type | ||
| 3. Fill in the "Application name" and "Homepage URL". | ||
| 4. Set the **Authorization callback URL** to `http://localhost:3000/auth/callback/authentik`. | ||
| - _(Make sure to replace `localhost:3000` with your production domain when deploying)_ | ||
| 5. Click **Register application**. | ||
| 6. Ensure you copy the **Client ID** and click **Generate a new client secret**. | ||
|
|
||
| </Step> | ||
|
|
||
| <Step> | ||
|
|
||
| ## Installation | ||
|
|
||
| Install the package using a package manager like `npm`, `pnpm`, or `yarn`: | ||
|
|
||
| ```npm | ||
| npm install @aura-stack/auth | ||
| ``` | ||
|
|
||
| </Step> | ||
|
|
||
| <Step> | ||
|
|
||
| ## Environment setup | ||
|
|
||
| Now, you must configure the environment variables required by Aura Auth, including the Authentik credentials and the encryption secrets. | ||
|
|
||
| ```bash title=".env" lineNumbers | ||
| # Aura Secrets | ||
| AURA_AUTH_SECRET="your-32-byte-secret" | ||
| AURA_AUTH_SALT="your-32-byte-salt" | ||
|
|
||
| # Authentik Credentials | ||
| AURA_AUTH_AUTHENTIK_CLIENT_ID="your_authentik_client_id" | ||
| AURA_AUTH_AUTHENTIK_CLIENT_SECRET="your_authentik_client_secret" | ||
| ``` | ||
|
|
||
| <Callout type="warn"> | ||
| **CRITICAL SECURITY WARNING:** The `AURA_AUTH_SECRET` and `AURA_AUTH_SALT` variables are used to encrypt and sign user sessions. | ||
| These MUST be securely generated, highly randomized strings consisting of at least 32 bytes to ensure adequate entropy. Never | ||
| hardcode these values in your repository. Use a secure generator (like `openssl rand -base64 32`) to create them, and store them | ||
| exclusively in your secure environment variables manager. | ||
| </Callout> | ||
|
|
||
| </Step> | ||
|
|
||
| <Step> | ||
|
|
||
| ## Configure the Auth Instance | ||
|
|
||
| Configure the `createAuth` instance inside an `auth.ts` file located at the root of your project. Ensure you explicitly export the `handlers`, `api`, and `jose` objects. | ||
|
|
||
| ```ts title="auth.ts" lineNumbers | ||
| import { createAuth } from "@aura-stack/auth" | ||
|
|
||
| export const auth = createAuth({ | ||
| oauth: ["authentik"], | ||
| }) | ||
|
|
||
| // Extract the required utilities | ||
| export const { handlers, api, jose } = auth | ||
| ``` | ||
|
|
||
| <Callout type="info"> | ||
| The `handlers` object contains mapping utilities for standard HTTP methods (`GET`, `POST`, `PATCH`) as well as a unified `ALL` | ||
| handler. This allows you to easily mount the authentication routes across any framework (Next.js, Elysia, Express, etc.). | ||
| </Callout> | ||
|
|
||
| </Step> | ||
|
|
||
| <Step> | ||
|
|
||
| ## Customizing the OAuth Provider | ||
|
|
||
| If you need to define custom scopes, change the response type, or map profile data differently, you can use the provider's factory function instead of a simple string identifier. | ||
|
|
||
| ```ts title="auth.ts" lineNumbers | ||
| import { createAuth } from "@aura-stack/auth" | ||
| import { authentik } from "@aura-stack/auth/oauth/authentik" | ||
|
|
||
| export const auth = createAuth({ | ||
| oauth: [ | ||
| authentik({ | ||
| authorize: { | ||
| params: { | ||
| // Override default scopes | ||
| scope: "read:user user:email", | ||
| }, | ||
| }, | ||
| }), | ||
| ], | ||
| }) | ||
|
|
||
| export const { handlers, api, jose } = auth | ||
| ``` | ||
|
|
||
| </Step> | ||
|
|
||
| <Step> | ||
|
|
||
| ## Sign In to Authentik (Client & Server) | ||
|
|
||
| There are multiple ways to trigger the sign-in flow depending on your ecosystem. | ||
|
|
||
| ### Sign-in Path (Direct Navigation) | ||
|
|
||
| The common route to trigger the auth flow natively without needing a client library is simply navigating the browser to: | ||
| `http://localhost:3000/auth/signIn/authentik` | ||
|
|
||
| --- | ||
|
|
||
| ### Client-Side (React, Vue, etc.) | ||
|
|
||
| You can utilize the `createAuthClient` utility to programmatically trigger sign-ins. You can also define a `redirectTo` destination. | ||
|
|
||
| <Callout type="warn"> | ||
| **Constraint Rule**: The `baseURL` passed into `createAuthClient` MUST exactly match the root domain and path where the HTTP | ||
| `handlers` expose their endpoints on the server. | ||
| </Callout> | ||
|
|
||
| ```ts title="components/Login.tsx" lineNumbers | ||
| import { createAuthClient } from "@aura-stack/auth/client" | ||
|
|
||
| export const authClient = createAuthClient({ | ||
| baseURL: "http://localhost:3000/auth", | ||
| }) | ||
|
|
||
| const triggerSignIn = async () => { | ||
| await authClient.signIn("authentik", { | ||
| redirectTo: "/dashboard", | ||
| }) | ||
| } | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ### Server-Side (Next.js Actions, Remix Loaders, etc.) | ||
|
|
||
| For environments supporting server-side actions, use the programmatic `api.signIn` method securely. | ||
|
|
||
| ```ts title="actions.ts" lineNumbers | ||
| import { api } from "./auth" | ||
|
|
||
| export const serverSignIn = async () => { | ||
| const response = await api.signIn("authentik", { | ||
| redirectTo: "http://localhost:3000/dashboard", | ||
| }) | ||
|
|
||
| // Example returning redirect location | ||
| return response.headers.get("Location") | ||
| } | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ### Session Retrieval | ||
|
|
||
| After a user successfully signs in, you can retrieve their session data securely. | ||
|
|
||
| **Client-Side:** | ||
|
|
||
| ```ts | ||
| const session = await authClient.getSession() | ||
| console.log(session?.user) // The authenticated Authentik user profile | ||
| ``` | ||
|
|
||
| **Server-Side:** | ||
|
|
||
| ```ts | ||
| // Note: You must pass the native Web Request object or Headers! | ||
| const session = await api.getSession(request) | ||
| console.log(session?.user) // Safely retrieved backend session | ||
| ``` | ||
|
|
||
| </Step> | ||
|
|
||
| </Steps> | ||
|
|
||
| --- | ||
|
|
||
| ## Resources | ||
|
|
||
| - [RFC - The OAuth 2.0 Authorization Framework](https://datatracker.ietf.org/doc/html/rfc6749) | ||
| - [Authentik - OAuth 2.0 Provider](https://docs.goauthentik.io/add-secure-apps/providers/oauth2/) | ||
| - [Authentik - Create an OAuth2 Provider](https://docs.goauthentik.io/add-secure-apps/providers/oauth2/create-oauth2-provider/) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import type { OpenIDProvider, User } from "@/@types/index.ts" | ||
|
|
||
| export interface AuthentikProfile { | ||
| iss: string | ||
| sub: string | ||
| aud: string | ||
| exp: number | ||
| iat: number | ||
| auth_time: number | ||
| acr: string | ||
| c_hash: string | ||
| nonce: string | ||
| at_hash: string | ||
| email: string | ||
| email_verified: boolean | ||
| name: string | ||
| given_name: string | ||
| family_name: string | ||
| preferred_username: string | ||
| nickname: string | ||
| } | ||
|
|
||
| /** | ||
| * Authentik OpenID Connect Provider | ||
| * | ||
| * @see [Authentik - OAuth 2.0 Provider](https://docs.goauthentik.io/add-secure-apps/providers/oauth2/) | ||
| * @see [Authentik - Create an OAuth2 Provider](https://docs.goauthentik.io/add-secure-apps/providers/oauth2/create-oauth2-provider/) | ||
| */ | ||
| export const authentik = <DefaultUser extends User = User>( | ||
| options?: Partial<OpenIDProvider<AuthentikProfile, DefaultUser>> | ||
| ): OpenIDProvider<AuthentikProfile, DefaultUser> => { | ||
| return { | ||
| id: "authentik", | ||
| name: "Authentik", | ||
| issuer: "https://authentik.company/application/o/:application_slug/.well-known/openid-configuration,", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🔴 Critical 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '## Files\n'
git ls-files packages/core/src/oauth/authentik.ts packages/core/src/@types/oidc.ts packages/core/test/presets.ts
printf '\n## Outline: authentik.ts\n'
ast-grep outline packages/core/src/oauth/authentik.ts --view expanded || true
printf '\n## Outline: oidc.ts\n'
ast-grep outline packages/core/src/@types/oidc.ts --view expanded || true
printf '\n## Outline: presets.ts\n'
ast-grep outline packages/core/test/presets.ts --view expanded || true
printf '\n## Read relevant slices\n'
for f in packages/core/src/oauth/authentik.ts packages/core/src/@types/oidc.ts packages/core/test/presets.ts; do
echo "===== $f ====="
nl -ba "$f" | sed -n '1,220p'
doneRepository: aura-stack-ts/auth Length of output: 3128 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '## Search for authentik usage and OIDC contract\n'
rg -n "authentik|issuer:|well-known/openid-configuration|email_verified|nickname|OpenIDProvider<" packages/core -S
printf '\n## Search for preset expectations around profile fields\n'
rg -n "nickname|email_verified|image|email" packages/core/test -SRepository: aura-stack-ts/auth Length of output: 26883 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '## Files\n'
git ls-files packages/core/src/oauth/authentik.ts packages/core/src/@types/oidc.ts packages/core/test/presets.ts
printf '\n## Outline: authentik.ts\n'
ast-grep outline packages/core/src/oauth/authentik.ts --view expanded || true
printf '\n## Outline: oidc.ts\n'
ast-grep outline packages/core/src/@types/oidc.ts --view expanded || true
printf '\n## Outline: presets.ts\n'
ast-grep outline packages/core/test/presets.ts --view expanded || true
printf '\n## Read relevant slices\n'
for f in packages/core/src/oauth/authentik.ts packages/core/src/@types/oidc.ts packages/core/test/presets.ts; do
echo "===== $f ====="
nl -ba "$f" | sed -n '1,220p'
doneRepository: aura-stack-ts/auth Length of output: 3128 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '## Search for authentik usage and OIDC contract\n'
rg -n "authentik|issuer:|well-known/openid-configuration|email_verified|nickname|OpenIDProvider<" packages/core -S
printf '\n## Search for preset expectations around profile fields\n'
rg -n "nickname|email_verified|image|email" packages/core/test -SRepository: aura-stack-ts/auth Length of output: 26883 🏁 Script executed: #!/bin/bash
set -euo pipefail
for f in packages/core/src/oauth/authentik.ts packages/core/src/@types/oidc.ts packages/core/test/presets.ts; do
echo "===== $f ====="
awk '{printf "%4d %s\n", NR, $0}' "$f" | sed -n '1,220p'
doneRepository: aura-stack-ts/auth Length of output: 21690 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "===== authentik provider ====="
awk '{printf "%4d %s\n", NR, $0}' packages/core/src/oauth/authentik.ts | sed -n '1,120p'
echo
echo "===== OIDC provider contract ====="
awk '{printf "%4d %s\n", NR, $0}' packages/core/src/@types/oidc.ts | sed -n '205,230p'
echo
echo "===== preset mapping ====="
awk '{printf "%4d %s\n", NR, $0}' packages/core/test/presets.ts | sed -n '1,90p'Repository: aura-stack-ts/auth Length of output: 5626 🏁 Script executed: #!/bin/bash
set -euo pipefail
for f in packages/core/src/oauth/authentik.ts packages/core/src/@types/oidc.ts packages/core/test/presets.ts; do
echo "===== $f ====="
awk '{printf "%4d %s\n", NR, $0}' "$f" | sed -n '1,220p'
doneRepository: aura-stack-ts/auth Length of output: 21690 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "===== authentik provider ====="
awk '{printf "%4d %s\n", NR, $0}' packages/core/src/oauth/authentik.ts | sed -n '1,120p'
echo
echo "===== OIDC provider contract ====="
awk '{printf "%4d %s\n", NR, $0}' packages/core/src/@types/oidc.ts | sed -n '205,230p'
echo
echo "===== preset mapping ====="
awk '{printf "%4d %s\n", NR, $0}' packages/core/test/presets.ts | sed -n '1,90p'Repository: aura-stack-ts/auth Length of output: 5626 🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n "authentik\s*\(" packages/core -SRepository: aura-stack-ts/auth Length of output: 156 🏁 Script executed: #!/bin/bash
set -euo pipefail
awk '{printf "%4d %s\n", NR, $0}' packages/core/src/actions/oidc/discovery.ts | sed -n '1,120p'Repository: aura-stack-ts/auth Length of output: 1987 🏁 Script executed: #!/bin/bash
set -euo pipefail
awk '{printf "%4d %s\n", NR, $0}' packages/core/src/actions/oidc/resolve-provider.ts | sed -n '1,120p'Repository: aura-stack-ts/auth Length of output: 2982 Make 🤖 Prompt for AI Agents |
||
| profile: (profile) => | ||
| ({ | ||
| sub: profile.sub, | ||
| name: profile.name, | ||
| email: profile.email, | ||
| image: null, | ||
| }) as DefaultUser, | ||
|
Comment on lines
+36
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Map the relevant file and nearby type definitions.
git ls-files packages/core/src/oauth/authentik.ts packages/core/src/** | sed -n '1,120p'
printf '\n--- outline authentik.ts ---\n'
ast-grep outline packages/core/src/oauth/authentik.ts --view expanded || true
printf '\n--- search for DefaultUser and AuthentikProfile ---\n'
rg -n "type DefaultUser|interface DefaultUser|DefaultUser|AuthentikProfile|email_verified|nickname|preferred_username" packages/core/src -SRepository: aura-stack-ts/auth Length of output: 15030 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Read the relevant file and the type definitions it depends on.
wc -l packages/core/src/oauth/authentik.ts || true
printf '\n--- authentik.ts ---\n'
cat -n packages/core/src/oauth/authentik.ts
printf '\n--- oauth type definitions search ---\n'
rg -n "type DefaultUser|interface DefaultUser|export .*DefaultUser|AuthentikProfile|preferred_username|email_verified|nickname" packages/core/src -SRepository: aura-stack-ts/auth Length of output: 5458 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect the shared OIDC/OAuth schema around DefaultUser if found.
files=$(rg -l "DefaultUser|AuthentikProfile|preferred_username|email_verified|nickname" packages/core/src -S || true)
printf '%s\n' "$files"
for f in $files; do
echo "=== $f ==="
wc -l "$f"
doneRepository: aura-stack-ts/auth Length of output: 2052 🏁 Script executed: #!/bin/bash
set -euo pipefail
wc -l packages/core/src/oauth/authentik.ts
printf '\n--- authentik.ts ---\n'
sed -n '1,120p' packages/core/src/oauth/authentik.ts
printf '\n--- relevant symbols ---\n'
rg -n "type DefaultUser|interface DefaultUser|export .*DefaultUser|AuthentikProfile|preferred_username|email_verified|nickname" packages/core/src -SRepository: aura-stack-ts/auth Length of output: 5130 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Show the exact file and the shared user/profile type definitions.
sed -n '1,120p' packages/core/src/oauth/authentik.ts
printf '\n--- search for DefaultUser / profile schema ---\n'
rg -n "AuthentikProfile|email_verified|nickname|preferred_username|DefaultUser" packages/core/src -SRepository: aura-stack-ts/auth Length of output: 12727 🌐 Web query:
💡 Result: In authentik, claims such as email_verified, nickname, and other profile information are managed via Scope Mappings in an OAuth2/OIDC Provider [1]. These claims are not static; they are dynamically generated using Python expressions defined in the provider's property mappings [1][2]. Key details regarding your query include: 1. email_verified Claim: Historically, authentik defaulted email_verified to True [3][4]. However, as of release 2025.10, this defaults to False because authentik does not have an internal mechanism to verify email addresses by default [3][1]. If your application requires this to be True, you can create a custom scope mapping or modify an existing one to return a hardcoded True value, or better, map it to a user attribute (e.g., request.user.attributes.get("email_verified", False)) that you manage elsewhere [3]. 2. nickname Claim: The nickname claim is typically included in the default OAuth Mapping: OpenID 'profile' scope [2]. The standard expression for this mapping is: return { "name": request.user.name, "given_name": request.user.name, "preferred_username": request.user.username, "nickname": request.user.username, "groups": [group.name for group in request.user.groups.all], } 3. DefaultUser and Package Context: There is no "DefaultUser" object in the OAuth context; rather, the mapping has access to a request object, specifically request.user, which represents the user currently authenticating [3][1][2]. This allows you to pull attributes, usernames, or groups directly from the user's profile [5][2][6]. 4. Implementation: To customize these claims, you must: * Navigate to Customization > Property Mappings in the authentik admin interface [3]. * Create or edit an OAuth2 Scope Mapping [1]. * Use the Python expression syntax (as shown above) to return a dictionary of claims [1]. * Assign this mapping to your specific OAuth2 Provider under the 'Property mappings' field [1][7]. * Ensure the client application requests the corresponding scope (e.g., 'profile' or 'email') when initiating the OAuth flow [3][7]. If you are using default setups, these mappings are managed by blueprints [2][8]. If you find your scopes are missing or behaving unexpectedly, verify that the default system blueprints are enabled in your instance [2][8]. Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '--- schemas.ts relevant user shape ---\n'
sed -n '240,280p' packages/core/src/schemas.ts
printf '\n--- any runtime usage of email_verified / nickname / preferred_username ---\n'
rg -n "\b(email_verified|nickname|preferred_username)\b" packages/core/src -SRepository: aura-stack-ts/auth Length of output: 247 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '--- schemas.ts relevant user shape ---\n'
sed -n '240,280p' packages/core/src/schemas.ts
printf '\n--- any runtime usage of email_verified / nickname / preferred_username ---\n'
rg -n "\b(email_verified|nickname|preferred_username)\b" packages/core/src -SRepository: aura-stack-ts/auth Length of output: 247 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '--- schemas.ts relevant user shape ---\n'
sed -n '240,280p' packages/core/src/schemas.ts
printf '\n--- any runtime usage of email_verified / nickname / preferred_username ---\n'
rg -n "\b(email_verified|nickname|preferred_username)\b" packages/core/src -SRepository: aura-stack-ts/auth Length of output: 247 Forward Authentik’s optional claims into the mapped user. 🤖 Prompt for AI Agents |
||
| ...options, | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
Repository: aura-stack-ts/auth
Length of output: 6054
🏁 Script executed:
Repository: aura-stack-ts/auth
Length of output: 263
🏁 Script executed:
Repository: aura-stack-ts/auth
Length of output: 12159
🏁 Script executed:
Repository: aura-stack-ts/auth
Length of output: 12159
Fix the customization example to use the top-level
scopeoption.authentik()acceptsscopedirectly on the provider config, notauthorize.params, so this example does not match the API. Use an OIDC scope string likeopenid profile emailinstead.Suggested fix
export const auth = createAuth({ oauth: [ authentik({ - authorize: { - params: { - // Override default scopes - scope: "read:user user:email", - }, - }, + scope: "openid profile email", }), ], })📝 Committable suggestion
🤖 Prompt for AI Agents