|
| 1 | +--- |
| 2 | +updated_at: September 08, 2025 |
| 3 | +title: "Wi-Fi Authentication Webhooks" |
| 4 | +html_title: "Wi-Fi Authentication Webhooks" |
| 5 | +description: Smallstep's RADIUS server can call external webhooks for EAP-TLS authorization decisions. |
| 6 | +--- |
| 7 | + |
| 8 | +> This feature is available to Smallstep Enterprise RADIUS customers. |
| 9 | +
|
| 10 | +## Overview |
| 11 | + |
| 12 | +With Wi-Fi authentication webhooks, you can integrate Smallstep’s RADIUS authentication workflow with your own device posture or authorization checks during EAP-TLS Wi-Fi connection requests. All you need is a webhook server that Smallstep can reach out to. Your webhook server will evaluate or log the presented client certificate, and return an authorization decision. |
| 13 | + |
| 14 | +Smallstep can authenticate to your webhook server using a bearer token or HTTP basic authentication. |
| 15 | + |
| 16 | +## Configuring a RADIUS Webhook in Smallstep |
| 17 | + |
| 18 | +Our [customer support team](https://support.smallstep.com/kb-tickets/new) can configure a new RADIUS webhook for you. |
| 19 | + |
| 20 | +## RADIUS Webhook specification |
| 21 | + |
| 22 | +Your webhook server should use a TLS server certificate issued by a public Web PKI CA. |
| 23 | + |
| 24 | +### Request format |
| 25 | + |
| 26 | +Your webhook server should expect the following request format: |
| 27 | + |
| 28 | +- Method: `POST` |
| 29 | +- Content-Type: `application/json` |
| 30 | +- Headers: |
| 31 | + - `X-Smallstep-Webhook-ID:` A UUID for the RADIUS webhook making the request |
| 32 | + - `X-Smallstep-Signature:` Hex‑encoded HMAC‑SHA256 of the raw request body using the webhook’s signing secret |
| 33 | + - `Authorization:` Optional. Either "Bearer <token>" or HTTP Basic auth, if configured. |
| 34 | +- Body (JSON): |
| 35 | + - `timestamp`: The RFC8222 timestamp of the request |
| 36 | + - `x509Certificate`: A JSON representation of the certificate that follows [this data structure](https://github.com/smallstep/crypto/blob/master/x509util/certificate.go#L17). Additionally, there is a `raw` field containing a base64-encoded DER representation of the client certificate. |
| 37 | + |
| 38 | +Example request body: |
| 39 | + |
| 40 | +```json |
| 41 | +{ |
| 42 | + "timestamp": "2024-01-15T10:30:00Z", |
| 43 | + "x509Certificate": { |
| 44 | + "subject": { |
| 45 | + "country": ["US"], |
| 46 | + "organization": ["Example Corp"], |
| 47 | + "organizationalUnit": ["Engineering"], |
| 48 | + "locality": ["San Francisco"], |
| 49 | + "province": ["CA"], |
| 50 | + "streetAddress": ["123 Main St"], |
| 51 | + "postalCode": ["94105"], |
| 52 | + "serialNumber": "123456", |
| 53 | + "commonName": "[email protected]", |
| 54 | + "names": [ |
| 55 | + { |
| 56 | + "type": "2.5.4.3", |
| 57 | + |
| 58 | + } |
| 59 | + ], |
| 60 | + "extraNames": [] |
| 61 | + }, |
| 62 | + "issuer": { |
| 63 | + "country": ["US"], |
| 64 | + "organization": ["Example CA"], |
| 65 | + "organizationalUnit": ["CA Unit"], |
| 66 | + "locality": ["San Francisco"], |
| 67 | + "province": ["CA"], |
| 68 | + "streetAddress": ["456 CA St"], |
| 69 | + "postalCode": ["94105"], |
| 70 | + "serialNumber": "CA123", |
| 71 | + "commonName": "Example Root CA", |
| 72 | + "names": [], |
| 73 | + "extraNames": [] |
| 74 | + }, |
| 75 | + "serialNumber": "270390854734985720984572058347298347234", |
| 76 | + "sans": [ |
| 77 | + { |
| 78 | + "type": "email", |
| 79 | + |
| 80 | + } |
| 81 | + ], |
| 82 | + "emailAddresses": [ "[email protected]"], |
| 83 | + "ipAddresses": [], |
| 84 | + "uris": [], |
| 85 | + "extensions": [], |
| 86 | + "keyUsage": ["digitalSignature", "keyEncipherment"], |
| 87 | + "extKeyUsage": ["serverAuth", "clientAuth"], |
| 88 | + "unknownExtKeyUsage": [], |
| 89 | + "subjectKeyId": "base64EncodedSKID==", |
| 90 | + "authorityKeyId": "base64EncodedAKID==", |
| 91 | + "ocspServer": ["http://ocsp.example.com"], |
| 92 | + "issuingCertificateURL": ["http://ca.example.com/ca.crt"], |
| 93 | + "dnsNames": ["example.com", "www.example.com"], |
| 94 | + "permittedDNSDomainsCritical": false, |
| 95 | + "permittedDNSDomains": [], |
| 96 | + "excludedDNSDomains": [], |
| 97 | + "permittedIPRanges": [], |
| 98 | + "excludedIPRanges": [], |
| 99 | + "permittedEmailAddresses": [], |
| 100 | + "excludedEmailAddresses": [], |
| 101 | + "permittedURIDomains": [], |
| 102 | + "excludedURIDomains": [], |
| 103 | + "crlDistributionPoints": ["http://crl.example.com/ca.crl"], |
| 104 | + "policyIdentifiers": [], |
| 105 | + "publicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...", |
| 106 | + "publicKeyAlgorithm": "RSA", |
| 107 | + "notBefore": "2024-01-01T00:00:00Z", |
| 108 | + "notAfter": "2025-01-01T00:00:00Z", |
| 109 | + "raw": "MIIDXTCCAkWgAwIBAgIJAKb..." |
| 110 | + } |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +### Signature verification |
| 115 | + |
| 116 | +For signature verification, you will need the signing secret associated with `X-Smallstep-Webhook-ID`, which was given to you when your Smallstep support representative configured the webhook on your behalf. To verify the signature: |
| 117 | + |
| 118 | +1. Compute HMAC‑SHA256 over the raw request body bytes |
| 119 | +2. Hex‑encode the result and compare to the `X-Smallstep-Signature:` request header value |
| 120 | + |
| 121 | +### Response format |
| 122 | + |
| 123 | +Your server should respond with the following: |
| 124 | + |
| 125 | +- Content-Type: `application/json` |
| 126 | +- HTTP status codes: |
| 127 | + - `200`: Webhook processed successfully |
| 128 | + - Anything else: Authorization will be denied by RADIUS |
| 129 | +- Body (JSON): |
| 130 | + - `allow`: boolean. Should the Wi-Fi client authentication request be allowed? |
| 131 | + |
| 132 | + Minimal success response: |
| 133 | + |
| 134 | + ```json |
| 135 | + { "allow": true } |
| 136 | + ``` |
| 137 | + |
| 138 | + - `error`: object (optional). If an error is passed, it will be visible in your Smallstep event log. |
| 139 | + |
| 140 | + Deny with reason: |
| 141 | + |
| 142 | + ```json |
| 143 | + { |
| 144 | + "allow": false, |
| 145 | + "error": { |
| 146 | + "message": "Device non-compliant with posture check", |
| 147 | + "code": "E1002" |
| 148 | + } |
| 149 | + } |
| 150 | + ``` |
| 151 | + |
| 152 | + |
| 153 | +## Example Code |
| 154 | + |
| 155 | +As a starting point for your implementation, Smallstep offers an [example RADIUS webhook server](https://github.com/smallstep/radius-webhooks/), written in Go. |
| 156 | + |
| 157 | +## Operational guidance |
| 158 | + |
| 159 | +- Multiple webhooks are supported. Webhooks are called after a client certificate is verified by Smallstep. They are called sequentially, but without any guarantee of order. |
| 160 | +- Timeouts (10 seconds) or non-`200` HTTP status codes result in a denial decision by Smallstep. Build for high availability and fast failover. Run at least two replicas behind a load balancer. |
| 161 | +- Smallstep may retry briefly if it receives a transient `5xx` HTTP status codes |
| 162 | +- Deny known‑bad cases using `"allow": false`; reserve non‑`200` HTTP status codes for unexpected failures. This will avoid incidental denies. |
| 163 | +- Store the signing secret securely. Rotate the secret by creating a new webhook, distributing its secret, then decommissioning the old one. |
| 164 | +- It is recommended that you log request IDs, the webhook ID, and your decision for auditing. If possible, avoid logging full certificates. |
0 commit comments