Airlock
view release on metacpan or search on metacpan
t/authentik/README.md view on Meta::CPAN
## Run
```bash
TEST_AIRLOCK_AUTHENTIK_URL=http://127.0.0.1:9000 \
TEST_AIRLOCK_AUTHENTIK_TOKEN="$AUTHENTIK_BOOTSTRAP_TOKEN" \
prove -lv t/91-live-authentik.t
```
The test needs the `p5-www-authentik` checkout next door, for two things it does
not carry itself: `WWW::Authentik` to make and remove the fixtures, and
`t/lib/AuthentikExecutor.pm` to log in and enrol TOTP through authentik's flow
executor. Without it the test skips. Three variables point at it:
| Variable | Default |
|---|---|
| `AIRLOCK_WWW_AUTHENTIK` | `~/dev/p5-www-authentik` |
| `AIRLOCK_WWW_AUTHENTIK_LIB` | `$AIRLOCK_WWW_AUTHENTIK/lib` |
| `AIRLOCK_AUTHENTIK_LIB` | `$AIRLOCK_WWW_AUTHENTIK/t/lib` |
Setting the first is enough unless the two directories live apart.
One run spends four requests on the device authorization endpoint, and authentik
throttles that endpoint to **20 an hour per client IP** â see the finding below.
The compose file next door sets `AUTHENTIK_THROTTLE__PROVIDERS__OAUTH2__DEVICE`
high enough; against an instance that does not, the fifth run within the hour
bails out and says so.
**Airlock does not depend on either at runtime.** Neither is in `cpanfile`, and
nothing under `lib/` mentions them. The executor is deliberately not copied in
here: the shape of a flow challenge belongs to authentik and changes with its
versions, a copy would drift unnoticed, and a distribution that ships no
provider code should not carry two hundred lines of one.
## Findings
authentik 2026.8.3, recorded 2026-10-04, through the whole chain.
| Login | acr | amr | auth_time |
|---|---|---|---|
| device flow, browser login, password | `goauthentik.io/providers/oauth2/default` | `pwd` | time of login |
| device flow, browser login, password + TOTP | the same | `pwd`, `mfa` | time of login |
| a second token from the same session | the same | unchanged | **the first login**, not the new token |
| a refreshed token | the same | unchanged | unchanged |
What the runs established:
- **authentik reports the second factor out of the box.** No mapper, no scope,
no flow setting: a password login carries `amr=pwd`, a login with TOTP
`amr=pwd,mfa`, and `Airlock::Factor::Upstream` tells them apart with its
defaults. This is the difference to Keycloak, which says nothing until
`t/keycloak/setup.pl` has run.
- **`acr` is one constant string** whatever happened, so `mfa_acr` stays empty
in `Airlock::Upstream::Authentik`. Sending `acr_values` changes nothing.
- **`auth_time` is when the session began** for the grants Airlock meets â the
authorization code and the device code both take it from the session's login
event â and it survives a refresh and further authorization requests.
`max_age` on the factor therefore measures the age of the authentication,
which is what a caller wants: a token minted now from a ten-minute-old
session is refused by `max_age => 300`. Two things it is not: for a
client-credentials or token-exchange grant `auth_time` is the minting time
(those carry no `amr`, so the factor refuses them anyway), and when authentik
finds no login event for a session it falls back to the current time, which
makes an old authentication look new. That last one fails open; it was read
in authentik's source, not provoked here.
- **`max_age=0` is the one value authentik throws away**, because its check is
on the truth of the number and zero is not true. Every other value works, and
so does `prompt=login`. Measured against a session six seconds old:
| sent | result |
|---|---|
| nothing | a code, no new login |
| `max_age=0` | a code, no new login â what `Airlock::Factor::Upstream->reauth_params` sends |
| `max_age=1`, `max_age=2` | sent back to log in |
| `max_age=3600` | a code, the session is younger than that |
| `prompt=login` | sent back to log in |
`Airlock::Upstream::Authentik->reauth_params` gives what works.
- `Airlock::Client` completes a device flow against authentik: discovery, start,
`authorization_pending` while waiting, and the token after the approval. The
device response carries `verification_uri_complete`.
- A device code is approved by opening `/device?code=â¦` with a session, which
redirects into the authorization flow; one run of that flow through the
executor ends at `ak-provider-oauth2-device-code-finish` and the poll
succeeds.
- **The device authorization endpoint is throttled, and says `slow_down`.**
RFC 8628 gives `slow_down` to the token endpoint, for a client polling too
fast. authentik also puts a rate limit on the authorization request that
starts the flow: `throttle.providers.oauth2.device`, 20 an hour per client IP
by default, answered as HTTP 429 with that same error code. A client cannot
tell it from a polling complaint except by which request it answered. Raise
`AUTHENTIK_THROTTLE__PROVIDERS__OAUTH2__DEVICE` on a test instance; in
production note that every device behind one NAT address shares the bucket.
- **authentik refuses a TOTP code twice.** The executor waits for the next
thirty-second window and submits again, so a run can take a minute.
- The brand needs a `flow_device_code`; without it `/device` has nothing to
show.
( run in 1.199 second using v1.01-cache-2.11-cpan-3f962061493 )