[GH-ISSUE #1563] OIDC callback endpoint should use GET and query parameters, not POST with JSON body #7982

Open
opened 2026-03-12 23:30:00 +03:00 by kerem · 2 comments
Owner

Originally created by @Tom60chat on GitHub (Feb 16, 2026).
Original GitHub issue: https://github.com/0xJacky/nginx-ui/issues/1563

Made with Copilot (with humain supervision)

Describe the bug

The OIDC callback handler (OIDCCallback at /oidc_callback) is currently registered as a POST endpoint and attempts to parse a JSON body for the code and state parameters. This does not conform to the OAuth 2.0 (RFC 6749) and OpenID Connect specifications.

Problem details

  • The OAuth2/OIDC specification requires that the callback (redirect URI) is called via HTTP GET, with the code and state included as URL query parameters (not a JSON body, and usually not POST).
  • Most providers (Auth0, Google, Azure AD, etc.) will send the user back using a browser redirect with these values in the URL (GET), not as a POST request.
  • The current code fails to work with compliant providers unless custom response modes are enabled and code is adapted accordingly.

Relevant code (from api/user/oidc.go):

var loginUser OIDCLoginUser
ok := cosy.BindAndValid(c, &loginUser) // expects JSON body

Specification references:

To Reproduce

  1. Start OIDC login flow
  2. Complete authentication in the provider
  3. See failure upon callback if using a standard OIDC provider

Expected behavior

The callback endpoint should:

  • Be registered as a GET route (e.g., r.GET("/oidc_callback", OIDCCallback))
  • Extract code and state from query parameters (using c.Query("code") and c.Query("state") in Gin)
  • Not expect a JSON body

Additional context

If you want to support POST with form-encoded body (for providers using response_mode=form_post), handle that as a special case—but standard OIDC and OAuth2 providers use GET + query parameters.


Let me know if you'd like suggestions for updated code!

Originally created by @Tom60chat on GitHub (Feb 16, 2026). Original GitHub issue: https://github.com/0xJacky/nginx-ui/issues/1563 > Made with Copilot (with humain supervision) ### Describe the bug The OIDC callback handler (`OIDCCallback` at `/oidc_callback`) is currently registered as a POST endpoint and attempts to parse a JSON body for the `code` and `state` parameters. This does not conform to the OAuth 2.0 (RFC 6749) and OpenID Connect specifications. #### Problem details - The OAuth2/OIDC specification requires that the callback (redirect URI) is called via HTTP GET, with the `code` and `state` included as URL query parameters (not a JSON body, and usually not POST). - Most providers (Auth0, Google, Azure AD, etc.) will send the user back using a browser redirect with these values in the URL (GET), not as a POST request. - The current code fails to work with compliant providers unless custom response modes are enabled and code is adapted accordingly. #### Relevant code (from `api/user/oidc.go`): ```go var loginUser OIDCLoginUser ok := cosy.BindAndValid(c, &loginUser) // expects JSON body ``` #### Specification references: - [OAuth 2.0 RFC 6749 Section 4.1.2](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2) - [OpenID Connect Core Spec: Authorization Code Flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth) ### To Reproduce 1. Start OIDC login flow 2. Complete authentication in the provider 3. See failure upon callback if using a standard OIDC provider ### Expected behavior The callback endpoint should: - Be registered as a GET route (e.g., `r.GET("/oidc_callback", OIDCCallback)`) - Extract `code` and `state` from query parameters (using `c.Query("code")` and `c.Query("state")` in Gin) - Not expect a JSON body ### Additional context If you want to support POST with form-encoded body (for providers using `response_mode=form_post`), handle that as a special case—but standard OIDC and OAuth2 providers use GET + query parameters. --- Let me know if you'd like suggestions for updated code!
Author
Owner

@Jraaay commented on GitHub (Feb 17, 2026):

Thank you for the report, but I believe this is actually working as designed. The implementation follows a Frontend-Mediated Callback pattern, which is a standard and well-established approach in Single Page Applications (SPAs).

How the flow actually works

  1. The user clicks "OIDC Login" → the frontend calls GET /oidc_uri to get the authorization URL.
  2. The browser redirects to the OIDC provider via window.location.href.
  3. After authentication, the provider redirects back to the frontend SPA (Login.vue) via HTTP GET with query parameters (?code=xxx&state=yyy) — this is fully compliant with RFC 6749 Section 4.1.2.
  4. The frontend JavaScript extracts code and state from the URL query parameters (Login.vue#L214-L217), then sends them to the backend API via POST /oidc_callback as JSON.

The key point is: the redirect_uri registered with the OIDC provider points to the frontend SPA page, not the backend API endpoint. The OIDC provider does redirect via GET with query parameters — the frontend receives them correctly and then relays them to the backend through an API call.

Why this design is intentional

  • SPA standard practice: In SPA architectures, the redirect URI typically points to a frontend route. The frontend handles the redirect, extracts the authorization code, and sends it to the backend via an API call. This avoids issues with backend handling browser redirects, CORS, and routing in SPA contexts.
  • Consistent with existing SSO: The Casdoor SSO integration in the same project uses the exact same pattern (POST /casdoor_callback), ensuring architectural consistency.
  • Security benefit: Sending the authorization code via POST body rather than GET query parameters to the backend means the code won't appear in server access logs.

This is a widely used pattern — for example, libraries like oidc-client-js and Auth0 SPA SDK follow the same approach.

How to correctly configure OIDC

To use OIDC login with Nginx UI, add the following to your app.ini configuration file:

[oidc]
ClientId     = your-client-id
ClientSecret = your-client-secret
Endpoint     = https://your-oidc-provider.com
RedirectUri  = https://your-nginx-ui-domain.com
Scopes       = openid profile email
Identifier   =

Or via environment variables:

Setting Environment Variable
ClientId NGINX_UI_OIDC_CLIENT_ID
ClientSecret NGINX_UI_OIDC_CLIENT_SECRET
Endpoint NGINX_UI_OIDC_ENDPOINT
RedirectUri NGINX_UI_OIDC_REDIRECT_URI
Scopes NGINX_UI_OIDC_SCOPES
Identifier NGINX_UI_OIDC_IDENTIFIER

Important notes:

  • RedirectUri must point to the root URL of your Nginx UI instance (e.g., https://your-domain.com). This is also the URL you should register as the "Redirect URI" / "Callback URL" in your OIDC provider's application settings. Since Nginx UI uses hash-based routing (#/login), the frontend will correctly pick up code and state from the URL query parameters via window.location.search after the redirect.
  • Endpoint is the OIDC issuer URL. For example, https://accounts.google.com for Google, or https://your-tenant.auth0.com for Auth0.
  • Scopes defaults to openid profile email username. Customize only if your provider requires specific scopes.
  • Identifier is an optional claim name used to match the OIDC user to a local Nginx UI user. If left empty, the system will try username. The matched value must correspond to an existing username in Nginx UI — users are not auto-created.
<!-- gh-comment-id:3911532873 --> @Jraaay commented on GitHub (Feb 17, 2026): Thank you for the report, but I believe this is actually working as designed. The implementation follows a Frontend-Mediated Callback pattern, which is a standard and well-established approach in Single Page Applications (SPAs). ## How the flow actually works 1. The user clicks "OIDC Login" → the frontend calls GET /oidc_uri to get the authorization URL. 2. The browser redirects to the OIDC provider via window.location.href. 3. After authentication, the provider redirects back to the frontend SPA (Login.vue) via HTTP GET with query parameters (?code=xxx&state=yyy) — this is fully compliant with [RFC 6749 Section 4.1.2](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2). 4. The frontend JavaScript extracts code and state from the URL query parameters ([Login.vue#L214-L217](https://github.com/0xJacky/nginx-ui/blob/0e7ea571109fc714e6fdc9ba27685302a57368ad/app/src/views/other/Login.vue#L214-L217)), then sends them to the backend API via POST /oidc_callback as JSON. The key point is: the redirect_uri registered with the OIDC provider points to the frontend SPA page, not the backend API endpoint. The OIDC provider does redirect via GET with query parameters — the frontend receives them correctly and then relays them to the backend through an API call. ## Why this design is intentional - SPA standard practice: In SPA architectures, the redirect URI typically points to a frontend route. The frontend handles the redirect, extracts the authorization code, and sends it to the backend via an API call. This avoids issues with backend handling browser redirects, CORS, and routing in SPA contexts. - Consistent with existing SSO: The Casdoor SSO integration in the same project uses the exact same pattern (POST /casdoor_callback), ensuring architectural consistency. - Security benefit: Sending the authorization code via POST body rather than GET query parameters to the backend means the code won't appear in server access logs. This is a widely used pattern — for example, libraries like [oidc-client-js](https://github.com/IdentityModel/oidc-client-js) and [Auth0 SPA SDK](https://github.com/auth0/auth0-spa-js) follow the same approach. ## How to correctly configure OIDC To use OIDC login with Nginx UI, add the following to your app.ini configuration file: ```ini [oidc] ClientId = your-client-id ClientSecret = your-client-secret Endpoint = https://your-oidc-provider.com RedirectUri = https://your-nginx-ui-domain.com Scopes = openid profile email Identifier = ``` Or via environment variables: Setting | Environment Variable -- | -- ClientId | NGINX_UI_OIDC_CLIENT_ID ClientSecret | NGINX_UI_OIDC_CLIENT_SECRET Endpoint | NGINX_UI_OIDC_ENDPOINT RedirectUri | NGINX_UI_OIDC_REDIRECT_URI Scopes | NGINX_UI_OIDC_SCOPES Identifier | NGINX_UI_OIDC_IDENTIFIER ## Important notes: - `RedirectUri` must point to the root URL of your Nginx UI instance (e.g., `https://your-domain.com`). This is also the URL you should register as the "Redirect URI" / "Callback URL" in your OIDC provider's application settings. Since Nginx UI uses hash-based routing (`#/login`), the frontend will correctly pick up `code` and `state` from the URL query parameters via `window.location.search` after the redirect. - `Endpoint` is the OIDC issuer URL. For example, `https://accounts.google.com` for Google, or `https://your-tenant.auth0.com` for Auth0. - `Scopes` defaults to `openid profile email username`. Customize only if your provider requires specific scopes. - `Identifier` is an optional claim name used to match the OIDC user to a local Nginx UI user. If left empty, the system will try `username`. The matched value must correspond to an existing username in Nginx UI — users are not auto-created.
Author
Owner

@Tom60chat commented on GitHub (Feb 23, 2026):

Sorry for the multiple edits in this issue, GitHub Copilot messed up and I have to manually reverse... that's what I get using AI

Apologies for my earlier misunderstanding!

I was not aware that the Frontend-Mediated Callback pattern existed. With the limited documentation, I did my best to figure out how the OIDC authentication flow worked, but now that I understand the SPA pattern, it all makes sense. I'm sorry for any confusion my earlier report may have caused—this approach is indeed correct and working well.

I've also changed my previous comments in #1488 reflect this clarification.


However, I did notice a possible issue while setting up OIDC:

When the /oidc_callback endpoint returns a 403 error (for example, because the mapped user does not exist), the error message ("User not exist") is not shown in the login screen. Instead, it always displays "State cookie not found," even though that's not the real problem.

Expected:
If the OIDC login fails due to a missing user or another backend error, the frontend should show the actual error message returned by the backend (e.g., "User not exist"), so users know what went wrong.

Actual:
The frontend always reports "State cookie not found," which makes troubleshooting harder.

I can put that as a separate issue


Thank you for the clarification and for your work on this project! 😸

<!-- gh-comment-id:3946280305 --> @Tom60chat commented on GitHub (Feb 23, 2026): *Sorry for the multiple edits in this issue, GitHub Copilot messed up and I have to manually reverse... that's what I get using AI* **Apologies for my earlier misunderstanding!** I was not aware that the Frontend-Mediated Callback pattern existed. With the limited documentation, I did my best to figure out how the OIDC authentication flow worked, but now that I understand the SPA pattern, it all makes sense. I'm sorry for any confusion my earlier report may have caused—this approach is indeed correct and working well. I've also changed my previous comments in #1488 reflect this clarification. --- **However, I did notice a possible issue while setting up OIDC:** When the `/oidc_callback` endpoint returns a 403 error (for example, because the mapped user does not exist), the error message ("User not exist") is not shown in the login screen. Instead, it always displays "State cookie not found," even though that's not the real problem. **Expected:** If the OIDC login fails due to a missing user or another backend error, the frontend should show the actual error message returned by the backend (e.g., "User not exist"), so users know what went wrong. **Actual:** The frontend always reports "State cookie not found," which makes troubleshooting harder. > I can put that as a separate issue --- Thank you for the clarification and for your work on this project! 😸
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/nginx-ui#7982
No description provided.