harmonize jwtProfile and existing interfaces / functions

This commit is contained in:
Livio Amstutz 2020-09-15 16:59:27 +02:00
parent 87b30dcd66
commit a56a4a018a
14 changed files with 259 additions and 632 deletions

View file

@ -20,3 +20,13 @@ type KeySet interface {
// use any HTTP client associated with the context through ClientContext.
VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error)
}
func CheckKey(keyID string, jws *jose.JSONWebSignature, keys ...jose.JSONWebKey) ([]byte, error, bool) {
for _, key := range keys {
if keyID == "" || key.KeyID == keyID {
payload, err := jws.Verify(&key)
return payload, err, true
}
}
return nil, nil, false
}

View file

@ -7,7 +7,7 @@ import (
"github.com/caos/oidc/pkg/utils"
)
func DiscoveryHandler(c Configuration, s Signer) func(http.ResponseWriter, *http.Request) {
func discoveryHandler(c Configuration, s Signer) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
Discover(w, CreateDiscoveryConfig(c, s))
}

View file

@ -13,7 +13,6 @@ import (
"gopkg.in/square/go-jose.v2"
"github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/rp"
"github.com/caos/oidc/pkg/utils"
)
@ -51,6 +50,7 @@ type OpenIDProvider interface {
Decoder() utils.Decoder
Encoder() utils.Encoder
IDTokenHintVerifier() IDTokenHintVerifier
JWTProfileVerifier() JWTProfileVerifier
Crypto() Crypto
DefaultLogoutRedirectURI() string
Signer() Signer
@ -72,9 +72,9 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router
handlers.AllowedHeaders([]string{"authorization", "content-type"}),
handlers.AllowedOriginValidator(allowAllOrigins),
))
router.HandleFunc(healthzEndpoint, Healthz)
router.HandleFunc(readinessEndpoint, Ready(o.Probes()))
router.HandleFunc(oidc.DiscoveryEndpoint, DiscoveryHandler(o, o.Signer()))
router.HandleFunc(healthzEndpoint, healthzHandler)
router.HandleFunc(readinessEndpoint, readyHandler(o.Probes()))
router.HandleFunc(oidc.DiscoveryEndpoint, discoveryHandler(o, o.Signer()))
router.Handle(o.AuthorizationEndpoint().Relative(), intercept(authorizeHandler(o)))
router.Handle(o.AuthorizationEndpoint().Relative()+"/{id}", intercept(authorizeCallbackHandler(o)))
router.Handle(o.TokenEndpoint().Relative(), intercept(tokenHandler(o)))
@ -131,8 +131,6 @@ func NewOpenIDProvider(ctx context.Context, config *Config, storage Storage, opO
o.signer = NewDefaultSigner(ctx, storage, keyCh)
go EnsureKey(ctx, storage, keyCh, o.timer, o.retry)
o.idTokenHintVerifier = NewIDTokenHintVerifier(config.Issuer, o)
o.httpHandler = CreateRouter(o, o.interceptors...)
o.decoder = schema.NewDecoder()
@ -151,6 +149,7 @@ type openidProvider struct {
storage Storage
signer Signer
idTokenHintVerifier IDTokenHintVerifier
jwtProfileVerifier JWTProfileVerifier
crypto Crypto
httpHandler http.Handler
decoder *schema.Decoder
@ -205,9 +204,19 @@ func (o *openidProvider) Encoder() utils.Encoder {
}
func (o *openidProvider) IDTokenHintVerifier() IDTokenHintVerifier {
if o.idTokenHintVerifier == nil {
o.idTokenHintVerifier = NewIDTokenHintVerifier(o.Issuer(), &openIDKeySet{o.Storage()})
}
return o.idTokenHintVerifier
}
func (o *openidProvider) JWTProfileVerifier() JWTProfileVerifier {
if o.jwtProfileVerifier == nil {
o.jwtProfileVerifier = NewJWTProfileVerifier(o.Storage(), o.Issuer())
}
return o.jwtProfileVerifier
}
func (o *openidProvider) Crypto() Crypto {
return o.crypto
}
@ -231,19 +240,23 @@ func (o *openidProvider) HttpHandler() http.Handler {
return o.httpHandler
}
type openIDKeySet struct {
Storage
}
//VerifySignature implements the oidc.KeySet interface
//providing an implementation for the keys stored in the OP Storage interface
func (o *openidProvider) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) {
func (o *openIDKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) {
keyID := ""
for _, sig := range jws.Signatures {
keyID = sig.Header.KeyID
break
}
keySet, err := o.Storage().GetKeySet(ctx)
keySet, err := o.Storage.GetKeySet(ctx)
if err != nil {
return nil, errors.New("error fetching keys")
}
payload, err, ok := rp.CheckKey(keyID, keySet.Keys, jws)
payload, err, ok := oidc.CheckKey(keyID, jws, keySet.Keys...)
if !ok {
return nil, errors.New("invalid kid")
}

View file

@ -10,11 +10,11 @@ import (
type ProbesFn func(context.Context) error
func Healthz(w http.ResponseWriter, r *http.Request) {
func healthzHandler(w http.ResponseWriter, r *http.Request) {
ok(w)
}
func Ready(probes []ProbesFn) func(w http.ResponseWriter, r *http.Request) {
func readyHandler(probes []ProbesFn) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
Readiness(w, r, probes...)
}

View file

@ -30,7 +30,7 @@ type OPStorage interface {
AuthorizeClientIDSecret(context.Context, string, string) error
GetUserinfoFromScopes(context.Context, string, []string) (*oidc.Userinfo, error)
GetUserinfoFromToken(context.Context, string, string) (*oidc.Userinfo, error)
GetKeyByID(ctx context.Context, keyID string) (*jose.JSONWebKeySet, error)
GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error)
}
type Storage interface {

View file

@ -4,12 +4,8 @@ import (
"context"
"errors"
"net/http"
"time"
"gopkg.in/square/go-jose.v2"
"github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/rp"
"github.com/caos/oidc/pkg/utils"
)
@ -20,11 +16,7 @@ type Exchanger interface {
Signer() Signer
Crypto() Crypto
AuthMethodPostSupported() bool
}
type VerifyExchanger interface {
Exchanger
ClientJWTVerifier() oidc.Verifier
JWTProfileVerifier() JWTProfileVerifier
}
func tokenHandler(exchanger Exchanger) func(w http.ResponseWriter, r *http.Request) {
@ -34,16 +26,16 @@ func tokenHandler(exchanger Exchanger) func(w http.ResponseWriter, r *http.Reque
CodeExchange(w, r, exchanger)
return
case string(oidc.GrantTypeBearer):
ex, _ := exchanger.(VerifyExchanger)
JWTExchange(w, r, ex)
JWTProfile(w, r, exchanger)
return
case "excahnge":
case "exchange":
TokenExchange(w, r, exchanger)
case "":
RequestError(w, r, ErrInvalidRequest("grant_type missing"))
return
default:
RequestError(w, r, ErrInvalidRequest("grant_type not supported"))
return
}
}
}
@ -144,41 +136,13 @@ func AuthorizeCodeChallenge(ctx context.Context, tokenReq *oidc.AccessTokenReque
return authReq, nil
}
type ClientJWTVerifier struct {
claims *oidc.JWTTokenRequest
storage Storage
issuer string
}
func (c ClientJWTVerifier) Storage() Storage {
return c.storage
}
func (c ClientJWTVerifier) Issuer() string {
return c.claims.Issuer
}
func (c ClientJWTVerifier) ClientID() string {
return c.issuer
}
func (c ClientJWTVerifier) MaxAgeIAT() time.Duration {
//TODO: define in conf/opts
return 1 * time.Hour
}
func (c ClientJWTVerifier) Offset() time.Duration {
//TODO: define in conf/opts
return time.Second
}
func JWTExchange(w http.ResponseWriter, r *http.Request, exchanger VerifyExchanger) {
assertion, err := ParseJWTTokenRequest(r, exchanger.Decoder())
func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger Exchanger) {
assertion, err := ParseJWTProfileRequest(r, exchanger.Decoder())
if err != nil {
RequestError(w, r, err)
}
claims, err := VerifyJWTAssertion(r.Context(), assertion, exchanger)
claims, err := VerifyJWTAssertion(r.Context(), assertion, exchanger.JWTProfileVerifier())
if err != nil {
RequestError(w, r, err)
return
@ -192,70 +156,7 @@ func JWTExchange(w http.ResponseWriter, r *http.Request, exchanger VerifyExchang
utils.MarshalJSON(w, resp)
}
type JWTAssertionVerifier interface {
Storage() Storage
oidc.Verifier
}
func VerifyJWTAssertion(ctx context.Context, assertion string, exchanger Exchanger) (*oidc.JWTTokenRequest, error) {
verifier := &ClientJWTVerifier{
storage: exchanger.Storage(),
issuer: exchanger.Issuer(),
claims: new(oidc.JWTTokenRequest),
}
payload, err := oidc.ParseToken(assertion, verifier.claims)
if err != nil {
return nil, err
}
if err = oidc.CheckAudience(verifier.claims, verifier.issuer); err != nil {
return nil, err
}
if err = oidc.CheckExpiration(verifier.claims, verifier.Offset()); err != nil {
return nil, err
}
if err = oidc.CheckIssuedAt(verifier.claims, verifier.MaxAgeIAT(), verifier.Offset()); err != nil {
return nil, err
}
if verifier.claims.Issuer != verifier.claims.Subject {
//TODO: implement delegation (openid core / oauth rfc)
}
verifier.Storage().GetClientByClientID(ctx, verifier.claims.Subject)
keySet := &ClientAssertionKeySet{exchanger.Storage(), verifier.claims.Subject}
if err = oidc.CheckSignature(ctx, assertion, payload, verifier.claims, nil, keySet); err != nil {
return nil, err
}
return verifier.claims, nil
}
type ClientAssertionKeySet struct {
Storage
id string
}
func (c *ClientAssertionKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) {
keyID := ""
for _, sig := range jws.Signatures {
keyID = sig.Header.KeyID
break
}
keySet, err := c.Storage.GetKeyByID(ctx, keyID)
if err != nil {
return nil, errors.New("error fetching keys")
}
payload, err, ok := rp.CheckKey(keyID, keySet.Keys, jws)
if !ok {
return nil, errors.New("invalid kid")
}
return payload, err
}
func ParseJWTTokenRequest(r *http.Request, decoder utils.Decoder) (string, error) {
func ParseJWTProfileRequest(r *http.Request, decoder utils.Decoder) (string, error) {
err := r.ParseForm()
if err != nil {
return "", ErrInvalidRequest("error parsing form")
@ -267,7 +168,6 @@ func ParseJWTTokenRequest(r *http.Request, decoder utils.Decoder) (string, error
if err != nil {
return "", ErrInvalidRequest("error decoding form")
}
//TODO: validations
return tokenReq.Token, nil
}

View file

@ -0,0 +1,99 @@
package op
import (
"context"
"errors"
"time"
"gopkg.in/square/go-jose.v2"
"github.com/caos/oidc/pkg/oidc"
)
type JWTProfileVerifier interface {
oidc.Verifier
Storage() Storage
}
type jwtProfileVerifier struct {
storage Storage
issuer string
}
func NewJWTProfileVerifier(storage Storage, issuer string) JWTProfileVerifier {
return &jwtProfileVerifier{
storage: storage,
issuer: issuer,
}
}
func (v *jwtProfileVerifier) Issuer() string {
return v.issuer
}
func (v *jwtProfileVerifier) Storage() Storage {
return v.storage
}
func (v *jwtProfileVerifier) MaxAgeIAT() time.Duration {
//TODO: define in conf/opts
return 1 * time.Hour
}
func (v *jwtProfileVerifier) Offset() time.Duration {
//TODO: define in conf/opts
return time.Second
}
func VerifyJWTAssertion(ctx context.Context, assertion string, v JWTProfileVerifier) (*oidc.JWTTokenRequest, error) {
request := new(oidc.JWTTokenRequest)
payload, err := oidc.ParseToken(assertion, request)
if err != nil {
return nil, err
}
if err = oidc.CheckAudience(request, v.Issuer()); err != nil {
return nil, err
}
if err = oidc.CheckExpiration(request, v.Offset()); err != nil {
return nil, err
}
if err = oidc.CheckIssuedAt(request, v.MaxAgeIAT(), v.Offset()); err != nil {
return nil, err
}
if request.Issuer != request.Subject {
//TODO: implement delegation (openid core / oauth rfc)
}
keySet := &jwtProfileKeySet{v.Storage(), request.Subject}
if err = oidc.CheckSignature(ctx, assertion, payload, request, nil, keySet); err != nil {
return nil, err
}
return request, nil
}
type jwtProfileKeySet struct {
Storage
userID string
}
func (k *jwtProfileKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) {
keyID := ""
for _, sig := range jws.Signatures {
keyID = sig.Header.KeyID
break
}
key, err := k.Storage.GetKeyByIDAndUserID(ctx, keyID, k.userID)
if err != nil {
return nil, errors.New("error fetching keys")
}
payload, err, ok := oidc.CheckKey(keyID, jws, *key)
if !ok {
return nil, errors.New("invalid kid")
}
return payload, err
}

View file

@ -1,236 +0,0 @@
package rp
import (
"context"
"net/http"
"strings"
"golang.org/x/oauth2"
"github.com/caos/oidc/pkg/oidc"
grants_tx "github.com/caos/oidc/pkg/oidc/grants/tokenexchange"
"github.com/caos/oidc/pkg/utils"
)
const (
idTokenKey = "id_token"
stateParam = "state"
pkceCode = "pkce"
)
//deprecated: use NewRelayingParty instead
//DefaultRP implements the `DelegationTokenExchangeRP` interface extending the `RelayingParty` interface
type DefaultRP struct {
endpoints Endpoints
oauthConfig oauth2.Config
config *Config
pkce bool
httpClient *http.Client
cookieHandler *utils.CookieHandler
errorHandler func(http.ResponseWriter, *http.Request, string, string, string)
idTokenVerifier IDTokenVerifier
verifierOpts []ConfFunc
onlyOAuth2 bool
}
func (p *DefaultRP) ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) {
return p.errorHandler
}
func (p *DefaultRP) OAuthConfig() *oauth2.Config {
return &p.oauthConfig
}
func (p *DefaultRP) IsPKCE() bool {
return p.pkce
}
func (p *DefaultRP) CookieHandler() *utils.CookieHandler {
return p.cookieHandler
}
func (p *DefaultRP) HttpClient() *http.Client {
return p.httpClient
}
func (p *DefaultRP) IsOAuth2Only() bool {
return p.onlyOAuth2
}
func (p *DefaultRP) IDTokenVerifier() IDTokenVerifier {
return p.idTokenVerifier
}
//deprecated: use NewRelayingParty instead
//
//NewDefaultRP creates `DefaultRP` with the given
//Config and possible configOptions
//it will run discovery on the provided issuer
//if no verifier is provided using the options the `DefaultVerifier` is set
func NewDefaultRP(rpConfig *Config, rpOpts ...DefaultRPOpts) (DelegationTokenExchangeRP, error) {
foundOpenID := false
for _, scope := range rpConfig.Scopes {
if scope == "openid" {
foundOpenID = true
}
}
p := &DefaultRP{
config: rpConfig,
httpClient: utils.DefaultHTTPClient,
onlyOAuth2: !foundOpenID,
}
for _, optFunc := range rpOpts {
optFunc(p)
}
if rpConfig.Endpoints.TokenURL != "" && rpConfig.Endpoints.AuthURL != "" {
p.oauthConfig = p.getOAuthConfig(rpConfig.Endpoints)
} else {
if err := p.discover(); err != nil {
return nil, err
}
}
if p.errorHandler == nil {
p.errorHandler = DefaultErrorHandler
}
if p.idTokenVerifier == nil {
p.idTokenVerifier = NewIDTokenVerifier(rpConfig.Issuer, rpConfig.ClientID, NewRemoteKeySet(p.httpClient, p.endpoints.JKWsURL))
}
return p, nil
}
//DefaultRPOpts is the type for providing dynamic options to the DefaultRP
type DefaultRPOpts func(p *DefaultRP)
/*
//WithCookieHandler set a `CookieHandler` for securing the various redirects
func WithCookieHandler(cookieHandler *utils.CookieHandler) DefaultRPOpts {
return func(p *DefaultRP) {
p.cookieHandler = cookieHandler
}
}
//WithPKCE sets the RP to use PKCE (oauth2 code challenge)
//it also sets a `CookieHandler` for securing the various redirects
//and exchanging the code challenge
func WithPKCE(cookieHandler *utils.CookieHandler) DefaultRPOpts {
return func(p *DefaultRP) {
p.pkce = true
p.cookieHandler = cookieHandler
}
}
//WithHTTPClient provides the ability to set an http client to be used for the relaying party and verifier
func WithHTTPClient(client *http.Client) DefaultRPOpts {
return func(p *DefaultRP) {
p.httpClient = client
}
}
func WithVerifierOpts(opts ...ConfFunc) DefaultRPOpts {
return func(p *DefaultRP) {
p.verifierOpts = opts
}
}
*/
//AuthURL is the `RelayingParty` interface implementation
//wrapping the oauth2 `AuthCodeURL`
//returning the url of the auth request
func (p *DefaultRP) AuthURL(state string, opts ...AuthURLOpt) string {
return AuthURL(state, p, opts...)
}
//AuthURL is the `RelayingParty` interface implementation
//extending the `AuthURL` method with a http redirect handler
func (p *DefaultRP) AuthURLHandler(state string) http.HandlerFunc {
return AuthURLHandler(
func() string {
return state
}, p,
)
}
//deprecated: Use CodeExchange func and provide a RelayingParty
//
//AuthURL is the `RelayingParty` interface implementation
//handling the oauth2 code exchange, extracting and validating the id_token
//returning it parsed together with the oauth2 tokens (access, refresh)
func (p *DefaultRP) CodeExchange(ctx context.Context, code string, opts ...CodeExchangeOpt) (tokens *oidc.Tokens, err error) {
return CodeExchange(ctx, code, p, opts...)
}
//AuthURL is the `RelayingParty` interface implementation
//extending the `CodeExchange` method with callback function
func (p *DefaultRP) CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string)) http.HandlerFunc {
return CodeExchangeHandler(callback, p)
}
// func (p *DefaultRP) Introspect(ctx context.Context, accessToken string) (oidc.TokenIntrospectResponse, error) {
// // req := &http.Request{}
// // resp, err := p.httpClient.Do(req)
// // if err != nil {
// // }
// // p.endpoints.IntrospectURL
// return nil, nil
// }
func (p *DefaultRP) Userinfo() {}
//ClientCredentials is the `RelayingParty` interface implementation
//handling the oauth2 client credentials grant
func (p *DefaultRP) ClientCredentials(ctx context.Context, scopes ...string) (newToken *oauth2.Token, err error) {
return ClientCredentials(ctx, p, scopes...)
}
//TokenExchange is the `TokenExchangeRP` interface implementation
//handling the oauth2 token exchange (draft)
func (p *DefaultRP) TokenExchange(ctx context.Context, request *grants_tx.TokenExchangeRequest) (newToken *oauth2.Token, err error) {
return TokenExchange(ctx, request, p)
}
//DelegationTokenExchange is the `TokenExchangeRP` interface implementation
//handling the oauth2 token exchange for a delegation token (draft)
func (p *DefaultRP) DelegationTokenExchange(ctx context.Context, subjectToken string, reqOpts ...grants_tx.TokenExchangeOption) (newToken *oauth2.Token, err error) {
return TokenExchange(ctx, DelegationTokenRequest(subjectToken, reqOpts...), p)
}
func (p *DefaultRP) discover() error {
wellKnown := strings.TrimSuffix(p.config.Issuer, "/") + oidc.DiscoveryEndpoint
req, err := http.NewRequest("GET", wellKnown, nil)
if err != nil {
return err
}
discoveryConfig := new(oidc.DiscoveryConfiguration)
err = utils.HttpRequest(p.httpClient, req, &discoveryConfig)
if err != nil {
return err
}
p.endpoints = GetEndpoints(discoveryConfig)
p.oauthConfig = p.getOAuthConfig(p.endpoints.Endpoint)
return nil
}
func (p *DefaultRP) getOAuthConfig(endpoint oauth2.Endpoint) oauth2.Config {
return oauth2.Config{
ClientID: p.config.ClientID,
ClientSecret: p.config.ClientSecret,
Endpoint: endpoint,
RedirectURL: p.config.CallbackURL,
Scopes: p.config.Scopes,
}
}
func (p *DefaultRP) Client(ctx context.Context, token *oauth2.Token) *http.Client {
return p.oauthConfig.Client(ctx, token)
}

View file

@ -1,190 +0,0 @@
package rp
import (
"context"
"time"
"github.com/caos/oidc/pkg/oidc"
)
//deprecated: use IDTokenVerifier or oidc.Verifier interfaces
//DefaultVerifier implements the `Verifier` interface
type DefaultVerifier struct {
config *verifierConfig
keySet oidc.KeySet
}
//ConfFunc is the type for providing dynamic options to the DefaultVerifier
type ConfFunc func(*verifierConfig)
//deprecated: use NewIDTokenVerifier
//NewDefaultVerifier creates `DefaultVerifier` with the given
//issuer, clientID, keyset and possible configOptions
func NewDefaultVerifier(issuer, clientID string, keySet oidc.KeySet, confOpts ...ConfFunc) Verifier {
conf := &verifierConfig{
issuer: issuer,
clientID: clientID,
iat: &iatConfig{
// offset: time.Duration(500 * time.Millisecond),
},
}
for _, opt := range confOpts {
if opt != nil {
opt(conf)
}
}
return &DefaultVerifier{config: conf, keySet: keySet}
}
//WithIgnoreAudience will turn off validation for audience claim (should only be used for id_token_hints)
func WithIgnoreAudience() func(*verifierConfig) {
return func(conf *verifierConfig) {
conf.ignoreAudience = true
}
}
//WithIgnoreExpiration will turn off validation for expiration claim (should only be used for id_token_hints)
func WithIgnoreExpiration() func(*verifierConfig) {
return func(conf *verifierConfig) {
conf.ignoreExpiration = true
}
}
//WithIgnoreIssuedAt will turn off iat claim verification
func WithIgnoreIssuedAt() func(*verifierConfig) {
return func(conf *verifierConfig) {
conf.iat.ignore = true
}
}
//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(*verifierConfig) {
return func(conf *verifierConfig) {
conf.iat.offset = offset
}
}
//WithIssuedAtMaxAge provides the ability to define the maximum duration between iat and now
func WithIssuedAtMaxAge(maxAge time.Duration) func(*verifierConfig) {
return func(conf *verifierConfig) {
conf.iat.maxAge = maxAge
}
}
//WithNonce TODO: ?
func WithNonce(nonce string) func(*verifierConfig) {
return func(conf *verifierConfig) {
conf.nonce = nonce
}
}
//WithACRVerifier sets the verifier for the acr claim
func WithACRVerifier(verifier oidc.ACRVerifier) func(*verifierConfig) {
return func(conf *verifierConfig) {
conf.acr = verifier
}
}
//WithAuthTimeMaxAge provides the ability to define the maximum duration between auth_time and now
func WithAuthTimeMaxAge(maxAge time.Duration) func(*verifierConfig) {
return func(conf *verifierConfig) {
conf.maxAge = maxAge
}
}
//WithSupportedSigningAlgorithms overwrites the default RS256 signing algorithm
func WithSupportedSigningAlgorithms(algs ...string) func(*verifierConfig) {
return func(conf *verifierConfig) {
conf.supportedSignAlgs = algs
}
}
type verifierConfig struct {
issuer string
clientID string
nonce string
ignoreAudience bool
ignoreExpiration bool
iat *iatConfig
acr oidc.ACRVerifier
maxAge time.Duration
supportedSignAlgs []string
// httpClient *http.Client
now time.Time
}
type iatConfig struct {
ignore bool
offset time.Duration
maxAge time.Duration
}
//deprecated: use oidc.DefaultACRVerifier directly
//DefaultACRVerifier implements `ACRVerifier` returning an error
//if non of the provided values matches the acr claim
func DefaultACRVerifier(possibleValues []string) oidc.ACRVerifier {
return oidc.DefaultACRVerifier(possibleValues)
}
//deprecated: use VerifyTokens(ctx context.Context, accessToken, idTokenString string, v IDTokenVerifier) (*oidc.IDTokenClaims, error) instead
//Verify implements the `Verify` method of the `Verifier` interface
//according to https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
//and https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowTokenValidation
func (v *DefaultVerifier) Verify(ctx context.Context, accessToken, idTokenString string) (*oidc.IDTokenClaims, error) {
v.config.now = time.Now().UTC()
return VerifyTokens(ctx, accessToken, idTokenString, v)
}
//deprecated: use VerifyIDToken(ctx context.Context, token string, v IDTokenVerifier) (*oidc.IDTokenClaims, error) instead
//Verify implements the `VerifyIDToken` method of the `Verifier` interface
//according to https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
func (v *DefaultVerifier) VerifyIDToken(ctx context.Context, idTokenString string) (*oidc.IDTokenClaims, error) {
return VerifyIDToken(ctx, idTokenString, v)
}
func (v *DefaultVerifier) now() time.Time {
if v.config.now.IsZero() {
v.config.now = time.Now().UTC().Round(time.Second)
}
return v.config.now
}
func (v *DefaultVerifier) Issuer() string {
return v.config.issuer
}
func (v *DefaultVerifier) ClientID() string {
return v.config.clientID
}
func (v *DefaultVerifier) SupportedSignAlgs() []string {
return v.config.supportedSignAlgs
}
func (v *DefaultVerifier) KeySet() oidc.KeySet {
return v.keySet
}
func (v *DefaultVerifier) ACR() oidc.ACRVerifier {
return v.config.acr
}
func (v *DefaultVerifier) MaxAge() time.Duration {
return v.config.maxAge
}
func (v *DefaultVerifier) MaxAgeIAT() time.Duration {
return v.config.iat.maxAge
}
func (v *DefaultVerifier) Offset() time.Duration {
return v.config.iat.offset
}
func (v *DefaultVerifier) Nonce(ctx context.Context) string {
return ""
}

View file

@ -74,7 +74,7 @@ func (r *remoteKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSig
}
keys := r.keysFromCache()
payload, err, ok := CheckKey(keyID, keys, jws)
payload, err, ok := oidc.CheckKey(keyID, jws, keys...)
if ok {
return payload, err
}
@ -84,7 +84,7 @@ func (r *remoteKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSig
return nil, fmt.Errorf("fetching keys %v", err)
}
payload, err, ok = CheckKey(keyID, keys, jws)
payload, err, ok = oidc.CheckKey(keyID, jws, keys...)
if !ok {
return nil, errors.New("invalid kid")
}

View file

@ -1,15 +0,0 @@
package rp
import (
"gopkg.in/square/go-jose.v2"
)
func CheckKey(keyID string, keys []jose.JSONWebKey, jws *jose.JSONWebSignature) ([]byte, error, bool) {
for _, key := range keys {
if keyID == "" || key.KeyID == keyID {
payload, err := jws.Verify(&key)
return payload, err, true
}
}
return nil, nil, false
}

View file

@ -13,6 +13,12 @@ import (
"golang.org/x/oauth2"
)
const (
idTokenKey = "id_token"
stateParam = "state"
pkceCode = "pkce"
)
//RelayingParty declares the minimal interface for oidc clients
type RelayingParty interface {
//OAuthConfig returns the oauth2 Config
@ -27,54 +33,12 @@ type RelayingParty interface {
//Client return a standard http client where the token can be used
Client(ctx context.Context, token *oauth2.Token) *http.Client
/*
//AuthURL returns the authorization endpoint with a given state
AuthURL(state string, opts ...AuthURLOpt) string
//AuthURLHandler should implement the AuthURL func as http.HandlerFunc
//(redirecting to the auth endpoint)
AuthURLHandler(state string) http.HandlerFunc
//CodeExchange implements the OIDC Token Request (oauth2 Authorization Code Grant)
//returning an `Access Token` and `ID Token Claims`
CodeExchange(ctx context.Context, code string, opts ...CodeExchangeOpt) (*oidc.Tokens, error)
//CodeExchangeHandler extends the CodeExchange func,
//calling the provided callback func on success with additional returned `state`
CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string)) http.HandlerFunc
//ClientCredentials implements the oauth2 Client Credentials Grant
//requesting an `Access Token` for the client itself, without user context
ClientCredentials(ctx context.Context, scopes ...string) (*oauth2.Token, error)
//Introspects calls the Introspect Endpoint
//for validating an (access) token
// Introspect(ctx context.Context, token string) (TokenIntrospectResponse, error)
//Userinfo implements the OIDC Userinfo call
//returning the info of the user for the requested scopes of an access token
Userinfo()
*/
HttpClient() *http.Client
IsOAuth2Only() bool
IDTokenVerifier() IDTokenVerifier
ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string)
}
//
////PasswortGrantRP extends the `RelayingParty` interface with the oauth2 `Password Grant`
////
////This interface is separated from the standard `RelayingParty` interface as the `password grant`
////is part of the oauth2 and therefore OIDC specification, but should only be used when there's no
////other possibility, so IMHO never ever. Ever.
//type PasswortGrantRP interface {
// RelayingParty
//
// //PasswordGrant implements the oauth2 `Password Grant`,
// //requesting an access token with the users `username` and `password`
// PasswordGrant(context.Context, string, string) (*oauth2.Token, error)
//}
var (
DefaultErrorHandler = func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) {
http.Error(w, errorType+": "+errorDesc, http.StatusInternalServerError)
@ -84,7 +48,6 @@ var (
type relayingParty struct {
endpoints Endpoints
oauthConfig *oauth2.Config
config *Configuration
pkce bool
@ -94,12 +57,12 @@ type relayingParty struct {
errorHandler func(http.ResponseWriter, *http.Request, string, string, string)
idTokenVerifier IDTokenVerifier
verifierOpts []ConfFunc
verifierOpts []VerifierOption
oauth2Only bool
}
func (rp *relayingParty) OAuthConfig() *oauth2.Config {
return rp.oauthConfig
return rp.config.Config
}
func (rp *relayingParty) IsPKCE() bool {
@ -119,11 +82,14 @@ func (rp *relayingParty) IsOAuth2Only() bool {
}
func (rp *relayingParty) IDTokenVerifier() IDTokenVerifier {
if rp.idTokenVerifier == nil {
rp.idTokenVerifier = NewIDTokenVerifier(rp.config.Issuer, rp.config.ClientID, NewRemoteKeySet(rp.httpClient, rp.endpoints.JKWsURL), rp.verifierOpts...)
}
return rp.idTokenVerifier
}
func (rp *relayingParty) Client(ctx context.Context, token *oauth2.Token) *http.Client {
panic("implement me")
return rp.config.Config.Client(ctx, token)
}
func (rp *relayingParty) ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) {
@ -147,15 +113,12 @@ func NewRelayingParty(config *Configuration, options ...Option) (RelayingParty,
optFunc(rp)
}
rp.oauthConfig = config.Config
if config.Endpoint.AuthURL != "" && config.Endpoint.TokenURL != "" {
rp.oauthConfig = config.Config
} else {
if isOpenID && config.Endpoint.AuthURL == "" && config.Endpoint.TokenURL == "" {
endpoints, err := Discover(config.Issuer, rp.httpClient)
if err != nil {
return nil, err
}
rp.oauthConfig.Endpoint = endpoints.Endpoint
rp.config.Endpoint = endpoints.Endpoint
rp.endpoints = endpoints
}
@ -170,6 +133,47 @@ func NewRelayingParty(config *Configuration, options ...Option) (RelayingParty,
return rp, nil
}
func NewRelayingParty2(clientID, clientSecret, redirectURI string, options ...Option) (RelayingParty, error) {
rp := &relayingParty{
config: &Configuration{
Config: &oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: redirectURI,
},
},
httpClient: utils.DefaultHTTPClient,
oauth2Only: true,
}
for _, optFunc := range options {
optFunc(rp)
}
if !rp.oauth2Only && rp.config.Endpoint.AuthURL == "" && rp.config.Endpoint.TokenURL == "" {
endpoints, err := Discover(rp.config.Issuer, rp.httpClient)
if err != nil {
return nil, err
}
rp.config.Endpoint = endpoints.Endpoint
rp.endpoints = endpoints
}
if rp.errorHandler == nil {
rp.errorHandler = DefaultErrorHandler
}
return rp, nil
}
func WithOIDC(issuer string, scopes []string) Option {
return func(rp *relayingParty) {
rp.config.Issuer = issuer
rp.config.Scopes = scopes
rp.oauth2Only = false
}
}
//DefaultRPOpts is the type for providing dynamic options to the DefaultRP
type Option func(*relayingParty)
@ -197,7 +201,7 @@ func WithHTTPClient(client *http.Client) Option {
}
}
func WithVerifierOpts(opts ...ConfFunc) Option {
func WithVerifierOpts(opts ...VerifierOption) Option {
return func(rp *relayingParty) {
rp.verifierOpts = opts
}
@ -419,13 +423,3 @@ func isOpenID(scopes []string) bool {
}
return false
}
//deprecated: use Configuration instead
type Config struct {
ClientID string
ClientSecret string
CallbackURL string
Issuer string
Scopes []string
Endpoints oauth2.Endpoint
}

View file

@ -103,16 +103,68 @@ func VerifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAl
//NewIDTokenVerifier returns an implementation of `IDTokenVerifier`
//for `VerifyTokens` and `VerifyIDToken`
func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet) IDTokenVerifier {
return &idTokenVerifier{
func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet, options ...VerifierOption) IDTokenVerifier {
v := &idTokenVerifier{
issuer: issuer,
clientID: clientID,
keySet: keySet,
offset: 5 * time.Second,
offset: 1 * time.Second,
nonce: func(_ context.Context) string {
return ""
},
}
for _, opts := range options {
opts(v)
}
return v
}
//VerifierOption is the type for providing dynamic options to the 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
}
}
//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.maxAge = maxAge
}
}
//WithNonce sets the function to check the nonce
func WithNonce(nonce func(context.Context) string) VerifierOption {
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
}
}
//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
}
}
//WithSupportedSigningAlgorithms overwrites the default RS256 signing algorithm
func WithSupportedSigningAlgorithms(algs ...string) VerifierOption {
return func(v *idTokenVerifier) {
v.supportedSignAlgs = algs
}
}
type idTokenVerifier struct {