mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
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.
This commit is contained in:
41
coderd/oauth2provider/oauth2providertest/fixtures.go
Normal file
41
coderd/oauth2provider/oauth2providertest/fixtures.go
Normal file
@ -0,0 +1,41 @@
|
||||
package oauth2providertest
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
)
|
||||
|
||||
// Test constants for OAuth2 testing
|
||||
const (
|
||||
// TestRedirectURI is the standard test redirect URI
|
||||
TestRedirectURI = "http://localhost:9876/callback"
|
||||
|
||||
// TestResourceURI is used for testing resource parameter
|
||||
TestResourceURI = "https://api.example.com"
|
||||
|
||||
// Invalid PKCE verifier for negative testing
|
||||
InvalidCodeVerifier = "wrong-verifier"
|
||||
)
|
||||
|
||||
// OAuth2ErrorTypes contains standard OAuth2 error codes
|
||||
var OAuth2ErrorTypes = struct {
|
||||
InvalidRequest string
|
||||
InvalidClient string
|
||||
InvalidGrant string
|
||||
UnauthorizedClient string
|
||||
UnsupportedGrantType string
|
||||
InvalidScope string
|
||||
}{
|
||||
InvalidRequest: "invalid_request",
|
||||
InvalidClient: "invalid_client",
|
||||
InvalidGrant: "invalid_grant",
|
||||
UnauthorizedClient: "unauthorized_client",
|
||||
UnsupportedGrantType: "unsupported_grant_type",
|
||||
InvalidScope: "invalid_scope",
|
||||
}
|
||||
|
||||
// GenerateCodeChallenge creates an S256 code challenge from a verifier
|
||||
func GenerateCodeChallenge(verifier string) string {
|
||||
h := sha256.Sum256([]byte(verifier))
|
||||
return base64.RawURLEncoding.EncodeToString(h[:])
|
||||
}
|
328
coderd/oauth2provider/oauth2providertest/helpers.go
Normal file
328
coderd/oauth2provider/oauth2providertest/helpers.go
Normal file
@ -0,0 +1,328 @@
|
||||
// Package oauth2providertest provides comprehensive testing utilities for OAuth2 identity provider functionality.
|
||||
// It includes helpers for creating OAuth2 apps, performing authorization flows, token exchanges,
|
||||
// PKCE challenge generation and verification, and testing error scenarios.
|
||||
package oauth2providertest
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
// AuthorizeParams contains parameters for OAuth2 authorization
|
||||
type AuthorizeParams struct {
|
||||
ClientID string
|
||||
ResponseType string
|
||||
RedirectURI string
|
||||
State string
|
||||
CodeChallenge string
|
||||
CodeChallengeMethod string
|
||||
Resource string
|
||||
Scope string
|
||||
}
|
||||
|
||||
// TokenExchangeParams contains parameters for token exchange
|
||||
type TokenExchangeParams struct {
|
||||
GrantType string
|
||||
Code string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
CodeVerifier string
|
||||
RedirectURI string
|
||||
RefreshToken string
|
||||
Resource string
|
||||
}
|
||||
|
||||
// OAuth2Error represents an OAuth2 error response
|
||||
type OAuth2Error struct {
|
||||
Error string `json:"error"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
}
|
||||
|
||||
// CreateTestOAuth2App creates an OAuth2 app for testing and returns the app and client secret
|
||||
func CreateTestOAuth2App(t *testing.T, client *codersdk.Client) (*codersdk.OAuth2ProviderApp, string) {
|
||||
t.Helper()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// Create unique app name with random suffix
|
||||
appName := fmt.Sprintf("test-oauth2-app-%s", testutil.MustRandString(t, 10))
|
||||
|
||||
req := codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: appName,
|
||||
CallbackURL: TestRedirectURI,
|
||||
}
|
||||
|
||||
app, err := client.PostOAuth2ProviderApp(ctx, req)
|
||||
require.NoError(t, err, "failed to create OAuth2 app")
|
||||
|
||||
// Create client secret
|
||||
secret, err := client.PostOAuth2ProviderAppSecret(ctx, app.ID)
|
||||
require.NoError(t, err, "failed to create OAuth2 app secret")
|
||||
|
||||
return &app, secret.ClientSecretFull
|
||||
}
|
||||
|
||||
// GeneratePKCE generates a random PKCE code verifier and challenge
|
||||
func GeneratePKCE(t *testing.T) (verifier, challenge string) {
|
||||
t.Helper()
|
||||
|
||||
// Generate 32 random bytes for verifier
|
||||
bytes := make([]byte, 32)
|
||||
_, err := rand.Read(bytes)
|
||||
require.NoError(t, err, "failed to generate random bytes")
|
||||
|
||||
// Create code verifier (base64url encoding without padding)
|
||||
verifier = base64.RawURLEncoding.EncodeToString(bytes)
|
||||
|
||||
// Create code challenge using S256 method
|
||||
challenge = GenerateCodeChallenge(verifier)
|
||||
|
||||
return verifier, challenge
|
||||
}
|
||||
|
||||
// GenerateState generates a random state parameter
|
||||
func GenerateState(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
bytes := make([]byte, 16)
|
||||
_, err := rand.Read(bytes)
|
||||
require.NoError(t, err, "failed to generate random bytes")
|
||||
|
||||
return base64.RawURLEncoding.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
// AuthorizeOAuth2App performs the OAuth2 authorization flow and returns the authorization code
|
||||
func AuthorizeOAuth2App(t *testing.T, client *codersdk.Client, baseURL string, params AuthorizeParams) string {
|
||||
t.Helper()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// Build authorization URL
|
||||
authURL, err := url.Parse(baseURL + "/oauth2/authorize")
|
||||
require.NoError(t, err, "failed to parse authorization URL")
|
||||
|
||||
query := url.Values{}
|
||||
query.Set("client_id", params.ClientID)
|
||||
query.Set("response_type", params.ResponseType)
|
||||
query.Set("redirect_uri", params.RedirectURI)
|
||||
query.Set("state", params.State)
|
||||
|
||||
if params.CodeChallenge != "" {
|
||||
query.Set("code_challenge", params.CodeChallenge)
|
||||
query.Set("code_challenge_method", params.CodeChallengeMethod)
|
||||
}
|
||||
if params.Resource != "" {
|
||||
query.Set("resource", params.Resource)
|
||||
}
|
||||
if params.Scope != "" {
|
||||
query.Set("scope", params.Scope)
|
||||
}
|
||||
|
||||
authURL.RawQuery = query.Encode()
|
||||
|
||||
// Create POST request to authorize endpoint (simulating user clicking "Allow")
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", authURL.String(), nil)
|
||||
require.NoError(t, err, "failed to create authorization request")
|
||||
|
||||
// Add session token
|
||||
req.Header.Set("Coder-Session-Token", client.SessionToken())
|
||||
|
||||
// Perform request
|
||||
httpClient := &http.Client{
|
||||
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
|
||||
// Don't follow redirects, we want to capture the redirect URL
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
require.NoError(t, err, "failed to perform authorization request")
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should get a redirect response (either 302 Found or 307 Temporary Redirect)
|
||||
require.True(t, resp.StatusCode == http.StatusFound || resp.StatusCode == http.StatusTemporaryRedirect,
|
||||
"expected redirect response, got %d", resp.StatusCode)
|
||||
|
||||
// Extract redirect URL
|
||||
location := resp.Header.Get("Location")
|
||||
require.NotEmpty(t, location, "missing Location header in redirect response")
|
||||
|
||||
// Parse redirect URL to extract authorization code
|
||||
redirectURL, err := url.Parse(location)
|
||||
require.NoError(t, err, "failed to parse redirect URL")
|
||||
|
||||
code := redirectURL.Query().Get("code")
|
||||
require.NotEmpty(t, code, "missing authorization code in redirect URL")
|
||||
|
||||
// Verify state parameter
|
||||
returnedState := redirectURL.Query().Get("state")
|
||||
require.Equal(t, params.State, returnedState, "state parameter mismatch")
|
||||
|
||||
return code
|
||||
}
|
||||
|
||||
// ExchangeCodeForToken exchanges an authorization code for tokens
|
||||
func ExchangeCodeForToken(t *testing.T, baseURL string, params TokenExchangeParams) *oauth2.Token {
|
||||
t.Helper()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// Prepare form data
|
||||
data := url.Values{}
|
||||
data.Set("grant_type", params.GrantType)
|
||||
|
||||
if params.Code != "" {
|
||||
data.Set("code", params.Code)
|
||||
}
|
||||
if params.ClientID != "" {
|
||||
data.Set("client_id", params.ClientID)
|
||||
}
|
||||
if params.ClientSecret != "" {
|
||||
data.Set("client_secret", params.ClientSecret)
|
||||
}
|
||||
if params.CodeVerifier != "" {
|
||||
data.Set("code_verifier", params.CodeVerifier)
|
||||
}
|
||||
if params.RedirectURI != "" {
|
||||
data.Set("redirect_uri", params.RedirectURI)
|
||||
}
|
||||
if params.RefreshToken != "" {
|
||||
data.Set("refresh_token", params.RefreshToken)
|
||||
}
|
||||
if params.Resource != "" {
|
||||
data.Set("resource", params.Resource)
|
||||
}
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", baseURL+"/oauth2/tokens", strings.NewReader(data.Encode()))
|
||||
require.NoError(t, err, "failed to create token request")
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
// Perform request
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err, "failed to perform token request")
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Parse response
|
||||
var tokenResp oauth2.Token
|
||||
err = json.NewDecoder(resp.Body).Decode(&tokenResp)
|
||||
require.NoError(t, err, "failed to decode token response")
|
||||
|
||||
require.NotEmpty(t, tokenResp.AccessToken, "missing access token")
|
||||
require.Equal(t, "Bearer", tokenResp.TokenType, "unexpected token type")
|
||||
|
||||
return &tokenResp
|
||||
}
|
||||
|
||||
// RequireOAuth2Error checks that the HTTP response contains an expected OAuth2 error
|
||||
func RequireOAuth2Error(t *testing.T, resp *http.Response, expectedError string) {
|
||||
t.Helper()
|
||||
|
||||
var errorResp OAuth2Error
|
||||
err := json.NewDecoder(resp.Body).Decode(&errorResp)
|
||||
require.NoError(t, err, "failed to decode error response")
|
||||
|
||||
require.Equal(t, expectedError, errorResp.Error, "unexpected OAuth2 error code")
|
||||
require.NotEmpty(t, errorResp.ErrorDescription, "missing error description")
|
||||
}
|
||||
|
||||
// PerformTokenExchangeExpectingError performs a token exchange expecting an OAuth2 error
|
||||
func PerformTokenExchangeExpectingError(t *testing.T, baseURL string, params TokenExchangeParams, expectedError string) {
|
||||
t.Helper()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// Prepare form data
|
||||
data := url.Values{}
|
||||
data.Set("grant_type", params.GrantType)
|
||||
|
||||
if params.Code != "" {
|
||||
data.Set("code", params.Code)
|
||||
}
|
||||
if params.ClientID != "" {
|
||||
data.Set("client_id", params.ClientID)
|
||||
}
|
||||
if params.ClientSecret != "" {
|
||||
data.Set("client_secret", params.ClientSecret)
|
||||
}
|
||||
if params.CodeVerifier != "" {
|
||||
data.Set("code_verifier", params.CodeVerifier)
|
||||
}
|
||||
if params.RedirectURI != "" {
|
||||
data.Set("redirect_uri", params.RedirectURI)
|
||||
}
|
||||
if params.RefreshToken != "" {
|
||||
data.Set("refresh_token", params.RefreshToken)
|
||||
}
|
||||
if params.Resource != "" {
|
||||
data.Set("resource", params.Resource)
|
||||
}
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", baseURL+"/oauth2/tokens", strings.NewReader(data.Encode()))
|
||||
require.NoError(t, err, "failed to create token request")
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
// Perform request
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err, "failed to perform token request")
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should be a 4xx error
|
||||
require.True(t, resp.StatusCode >= 400 && resp.StatusCode < 500, "expected 4xx status code, got %d", resp.StatusCode)
|
||||
|
||||
// Check OAuth2 error
|
||||
RequireOAuth2Error(t, resp, expectedError)
|
||||
}
|
||||
|
||||
// FetchOAuth2Metadata fetches and returns OAuth2 authorization server metadata
|
||||
func FetchOAuth2Metadata(t *testing.T, baseURL string) map[string]any {
|
||||
t.Helper()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/.well-known/oauth-authorization-server", nil)
|
||||
require.NoError(t, err, "failed to create metadata request")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err, "failed to fetch metadata")
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode, "unexpected metadata response status")
|
||||
|
||||
var metadata map[string]any
|
||||
err = json.NewDecoder(resp.Body).Decode(&metadata)
|
||||
require.NoError(t, err, "failed to decode metadata response")
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
// CleanupOAuth2App deletes an OAuth2 app (helper for test cleanup)
|
||||
func CleanupOAuth2App(t *testing.T, client *codersdk.Client, appID uuid.UUID) {
|
||||
t.Helper()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
err := client.DeleteOAuth2ProviderApp(ctx, appID)
|
||||
if err != nil {
|
||||
t.Logf("Warning: failed to cleanup OAuth2 app %s: %v", appID, err)
|
||||
}
|
||||
}
|
341
coderd/oauth2provider/oauth2providertest/oauth2_test.go
Normal file
341
coderd/oauth2provider/oauth2providertest/oauth2_test.go
Normal file
@ -0,0 +1,341 @@
|
||||
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,
|
||||
)
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user