Merge branch 'next' into next-main
This commit is contained in:
commit
d9487ef77d
118 changed files with 6091 additions and 981 deletions
|
@ -4,9 +4,9 @@ import (
|
|||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package rp
|
||||
|
||||
import (
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc/grants/tokenexchange"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc/grants/tokenexchange"
|
||||
)
|
||||
|
||||
// DelegationTokenRequest is an implementation of TokenExchangeRequest
|
||||
|
|
|
@ -5,8 +5,8 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/oidc/v2/pkg/client"
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/client"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc.ClientCredentialsRequest, error) {
|
||||
|
@ -32,19 +32,21 @@ func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc.
|
|||
// DeviceAuthorization starts a new Device Authorization flow as defined
|
||||
// in RFC 8628, section 3.1 and 3.2:
|
||||
// https://www.rfc-editor.org/rfc/rfc8628#section-3.1
|
||||
func DeviceAuthorization(scopes []string, rp RelyingParty) (*oidc.DeviceAuthorizationResponse, error) {
|
||||
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
|
||||
}
|
||||
|
||||
return client.CallDeviceAuthorizationEndpoint(req, rp)
|
||||
return client.CallDeviceAuthorizationEndpoint(ctx, req, rp, authFn)
|
||||
}
|
||||
|
||||
// DeviceAccessToken attempts to obtain tokens from a Device Authorization,
|
||||
// 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,
|
||||
|
|
|
@ -7,10 +7,10 @@ import (
|
|||
"net/http"
|
||||
"sync"
|
||||
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
jose "github.com/go-jose/go-jose/v3"
|
||||
|
||||
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
func NewRemoteKeySet(client *http.Client, jwksURL string, opts ...func(*remoteKeySet)) oidc.KeySet {
|
||||
|
|
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)
|
||||
}
|
|
@ -7,16 +7,17 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v3"
|
||||
"github.com/google/uuid"
|
||||
"github.com/zitadel/logging"
|
||||
"golang.org/x/exp/slog"
|
||||
"golang.org/x/oauth2"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
|
||||
"github.com/zitadel/oidc/v2/pkg/client"
|
||||
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/client"
|
||||
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -63,11 +64,14 @@ type RelyingParty interface {
|
|||
// be used to start a DeviceAuthorization flow.
|
||||
GetDeviceAuthorizationEndpoint() string
|
||||
|
||||
// IDTokenVerifier returns the verifier interface used for oidc id_token verification
|
||||
IDTokenVerifier() IDTokenVerifier
|
||||
// IDTokenVerifier returns the verifier used for oidc id_token verification
|
||||
IDTokenVerifier() *IDTokenVerifier
|
||||
// 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)
|
||||
|
@ -88,9 +92,10 @@ type relyingParty struct {
|
|||
cookieHandler *httphelper.CookieHandler
|
||||
|
||||
errorHandler func(http.ResponseWriter, *http.Request, string, string, string)
|
||||
idTokenVerifier IDTokenVerifier
|
||||
idTokenVerifier *IDTokenVerifier
|
||||
verifierOpts []VerifierOption
|
||||
signer jose.Signer
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func (rp *relyingParty) OAuthConfig() *oauth2.Config {
|
||||
|
@ -137,7 +142,7 @@ func (rp *relyingParty) GetRevokeEndpoint() string {
|
|||
return rp.endpoints.RevokeURL
|
||||
}
|
||||
|
||||
func (rp *relyingParty) IDTokenVerifier() IDTokenVerifier {
|
||||
func (rp *relyingParty) IDTokenVerifier() *IDTokenVerifier {
|
||||
if rp.idTokenVerifier == nil {
|
||||
rp.idTokenVerifier = NewIDTokenVerifier(rp.issuer, rp.oauthConfig.ClientID, NewRemoteKeySet(rp.httpClient, rp.endpoints.JKWsURL), rp.verifierOpts...)
|
||||
}
|
||||
|
@ -151,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
|
||||
|
@ -177,7 +190,7 @@ func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingPart
|
|||
// NewRelyingPartyOIDC creates an (OIDC) RelyingParty with the given
|
||||
// issuer, clientID, clientSecret, redirectURI, scopes and possible configOptions
|
||||
// it will run discovery on the provided issuer and use the found endpoints
|
||||
func NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...Option) (RelyingParty, error) {
|
||||
func NewRelyingPartyOIDC(ctx context.Context, issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...Option) (RelyingParty, error) {
|
||||
rp := &relyingParty{
|
||||
issuer: issuer,
|
||||
oauthConfig: &oauth2.Config{
|
||||
|
@ -195,7 +208,8 @@ func NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, sco
|
|||
return nil, err
|
||||
}
|
||||
}
|
||||
discoveryConfiguration, err := client.Discover(rp.issuer, rp.httpClient, rp.DiscoveryEndpoint)
|
||||
ctx = logCtxWithRPData(ctx, rp, "function", "NewRelyingPartyOIDC")
|
||||
discoveryConfiguration, err := client.Discover(ctx, rp.issuer, rp.httpClient, rp.DiscoveryEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -282,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 {
|
||||
|
@ -310,26 +333,6 @@ func SignerFromKeyAndKeyID(key []byte, keyID string) SignerFromKey {
|
|||
}
|
||||
}
|
||||
|
||||
// Discover calls the discovery endpoint of the provided issuer and returns the found endpoints
|
||||
//
|
||||
// deprecated: use client.Discover
|
||||
func Discover(issuer string, httpClient *http.Client) (Endpoints, error) {
|
||||
wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint
|
||||
req, err := http.NewRequest("GET", wellKnown, nil)
|
||||
if err != nil {
|
||||
return Endpoints{}, err
|
||||
}
|
||||
discoveryConfig := new(oidc.DiscoveryConfiguration)
|
||||
err = httphelper.HttpRequest(httpClient, req, &discoveryConfig)
|
||||
if err != nil {
|
||||
return Endpoints{}, err
|
||||
}
|
||||
if discoveryConfig.Issuer != issuer {
|
||||
return Endpoints{}, fmt.Errorf("%w: Expected: %s, got: %s", oidc.ErrIssuerInvalid, discoveryConfig.Issuer, issuer)
|
||||
}
|
||||
return GetEndpoints(discoveryConfig), nil
|
||||
}
|
||||
|
||||
// AuthURL returns the auth request url
|
||||
// (wrapping the oauth2 `AuthCodeURL`)
|
||||
func AuthURL(state string, rp RelyingParty, opts ...AuthURLOpt) string {
|
||||
|
@ -377,9 +380,29 @@ func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelyingParty) (stri
|
|||
return oidc.NewSHACodeChallenge(codeVerifier), nil
|
||||
}
|
||||
|
||||
// ErrMissingIDToken is returned when an id_token was expected,
|
||||
// but not received in the token response.
|
||||
var ErrMissingIDToken = errors.New("id_token missing")
|
||||
|
||||
func verifyTokenResponse[C oidc.IDClaims](ctx context.Context, token *oauth2.Token, rp RelyingParty) (*oidc.Tokens[C], error) {
|
||||
if rp.IsOAuth2Only() {
|
||||
return &oidc.Tokens[C]{Token: token}, nil
|
||||
}
|
||||
idTokenString, ok := token.Extra(idTokenKey).(string)
|
||||
if !ok {
|
||||
return &oidc.Tokens[C]{Token: token}, ErrMissingIDToken
|
||||
}
|
||||
idToken, err := VerifyTokens[C](ctx, token.AccessToken, idTokenString, rp.IDTokenVerifier())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &oidc.Tokens[C]{Token: token, IDTokenClaims: idToken, IDToken: idTokenString}, nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
@ -390,22 +413,7 @@ func CodeExchange[C oidc.IDClaims](ctx context.Context, code string, rp RelyingP
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rp.IsOAuth2Only() {
|
||||
return &oidc.Tokens[C]{Token: token}, nil
|
||||
}
|
||||
|
||||
idTokenString, ok := token.Extra(idTokenKey).(string)
|
||||
if !ok {
|
||||
return nil, errors.New("id_token missing")
|
||||
}
|
||||
|
||||
idToken, err := VerifyTokens[C](ctx, token.AccessToken, idTokenString, rp.IDTokenVerifier())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &oidc.Tokens[C]{Token: token, IDTokenClaims: idToken, IDToken: idTokenString}, nil
|
||||
return verifyTokenResponse[C](ctx, token, rp)
|
||||
}
|
||||
|
||||
type CodeExchangeCallback[C oidc.IDClaims] func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty)
|
||||
|
@ -457,14 +465,18 @@ func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp R
|
|||
}
|
||||
}
|
||||
|
||||
type CodeExchangeUserinfoCallback[C oidc.IDClaims] func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, provider RelyingParty, info *oidc.UserInfo)
|
||||
type SubjectGetter interface {
|
||||
GetSubject() string
|
||||
}
|
||||
|
||||
type CodeExchangeUserinfoCallback[C oidc.IDClaims, U SubjectGetter] func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, provider RelyingParty, info U)
|
||||
|
||||
// UserinfoCallback wraps the callback function of the CodeExchangeHandler
|
||||
// and calls the userinfo endpoint with the access token
|
||||
// on success it will pass the userinfo into its callback function as well
|
||||
func UserinfoCallback[C oidc.IDClaims](f CodeExchangeUserinfoCallback[C]) CodeExchangeCallback[C] {
|
||||
func UserinfoCallback[C oidc.IDClaims, U SubjectGetter](f CodeExchangeUserinfoCallback[C, U]) CodeExchangeCallback[C] {
|
||||
return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty) {
|
||||
info, err := Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), rp)
|
||||
info, err := Userinfo[U](r.Context(), tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), rp)
|
||||
if err != nil {
|
||||
http.Error(w, "userinfo failed: "+err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
|
@ -473,19 +485,26 @@ func UserinfoCallback[C oidc.IDClaims](f CodeExchangeUserinfoCallback[C]) CodeEx
|
|||
}
|
||||
}
|
||||
|
||||
// Userinfo will call the OIDC Userinfo Endpoint with the provided token
|
||||
func Userinfo(token, tokenType, subject string, rp RelyingParty) (*oidc.UserInfo, error) {
|
||||
req, err := http.NewRequest("GET", rp.UserinfoEndpoint(), nil)
|
||||
// Userinfo will call the OIDC [UserInfo] Endpoint with the provided token and returns
|
||||
// the response in an instance of type U.
|
||||
// [*oidc.UserInfo] can be used as a good example, or use a custom type if type-safe
|
||||
// access to custom claims is needed.
|
||||
//
|
||||
// [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 {
|
||||
return nil, err
|
||||
return nilU, err
|
||||
}
|
||||
req.Header.Set("authorization", tokenType+" "+token)
|
||||
userinfo := new(oidc.UserInfo)
|
||||
if err := httphelper.HttpRequest(rp.HttpClient(), req, &userinfo); err != nil {
|
||||
return nil, err
|
||||
return nilU, err
|
||||
}
|
||||
if userinfo.Subject != subject {
|
||||
return nil, ErrUserInfoSubNotMatching
|
||||
if userinfo.GetSubject() != subject {
|
||||
return nilU, ErrUserInfoSubNotMatching
|
||||
}
|
||||
return userinfo, nil
|
||||
}
|
||||
|
@ -554,7 +573,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
|
||||
|
@ -626,11 +645,15 @@ type RefreshTokenRequest struct {
|
|||
GrantType oidc.GrantType `schema:"grant_type"`
|
||||
}
|
||||
|
||||
// RefreshAccessToken performs a token refresh. If it doesn't error, it will always
|
||||
// RefreshTokens performs a token refresh. If it doesn't error, it will always
|
||||
// provide a new AccessToken. It may provide a new RefreshToken, and if it does, then
|
||||
// the old one should be considered invalid. It may also provide a new IDToken. The
|
||||
// new IDToken can be retrieved with token.Extra("id_token").
|
||||
func RefreshAccessToken(rp RelyingParty, refreshToken, clientAssertion, clientAssertionType string) (*oauth2.Token, error) {
|
||||
// the old one should be considered invalid.
|
||||
//
|
||||
// In case the RP is not OAuth2 only and an IDToken was part of the response,
|
||||
// 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,
|
||||
|
@ -640,17 +663,28 @@ func RefreshAccessToken(rp RelyingParty, refreshToken, clientAssertion, clientAs
|
|||
ClientAssertionType: clientAssertionType,
|
||||
GrantType: oidc.GrantTypeRefreshToken,
|
||||
}
|
||||
return client.CallTokenEndpoint(request, tokenEndpointCaller{RelyingParty: rp})
|
||||
newToken, err := client.CallTokenEndpoint(ctx, request, tokenEndpointCaller{RelyingParty: rp})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tokens, err := verifyTokenResponse[C](ctx, newToken, rp)
|
||||
if err == nil || errors.Is(err, ErrMissingIDToken) {
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse
|
||||
// ...except that it might not contain an id_token.
|
||||
return tokens, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func EndSession(rp RelyingParty, idToken, optionalRedirectURI, optionalState string) (*url.URL, error) {
|
||||
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,
|
||||
PostLogoutRedirectURI: optionalRedirectURI,
|
||||
State: optionalState,
|
||||
}
|
||||
return client.CallEndSessionEndpoint(request, nil, rp)
|
||||
return client.CallEndSessionEndpoint(ctx, request, nil, rp)
|
||||
}
|
||||
|
||||
// RevokeToken requires a RelyingParty that is also a client.RevokeCaller. The RelyingParty
|
||||
|
@ -658,7 +692,8 @@ func EndSession(rp RelyingParty, idToken, optionalRedirectURI, optionalState str
|
|||
// NewRelyingPartyOAuth() does not.
|
||||
//
|
||||
// tokenTypeHint should be either "id_token" or "refresh_token".
|
||||
func RevokeToken(rp RelyingParty, token string, tokenTypeHint string) error {
|
||||
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,
|
||||
|
@ -666,7 +701,7 @@ func RevokeToken(rp RelyingParty, token string, tokenTypeHint string) error {
|
|||
ClientSecret: rp.OAuthConfig().ClientSecret,
|
||||
}
|
||||
if rc, ok := rp.(client.RevokeCaller); ok && rc.GetRevokeEndpoint() != "" {
|
||||
return client.CallRevokeEndpoint(request, nil, rc)
|
||||
return client.CallRevokeEndpoint(ctx, request, nil, rc)
|
||||
}
|
||||
return fmt.Errorf("RelyingParty does not support RevokeCaller")
|
||||
}
|
||||
|
|
107
pkg/client/rp/relying_party_test.go
Normal file
107
pkg/client/rp/relying_party_test.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
package rp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
tu "github.com/zitadel/oidc/v3/internal/testutil"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func Test_verifyTokenResponse(t *testing.T) {
|
||||
verifier := &IDTokenVerifier{
|
||||
Issuer: tu.ValidIssuer,
|
||||
MaxAgeIAT: 2 * time.Minute,
|
||||
ClientID: tu.ValidClientID,
|
||||
Offset: time.Second,
|
||||
SupportedSignAlgs: []string{string(tu.SignatureAlgorithm)},
|
||||
KeySet: tu.KeySet{},
|
||||
MaxAge: 2 * time.Minute,
|
||||
ACR: tu.ACRVerify,
|
||||
Nonce: func(context.Context) string { return tu.ValidNonce },
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
oauth2Only bool
|
||||
tokens func() (token *oauth2.Token, want *oidc.Tokens[*oidc.IDTokenClaims])
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "succes, oauth2 only",
|
||||
oauth2Only: true,
|
||||
tokens: func() (*oauth2.Token, *oidc.Tokens[*oidc.IDTokenClaims]) {
|
||||
accesToken, _ := tu.ValidAccessToken()
|
||||
token := &oauth2.Token{
|
||||
AccessToken: accesToken,
|
||||
}
|
||||
return token, &oidc.Tokens[*oidc.IDTokenClaims]{
|
||||
Token: token,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "id_token missing error",
|
||||
oauth2Only: false,
|
||||
tokens: func() (*oauth2.Token, *oidc.Tokens[*oidc.IDTokenClaims]) {
|
||||
accesToken, _ := tu.ValidAccessToken()
|
||||
token := &oauth2.Token{
|
||||
AccessToken: accesToken,
|
||||
}
|
||||
return token, &oidc.Tokens[*oidc.IDTokenClaims]{
|
||||
Token: token,
|
||||
}
|
||||
},
|
||||
wantErr: ErrMissingIDToken,
|
||||
},
|
||||
{
|
||||
name: "verify tokens error",
|
||||
oauth2Only: false,
|
||||
tokens: func() (*oauth2.Token, *oidc.Tokens[*oidc.IDTokenClaims]) {
|
||||
accesToken, _ := tu.ValidAccessToken()
|
||||
token := &oauth2.Token{
|
||||
AccessToken: accesToken,
|
||||
}
|
||||
token = token.WithExtra(map[string]any{
|
||||
"id_token": "foobar",
|
||||
})
|
||||
return token, nil
|
||||
},
|
||||
wantErr: oidc.ErrParse,
|
||||
},
|
||||
{
|
||||
name: "success, with id_token",
|
||||
oauth2Only: false,
|
||||
tokens: func() (*oauth2.Token, *oidc.Tokens[*oidc.IDTokenClaims]) {
|
||||
accesToken, _ := tu.ValidAccessToken()
|
||||
token := &oauth2.Token{
|
||||
AccessToken: accesToken,
|
||||
}
|
||||
idToken, claims := tu.ValidIDToken()
|
||||
token = token.WithExtra(map[string]any{
|
||||
"id_token": idToken,
|
||||
})
|
||||
return token, &oidc.Tokens[*oidc.IDTokenClaims]{
|
||||
Token: token,
|
||||
IDTokenClaims: claims,
|
||||
IDToken: idToken,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rp := &relyingParty{
|
||||
oauth2Only: tt.oauth2Only,
|
||||
idTokenVerifier: verifier,
|
||||
}
|
||||
token, want := tt.tokens()
|
||||
got, err := verifyTokenResponse[*oidc.IDTokenClaims](context.Background(), token, rp)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, want, got)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ import (
|
|||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc/grants/tokenexchange"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc/grants/tokenexchange"
|
||||
)
|
||||
|
||||
// TokenExchangeRP extends the `RelyingParty` interface for the *draft* oauth2 `Token Exchange`
|
||||
|
|
45
pkg/client/rp/userinfo_example_test.go
Normal file
45
pkg/client/rp/userinfo_example_test.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package rp_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
type UserInfo struct {
|
||||
Subject string `json:"sub,omitempty"`
|
||||
oidc.UserInfoProfile
|
||||
oidc.UserInfoEmail
|
||||
oidc.UserInfoPhone
|
||||
Address *oidc.UserInfoAddress `json:"address,omitempty"`
|
||||
|
||||
// Foo and Bar are custom claims
|
||||
Foo string `json:"foo,omitempty"`
|
||||
Bar struct {
|
||||
Val1 string `json:"val_1,omitempty"`
|
||||
Val2 string `json:"val_2,omitempty"`
|
||||
} `json:"bar,omitempty"`
|
||||
|
||||
// Claims are all the combined claims, including custom.
|
||||
Claims map[string]any `json:"-,omitempty"`
|
||||
}
|
||||
|
||||
func (u *UserInfo) GetSubject() string {
|
||||
return u.Subject
|
||||
}
|
||||
|
||||
func ExampleUserinfo_custom() {
|
||||
rpo, err := rp.NewRelyingPartyOIDC(context.TODO(), "http://localhost:8080", "clientid", "clientsecret", "http://example.com/redirect", []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopePhone})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
info, err := rp.Userinfo[*UserInfo](context.TODO(), "accesstokenstring", "Bearer", "userid", rpo)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(info)
|
||||
}
|
|
@ -4,24 +4,14 @@ import (
|
|||
"context"
|
||||
"time"
|
||||
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
jose "github.com/go-jose/go-jose/v3"
|
||||
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
type IDTokenVerifier interface {
|
||||
oidc.Verifier
|
||||
ClientID() string
|
||||
SupportedSignAlgs() []string
|
||||
KeySet() oidc.KeySet
|
||||
Nonce(context.Context) string
|
||||
ACR() oidc.ACRVerifier
|
||||
MaxAge() time.Duration
|
||||
}
|
||||
|
||||
// VerifyTokens implement the Token Response Validation as defined in OIDC specification
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#TokenResponseValidation
|
||||
func VerifyTokens[C oidc.IDClaims](ctx context.Context, accessToken, idToken string, v IDTokenVerifier) (claims C, err error) {
|
||||
func VerifyTokens[C oidc.IDClaims](ctx context.Context, accessToken, idToken string, v *IDTokenVerifier) (claims C, err error) {
|
||||
var nilClaims C
|
||||
|
||||
claims, err = VerifyIDToken[C](ctx, idToken, v)
|
||||
|
@ -36,7 +26,7 @@ func VerifyTokens[C oidc.IDClaims](ctx context.Context, accessToken, idToken str
|
|||
|
||||
// VerifyIDToken validates the id token according to
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
|
||||
func VerifyIDToken[C oidc.Claims](ctx context.Context, token string, v IDTokenVerifier) (claims C, err error) {
|
||||
func VerifyIDToken[C oidc.Claims](ctx context.Context, token string, v *IDTokenVerifier) (claims C, err error) {
|
||||
var nilClaims C
|
||||
|
||||
decrypted, err := oidc.DecryptToken(token)
|
||||
|
@ -52,27 +42,27 @@ func VerifyIDToken[C oidc.Claims](ctx context.Context, token string, v IDTokenVe
|
|||
return nilClaims, err
|
||||
}
|
||||
|
||||
if err = oidc.CheckIssuer(claims, v.Issuer()); err != nil {
|
||||
if err = oidc.CheckIssuer(claims, v.Issuer); err != nil {
|
||||
return nilClaims, err
|
||||
}
|
||||
|
||||
if err = oidc.CheckAudience(claims, v.ClientID()); err != nil {
|
||||
if err = oidc.CheckAudience(claims, v.ClientID); err != nil {
|
||||
return nilClaims, err
|
||||
}
|
||||
|
||||
if err = oidc.CheckAuthorizedParty(claims, v.ClientID()); err != nil {
|
||||
if err = oidc.CheckAuthorizedParty(claims, v.ClientID); err != nil {
|
||||
return nilClaims, err
|
||||
}
|
||||
|
||||
if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs(), v.KeySet()); err != nil {
|
||||
if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs, v.KeySet); err != nil {
|
||||
return nilClaims, err
|
||||
}
|
||||
|
||||
if err = oidc.CheckExpiration(claims, v.Offset()); err != nil {
|
||||
if err = oidc.CheckExpiration(claims, v.Offset); err != nil {
|
||||
return nilClaims, err
|
||||
}
|
||||
|
||||
if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT(), v.Offset()); err != nil {
|
||||
if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT, v.Offset); err != nil {
|
||||
return nilClaims, err
|
||||
}
|
||||
|
||||
|
@ -80,16 +70,18 @@ func VerifyIDToken[C oidc.Claims](ctx context.Context, token string, v IDTokenVe
|
|||
return nilClaims, err
|
||||
}
|
||||
|
||||
if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR()); err != nil {
|
||||
if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR); err != nil {
|
||||
return nilClaims, err
|
||||
}
|
||||
|
||||
if err = oidc.CheckAuthTime(claims, v.MaxAge()); err != nil {
|
||||
if err = oidc.CheckAuthTime(claims, v.MaxAge); err != nil {
|
||||
return nilClaims, err
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
type IDTokenVerifier oidc.Verifier
|
||||
|
||||
// VerifyAccessToken validates the access token according to
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowTokenValidation
|
||||
func VerifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAlgorithm) error {
|
||||
|
@ -107,15 +99,14 @@ func VerifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAl
|
|||
return nil
|
||||
}
|
||||
|
||||
// NewIDTokenVerifier returns an implementation of `IDTokenVerifier`
|
||||
// for `VerifyTokens` and `VerifyIDToken`
|
||||
func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet, options ...VerifierOption) IDTokenVerifier {
|
||||
v := &idTokenVerifier{
|
||||
issuer: issuer,
|
||||
clientID: clientID,
|
||||
keySet: keySet,
|
||||
offset: time.Second,
|
||||
nonce: func(_ context.Context) string {
|
||||
// NewIDTokenVerifier returns a oidc.Verifier suitable for ID token verification.
|
||||
func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet, options ...VerifierOption) *IDTokenVerifier {
|
||||
v := &IDTokenVerifier{
|
||||
Issuer: issuer,
|
||||
ClientID: clientID,
|
||||
KeySet: keySet,
|
||||
Offset: time.Second,
|
||||
Nonce: func(_ context.Context) string {
|
||||
return ""
|
||||
},
|
||||
}
|
||||
|
@ -128,95 +119,47 @@ func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet, options ...
|
|||
}
|
||||
|
||||
// VerifierOption is the type for providing dynamic options to the IDTokenVerifier
|
||||
type VerifierOption func(*idTokenVerifier)
|
||||
type VerifierOption func(*IDTokenVerifier)
|
||||
|
||||
// WithIssuedAtOffset mitigates the risk of iat to be in the future
|
||||
// because of clock skews with the ability to add an offset to the current time
|
||||
func WithIssuedAtOffset(offset time.Duration) func(*idTokenVerifier) {
|
||||
return func(v *idTokenVerifier) {
|
||||
v.offset = offset
|
||||
func WithIssuedAtOffset(offset time.Duration) VerifierOption {
|
||||
return func(v *IDTokenVerifier) {
|
||||
v.Offset = offset
|
||||
}
|
||||
}
|
||||
|
||||
// WithIssuedAtMaxAge provides the ability to define the maximum duration between iat and now
|
||||
func WithIssuedAtMaxAge(maxAge time.Duration) func(*idTokenVerifier) {
|
||||
return func(v *idTokenVerifier) {
|
||||
v.maxAgeIAT = maxAge
|
||||
func WithIssuedAtMaxAge(maxAge time.Duration) VerifierOption {
|
||||
return func(v *IDTokenVerifier) {
|
||||
v.MaxAgeIAT = maxAge
|
||||
}
|
||||
}
|
||||
|
||||
// WithNonce sets the function to check the nonce
|
||||
func WithNonce(nonce func(context.Context) string) VerifierOption {
|
||||
return func(v *idTokenVerifier) {
|
||||
v.nonce = nonce
|
||||
return func(v *IDTokenVerifier) {
|
||||
v.Nonce = nonce
|
||||
}
|
||||
}
|
||||
|
||||
// WithACRVerifier sets the verifier for the acr claim
|
||||
func WithACRVerifier(verifier oidc.ACRVerifier) VerifierOption {
|
||||
return func(v *idTokenVerifier) {
|
||||
v.acr = verifier
|
||||
return func(v *IDTokenVerifier) {
|
||||
v.ACR = verifier
|
||||
}
|
||||
}
|
||||
|
||||
// WithAuthTimeMaxAge provides the ability to define the maximum duration between auth_time and now
|
||||
func WithAuthTimeMaxAge(maxAge time.Duration) VerifierOption {
|
||||
return func(v *idTokenVerifier) {
|
||||
v.maxAge = maxAge
|
||||
return func(v *IDTokenVerifier) {
|
||||
v.MaxAge = maxAge
|
||||
}
|
||||
}
|
||||
|
||||
// WithSupportedSigningAlgorithms overwrites the default RS256 signing algorithm
|
||||
func WithSupportedSigningAlgorithms(algs ...string) VerifierOption {
|
||||
return func(v *idTokenVerifier) {
|
||||
v.supportedSignAlgs = algs
|
||||
return func(v *IDTokenVerifier) {
|
||||
v.SupportedSignAlgs = algs
|
||||
}
|
||||
}
|
||||
|
||||
type idTokenVerifier struct {
|
||||
issuer string
|
||||
maxAgeIAT time.Duration
|
||||
offset time.Duration
|
||||
clientID string
|
||||
supportedSignAlgs []string
|
||||
keySet oidc.KeySet
|
||||
acr oidc.ACRVerifier
|
||||
maxAge time.Duration
|
||||
nonce func(ctx context.Context) string
|
||||
}
|
||||
|
||||
func (i *idTokenVerifier) Issuer() string {
|
||||
return i.issuer
|
||||
}
|
||||
|
||||
func (i *idTokenVerifier) MaxAgeIAT() time.Duration {
|
||||
return i.maxAgeIAT
|
||||
}
|
||||
|
||||
func (i *idTokenVerifier) Offset() time.Duration {
|
||||
return i.offset
|
||||
}
|
||||
|
||||
func (i *idTokenVerifier) ClientID() string {
|
||||
return i.clientID
|
||||
}
|
||||
|
||||
func (i *idTokenVerifier) SupportedSignAlgs() []string {
|
||||
return i.supportedSignAlgs
|
||||
}
|
||||
|
||||
func (i *idTokenVerifier) KeySet() oidc.KeySet {
|
||||
return i.keySet
|
||||
}
|
||||
|
||||
func (i *idTokenVerifier) Nonce(ctx context.Context) string {
|
||||
return i.nonce(ctx)
|
||||
}
|
||||
|
||||
func (i *idTokenVerifier) ACR() oidc.ACRVerifier {
|
||||
return i.acr
|
||||
}
|
||||
|
||||
func (i *idTokenVerifier) MaxAge() time.Duration {
|
||||
return i.maxAge
|
||||
}
|
||||
|
|
|
@ -5,24 +5,24 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
tu "github.com/zitadel/oidc/v2/internal/testutil"
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
tu "github.com/zitadel/oidc/v3/internal/testutil"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
func TestVerifyTokens(t *testing.T) {
|
||||
verifier := &idTokenVerifier{
|
||||
issuer: tu.ValidIssuer,
|
||||
maxAgeIAT: 2 * time.Minute,
|
||||
offset: time.Second,
|
||||
supportedSignAlgs: []string{string(tu.SignatureAlgorithm)},
|
||||
keySet: tu.KeySet{},
|
||||
maxAge: 2 * time.Minute,
|
||||
acr: tu.ACRVerify,
|
||||
nonce: func(context.Context) string { return tu.ValidNonce },
|
||||
clientID: tu.ValidClientID,
|
||||
verifier := &IDTokenVerifier{
|
||||
Issuer: tu.ValidIssuer,
|
||||
MaxAgeIAT: 2 * time.Minute,
|
||||
Offset: time.Second,
|
||||
SupportedSignAlgs: []string{string(tu.SignatureAlgorithm)},
|
||||
KeySet: tu.KeySet{},
|
||||
MaxAge: 2 * time.Minute,
|
||||
ACR: tu.ACRVerify,
|
||||
Nonce: func(context.Context) string { return tu.ValidNonce },
|
||||
ClientID: tu.ValidClientID,
|
||||
}
|
||||
accessToken, _ := tu.ValidAccessToken()
|
||||
atHash, err := oidc.ClaimHash(accessToken, tu.SignatureAlgorithm)
|
||||
|
@ -91,15 +91,15 @@ func TestVerifyTokens(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestVerifyIDToken(t *testing.T) {
|
||||
verifier := &idTokenVerifier{
|
||||
issuer: tu.ValidIssuer,
|
||||
maxAgeIAT: 2 * time.Minute,
|
||||
offset: time.Second,
|
||||
supportedSignAlgs: []string{string(tu.SignatureAlgorithm)},
|
||||
keySet: tu.KeySet{},
|
||||
maxAge: 2 * time.Minute,
|
||||
acr: tu.ACRVerify,
|
||||
nonce: func(context.Context) string { return tu.ValidNonce },
|
||||
verifier := &IDTokenVerifier{
|
||||
Issuer: tu.ValidIssuer,
|
||||
MaxAgeIAT: 2 * time.Minute,
|
||||
Offset: time.Second,
|
||||
SupportedSignAlgs: []string{string(tu.SignatureAlgorithm)},
|
||||
KeySet: tu.KeySet{},
|
||||
MaxAge: 2 * time.Minute,
|
||||
ACR: tu.ACRVerify,
|
||||
Nonce: func(context.Context) string { return tu.ValidNonce },
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
|
@ -231,7 +231,7 @@ func TestVerifyIDToken(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
token, want := tt.tokenClaims()
|
||||
verifier.clientID = tt.clientID
|
||||
verifier.ClientID = tt.clientID
|
||||
got, err := VerifyIDToken[*oidc.IDTokenClaims](context.Background(), token, verifier)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
|
@ -312,7 +312,7 @@ func TestNewIDTokenVerifier(t *testing.T) {
|
|||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want IDTokenVerifier
|
||||
want *IDTokenVerifier
|
||||
}{
|
||||
{
|
||||
name: "nil nonce", // otherwise assert.Equal will fail on the function
|
||||
|
@ -329,16 +329,16 @@ func TestNewIDTokenVerifier(t *testing.T) {
|
|||
WithSupportedSigningAlgorithms("ABC", "DEF"),
|
||||
},
|
||||
},
|
||||
want: &idTokenVerifier{
|
||||
issuer: tu.ValidIssuer,
|
||||
offset: time.Minute,
|
||||
maxAgeIAT: time.Hour,
|
||||
clientID: tu.ValidClientID,
|
||||
keySet: tu.KeySet{},
|
||||
nonce: nil,
|
||||
acr: nil,
|
||||
maxAge: 2 * time.Hour,
|
||||
supportedSignAlgs: []string{"ABC", "DEF"},
|
||||
want: &IDTokenVerifier{
|
||||
Issuer: tu.ValidIssuer,
|
||||
Offset: time.Minute,
|
||||
MaxAgeIAT: time.Hour,
|
||||
ClientID: tu.ValidClientID,
|
||||
KeySet: tu.KeySet{},
|
||||
Nonce: nil,
|
||||
ACR: nil,
|
||||
MaxAge: 2 * time.Hour,
|
||||
SupportedSignAlgs: []string{"ABC", "DEF"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -4,9 +4,9 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
|
||||
tu "github.com/zitadel/oidc/v2/internal/testutil"
|
||||
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
tu "github.com/zitadel/oidc/v3/internal/testutil"
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
// MyCustomClaims extends the TokenClaims base,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue