rlm_rest: update attributes on http/401 response?
Hi all, we are using the rlm_rest module to make authentication decisions. we would like to pass the rejection reason our REST API came up with back to FreeRadius for logging with rlm_sqllog. We managed to pass attributes back when returning a JSON payload with http/200 (accept), but it doesn't work for http/401 (reject). Is there a way during a reject to signal some log message from the REST API back to freeradius (like it works during accept)? I've noticed that the documentation says the following: # Authorize/Authenticate # # Code Meaning Process body Module code # 401 unauthorized yes reject # 2xx successful yes ok/updated but also: # If the body is processed and found to contain attribute updated will be returned, # except in the case of a 401 code. As far as I understand, "Process body = yes" should mean that attributes are updated, except for the case of 401, where this doesn't happen. Here's a rough sketch of our setup and what we tried: We have configured a REST request in the authorize and post-auth sections. When a client is allowed into the network, a http/200 message with this payload (and corresponding content-type header) is returned from our REST API: { "Extreme-Netlogin-Extended-Vlan":"UvlanWhatever", "control:mycompany-vlan":"vlanWhatever" } FreeRADIUS correctly parses this and adds the VLAN tag in the response headed to the NAS, and the "control:" are available for use, like in the snippet below. server default { post-auth { if (&control:mycompany-vlan == "vlanSpecialCase") { update control { Auth-Type := Accept mycompany-reject-reason := "none" } } else { eap } } } we save some extra detail in `mycompany-reject-reason`. This is then stored in our logs. That works beautifully. However, when a client is rejected, we would like to also update `mycompany-reject-reason` but with the rejection message coming from the REST call, which returns a http/401 like this: { "control:mycompany-reason": "why the client was rejected" } we have tried to then update the `mycompany-reason` unlang attribute: server default { post-auth { Post-Auth-Type REJECT { update control { mycompany-reject-reason := &control:mycompany-reason } } } However, this does not work: no attributes are updated, and this gets pritned to the logs: Tue Mar 24 15:07:29 2026 : ERROR: (822634) rest: ERROR: Server returned: Tue Mar 24 15:07:29 2026 : ERROR: (822634) rest: ERROR: {"control:mycompany-reason":"the failure message"} Tue Mar 24 15:07:29 2026 : Auth: (822634) Rejected in post-auth: [user@example.com/<via Auth-Type = eap>] (from client nas01.example.com port 0 cli aabbccddeeff)
On Mar 25, 2026, at 4:52 AM, GIRSTMAIR Tobias via Freeradius-Users <freeradius-users@lists.freeradius.org> wrote:
we are using the rlm_rest module to make authentication decisions. we would like to pass the rejection reason our REST API came up with back to FreeRadius for logging with rlm_sqllog. We managed to pass attributes back when returning a JSON payload with http/200 (accept), but it doesn't work for http/401 (reject).
Yes.
Is there a way during a reject to signal some log message from the REST API back to freeradius (like it works during accept)?
You'll have to change the source code to add that.
I've noticed that the documentation says the following:
# Authorize/Authenticate # # Code Meaning Process body Module code # 401 unauthorized yes reject # 2xx successful yes ok/updated
but also:
# If the body is processed and found to contain attribute updated will be returned, # except in the case of a 401 code.
Hm... the code looks like it processes the response and adds attributes for the 401 case. But I haven't tried it in detail myself.
However, when a client is rejected, we would like to also update `mycompany-reject-reason` but with the rejection message coming from the REST call, which returns a http/401 like this:
{ "control:mycompany-reason": "why the client was rejected" }
That sounds useful, yes.
However, this does not work: no attributes are updated, and this gets pritned to the logs:
Perhaps try running the server in debug mode, as suggested pretty much everywhere. Set up a test system. VMs are free.
Tue Mar 24 15:07:29 2026 : ERROR: (822634) rest: ERROR: Server returned: Tue Mar 24 15:07:29 2026 : ERROR: (822634) rest: ERROR: {"control:mycompany-reason":"the failure message"}
That looks like it's at least seeing the json message.
Tue Mar 24 15:07:29 2026 : Auth: (822634) Rejected in post-auth: [user@example.com/<via Auth-Type = eap>] (from client nas01.example.com port 0 cli aabbccddeeff)
Since you're not looking at the debug output, you won't be able to see what's happening in the "post-auth" section. Run the server in debug mode. As ALL of the documentation says, it's the best way to solve these issues. Alan DeKok.
Hi! On Wed, 2026-03-25 at 11:48 +1300, Alan DeKok wrote:
Hm... the code looks like it processes the response and adds attributes for the 401 case. But I haven't tried it in detail myself.
That was our original understanding from reading the table[1] in the documentation as well. But while researching why this doesn't work, I noticed that apparent exception noted in [2]. [1]: https://www.freeradius.org/documentation/freeradius-server/4.0.0/reference/r... [2]: https://www.freeradius.org/documentation/freeradius-server/4.0.0/reference/r...
However, when a client is rejected, we would like to also update `mycompany-reject-reason` but with the rejection message coming from the REST call, which returns a http/401 like this:
{ "control:mycompany-reason": "why the client was rejected" }
That sounds useful, yes.
However, this does not work: no attributes are updated, and this gets pritned to the logs:
Perhaps try running the server in debug mode, as suggested pretty much everywhere. Set up a test system. VMs are free.
Sorry, should have done that at the start. I attached a rejection trace at the end. I believe the relevant parts are these: (14) # Executing section post-auth from file /etc/raddb/sites-enabled/default (14) post-auth { ... rlm_rest (rest): Reserved connection (0) (14) rest: Expanding URI components (14) rest: Sending HTTP POST to "http://localhost:5001/api/postauth/cert/user%40example.com?nas=10.11.12.13&c..." (14) rest: Processing response header (14) rest: Status : 401 (UNAUTHORIZED) (14) rest: Type : json (application/json) (14) rest: Adding reply:REST-HTTP-Status-Code = "401" (14) rest: ERROR: Server returned: (14) rest: ERROR: {"control:mycompany-reason":"Unknown CN user@example.com"} rlm_rest (rest): Released connection (0) (14) [rest] = invalid ... (14) Using Post-Auth-Type Reject (14) # Executing group from file /etc/raddb/sites-enabled/default (14) Post-Auth-Type REJECT { (14) update { (14) No attributes updated for RHS &control:mycompany-reason (14) } # update = noop The "ERROR: Server returned:" message looks like Freeradius did not expect any payload for a 401 :/ Thanks, tobi. The full log starts here: (14) Received Access-Request Id 113 from 10.11.12.13:53424 to 10.10.10.10:1812 length 201 (14) Service-Type = Framed-User (14) Framed-MTU = 1400 (14) User-Name = "anon@example.com" (14) State = 0x26125fd92b1d52e97be8526948788de2 (14) NAS-Port-Id = "1" (14) NAS-Port-Type = Wireless-802.11 (14) Acct-Session-Id = "82d000b3" (14) Calling-Station-Id = "AA-BB-CC-DD-EE-FF" (14) Called-Station-Id = "FF-EE-DD-CC-BB-AA" (14) EAP-Message = 0x020f00060d00 (14) NAS-Identifier = "nas01.example.org" (14) NAS-IP-Address = 10.11.12.13 (14) Message-Authenticator = 0x591b844ab0ca0b6cc3e43ce71146f962 (14) Restoring &session-state (14) &session-state:Framed-MTU = 994 (14) &session-state:TLS-Session-Information = "(TLS) recv TLS 1.3 Handshake, ClientHello" (14) &session-state:TLS-Session-Information = "(TLS) send TLS 1.2 Handshake, ServerHello" (14) &session-state:TLS-Session-Information = "(TLS) send TLS 1.2 Handshake, Certificate" (14) &session-state:TLS-Session-Information = "(TLS) send TLS 1.2 Handshake, ServerKeyExchange" (14) &session-state:TLS-Session-Information = "(TLS) send TLS 1.2 Handshake, CertificateRequest" (14) &session-state:TLS-Session-Information = "(TLS) send TLS 1.2 Handshake, ServerHelloDone" (14) &session-state:TLS-Session-Information = "(TLS) recv TLS 1.2 Handshake, Certificate" (14) &session-state:TLS-Session-Information = "(TLS) recv TLS 1.2 Handshake, ClientKeyExchange" (14) &session-state:TLS-Session-Information = "(TLS) recv TLS 1.2 Handshake, CertificateVerify" (14) &session-state:TLS-Session-Information = "(TLS) recv TLS 1.2 Handshake, Finished" (14) &session-state:TLS-Session-Information = "(TLS) send TLS 1.2 ChangeCipherSpec" (14) &session-state:TLS-Session-Information = "(TLS) send TLS 1.2 Handshake, Finished" (14) &session-state:TLS-Session-Cipher-Suite = "ECDHE-RSA-AES128-GCM-SHA256" (14) &session-state:TLS-Session-Version = "TLS 1.2" (14) # Executing section authorize from file /etc/raddb/sites-enabled/default (14) authorize { (14) policy filter_username { (14) if (&User-Name) { (14) if (&User-Name) -> TRUE (14) if (&User-Name) { (14) if (&User-Name =~ / /) { (14) if (&User-Name =~ / /) -> FALSE (14) if (&User-Name =~ /@[^@]*@/ ) { (14) if (&User-Name =~ /@[^@]*@/ ) -> FALSE (14) if (&User-Name =~ /\.\./ ) { (14) if (&User-Name =~ /\.\./ ) -> FALSE (14) if ((&User-Name =~ /@/) && (&User-Name !~ /@(.+)\.(.+)$/)) { (14) if ((&User-Name =~ /@/) && (&User-Name !~ /@(.+)\.(.+)$/)) -> FALSE (14) if (&User-Name =~ /\.$/) { (14) if (&User-Name =~ /\.$/) -> FALSE (14) if (&User-Name =~ /@\./) { (14) if (&User-Name =~ /@\./) -> FALSE (14) } # if (&User-Name) = notfound (14) } # policy filter_username = notfound (14) suffix: Checking for suffix after "@" (14) suffix: Looking up realm "example.com" for User-Name = "anon@example.com" (14) suffix: Found realm "example.com" (14) suffix: Adding Stripped-User-Name = "anon" (14) suffix: Adding Realm = "example.com" (14) suffix: Authentication realm is LOCAL (14) [suffix] = ok (14) if (!&Realm) { (14) if (!&Realm) -> FALSE (14) eap: Peer sent EAP Response (code 2) ID 15 length 6 (14) eap: No EAP Start, assuming it's an on-going EAP conversation (14) [eap] = updated (14) } # authorize = updated (14) Found Auth-Type = eap (14) # Executing group from file /etc/raddb/sites-enabled/default (14) Auth-Type eap { (14) eap: Expiring EAP session with state 0x26125fd92b1d52e9 (14) eap: Finished EAP session with state 0x26125fd92b1d52e9 (14) eap: Previous EAP request found for state 0x26125fd92b1d52e9, released from the list (14) eap: Peer sent packet with method EAP TLS (13) (14) eap: Calling submodule eap_tls to process data (14) eap_tls: (TLS) Peer ACKed our handshake fragment. handshake is finished (14) eap: Sending EAP Success (code 3) ID 15 length 4 (14) eap: Freeing handler (14) [eap] = ok (14) if (handled && (Response-Packet-Type == Access-Challenge)) { (14) if (handled && (Response-Packet-Type == Access-Challenge)) -> FALSE (14) } # Auth-Type eap = ok (14) # Executing section post-auth from file /etc/raddb/sites-enabled/default (14) post-auth { (14) update { (14) session-state:mycompany-Outer-Name := &User-Name -> 'anon@example.com' (14) } # update = noop (14) if (&control:Proxy-To-Realm) { (14) if (&control:Proxy-To-Realm) -> FALSE (14) else { (14) if (EAP-Message) { (14) if (EAP-Message) -> TRUE (14) if (EAP-Message) { (14) if (EAP-Type == TLS) { (14) if (EAP-Type == TLS) -> TRUE (14) if (EAP-Type == TLS) { (14) update { (14) Stripped-User-Name := &TLS-Client-Cert-Common-Name -> 'user@example.com' (14) User-Name := &TLS-Client-Cert-Common-Name -> 'user@example.com' (14) session-state:mycompany-Common-Name := &TLS-Client-Cert-Common-Name -> 'user@example.com' (14) session-state:mycompany-Auth-Method := "cert" (14) } # update = noop (14) update control { (14) &mycompany-Rest-Type := "cert" (14) } # update control = noop rlm_rest (rest): Reserved connection (0) (14) rest: Expanding URI components (14) rest: EXPAND http://localhost:5001 (14) rest: --> http://localhost:5001 (14) rest: EXPAND /api/postauth/%{control:mycompany-Rest-Type}/%{Stripped-User-Name}?nas=%{Packet-Src-IP-Address}&calledStationId=%{Called-Station-Id} (14) rest: --> /api/postauth/cert/user%40example.com?nas=10.11.12.13&calledStationId=FF-EE-DD-CC-BB-AA (14) rest: Sending HTTP POST to "http://localhost:5001/api/postauth/cert/user%40example.com?nas=10.11.12.13&c..." (14) rest: EXPAND restapiuser (14) rest: --> restapiuser (14) rest: EXPAND restapipassword (14) rest: --> restapipassword (14) rest: Processing response header (14) rest: Status : 401 (UNAUTHORIZED) (14) rest: Type : json (application/json) (14) rest: Adding reply:REST-HTTP-Status-Code = "401" (14) rest: ERROR: Server returned: (14) rest: ERROR: {"control:mycompany-reason":"Unknown CN user@example.com"} rlm_rest (rest): Released connection (0) (14) [rest] = invalid (14) } # if (EAP-Type == TLS) = invalid (14) } # if (EAP-Message) = invalid (14) } # else = invalid (14) } # post-auth = invalid (14) Using Post-Auth-Type Reject (14) # Executing group from file /etc/raddb/sites-enabled/default (14) Post-Auth-Type REJECT { (14) if (&control:Proxy-To-Realm) { (14) if (&control:Proxy-To-Realm) -> FALSE (14) update { (14) No attributes updated for RHS &control:mycompany-reason (14) session-state:mycompany-Outer-Name := &User-Name -> 'user@example.com' (14) } # update = noop (14) if (EAP-Message && !&session-state:mycompany-reason) { (14) if (EAP-Message && !&session-state:mycompany-reason) -> TRUE (14) if (EAP-Message && !&session-state:mycompany-reason) { (14) update { (14) session-state:mycompany-reason := &Module-Failure-Message -> 'rest: Server returned:' (14) } # update = noop (14) } # if (EAP-Message && !&session-state:mycompany-reason) = noop (14) policy rewrite_calling_station_id { (14) if (&Calling-Station-Id && (&Calling-Station-Id =~ /^([0-9a-f]{2})[^0-9a-f]?([0-9a-f]{2})[^0-9a-f]?([0-9a-f]{2})[^0-9a-f]?([0-9a-f]{2})[^0-9a-f]?([0-9a-f]{2})[^0-9a-f]?([0-9a-f]{2})$/i)) { (14) if (&Calling-Station-Id && (&Calling-Station-Id =~ /^([0-9a-f]{2})[^0-9a-f]?([0-9a-f]{2})[^0-9a-f]?([0-9a-f]{2})[^0-9a-f]?([0-9a-f]{2})[^0-9a-f]?([0-9a-f]{2})[^0-9a-f]?([0-9a-f]{2})$/i)) -> TRUE (14) if (&Calling-Station-Id && (&Calling-Station-Id =~ /^([0-9a-f]{2})[^0-9a-f]?([0-9a-f]{2})[^0-9a-f]?([0-9a-f]{2})[^0-9a-f]?([0-9a-f]{2})[^0-9a-f]?([0-9a-f]{2})[^0-9a-f]?([0-9a-f]{2})$/i)) { (14) update request { (14) EXPAND %{toupper:%{1}%{2}%{3}%{4}%{5}%{6}} (14) --> 3A0412A35FD6 (14) &Calling-Station-Id := 3A0412A35FD6 (14) } # update request = noop (14) [updated] = updated (14) } # if (&Calling-Station-Id && (&Calling-Station-Id =~ /^([0-9a-f]{2})[^0-9a-f]?([0-9a-f]{2})[^0-9a-f]?([0-9a-f]{2})[^0-9a-f]?([0-9a-f]{2})[^0-9a-f]?([0-9a-f]{2})[^0-9a-f]?([0-9a-f]{2})$/i)) = updated (14) ... skipping else: Preceding "if" was taken (14) } # policy rewrite_calling_station_id = updated (14) attr_filter.access_reject: EXPAND %{User-Name} (14) attr_filter.access_reject: --> user@example.com (14) attr_filter.access_reject: Matched entry DEFAULT at line 11 (14) [attr_filter.access_reject] = updated (14) } # Post-Auth-Type REJECT = updated (14) Rejected in post-auth: [user@example.com/<via Auth-Type = eap>] (from client nas01.example.com port 0 cli AABBCCDDEEFF) (14) Login incorrect (rest: Server returned:): [user@example.com/<via Auth-Type = eap>] (from client nas01.example.com port 0 cli AABBCCDDEEFF) (14) Sent Access-Reject Id 113 from 10.10.10.10:1812 to 10.11.12.13:53424 length 44 (14) EAP-Message = 0x030f0004 (14) Message-Authenticator = 0x00000000000000000000000000000000 (14) Finished request
On 25 Mar 2026 at 4:52:09 AM, GIRSTMAIR Tobias via Freeradius-Users < freeradius-users@lists.freeradius.org> wrote:
Hi all,
However, this does not work: no attributes are updated, and this gets pritned to the logs:
Tue Mar 24 15:07:29 2026 : ERROR: (822634) rest: ERROR: Server returned: Tue Mar 24 15:07:29 2026 : ERROR: (822634) rest: ERROR: {"control:mycompany-reason":"the failure message"} Tue Mar 24 15:07:29 2026 : Auth: (822634) Rejected in post-auth: [user@example.com/<via Auth-Type = eap>] (from client nas01.example.com port 0 cli aabbccddeeff)
Does the 401 have a Content-Type: application/json header? -- Nathan Ward
participants (3)
-
Alan DeKok -
GIRSTMAIR Tobias -
Nathan Ward