Skip to content

feat: deferred OIDC auth for unauthenticated authorize requests#5852

Open
Kalabint wants to merge 3 commits intotraccar:masterfrom
Kalabint:fix/oidc-pending-auth
Open

feat: deferred OIDC auth for unauthenticated authorize requests#5852
Kalabint wants to merge 3 commits intotraccar:masterfrom
Kalabint:fix/oidc-pending-auth

Conversation

@Kalabint
Copy link
Copy Markdown

/api/oidc/authorize without a session currently calls issueCode(userId=0).
Instead: park PKCE params server-side, set an httpOnly cookie, redirect to /.

Custom /authorize/resume endpoint issues the real code after the user
authenticates. No spec equivalent — the standard doesn't address deferred
auth for SPAs that intercept the authorize redirect.

Pair with traccar-web PR #1756

…ng state

When /api/oidc/authorize is called without an active session (getUserId()==0),
issuing an authorization code is not possible. Previously the endpoint would
proceed and issue a code with userId=0.

Instead, store the authorization request parameters in OidcSessionManager
under a random token (10-min TTL, single-use) and set an httpOnly cookie
(oidc_pending, scoped to /api/oidc). Redirect the browser to / so the
existing SPA login flow runs normally.

Add GET /api/oidc/authorize/resume: after a successful login, the client
calls this endpoint. If a pending authorization is found for the cookie,
consume it and issue a real code for the now-authenticated user, returning
{"location": "<redirect_uri>?code=...&state=..."} for the client to follow.
Returns 204 if no pending authorization exists (non-OIDC login path).

TOKEN_BYTES constant makes the entropy size explicit. putIfAbsent with retry
loop in storePending() makes the uniqueness requirement explicit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@tananaev
Copy link
Copy Markdown
Member

What is the use case exactly?

@Kalabint
Copy link
Copy Markdown
Author

This solves the 401 error from Suggestion 1

@tananaev
Copy link
Copy Markdown
Member

How does it work in a standard?

@Kalabint
Copy link
Copy Markdown
Author

Kalabint commented Apr 16, 2026

As far as I can tell, the OIDC Standard isn't defining this part, just the following:

3.1.2.3. Authorization Server Authenticates End-User

If the request is valid, the Authorization Server attempts to Authenticate the End-User or determines whether the End-User is Authenticated, depending upon the request parameter values used. The methods used by the Authorization Server to Authenticate the End-User (e.g., username and password, session cookies, etc.) are beyond the scope of this specification. An Authentication user interface MAY be displayed by the Authorization Server, depending upon the request parameter values used and the authentication methods used.

The Authorization Server MUST attempt to Authenticate the End-User in the following cases:

  • The End-User is not already Authenticated.
  • The Authentication Request contains the prompt parameter with the value login. In this case, the Authorization Server MUST reauthenticate the End-User even if the End-User is already authenticated.

The Authorization Server MUST NOT interact with the End-User in the following case:

  • The Authentication Request contains the prompt parameter with the value none. In this case, the Authorization Server MUST return an error if an End-User is not already Authenticated or could not be silently Authenticated.

When interacting with the End-User, the Authorization Server MUST employ appropriate measures against Cross-Site Request Forgery and Clickjacking as described in Sections 10.12 and 10.13 of OAuth 2.0 [RFC6749].

Source: https://openid.net/specs/openid-connect-core-1_0.html#Authenticates

I tested it with my own Mapping Application and Claude Connector.

@tananaev
Copy link
Copy Markdown
Member

Then why do we need /authorize/resume?

Replace server-side pending state (ConcurrentHashMap + /authorize/resume
endpoint) with a signed ES256 JWT set as an httpOnly, SameSite=Lax,
scheme-aware Secure cookie (oidc_auth_state).

When /api/oidc/authorize is called without an active session, PKCE
parameters are packed into the JWT and the browser is redirected to
/?return=/api/oidc/authorize. After login, LoginPage.jsx performs a
full-page navigation (window.location.href) to that URL. The authorize
endpoint detects the cookie, verifies the JWT, re-validates the client
and redirect URI, issues the code, and clears the cookie in the response.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Kalabint
Copy link
Copy Markdown
Author

No, we don't and its not a good way to do it. It seems I was coding up a custom solution, based on my own OIDC Traccar fixes, which influenced the path forward.

Reworked the whole to a stateless JWT Cookie based system (had a look how other IdP work), which can also work on Load balanced traccar system.

I hope this is better

@tananaev
Copy link
Copy Markdown
Member

This still looks overcomplicated. Why can't we just save values in the current session?

Traccar rotates the session ID on login, dropping any pre-login session
values. EC signing just to round-trip public query params is overkill.

Reworked to use the ?return URL to carry OIDC params through the login
redirect, removing the need for server-side state entirely.

- Remove createAuthRequest() and getAuthRequest() from OidcSessionManager
- Remove oidc_auth_state cookie from OidcResource
- Build ?return URL with full OIDC param set for post-login redirect

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Kalabint
Copy link
Copy Markdown
Author

Reworked to have it work without any cookies, and removed the need for server storage/logic entirely.

@QueryParam("code_challenge") String codeChallenge,
@QueryParam("code_challenge_method") String codeChallengeMethod,
@QueryParam("nonce") String nonce) {
@QueryParam("nonce") String nonce) throws GeneralSecurityException, JOSEException, StorageException {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need these?

throw new WebApplicationException(Response.Status.BAD_REQUEST);
}

if (getUserId() == 0) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please update description to explain how this implementation works.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants