zitadel-oidc/example/server/storage/oidc.go
Tim Möhlmann 0879c88399
feat: add slog logging (#432)
* feat(op): user slog for logging

integrate with golang.org/x/exp/slog for logging.
provide a middleware for request scoped logging.

BREAKING CHANGES:

1. OpenIDProvider and sub-interfaces get a Logger()
method to return the configured logger;
2. AuthRequestError now takes the complete Authorizer,
instead of only the encoder. So that it may use its Logger() method.
3. RequestError now takes a Logger as argument.

* use zitadel/logging

* finish op and testing
without middleware for now

* minimum go version 1.19

* update go mod

* log value testing only on go 1.20 or later

* finish the RP and example

* ping logging release
2023-08-29 14:07:45 +02:00

219 lines
5.1 KiB
Go

package storage
import (
"time"
"golang.org/x/exp/slog"
"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 exchage
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
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 "" // we won't handle response mode in this example
}
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, 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 {
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,
Nonce: authReq.Nonce,
CodeChallenge: &OIDCCodeChallenge{
Challenge: authReq.CodeChallenge,
Method: string(authReq.CodeChallengeMethod),
},
}
}
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
}