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
This commit is contained in:
parent
6708ef4c24
commit
0879c88399
34 changed files with 800 additions and 85 deletions
|
@ -14,6 +14,7 @@ import (
|
|||
"golang.org/x/oauth2"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/oidc/v3/pkg/crypto"
|
||||
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
|
@ -37,6 +38,10 @@ func Discover(ctx context.Context, issuer string, httpClient *http.Client, wellK
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if logger, ok := logging.FromContext(ctx); ok {
|
||||
logger.Debug("discover", "config", discoveryConfig)
|
||||
}
|
||||
|
||||
if discoveryConfig.Issuer != issuer {
|
||||
return nil, oidc.ErrIssuerInvalid
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
"github.com/jeremija/gosubmit"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/exp/slog"
|
||||
|
||||
"github.com/zitadel/oidc/v3/example/server/exampleop"
|
||||
"github.com/zitadel/oidc/v3/example/server/storage"
|
||||
|
@ -29,6 +30,13 @@ import (
|
|||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
var Logger = slog.New(
|
||||
slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
||||
AddSource: true,
|
||||
Level: slog.LevelDebug,
|
||||
}),
|
||||
)
|
||||
|
||||
var CTX context.Context
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
@ -49,7 +57,7 @@ func TestRelyingPartySession(t *testing.T) {
|
|||
opServer := httptest.NewServer(&dh)
|
||||
defer opServer.Close()
|
||||
t.Logf("auth server at %s", opServer.URL)
|
||||
dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage)
|
||||
dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger)
|
||||
|
||||
seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
|
||||
clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25)
|
||||
|
@ -100,7 +108,7 @@ func TestResourceServerTokenExchange(t *testing.T) {
|
|||
opServer := httptest.NewServer(&dh)
|
||||
defer opServer.Close()
|
||||
t.Logf("auth server at %s", opServer.URL)
|
||||
dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage)
|
||||
dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger)
|
||||
|
||||
seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
|
||||
clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25)
|
||||
|
|
|
@ -33,6 +33,7 @@ func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc.
|
|||
// in RFC 8628, section 3.1 and 3.2:
|
||||
// https://www.rfc-editor.org/rfc/rfc8628#section-3.1
|
||||
func DeviceAuthorization(ctx context.Context, scopes []string, rp RelyingParty, authFn any) (*oidc.DeviceAuthorizationResponse, error) {
|
||||
ctx = logCtxWithRPData(ctx, rp, "function", "DeviceAuthorization")
|
||||
req, err := newDeviceClientCredentialsRequest(scopes, rp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -45,6 +46,7 @@ func DeviceAuthorization(ctx context.Context, scopes []string, rp RelyingParty,
|
|||
// by means of polling as defined in RFC, section 3.3 and 3.4:
|
||||
// https://www.rfc-editor.org/rfc/rfc8628#section-3.4
|
||||
func DeviceAccessToken(ctx context.Context, deviceCode string, interval time.Duration, rp RelyingParty) (resp *oidc.AccessTokenResponse, err error) {
|
||||
ctx = logCtxWithRPData(ctx, rp, "function", "DeviceAccessToken")
|
||||
req := &client.DeviceAccessTokenRequest{
|
||||
DeviceAccessTokenRequest: oidc.DeviceAccessTokenRequest{
|
||||
GrantType: oidc.GrantTypeDeviceCode,
|
||||
|
|
17
pkg/client/rp/log.go
Normal file
17
pkg/client/rp/log.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
package rp
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
func logCtxWithRPData(ctx context.Context, rp RelyingParty, attrs ...any) context.Context {
|
||||
logger, ok := rp.Logger(ctx)
|
||||
if !ok {
|
||||
return ctx
|
||||
}
|
||||
logger = logger.With(slog.Group("rp", attrs...))
|
||||
return logging.ToContext(ctx, logger)
|
||||
}
|
|
@ -10,6 +10,8 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/zitadel/logging"
|
||||
"golang.org/x/exp/slog"
|
||||
"golang.org/x/oauth2"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
|
||||
|
@ -67,6 +69,9 @@ type RelyingParty interface {
|
|||
// ErrorHandler returns the handler used for callback errors
|
||||
|
||||
ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string)
|
||||
|
||||
// Logger from the context, or a fallback if set.
|
||||
Logger(context.Context) (logger *slog.Logger, ok bool)
|
||||
}
|
||||
|
||||
type ErrorHandler func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string)
|
||||
|
@ -90,6 +95,7 @@ type relyingParty struct {
|
|||
idTokenVerifier *IDTokenVerifier
|
||||
verifierOpts []VerifierOption
|
||||
signer jose.Signer
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func (rp *relyingParty) OAuthConfig() *oauth2.Config {
|
||||
|
@ -150,6 +156,14 @@ func (rp *relyingParty) ErrorHandler() func(http.ResponseWriter, *http.Request,
|
|||
return rp.errorHandler
|
||||
}
|
||||
|
||||
func (rp *relyingParty) Logger(ctx context.Context) (logger *slog.Logger, ok bool) {
|
||||
logger, ok = logging.FromContext(ctx)
|
||||
if ok {
|
||||
return logger, ok
|
||||
}
|
||||
return rp.logger, rp.logger != nil
|
||||
}
|
||||
|
||||
// NewRelyingPartyOAuth creates an (OAuth2) RelyingParty with the given
|
||||
// OAuth2 Config and possible configOptions
|
||||
// it will use the AuthURL and TokenURL set in config
|
||||
|
@ -194,6 +208,7 @@ func NewRelyingPartyOIDC(ctx context.Context, issuer, clientID, clientSecret, re
|
|||
return nil, err
|
||||
}
|
||||
}
|
||||
ctx = logCtxWithRPData(ctx, rp, "function", "NewRelyingPartyOIDC")
|
||||
discoveryConfiguration, err := client.Discover(ctx, rp.issuer, rp.httpClient, rp.DiscoveryEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -281,6 +296,15 @@ func WithJWTProfile(signerFromKey SignerFromKey) Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithLogger sets a logger that is used
|
||||
// in case the request context does not contain a logger.
|
||||
func WithLogger(logger *slog.Logger) Option {
|
||||
return func(rp *relyingParty) error {
|
||||
rp.logger = logger
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type SignerFromKey func() (jose.Signer, error)
|
||||
|
||||
func SignerFromKeyPath(path string) SignerFromKey {
|
||||
|
@ -378,6 +402,7 @@ func verifyTokenResponse[C oidc.IDClaims](ctx context.Context, token *oauth2.Tok
|
|||
// CodeExchange handles the oauth2 code exchange, extracting and validating the id_token
|
||||
// returning it parsed together with the oauth2 tokens (access, refresh)
|
||||
func CodeExchange[C oidc.IDClaims](ctx context.Context, code string, rp RelyingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens[C], err error) {
|
||||
ctx = logCtxWithRPData(ctx, rp, "function", "CodeExchange")
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient())
|
||||
codeOpts := make([]oauth2.AuthCodeOption, 0)
|
||||
for _, opt := range opts {
|
||||
|
@ -467,6 +492,7 @@ func UserinfoCallback[C oidc.IDClaims, U SubjectGetter](f CodeExchangeUserinfoCa
|
|||
// [UserInfo]: https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
|
||||
func Userinfo[U SubjectGetter](ctx context.Context, token, tokenType, subject string, rp RelyingParty) (userinfo U, err error) {
|
||||
var nilU U
|
||||
ctx = logCtxWithRPData(ctx, rp, "function", "Userinfo")
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rp.UserinfoEndpoint(), nil)
|
||||
if err != nil {
|
||||
|
@ -546,7 +572,7 @@ func withURLParam(key, value string) func() []oauth2.AuthCodeOption {
|
|||
// This is the generalized, unexported, function used by both
|
||||
// URLParamOpt and AuthURLOpt.
|
||||
func withPrompt(prompt ...string) func() []oauth2.AuthCodeOption {
|
||||
return withURLParam("prompt", oidc.SpaceDelimitedArray(prompt).Encode())
|
||||
return withURLParam("prompt", oidc.SpaceDelimitedArray(prompt).String())
|
||||
}
|
||||
|
||||
type URLParamOpt func() []oauth2.AuthCodeOption
|
||||
|
@ -621,6 +647,7 @@ type RefreshTokenRequest struct {
|
|||
// the IDToken and AccessToken will be verfied
|
||||
// and the IDToken and IDTokenClaims fields will be populated in the returned object.
|
||||
func RefreshTokens[C oidc.IDClaims](ctx context.Context, rp RelyingParty, refreshToken, clientAssertion, clientAssertionType string) (*oidc.Tokens[C], error) {
|
||||
ctx = logCtxWithRPData(ctx, rp, "function", "RefreshTokens")
|
||||
request := RefreshTokenRequest{
|
||||
RefreshToken: refreshToken,
|
||||
Scopes: rp.OAuthConfig().Scopes,
|
||||
|
@ -644,6 +671,7 @@ func RefreshTokens[C oidc.IDClaims](ctx context.Context, rp RelyingParty, refres
|
|||
}
|
||||
|
||||
func EndSession(ctx context.Context, rp RelyingParty, idToken, optionalRedirectURI, optionalState string) (*url.URL, error) {
|
||||
ctx = logCtxWithRPData(ctx, rp, "function", "EndSession")
|
||||
request := oidc.EndSessionRequest{
|
||||
IdTokenHint: idToken,
|
||||
ClientID: rp.OAuthConfig().ClientID,
|
||||
|
@ -659,6 +687,7 @@ func EndSession(ctx context.Context, rp RelyingParty, idToken, optionalRedirectU
|
|||
//
|
||||
// tokenTypeHint should be either "id_token" or "refresh_token".
|
||||
func RevokeToken(ctx context.Context, rp RelyingParty, token string, tokenTypeHint string) error {
|
||||
ctx = logCtxWithRPData(ctx, rp, "function", "RevokeToken")
|
||||
request := client.RevokeRequest{
|
||||
Token: token,
|
||||
TokenTypeHint: tokenTypeHint,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue