Skip to content

Conversation

@cemalkilic
Copy link
Contributor

Summary

This PR completes the OAuth2 server implementation by adding the /token endpoint, enabling full OAuth2 authorization code flow & refresh token support.

Key Features Added:

OAuth Token Endpoint (POST /oauth/token) supporting:

  • authorization_code grant type for exchanging authorization codes for access
  • refresh_token grant type for token refresh
  • Both JSON and form-encoded request bodies
  • OAuth Client authentication via Basic auth or request body parameters (form params and JSON body)

Token Service Integration:

  • Integrated OAuth server with the existing token service
  • Added OAuth-specific authentication method (oauth_provider/authorization_code)
  • Enhanced token generation to include OAuth client context in JWT claims.

Database Changes:

  • Added oauth_client_id field to sessions table for OAuth client tracking. So an OAuth clients can use a refresh token only if the session is issued for them. Similarly, a session issued to a client can only be refreshed by that client (i.e user can't use /token?grant_type=refresh_token endpoint with a refresh token obtained through /oauth/token endpoint.)

Next Steps

  • Adding ratelimit for the /token endpoint
  • Store token auth method for oauth clients in the database

@cemalkilic cemalkilic requested a review from a team as a code owner September 8, 2025 15:50
@coveralls
Copy link

coveralls commented Sep 8, 2025

Pull Request Test Coverage Report for Build 17646441416

Details

  • 88 of 303 (29.04%) changed or added relevant lines in 13 files are covered.
  • 13 unchanged lines in 2 files lost coverage.
  • Overall coverage decreased (-0.8%) to 67.943%

Changes Missing Coverage Covered Lines Changed/Added Lines %
internal/api/middleware.go 0 2 0.0%
internal/models/errors.go 0 2 0.0%
internal/models/refresh_token.go 1 3 33.33%
internal/api/shared/context.go 8 12 66.67%
internal/models/factor.go 0 4 0.0%
internal/models/oauth_client.go 0 7 0.0%
internal/tokens/service.go 43 57 75.44%
internal/api/oauthserver/auth.go 0 24 0.0%
internal/api/oauthserver/handlers.go 10 166 6.02%
Files with Coverage Reduction New Missed Lines %
internal/api/oauthserver/auth.go 6 0.0%
internal/api/middleware.go 7 75.99%
Totals Coverage Status
Change from base Build 17459483364: -0.8%
Covered Lines: 12630
Relevant Lines: 18589

💛 - Coveralls

@cemalkilic cemalkilic changed the title feat(oauth2): add /token endpoint feat(oauth2): add /oauth/token endpoint Sep 8, 2025
Copy link
Contributor

@cstockton cstockton left a comment

Choose a reason for hiding this comment

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

Nothing jumps out at me, but I did see this in the handler.go that is a bit confusing to me. Given ClientSecret is not initialized I think it's essentially a NOOP. I think I understand the flow of the client secret, but can we remove the code / comment below to make it more clear?

func oauthServerClientToResponse(client *models.OAuthServerClient, includeSecret bool) *OAuthServerClientResponse {

// cstockton: ClientSecret is not set here

	response := &OAuthServerClientResponse{
		ClientID:   client.ID.String(),
		ClientType: client.ClientType,

		// OAuth 2.1 DCR fields
		RedirectURIs:            client.GetRedirectURIs(),
		TokenEndpointAuthMethod: tokenEndpointAuthMethods,
		GrantTypes:              client.GetGrantTypes(),
		ResponseTypes:           []string{"code"}, // Always "code" in OAuth 2.1
		ClientName:              utilities.StringValue(client.ClientName),
		ClientURI:               utilities.StringValue(client.ClientURI),
		LogoURI:                 utilities.StringValue(client.LogoURI),

		// Metadata fields
		RegistrationType: client.RegistrationType,
		CreatedAt:        client.CreatedAt,
		UpdatedAt:        client.UpdatedAt,
	}

	// Only include client_secret during registration and only for confidential clients
	if includeSecret && client.IsConfidential() {

// cstockton: So I don't entirely understand this, but would like to:

		// Note: This will be filled in by the handler with the plaintext secret
		response.ClientSecret = ""
	}

@cemalkilic
Copy link
Contributor Author

Nothing jumps out at me, but I did see this in the handler.go that is a bit confusing to me. Given ClientSecret is not initialized I think it's essentially a NOOP. I think I understand the flow of the client secret, but can we remove the code / comment below to make it more clear?

func oauthServerClientToResponse(client *models.OAuthServerClient, includeSecret bool) *OAuthServerClientResponse {

// cstockton: ClientSecret is not set here

	response := &OAuthServerClientResponse{
		ClientID:   client.ID.String(),
		ClientType: client.ClientType,

		// OAuth 2.1 DCR fields
		RedirectURIs:            client.GetRedirectURIs(),
		TokenEndpointAuthMethod: tokenEndpointAuthMethods,
		GrantTypes:              client.GetGrantTypes(),
		ResponseTypes:           []string{"code"}, // Always "code" in OAuth 2.1
		ClientName:              utilities.StringValue(client.ClientName),
		ClientURI:               utilities.StringValue(client.ClientURI),
		LogoURI:                 utilities.StringValue(client.LogoURI),

		// Metadata fields
		RegistrationType: client.RegistrationType,
		CreatedAt:        client.CreatedAt,
		UpdatedAt:        client.UpdatedAt,
	}

	// Only include client_secret during registration and only for confidential clients
	if includeSecret && client.IsConfidential() {

// cstockton: So I don't entirely understand this, but would like to:

		// Note: This will be filled in by the handler with the plaintext secret
		response.ClientSecret = ""
	}

Good catch, yeah this part is unnecessary. I added this part before implementing #2152, so we don't need anymore, removed the includeSecret param 👍

Copy link
Contributor

@hf hf left a comment

Choose a reason for hiding this comment

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

Please consider the index!

@cemalkilic cemalkilic merged commit a89a0b0 into master Sep 11, 2025
6 checks passed
@cemalkilic cemalkilic deleted the cemal/feat-add-oauth-token-endpoint branch September 11, 2025 14:36
cemalkilic pushed a commit that referenced this pull request Sep 23, 2025
🤖 I have created a release *beep* *boop*
---


##
[2.180.0](v2.179.0...v2.180.0)
(2025-09-23)


### Features

* add OAuth client type
([#2152](#2152))
([b118f1f](b118f1f))
* add phone to sms webhook payload
([#2160](#2160))
([d475ac1](d475ac1))
* background template reloading p1 - baseline decomposition
([#2148](#2148))
([746c937](746c937))
* config reloading with fsnotify, poller fallback, and signals
([#2161](#2161))
([c77d512](c77d512))
* enhance issuer URL validation in OAuth server metadata
([#2164](#2164))
([a9424d2](a9424d2))
* implement OAuth2 authorization endpoint
([#2107](#2107))
([5318552](5318552))
* **oauth2:** add `/oauth/token` endpoint
([#2159](#2159))
([a89a0b0](a89a0b0))
* **oauth2:** add admin endpoint to regenerate OAuth client secrets
([#2170](#2170))
([0bd1c28](0bd1c28))
* **oauth2:** return redirect_uri on GET authorization
([#2175](#2175))
([b0a0c3e](b0a0c3e))
* **oauth2:** use `id` field as the public client_id
([#2154](#2154))
([86b7de4](86b7de4))
* **openapi:** add OAuth 2.1 server endpoints and clarify OAuth modes
([#2165](#2165))
([1f804a2](1f804a2))
* password changed email notification
([#2176](#2176))
([fe0fd04](fe0fd04))
* support `transfer_sub` in apple id tokens
([#2162](#2162))
([8a71006](8a71006))


### Bug Fixes

* ensure request context exists in API db operations
([#2171](#2171))
([060a992](060a992))
* **makefile:** remove invalid @ symbol from shell commands
([#2168](#2168))
([e6afe45](e6afe45))
* **oauth2:** switch to Origin header for request validation
([#2174](#2174))
([42bc9ab](42bc9ab))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
issuedat pushed a commit that referenced this pull request Sep 30, 2025
## Summary
This PR completes the OAuth2 server implementation by adding the
`/token` endpoint, enabling full OAuth2 authorization code flow &
refresh token support.

## Key Features Added:
### OAuth Token Endpoint (POST /oauth/token) supporting:
- `authorization_code` grant type for exchanging authorization codes for
access
- refresh_token grant type for token refresh
- Both JSON and form-encoded request bodies
- OAuth Client authentication via Basic auth or request body parameters
(form params and JSON body)

### Token Service Integration:
- Integrated OAuth server with the existing token service
- Added OAuth-specific authentication method
(`oauth_provider/authorization_code`)
- Enhanced token generation to include OAuth client context in JWT
claims.

## Database Changes:
- Added `oauth_client_id` field to `sessions` table for OAuth client
tracking. So an OAuth clients can use a refresh token only if the
session is issued for them. Similarly, a session issued to a client can
only be refreshed by that client (i.e user can't use
`/token?grant_type=refresh_token` endpoint with a refresh token obtained
through `/oauth/token` endpoint.)

## Next Steps
- Adding ratelimit for the `/token` endpoint
- Store token auth method for oauth clients in the database
issuedat pushed a commit that referenced this pull request Sep 30, 2025
🤖 I have created a release *beep* *boop*
---


##
[2.180.0](v2.179.0...v2.180.0)
(2025-09-23)


### Features

* add OAuth client type
([#2152](#2152))
([b118f1f](b118f1f))
* add phone to sms webhook payload
([#2160](#2160))
([d475ac1](d475ac1))
* background template reloading p1 - baseline decomposition
([#2148](#2148))
([746c937](746c937))
* config reloading with fsnotify, poller fallback, and signals
([#2161](#2161))
([c77d512](c77d512))
* enhance issuer URL validation in OAuth server metadata
([#2164](#2164))
([a9424d2](a9424d2))
* implement OAuth2 authorization endpoint
([#2107](#2107))
([5318552](5318552))
* **oauth2:** add `/oauth/token` endpoint
([#2159](#2159))
([a89a0b0](a89a0b0))
* **oauth2:** add admin endpoint to regenerate OAuth client secrets
([#2170](#2170))
([0bd1c28](0bd1c28))
* **oauth2:** return redirect_uri on GET authorization
([#2175](#2175))
([b0a0c3e](b0a0c3e))
* **oauth2:** use `id` field as the public client_id
([#2154](#2154))
([86b7de4](86b7de4))
* **openapi:** add OAuth 2.1 server endpoints and clarify OAuth modes
([#2165](#2165))
([1f804a2](1f804a2))
* password changed email notification
([#2176](#2176))
([fe0fd04](fe0fd04))
* support `transfer_sub` in apple id tokens
([#2162](#2162))
([8a71006](8a71006))


### Bug Fixes

* ensure request context exists in API db operations
([#2171](#2171))
([060a992](060a992))
* **makefile:** remove invalid @ symbol from shell commands
([#2168](#2168))
([e6afe45](e6afe45))
* **oauth2:** switch to Origin header for request validation
([#2174](#2174))
([42bc9ab](42bc9ab))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
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.

6 participants