Skip to content

Commit 1f4a4f3

Browse files
authored
fix(authentik): device code flow (#10)
* fix(envoy): strip header + set issuer * feat: add issuer value * feat: issuer helpers * fix(authentik): make brand default * doc(readme): api usage * fix(authentik): enable offline_access for refresh tokens
1 parent 9f884a1 commit 1f4a4f3

7 files changed

Lines changed: 94 additions & 7 deletions

File tree

‎README.md‎

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ The chart only creates custom resources that rely on these systems being install
1414
- [cert-manager](https://cert-manager.io/) with a `ClusterIssuer` matching `envoy.clusterissuer`
1515
- A PostgreSQL server, with roles and databases created up front. See [postgresql.md](docs/postgresql.md)
1616

17-
## Usage
17+
## Installation
1818

1919
The repository contains a [`justfile`](justfile) to automate routine commands.
2020
You may use it as reference, or run it with `just` (by default, just will list available recipes).
@@ -97,3 +97,56 @@ just to make sure the user is created in openwebui.
9797
click on the user icon in the bottom left, go to "Admin Panel" -> "Settings" -> "Models". For each model, click on the
9898
Pen icon to edit, then the "Access" button in the top right. Set to "Public", close and "save". This has to be done each
9999
time models are changed.
100+
101+
## Usage
102+
103+
### Web interface
104+
105+
Open `https://openwebui.<baseDomain>` and sign in with the "authentik" button, which
106+
delegates to GitLab. The first sign-in creates the account; accounts are matched by
107+
email, so signing in a different way later does not create a duplicate.
108+
109+
Members of the `gateway admins` group in authentik become OpenWebUI admins.
110+
111+
Newly added models stay private until an admin makes them public once, see
112+
[Manual installation steps](#manual-installation-steps).
113+
114+
A model that has scaled to zero takes a minute or two to answer the first message.
115+
116+
### API access
117+
118+
The gateway at `https://gateway.<baseDomain>/v1` speaks the OpenAI API and takes an
119+
authentik access token as its bearer credential. `GET /v1/models` lists the model names to
120+
use; they are the `models.*.fullName` values.
121+
122+
Get a token with the device code flow. `CLIENT_ID` is `authentik.oauthApp.clientId` -- a
123+
public client, so it is not a secret:
124+
125+
```bash
126+
DOMAIN=<baseDomain>
127+
CLIENT_ID=<authentik.oauthApp.clientId>
128+
129+
# 1. start the flow, then open verification_uri_complete in a browser and approve
130+
curl -s "https://authentik.$DOMAIN/application/o/device/" \
131+
-d client_id="$CLIENT_ID" -d scope="openid profile email offline_access" \
132+
| tee /tmp/dev.json | jq
133+
134+
# 2. exchange the device code for a token (returns authorization_pending until approved)
135+
TOKEN=$(curl -s "https://authentik.$DOMAIN/application/o/token/" \
136+
-d grant_type=urn:ietf:params:oauth:grant-type:device_code \
137+
-d client_id="$CLIENT_ID" \
138+
-d device_code="$(jq -r .device_code /tmp/dev.json)" | jq -r .access_token)
139+
140+
curl "https://gateway.$DOMAIN/v1/chat/completions" \
141+
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
142+
-d '{"model":"<fullName>","messages":[{"role":"user","content":"hello"}]}'
143+
```
144+
145+
Or point any OpenAI client at it:
146+
`OpenAI(base_url=f"https://gateway.{DOMAIN}/v1", api_key=TOKEN)`.
147+
148+
The token is yours, so rate limits and usage are attributed to you. Access tokens last
149+
`authentik.oauthApp.accessTokenValidity` (30 days by default); the device grant also
150+
returns a refresh token, valid for `refreshTokenValidity`, so a client can renew without
151+
a second browser approval. A model that has scaled to zero takes a minute or two to answer
152+
the first request.

‎templates/_helpers.tpl‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,34 @@ Usage: {{ include "model.fullname" (merge (dict "modelName" $name) $) }}
3737
{{- printf "%s-model-%s" .Release.Name .modelName -}}
3838
{{- end -}}
3939

40+
{{/*
41+
Base URL of this release's authentik OAuth2 provider, with a trailing slash.
42+
Callers append the endpoint they need; authentik issues per-provider URLs, so this
43+
prefix is also the `iss` claim on the tokens it signs.
44+
*/}}
45+
{{- define "authentik.providerUrl" -}}
46+
{{- printf "https://authentik.%s/application/o/%s/" .Values.envoy.baseDomain .Values.authentik.oauthApp.name -}}
47+
{{- end -}}
48+
4049
{{/*
4150
JWKS URI: use authentik if enabled, otherwise configurable
4251
*/}}
4352
{{- define "envoy.jwksUri" -}}
4453
{{- if .Values.envoy.security.jwksUri -}}
4554
{{ .Values.envoy.security.jwksUri }}
4655
{{- else if .Values.authentik.enabled -}}
47-
{{- printf "https://authentik.%s/application/o/%s/jwks/" .Values.envoy.baseDomain .Values.authentik.oauthApp.name -}}
56+
{{- printf "%sjwks/" (include "authentik.providerUrl" .) -}}
57+
{{- end -}}
58+
{{- end -}}
59+
60+
{{/*
61+
Expected `iss` claim. One authentik signs every provider with the same key, so the
62+
JWKS alone would also accept another application's tokens.
63+
*/}}
64+
{{- define "envoy.jwtIssuer" -}}
65+
{{- if .Values.envoy.security.jwksUri -}}
66+
{{- .Values.envoy.security.issuer | required "envoy.security.issuer is required alongside envoy.security.jwksUri, or the gateway accepts every token that JWKS validates" -}}
67+
{{- else if .Values.authentik.enabled -}}
68+
{{- include "authentik.providerUrl" . -}}
4869
{{- end -}}
4970
{{- end -}}

‎templates/authentik-blueprints-secret.yaml‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ stringData:
4141
name: vllm-service-account
4242
is_active: true
4343
type: service_account
44-
4544
# OAuth2 Provider (depends on: certificatekeypair #1)
4645
- model: authentik_providers_oauth2.oauth2provider
4746
identifiers:
@@ -73,6 +72,8 @@ stringData:
7372
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]]
7473
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]]
7574
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, entitlements]]
75+
{{- /* Required for refresh tokens: refresh token are only issued when offline_access survives that. */}}
76+
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, offline_access]]
7677
7778
# OAuth2 Application (depends on: oauth2provider #5)
7879
- model: authentik_core.application
@@ -107,10 +108,12 @@ stringData:
107108
sources:
108109
- !Find [authentik_sources_oauth.oauthsource, [slug, datascience-gitlab]]
109110
110-
# Brand (depends on: default-device-code flow, which is a built-in flow)
111+
# Authentik picks a brand whose domain matches the request host or which is flagged default
112+
# and synthesises an unconfigured in-memory brand when neither matches.
111113
- model: authentik_brands.brand
112114
identifiers:
113115
domain: authentik-default
114116
attrs:
117+
default: true
115118
flow_device_code: !Find [authentik_flows.flow, [slug, default-source-pre-authentication]]
116119
{{- end }}

‎templates/envoy/client-traffic-policy.yaml‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,11 @@ spec:
1313
name: {{ include "envoy.fullname" . }}
1414
connection:
1515
bufferLimit: {{ .Values.envoy.clientBufferLimit | default "50Mi" }}
16+
{{- /* A client-set x-sub would outrank the verified claim: jwt_authn appends, and
17+
readers take the first value. Mirror claimToHeaders in security-policy.yaml. */}}
18+
headers:
19+
earlyRequestHeaders:
20+
remove:
21+
- x-sub
22+
- x-name
1623
{{- end }}

‎templates/envoy/security-policy.yaml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ spec:
1414
- name: authentik
1515
remoteJWKS:
1616
uri: {{ include "envoy.jwksUri" . }}
17+
{{- with (include "envoy.jwtIssuer" .) }}
18+
issuer: {{ . }}
19+
{{- end }}
1720
recomputeRoute: true
1821
claimToHeaders:
1922
- claim: sub

‎templates/openwebui/deployment.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ spec:
6161
- name: OAUTH_ADMIN_ROLES
6262
value: gateway admins
6363
- name: OPENID_PROVIDER_URL
64-
value: https://authentik.{{ .Values.envoy.baseDomain }}/application/o/{{ .Values.authentik.oauthApp.name }}/.well-known/openid-configuration
64+
value: {{ include "authentik.providerUrl" . }}.well-known/openid-configuration
6565
- name: OPENID_REDIRECT_URI
6666
value: https://openwebui.{{ .Values.envoy.baseDomain }}/oauth/oidc/callback
6767
- name: OAUTH_PROVIDER_NAME

‎values.yaml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ envoy:
55
clientBufferLimit: 50Mi
66
security:
77
jwksUri:
8+
# `iss` to require. Derived from authentik by default; required when jwksUri is set.
9+
issuer:
810
gatewayClass:
911
enabled: false
1012
name:
@@ -137,5 +139,3 @@ authentik:
137139
clientSecret:
138140
accessTokenValidity: "days=30"
139141
refreshTokenValidity: "days=365"
140-
serviceAccount:
141-
create: false

0 commit comments

Comments
 (0)