feat(radius): WLAN app passwords expire 60 days after last use
New app passwords start with 29 days (below the 30-day API cap, so no group attribute is needed). The expression policy dot1x-app-password-renew, re-evaluated on the dot1x-login binding, renews the app password that TokenBackend matched to 60 days on each 802.1X login (at most one write per day; always passes, failure_result true). authentik rejects expired app passwords and deletes them in clean_expired_models (every 5 min). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+11
-8
@@ -154,19 +154,22 @@ Android gets a Passpoint profile (`application/x-wifi-config`) with its own
|
||||
app password. "Auf Android installieren" (`/android/install`) signs the user in
|
||||
through the authentik OAuth2 application `wlan-setup` (scope
|
||||
`goauthentik.io/api`). On `/android/callback`, nginx (njs) exchanges the code,
|
||||
creates an app password (`wlan-android-<user>-<timestamp>`, 10 years) with the
|
||||
creates an app password (`wlan-android-<user>-<timestamp>-<random>`) with the
|
||||
user's own access token, and returns the profile in the response to that GET,
|
||||
as the Android installer requires. The page stores nothing. Deleting the app
|
||||
password in authentik removes that device from the WLAN. The profile matches
|
||||
WLANs that advertise Hotspot 2.0 with domain and NAI realm `renner.uno`
|
||||
(EAP-TTLS/PAP).
|
||||
(EAP-TTLS/PAP). Creating the app password needs no admin rights.
|
||||
|
||||
App password lifetimes are capped by the group attribute
|
||||
`goauthentik.io/user/token-maximum-lifetime` (without it: 30 days). WLAN users
|
||||
need a group with `goauthentik.io/user/token-maximum-lifetime: days=3650`, or
|
||||
the creation fails with `Token expires exceeds maximum lifetime` (502 on the
|
||||
callback, reason in the wlan-setup log). Creating the app password needs no
|
||||
admin rights.
|
||||
App passwords expire when unused. The page creates them with 29 days (the API
|
||||
caps new tokens at 30 days unless the group attribute
|
||||
`goauthentik.io/user/token-maximum-lifetime` allows more). Every 802.1X login
|
||||
with an app password renews it to 60 days: the policy
|
||||
`dot1x-app-password-renew` on the `dot1x-login` binding (at most one write per
|
||||
day). authentik rejects expired app passwords and deletes them in its
|
||||
`clean_expired_models` task. A device that has not connected for 60 days, or a
|
||||
profile that was never installed, loses its app password and needs a new
|
||||
profile from the page.
|
||||
|
||||
All clients use EAP-TTLS with inner PAP. The server certificate is a public
|
||||
Let's Encrypt certificate for `radius.renner.uno`, so no CA needs to be
|
||||
|
||||
@@ -1484,7 +1484,7 @@ data:
|
||||
state: present
|
||||
attrs:
|
||||
# App passwords: the WLAN credential that devices store (Android
|
||||
# Passpoint profiles carry it), created by the user in authentik.
|
||||
# Passpoint profiles carry it), created by wlan.renner.uno.
|
||||
backends:
|
||||
- authentik.core.auth.InbuiltBackend
|
||||
- authentik.core.auth.TokenBackend
|
||||
@@ -1509,12 +1509,53 @@ data:
|
||||
order: 20
|
||||
state: present
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
- id: binding-dot1x-login
|
||||
model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
target: !KeyOf dot1x-flow
|
||||
stage: !KeyOf dot1x-login
|
||||
order: 100
|
||||
state: present
|
||||
attrs:
|
||||
evaluate_on_plan: false
|
||||
re_evaluate_policies: true
|
||||
|
||||
# Sliding expiry: an app password used for 802.1X expires 60 days
|
||||
# after its last use; authentik rejects and deletes expired app
|
||||
# passwords. auth_method_args.token is set by TokenBackend. Always
|
||||
# passes; failure_result true keeps an error from blocking the login.
|
||||
- id: dot1x-app-password-renew
|
||||
model: authentik_policies_expression.expressionpolicy
|
||||
identifiers:
|
||||
name: dot1x-app-password-renew
|
||||
state: present
|
||||
attrs:
|
||||
expression: |
|
||||
from datetime import timedelta
|
||||
from django.utils.timezone import now
|
||||
from authentik.core.models import Token, TokenIntents
|
||||
ref = (request.context.get("auth_method_args") or {}).get("token")
|
||||
if request.context.get("auth_method") == "token" and isinstance(ref, dict):
|
||||
renewed = now() + timedelta(days=60)
|
||||
Token.objects.filter(
|
||||
pk=ref.get("pk"),
|
||||
user=request.user,
|
||||
intent=TokenIntents.INTENT_APP_PASSWORD,
|
||||
expiring=True,
|
||||
expires__lt=renewed - timedelta(days=1),
|
||||
).update(expires=renewed)
|
||||
return True
|
||||
|
||||
- model: authentik_policies.policybinding
|
||||
identifiers:
|
||||
policy: !KeyOf dot1x-app-password-renew
|
||||
target: !KeyOf binding-dot1x-login
|
||||
order: 0
|
||||
state: present
|
||||
attrs:
|
||||
enabled: true
|
||||
negate: false
|
||||
failure_result: true
|
||||
|
||||
- id: dot1x-has-vlan
|
||||
model: authentik_policies_expression.expressionpolicy
|
||||
|
||||
@@ -213,8 +213,9 @@ data:
|
||||
# radius.renner.uno (Extension/Android/AAAServerTrustedNames, as Android's
|
||||
# PpsMoParser expects it). The credential is a new app password per install,
|
||||
# created as the user with the access token of the "WLAN setup" OAuth client
|
||||
# (authorization code, scope goauthentik.io/api); its lifetime needs the
|
||||
# group attribute goauthentik.io/user/token-maximum-lifetime >= days=3650.
|
||||
# (authorization code, scope goauthentik.io/api). It starts with 29 days
|
||||
# (below the 30-day token cap); each 802.1X login renews it to 60 days
|
||||
# (policy dot1x-app-password-renew).
|
||||
passpoint.js: |
|
||||
const ISRG_ROOT_X1 = "MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAwTzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygch77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6UA5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sWT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyHB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UCB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUvKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWnOlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTnjh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbwqHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CIrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkqhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZLubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KKNFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7UrTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdCjNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVcoyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPAmRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57demyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=";
|
||||
const USER = /^[A-Za-z0-9._@+-]{1,64}$/;
|
||||
@@ -256,7 +257,7 @@ data:
|
||||
const REDIRECT_URI = "https://wlan.renner.uno/android/callback";
|
||||
const AUTHORIZE = "https://auth.renner.uno/application/o/authorize/";
|
||||
const STATE_COOKIE = "wlan_oauth_state";
|
||||
const LIFETIME_DAYS = 3650;
|
||||
const LIFETIME_DAYS = 29;
|
||||
|
||||
function random() {
|
||||
const bytes = new Uint8Array(24);
|
||||
|
||||
Reference in New Issue
Block a user