Finally the RFC Best Current Practice for OAuth 2.0 Security has been approved. According to the RFC: > Authorization servers MUST support PKCE [RFC7636]. > > If a client sends a valid PKCE code_challenge parameter in the authorization request, the authorization server MUST enforce the correct usage of code_verifier at the token endpoint. Isn’t it time we strengthen PKCE support a bit more? This PR updates the logic so that PKCE is always verified, even when the Auth Method is not "none".
235 lines
5.4 KiB
Go
235 lines
5.4 KiB
Go
package storage
|
|
|
|
import (
|
|
"log/slog"
|
|
"time"
|
|
|
|
"golang.org/x/text/language"
|
|
|
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
|
"github.com/zitadel/oidc/v3/pkg/op"
|
|
)
|
|
|
|
const (
|
|
// CustomScope is an example for how to use custom scopes in this library
|
|
//(in this scenario, when requested, it will return a custom claim)
|
|
CustomScope = "custom_scope"
|
|
|
|
// CustomClaim is an example for how to return custom claims with this library
|
|
CustomClaim = "custom_claim"
|
|
|
|
// CustomScopeImpersonatePrefix is an example scope prefix for passing user id to impersonate using token exchange
|
|
CustomScopeImpersonatePrefix = "custom_scope:impersonate:"
|
|
)
|
|
|
|
type AuthRequest struct {
|
|
ID string
|
|
CreationDate time.Time
|
|
ApplicationID string
|
|
CallbackURI string
|
|
TransferState string
|
|
Prompt []string
|
|
UiLocales []language.Tag
|
|
LoginHint string
|
|
MaxAuthAge *time.Duration
|
|
UserID string
|
|
Scopes []string
|
|
ResponseType oidc.ResponseType
|
|
ResponseMode oidc.ResponseMode
|
|
Nonce string
|
|
CodeChallenge *OIDCCodeChallenge
|
|
|
|
done bool
|
|
authTime time.Time
|
|
}
|
|
|
|
// LogValue allows you to define which fields will be logged.
|
|
// Implements the [slog.LogValuer]
|
|
func (a *AuthRequest) LogValue() slog.Value {
|
|
return slog.GroupValue(
|
|
slog.String("id", a.ID),
|
|
slog.Time("creation_date", a.CreationDate),
|
|
slog.Any("scopes", a.Scopes),
|
|
slog.String("response_type", string(a.ResponseType)),
|
|
slog.String("app_id", a.ApplicationID),
|
|
slog.String("callback_uri", a.CallbackURI),
|
|
)
|
|
}
|
|
|
|
func (a *AuthRequest) GetID() string {
|
|
return a.ID
|
|
}
|
|
|
|
func (a *AuthRequest) GetACR() string {
|
|
return "" // we won't handle acr in this example
|
|
}
|
|
|
|
func (a *AuthRequest) GetAMR() []string {
|
|
// this example only uses password for authentication
|
|
if a.done {
|
|
return []string{"pwd"}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *AuthRequest) GetAudience() []string {
|
|
return []string{a.ApplicationID} // this example will always just use the client_id as audience
|
|
}
|
|
|
|
func (a *AuthRequest) GetAuthTime() time.Time {
|
|
return a.authTime
|
|
}
|
|
|
|
func (a *AuthRequest) GetClientID() string {
|
|
return a.ApplicationID
|
|
}
|
|
|
|
func (a *AuthRequest) GetCodeChallenge() *oidc.CodeChallenge {
|
|
return CodeChallengeToOIDC(a.CodeChallenge)
|
|
}
|
|
|
|
func (a *AuthRequest) GetNonce() string {
|
|
return a.Nonce
|
|
}
|
|
|
|
func (a *AuthRequest) GetRedirectURI() string {
|
|
return a.CallbackURI
|
|
}
|
|
|
|
func (a *AuthRequest) GetResponseType() oidc.ResponseType {
|
|
return a.ResponseType
|
|
}
|
|
|
|
func (a *AuthRequest) GetResponseMode() oidc.ResponseMode {
|
|
return a.ResponseMode
|
|
}
|
|
|
|
func (a *AuthRequest) GetScopes() []string {
|
|
return a.Scopes
|
|
}
|
|
|
|
func (a *AuthRequest) GetState() string {
|
|
return a.TransferState
|
|
}
|
|
|
|
func (a *AuthRequest) GetSubject() string {
|
|
return a.UserID
|
|
}
|
|
|
|
func (a *AuthRequest) Done() bool {
|
|
return a.done
|
|
}
|
|
|
|
func PromptToInternal(oidcPrompt oidc.SpaceDelimitedArray) []string {
|
|
prompts := make([]string, 0, len(oidcPrompt))
|
|
for _, oidcPrompt := range oidcPrompt {
|
|
switch oidcPrompt {
|
|
case oidc.PromptNone,
|
|
oidc.PromptLogin,
|
|
oidc.PromptConsent,
|
|
oidc.PromptSelectAccount:
|
|
prompts = append(prompts, oidcPrompt)
|
|
}
|
|
}
|
|
return prompts
|
|
}
|
|
|
|
func MaxAgeToInternal(maxAge *uint) *time.Duration {
|
|
if maxAge == nil {
|
|
return nil
|
|
}
|
|
dur := time.Duration(*maxAge) * time.Second
|
|
return &dur
|
|
}
|
|
|
|
func authRequestToInternal(authReq *oidc.AuthRequest, userID string) *AuthRequest {
|
|
var codeChallenge *OIDCCodeChallenge
|
|
if authReq.CodeChallenge != "" {
|
|
codeChallenge = &OIDCCodeChallenge{
|
|
Challenge: authReq.CodeChallenge,
|
|
Method: string(authReq.CodeChallengeMethod),
|
|
}
|
|
}
|
|
|
|
return &AuthRequest{
|
|
CreationDate: time.Now(),
|
|
ApplicationID: authReq.ClientID,
|
|
CallbackURI: authReq.RedirectURI,
|
|
TransferState: authReq.State,
|
|
Prompt: PromptToInternal(authReq.Prompt),
|
|
UiLocales: authReq.UILocales,
|
|
LoginHint: authReq.LoginHint,
|
|
MaxAuthAge: MaxAgeToInternal(authReq.MaxAge),
|
|
UserID: userID,
|
|
Scopes: authReq.Scopes,
|
|
ResponseType: authReq.ResponseType,
|
|
ResponseMode: authReq.ResponseMode,
|
|
Nonce: authReq.Nonce,
|
|
CodeChallenge: codeChallenge,
|
|
}
|
|
}
|
|
|
|
type AuthRequestWithSessionState struct {
|
|
*AuthRequest
|
|
SessionState string
|
|
}
|
|
|
|
func (a *AuthRequestWithSessionState) GetSessionState() string {
|
|
return a.SessionState
|
|
}
|
|
|
|
type OIDCCodeChallenge struct {
|
|
Challenge string
|
|
Method string
|
|
}
|
|
|
|
func CodeChallengeToOIDC(challenge *OIDCCodeChallenge) *oidc.CodeChallenge {
|
|
if challenge == nil {
|
|
return nil
|
|
}
|
|
challengeMethod := oidc.CodeChallengeMethodPlain
|
|
if challenge.Method == "S256" {
|
|
challengeMethod = oidc.CodeChallengeMethodS256
|
|
}
|
|
return &oidc.CodeChallenge{
|
|
Challenge: challenge.Challenge,
|
|
Method: challengeMethod,
|
|
}
|
|
}
|
|
|
|
// RefreshTokenRequestFromBusiness will simply wrap the storage RefreshToken to implement the op.RefreshTokenRequest interface
|
|
func RefreshTokenRequestFromBusiness(token *RefreshToken) op.RefreshTokenRequest {
|
|
return &RefreshTokenRequest{token}
|
|
}
|
|
|
|
type RefreshTokenRequest struct {
|
|
*RefreshToken
|
|
}
|
|
|
|
func (r *RefreshTokenRequest) GetAMR() []string {
|
|
return r.AMR
|
|
}
|
|
|
|
func (r *RefreshTokenRequest) GetAudience() []string {
|
|
return r.Audience
|
|
}
|
|
|
|
func (r *RefreshTokenRequest) GetAuthTime() time.Time {
|
|
return r.AuthTime
|
|
}
|
|
|
|
func (r *RefreshTokenRequest) GetClientID() string {
|
|
return r.ApplicationID
|
|
}
|
|
|
|
func (r *RefreshTokenRequest) GetScopes() []string {
|
|
return r.Scopes
|
|
}
|
|
|
|
func (r *RefreshTokenRequest) GetSubject() string {
|
|
return r.UserID
|
|
}
|
|
|
|
func (r *RefreshTokenRequest) SetCurrentScopes(scopes []string) {
|
|
r.Scopes = scopes
|
|
}
|