mirror of
https://github.com/coder/coder.git
synced 2025-07-06 15:41:45 +00:00
# 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.
342 lines
11 KiB
Go
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,
|
|
)
|
|
})
|
|
}
|