Skip to content

Commit

Permalink
feat: cache OIDC providers
Browse files Browse the repository at this point in the history
This change significantly reduces the number of requests to `/.well-known/openid-configuration` endpoints.
  • Loading branch information
zepatrik committed Nov 25, 2024
1 parent e6d2d4d commit 05f911d
Show file tree
Hide file tree
Showing 25 changed files with 169 additions and 141 deletions.
17 changes: 4 additions & 13 deletions selfservice/strategy/oidc/pkce.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ func maybePKCE(ctx context.Context, d pkceDependencies, _p Provider) (verifier s
return ""
}

p, ok := _p.(OAuth2Provider)
p, ok := _p.(PKCEEnabledProvider)
if !ok {
return ""
}

if p.Config().PKCE != "force" {
// autodiscover PKCE support
pkceSupported, err := discoverPKCE(ctx, d, p)
pkceSupported, err := p.PKCEEnabled(ctx)
if err != nil {
d.Logger().WithError(err).Warnf("Failed to autodiscover PKCE support for provider %q. Continuing without PKCE.", p.Config().ID)
return ""
Expand All @@ -59,20 +59,11 @@ func maybePKCE(ctx context.Context, d pkceDependencies, _p Provider) (verifier s
return oauth2.GenerateVerifier()
}

func discoverPKCE(ctx context.Context, d pkceDependencies, p OAuth2Provider) (pkceSupported bool, err error) {
if p.Config().IssuerURL == "" {
return false, errors.New("Issuer URL must be set to autodiscover PKCE support")
}

ctx = gooidc.ClientContext(ctx, d.HTTPClient(ctx).HTTPClient)
gp, err := gooidc.NewProvider(ctx, p.Config().IssuerURL)
if err != nil {
return false, errors.Wrap(err, "failed to initialize provider")
}
func discoverPKCE(p *gooidc.Provider) (pkceSupported bool, err error) {
var claims struct {
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
}
if err := gp.Claims(&claims); err != nil {
if err := p.Claims(&claims); err != nil {
return false, errors.Wrap(err, "failed to deserialize provider claims")
}
return slices.Contains(claims.CodeChallengeMethodsSupported, "S256"), nil
Expand Down
70 changes: 37 additions & 33 deletions selfservice/strategy/oidc/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,28 @@ import (
"github.com/ory/kratos/x"
)

type Provider interface {
Config() *Configuration
}

type OAuth2Provider interface {
Provider
AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption
OAuth2(ctx context.Context) (*oauth2.Config, error)
Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error)
}

type OAuth1Provider interface {
Provider
OAuth1(ctx context.Context) *oauth1.Config
AuthURL(ctx context.Context, state string) (string, error)
Claims(ctx context.Context, token *oauth1.Token) (*Claims, error)
ExchangeToken(ctx context.Context, req *http.Request) (*oauth1.Token, error)
}
type (
Provider interface {
Config() *Configuration
}
OAuth2Provider interface {
Provider
AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption
OAuth2(ctx context.Context) (*oauth2.Config, error)
Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error)
}
OAuth1Provider interface {
Provider
OAuth1(ctx context.Context) *oauth1.Config
AuthURL(ctx context.Context, state string) (string, error)
Claims(ctx context.Context, token *oauth1.Token) (*Claims, error)
ExchangeToken(ctx context.Context, req *http.Request) (*oauth1.Token, error)
}
PKCEEnabledProvider interface {
OAuth2Provider
PKCEEnabled(ctx context.Context) (bool, error)
}
)

type OAuth2TokenExchanger interface {
Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error)
Expand All @@ -51,22 +55,22 @@ type NonceValidationSkipper interface {
CanSkipNonce(*Claims) bool
}

// ConvertibleBoolean is used as Apple casually sends the email_verified field as a string.
type Claims struct {
Issuer string `json:"iss,omitempty"`
Subject string `json:"sub,omitempty"`
Object string `json:"oid,omitempty"`
Name string `json:"name,omitempty"`
GivenName string `json:"given_name,omitempty"`
FamilyName string `json:"family_name,omitempty"`
LastName string `json:"last_name,omitempty"`
MiddleName string `json:"middle_name,omitempty"`
Nickname string `json:"nickname,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty"`
Profile string `json:"profile,omitempty"`
Picture string `json:"picture,omitempty"`
Website string `json:"website,omitempty"`
Email string `json:"email,omitempty"`
Issuer string `json:"iss,omitempty"`
Subject string `json:"sub,omitempty"`
Object string `json:"oid,omitempty"`
Name string `json:"name,omitempty"`
GivenName string `json:"given_name,omitempty"`
FamilyName string `json:"family_name,omitempty"`
LastName string `json:"last_name,omitempty"`
MiddleName string `json:"middle_name,omitempty"`
Nickname string `json:"nickname,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty"`
Profile string `json:"profile,omitempty"`
Picture string `json:"picture,omitempty"`
Website string `json:"website,omitempty"`
Email string `json:"email,omitempty"`
// ConvertibleBoolean is used as Apple casually sends the email_verified field as a string.
EmailVerified x.ConvertibleBoolean `json:"email_verified,omitempty"`
Gender string `json:"gender,omitempty"`
Birthdate string `json:"birthdate,omitempty"`
Expand Down
2 changes: 2 additions & 0 deletions selfservice/strategy/oidc/provider_apple.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ type ProviderApple struct {
JWKSUrl string
}

var _ OAuth2Provider = (*ProviderApple)(nil)

func NewProviderApple(
config *Configuration,
reg Dependencies,
Expand Down
2 changes: 2 additions & 0 deletions selfservice/strategy/oidc/provider_auth0.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ type ProviderAuth0 struct {
*ProviderGenericOIDC
}

var _ OAuth2Provider = (*ProviderAuth0)(nil)

func NewProviderAuth0(
config *Configuration,
reg Dependencies,
Expand Down
9 changes: 4 additions & 5 deletions selfservice/strategy/oidc/provider_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ package oidc

import (
"encoding/json"
"maps"
"net/url"
"slices"
"strings"

"github.com/pkg/errors"
"golang.org/x/exp/maps"

"github.com/ory/herodot"

"github.com/ory/x/urlx"
)

Expand Down Expand Up @@ -181,14 +181,13 @@ var supportedProviders = map[string]func(config *Configuration, reg Dependencies
}

func (c ConfigurationCollection) Provider(id string, reg Dependencies) (Provider, error) {
for k := range c.Providers {
p := c.Providers[k]
for _, p := range c.Providers {
if p.ID == id {
if f, ok := supportedProviders[p.Provider]; ok {
return f(&p, reg), nil
}

return nil, errors.Errorf("provider type %s is not supported, supported are: %v", p.Provider, maps.Keys(supportedProviders))
return nil, errors.Errorf("provider type %s is not supported, supported are: %v", p.Provider, slices.Collect(maps.Keys(supportedProviders)))

Check failure on line 190 in selfservice/strategy/oidc/provider_config.go

View workflow job for this annotation

GitHub Actions / Run tests and lints

undefined: slices.Collect

Check failure on line 190 in selfservice/strategy/oidc/provider_config.go

View workflow job for this annotation

GitHub Actions / Run tests and lints

undefined: maps.Keys

Check failure on line 190 in selfservice/strategy/oidc/provider_config.go

View workflow job for this annotation

GitHub Actions / Run tests and lints

undefined: slices.Collect

Check failure on line 190 in selfservice/strategy/oidc/provider_config.go

View workflow job for this annotation

GitHub Actions / Run tests and lints

undefined: maps.Keys

Check failure on line 190 in selfservice/strategy/oidc/provider_config.go

View workflow job for this annotation

GitHub Actions / Run Playwright end-to-end tests (sqlite)

undefined: slices.Collect

Check failure on line 190 in selfservice/strategy/oidc/provider_config.go

View workflow job for this annotation

GitHub Actions / Run Playwright end-to-end tests (sqlite)

undefined: maps.Keys

Check failure on line 190 in selfservice/strategy/oidc/provider_config.go

View workflow job for this annotation

GitHub Actions / Run Playwright end-to-end tests (cockroach)

undefined: slices.Collect

Check failure on line 190 in selfservice/strategy/oidc/provider_config.go

View workflow job for this annotation

GitHub Actions / Run Playwright end-to-end tests (cockroach)

undefined: maps.Keys

Check failure on line 190 in selfservice/strategy/oidc/provider_config.go

View workflow job for this annotation

GitHub Actions / Run Playwright end-to-end tests (postgres)

undefined: slices.Collect

Check failure on line 190 in selfservice/strategy/oidc/provider_config.go

View workflow job for this annotation

GitHub Actions / Run Playwright end-to-end tests (postgres)

undefined: maps.Keys

Check failure on line 190 in selfservice/strategy/oidc/provider_config.go

View workflow job for this annotation

GitHub Actions / Run Playwright end-to-end tests (mysql)

undefined: slices.Collect

Check failure on line 190 in selfservice/strategy/oidc/provider_config.go

View workflow job for this annotation

GitHub Actions / Run Playwright end-to-end tests (mysql)

undefined: maps.Keys
}
}
return nil, errors.WithStack(herodot.ErrNotFound.WithReasonf(`OpenID Connect Provider "%s" is unknown or has not been configured`, id))
Expand Down
2 changes: 2 additions & 0 deletions selfservice/strategy/oidc/provider_dingtalk.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ type ProviderDingTalk struct {
reg Dependencies
}

var _ OAuth2Provider = (*ProviderDingTalk)(nil)

func NewProviderDingTalk(
config *Configuration,
reg Dependencies,
Expand Down
2 changes: 2 additions & 0 deletions selfservice/strategy/oidc/provider_discord.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import (
"github.com/ory/x/stringsx"
)

var _ OAuth2Provider = (*ProviderDiscord)(nil)

type ProviderDiscord struct {
config *Configuration
reg Dependencies
Expand Down
2 changes: 2 additions & 0 deletions selfservice/strategy/oidc/provider_facebook.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import (
"github.com/ory/herodot"
)

var _ OAuth2Provider = (*ProviderFacebook)(nil)

type ProviderFacebook struct {
*ProviderGenericOIDC
}
Expand Down
58 changes: 45 additions & 13 deletions selfservice/strategy/oidc/provider_generic_oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,37 @@ package oidc
import (
"context"
"net/url"
"slices"

gooidc "github.com/coreos/go-oidc/v3/oidc"
"github.com/dgraph-io/ristretto"
"github.com/pkg/errors"
"golang.org/x/oauth2"

gooidc "github.com/coreos/go-oidc/v3/oidc"

"github.com/ory/herodot"
"github.com/ory/x/stringslice"
)

var _ Provider = new(ProviderGenericOIDC)
var (
providers *ristretto.Cache[string, *gooidc.Provider]

_ OAuth2Provider = (*ProviderGenericOIDC)(nil)
)

func init() {
maxItems := int64(10_000)
providers, _ = ristretto.NewCache(&ristretto.Config[string, *gooidc.Provider]{
NumCounters: maxItems * 10,
MaxCost: maxItems,
BufferItems: 64,
Metrics: true,
IgnoreInternalCost: true,
Cost: func(*gooidc.Provider) int64 {
return 1
},
})
}

type ProviderGenericOIDC struct {
p *gooidc.Provider
config *Configuration
reg Dependencies
}
Expand Down Expand Up @@ -47,20 +64,27 @@ func (g *ProviderGenericOIDC) withHTTPClientContext(ctx context.Context) context
return gooidc.ClientContext(ctx, g.reg.HTTPClient(ctx).HTTPClient)
}

// provider returns the OpenID Connect Provider. If the provider is already cached, it will be returned from the cache.
// These are the assumptions we're making why we can cache the provider this way:
// - the issuer URL fully identifies the provider
// - the provider's configuration does not change (often)
// - the http.Client we inject through the context is always the same, it is not hot-reloadable
func (g *ProviderGenericOIDC) provider(ctx context.Context) (*gooidc.Provider, error) {
if g.p == nil {
p, err := gooidc.NewProvider(g.withHTTPClientContext(ctx), g.config.IssuerURL)
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to initialize OpenID Connect Provider: %s", err))
}
g.p = p
p, ok := providers.Get(g.config.IssuerURL)
if ok {
return p, nil
}
return g.p, nil
p, err := gooidc.NewProvider(g.withHTTPClientContext(ctx), g.config.IssuerURL)
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to initialize OpenID Connect Provider: %s", err))
}
providers.Set(g.config.IssuerURL, p, 1)
return p, nil
}

func (g *ProviderGenericOIDC) oauth2ConfigFromEndpoint(ctx context.Context, endpoint oauth2.Endpoint) *oauth2.Config {
scope := g.config.Scope
if !stringslice.Has(scope, gooidc.ScopeOpenID) {
if !slices.Contains(scope, gooidc.ScopeOpenID) {
scope = append(scope, gooidc.ScopeOpenID)
}

Expand Down Expand Up @@ -214,3 +238,11 @@ func (g *ProviderGenericOIDC) verifiedIDToken(ctx context.Context, exchange *oau

return token, nil
}

func (g *ProviderGenericOIDC) PKCEEnabled(ctx context.Context) (bool, error) {
p, err := g.provider(ctx)
if err != nil {
return false, err
}
return discoverPKCE(p)
}
2 changes: 2 additions & 0 deletions selfservice/strategy/oidc/provider_github.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
"github.com/ory/herodot"
)

var _ OAuth2Provider = (*ProviderGitHub)(nil)

type ProviderGitHub struct {
config *Configuration
reg Dependencies
Expand Down
2 changes: 2 additions & 0 deletions selfservice/strategy/oidc/provider_gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const (
defaultEndpoint = "https://gitlab.com"
)

var _ OAuth2Provider = (*ProviderGitLab)(nil)

type ProviderGitLab struct {
*ProviderGenericOIDC
}
Expand Down
2 changes: 2 additions & 0 deletions selfservice/strategy/oidc/provider_google.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"github.com/ory/x/stringslice"
)

var _ OAuth2Provider = (*ProviderGoogle)(nil)

type ProviderGoogle struct {
*ProviderGenericOIDC
JWKSUrl string
Expand Down
2 changes: 2 additions & 0 deletions selfservice/strategy/oidc/provider_lark.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (
"github.com/ory/x/httpx"
)

var _ OAuth2Provider = (*ProviderLark)(nil)

type ProviderLark struct {
*ProviderGenericOIDC
}
Expand Down
2 changes: 2 additions & 0 deletions selfservice/strategy/oidc/provider_linkedin.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ const (
IntrospectionURL string = "https://www.linkedin.com/oauth/v2/introspectToken"
)

var _ OAuth2Provider = (*ProviderLinkedIn)(nil)

type ProviderLinkedIn struct {
config *Configuration
reg Dependencies
Expand Down
Loading

0 comments on commit 05f911d

Please sign in to comment.