|
|
|
@@ -1,482 +0,0 @@
|
|
|
|
|
# wlan.renner.uno: setup files for "Renner UniFi" (802.1X, EAP-TTLS/PAP
|
|
|
|
|
# against freeradius, outer identity anonymous, server name radius.renner.uno).
|
|
|
|
|
# Served by wlan-setup (wlan-setup.yaml) behind the Authentik gate; the
|
|
|
|
|
# username comes from X-authentik-username. The Android and Apple Passpoint
|
|
|
|
|
# profiles carry a credential: a per-device authentik app password that the
|
|
|
|
|
# page creates with the user's own OAuth access token.
|
|
|
|
|
apiVersion: v1
|
|
|
|
|
kind: ConfigMap
|
|
|
|
|
metadata:
|
|
|
|
|
name: wlan-setup
|
|
|
|
|
namespace: authentik
|
|
|
|
|
data:
|
|
|
|
|
# The image's nginx.conf plus the njs module and the OAuth client secret
|
|
|
|
|
# passpoint.js reads from the environment.
|
|
|
|
|
nginx.conf: |
|
|
|
|
|
load_module modules/ngx_http_js_module.so;
|
|
|
|
|
|
|
|
|
|
env WLAN_SETUP_CLIENT_SECRET;
|
|
|
|
|
|
|
|
|
|
worker_processes 1;
|
|
|
|
|
error_log /var/log/nginx/error.log notice;
|
|
|
|
|
pid /tmp/nginx.pid;
|
|
|
|
|
|
|
|
|
|
events {
|
|
|
|
|
worker_connections 1024;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
http {
|
|
|
|
|
proxy_temp_path /tmp/proxy_temp;
|
|
|
|
|
client_body_temp_path /tmp/client_temp;
|
|
|
|
|
fastcgi_temp_path /tmp/fastcgi_temp;
|
|
|
|
|
uwsgi_temp_path /tmp/uwsgi_temp;
|
|
|
|
|
scgi_temp_path /tmp/scgi_temp;
|
|
|
|
|
|
|
|
|
|
include /etc/nginx/mime.types;
|
|
|
|
|
default_type application/octet-stream;
|
|
|
|
|
|
|
|
|
|
log_format main '$remote_addr - [$time_local] "$request" $status $body_bytes_sent "$http_user_agent"';
|
|
|
|
|
access_log /var/log/nginx/access.log main;
|
|
|
|
|
|
|
|
|
|
sendfile on;
|
|
|
|
|
keepalive_timeout 65;
|
|
|
|
|
|
|
|
|
|
include /etc/nginx/conf.d/*.conf;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
default.conf: |
|
|
|
|
|
# Only plain usernames reach a generated file; anything else leaves the
|
|
|
|
|
# field empty and the device asks for it.
|
|
|
|
|
map $http_x_authentik_username $wifi_user {
|
|
|
|
|
"~^[A-Za-z0-9._@+-]{1,64}$" $http_x_authentik_username;
|
|
|
|
|
default "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
js_path /etc/nginx/njs/;
|
|
|
|
|
js_import passpoint from passpoint.js;
|
|
|
|
|
|
|
|
|
|
# authentik-server, resolved once at start; reached only by passpoint.js
|
|
|
|
|
# subrequests (/_ak/...).
|
|
|
|
|
upstream authentik {
|
|
|
|
|
server authentik-server.authentik.svc.cluster.local:80;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
server {
|
|
|
|
|
listen 8080;
|
|
|
|
|
listen [::]:8080;
|
|
|
|
|
server_tokens off;
|
|
|
|
|
root /usr/share/nginx/html;
|
|
|
|
|
add_header Cache-Control "no-store" always;
|
|
|
|
|
|
|
|
|
|
location = / {
|
|
|
|
|
try_files /index.html =404;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
location = /whoami {
|
|
|
|
|
default_type text/plain;
|
|
|
|
|
return 200 $wifi_user;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
location = /renner-unifi-wlan.cmd {
|
|
|
|
|
default_type application/octet-stream;
|
|
|
|
|
add_header Cache-Control "no-store" always;
|
|
|
|
|
add_header Content-Disposition 'attachment; filename="renner-unifi-wlan.cmd"' always;
|
|
|
|
|
try_files /renner-unifi-wlan.cmd =404;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Android and Apple: a tap on /<platform>/install starts an
|
|
|
|
|
# authorization-code sign-in at authentik (the user is already signed
|
|
|
|
|
# in there); the callback creates the app password as the user and
|
|
|
|
|
# answers the same navigation with the Passpoint profile. Android:
|
|
|
|
|
# application/x-wifi-config, base64, no Content-Disposition, as its
|
|
|
|
|
# installer requires. Apple: a .mobileconfig download.
|
|
|
|
|
location ~ ^/(android|apple)/install$ {
|
|
|
|
|
js_content passpoint.install;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
location ~ ^/(android|apple)/callback$ {
|
|
|
|
|
# The query carries the one-time authorization code.
|
|
|
|
|
access_log off;
|
|
|
|
|
js_content passpoint.callback;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Subrequests from passpoint.js only. Credential and content type
|
|
|
|
|
# arrive as arguments and leave as headers; proxy_pass with a variable
|
|
|
|
|
# URI does not forward the arguments. $arg_* only: subrequests share
|
|
|
|
|
# the parent's variable cache, so a map would keep its first value.
|
|
|
|
|
location ~ ^/_ak(/.*)$ {
|
|
|
|
|
internal;
|
|
|
|
|
proxy_pass http://authentik$1;
|
|
|
|
|
# njs reads the whole reply from this buffer (default one page).
|
|
|
|
|
subrequest_output_buffer_size 256k;
|
|
|
|
|
# Only the headers set here reach authentik.
|
|
|
|
|
proxy_pass_request_headers off;
|
|
|
|
|
proxy_set_header Host auth.renner.uno;
|
|
|
|
|
proxy_set_header Cookie "";
|
|
|
|
|
proxy_set_header Authorization "$arg_scheme $arg_cred";
|
|
|
|
|
proxy_set_header Content-Type $arg_ct;
|
|
|
|
|
proxy_set_header Accept "application/json";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
location = /healthz {
|
|
|
|
|
access_log off;
|
|
|
|
|
default_type text/plain;
|
|
|
|
|
return 200 "ok\n";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
location / {
|
|
|
|
|
return 404;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Windows: extracts the WLAN profile (everything after line 8 of itself)
|
|
|
|
|
# into %TEMP% and imports it for the current user with netsh. EAP-TTLS with
|
|
|
|
|
# PAPAuthentication, outer identity anonymous, server name
|
|
|
|
|
# radius.renner.uno, trust anchor ISRG Root X1 (SHA-1 thumbprint; the
|
|
|
|
|
# Let's Encrypt chain of radius-tls ends there). Windows asks for username
|
|
|
|
|
# and password on connect. CRLF line endings: cmd.exe needs them.
|
|
|
|
|
renner-unifi-wlan.cmd: "@echo off\r\nrem WLAN Renner UniFi einrichten (802.1X, Anmeldung mit dem authentik-Konto)\r\nset \"WLANXML=%TEMP%\\renner-unifi-wlan.xml\"\r\nmore +8 \"%~f0\" > \"%WLANXML%\"\r\nnetsh wlan add profile filename=\"%WLANXML%\" user=current\r\ndel \"%WLANXML%\"\r\nnetsh wlan connect name=\"Renner UniFi\"\r\npause & exit /b\r\n<?xml version=\"1.0\"?><WLANProfile xmlns=\"http://www.microsoft.com/networking/WLAN/profile/v1\"><name>Renner UniFi</name><SSIDConfig><SSID><hex>52656E6E657220556E694669</hex><name>Renner UniFi</name></SSID><nonBroadcast>false</nonBroadcast></SSIDConfig><connectionType>ESS</connectionType><connectionMode>auto</connectionMode><MSM><security><authEncryption><authentication>WPA2</authentication><encryption>AES</encryption><useOneX>true</useOneX></authEncryption><OneX xmlns=\"http://www.microsoft.com/networking/OneX/v1\"><cacheUserData>true</cacheUserData><authMode>user</authMode><EAPConfig><EapHostConfig xmlns=\"http://www.microsoft.com/provisioning/EapHostConfig\"><EapMethod><Type xmlns=\"http://www.microsoft.com/provisioning/EapCommon\">21</Type><VendorId xmlns=\"http://www.microsoft.com/provisioning/EapCommon\">0</VendorId><VendorType xmlns=\"http://www.microsoft.com/provisioning/EapCommon\">0</VendorType><AuthorId xmlns=\"http://www.microsoft.com/provisioning/EapCommon\">311</AuthorId></EapMethod><Config xmlns=\"http://www.microsoft.com/provisioning/EapHostConfig\"><EapTtls xmlns=\"http://www.microsoft.com/provisioning/EapTtlsConnectionPropertiesV1\"><ServerValidation><ServerNames>radius.renner.uno</ServerNames><TrustedRootCAHash>ca bd 2a 79 a1 07 6a 31 f2 1d 25 36 35 cb 03 9d 43 29 a5 e8</TrustedRootCAHash><DisablePrompt>false</DisablePrompt></ServerValidation><Phase2Authentication><PAPAuthentication/></Phase2Authentication><Phase1Identity><IdentityPrivacy>true</IdentityPrivacy><AnonymousIdentity>anonymous</AnonymousIdentity></Phase1Identity></EapTtls></Config></EapHostConfig></EAPConfig></OneX></security></MSM></WLANProfile>\r\n"
|
|
|
|
|
|
|
|
|
|
# Android Passpoint R1 provisioning (source.android.com, Passpoint): the
|
|
|
|
|
# body is base64 of a MIME multipart with the PPS MO and the trust anchor.
|
|
|
|
|
# HomeSP FQDN and realm renner.uno match the Hotspot 2.0 domain and NAI realm
|
|
|
|
|
# the Passpoint WLAN announces; EAP-TTLS (21) with inner PAP. Trust anchor
|
|
|
|
|
# ISRG Root X1 (the radius-tls chain ends there), AAA server name
|
|
|
|
|
# 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). 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).
|
|
|
|
|
# Apple: com.apple.wifi.managed with IsHotspot, DomainName and NAIRealmNames
|
|
|
|
|
# renner.uno (no SSID: Passpoint matching), TTLS (21) with inner PAP, outer
|
|
|
|
|
# identity anonymous, trusted server name radius.renner.uno, username and
|
|
|
|
|
# app password. Fixed PayloadIdentifier/UUIDs so a reinstall replaces the
|
|
|
|
|
# previous profile.
|
|
|
|
|
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}$/;
|
|
|
|
|
|
|
|
|
|
function b64(s) { return Buffer.from(s).toString("base64"); }
|
|
|
|
|
function node(name, inner) { return "<Node><NodeName>" + name + "</NodeName>" + inner + "</Node>"; }
|
|
|
|
|
function leaf(name, value) { return node(name, "<Value>" + value + "</Value>"); }
|
|
|
|
|
|
|
|
|
|
function profile(user, password) {
|
|
|
|
|
const pps = '<MgmtTree xmlns="syncml:dmddf1.2"><VerDTD>1.2</VerDTD>' +
|
|
|
|
|
node("PerProviderSubscription",
|
|
|
|
|
"<RTProperties><Type><DDFName>urn:wfa:mo:hotspot2dot0-perprovidersubscription:1.0</DDFName></Type></RTProperties>" +
|
|
|
|
|
node("i001",
|
|
|
|
|
node("HomeSP", leaf("FriendlyName", "Renner") + leaf("FQDN", "renner.uno")) +
|
|
|
|
|
node("Credential",
|
|
|
|
|
leaf("Realm", "renner.uno") +
|
|
|
|
|
node("UsernamePassword",
|
|
|
|
|
leaf("Username", user) +
|
|
|
|
|
leaf("Password", b64(password)) +
|
|
|
|
|
node("EAPMethod", leaf("EAPType", "21") + leaf("InnerMethod", "PAP")))) +
|
|
|
|
|
node("Extension", node("Android",
|
|
|
|
|
node("AAAServerTrustedNames", leaf("FQDN", "radius.renner.uno")))))) +
|
|
|
|
|
"</MgmtTree>";
|
|
|
|
|
const mime = "Content-Type: multipart/mixed; boundary={boundary}\r\n" +
|
|
|
|
|
"Content-Transfer-Encoding: base64\r\n\r\n" +
|
|
|
|
|
"--{boundary}\r\n" +
|
|
|
|
|
"Content-Type: application/x-passpoint-profile\r\n" +
|
|
|
|
|
"Content-Transfer-Encoding: base64\r\n\r\n" +
|
|
|
|
|
b64(pps) + "\r\n" +
|
|
|
|
|
"--{boundary}\r\n" +
|
|
|
|
|
"Content-Type: application/x-x509-ca-cert\r\n" +
|
|
|
|
|
"Content-Transfer-Encoding: base64\r\n\r\n" +
|
|
|
|
|
ISRG_ROOT_X1 + "\r\n" +
|
|
|
|
|
"--{boundary}--\r\n";
|
|
|
|
|
return b64(mime);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function xml(s) {
|
|
|
|
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mobileconfig(user, password) {
|
|
|
|
|
return '<?xml version="1.0" encoding="UTF-8"?>\n' +
|
|
|
|
|
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n' +
|
|
|
|
|
'<plist version="1.0"><dict>' +
|
|
|
|
|
'<key>PayloadContent</key><array><dict>' +
|
|
|
|
|
'<key>PayloadType</key><string>com.apple.wifi.managed</string>' +
|
|
|
|
|
'<key>PayloadVersion</key><integer>1</integer>' +
|
|
|
|
|
'<key>PayloadIdentifier</key><string>uno.renner.wifi.renner-unifi.network</string>' +
|
|
|
|
|
'<key>PayloadUUID</key><string>824959bf-c55d-4ed2-b569-c33f3759c2b6</string>' +
|
|
|
|
|
'<key>PayloadDisplayName</key><string>Renner UniFi</string>' +
|
|
|
|
|
'<key>PayloadOrganization</key><string>Renner</string>' +
|
|
|
|
|
'<key>IsHotspot</key><true/>' +
|
|
|
|
|
'<key>DomainName</key><string>renner.uno</string>' +
|
|
|
|
|
'<key>NAIRealmNames</key><array><string>renner.uno</string></array>' +
|
|
|
|
|
'<key>DisplayedOperatorName</key><string>Renner</string>' +
|
|
|
|
|
'<key>ServiceProviderRoamingEnabled</key><false/>' +
|
|
|
|
|
'<key>AutoJoin</key><true/>' +
|
|
|
|
|
'<key>EncryptionType</key><string>Any</string>' +
|
|
|
|
|
'<key>EAPClientConfiguration</key><dict>' +
|
|
|
|
|
'<key>UserName</key><string>' + xml(user) + '</string>' +
|
|
|
|
|
'<key>UserPassword</key><string>' + xml(password) + '</string>' +
|
|
|
|
|
'<key>OuterIdentity</key><string>anonymous</string>' +
|
|
|
|
|
'<key>AcceptEAPTypes</key><array><integer>21</integer></array>' +
|
|
|
|
|
'<key>TTLSInnerAuthentication</key><string>PAP</string>' +
|
|
|
|
|
'<key>TLSTrustedServerNames</key><array><string>radius.renner.uno</string></array>' +
|
|
|
|
|
'</dict></dict></array>' +
|
|
|
|
|
'<key>PayloadType</key><string>Configuration</string>' +
|
|
|
|
|
'<key>PayloadVersion</key><integer>1</integer>' +
|
|
|
|
|
'<key>PayloadIdentifier</key><string>uno.renner.wifi.renner-unifi</string>' +
|
|
|
|
|
'<key>PayloadUUID</key><string>7175a2a3-ddd3-42ea-b4ad-b7e5501892f3</string>' +
|
|
|
|
|
'<key>PayloadDisplayName</key><string>WLAN Renner UniFi</string>' +
|
|
|
|
|
'<key>PayloadOrganization</key><string>Renner</string>' +
|
|
|
|
|
'</dict></plist>\n';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const PLATFORMS = {
|
|
|
|
|
android: { label: "Android" },
|
|
|
|
|
apple: { label: "Apple" },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// "android" or "apple" from /<platform>/install or /<platform>/callback.
|
|
|
|
|
function platform(r) {
|
|
|
|
|
return r.uri.split("/")[1];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function redirectUri(name) {
|
|
|
|
|
return "https://wlan.renner.uno/" + name + "/callback";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const CLIENT_ID = "wlan-setup";
|
|
|
|
|
const AUTHORIZE = "https://auth.renner.uno/application/o/authorize/";
|
|
|
|
|
const STATE_COOKIE = "wlan_oauth_state";
|
|
|
|
|
const LIFETIME_DAYS = 29;
|
|
|
|
|
|
|
|
|
|
function random() {
|
|
|
|
|
const bytes = new Uint8Array(24);
|
|
|
|
|
crypto.getRandomValues(bytes);
|
|
|
|
|
return Buffer.from(bytes).toString("base64url");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cookie(r, name) {
|
|
|
|
|
const all = r.headersIn["Cookie"] || "";
|
|
|
|
|
const parts = all.split(";");
|
|
|
|
|
for (let n = 0; n < parts.length; n++) {
|
|
|
|
|
const part = parts[n];
|
|
|
|
|
const i = part.indexOf("=");
|
|
|
|
|
if (i > 0 && part.substring(0, i).trim() === name) return part.substring(i + 1).trim();
|
|
|
|
|
}
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function form(fields) {
|
|
|
|
|
return Object.keys(fields).map(k => encodeURIComponent(k) + "=" + encodeURIComponent(fields[k])).join("&");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const FORM = "application/x-www-form-urlencoded";
|
|
|
|
|
const JSON_CT = "application/json";
|
|
|
|
|
const CRED = /^[A-Za-z0-9._~+\/=-]+$/;
|
|
|
|
|
|
|
|
|
|
// authentik call through the internal /_ak location; the arguments
|
|
|
|
|
// become the Authorization and Content-Type headers there (raw, nginx
|
|
|
|
|
// does not decode $arg_*).
|
|
|
|
|
async function ak(r, method, path, scheme, cred, ct, body) {
|
|
|
|
|
if (!CRED.test(cred)) throw new Error(method + " " + path + ": no credential");
|
|
|
|
|
const args = "scheme=" + scheme + "&cred=" + cred + "&ct=" + ct;
|
|
|
|
|
const opts = { method: method, args: args };
|
|
|
|
|
if (body !== undefined) opts.body = body;
|
|
|
|
|
const reply = await r.subrequest("/_ak" + path, opts);
|
|
|
|
|
if (reply.status < 200 || reply.status > 299) {
|
|
|
|
|
throw new Error(method + " " + path + " -> " + reply.status + " " + reply.responseText.substring(0, 300));
|
|
|
|
|
}
|
|
|
|
|
return JSON.parse(reply.responseText);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function install(r) {
|
|
|
|
|
const name = platform(r);
|
|
|
|
|
const state = random();
|
|
|
|
|
r.headersOut["Set-Cookie"] = STATE_COOKIE + "=" + state +
|
|
|
|
|
"; Path=/" + name + "/; Max-Age=300; HttpOnly; Secure; SameSite=Lax";
|
|
|
|
|
r.return(302, AUTHORIZE + "?" + form({
|
|
|
|
|
response_type: "code",
|
|
|
|
|
client_id: CLIENT_ID,
|
|
|
|
|
redirect_uri: redirectUri(name),
|
|
|
|
|
scope: "openid profile goauthentik.io/api",
|
|
|
|
|
state: state,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function callback(r) {
|
|
|
|
|
const name = platform(r);
|
|
|
|
|
try {
|
|
|
|
|
const state = cookie(r, STATE_COOKIE);
|
|
|
|
|
if (!state || r.args.state !== state || !r.args.code) { r.return(400, "Bitte die Einrichtung neu starten.\n"); return; }
|
|
|
|
|
const client = Buffer.from(CLIENT_ID + ":" + process.env.WLAN_SETUP_CLIENT_SECRET).toString("base64");
|
|
|
|
|
const grant = await ak(r, "POST", "/application/o/token/", "Basic", client, FORM, form({
|
|
|
|
|
grant_type: "authorization_code",
|
|
|
|
|
code: r.args.code,
|
|
|
|
|
redirect_uri: redirectUri(name),
|
|
|
|
|
}));
|
|
|
|
|
const access = grant.access_token;
|
|
|
|
|
const me = await ak(r, "GET", "/api/v3/core/users/me/", "Bearer", access, "");
|
|
|
|
|
const user = me.user.username;
|
|
|
|
|
if (!USER.test(user) || user !== r.headersIn["X-authentik-username"]) { r.return(403); return; }
|
|
|
|
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\..*$/, "");
|
|
|
|
|
const identifier = "wlan-" + name + "-" + user.replace(/[^A-Za-z0-9-]/g, "-").toLowerCase() + "-" + stamp + "-" + random().substring(0, 6).replace(/[^A-Za-z0-9]/g, "x");
|
|
|
|
|
const expires = new Date(now.getTime() + LIFETIME_DAYS * 86400000).toISOString();
|
|
|
|
|
await ak(r, "POST", "/api/v3/core/tokens/", "Bearer", access, JSON_CT, JSON.stringify({
|
|
|
|
|
identifier: identifier,
|
|
|
|
|
intent: "app_password",
|
|
|
|
|
description: "WLAN " + PLATFORMS[name].label + ", " + now.toISOString().substring(0, 10) + " (wlan.renner.uno)",
|
|
|
|
|
expiring: true,
|
|
|
|
|
expires: expires,
|
|
|
|
|
}));
|
|
|
|
|
const key = (await ak(r, "GET", "/api/v3/core/tokens/" + identifier + "/view_key/", "Bearer", access, "")).key;
|
|
|
|
|
|
|
|
|
|
r.headersOut["Set-Cookie"] = STATE_COOKIE + "=; Path=/" + name + "/; Max-Age=0; HttpOnly; Secure; SameSite=Lax";
|
|
|
|
|
if (name === "apple") {
|
|
|
|
|
r.headersOut["Content-Type"] = "application/x-apple-aspen-config";
|
|
|
|
|
r.headersOut["Content-Disposition"] = 'attachment; filename="renner-unifi.mobileconfig"';
|
|
|
|
|
r.return(200, mobileconfig(user, key));
|
|
|
|
|
} else {
|
|
|
|
|
r.headersOut["Content-Type"] = "application/x-wifi-config";
|
|
|
|
|
r.headersOut["Content-Transfer-Encoding"] = "base64";
|
|
|
|
|
r.return(200, profile(user, key));
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
r.error("wlan-setup " + name + ": " + e.message);
|
|
|
|
|
r.return(502, "Die Einrichtung ist fehlgeschlagen.\n");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default { install, callback };
|
|
|
|
|
|
|
|
|
|
index.html: |
|
|
|
|
|
<!doctype html>
|
|
|
|
|
<html lang="de">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="utf-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
|
|
<title>WLAN einrichten</title>
|
|
|
|
|
<style>
|
|
|
|
|
:root { --bg: #ffffff; --fg: #1d1d1f; --muted: #5f6368; --card: #f4f5f7; --accent: #0b63ce; --code: #e9ebef; }
|
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
|
|
|
:root { --bg: #121314; --fg: #e8eaed; --muted: #9aa0a6; --card: #1e1f22; --accent: #7cacf8; --code: #2a2b2f; }
|
|
|
|
|
}
|
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
|
body { margin: 0; background: var(--bg); color: var(--fg); font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
|
|
|
|
|
main { max-width: 42rem; margin: 0 auto; padding: 1.5rem 1rem 3rem; }
|
|
|
|
|
h1 { font-size: 1.6rem; margin: 0 0 .25rem; }
|
|
|
|
|
h2 { font-size: 1.15rem; margin: 0 0 .5rem; }
|
|
|
|
|
p { margin: .5rem 0; }
|
|
|
|
|
.muted { color: var(--muted); }
|
|
|
|
|
section { background: var(--card); border-radius: 12px; padding: 1rem 1.1rem; margin: 1rem 0; }
|
|
|
|
|
section.current { outline: 2px solid var(--accent); }
|
|
|
|
|
a.button, button { display: inline-block; background: var(--accent); color: #fff; border: 0; border-radius: 8px; padding: .6rem 1rem; font: inherit; text-decoration: none; cursor: pointer; }
|
|
|
|
|
@media (prefers-color-scheme: dark) { a.button, button { color: #121314; } }
|
|
|
|
|
ol { padding-left: 1.3rem; margin: .5rem 0; }
|
|
|
|
|
pre { background: var(--code); border-radius: 8px; padding: .75rem; overflow-x: auto; white-space: pre-wrap; word-break: break-all; font-size: .85rem; margin: .5rem 0; }
|
|
|
|
|
details > summary { cursor: pointer; color: var(--accent); margin: 1.5rem 0 .5rem; }
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<main>
|
|
|
|
|
<h1>WLAN einrichten</h1>
|
|
|
|
|
<p>WLAN <strong>Renner UniFi</strong> für dein Konto <strong id="user">…</strong>. Android, iPhone, iPad und Mac bekommen ein eigenes WLAN-App-Passwort; Windows und Linux melden sich mit deinem authentik-Passwort an.</p>
|
|
|
|
|
|
|
|
|
|
<div id="detected"></div>
|
|
|
|
|
|
|
|
|
|
<details id="others">
|
|
|
|
|
<summary>Andere Geräte</summary>
|
|
|
|
|
<div id="rest"></div>
|
|
|
|
|
</details>
|
|
|
|
|
|
|
|
|
|
<template id="t-android">
|
|
|
|
|
<section data-os="android">
|
|
|
|
|
<h2>Android</h2>
|
|
|
|
|
<p><a class="button" href="/android/install">Auf Android installieren</a></p>
|
|
|
|
|
<p class="muted">Das Gerät bekommt ein eigenes WLAN-App-Passwort in deinem authentik-Konto; dort gelöscht, ist das Gerät aus dem WLAN.</p>
|
|
|
|
|
</section>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<template id="t-apple">
|
|
|
|
|
<section data-os="apple">
|
|
|
|
|
<h2>iPhone, iPad, Mac</h2>
|
|
|
|
|
<p><a class="button" href="/apple/install">Profil laden</a></p>
|
|
|
|
|
<ol>
|
|
|
|
|
<li>iPhone/iPad: Einstellungen → „Profil geladen“ → Installieren. Mac: Systemeinstellungen → Allgemein → Geräteverwaltung → Profil doppelklicken.</li>
|
|
|
|
|
<li>Das Gerät verbindet sich danach selbst mit „Renner UniFi“.</li>
|
|
|
|
|
</ol>
|
|
|
|
|
<p class="muted">Das Gerät bekommt ein eigenes WLAN-App-Passwort in deinem authentik-Konto; dort gelöscht, ist das Gerät aus dem WLAN.</p>
|
|
|
|
|
</section>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<template id="t-windows">
|
|
|
|
|
<section data-os="windows">
|
|
|
|
|
<h2>Windows</h2>
|
|
|
|
|
<p><a class="button" href="/renner-unifi-wlan.cmd">Einrichtungsdatei laden</a></p>
|
|
|
|
|
<ol>
|
|
|
|
|
<li>Die geladene Datei doppelklicken. Fragt Windows nach („Der Computer wurde durch Windows geschützt“): „Weitere Informationen“ → „Trotzdem ausführen“.</li>
|
|
|
|
|
<li>Beim Verbinden Benutzername <strong class="u">…</strong> und dein Passwort eingeben.</li>
|
|
|
|
|
</ol>
|
|
|
|
|
</section>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<template id="t-linux">
|
|
|
|
|
<section data-os="linux">
|
|
|
|
|
<h2>Linux (NetworkManager)</h2>
|
|
|
|
|
<p>Im Terminal ausführen, danach fragt NetworkManager nach dem Passwort:</p>
|
|
|
|
|
<pre id="nmcli"></pre>
|
|
|
|
|
<p><button type="button" id="copy-nmcli">Befehl kopieren</button></p>
|
|
|
|
|
</section>
|
|
|
|
|
</template>
|
|
|
|
|
</main>
|
|
|
|
|
<script>
|
|
|
|
|
(function () {
|
|
|
|
|
var ua = navigator.userAgent;
|
|
|
|
|
var os = "other";
|
|
|
|
|
if (/Android/i.test(ua)) os = "android";
|
|
|
|
|
else if (/iPhone|iPad|iPod|Macintosh/i.test(ua)) os = "apple";
|
|
|
|
|
else if (/Windows/i.test(ua)) os = "windows";
|
|
|
|
|
else if (/Linux|X11/i.test(ua)) os = "linux";
|
|
|
|
|
|
|
|
|
|
var detected = document.getElementById("detected");
|
|
|
|
|
var rest = document.getElementById("rest");
|
|
|
|
|
["android", "apple", "windows", "linux"].forEach(function (name) {
|
|
|
|
|
var node = document.getElementById("t-" + name).content.cloneNode(true);
|
|
|
|
|
if (name === os) { node.querySelector("section").classList.add("current"); detected.appendChild(node); }
|
|
|
|
|
else rest.appendChild(node);
|
|
|
|
|
});
|
|
|
|
|
if (os === "other") document.getElementById("others").open = true;
|
|
|
|
|
|
|
|
|
|
var currentUser = "";
|
|
|
|
|
function quote(s) { return "'" + s.replace(/'/g, "'\\''") + "'"; }
|
|
|
|
|
function render(user) {
|
|
|
|
|
currentUser = user;
|
|
|
|
|
var shown = user || "(dein Benutzername)";
|
|
|
|
|
document.getElementById("user").textContent = shown;
|
|
|
|
|
document.querySelectorAll(".u").forEach(function (el) { el.textContent = shown; });
|
|
|
|
|
document.getElementById("nmcli").textContent =
|
|
|
|
|
"nmcli connection add type wifi con-name 'Renner UniFi' ssid 'Renner UniFi' " +
|
|
|
|
|
"wifi-sec.key-mgmt wpa-eap 802-1x.eap ttls 802-1x.phase2-auth pap " +
|
|
|
|
|
"802-1x.anonymous-identity anonymous 802-1x.identity " + quote(user || "BENUTZERNAME") + " " +
|
|
|
|
|
"802-1x.domain-suffix-match radius.renner.uno 802-1x.system-ca-certs yes " +
|
|
|
|
|
"802-1x.password-flags 2 && nmcli --ask connection up 'Renner UniFi'";
|
|
|
|
|
}
|
|
|
|
|
render("");
|
|
|
|
|
fetch("/whoami", { credentials: "same-origin" })
|
|
|
|
|
.then(function (r) { return r.ok ? r.text() : ""; })
|
|
|
|
|
.then(function (t) { render(t.trim()); })
|
|
|
|
|
.catch(function () {});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById("copy-nmcli").addEventListener("click", function () {
|
|
|
|
|
var text = document.getElementById("nmcli").textContent;
|
|
|
|
|
var btn = this;
|
|
|
|
|
navigator.clipboard.writeText(text).then(function () {
|
|
|
|
|
btn.textContent = "Kopiert";
|
|
|
|
|
setTimeout(function () { btn.textContent = "Befehl kopieren"; }, 2000);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
})();
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|