mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2025-12-31 04:22:28 +00:00
Implement PKCE (Proof Key for Code Exchange) support
PKCE was advertised in the discovery document but not actually implemented. This commit adds full PKCE support: - Store code_challenge and code_challenge_method in authorization code JWT - Accept code_verifier parameter in token endpoint - Validate code_verifier against stored code_challenge - Support both S256 (SHA256) and plain code challenge methods - PKCE validation is required when code_challenge is present This prevents authorization code interception attacks by requiring the client to prove possession of the code_verifier that was used to generate the code_challenge.
This commit is contained in:
@@ -2,7 +2,6 @@ package controller
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -143,8 +142,8 @@ func (controller *OIDCController) authorizeHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Generate authorization code
|
||||
authCode, err := controller.oidc.GenerateAuthorizationCode(&userContext, clientID, redirectURI, scopes, nonce)
|
||||
// Generate authorization code (including PKCE challenge if provided)
|
||||
authCode, err := controller.oidc.GenerateAuthorizationCode(&userContext, clientID, redirectURI, scopes, nonce, codeChallenge, codeChallengeMethod)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to generate authorization code")
|
||||
controller.redirectError(c, redirectURI, state, "server_error", "Internal server error")
|
||||
@@ -223,14 +222,29 @@ func (controller *OIDCController) tokenHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get code_verifier for PKCE validation
|
||||
codeVerifier := c.PostForm("code_verifier")
|
||||
if codeVerifier == "" {
|
||||
codeVerifier = c.Query("code_verifier")
|
||||
}
|
||||
|
||||
// Validate authorization code
|
||||
userContext, scopes, nonce, err := controller.oidc.ValidateAuthorizationCode(code, clientID, redirectURI)
|
||||
userContext, scopes, nonce, codeChallenge, codeChallengeMethod, err := controller.oidc.ValidateAuthorizationCode(code, clientID, redirectURI)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to validate authorization code")
|
||||
controller.tokenError(c, "invalid_grant", "Invalid or expired authorization code")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate PKCE if code challenge was provided
|
||||
if codeChallenge != "" {
|
||||
if err := controller.oidc.ValidatePKCE(codeChallenge, codeChallengeMethod, codeVerifier); err != nil {
|
||||
log.Error().Err(err).Msg("PKCE validation failed")
|
||||
controller.tokenError(c, "invalid_grant", "Invalid code_verifier")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
accessToken, err := controller.oidc.GenerateAccessToken(userContext, clientID, scopes)
|
||||
if err != nil {
|
||||
|
||||
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
@@ -154,7 +155,7 @@ func (oidc *OIDCService) ValidateScope(client *model.OIDCClient, requestedScopes
|
||||
return validScopes, nil
|
||||
}
|
||||
|
||||
func (oidc *OIDCService) GenerateAuthorizationCode(userContext *config.UserContext, clientID string, redirectURI string, scopes []string, nonce string) (string, error) {
|
||||
func (oidc *OIDCService) GenerateAuthorizationCode(userContext *config.UserContext, clientID string, redirectURI string, scopes []string, nonce string, codeChallenge string, codeChallengeMethod string) (string, error) {
|
||||
code := uuid.New().String()
|
||||
|
||||
// Store authorization code in a temporary structure
|
||||
@@ -187,6 +188,17 @@ func (oidc *OIDCService) GenerateAuthorizationCode(userContext *config.UserConte
|
||||
claims["nonce"] = nonce
|
||||
}
|
||||
|
||||
// Store PKCE challenge if provided
|
||||
if codeChallenge != "" {
|
||||
claims["code_challenge"] = codeChallenge
|
||||
if codeChallengeMethod != "" {
|
||||
claims["code_challenge_method"] = codeChallengeMethod
|
||||
} else {
|
||||
// Default to plain if method not specified
|
||||
claims["code_challenge_method"] = "plain"
|
||||
}
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
codeToken, err := token.SignedString(oidc.privateKey)
|
||||
if err != nil {
|
||||
@@ -197,7 +209,7 @@ func (oidc *OIDCService) GenerateAuthorizationCode(userContext *config.UserConte
|
||||
return codeToken, nil
|
||||
}
|
||||
|
||||
func (oidc *OIDCService) ValidateAuthorizationCode(codeToken string, clientID string, redirectURI string) (*config.UserContext, []string, string, error) {
|
||||
func (oidc *OIDCService) ValidateAuthorizationCode(codeToken string, clientID string, redirectURI string) (*config.UserContext, []string, string, string, string, error) {
|
||||
token, err := jwt.Parse(codeToken, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
@@ -206,31 +218,31 @@ func (oidc *OIDCService) ValidateAuthorizationCode(codeToken string, clientID st
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, "", fmt.Errorf("failed to parse authorization code: %w", err)
|
||||
return nil, nil, "", "", "", fmt.Errorf("failed to parse authorization code: %w", err)
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
return nil, nil, "", errors.New("invalid authorization code")
|
||||
return nil, nil, "", "", "", errors.New("invalid authorization code")
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, nil, "", errors.New("invalid token claims")
|
||||
return nil, nil, "", "", "", errors.New("invalid token claims")
|
||||
}
|
||||
|
||||
// Verify client_id and redirect_uri match
|
||||
if claims["client_id"] != clientID {
|
||||
return nil, nil, "", errors.New("client_id mismatch")
|
||||
return nil, nil, "", "", "", errors.New("client_id mismatch")
|
||||
}
|
||||
|
||||
if claims["redirect_uri"] != redirectURI {
|
||||
return nil, nil, "", errors.New("redirect_uri mismatch")
|
||||
return nil, nil, "", "", "", errors.New("redirect_uri mismatch")
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
exp, ok := claims["exp"].(float64)
|
||||
if !ok || time.Now().Unix() > int64(exp) {
|
||||
return nil, nil, "", errors.New("authorization code expired")
|
||||
return nil, nil, "", "", "", errors.New("authorization code expired")
|
||||
}
|
||||
|
||||
userContext := &config.UserContext{
|
||||
@@ -251,8 +263,41 @@ func (oidc *OIDCService) ValidateAuthorizationCode(codeToken string, clientID st
|
||||
}
|
||||
|
||||
nonce := getStringClaim(claims, "nonce")
|
||||
codeChallenge := getStringClaim(claims, "code_challenge")
|
||||
codeChallengeMethod := getStringClaim(claims, "code_challenge_method")
|
||||
|
||||
return userContext, scopes, nonce, nil
|
||||
return userContext, scopes, nonce, codeChallenge, codeChallengeMethod, nil
|
||||
}
|
||||
|
||||
func (oidc *OIDCService) ValidatePKCE(codeChallenge string, codeChallengeMethod string, codeVerifier string) error {
|
||||
if codeChallenge == "" {
|
||||
// PKCE not used, validation passes
|
||||
return nil
|
||||
}
|
||||
|
||||
if codeVerifier == "" {
|
||||
return errors.New("code_verifier required when code_challenge is present")
|
||||
}
|
||||
|
||||
switch codeChallengeMethod {
|
||||
case "S256":
|
||||
// Compute SHA256 hash of code_verifier
|
||||
hash := sha256.Sum256([]byte(codeVerifier))
|
||||
// Base64URL encode (without padding)
|
||||
computedChallenge := base64.RawURLEncoding.EncodeToString(hash[:])
|
||||
if computedChallenge != codeChallenge {
|
||||
return errors.New("code_verifier does not match code_challenge")
|
||||
}
|
||||
case "plain":
|
||||
// Direct comparison
|
||||
if codeVerifier != codeChallenge {
|
||||
return errors.New("code_verifier does not match code_challenge")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported code_challenge_method: %s", codeChallengeMethod)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (oidc *OIDCService) GenerateAccessToken(userContext *config.UserContext, clientID string, scopes []string) (string, error) {
|
||||
@@ -556,4 +601,3 @@ func getStringClaim(claims jwt.MapClaims, key string) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user