Open
Conversation
Toolbox API comes with a basic oauth2 client. This commit sets-up details about two important oauth flows: - authorization flow, in which the user is sent to web page where an authorization code is generated which is exchanged for an access token. - details about token refresh endpoint where users can obtain a new access token and a new refresh token. A couple of important aspects: - the client app id is resolved in upstream - as well as the actual endpoints for authorization and token refresh - S256 is the only code challenge supported
…ation url OAuth endpoint `.well-known/oauth-authorization-server` provides metadata about the endpoint for dynamic client registration and supported response types. This commit adds support for deserializing these values.
OAuth allows programatic client registration for apps like Coder Toolbox via the DCR endpoint which requires a name for the client app, the requested scopes, redirect URI, etc... DCR replies back with a similar structure but in addition it returs two very important properties: client_id - a unique client identifier string and also a client_secret - a secret string value used by clients to authenticate to the token endpoint.
Code Toolbox plugin should protect against authorization code interception attacks by making use of the PKCE security extension which involves a cryptographically random string (128 characters) known as code verifier and a code challenge - derived from code verifier using the S256 challenge method.
The OAuth2-compatible authentication manager provided by Toolbox
- authentication and token endpoints are now passed via the login configuration object - similar for client_id and client_secret - PCKE is now enabled
…injection - remove ServiceLocator dependency from CoderToolboxContext - move OAuth manager creation to CoderToolboxExtension for cleaner separation - Refactor CoderOAuthManager to use configuration-based approach instead of constructor injection The idea behind these changes is that createRefreshConfig API does not receive a configuration object that can provide the client id and secret and even the refresh url. So initially we worked around the issue by passing the necessary data via the constructor. However this approach means a couple of things: - the actual auth manager can be created only at a very late stage, when a URL is provided by users - can't easily pass arround the auth manager without coupling the components - have to recreate a new auth manager instance if the user logs out and logs in to a different URL - service locator needs to be passed around because this is the actual factory of oauth managers in Toolbox Instead, we went with a differet approach, COderOAuthManager will derive and store the refresh configs once the authorization config is received. If the user logs out and logs in to a different URL the refresh data is also guaranteed to be updated. And on top of that - this approach allows us to get rid of all of the issues mentioned above.
Toolbox can handle automatically the exchange of an authorization code with a token by handling the custom URI for oauth. This commit calls the necessary API in the Coder Toolbox URI handling.
POST /api/v2/oauth2-provider/apps is actually for manual admin registration for admin created apps. Programmatic Dynamic Client Registration is done via `POST /oauth2/register`. At the same time I included `registration_access_token` and `registration_client_uri` to use it later in order to refresh the client secret without re-registering the client app.
A bunch of code thrown around to launch the OAuth flow. Still needs a couple of things: - persist the client id and registration uri and token - re-use client id instead of re-register every time - properly handle scenarios where OAuth is not available - the OAuth right now can be enabled if we log out and then hit next in the deployment screen
A new config `preferAuthViaApiToken` allows users to continue to use API tokens for authentication when OAuth2 is available on the Coder deployment.
Account implementation with logic to resolve the account once the token is retrieved. Marshalling logic for the account is also added. There is a limitation in the Toolbox API where createRefreshConfig is not receiving the auth params. We worked around by capturing and storing these params in the createAuthConfig but this is unreliable. Instead we use the account to pass the missing info around.
OAuth2 should be launched if user prefers is over any other method of auth and if only the server supports it.
Fallback on client_secret_basic or None depending on what the Coder server supports.
…n endpoint Based on the auth method type we need to send client id and client secret as a basic auth header or part of the body as an encoded url form
We encountered a couple of issues with the Toolbox API which is inflexible:
- we don't have complete control over which parameters are sent as query&body
- we don't have fully basic + headers + body logging for debugging purposes
- doesn't integrate that well with our existing http client used for polling
- spent more than a couple of hours trying to understand why Coder rejects the
authorization call with:
```
{"error":"invalid_request","error_description":"The request is missing required parameters or is otherwise malformed"} from Coder server.
```
Instead we will slowly discard the existing logic and rely on enhancements to our existing http client.
Basically, the login screen will try to first determine if mTLS auth is configured and use that, otherwise
it will check if the user wants to use OAuth over API token, if available. When the flag is
true then the login screen will query the Coder server to see if OAuth2 is supported.
If that is true then browser is launched pointing to the authentication URL. If not we will default to
the API token authentication.
…ization mTLS always takes precedence over OAuth and API token authorization, while API Token is the default method if mTLS is not used. But now user can prefer OAuth authorization over API tokens, if OAuth is available. The setting is now available in the Settings page, no longer in the main login screen as it was the case in the initial draft of the PR.
For easy navigation as there were too many of them, mixed with no logical differentiation. Now we cluster on a couple of topics: - General - Security & Authentication - CLI - SSH
Initially the OAuth implementation was trying to use the existing OAuth API/Client provided by Toolbox which is not very flexible.
jeremyruppel
reviewed
Mar 6, 2026
zedkipp
reviewed
Mar 7, 2026
The OAuth2 server implementation needs to provide an authorization code that can be exchanged for an access token. But in order to make sure the authorization code is for the "our" login request, the client provides a state value when launching the authorization URL which the OAuth2 server has to send back when with the auth code. This fix makes sure the authorization code is actually sent, and that the state value is the same as in our initial request.
This fix reports an error to the user when token exchange request is failing, or returning an empty body or a body that does not contain the token.
The logic for exchanging auth code to tokens, refreshing tokens was used in multiple places without any code reuse strategy. Extracted an OAuth service that handles the basic operations.
The metadata endpoint provide an absolute URL for the client registration endpoint which we should use instead of hardcoding the path relative to the base url.
ThomasK33
reviewed
Mar 17, 2026
| @field:Json(name = "scope") val scope: String, | ||
| @field:Json(name = "token_endpoint_auth_method") val tokenEndpointAuthMethod: TokenEndpointAuthMethod, | ||
| @field:Json(name = "client_id_issued_at") val clientIdIssuedAt: Long?, | ||
| @field:Json(name = "client_secret_expires_at") val clientSecretExpiresAt: Long?, |
Member
There was a problem hiding this comment.
This field should always be returned, so I think this doesn't need to be an optional?
client_secret_expires_at
REQUIRED if "client_secret" is issued. Time at which the client
secret will expire or 0 if it will not expire. The time is
represented as the number of seconds from 1970-01-01T00:00:00Z as
measured in UTC until the date/time of expiration.
Collaborator
Author
There was a problem hiding this comment.
This is how it is defined in the OAuth2ClientRegistrationResponse struct
ClientSecretExpiresAt int64 `json:"client_secret_expires_at,omitempty"`
https://datatracker.ietf.org/doc/html/rfc7591 normalizes the client registration error responses and forces providers to always include json with an error code and an error message. This patch captures the error response and builds a pretty message and displays it to the user.
RFC 6749 §4.1.2.1 + RFC 7636 §4.4.1 specify that the error code and optional error_description can be returned as a query params int the callback URI. Similarly, RFC 6749 §5.2 — the exchange of authorization codes to tokens can return a json body containing an error code and an error message that was never handled in our code.
This upgrade will need TBX 3.4 or higher to be installed. The upgrade is needed to benefit from the fixes related to displaying UI pages in the URI handler. In addition I reworked the main build.gradle and extracted everything into a small custom plugin.
Due to the dependency on the new API.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Recent versions of Coder act as an OAuth 2.1 authorization server for first- and third‑party applications.
This PR aims at providing support for authenticating via OAuth with Coder Toolbox and still retain backward compatibility for authentication via API tokens or via certificates.
This PR is a WIP: