Enhance client creation and dynamic registration in OAuth service
- Added default values for grant types, response types, application type, and token endpoint auth method during dynamic client registration. - Implemented nil checks for client information in the CreateClient and UpdateClient methods to ensure robust error handling and validation.
This commit is contained in:
parent
d3874b28ad
commit
944c3e1b8e
3 changed files with 934 additions and 0 deletions
|
|
@ -108,6 +108,20 @@ func (s *Service) DynamicClientRegistration(ctx context.Context, request *types.
|
|||
clientInfo.TokenEndpointAuthMethod = s.config.Client.DefaultTokenEndpointAuthMethod
|
||||
}
|
||||
|
||||
// Update the request with defaults for the response
|
||||
if len(request.GrantTypes) == 0 {
|
||||
request.GrantTypes = clientInfo.GrantTypes
|
||||
}
|
||||
if len(request.ResponseTypes) == 0 {
|
||||
request.ResponseTypes = clientInfo.ResponseTypes
|
||||
}
|
||||
if request.ApplicationType == "" {
|
||||
request.ApplicationType = clientInfo.ApplicationType
|
||||
}
|
||||
if request.TokenEndpointAuthMethod == "" {
|
||||
request.TokenEndpointAuthMethod = clientInfo.TokenEndpointAuthMethod
|
||||
}
|
||||
|
||||
// Create the client
|
||||
createdClient, err := s.clientProvider.CreateClient(ctx, clientInfo)
|
||||
if err != nil {
|
||||
|
|
|
|||
904
openapi/oauth/client_test.go
Normal file
904
openapi/oauth/client_test.go
Normal file
|
|
@ -0,0 +1,904 @@
|
|||
package oauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||
)
|
||||
|
||||
// generateUniqueClientID generates a unique client ID for testing
|
||||
func generateUniqueClientID(prefix string) string {
|
||||
bytes := make([]byte, 4)
|
||||
rand.Read(bytes)
|
||||
return prefix + "-" + hex.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Client Management Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
service, _, _, cleanup := setupOAuthTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("register new client successfully", func(t *testing.T) {
|
||||
clientInfo := &types.ClientInfo{
|
||||
ClientID: generateUniqueClientID("test-register-new-client"),
|
||||
ClientSecret: "test-secret",
|
||||
ClientName: "Test Registration Client",
|
||||
ClientType: types.ClientTypeConfidential,
|
||||
RedirectURIs: []string{"https://localhost/callback"},
|
||||
GrantTypes: []string{types.GrantTypeAuthorizationCode},
|
||||
ResponseTypes: []string{types.ResponseTypeCode},
|
||||
Scope: "openid profile",
|
||||
}
|
||||
|
||||
result, err := service.Register(ctx, clientInfo)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, clientInfo.ClientID, result.ClientID)
|
||||
assert.Equal(t, clientInfo.ClientName, result.ClientName)
|
||||
assert.Equal(t, clientInfo.ClientType, result.ClientType)
|
||||
assert.Equal(t, clientInfo.RedirectURIs, result.RedirectURIs)
|
||||
assert.Equal(t, clientInfo.GrantTypes, result.GrantTypes)
|
||||
assert.Equal(t, clientInfo.ResponseTypes, result.ResponseTypes)
|
||||
assert.Equal(t, clientInfo.Scope, result.Scope)
|
||||
})
|
||||
|
||||
t.Run("register with nil client info", func(t *testing.T) {
|
||||
result, err := service.Register(ctx, nil)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
|
||||
// Check that the error is related to nil client info
|
||||
assert.Contains(t, err.Error(), "Client information is required")
|
||||
})
|
||||
|
||||
t.Run("register with empty client info", func(t *testing.T) {
|
||||
clientInfo := &types.ClientInfo{}
|
||||
|
||||
result, err := service.Register(ctx, clientInfo)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
})
|
||||
|
||||
t.Run("register client with existing ID", func(t *testing.T) {
|
||||
// Use one of the pre-existing test clients
|
||||
clientInfo := &types.ClientInfo{
|
||||
ClientID: testClients[0].ClientID, // This client already exists
|
||||
ClientSecret: "new-secret",
|
||||
ClientName: "Duplicate Client",
|
||||
ClientType: types.ClientTypeConfidential,
|
||||
RedirectURIs: []string{"https://localhost/callback"},
|
||||
GrantTypes: []string{types.GrantTypeAuthorizationCode},
|
||||
ResponseTypes: []string{types.ResponseTypeCode},
|
||||
Scope: "openid profile",
|
||||
}
|
||||
|
||||
result, err := service.Register(ctx, clientInfo)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateClient(t *testing.T) {
|
||||
service, _, _, cleanup := setupOAuthTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("update existing client successfully", func(t *testing.T) {
|
||||
// Use first test client
|
||||
clientID := testClients[0].ClientID
|
||||
updatedInfo := &types.ClientInfo{
|
||||
ClientID: clientID,
|
||||
ClientSecret: "updated-secret",
|
||||
ClientName: "Updated Client Name",
|
||||
ClientType: types.ClientTypeConfidential,
|
||||
RedirectURIs: []string{"https://localhost/callback"},
|
||||
GrantTypes: []string{types.GrantTypeAuthorizationCode, types.GrantTypeRefreshToken},
|
||||
ResponseTypes: []string{types.ResponseTypeCode},
|
||||
Scope: "openid profile email",
|
||||
}
|
||||
|
||||
result, err := service.UpdateClient(ctx, clientID, updatedInfo)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, clientID, result.ClientID)
|
||||
assert.Equal(t, updatedInfo.ClientName, result.ClientName)
|
||||
assert.Equal(t, updatedInfo.RedirectURIs, result.RedirectURIs)
|
||||
assert.Equal(t, updatedInfo.Scope, result.Scope)
|
||||
})
|
||||
|
||||
t.Run("update non-existing client", func(t *testing.T) {
|
||||
clientID := "non-existing-client"
|
||||
updatedInfo := &types.ClientInfo{
|
||||
ClientID: clientID,
|
||||
ClientName: "Non-existing Client",
|
||||
}
|
||||
|
||||
result, err := service.UpdateClient(ctx, clientID, updatedInfo)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
})
|
||||
|
||||
t.Run("update with nil client info", func(t *testing.T) {
|
||||
clientID := testClients[0].ClientID
|
||||
|
||||
result, err := service.UpdateClient(ctx, clientID, nil)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
})
|
||||
|
||||
t.Run("update with empty client ID", func(t *testing.T) {
|
||||
updatedInfo := &types.ClientInfo{
|
||||
ClientName: "Updated Client",
|
||||
}
|
||||
|
||||
result, err := service.UpdateClient(ctx, "", updatedInfo)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteClient(t *testing.T) {
|
||||
service, _, _, cleanup := setupOAuthTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("delete existing client successfully", func(t *testing.T) {
|
||||
// Create a client to delete
|
||||
clientInfo := &types.ClientInfo{
|
||||
ClientID: generateUniqueClientID("test-delete-client"),
|
||||
ClientSecret: "test-secret",
|
||||
ClientName: "Test Delete Client",
|
||||
ClientType: types.ClientTypeConfidential,
|
||||
RedirectURIs: []string{"https://localhost/callback"},
|
||||
GrantTypes: []string{types.GrantTypeAuthorizationCode},
|
||||
ResponseTypes: []string{types.ResponseTypeCode},
|
||||
Scope: "openid profile",
|
||||
}
|
||||
|
||||
_, err := service.Register(ctx, clientInfo)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Delete the client
|
||||
err = service.DeleteClient(ctx, clientInfo.ClientID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify client is deleted
|
||||
clientProvider := service.GetClientProvider()
|
||||
deletedClient, err := clientProvider.GetClientByID(ctx, clientInfo.ClientID)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, deletedClient)
|
||||
})
|
||||
|
||||
t.Run("delete non-existing client", func(t *testing.T) {
|
||||
err := service.DeleteClient(ctx, "non-existing-client")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("delete with empty client ID", func(t *testing.T) {
|
||||
err := service.DeleteClient(ctx, "")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateScope(t *testing.T) {
|
||||
service, _, _, cleanup := setupOAuthTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("validate valid scopes", func(t *testing.T) {
|
||||
clientID := testClients[0].ClientID
|
||||
requestedScopes := []string{"openid", "profile"}
|
||||
|
||||
result, err := service.ValidateScope(ctx, requestedScopes, clientID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.True(t, result.Valid)
|
||||
})
|
||||
|
||||
t.Run("validate with non-existing client", func(t *testing.T) {
|
||||
clientID := "non-existing-client"
|
||||
requestedScopes := []string{"openid", "profile"}
|
||||
|
||||
result, err := service.ValidateScope(ctx, requestedScopes, clientID)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
})
|
||||
|
||||
t.Run("validate with empty scopes", func(t *testing.T) {
|
||||
clientID := testClients[0].ClientID
|
||||
requestedScopes := []string{}
|
||||
|
||||
result, err := service.ValidateScope(ctx, requestedScopes, clientID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
})
|
||||
|
||||
t.Run("validate with empty client ID", func(t *testing.T) {
|
||||
requestedScopes := []string{"openid", "profile"}
|
||||
|
||||
result, err := service.ValidateScope(ctx, requestedScopes, "")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Dynamic Client Registration Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestDynamicClientRegistration(t *testing.T) {
|
||||
service, _, _, cleanup := setupOAuthTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("register confidential client successfully", func(t *testing.T) {
|
||||
request := &types.DynamicClientRegistrationRequest{
|
||||
ClientName: "Dynamic Test Client",
|
||||
RedirectURIs: []string{"https://localhost/callback"},
|
||||
GrantTypes: []string{types.GrantTypeAuthorizationCode, types.GrantTypeRefreshToken},
|
||||
ResponseTypes: []string{types.ResponseTypeCode},
|
||||
ApplicationType: types.ApplicationTypeWeb,
|
||||
TokenEndpointAuthMethod: types.TokenEndpointAuthBasic,
|
||||
Scope: "openid profile email",
|
||||
ClientURI: "https://localhost",
|
||||
LogoURI: "https://localhost/logo.png",
|
||||
TosURI: "https://localhost/tos",
|
||||
PolicyURI: "https://localhost/policy",
|
||||
Contacts: []string{"admin@localhost"},
|
||||
}
|
||||
|
||||
response, err := service.DynamicClientRegistration(ctx, request)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, response)
|
||||
assert.NotEmpty(t, response.ClientID)
|
||||
assert.NotEmpty(t, response.ClientSecret)
|
||||
assert.Greater(t, response.ClientIDIssuedAt, int64(0))
|
||||
assert.Equal(t, request.ClientName, response.DynamicClientRegistrationRequest.ClientName)
|
||||
assert.Equal(t, request.RedirectURIs, response.DynamicClientRegistrationRequest.RedirectURIs)
|
||||
assert.Equal(t, request.GrantTypes, response.DynamicClientRegistrationRequest.GrantTypes)
|
||||
assert.Equal(t, request.ResponseTypes, response.DynamicClientRegistrationRequest.ResponseTypes)
|
||||
assert.Equal(t, request.ApplicationType, response.DynamicClientRegistrationRequest.ApplicationType)
|
||||
assert.Equal(t, request.TokenEndpointAuthMethod, response.DynamicClientRegistrationRequest.TokenEndpointAuthMethod)
|
||||
assert.Equal(t, request.Scope, response.DynamicClientRegistrationRequest.Scope)
|
||||
assert.Equal(t, request.ClientURI, response.DynamicClientRegistrationRequest.ClientURI)
|
||||
assert.Equal(t, request.LogoURI, response.DynamicClientRegistrationRequest.LogoURI)
|
||||
assert.Equal(t, request.TosURI, response.DynamicClientRegistrationRequest.TosURI)
|
||||
assert.Equal(t, request.PolicyURI, response.DynamicClientRegistrationRequest.PolicyURI)
|
||||
assert.Equal(t, request.Contacts, response.DynamicClientRegistrationRequest.Contacts)
|
||||
})
|
||||
|
||||
t.Run("register public client successfully", func(t *testing.T) {
|
||||
request := &types.DynamicClientRegistrationRequest{
|
||||
ClientName: "Dynamic Public Client",
|
||||
RedirectURIs: []string{"https://localhost/callback"},
|
||||
GrantTypes: []string{types.GrantTypeAuthorizationCode},
|
||||
ResponseTypes: []string{types.ResponseTypeCode},
|
||||
ApplicationType: types.ApplicationTypeNative,
|
||||
TokenEndpointAuthMethod: "none",
|
||||
Scope: "openid profile",
|
||||
}
|
||||
|
||||
response, err := service.DynamicClientRegistration(ctx, request)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, response)
|
||||
assert.NotEmpty(t, response.ClientID)
|
||||
assert.Empty(t, response.ClientSecret) // Public clients don't have secrets
|
||||
assert.Greater(t, response.ClientIDIssuedAt, int64(0))
|
||||
assert.Equal(t, request.ClientName, response.DynamicClientRegistrationRequest.ClientName)
|
||||
assert.Equal(t, request.RedirectURIs, response.DynamicClientRegistrationRequest.RedirectURIs)
|
||||
assert.Equal(t, request.GrantTypes, response.DynamicClientRegistrationRequest.GrantTypes)
|
||||
assert.Equal(t, request.ResponseTypes, response.DynamicClientRegistrationRequest.ResponseTypes)
|
||||
assert.Equal(t, request.ApplicationType, response.DynamicClientRegistrationRequest.ApplicationType)
|
||||
assert.Equal(t, request.TokenEndpointAuthMethod, response.DynamicClientRegistrationRequest.TokenEndpointAuthMethod)
|
||||
assert.Equal(t, request.Scope, response.DynamicClientRegistrationRequest.Scope)
|
||||
})
|
||||
|
||||
t.Run("register with default values", func(t *testing.T) {
|
||||
request := &types.DynamicClientRegistrationRequest{
|
||||
ClientName: "Minimal Client",
|
||||
RedirectURIs: []string{"https://localhost/callback"},
|
||||
}
|
||||
|
||||
response, err := service.DynamicClientRegistration(ctx, request)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, response)
|
||||
assert.NotEmpty(t, response.ClientID)
|
||||
assert.NotEmpty(t, response.ClientSecret)
|
||||
assert.Equal(t, request.ClientName, response.DynamicClientRegistrationRequest.ClientName)
|
||||
assert.Equal(t, request.RedirectURIs, response.DynamicClientRegistrationRequest.RedirectURIs)
|
||||
// Check defaults were applied
|
||||
assert.Equal(t, service.config.Client.DefaultGrantTypes, response.DynamicClientRegistrationRequest.GrantTypes)
|
||||
assert.Equal(t, service.config.Client.DefaultResponseTypes, response.DynamicClientRegistrationRequest.ResponseTypes)
|
||||
assert.Equal(t, types.ApplicationTypeWeb, response.DynamicClientRegistrationRequest.ApplicationType)
|
||||
assert.Equal(t, service.config.Client.DefaultTokenEndpointAuthMethod, response.DynamicClientRegistrationRequest.TokenEndpointAuthMethod)
|
||||
})
|
||||
|
||||
t.Run("register with dynamic registration disabled", func(t *testing.T) {
|
||||
// Temporarily disable dynamic registration
|
||||
originalEnabled := service.config.Features.DynamicClientRegistrationEnabled
|
||||
service.config.Features.DynamicClientRegistrationEnabled = false
|
||||
defer func() {
|
||||
service.config.Features.DynamicClientRegistrationEnabled = originalEnabled
|
||||
}()
|
||||
|
||||
request := &types.DynamicClientRegistrationRequest{
|
||||
ClientName: "Disabled Feature Client",
|
||||
RedirectURIs: []string{"https://localhost/callback"},
|
||||
}
|
||||
|
||||
response, err := service.DynamicClientRegistration(ctx, request)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, response)
|
||||
|
||||
oauthErr, ok := err.(*types.ErrorResponse)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, types.ErrorInvalidRequest, oauthErr.Code)
|
||||
assert.Contains(t, oauthErr.ErrorDescription, "Dynamic client registration is not enabled")
|
||||
})
|
||||
|
||||
t.Run("register with invalid request", func(t *testing.T) {
|
||||
request := &types.DynamicClientRegistrationRequest{
|
||||
ClientName: "Invalid Client",
|
||||
RedirectURIs: []string{}, // Empty redirect URIs should cause error
|
||||
}
|
||||
|
||||
response, err := service.DynamicClientRegistration(ctx, request)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, response)
|
||||
|
||||
oauthErr, ok := err.(*types.ErrorResponse)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, types.ErrorInvalidRequest, oauthErr.Code)
|
||||
assert.Contains(t, oauthErr.ErrorDescription, "At least one redirect URI is required")
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Client ID and Secret Generation Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestGenerateClientID(t *testing.T) {
|
||||
service, _, _, cleanup := setupOAuthTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
t.Run("generate client ID with default length", func(t *testing.T) {
|
||||
clientID, err := service.generateClientID()
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, clientID)
|
||||
assert.Greater(t, len(clientID), 0)
|
||||
|
||||
// Check it's valid base64url
|
||||
assert.NotContains(t, clientID, "=")
|
||||
assert.NotContains(t, clientID, "+")
|
||||
assert.NotContains(t, clientID, "/")
|
||||
})
|
||||
|
||||
t.Run("generate multiple client IDs are unique", func(t *testing.T) {
|
||||
clientIDs := make(map[string]bool)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
clientID, err := service.generateClientID()
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, clientID)
|
||||
|
||||
// Check uniqueness
|
||||
assert.False(t, clientIDs[clientID], "Client ID should be unique")
|
||||
clientIDs[clientID] = true
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("generate client ID with custom length", func(t *testing.T) {
|
||||
// Set custom length
|
||||
originalLength := service.config.Client.ClientIDLength
|
||||
service.config.Client.ClientIDLength = 16
|
||||
defer func() {
|
||||
service.config.Client.ClientIDLength = originalLength
|
||||
}()
|
||||
|
||||
clientID, err := service.generateClientID()
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, clientID)
|
||||
|
||||
// Length should be approximately 16 * 4/3 (base64 encoding)
|
||||
expectedMinLength := 16
|
||||
assert.GreaterOrEqual(t, len(clientID), expectedMinLength)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGenerateClientSecret(t *testing.T) {
|
||||
service, _, _, cleanup := setupOAuthTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
t.Run("generate client secret with default length", func(t *testing.T) {
|
||||
clientSecret, err := service.generateClientSecret()
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, clientSecret)
|
||||
assert.Greater(t, len(clientSecret), 0)
|
||||
|
||||
// Check it's valid base64url
|
||||
assert.NotContains(t, clientSecret, "=")
|
||||
assert.NotContains(t, clientSecret, "+")
|
||||
assert.NotContains(t, clientSecret, "/")
|
||||
})
|
||||
|
||||
t.Run("generate multiple client secrets are unique", func(t *testing.T) {
|
||||
clientSecrets := make(map[string]bool)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
clientSecret, err := service.generateClientSecret()
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, clientSecret)
|
||||
|
||||
// Check uniqueness
|
||||
assert.False(t, clientSecrets[clientSecret], "Client secret should be unique")
|
||||
clientSecrets[clientSecret] = true
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("generate client secret with custom length", func(t *testing.T) {
|
||||
// Set custom length
|
||||
originalLength := service.config.Client.ClientSecretLength
|
||||
service.config.Client.ClientSecretLength = 32
|
||||
defer func() {
|
||||
service.config.Client.ClientSecretLength = originalLength
|
||||
}()
|
||||
|
||||
clientSecret, err := service.generateClientSecret()
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, clientSecret)
|
||||
|
||||
// Length should be approximately 32 * 4/3 (base64 encoding)
|
||||
expectedMinLength := 32
|
||||
assert.GreaterOrEqual(t, len(clientSecret), expectedMinLength)
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Request Validation Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestValidateDynamicClientRegistrationRequest(t *testing.T) {
|
||||
service, _, _, cleanup := setupOAuthTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
t.Run("validate valid request", func(t *testing.T) {
|
||||
request := &types.DynamicClientRegistrationRequest{
|
||||
ClientName: "Valid Client",
|
||||
RedirectURIs: []string{"https://localhost/callback"},
|
||||
GrantTypes: []string{types.GrantTypeAuthorizationCode},
|
||||
ResponseTypes: []string{types.ResponseTypeCode},
|
||||
ApplicationType: types.ApplicationTypeWeb,
|
||||
}
|
||||
|
||||
err := service.validateDynamicClientRegistrationRequest(request)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("validate request with no redirect URIs", func(t *testing.T) {
|
||||
request := &types.DynamicClientRegistrationRequest{
|
||||
ClientName: "No Redirect URIs",
|
||||
RedirectURIs: []string{},
|
||||
}
|
||||
|
||||
err := service.validateDynamicClientRegistrationRequest(request)
|
||||
assert.Error(t, err)
|
||||
|
||||
oauthErr, ok := err.(*types.ErrorResponse)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, types.ErrorInvalidRequest, oauthErr.Code)
|
||||
assert.Contains(t, oauthErr.ErrorDescription, "At least one redirect URI is required")
|
||||
})
|
||||
|
||||
t.Run("validate request with invalid redirect URI", func(t *testing.T) {
|
||||
request := &types.DynamicClientRegistrationRequest{
|
||||
ClientName: "Invalid Redirect URI",
|
||||
RedirectURIs: []string{"://invalid-uri"},
|
||||
}
|
||||
|
||||
err := service.validateDynamicClientRegistrationRequest(request)
|
||||
assert.Error(t, err)
|
||||
|
||||
oauthErr, ok := err.(*types.ErrorResponse)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, types.ErrorInvalidRequest, oauthErr.Code)
|
||||
assert.Contains(t, oauthErr.ErrorDescription, "Invalid redirect URI format")
|
||||
})
|
||||
|
||||
t.Run("validate request with invalid grant type", func(t *testing.T) {
|
||||
request := &types.DynamicClientRegistrationRequest{
|
||||
ClientName: "Invalid Grant Type",
|
||||
RedirectURIs: []string{"https://localhost/callback"},
|
||||
GrantTypes: []string{"invalid_grant_type"},
|
||||
}
|
||||
|
||||
err := service.validateDynamicClientRegistrationRequest(request)
|
||||
assert.Error(t, err)
|
||||
|
||||
oauthErr, ok := err.(*types.ErrorResponse)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, types.ErrorInvalidRequest, oauthErr.Code)
|
||||
assert.Contains(t, oauthErr.ErrorDescription, "Invalid grant type: invalid_grant_type")
|
||||
})
|
||||
|
||||
t.Run("validate request with invalid response type", func(t *testing.T) {
|
||||
request := &types.DynamicClientRegistrationRequest{
|
||||
ClientName: "Invalid Response Type",
|
||||
RedirectURIs: []string{"https://localhost/callback"},
|
||||
ResponseTypes: []string{"invalid_response_type"},
|
||||
}
|
||||
|
||||
err := service.validateDynamicClientRegistrationRequest(request)
|
||||
assert.Error(t, err)
|
||||
|
||||
oauthErr, ok := err.(*types.ErrorResponse)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, types.ErrorInvalidRequest, oauthErr.Code)
|
||||
assert.Contains(t, oauthErr.ErrorDescription, "Invalid response type: invalid_response_type")
|
||||
})
|
||||
|
||||
t.Run("validate request with invalid application type", func(t *testing.T) {
|
||||
request := &types.DynamicClientRegistrationRequest{
|
||||
ClientName: "Invalid Application Type",
|
||||
RedirectURIs: []string{"https://localhost/callback"},
|
||||
ApplicationType: "invalid_application_type",
|
||||
}
|
||||
|
||||
err := service.validateDynamicClientRegistrationRequest(request)
|
||||
assert.Error(t, err)
|
||||
|
||||
oauthErr, ok := err.(*types.ErrorResponse)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, types.ErrorInvalidRequest, oauthErr.Code)
|
||||
assert.Contains(t, oauthErr.ErrorDescription, "Invalid application type")
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateRedirectURIForRegistration(t *testing.T) {
|
||||
service, _, _, cleanup := setupOAuthTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
t.Run("validate valid HTTPS URI", func(t *testing.T) {
|
||||
err := service.validateRedirectURIForRegistration("https://localhost/callback")
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("validate valid HTTP URI", func(t *testing.T) {
|
||||
err := service.validateRedirectURIForRegistration("http://localhost/callback")
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("validate invalid URI format", func(t *testing.T) {
|
||||
err := service.validateRedirectURIForRegistration("://invalid-uri")
|
||||
assert.Error(t, err)
|
||||
|
||||
oauthErr, ok := err.(*types.ErrorResponse)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, types.ErrorInvalidRequest, oauthErr.Code)
|
||||
assert.Contains(t, oauthErr.ErrorDescription, "Invalid redirect URI format")
|
||||
})
|
||||
|
||||
t.Run("validate with scheme restrictions", func(t *testing.T) {
|
||||
// Set restricted schemes
|
||||
originalSchemes := service.config.Client.AllowedRedirectURISchemes
|
||||
service.config.Client.AllowedRedirectURISchemes = []string{"https"}
|
||||
defer func() {
|
||||
service.config.Client.AllowedRedirectURISchemes = originalSchemes
|
||||
}()
|
||||
|
||||
// HTTPS should be allowed
|
||||
err := service.validateRedirectURIForRegistration("https://localhost/callback")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// HTTP should be rejected
|
||||
err = service.validateRedirectURIForRegistration("http://example.com/callback")
|
||||
assert.Error(t, err)
|
||||
|
||||
oauthErr, ok := err.(*types.ErrorResponse)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, types.ErrorInvalidRequest, oauthErr.Code)
|
||||
assert.Contains(t, oauthErr.ErrorDescription, "Redirect URI scheme 'http' is not allowed")
|
||||
})
|
||||
|
||||
t.Run("validate with host restrictions", func(t *testing.T) {
|
||||
// Set restricted hosts
|
||||
originalHosts := service.config.Client.AllowedRedirectURIHosts
|
||||
service.config.Client.AllowedRedirectURIHosts = []string{"localhost", "127.0.0.1"}
|
||||
defer func() {
|
||||
service.config.Client.AllowedRedirectURIHosts = originalHosts
|
||||
}()
|
||||
|
||||
// Localhost should be allowed
|
||||
err := service.validateRedirectURIForRegistration("https://localhost/callback")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 127.0.0.1 should be allowed
|
||||
err = service.validateRedirectURIForRegistration("https://127.0.0.1/callback")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Other hosts should be rejected
|
||||
err = service.validateRedirectURIForRegistration("https://example.com/callback")
|
||||
assert.Error(t, err)
|
||||
|
||||
oauthErr, ok := err.(*types.ErrorResponse)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, types.ErrorInvalidRequest, oauthErr.Code)
|
||||
assert.Contains(t, oauthErr.ErrorDescription, "Redirect URI host 'example.com' is not allowed")
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Grant Type and Response Type Validation Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestIsValidGrantType(t *testing.T) {
|
||||
service, _, _, cleanup := setupOAuthTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
t.Run("validate valid grant types", func(t *testing.T) {
|
||||
validGrantTypes := []string{
|
||||
types.GrantTypeAuthorizationCode,
|
||||
types.GrantTypeRefreshToken,
|
||||
types.GrantTypeClientCredentials,
|
||||
types.GrantTypeDeviceCode,
|
||||
types.GrantTypeTokenExchange,
|
||||
}
|
||||
|
||||
for _, grantType := range validGrantTypes {
|
||||
assert.True(t, service.isValidGrantType(grantType), "Grant type %s should be valid", grantType)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("validate invalid grant types", func(t *testing.T) {
|
||||
invalidGrantTypes := []string{
|
||||
"invalid_grant_type",
|
||||
"password", // Not supported in OAuth 2.1
|
||||
"implicit", // Not supported in OAuth 2.1
|
||||
"",
|
||||
"authorization_code_invalid",
|
||||
}
|
||||
|
||||
for _, grantType := range invalidGrantTypes {
|
||||
assert.False(t, service.isValidGrantType(grantType), "Grant type %s should be invalid", grantType)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsValidResponseType(t *testing.T) {
|
||||
service, _, _, cleanup := setupOAuthTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
t.Run("validate valid response types", func(t *testing.T) {
|
||||
validResponseTypes := []string{
|
||||
types.ResponseTypeCode,
|
||||
types.ResponseTypeToken,
|
||||
types.ResponseTypeIDToken,
|
||||
"code token",
|
||||
"code id_token",
|
||||
"token id_token",
|
||||
"code token id_token",
|
||||
}
|
||||
|
||||
for _, responseType := range validResponseTypes {
|
||||
assert.True(t, service.isValidResponseType(responseType), "Response type %s should be valid", responseType)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("validate invalid response types", func(t *testing.T) {
|
||||
invalidResponseTypes := []string{
|
||||
"invalid_response_type",
|
||||
"code invalid",
|
||||
"",
|
||||
"token code", // Wrong order
|
||||
"id_token code", // Wrong order
|
||||
}
|
||||
|
||||
for _, responseType := range invalidResponseTypes {
|
||||
assert.False(t, service.isValidResponseType(responseType), "Response type %s should be invalid", responseType)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Client Secret Expiration Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestClientSecretExpiration(t *testing.T) {
|
||||
service, _, _, cleanup := setupOAuthTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("client secret with expiration", func(t *testing.T) {
|
||||
// Set client secret lifetime
|
||||
originalLifetime := service.config.Client.ClientSecretLifetime
|
||||
service.config.Client.ClientSecretLifetime = 24 * time.Hour
|
||||
defer func() {
|
||||
service.config.Client.ClientSecretLifetime = originalLifetime
|
||||
}()
|
||||
|
||||
request := &types.DynamicClientRegistrationRequest{
|
||||
ClientName: "Expiring Secret Client",
|
||||
RedirectURIs: []string{"https://localhost/callback"},
|
||||
}
|
||||
|
||||
response, err := service.DynamicClientRegistration(ctx, request)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, response)
|
||||
assert.NotEmpty(t, response.ClientSecret)
|
||||
assert.Greater(t, response.ClientSecretExpiresAt, int64(0))
|
||||
|
||||
// Check that expiration is approximately 24 hours from now
|
||||
expectedExpiration := time.Now().Add(24 * time.Hour).Unix()
|
||||
assert.InDelta(t, expectedExpiration, response.ClientSecretExpiresAt, 60) // Within 1 minute
|
||||
})
|
||||
|
||||
t.Run("client secret without expiration", func(t *testing.T) {
|
||||
// Set client secret lifetime to 0 (no expiration)
|
||||
originalLifetime := service.config.Client.ClientSecretLifetime
|
||||
service.config.Client.ClientSecretLifetime = 0
|
||||
defer func() {
|
||||
service.config.Client.ClientSecretLifetime = originalLifetime
|
||||
}()
|
||||
|
||||
request := &types.DynamicClientRegistrationRequest{
|
||||
ClientName: "Non-Expiring Secret Client",
|
||||
RedirectURIs: []string{"https://localhost/callback"},
|
||||
}
|
||||
|
||||
response, err := service.DynamicClientRegistration(ctx, request)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, response)
|
||||
assert.NotEmpty(t, response.ClientSecret)
|
||||
assert.Equal(t, int64(0), response.ClientSecretExpiresAt)
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Edge Cases and Error Handling Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestClientRegistrationEdgeCases(t *testing.T) {
|
||||
service, _, _, cleanup := setupOAuthTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("register with very long client name", func(t *testing.T) {
|
||||
request := &types.DynamicClientRegistrationRequest{
|
||||
ClientName: strings.Repeat("A", 1000), // Very long name
|
||||
RedirectURIs: []string{"https://localhost/callback"},
|
||||
}
|
||||
|
||||
response, err := service.DynamicClientRegistration(ctx, request)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, response)
|
||||
assert.Equal(t, request.ClientName, response.ClientName)
|
||||
})
|
||||
|
||||
t.Run("register with many redirect URIs", func(t *testing.T) {
|
||||
redirectURIs := make([]string, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
redirectURIs[i] = "https://localhost/callback" + string(rune('0'+i))
|
||||
}
|
||||
|
||||
request := &types.DynamicClientRegistrationRequest{
|
||||
ClientName: "Many Redirect URIs Client",
|
||||
RedirectURIs: redirectURIs,
|
||||
}
|
||||
|
||||
response, err := service.DynamicClientRegistration(ctx, request)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, response)
|
||||
assert.Equal(t, redirectURIs, response.RedirectURIs)
|
||||
})
|
||||
|
||||
t.Run("register with mixed grant types", func(t *testing.T) {
|
||||
request := &types.DynamicClientRegistrationRequest{
|
||||
ClientName: "Mixed Grant Types Client",
|
||||
RedirectURIs: []string{"https://localhost/callback"},
|
||||
GrantTypes: []string{
|
||||
types.GrantTypeAuthorizationCode,
|
||||
types.GrantTypeRefreshToken,
|
||||
types.GrantTypeClientCredentials,
|
||||
},
|
||||
}
|
||||
|
||||
response, err := service.DynamicClientRegistration(ctx, request)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, response)
|
||||
assert.Equal(t, request.GrantTypes, response.GrantTypes)
|
||||
})
|
||||
|
||||
t.Run("register with JWT token endpoint auth method", func(t *testing.T) {
|
||||
request := &types.DynamicClientRegistrationRequest{
|
||||
ClientName: "JWT Auth Client",
|
||||
RedirectURIs: []string{"https://localhost/callback"},
|
||||
TokenEndpointAuthMethod: types.TokenEndpointAuthJWT,
|
||||
}
|
||||
|
||||
response, err := service.DynamicClientRegistration(ctx, request)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, response)
|
||||
assert.NotEmpty(t, response.ClientSecret)
|
||||
assert.Equal(t, types.TokenEndpointAuthJWT, response.TokenEndpointAuthMethod)
|
||||
})
|
||||
}
|
||||
|
||||
func TestClientRegistrationWithCustomConfiguration(t *testing.T) {
|
||||
service, _, _, cleanup := setupOAuthTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("register with custom client ID and secret lengths", func(t *testing.T) {
|
||||
// Set custom lengths
|
||||
originalIDLength := service.config.Client.ClientIDLength
|
||||
originalSecretLength := service.config.Client.ClientSecretLength
|
||||
service.config.Client.ClientIDLength = 16
|
||||
service.config.Client.ClientSecretLength = 32
|
||||
defer func() {
|
||||
service.config.Client.ClientIDLength = originalIDLength
|
||||
service.config.Client.ClientSecretLength = originalSecretLength
|
||||
}()
|
||||
|
||||
request := &types.DynamicClientRegistrationRequest{
|
||||
ClientName: "Custom Length Client",
|
||||
RedirectURIs: []string{"https://localhost/callback"},
|
||||
}
|
||||
|
||||
response, err := service.DynamicClientRegistration(ctx, request)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, response)
|
||||
assert.NotEmpty(t, response.ClientID)
|
||||
assert.NotEmpty(t, response.ClientSecret)
|
||||
|
||||
// Check that the lengths are appropriate for the custom settings
|
||||
assert.Greater(t, len(response.ClientID), 10)
|
||||
assert.Greater(t, len(response.ClientSecret), 20)
|
||||
})
|
||||
|
||||
t.Run("register with custom default values", func(t *testing.T) {
|
||||
// Set custom defaults
|
||||
originalGrantTypes := service.config.Client.DefaultGrantTypes
|
||||
originalResponseTypes := service.config.Client.DefaultResponseTypes
|
||||
originalAuthMethod := service.config.Client.DefaultTokenEndpointAuthMethod
|
||||
|
||||
service.config.Client.DefaultGrantTypes = []string{types.GrantTypeClientCredentials}
|
||||
service.config.Client.DefaultResponseTypes = []string{types.ResponseTypeCode}
|
||||
service.config.Client.DefaultTokenEndpointAuthMethod = types.TokenEndpointAuthPost
|
||||
|
||||
defer func() {
|
||||
service.config.Client.DefaultGrantTypes = originalGrantTypes
|
||||
service.config.Client.DefaultResponseTypes = originalResponseTypes
|
||||
service.config.Client.DefaultTokenEndpointAuthMethod = originalAuthMethod
|
||||
}()
|
||||
|
||||
request := &types.DynamicClientRegistrationRequest{
|
||||
ClientName: "Custom Defaults Client",
|
||||
RedirectURIs: []string{"https://localhost/callback"},
|
||||
}
|
||||
|
||||
response, err := service.DynamicClientRegistration(ctx, request)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, response)
|
||||
assert.Equal(t, []string{types.GrantTypeClientCredentials}, response.GrantTypes)
|
||||
assert.Equal(t, []string{types.ResponseTypeCode}, response.ResponseTypes)
|
||||
assert.Equal(t, types.TokenEndpointAuthPost, response.TokenEndpointAuthMethod)
|
||||
})
|
||||
}
|
||||
|
|
@ -151,6 +151,14 @@ func (c *DefaultClient) GetClientByCredentials(ctx context.Context, clientID str
|
|||
|
||||
// CreateClient creates a new OAuth client and returns the client information
|
||||
func (c *DefaultClient) CreateClient(ctx context.Context, clientInfo *types.ClientInfo) (*types.ClientInfo, error) {
|
||||
// Check for nil client info
|
||||
if clientInfo == nil {
|
||||
return nil, &types.ErrorResponse{
|
||||
Code: types.ErrorInvalidRequest,
|
||||
ErrorDescription: "Client information is required",
|
||||
}
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if clientInfo.ClientID == "" {
|
||||
return nil, &types.ErrorResponse{
|
||||
|
|
@ -210,6 +218,14 @@ func (c *DefaultClient) CreateClient(ctx context.Context, clientInfo *types.Clie
|
|||
|
||||
// UpdateClient updates an existing OAuth client configuration
|
||||
func (c *DefaultClient) UpdateClient(ctx context.Context, clientID string, clientInfo *types.ClientInfo) (*types.ClientInfo, error) {
|
||||
// Check for nil client info
|
||||
if clientInfo == nil {
|
||||
return nil, &types.ErrorResponse{
|
||||
Code: types.ErrorInvalidRequest,
|
||||
ErrorDescription: "Client information is required",
|
||||
}
|
||||
}
|
||||
|
||||
// Check if client exists
|
||||
existing, err := c.GetClientByID(ctx, clientID)
|
||||
if err != nil {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue