mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2025-12-31 04:22:28 +00:00
CRITICAL: Add replay protection for authorization codes
Authorization codes were implemented as stateless JWTs with no tracking, allowing the same code to be exchanged for tokens multiple times. This violates OAuth 2.0 RFC 6749 Section 4.1.2 which mandates that authorization codes MUST be single-use. This change: - Adds oidc_authorization_codes table to track code usage - Stores authorization codes in database when generated - Validates code exists and hasn't been used before exchange - Marks code as used immediately after validation - Prevents replay attacks where intercepted codes could be reused Security impact: - Prevents attackers from reusing intercepted authorization codes - Ensures compliance with OAuth 2.0 security requirements - Adds database-backed single-use enforcement
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
DROP INDEX IF EXISTS "idx_oidc_auth_codes_expires_at";
|
||||
DROP TABLE IF EXISTS "oidc_authorization_codes";
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE IF NOT EXISTS "oidc_authorization_codes" (
|
||||
"code" TEXT NOT NULL PRIMARY KEY,
|
||||
"client_id" TEXT NOT NULL,
|
||||
"redirect_uri" TEXT NOT NULL,
|
||||
"used" BOOLEAN NOT NULL DEFAULT 0,
|
||||
"expires_at" INTEGER NOT NULL,
|
||||
"created_at" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "idx_oidc_auth_codes_expires_at" ON "oidc_authorization_codes"("expires_at");
|
||||
|
||||
15
internal/model/oidc_authorization_code_model.go
Normal file
15
internal/model/oidc_authorization_code_model.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package model
|
||||
|
||||
type OIDCAuthorizationCode struct {
|
||||
Code string `gorm:"column:code;primaryKey"`
|
||||
ClientID string `gorm:"column:client_id;not null"`
|
||||
RedirectURI string `gorm:"column:redirect_uri;not null"`
|
||||
Used bool `gorm:"column:used;default:false"`
|
||||
ExpiresAt int64 `gorm:"column:expires_at;not null"`
|
||||
CreatedAt int64 `gorm:"column:created_at;not null"`
|
||||
}
|
||||
|
||||
func (OIDCAuthorizationCode) TableName() string {
|
||||
return "oidc_authorization_codes"
|
||||
}
|
||||
|
||||
@@ -216,20 +216,24 @@ func (oidc *OIDCService) ValidateScope(client *model.OIDCClient, requestedScopes
|
||||
|
||||
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()
|
||||
now := time.Now()
|
||||
expiresAt := now.Add(10 * time.Minute).Unix()
|
||||
|
||||
// Store authorization code in a temporary structure
|
||||
// In a production system, you'd want to store this in a database with expiry
|
||||
authCode := map[string]interface{}{
|
||||
"code": code,
|
||||
"userContext": userContext,
|
||||
"clientID": clientID,
|
||||
"redirectURI": redirectURI,
|
||||
"scopes": scopes,
|
||||
"nonce": nonce,
|
||||
"expiresAt": time.Now().Add(10 * time.Minute).Unix(),
|
||||
// Store authorization code in database for replay protection
|
||||
authCodeRecord := model.OIDCAuthorizationCode{
|
||||
Code: code,
|
||||
ClientID: clientID,
|
||||
RedirectURI: redirectURI,
|
||||
Used: false,
|
||||
ExpiresAt: expiresAt,
|
||||
CreatedAt: now.Unix(),
|
||||
}
|
||||
|
||||
// For now, we'll encode it as a JWT for stateless operation
|
||||
if err := oidc.config.Database.Create(&authCodeRecord).Error; err != nil {
|
||||
return "", fmt.Errorf("failed to store authorization code: %w", err)
|
||||
}
|
||||
|
||||
// Encode as JWT for stateless operation (but code is tracked in DB)
|
||||
claims := jwt.MapClaims{
|
||||
"code": code,
|
||||
"username": userContext.Username,
|
||||
@@ -239,8 +243,8 @@ func (oidc *OIDCService) GenerateAuthorizationCode(userContext *config.UserConte
|
||||
"client_id": clientID,
|
||||
"redirect_uri": redirectURI,
|
||||
"scopes": scopes,
|
||||
"exp": time.Now().Add(10 * time.Minute).Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
"exp": expiresAt,
|
||||
"iat": now.Unix(),
|
||||
}
|
||||
|
||||
if nonce != "" {
|
||||
@@ -261,10 +265,11 @@ func (oidc *OIDCService) GenerateAuthorizationCode(userContext *config.UserConte
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
codeToken, err := token.SignedString(oidc.privateKey)
|
||||
if err != nil {
|
||||
// Clean up the database record if JWT signing fails
|
||||
oidc.config.Database.Delete(&authCodeRecord)
|
||||
return "", fmt.Errorf("failed to sign authorization code: %w", err)
|
||||
}
|
||||
|
||||
_ = authCode // Suppress unused variable warning
|
||||
return codeToken, nil
|
||||
}
|
||||
|
||||
@@ -289,6 +294,32 @@ func (oidc *OIDCService) ValidateAuthorizationCode(codeToken string, clientID st
|
||||
return nil, nil, "", "", "", errors.New("invalid token claims")
|
||||
}
|
||||
|
||||
// Extract code from JWT for database lookup
|
||||
code, ok := claims["code"].(string)
|
||||
if !ok || code == "" {
|
||||
return nil, nil, "", "", "", errors.New("missing code in authorization code token")
|
||||
}
|
||||
|
||||
// Check database for replay protection - verify code exists and hasn't been used
|
||||
var authCodeRecord model.OIDCAuthorizationCode
|
||||
err = oidc.config.Database.Where("code = ?", code).First(&authCodeRecord).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil, "", "", "", errors.New("authorization code not found")
|
||||
}
|
||||
return nil, nil, "", "", "", fmt.Errorf("failed to query authorization code: %w", err)
|
||||
}
|
||||
|
||||
// Check if code has already been used (replay attack protection)
|
||||
if authCodeRecord.Used {
|
||||
return nil, nil, "", "", "", errors.New("authorization code has already been used")
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
if time.Now().Unix() > authCodeRecord.ExpiresAt {
|
||||
return nil, nil, "", "", "", errors.New("authorization code expired")
|
||||
}
|
||||
|
||||
// Verify client_id and redirect_uri match
|
||||
if claims["client_id"] != clientID {
|
||||
return nil, nil, "", "", "", errors.New("client_id mismatch")
|
||||
@@ -298,10 +329,19 @@ func (oidc *OIDCService) ValidateAuthorizationCode(codeToken string, clientID st
|
||||
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")
|
||||
// Verify database record matches request parameters
|
||||
if authCodeRecord.ClientID != clientID {
|
||||
return nil, nil, "", "", "", errors.New("client_id mismatch")
|
||||
}
|
||||
|
||||
if authCodeRecord.RedirectURI != redirectURI {
|
||||
return nil, nil, "", "", "", errors.New("redirect_uri mismatch")
|
||||
}
|
||||
|
||||
// Mark code as used to prevent replay attacks
|
||||
authCodeRecord.Used = true
|
||||
if err := oidc.config.Database.Save(&authCodeRecord).Error; err != nil {
|
||||
return nil, nil, "", "", "", fmt.Errorf("failed to mark authorization code as used: %w", err)
|
||||
}
|
||||
|
||||
userContext := &config.UserContext{
|
||||
|
||||
Reference in New Issue
Block a user