Files
coder/coderd/oauth2provider/oauth2providertest/oauth2_test.go
Thomas Kosiewski c65013384a refactor: move OAuth2 provider code to dedicated package (#18746)
# Refactor OAuth2 Provider Code into Dedicated Package

This PR refactors the OAuth2 provider functionality by moving it from the main `coderd` package into a dedicated `oauth2provider` package. The change improves code organization and maintainability without changing functionality.

Key changes:

- Created a new `oauth2provider` package to house all OAuth2 provider-related code
- Moved existing OAuth2 provider functionality from `coderd/identityprovider` to the new package
- Refactored handler functions to follow a consistent pattern of returning `http.HandlerFunc` instead of being handlers directly
- Split large files into smaller, more focused files organized by functionality:
  - `app_secrets.go` - Manages OAuth2 application secrets
  - `apps.go` - Handles OAuth2 application CRUD operations
  - `authorize.go` - Implements the authorization flow
  - `metadata.go` - Provides OAuth2 metadata endpoints
  - `registration.go` - Handles dynamic client registration
  - `revoke.go` - Implements token revocation
  - `secrets.go` - Manages secret generation and validation
  - `tokens.go` - Handles token issuance and validation

This refactoring improves code organization and makes the OAuth2 provider functionality more maintainable while preserving all existing behavior.
2025-07-03 20:24:45 +02:00

342 lines
11 KiB
Go

package oauth2providertest_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/oauth2provider/oauth2providertest"
)
func TestOAuth2AuthorizationServerMetadata(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: false,
})
_ = coderdtest.CreateFirstUser(t, client)
// Fetch OAuth2 metadata
metadata := oauth2providertest.FetchOAuth2Metadata(t, client.URL.String())
// Verify required metadata fields
require.Contains(t, metadata, "issuer", "missing issuer in metadata")
require.Contains(t, metadata, "authorization_endpoint", "missing authorization_endpoint in metadata")
require.Contains(t, metadata, "token_endpoint", "missing token_endpoint in metadata")
// Verify response types
responseTypes, ok := metadata["response_types_supported"].([]any)
require.True(t, ok, "response_types_supported should be an array")
require.Contains(t, responseTypes, "code", "should support authorization code flow")
// Verify grant types
grantTypes, ok := metadata["grant_types_supported"].([]any)
require.True(t, ok, "grant_types_supported should be an array")
require.Contains(t, grantTypes, "authorization_code", "should support authorization_code grant")
require.Contains(t, grantTypes, "refresh_token", "should support refresh_token grant")
// Verify PKCE support
challengeMethods, ok := metadata["code_challenge_methods_supported"].([]any)
require.True(t, ok, "code_challenge_methods_supported should be an array")
require.Contains(t, challengeMethods, "S256", "should support S256 PKCE method")
// Verify endpoints are proper URLs
authEndpoint, ok := metadata["authorization_endpoint"].(string)
require.True(t, ok, "authorization_endpoint should be a string")
require.Contains(t, authEndpoint, "/oauth2/authorize", "authorization endpoint should be /oauth2/authorize")
tokenEndpoint, ok := metadata["token_endpoint"].(string)
require.True(t, ok, "token_endpoint should be a string")
require.Contains(t, tokenEndpoint, "/oauth2/tokens", "token endpoint should be /oauth2/tokens")
}
func TestOAuth2PKCEFlow(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: false,
})
_ = coderdtest.CreateFirstUser(t, client)
// Create OAuth2 app
app, clientSecret := oauth2providertest.CreateTestOAuth2App(t, client)
t.Cleanup(func() {
oauth2providertest.CleanupOAuth2App(t, client, app.ID)
})
// Generate PKCE parameters
codeVerifier, codeChallenge := oauth2providertest.GeneratePKCE(t)
state := oauth2providertest.GenerateState(t)
// Perform authorization
authParams := oauth2providertest.AuthorizeParams{
ClientID: app.ID.String(),
ResponseType: "code",
RedirectURI: oauth2providertest.TestRedirectURI,
State: state,
CodeChallenge: codeChallenge,
CodeChallengeMethod: "S256",
}
code := oauth2providertest.AuthorizeOAuth2App(t, client, client.URL.String(), authParams)
require.NotEmpty(t, code, "should receive authorization code")
// Exchange code for token with PKCE
tokenParams := oauth2providertest.TokenExchangeParams{
GrantType: "authorization_code",
Code: code,
ClientID: app.ID.String(),
ClientSecret: clientSecret,
CodeVerifier: codeVerifier,
RedirectURI: oauth2providertest.TestRedirectURI,
}
token := oauth2providertest.ExchangeCodeForToken(t, client.URL.String(), tokenParams)
require.NotEmpty(t, token.AccessToken, "should receive access token")
require.NotEmpty(t, token.RefreshToken, "should receive refresh token")
require.Equal(t, "Bearer", token.TokenType, "token type should be Bearer")
}
func TestOAuth2InvalidPKCE(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: false,
})
_ = coderdtest.CreateFirstUser(t, client)
// Create OAuth2 app
app, clientSecret := oauth2providertest.CreateTestOAuth2App(t, client)
t.Cleanup(func() {
oauth2providertest.CleanupOAuth2App(t, client, app.ID)
})
// Generate PKCE parameters
_, codeChallenge := oauth2providertest.GeneratePKCE(t)
state := oauth2providertest.GenerateState(t)
// Perform authorization
authParams := oauth2providertest.AuthorizeParams{
ClientID: app.ID.String(),
ResponseType: "code",
RedirectURI: oauth2providertest.TestRedirectURI,
State: state,
CodeChallenge: codeChallenge,
CodeChallengeMethod: "S256",
}
code := oauth2providertest.AuthorizeOAuth2App(t, client, client.URL.String(), authParams)
require.NotEmpty(t, code, "should receive authorization code")
// Attempt token exchange with wrong code verifier
tokenParams := oauth2providertest.TokenExchangeParams{
GrantType: "authorization_code",
Code: code,
ClientID: app.ID.String(),
ClientSecret: clientSecret,
CodeVerifier: oauth2providertest.InvalidCodeVerifier,
RedirectURI: oauth2providertest.TestRedirectURI,
}
oauth2providertest.PerformTokenExchangeExpectingError(
t, client.URL.String(), tokenParams, oauth2providertest.OAuth2ErrorTypes.InvalidGrant,
)
}
func TestOAuth2WithoutPKCE(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: false,
})
_ = coderdtest.CreateFirstUser(t, client)
// Create OAuth2 app
app, clientSecret := oauth2providertest.CreateTestOAuth2App(t, client)
t.Cleanup(func() {
oauth2providertest.CleanupOAuth2App(t, client, app.ID)
})
state := oauth2providertest.GenerateState(t)
// Perform authorization without PKCE
authParams := oauth2providertest.AuthorizeParams{
ClientID: app.ID.String(),
ResponseType: "code",
RedirectURI: oauth2providertest.TestRedirectURI,
State: state,
}
code := oauth2providertest.AuthorizeOAuth2App(t, client, client.URL.String(), authParams)
require.NotEmpty(t, code, "should receive authorization code")
// Exchange code for token without PKCE
tokenParams := oauth2providertest.TokenExchangeParams{
GrantType: "authorization_code",
Code: code,
ClientID: app.ID.String(),
ClientSecret: clientSecret,
RedirectURI: oauth2providertest.TestRedirectURI,
}
token := oauth2providertest.ExchangeCodeForToken(t, client.URL.String(), tokenParams)
require.NotEmpty(t, token.AccessToken, "should receive access token")
require.NotEmpty(t, token.RefreshToken, "should receive refresh token")
}
func TestOAuth2ResourceParameter(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: false,
})
_ = coderdtest.CreateFirstUser(t, client)
// Create OAuth2 app
app, clientSecret := oauth2providertest.CreateTestOAuth2App(t, client)
t.Cleanup(func() {
oauth2providertest.CleanupOAuth2App(t, client, app.ID)
})
state := oauth2providertest.GenerateState(t)
// Perform authorization with resource parameter
authParams := oauth2providertest.AuthorizeParams{
ClientID: app.ID.String(),
ResponseType: "code",
RedirectURI: oauth2providertest.TestRedirectURI,
State: state,
Resource: oauth2providertest.TestResourceURI,
}
code := oauth2providertest.AuthorizeOAuth2App(t, client, client.URL.String(), authParams)
require.NotEmpty(t, code, "should receive authorization code")
// Exchange code for token with resource parameter
tokenParams := oauth2providertest.TokenExchangeParams{
GrantType: "authorization_code",
Code: code,
ClientID: app.ID.String(),
ClientSecret: clientSecret,
RedirectURI: oauth2providertest.TestRedirectURI,
Resource: oauth2providertest.TestResourceURI,
}
token := oauth2providertest.ExchangeCodeForToken(t, client.URL.String(), tokenParams)
require.NotEmpty(t, token.AccessToken, "should receive access token")
require.NotEmpty(t, token.RefreshToken, "should receive refresh token")
}
func TestOAuth2TokenRefresh(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: false,
})
_ = coderdtest.CreateFirstUser(t, client)
// Create OAuth2 app
app, clientSecret := oauth2providertest.CreateTestOAuth2App(t, client)
t.Cleanup(func() {
oauth2providertest.CleanupOAuth2App(t, client, app.ID)
})
state := oauth2providertest.GenerateState(t)
// Get initial token
authParams := oauth2providertest.AuthorizeParams{
ClientID: app.ID.String(),
ResponseType: "code",
RedirectURI: oauth2providertest.TestRedirectURI,
State: state,
}
code := oauth2providertest.AuthorizeOAuth2App(t, client, client.URL.String(), authParams)
tokenParams := oauth2providertest.TokenExchangeParams{
GrantType: "authorization_code",
Code: code,
ClientID: app.ID.String(),
ClientSecret: clientSecret,
RedirectURI: oauth2providertest.TestRedirectURI,
}
initialToken := oauth2providertest.ExchangeCodeForToken(t, client.URL.String(), tokenParams)
require.NotEmpty(t, initialToken.RefreshToken, "should receive refresh token")
// Use refresh token to get new access token
refreshParams := oauth2providertest.TokenExchangeParams{
GrantType: "refresh_token",
RefreshToken: initialToken.RefreshToken,
ClientID: app.ID.String(),
ClientSecret: clientSecret,
}
refreshedToken := oauth2providertest.ExchangeCodeForToken(t, client.URL.String(), refreshParams)
require.NotEmpty(t, refreshedToken.AccessToken, "should receive new access token")
require.NotEqual(t, initialToken.AccessToken, refreshedToken.AccessToken, "new access token should be different")
}
func TestOAuth2ErrorResponses(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: false,
})
_ = coderdtest.CreateFirstUser(t, client)
t.Run("InvalidClient", func(t *testing.T) {
t.Parallel()
tokenParams := oauth2providertest.TokenExchangeParams{
GrantType: "authorization_code",
Code: "invalid-code",
ClientID: "non-existent-client",
ClientSecret: "invalid-secret",
}
oauth2providertest.PerformTokenExchangeExpectingError(
t, client.URL.String(), tokenParams, oauth2providertest.OAuth2ErrorTypes.InvalidClient,
)
})
t.Run("InvalidGrantType", func(t *testing.T) {
t.Parallel()
app, clientSecret := oauth2providertest.CreateTestOAuth2App(t, client)
t.Cleanup(func() {
oauth2providertest.CleanupOAuth2App(t, client, app.ID)
})
tokenParams := oauth2providertest.TokenExchangeParams{
GrantType: "invalid_grant_type",
ClientID: app.ID.String(),
ClientSecret: clientSecret,
}
oauth2providertest.PerformTokenExchangeExpectingError(
t, client.URL.String(), tokenParams, oauth2providertest.OAuth2ErrorTypes.UnsupportedGrantType,
)
})
t.Run("MissingCode", func(t *testing.T) {
t.Parallel()
app, clientSecret := oauth2providertest.CreateTestOAuth2App(t, client)
t.Cleanup(func() {
oauth2providertest.CleanupOAuth2App(t, client, app.ID)
})
tokenParams := oauth2providertest.TokenExchangeParams{
GrantType: "authorization_code",
ClientID: app.ID.String(),
ClientSecret: clientSecret,
}
oauth2providertest.PerformTokenExchangeExpectingError(
t, client.URL.String(), tokenParams, oauth2providertest.OAuth2ErrorTypes.InvalidRequest,
)
})
}