Merge pull request #1009 from trheyi/main
Implement dynamic client registration and enhance OAuth service funct…
This commit is contained in:
commit
16c9783b98
8 changed files with 1176 additions and 40 deletions
|
|
@ -2,6 +2,11 @@ package oauth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
)
|
)
|
||||||
|
|
@ -29,6 +34,269 @@ func (s *Service) ValidateScope(ctx context.Context, requestedScopes []string, c
|
||||||
// DynamicClientRegistration handles dynamic client registration
|
// DynamicClientRegistration handles dynamic client registration
|
||||||
// This implements RFC 7591 for automatic client registration
|
// This implements RFC 7591 for automatic client registration
|
||||||
func (s *Service) DynamicClientRegistration(ctx context.Context, request *types.DynamicClientRegistrationRequest) (*types.DynamicClientRegistrationResponse, error) {
|
func (s *Service) DynamicClientRegistration(ctx context.Context, request *types.DynamicClientRegistrationRequest) (*types.DynamicClientRegistrationResponse, error) {
|
||||||
// TODO: Implement dynamic client registration
|
// Check if dynamic client registration is enabled
|
||||||
return nil, nil
|
if !s.config.Features.DynamicClientRegistrationEnabled {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidRequest,
|
||||||
|
ErrorDescription: "Dynamic client registration is not enabled",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the request
|
||||||
|
if err := s.validateDynamicClientRegistrationRequest(request); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate client ID and secret
|
||||||
|
clientID, err := s.generateClientID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorServerError,
|
||||||
|
ErrorDescription: "Failed to generate client ID",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clientSecret := ""
|
||||||
|
// Determine client type based on token endpoint auth method
|
||||||
|
clientType := types.ClientTypePublic
|
||||||
|
if request.TokenEndpointAuthMethod == "" ||
|
||||||
|
request.TokenEndpointAuthMethod == types.TokenEndpointAuthBasic ||
|
||||||
|
request.TokenEndpointAuthMethod == types.TokenEndpointAuthPost ||
|
||||||
|
request.TokenEndpointAuthMethod == types.TokenEndpointAuthJWT {
|
||||||
|
clientType = types.ClientTypeConfidential
|
||||||
|
clientSecret, err = s.generateClientSecret()
|
||||||
|
if err != nil {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorServerError,
|
||||||
|
ErrorDescription: "Failed to generate client secret",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create client info from request
|
||||||
|
clientInfo := &types.ClientInfo{
|
||||||
|
ClientID: clientID,
|
||||||
|
ClientSecret: clientSecret,
|
||||||
|
ClientName: request.ClientName,
|
||||||
|
ClientType: clientType,
|
||||||
|
RedirectURIs: request.RedirectURIs,
|
||||||
|
ResponseTypes: request.ResponseTypes,
|
||||||
|
GrantTypes: request.GrantTypes,
|
||||||
|
ApplicationType: request.ApplicationType,
|
||||||
|
Contacts: request.Contacts,
|
||||||
|
ClientURI: request.ClientURI,
|
||||||
|
LogoURI: request.LogoURI,
|
||||||
|
Scope: request.Scope,
|
||||||
|
TosURI: request.TosURI,
|
||||||
|
PolicyURI: request.PolicyURI,
|
||||||
|
JwksURI: request.JwksURI,
|
||||||
|
JwksValue: request.Jwks,
|
||||||
|
TokenEndpointAuthMethod: request.TokenEndpointAuthMethod,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set defaults if not provided
|
||||||
|
if len(clientInfo.GrantTypes) == 0 {
|
||||||
|
clientInfo.GrantTypes = s.config.Client.DefaultGrantTypes
|
||||||
|
}
|
||||||
|
if len(clientInfo.ResponseTypes) == 0 {
|
||||||
|
clientInfo.ResponseTypes = s.config.Client.DefaultResponseTypes
|
||||||
|
}
|
||||||
|
if clientInfo.ApplicationType == "" {
|
||||||
|
clientInfo.ApplicationType = types.ApplicationTypeWeb
|
||||||
|
}
|
||||||
|
if clientInfo.TokenEndpointAuthMethod == "" {
|
||||||
|
clientInfo.TokenEndpointAuthMethod = s.config.Client.DefaultTokenEndpointAuthMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the client
|
||||||
|
createdClient, err := s.clientProvider.CreateClient(ctx, clientInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create response
|
||||||
|
response := &types.DynamicClientRegistrationResponse{
|
||||||
|
ClientID: createdClient.ClientID,
|
||||||
|
ClientSecret: createdClient.ClientSecret,
|
||||||
|
ClientIDIssuedAt: createdClient.CreatedAt.Unix(),
|
||||||
|
DynamicClientRegistrationRequest: request,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set client secret expiration (0 means it never expires)
|
||||||
|
if s.config.Client.ClientSecretLifetime > 0 {
|
||||||
|
response.ClientSecretExpiresAt = createdClient.CreatedAt.Add(s.config.Client.ClientSecretLifetime).Unix()
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateClientID generates a random client ID
|
||||||
|
func (s *Service) generateClientID() (string, error) {
|
||||||
|
length := s.config.Client.ClientIDLength
|
||||||
|
if length == 0 {
|
||||||
|
length = 32
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes := make([]byte, length)
|
||||||
|
if _, err := rand.Read(bytes); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use base64 URL encoding without padding
|
||||||
|
return strings.TrimRight(base64.URLEncoding.EncodeToString(bytes), "="), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateClientSecret generates a random client secret
|
||||||
|
func (s *Service) generateClientSecret() (string, error) {
|
||||||
|
length := s.config.Client.ClientSecretLength
|
||||||
|
if length == 0 {
|
||||||
|
length = 64
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes := make([]byte, length)
|
||||||
|
if _, err := rand.Read(bytes); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use base64 URL encoding without padding
|
||||||
|
return strings.TrimRight(base64.URLEncoding.EncodeToString(bytes), "="), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateDynamicClientRegistrationRequest validates the dynamic client registration request
|
||||||
|
func (s *Service) validateDynamicClientRegistrationRequest(request *types.DynamicClientRegistrationRequest) error {
|
||||||
|
// Validate redirect URIs
|
||||||
|
if len(request.RedirectURIs) == 0 {
|
||||||
|
return &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidRequest,
|
||||||
|
ErrorDescription: "At least one redirect URI is required",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate redirect URI schemes and hosts
|
||||||
|
for _, uri := range request.RedirectURIs {
|
||||||
|
if err := s.validateRedirectURIForRegistration(uri); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate grant types
|
||||||
|
if len(request.GrantTypes) > 0 {
|
||||||
|
for _, grantType := range request.GrantTypes {
|
||||||
|
if !s.isValidGrantType(grantType) {
|
||||||
|
return &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidRequest,
|
||||||
|
ErrorDescription: fmt.Sprintf("Invalid grant type: %s", grantType),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate response types
|
||||||
|
if len(request.ResponseTypes) > 0 {
|
||||||
|
for _, responseType := range request.ResponseTypes {
|
||||||
|
if !s.isValidResponseType(responseType) {
|
||||||
|
return &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidRequest,
|
||||||
|
ErrorDescription: fmt.Sprintf("Invalid response type: %s", responseType),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate application type
|
||||||
|
if request.ApplicationType != "" {
|
||||||
|
if request.ApplicationType != types.ApplicationTypeWeb && request.ApplicationType != types.ApplicationTypeNative {
|
||||||
|
return &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidRequest,
|
||||||
|
ErrorDescription: "Invalid application type",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateRedirectURIForRegistration validates redirect URI for dynamic registration
|
||||||
|
func (s *Service) validateRedirectURIForRegistration(uri string) error {
|
||||||
|
parsedURI, err := url.Parse(uri)
|
||||||
|
if err != nil {
|
||||||
|
return &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidRequest,
|
||||||
|
ErrorDescription: "Invalid redirect URI format",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check allowed schemes
|
||||||
|
if len(s.config.Client.AllowedRedirectURISchemes) > 0 {
|
||||||
|
schemeAllowed := false
|
||||||
|
for _, scheme := range s.config.Client.AllowedRedirectURISchemes {
|
||||||
|
if parsedURI.Scheme == scheme {
|
||||||
|
schemeAllowed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !schemeAllowed {
|
||||||
|
return &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidRequest,
|
||||||
|
ErrorDescription: fmt.Sprintf("Redirect URI scheme '%s' is not allowed", parsedURI.Scheme),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check allowed hosts
|
||||||
|
if len(s.config.Client.AllowedRedirectURIHosts) > 0 {
|
||||||
|
hostAllowed := false
|
||||||
|
for _, host := range s.config.Client.AllowedRedirectURIHosts {
|
||||||
|
if parsedURI.Host == host {
|
||||||
|
hostAllowed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hostAllowed {
|
||||||
|
return &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidRequest,
|
||||||
|
ErrorDescription: fmt.Sprintf("Redirect URI host '%s' is not allowed", parsedURI.Host),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValidGrantType checks if a grant type is valid
|
||||||
|
func (s *Service) isValidGrantType(grantType string) bool {
|
||||||
|
validGrantTypes := []string{
|
||||||
|
types.GrantTypeAuthorizationCode,
|
||||||
|
types.GrantTypeRefreshToken,
|
||||||
|
types.GrantTypeClientCredentials,
|
||||||
|
types.GrantTypeDeviceCode,
|
||||||
|
types.GrantTypeTokenExchange,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, valid := range validGrantTypes {
|
||||||
|
if grantType == valid {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValidResponseType checks if a response type is valid
|
||||||
|
func (s *Service) isValidResponseType(responseType string) bool {
|
||||||
|
validResponseTypes := []string{
|
||||||
|
types.ResponseTypeCode,
|
||||||
|
types.ResponseTypeToken,
|
||||||
|
types.ResponseTypeIDToken,
|
||||||
|
"code token",
|
||||||
|
"code id_token",
|
||||||
|
"token id_token",
|
||||||
|
"code token id_token",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, valid := range validResponseTypes {
|
||||||
|
if responseType == valid {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package oauth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
)
|
)
|
||||||
|
|
@ -19,34 +20,386 @@ func (s *Service) ProtectedResource(ctx context.Context) string {
|
||||||
// Authorize processes an authorization request and returns an authorization code
|
// Authorize processes an authorization request and returns an authorization code
|
||||||
// The authorization code can be exchanged for an access token
|
// The authorization code can be exchanged for an access token
|
||||||
func (s *Service) Authorize(ctx context.Context, request *types.AuthorizationRequest) (*types.AuthorizationResponse, error) {
|
func (s *Service) Authorize(ctx context.Context, request *types.AuthorizationRequest) (*types.AuthorizationResponse, error) {
|
||||||
// TODO: Implement authorization flow
|
// Validate client
|
||||||
return nil, nil
|
_, err := s.clientProvider.GetClientByID(ctx, request.ClientID)
|
||||||
|
if err != nil {
|
||||||
|
return &types.AuthorizationResponse{
|
||||||
|
Error: types.ErrorInvalidClient,
|
||||||
|
ErrorDescription: "Invalid client",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate redirect URI
|
||||||
|
if request.RedirectURI == "" {
|
||||||
|
return &types.AuthorizationResponse{
|
||||||
|
Error: types.ErrorInvalidRequest,
|
||||||
|
ErrorDescription: "Missing redirect URI",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
validationResult, err := s.clientProvider.ValidateRedirectURI(ctx, request.ClientID, request.RedirectURI)
|
||||||
|
if err != nil || !validationResult.Valid {
|
||||||
|
return &types.AuthorizationResponse{
|
||||||
|
Error: types.ErrorInvalidRequest,
|
||||||
|
ErrorDescription: "Invalid redirect URI",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate response type
|
||||||
|
if request.ResponseType == "" {
|
||||||
|
return &types.AuthorizationResponse{
|
||||||
|
Error: types.ErrorInvalidRequest,
|
||||||
|
ErrorDescription: "Missing response type",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
validResponseTypes := []string{"code", "token", "id_token"}
|
||||||
|
validResponseType := false
|
||||||
|
for _, validType := range validResponseTypes {
|
||||||
|
if request.ResponseType == validType || strings.Contains(request.ResponseType, validType) {
|
||||||
|
validResponseType = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !validResponseType {
|
||||||
|
return &types.AuthorizationResponse{
|
||||||
|
Error: types.ErrorUnsupportedResponseType,
|
||||||
|
ErrorDescription: "Unsupported response type",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate scope if provided
|
||||||
|
if request.Scope != "" {
|
||||||
|
scopes := strings.Fields(request.Scope)
|
||||||
|
scopeValidation, err := s.clientProvider.ValidateScope(ctx, request.ClientID, scopes)
|
||||||
|
if err != nil || !scopeValidation.Valid {
|
||||||
|
return &types.AuthorizationResponse{
|
||||||
|
Error: types.ErrorInvalidScope,
|
||||||
|
ErrorDescription: "Invalid scope",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate authorization code
|
||||||
|
authCode, err := s.generateAuthorizationCode(request.ClientID, request.State)
|
||||||
|
if err != nil {
|
||||||
|
return &types.AuthorizationResponse{
|
||||||
|
Error: types.ErrorServerError,
|
||||||
|
ErrorDescription: "Failed to generate authorization code",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &types.AuthorizationResponse{
|
||||||
|
Code: authCode,
|
||||||
|
State: request.State,
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token exchanges an authorization code for an access token
|
// Token exchanges an authorization code for an access token
|
||||||
// This is the core token endpoint functionality
|
// This is the core token endpoint functionality
|
||||||
func (s *Service) Token(ctx context.Context, grantType string, code string, clientID string, codeVerifier string) (*types.Token, error) {
|
func (s *Service) Token(ctx context.Context, grantType string, code string, clientID string, codeVerifier string) (*types.Token, error) {
|
||||||
// TODO: Implement token exchange
|
// Validate client
|
||||||
return nil, nil
|
client, err := s.clientProvider.GetClientByID(ctx, clientID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidClient,
|
||||||
|
ErrorDescription: "Invalid client",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate grant type
|
||||||
|
switch grantType {
|
||||||
|
case types.GrantTypeAuthorizationCode:
|
||||||
|
return s.handleAuthorizationCodeGrant(ctx, client, code, codeVerifier)
|
||||||
|
case types.GrantTypeClientCredentials:
|
||||||
|
return s.handleClientCredentialsGrant(ctx, client)
|
||||||
|
case types.GrantTypeRefreshToken:
|
||||||
|
return s.handleRefreshTokenGrant(ctx, client, code) // code is refresh token in this case
|
||||||
|
default:
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorUnsupportedGrantType,
|
||||||
|
ErrorDescription: "Unsupported grant type",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Revoke revokes an access token or refresh token
|
// Revoke revokes an access token or refresh token
|
||||||
// Once revoked, the token cannot be used for accessing protected resources
|
// Once revoked, the token cannot be used for accessing protected resources
|
||||||
func (s *Service) Revoke(ctx context.Context, token string, tokenTypeHint string) error {
|
func (s *Service) Revoke(ctx context.Context, token string, tokenTypeHint string) error {
|
||||||
// TODO: Implement token revocation
|
// Revoke token using user provider
|
||||||
|
if err := s.userProvider.RevokeToken(token); err != nil {
|
||||||
|
return &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidToken,
|
||||||
|
ErrorDescription: "Failed to revoke token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefreshToken exchanges a refresh token for a new access token
|
// RefreshToken exchanges a refresh token for a new access token
|
||||||
// This allows clients to obtain fresh access tokens without user interaction
|
// This allows clients to obtain fresh access tokens without user interaction
|
||||||
func (s *Service) RefreshToken(ctx context.Context, refreshToken string, scope string) (*types.RefreshTokenResponse, error) {
|
func (s *Service) RefreshToken(ctx context.Context, refreshToken string, scope string) (*types.RefreshTokenResponse, error) {
|
||||||
// TODO: Implement refresh token exchange
|
// Validate refresh token
|
||||||
return nil, nil
|
if !s.userProvider.TokenExists(refreshToken) {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidGrant,
|
||||||
|
ErrorDescription: "Invalid refresh token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get token data
|
||||||
|
tokenData, err := s.userProvider.GetTokenData(refreshToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidGrant,
|
||||||
|
ErrorDescription: "Invalid refresh token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract client ID from token data
|
||||||
|
clientID, ok := tokenData["client_id"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidGrant,
|
||||||
|
ErrorDescription: "Invalid token format",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate client
|
||||||
|
client, err := s.clientProvider.GetClientByID(ctx, clientID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidClient,
|
||||||
|
ErrorDescription: "Invalid client",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate scope if provided
|
||||||
|
if scope != "" {
|
||||||
|
scopes := strings.Fields(scope)
|
||||||
|
scopeValidation, err := s.clientProvider.ValidateScope(ctx, client.ClientID, scopes)
|
||||||
|
if err != nil || !scopeValidation.Valid {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidScope,
|
||||||
|
ErrorDescription: "Invalid scope",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new access token
|
||||||
|
newAccessToken, err := s.generateAccessToken(clientID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorServerError,
|
||||||
|
ErrorDescription: "Failed to generate access token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &types.RefreshTokenResponse{
|
||||||
|
AccessToken: newAccessToken,
|
||||||
|
TokenType: "Bearer",
|
||||||
|
ExpiresIn: 3600, // 1 hour
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include scope if provided
|
||||||
|
if scope != "" {
|
||||||
|
response.Scope = scope
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include refresh token if rotation is enabled
|
||||||
|
if s.config.Features.RefreshTokenRotationEnabled {
|
||||||
|
newRefreshToken, err := s.generateRefreshToken(clientID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorServerError,
|
||||||
|
ErrorDescription: "Failed to generate refresh token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response.RefreshToken = newRefreshToken
|
||||||
|
|
||||||
|
// Revoke old refresh token
|
||||||
|
s.userProvider.RevokeToken(refreshToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RotateRefreshToken rotates a refresh token and invalidates the old one
|
// RotateRefreshToken rotates a refresh token and invalidates the old one
|
||||||
// This implements refresh token rotation for enhanced security
|
// This implements refresh token rotation for enhanced security
|
||||||
func (s *Service) RotateRefreshToken(ctx context.Context, oldToken string) (*types.RefreshTokenResponse, error) {
|
func (s *Service) RotateRefreshToken(ctx context.Context, oldToken string) (*types.RefreshTokenResponse, error) {
|
||||||
// TODO: Implement refresh token rotation
|
// Check if refresh token rotation is enabled
|
||||||
return nil, nil
|
if !s.config.Features.RefreshTokenRotationEnabled {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidRequest,
|
||||||
|
ErrorDescription: "Refresh token rotation is not enabled",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate old token
|
||||||
|
if !s.userProvider.TokenExists(oldToken) {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidGrant,
|
||||||
|
ErrorDescription: "Invalid refresh token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get token data
|
||||||
|
tokenData, err := s.userProvider.GetTokenData(oldToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidGrant,
|
||||||
|
ErrorDescription: "Invalid refresh token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract client ID from token data
|
||||||
|
clientID, ok := tokenData["client_id"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidGrant,
|
||||||
|
ErrorDescription: "Invalid token format",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new tokens
|
||||||
|
newAccessToken, err := s.generateAccessToken(clientID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorServerError,
|
||||||
|
ErrorDescription: "Failed to generate access token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newRefreshToken, err := s.generateRefreshToken(clientID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorServerError,
|
||||||
|
ErrorDescription: "Failed to generate refresh token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revoke old token
|
||||||
|
err = s.userProvider.RevokeToken(oldToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorServerError,
|
||||||
|
ErrorDescription: "Failed to revoke old token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &types.RefreshTokenResponse{
|
||||||
|
AccessToken: newAccessToken,
|
||||||
|
RefreshToken: newRefreshToken,
|
||||||
|
TokenType: "Bearer",
|
||||||
|
ExpiresIn: 3600, // 1 hour
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper methods for token grant types
|
||||||
|
|
||||||
|
// handleAuthorizationCodeGrant handles authorization code grant
|
||||||
|
func (s *Service) handleAuthorizationCodeGrant(ctx context.Context, client *types.ClientInfo, code string, codeVerifier string) (*types.Token, error) {
|
||||||
|
// TODO: Validate authorization code
|
||||||
|
// In a real implementation, this would validate the authorization code and extract user info
|
||||||
|
|
||||||
|
// Generate access token
|
||||||
|
accessToken, err := s.generateAccessToken(client.ClientID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorServerError,
|
||||||
|
ErrorDescription: "Failed to generate access token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
token := &types.Token{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
TokenType: "Bearer",
|
||||||
|
ExpiresIn: 3600, // 1 hour
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate refresh token if supported
|
||||||
|
if types.Contains(client.GrantTypes, types.GrantTypeRefreshToken) {
|
||||||
|
refreshToken, err := s.generateRefreshToken(client.ClientID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorServerError,
|
||||||
|
ErrorDescription: "Failed to generate refresh token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
token.RefreshToken = refreshToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleClientCredentialsGrant handles client credentials grant
|
||||||
|
func (s *Service) handleClientCredentialsGrant(ctx context.Context, client *types.ClientInfo) (*types.Token, error) {
|
||||||
|
// Generate access token
|
||||||
|
accessToken, err := s.generateAccessToken(client.ClientID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorServerError,
|
||||||
|
ErrorDescription: "Failed to generate access token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
token := &types.Token{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
TokenType: "Bearer",
|
||||||
|
ExpiresIn: 3600, // 1 hour
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleRefreshTokenGrant handles refresh token grant
|
||||||
|
func (s *Service) handleRefreshTokenGrant(ctx context.Context, client *types.ClientInfo, refreshToken string) (*types.Token, error) {
|
||||||
|
// Validate refresh token
|
||||||
|
if !s.userProvider.TokenExists(refreshToken) {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidGrant,
|
||||||
|
ErrorDescription: "Invalid refresh token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new access token
|
||||||
|
accessToken, err := s.generateAccessToken(client.ClientID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorServerError,
|
||||||
|
ErrorDescription: "Failed to generate access token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
token := &types.Token{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
TokenType: "Bearer",
|
||||||
|
ExpiresIn: 3600, // 1 hour
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include refresh token if rotation is enabled
|
||||||
|
if s.config.Features.RefreshTokenRotationEnabled {
|
||||||
|
newRefreshToken, err := s.generateRefreshToken(client.ClientID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorServerError,
|
||||||
|
ErrorDescription: "Failed to generate refresh token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
token.RefreshToken = newRefreshToken
|
||||||
|
|
||||||
|
// Revoke old refresh token
|
||||||
|
s.userProvider.RevokeToken(refreshToken)
|
||||||
|
} else {
|
||||||
|
// Reuse the same refresh token
|
||||||
|
token.RefreshToken = refreshToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package oauth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
)
|
)
|
||||||
|
|
@ -9,20 +10,87 @@ import (
|
||||||
// JWKS returns the JSON Web Key Set for token verification
|
// JWKS returns the JSON Web Key Set for token verification
|
||||||
// This endpoint provides public keys for validating JWT tokens
|
// This endpoint provides public keys for validating JWT tokens
|
||||||
func (s *Service) JWKS(ctx context.Context) (*types.JWKSResponse, error) {
|
func (s *Service) JWKS(ctx context.Context) (*types.JWKSResponse, error) {
|
||||||
// TODO: Implement JWKS endpoint
|
// TODO: Implement JWKS endpoint - this requires certificate/key management
|
||||||
return nil, nil
|
// For now, return empty JWKS
|
||||||
|
return &types.JWKSResponse{
|
||||||
|
Keys: []types.JWK{},
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Endpoints returns a map of all available OAuth endpoints
|
// Endpoints returns a map of all available OAuth endpoints
|
||||||
// This provides endpoint discovery for clients
|
// This provides endpoint discovery for clients
|
||||||
func (s *Service) Endpoints(ctx context.Context) (map[string]string, error) {
|
func (s *Service) Endpoints(ctx context.Context) (map[string]string, error) {
|
||||||
// TODO: Implement endpoint discovery
|
baseURL := s.config.IssuerURL
|
||||||
return nil, nil
|
|
||||||
|
endpoints := map[string]string{
|
||||||
|
"authorization_endpoint": fmt.Sprintf("%s/oauth/authorize", baseURL),
|
||||||
|
"token_endpoint": fmt.Sprintf("%s/oauth/token", baseURL),
|
||||||
|
"userinfo_endpoint": fmt.Sprintf("%s/oauth/userinfo", baseURL),
|
||||||
|
"jwks_uri": fmt.Sprintf("%s/oauth/jwks", baseURL),
|
||||||
|
"registration_endpoint": fmt.Sprintf("%s/oauth/register", baseURL),
|
||||||
|
"introspection_endpoint": fmt.Sprintf("%s/oauth/introspect", baseURL),
|
||||||
|
"revocation_endpoint": fmt.Sprintf("%s/oauth/revoke", baseURL),
|
||||||
|
"device_authorization_endpoint": fmt.Sprintf("%s/oauth/device", baseURL),
|
||||||
|
"pushed_authorization_request_endpoint": fmt.Sprintf("%s/oauth/par", baseURL),
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpoints, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetServerMetadata returns OAuth 2.0 Authorization Server Metadata
|
// GetServerMetadata returns OAuth 2.0 Authorization Server Metadata
|
||||||
// This implements RFC 8414 for server discovery
|
// This implements RFC 8414 for server discovery
|
||||||
func (s *Service) GetServerMetadata(ctx context.Context) (*types.AuthorizationServerMetadata, error) {
|
func (s *Service) GetServerMetadata(ctx context.Context) (*types.AuthorizationServerMetadata, error) {
|
||||||
// TODO: Implement server metadata
|
endpoints, err := s.Endpoints(ctx)
|
||||||
return nil, nil
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata := &types.AuthorizationServerMetadata{
|
||||||
|
Issuer: s.config.IssuerURL,
|
||||||
|
AuthorizationEndpoint: endpoints["authorization_endpoint"],
|
||||||
|
TokenEndpoint: endpoints["token_endpoint"],
|
||||||
|
UserinfoEndpoint: endpoints["userinfo_endpoint"],
|
||||||
|
JwksURI: endpoints["jwks_uri"],
|
||||||
|
RegistrationEndpoint: endpoints["registration_endpoint"],
|
||||||
|
ScopesSupported: []string{"openid", "profile", "email", "address", "phone", "offline_access"},
|
||||||
|
ResponseTypesSupported: []string{"code", "token", "id_token", "code token", "code id_token", "token id_token", "code token id_token"},
|
||||||
|
ResponseModesSupported: []string{"query", "fragment", "form_post"},
|
||||||
|
GrantTypesSupported: []string{"authorization_code", "client_credentials", "refresh_token"},
|
||||||
|
TokenEndpointAuthMethodsSupported: []string{"client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt"},
|
||||||
|
TokenEndpointAuthSigningAlgValuesSupported: []string{"RS256", "HS256"},
|
||||||
|
ServiceDocumentation: fmt.Sprintf("%s/docs", s.config.IssuerURL),
|
||||||
|
UILocalesSupported: []string{"en-US", "en-GB", "en-CA", "fr-FR", "fr-CA"},
|
||||||
|
OpPolicyURI: fmt.Sprintf("%s/policy", s.config.IssuerURL),
|
||||||
|
OpTosURI: fmt.Sprintf("%s/terms", s.config.IssuerURL),
|
||||||
|
RevocationEndpoint: endpoints["revocation_endpoint"],
|
||||||
|
RevocationEndpointAuthMethodsSupported: []string{"client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt"},
|
||||||
|
IntrospectionEndpoint: endpoints["introspection_endpoint"],
|
||||||
|
IntrospectionEndpointAuthMethodsSupported: []string{"client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt"},
|
||||||
|
CodeChallengeMethodsSupported: []string{"plain", "S256"},
|
||||||
|
DeviceAuthorizationEndpoint: endpoints["device_authorization_endpoint"],
|
||||||
|
PushedAuthorizationRequestEndpoint: endpoints["pushed_authorization_request_endpoint"],
|
||||||
|
RequirePushedAuthorizationRequests: false,
|
||||||
|
DPoPSigningAlgValuesSupported: []string{"RS256", "PS256", "ES256"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add feature-specific endpoints and capabilities
|
||||||
|
if s.config.Features.DeviceFlowEnabled {
|
||||||
|
metadata.DeviceAuthorizationEndpoint = endpoints["device_authorization_endpoint"]
|
||||||
|
metadata.GrantTypesSupported = append(metadata.GrantTypesSupported, "urn:ietf:params:oauth:grant-type:device_code")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.config.Features.TokenExchangeEnabled {
|
||||||
|
metadata.GrantTypesSupported = append(metadata.GrantTypesSupported, "urn:ietf:params:oauth:grant-type:token-exchange")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.config.Features.PushedAuthorizationEnabled {
|
||||||
|
metadata.PushedAuthorizationRequestEndpoint = endpoints["pushed_authorization_request_endpoint"]
|
||||||
|
metadata.RequirePushedAuthorizationRequests = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.config.Features.DynamicClientRegistrationEnabled {
|
||||||
|
metadata.RegistrationEndpoint = endpoints["registration_endpoint"]
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ type Service struct {
|
||||||
cache store.Store
|
cache store.Store
|
||||||
userProvider types.UserProvider
|
userProvider types.UserProvider
|
||||||
clientProvider types.ClientProvider
|
clientProvider types.ClientProvider
|
||||||
|
prefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config OAuth service configuration
|
// Config OAuth service configuration
|
||||||
|
|
@ -98,10 +99,11 @@ func NewService(config *Config) (*Service, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use UserProvider from config, or create a default one if not provided
|
// Use UserProvider from config, or create a default one if not provided
|
||||||
|
keyPrefix := fmt.Sprintf("%s:", share.App.Prefix)
|
||||||
userProvider := config.UserProvider
|
userProvider := config.UserProvider
|
||||||
if userProvider == nil {
|
if userProvider == nil {
|
||||||
userProvider = user.NewDefaultUser(&user.DefaultUserOptions{
|
userProvider = user.NewDefaultUser(&user.DefaultUserOptions{
|
||||||
Prefix: fmt.Sprintf("%s:", share.App.Prefix),
|
Prefix: keyPrefix,
|
||||||
Model: "__yao.user",
|
Model: "__yao.user",
|
||||||
Cache: config.Cache,
|
Cache: config.Cache,
|
||||||
TokenStore: config.Store,
|
TokenStore: config.Store,
|
||||||
|
|
@ -113,7 +115,7 @@ func NewService(config *Config) (*Service, error) {
|
||||||
if clientProvider == nil {
|
if clientProvider == nil {
|
||||||
var err error
|
var err error
|
||||||
clientProvider, err = client.NewDefaultClient(&client.DefaultClientOptions{
|
clientProvider, err = client.NewDefaultClient(&client.DefaultClientOptions{
|
||||||
Prefix: fmt.Sprintf("%s:", share.App.Prefix),
|
Prefix: keyPrefix,
|
||||||
Store: config.Store,
|
Store: config.Store,
|
||||||
Cache: config.Cache,
|
Cache: config.Cache,
|
||||||
})
|
})
|
||||||
|
|
@ -128,6 +130,7 @@ func NewService(config *Config) (*Service, error) {
|
||||||
cache: config.Cache,
|
cache: config.Cache,
|
||||||
userProvider: userProvider,
|
userProvider: userProvider,
|
||||||
clientProvider: clientProvider,
|
clientProvider: clientProvider,
|
||||||
|
prefix: keyPrefix,
|
||||||
}
|
}
|
||||||
|
|
||||||
return service, nil
|
return service, nil
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,12 @@ package oauth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
)
|
)
|
||||||
|
|
@ -9,42 +15,234 @@ import (
|
||||||
// GenerateCodeChallenge generates a code challenge from a code verifier
|
// GenerateCodeChallenge generates a code challenge from a code verifier
|
||||||
// This is used for PKCE (Proof Key for Code Exchange) flow
|
// This is used for PKCE (Proof Key for Code Exchange) flow
|
||||||
func (s *Service) GenerateCodeChallenge(ctx context.Context, codeVerifier string, method string) (string, error) {
|
func (s *Service) GenerateCodeChallenge(ctx context.Context, codeVerifier string, method string) (string, error) {
|
||||||
// TODO: Implement code challenge generation
|
switch method {
|
||||||
return "", nil
|
case "S256":
|
||||||
|
// SHA256 hash of the code verifier
|
||||||
|
hash := sha256.Sum256([]byte(codeVerifier))
|
||||||
|
return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(hash[:]), nil
|
||||||
|
case "plain":
|
||||||
|
// Plain text code verifier (not recommended for production)
|
||||||
|
return codeVerifier, nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unsupported code challenge method: %s", method)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateCodeChallenge validates a code verifier against a code challenge
|
// ValidateCodeChallenge validates a code verifier against a code challenge
|
||||||
// This verifies the PKCE code challenge during token exchange
|
// This verifies the PKCE code challenge during token exchange
|
||||||
func (s *Service) ValidateCodeChallenge(ctx context.Context, codeVerifier string, codeChallenge string, method string) error {
|
func (s *Service) ValidateCodeChallenge(ctx context.Context, codeVerifier string, codeChallenge string, method string) error {
|
||||||
// TODO: Implement code challenge validation
|
expectedChallenge, err := s.GenerateCodeChallenge(ctx, codeVerifier, method)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if expectedChallenge != codeChallenge {
|
||||||
|
return fmt.Errorf("code challenge verification failed")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateStateParameter validates OAuth state parameters
|
// ValidateStateParameter validates OAuth state parameters
|
||||||
// This prevents CSRF attacks by verifying state parameters
|
// This prevents CSRF attacks by verifying state parameters
|
||||||
func (s *Service) ValidateStateParameter(ctx context.Context, state string, clientID string) (*types.ValidationResult, error) {
|
func (s *Service) ValidateStateParameter(ctx context.Context, state string, clientID string) (*types.ValidationResult, error) {
|
||||||
// TODO: Implement state parameter validation
|
result := &types.ValidationResult{Valid: false}
|
||||||
return nil, nil
|
|
||||||
|
// Get state parameter from store
|
||||||
|
stateKey := s.stateParameterKey(clientID, state)
|
||||||
|
|
||||||
|
// Try cache first if available
|
||||||
|
if s.cache != nil {
|
||||||
|
if cached, ok := s.cache.Get(stateKey); ok {
|
||||||
|
if stateParam, ok := cached.(*types.StateParameter); ok {
|
||||||
|
// Check if state parameter is still valid
|
||||||
|
if time.Now().Before(stateParam.ExpiresAt) {
|
||||||
|
result.Valid = true
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try store
|
||||||
|
data, ok := s.store.Get(stateKey)
|
||||||
|
if !ok {
|
||||||
|
result.Errors = append(result.Errors, "State parameter not found")
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse state parameter from store
|
||||||
|
stateParam, ok := data.(*types.StateParameter)
|
||||||
|
if !ok {
|
||||||
|
result.Errors = append(result.Errors, "Invalid state parameter format")
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if state parameter is still valid
|
||||||
|
if time.Now().After(stateParam.ExpiresAt) {
|
||||||
|
result.Errors = append(result.Errors, "State parameter has expired")
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that the state parameter belongs to the client
|
||||||
|
if stateParam.ClientID != clientID {
|
||||||
|
result.Errors = append(result.Errors, "State parameter does not belong to this client")
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Valid = true
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateStateParameter generates a secure state parameter
|
// GenerateStateParameter generates a secure state parameter
|
||||||
// This creates cryptographically secure state values for CSRF protection
|
// This creates cryptographically secure state values for CSRF protection
|
||||||
func (s *Service) GenerateStateParameter(ctx context.Context, clientID string) (*types.StateParameter, error) {
|
func (s *Service) GenerateStateParameter(ctx context.Context, clientID string) (*types.StateParameter, error) {
|
||||||
// TODO: Implement state parameter generation
|
// Generate random state value
|
||||||
return nil, nil
|
length := s.config.Security.StateParameterLength
|
||||||
|
if length == 0 {
|
||||||
|
length = 32
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes := make([]byte, length)
|
||||||
|
if _, err := rand.Read(bytes); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate state parameter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stateValue := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(bytes)
|
||||||
|
|
||||||
|
// Create state parameter
|
||||||
|
stateParam := &types.StateParameter{
|
||||||
|
Value: stateValue,
|
||||||
|
ClientID: clientID,
|
||||||
|
ExpiresAt: time.Now().Add(s.config.Security.StateParameterLifetime),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store state parameter
|
||||||
|
stateKey := s.stateParameterKey(clientID, stateValue)
|
||||||
|
|
||||||
|
// Store in cache if available
|
||||||
|
if s.cache != nil {
|
||||||
|
s.cache.Set(stateKey, stateParam, s.config.Security.StateParameterLifetime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store in persistent store
|
||||||
|
if err := s.store.Set(stateKey, stateParam, s.config.Security.StateParameterLifetime); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to store state parameter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return stateParam, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateRedirectURI validates redirect URIs against registered URIs
|
// ValidateRedirectURI validates redirect URIs against registered URIs
|
||||||
func (s *Service) ValidateRedirectURI(ctx context.Context, redirectURI string, registeredURIs []string) (*types.ValidationResult, error) {
|
func (s *Service) ValidateRedirectURI(ctx context.Context, redirectURI string, registeredURIs []string) (*types.ValidationResult, error) {
|
||||||
// This method signature doesn't match our ClientProvider interface
|
// This method signature doesn't match our ClientProvider interface
|
||||||
// We need the clientID to validate, so let's assume we can extract it from context
|
// For now, we'll do a basic validation since we don't have a clientID
|
||||||
// or we need to modify the interface
|
result := &types.ValidationResult{Valid: false}
|
||||||
return &types.ValidationResult{Valid: true}, nil
|
|
||||||
|
// If no registered URIs provided, cannot validate
|
||||||
|
if len(registeredURIs) == 0 {
|
||||||
|
result.Errors = append(result.Errors, "No registered URIs provided")
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if redirect URI matches any registered URI
|
||||||
|
for _, uri := range registeredURIs {
|
||||||
|
if uri == redirectURI {
|
||||||
|
result.Valid = true
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Errors = append(result.Errors, "Redirect URI not found in registered URIs")
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateRedirectURIForClient validates redirect URIs for a specific client
|
||||||
|
func (s *Service) ValidateRedirectURIForClient(ctx context.Context, clientID string, redirectURI string) (*types.ValidationResult, error) {
|
||||||
|
return s.clientProvider.ValidateRedirectURI(ctx, clientID, redirectURI)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PushAuthorizationRequest processes a pushed authorization request
|
// PushAuthorizationRequest processes a pushed authorization request
|
||||||
// This implements RFC 9126 for enhanced security
|
// This implements RFC 9126 for enhanced security
|
||||||
func (s *Service) PushAuthorizationRequest(ctx context.Context, request *types.PushedAuthorizationRequest) (*types.PushedAuthorizationResponse, error) {
|
func (s *Service) PushAuthorizationRequest(ctx context.Context, request *types.PushedAuthorizationRequest) (*types.PushedAuthorizationResponse, error) {
|
||||||
// TODO: Implement pushed authorization request
|
// Validate client
|
||||||
return nil, nil
|
_, err := s.clientProvider.GetClientByID(ctx, request.ClientID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidClient,
|
||||||
|
ErrorDescription: "Invalid client",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate redirect URI
|
||||||
|
validationResult, err := s.clientProvider.ValidateRedirectURI(ctx, request.ClientID, request.RedirectURI)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !validationResult.Valid {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidRequest,
|
||||||
|
ErrorDescription: "Invalid redirect URI",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate scopes if provided
|
||||||
|
if request.Scope != "" {
|
||||||
|
scopes := strings.Fields(request.Scope)
|
||||||
|
scopeValidation, err := s.clientProvider.ValidateScope(ctx, request.ClientID, scopes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !scopeValidation.Valid {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidScope,
|
||||||
|
ErrorDescription: "Invalid scope",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate request URI
|
||||||
|
requestURI := s.generateRequestURI()
|
||||||
|
|
||||||
|
// Store the request
|
||||||
|
requestKey := s.pushedAuthRequestKey(requestURI)
|
||||||
|
expiresIn := 600 // 10 minutes
|
||||||
|
|
||||||
|
if s.cache != nil {
|
||||||
|
s.cache.Set(requestKey, request, time.Duration(expiresIn)*time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.store.Set(requestKey, request, time.Duration(expiresIn)*time.Second); err != nil {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorServerError,
|
||||||
|
ErrorDescription: "Failed to store pushed authorization request",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &types.PushedAuthorizationResponse{
|
||||||
|
RequestURI: requestURI,
|
||||||
|
ExpiresIn: expiresIn,
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
|
||||||
|
// stateParameterKey generates a key for state parameter storage
|
||||||
|
func (s *Service) stateParameterKey(clientID string, state string) string {
|
||||||
|
return fmt.Sprintf("%soauth:state:%s:%s", s.prefix, clientID, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pushedAuthRequestKey generates a key for pushed authorization request storage
|
||||||
|
func (s *Service) pushedAuthRequestKey(requestURI string) string {
|
||||||
|
return fmt.Sprintf("%soauth:par:%s", s.prefix, requestURI)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateRequestURI generates a request URI for pushed authorization requests
|
||||||
|
func (s *Service) generateRequestURI() string {
|
||||||
|
bytes := make([]byte, 32)
|
||||||
|
rand.Read(bytes)
|
||||||
|
return fmt.Sprintf("urn:ietf:params:oauth:request_uri:%s",
|
||||||
|
base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(bytes))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@ package oauth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
)
|
)
|
||||||
|
|
@ -9,27 +14,255 @@ import (
|
||||||
// Introspect returns information about an access token
|
// Introspect returns information about an access token
|
||||||
// This endpoint allows resource servers to validate tokens
|
// This endpoint allows resource servers to validate tokens
|
||||||
func (s *Service) Introspect(ctx context.Context, token string) (*types.TokenIntrospectionResponse, error) {
|
func (s *Service) Introspect(ctx context.Context, token string) (*types.TokenIntrospectionResponse, error) {
|
||||||
// TODO: Implement token introspection
|
// Try to get token data from user provider
|
||||||
return nil, nil
|
tokenData, err := s.userProvider.GetTokenData(token)
|
||||||
|
if err != nil {
|
||||||
|
return &types.TokenIntrospectionResponse{Active: false}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token exists and is valid
|
||||||
|
if tokenData == nil {
|
||||||
|
return &types.TokenIntrospectionResponse{Active: false}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract token information
|
||||||
|
response := &types.TokenIntrospectionResponse{
|
||||||
|
Active: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract standard fields from token data
|
||||||
|
if clientID, ok := tokenData["client_id"].(string); ok {
|
||||||
|
response.ClientID = clientID
|
||||||
|
}
|
||||||
|
if username, ok := tokenData["username"].(string); ok {
|
||||||
|
response.Username = username
|
||||||
|
}
|
||||||
|
if subject, ok := tokenData["sub"].(string); ok {
|
||||||
|
response.Subject = subject
|
||||||
|
}
|
||||||
|
if tokenType, ok := tokenData["token_type"].(string); ok {
|
||||||
|
response.TokenType = tokenType
|
||||||
|
} else {
|
||||||
|
response.TokenType = "Bearer"
|
||||||
|
}
|
||||||
|
if scope, ok := tokenData["scope"].(string); ok {
|
||||||
|
response.Scope = scope
|
||||||
|
}
|
||||||
|
if exp, ok := tokenData["exp"].(int64); ok {
|
||||||
|
response.ExpiresAt = exp
|
||||||
|
}
|
||||||
|
if iat, ok := tokenData["iat"].(int64); ok {
|
||||||
|
response.IssuedAt = iat
|
||||||
|
}
|
||||||
|
if nbf, ok := tokenData["nbf"].(int64); ok {
|
||||||
|
response.NotBefore = nbf
|
||||||
|
}
|
||||||
|
if aud, ok := tokenData["aud"].([]string); ok {
|
||||||
|
response.Audience = aud
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token is expired
|
||||||
|
if response.ExpiresAt > 0 && time.Now().Unix() > response.ExpiresAt {
|
||||||
|
response.Active = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenExchange exchanges one token for another token
|
// TokenExchange exchanges one token for another token
|
||||||
// This implements RFC 8693 for token exchange scenarios
|
// This implements RFC 8693 for token exchange scenarios
|
||||||
func (s *Service) TokenExchange(ctx context.Context, subjectToken string, subjectTokenType string, audience string, scope string) (*types.TokenExchangeResponse, error) {
|
func (s *Service) TokenExchange(ctx context.Context, subjectToken string, subjectTokenType string, audience string, scope string) (*types.TokenExchangeResponse, error) {
|
||||||
// TODO: Implement token exchange
|
// Check if token exchange is enabled
|
||||||
return nil, nil
|
if !s.config.Features.TokenExchangeEnabled {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorUnsupportedGrantType,
|
||||||
|
ErrorDescription: "Token exchange is not enabled",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate subject token
|
||||||
|
introspectionResult, err := s.Introspect(ctx, subjectToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidGrant,
|
||||||
|
ErrorDescription: "Invalid subject token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !introspectionResult.Active {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidGrant,
|
||||||
|
ErrorDescription: "Subject token is not active",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate audience if provided
|
||||||
|
if audience != "" {
|
||||||
|
if err := s.validateAudience(audience); err != nil {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidRequest,
|
||||||
|
ErrorDescription: "Invalid audience",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate scope if provided
|
||||||
|
if scope != "" {
|
||||||
|
scopes := strings.Fields(scope)
|
||||||
|
if introspectionResult.ClientID != "" {
|
||||||
|
scopeValidation, err := s.clientProvider.ValidateScope(ctx, introspectionResult.ClientID, scopes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !scopeValidation.Valid {
|
||||||
|
return nil, &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidScope,
|
||||||
|
ErrorDescription: "Invalid scope",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new token (placeholder implementation)
|
||||||
|
// In a real implementation, this would generate a JWT or opaque token
|
||||||
|
newToken := "exchanged_" + subjectToken[:20] + "_" + audience
|
||||||
|
|
||||||
|
response := &types.TokenExchangeResponse{
|
||||||
|
AccessToken: newToken,
|
||||||
|
IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token",
|
||||||
|
TokenType: "Bearer",
|
||||||
|
ExpiresIn: 3600, // 1 hour
|
||||||
|
}
|
||||||
|
|
||||||
|
if scope != "" {
|
||||||
|
response.Scope = scope
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateTokenAudience validates token audience claims
|
// ValidateTokenAudience validates token audience claims
|
||||||
// This ensures tokens are only used with their intended audiences
|
// This ensures tokens are only used with their intended audiences
|
||||||
func (s *Service) ValidateTokenAudience(ctx context.Context, token string, expectedAudience string) (*types.ValidationResult, error) {
|
func (s *Service) ValidateTokenAudience(ctx context.Context, token string, expectedAudience string) (*types.ValidationResult, error) {
|
||||||
// TODO: Implement token audience validation
|
result := &types.ValidationResult{Valid: false}
|
||||||
return nil, nil
|
|
||||||
|
// Get token introspection
|
||||||
|
introspectionResult, err := s.Introspect(ctx, token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !introspectionResult.Active {
|
||||||
|
result.Errors = append(result.Errors, "Token is not active")
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check audience
|
||||||
|
if len(introspectionResult.Audience) == 0 {
|
||||||
|
// If no audience is specified in token, allow access
|
||||||
|
result.Valid = true
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if expected audience is in token audience list
|
||||||
|
for _, aud := range introspectionResult.Audience {
|
||||||
|
if aud == expectedAudience {
|
||||||
|
result.Valid = true
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Errors = append(result.Errors, "Token audience does not match expected audience")
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateTokenBinding validates token binding information
|
// ValidateTokenBinding validates token binding information
|
||||||
// This ensures tokens are bound to the correct client or device
|
// This ensures tokens are bound to the correct client or device
|
||||||
func (s *Service) ValidateTokenBinding(ctx context.Context, token string, binding *types.TokenBinding) (*types.ValidationResult, error) {
|
func (s *Service) ValidateTokenBinding(ctx context.Context, token string, binding *types.TokenBinding) (*types.ValidationResult, error) {
|
||||||
// TODO: Implement token binding validation
|
result := &types.ValidationResult{Valid: false}
|
||||||
return nil, nil
|
|
||||||
|
// Check if token binding is enabled
|
||||||
|
if !s.config.Features.TokenBindingEnabled {
|
||||||
|
result.Valid = true // If not enabled, always valid
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get token introspection
|
||||||
|
introspectionResult, err := s.Introspect(ctx, token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !introspectionResult.Active {
|
||||||
|
result.Errors = append(result.Errors, "Token is not active")
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate binding type
|
||||||
|
switch binding.BindingType {
|
||||||
|
case types.TokenBindingTypeDPoP:
|
||||||
|
// DPoP binding validation would go here
|
||||||
|
result.Valid = true // Placeholder
|
||||||
|
case types.TokenBindingTypeMTLS:
|
||||||
|
// mTLS binding validation would go here
|
||||||
|
result.Valid = true // Placeholder
|
||||||
|
case types.TokenBindingTypeCertificate:
|
||||||
|
// Certificate binding validation would go here
|
||||||
|
result.Valid = true // Placeholder
|
||||||
|
default:
|
||||||
|
result.Errors = append(result.Errors, "Unknown token binding type")
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
|
||||||
|
// validateAudience validates if an audience is valid
|
||||||
|
func (s *Service) validateAudience(audience string) error {
|
||||||
|
// Basic audience validation
|
||||||
|
if audience == "" {
|
||||||
|
return &types.ErrorResponse{
|
||||||
|
Code: types.ErrorInvalidRequest,
|
||||||
|
ErrorDescription: "Audience cannot be empty",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add more sophisticated audience validation here
|
||||||
|
// For example, checking against a whitelist of valid audiences
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token generation helper methods
|
||||||
|
|
||||||
|
// generateAccessToken generates a new access token
|
||||||
|
func (s *Service) generateAccessToken(clientID string) (string, error) {
|
||||||
|
return s.generateToken("ak", clientID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateRefreshToken generates a new refresh token
|
||||||
|
func (s *Service) generateRefreshToken(clientID string) (string, error) {
|
||||||
|
return s.generateToken("rfk", clientID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateAuthorizationCode generates a new authorization code
|
||||||
|
func (s *Service) generateAuthorizationCode(clientID string, state string) (string, error) {
|
||||||
|
return s.generateToken("ac", clientID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateToken generates a token with the specified type and client ID
|
||||||
|
func (s *Service) generateToken(tokenType string, clientID string) (string, error) {
|
||||||
|
// Generate random bytes for token
|
||||||
|
randomBytes := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(randomBytes); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to generate random bytes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create token with type, client ID, timestamp, and random component
|
||||||
|
randomPart := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(randomBytes)
|
||||||
|
timestamp := time.Now().Format("20060102150405")
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s_%s_%s_%s", tokenType, clientID, timestamp, randomPart), nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,9 @@ const (
|
||||||
|
|
||||||
// OAuth 2.1 Response Types
|
// OAuth 2.1 Response Types
|
||||||
const (
|
const (
|
||||||
ResponseTypeCode = "code"
|
ResponseTypeCode = "code"
|
||||||
|
ResponseTypeToken = "token"
|
||||||
|
ResponseTypeIDToken = "id_token"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OAuth 2.1 Token Types
|
// OAuth 2.1 Token Types
|
||||||
|
|
|
||||||
11
openapi/oauth/types/utils.go
Normal file
11
openapi/oauth/types/utils.go
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
// Contains checks if a slice contains a string
|
||||||
|
func Contains(slice []string, item string) bool {
|
||||||
|
for _, s := range slice {
|
||||||
|
if s == item {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue