feat: 实现 OIDC 服务端会话与请求刷新
使用 AES-256-GCM 保存认证中心令牌,并以随机 Cookie 哈希关联 PostgreSQL 会话。加入闲置与绝对期限、Refresh Token 轮换以及多实例刷新锁。
This commit is contained in:
@@ -21,6 +21,7 @@ import (
|
||||
)
|
||||
|
||||
const maxOIDCResponseBytes = 1 << 20
|
||||
const defaultOIDCHTTPTimeout = 10 * time.Second
|
||||
|
||||
type OIDCConfig struct {
|
||||
Issuer string
|
||||
@@ -82,7 +83,7 @@ func NewOIDCVerifier(config OIDCConfig) (*OIDCVerifier, error) {
|
||||
}
|
||||
client := config.HTTPClient
|
||||
if client == nil {
|
||||
client = &http.Client{Timeout: 10 * time.Second, CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
|
||||
client = &http.Client{Timeout: defaultOIDCHTTPTimeout, CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}}
|
||||
}
|
||||
@@ -151,6 +152,45 @@ func (v *OIDCVerifier) Verify(ctx context.Context, raw string) (*User, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (v *OIDCVerifier) VerifyIDToken(ctx context.Context, raw, clientID, expectedNonce string) (string, error) {
|
||||
clientID = strings.TrimSpace(clientID)
|
||||
expectedNonce = strings.TrimSpace(expectedNonce)
|
||||
if clientID == "" || expectedNonce == "" {
|
||||
return "", oidcUnauthorized("ID token validation context is invalid", nil)
|
||||
}
|
||||
parser := jwt.NewParser(jwt.WithValidMethods([]string{"RS256", "ES256"}))
|
||||
unverified, _, err := parser.ParseUnverified(raw, jwt.MapClaims{})
|
||||
if err != nil || unverified == nil {
|
||||
return "", oidcUnauthorized("ID token envelope is invalid", err)
|
||||
}
|
||||
kid, _ := unverified.Header["kid"].(string)
|
||||
if kid == "" {
|
||||
return "", oidcUnauthorized("ID token kid is missing", nil)
|
||||
}
|
||||
key, err := v.key(ctx, kid)
|
||||
if err != nil {
|
||||
return "", oidcUnauthorized("ID token signing key lookup failed", err)
|
||||
}
|
||||
token, err := jwt.Parse(raw, func(token *jwt.Token) (any, error) {
|
||||
if token.Header["kid"] != kid {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
return key, nil
|
||||
}, jwt.WithValidMethods([]string{"RS256", "ES256"}), jwt.WithIssuer(v.config.Issuer),
|
||||
jwt.WithAudience(clientID), jwt.WithExpirationRequired(), jwt.WithLeeway(30*time.Second))
|
||||
if err != nil || !token.Valid {
|
||||
return "", oidcUnauthorized("ID token signature or registered claims are invalid", err)
|
||||
}
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok || stringClaim(claims, "sub") == "" || stringClaim(claims, "nonce") != expectedNonce {
|
||||
return "", oidcUnauthorized("ID token subject or nonce is invalid", nil)
|
||||
}
|
||||
if _, ok := numericDateClaim(claims["nbf"]); !ok {
|
||||
return "", oidcUnauthorized("ID token nbf is missing", nil)
|
||||
}
|
||||
return stringClaim(claims, "sub"), nil
|
||||
}
|
||||
|
||||
func oidcUnauthorized(reason string, cause error) error {
|
||||
if cause != nil {
|
||||
return fmt.Errorf("%w: %s: %v", ErrUnauthorized, reason, cause)
|
||||
|
||||
Reference in New Issue
Block a user