RP-initiated
1 TopicWe Heard You! R35 Brings Frictionless OIDC Logout and Richer Claims to NGINX Plus
Quick Overview Hello friends! NGINX Plus R35 ships four new directives in the built-in ngx_http_oidc_module - logout_uri, post_logout_uri, logout_token_hint, and userinfo. Together, they finally close some of the most common end-to-end OIDC gaps: a clean, standards-aligned RP-initiated logout and easy access to user profile claims. R35 adds a new http_auth_require_module with the auth_require directive so you can implement RBAC checks directly - no more auth_jwt_require + auth_jwt workaround from R34. auth_require isn’t OIDC-specific, you can use it anywhere. In this post, though, we’ll look at it briefly through an OIDC lens. Rather than drowning you in implementation minutiae or some code samples, we’ll walk the actual traffic flow step by step. That way, both admins and engineers can see exactly what’s on the wire and why it matters. What’s new logout_uri - A local path users hit to start logout. NGINX constructs the correct RP-initiated logout request to your IdP, attaching all required parameters for you. post_logout_uri - Where the IdP should send the user after a successful logout. Set it in NGINX and also allow it in your IdP application settings. logout_token_hint on|off - When on, NGINX adds id_token_hint=<JWT> to the IdP’s logout endpoint. Some IdPs (e.g., OneLogin) require this and will return HTTP 400 without it. For most providers, it’s optional. userinfo on|off - When on, NGINX automatically calls userinfo endpoint, fetches extended claims, and exposes them as $oidc_claim_* variables. You can also inspect the raw JSON via $oidc_userinfo. There’s also the new http_auth_require_module with the auth_require directive. It’s not OIDC‑specific, and you can use it anywhere, but in OIDC setups, it’s a straightforward way to implement RBAC directly against $oidc_claim_* claims without reaching for auth_jwt_require. Using OneLogin as the example In a previous article, we used Keycloak as the IdP. This time we’ll try a hosted provider, OneLogin - because it has a few behaviors that make the new features shine. Everything here applies to other providers with the usual small differences. Create an application: OpenID Connect (OIDC) -> Web Application. Sign-in Redirect URIs: https://demo.route443.dev/oidc_callback Sign-out Redirect URIs: https://demo.route443.dev/post_logout/ (Both must match what you configure in NGINX.) On the SSO tab, copy Client ID, Client Secret, and Issuer (typically https://<subdomain>.onelogin.com/oidc/2). On Assignments, grant yourself access, otherwise OneLogin will respond with access_denied after auth. You can refer to our deployment guide for OneLogin. Metadata sanity check Let’s fetch OneLogin’s metadata and note the endpoints that matter for our flow. By default, OneLogin publishes the OpenID Provider Configuration at: https://.onelogin.com/oidc/2/.well-known/openid-configuration curl https://<subdomain>.onelogin.com/oidc/2/.well-known/openid-configuration | jq Example (trimmed): { "issuer": "https://route443-dev.onelogin.com/oidc/2", "authorization_endpoint": "https://route443-dev.onelogin.com/oidc/2/auth", "token_endpoint": "https://route443-dev.onelogin.com/oidc/2/token", "userinfo_endpoint": "https://route443-dev.onelogin.com/oidc/2/me", "end_session_endpoint": "https://route443-dev.onelogin.com/oidc/2/logout" } Two important notes: For RP-initiated logout, NGINX only uses end_session_endpoint from metadata - you can’t override it in the config. If it’s missing, you won’t get a proper RP-initiated logout. If userinfo is on, NGINX will call userinfo_endpoint immediately after exchanging the code for tokens. If userinfo is unavailable, NGINX returns HTTP 500 to the client. Unlike some clients, this is not a soft failure, so if you enable userinfo, make sure that endpoint is up during login. A minimal, working R35 config http { resolver 1.1.1.1 valid=300s ipv4=on; oidc_provider onelogin { issuer https://route443-dev.onelogin.com/oidc/2; client_id 37e2eb90-...; client_secret 4aeca...; logout_uri /logout; post_logout_uri https://demo.route443.dev/post_logout/; logout_token_hint on; userinfo on; } server { listen 443 ssl; server_name demo.route443.dev; ssl_certificate ...; ssl_certificate_key ...; auth_oidc onelogin; proxy_set_header X-Sub $oidc_claim_sub; proxy_set_header X-Userinfo $oidc_userinfo; proxy_pass http://app; location /post_logout/ { return 200 "Arrivederci!\n"; default_type text/plain; } } upstream app { server 127.0.0.1:8080; } } Reload (nginx -s reload) and we’re ready to test. Quick note: on logout_uri the value (/logout) is a trigger, not a location you need to implement yourself. If your app exposes a “Sign out, %username%” link like /logout?user=foo, hitting that URL causes NGINX to perform RP-initiated logout against the IdP. Alternatively, your app can render a different link that points to wherever you’ve configured logout_uri. The key idea is: your app points to the local logout_uri and NGINX handles the IdP call. What userinfo on Does Under the Hood Open your app in a fresh browser session. On the first request, NGINX sees you’re unauthenticated (based on the session cookie) and redirects you to OneLogin. After the authorization code flow, we will now move to the Exchange Code -> Tokens step. We covered this process in detail in a previous article. Because userinfo on is enabled right after NGINX obtains and validates the token set, it calls the userinfo_endpoint from the metadata: NGINX -> OneLogin GET /oidc/2/me HTTP/1.1 Host: route443-dev.onelogin.com Connection: close Authorization: Bearer <access_token> OneLogin -> NGINX HTTP/1.1 200 OK Content-Type: application/json ... {"sub":"177988316","email":"user4@route443.dev","preferred_username":"user4","name":"user4"} NGINX parses the JSON and merges these claims into $oidc_claim_* . Userinfo claims override same-named claims from the id_token. In this example, $oidc_claim_email becomes user4@route443.dev. For troubleshooting, you can inspect the raw body via $oidc_userinfo variable. RP-Initiated Logout Alright, after authentication and authorization have successfully passed and the user has gained access to the application, let’s try signing out. To do this, we’ll open the following link in the browser: https://demo.route443.dev/logout?user=user4. Based on the logout_uri directive, NGINX will understand that it needs to initiate an RP-initiated logout and will redirect the user to the OneLogin provider’s end_session_endpoint, that is, to https://route443-dev.onelogin.com/oidc/2/logout, which we obtained from the metadata. At the same time, NGINX will add the id_token_hint parameter to the request, which contains the user’s ID token that we previously obtained. So the request will look like this: User Agent -> NGINX: HTTP GET /logout?user=user4 Host: demo.route443.dev Cookie: NGX_OIDC_SESSION=ae00b3f...; NGINX -> User Agent: HTTP/1.1 302 Found Location: https://route443-dev.onelogin.com/oidc/2/logout?client_id=37e2eb90...&id_token_hint=ey...&post_logout_redirect_uri=https://demo.route443.dev/post_logout/ Set-Cookie: NGX_OIDC_SESSION=; httponly; secure; path=/ Look closely at the redirect NGINX issues when you hit your local logout_uri. You’ll see it added id_token_hint=<JWT> before sending the browser to the IdP’s end_session_endpoint. That happens because you enabled logout_token_hint on in your NGINX config. With OneLogin this isn’t optional: omit the hint and you’ll be greeted by HTTP 400. With most other providers, the hint is optional, which is exactly why we don’t recommend turning it on unless your IdP demands it. This is the only request where NGINX puts the ID token on the wire in clear view of the user agent and intermediaries, so if you don’t need to expose it, don’t. There’s also UX nuance here. Some IdPs change behavior depending on whether id_token_hint is present. With the hint, you might see an explicit "Are you sure you want to sign out?" confirmation. Without it, the same provider might tear down the session immediately. Same endpoint, different feel. Know what your IdP does and choose intentionally. You’ll notice another parameter NGINX appends: post_logout_redirect_uri. That’s the return address after a successful logout - it must match what you configured in NGINX and what you allowed in the IdP app. In our example, it’s https://demo.route443.dev/post_logout/, which is exactly where the browser lands after OneLogin is done. Now, about the cookie that mysteriously vanishes. NGINX clears NGX_OIDC_SESSION right away. It does this defensively because it cannot predict what the IdP will do next - bounce you to a login page, show a confirmation screen, or even fail. Clearing the local session guarantees you won’t keep accidental access if the IdP misbehaves. Why does that matter? Imagine the provider fails to fully drop the server-side session state. On your next request back to the app, NGINX will dutifully send you to the IdP, the IdP will happily say “oh, you’re still good,” and you’ll be right back in the app with no fresh authentication. That’s not the logout story you want. The takeaway is simple: test RP-initiated logout meticulously with your provider, verify that the server-side session is killed, and only then call it done. In our happy-path run, the flow is pleasantly uneventful: NGINX redirects with id_token_hint (because OneLogin requires it) and post_logout_redirect_uri, OneLogin terminates the session, sends the browser back to /post_logout/, and the user gets their minimalist "Arrivederci!" confirmation. Clean in, clean out: User Agent -> OneLogin: HTTP GET /oidc/2/logout?client_id=37e2eb90...&id_token_hint=ey...&post_logout_redirect_uri=https://demo.route443.dev/post_logout/ Host: route443-dev.onelogin.com OneLogin -> User Agent: HTTP 302 Moved Temporarily Location: https://demo.route443.dev/post_logout/ Declarative RBAC OIDC in NGINX isn’t only about passing identity claims upstream for SSO, it’s also about using those claims to control who gets to which resource. In R34, we had to lean on auth_jwt_require with a little dance:: after a user authenticated, we’d re-feed the ID token to the auth_jwt module just so we could gate access on claims. It worked, but it added config noise, extra $jwt_* variables, and CPU (parsing the token on every request). R35 finally removes that crutch. The new auth_require directive lets us use OIDC claims directly in NGINX, no more "auth_jwt" workaround. The module itself isn’t tied to OIDC, you can pair it with any NGINX config, but with auth_oidc it gives you clean, declarative RBAC right in your config. We’ll keep it practical: imagine two areas in your app, /admin and /support. Admins should access /admin location, admins or folks with the support permission should see /support location. Here’s what that looks like in NGINX. map $oidc_claim_groups $is_admin { default 0; ~*(^|\s)admin(\s|$) 1; } map $oidc_claim_email $is_corp_user { default 0; ~*@example\.com$ 1; } # OR logic map "$is_admin$is_corp_user" $admin_or_corp { default 0; ~1 1; } server { # ... location /admin/ { auth_require $is_admin; proxy_pass http://app; } location /support/ { auth_require $admin_or_corp; proxy_pass http://app; } } In this example we keep things simple: $is_admin comes from the groups claim (we treat it as a space-delimited string) and $is_corp_user checks that the user’s email ends with @example.com. We then build a tiny OR with another map: if either flag is 1, $admin_or_corp becomes 1. From there, auth_require is straightforward - it allows access when the referenced variable is non-empty and not "0" and denies with HTTP 403 by default (you can override the status with error=<4xx|5xx>). Remember that listing multiple variables in a single auth_require is a logical AND, for OR, precompute a boolean with map as shown above. Wrap-Up OIDC improvements in R35 are a meaningful milestone for the module. NGINX Plus R35 lifts the OIDC client from "almost there" to nearly complete: a reliable RP‑initiated logout, first‑class userinfo integration, and a new auth_require for clean, declarative RBAC right in your configuration. We’re not done, though: we still plan to fill a few remaining gaps: Front‑Channel and Back‑Channel logouts, PKCE, and a handful of other niceties that should cover nearly all deployment requirements. Stay tuned!230Views1like0Comments