From 4b5f82d880aeabd881766d2d91bee8086d9a3b92 Mon Sep 17 00:00:00 2001 From: Eric Chiang Date: Fri, 3 Jan 2025 20:44:20 -0800 Subject: [PATCH] oidc: add JSON tags to ProviderConfig This PR adds JSON tags to allow parsing a ProviderConfig directly from the OpenID Connect JSON metadata document. Since this is the preferred workaround for providers that don't support discovery in a spec-compliant way, such as returning the wrong issuer, or requiring a URL parameter, make this path easier and add an example to the godoc. Updates #445 Updates #444 Updates #439 Updates #442 Updates #344 Fixes #290 --- oidc/oidc.go | 50 +++++++++++++++++++++++------ oidc/oidc_test.go | 80 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 10 deletions(-) diff --git a/oidc/oidc.go b/oidc/oidc.go index 17419f3..f6a7ea8 100644 --- a/oidc/oidc.go +++ b/oidc/oidc.go @@ -154,40 +154,65 @@ var supportedAlgorithms = map[string]bool{ EdDSA: true, } -// ProviderConfig allows creating providers when discovery isn't supported. It's -// generally easier to use NewProvider directly. +// ProviderConfig allows direct creation of a [Provider] from metadata +// configuration. This is intended for interop with providers that don't support +// discovery, or host the JSON discovery document at an off-spec path. +// +// The ProviderConfig struct specifies JSON struct tags to support document +// parsing. +// +// // Directly fetch the metadata document. +// resp, err := http.Get("https://login.example.com/custom-metadata-path") +// if err != nil { +// // ... +// } +// defer resp.Body.Close() +// +// // Parse config from JSON metadata. +// config := &oidc.ProviderConfig{} +// if err := json.NewDecoder(resp.Body).Decode(config); err != nil { +// // ... +// } +// p := config.NewProvider(context.Background()) +// +// For providers that implement discovery, use [NewProvider] instead. +// +// See: https://openid.net/specs/openid-connect-discovery-1_0.html type ProviderConfig struct { // IssuerURL is the identity of the provider, and the string it uses to sign // ID tokens with. For example "https://accounts.google.com". This value MUST // match ID tokens exactly. - IssuerURL string + IssuerURL string `json:"issuer"` // AuthURL is the endpoint used by the provider to support the OAuth 2.0 // authorization endpoint. - AuthURL string + AuthURL string `json:"authorization_endpoint"` // TokenURL is the endpoint used by the provider to support the OAuth 2.0 // token endpoint. - TokenURL string + TokenURL string `json:"token_endpoint"` // DeviceAuthURL is the endpoint used by the provider to support the OAuth 2.0 // device authorization endpoint. - DeviceAuthURL string + DeviceAuthURL string `json:"device_authorization_endpoint"` // UserInfoURL is the endpoint used by the provider to support the OpenID // Connect UserInfo flow. // // https://openid.net/specs/openid-connect-core-1_0.html#UserInfo - UserInfoURL string + UserInfoURL string `json:"userinfo_endpoint"` // JWKSURL is the endpoint used by the provider to advertise public keys to // verify issued ID tokens. This endpoint is polled as new keys are made // available. - JWKSURL string + JWKSURL string `json:"jwks_uri"` // Algorithms, if provided, indicate a list of JWT algorithms allowed to sign // ID tokens. If not provided, this defaults to the algorithms advertised by // the JWK endpoint, then the set of algorithms supported by this package. - Algorithms []string + Algorithms []string `json:"id_token_signing_alg_values_supported"` } // NewProvider initializes a provider from a set of endpoints, rather than // through discovery. +// +// The provided context is only used for [http.Client] configuration through +// [ClientContext], not cancelation. func (p *ProviderConfig) NewProvider(ctx context.Context) *Provider { return &Provider{ issuer: p.IssuerURL, @@ -202,9 +227,14 @@ func (p *ProviderConfig) NewProvider(ctx context.Context) *Provider { } // NewProvider uses the OpenID Connect discovery mechanism to construct a Provider. -// // The issuer is the URL identifier for the service. For example: "https://accounts.google.com" // or "https://login.salesforce.com". +// +// OpenID Connect providers that don't implement discovery or host the discovery +// document at a non-spec complaint path (such as requiring a URL parameter), +// should use [ProviderConfig] instead. +// +// See: https://openid.net/specs/openid-connect-discovery-1_0.html func NewProvider(ctx context.Context, issuer string) (*Provider, error) { wellKnown := strings.TrimSuffix(issuer, "/") + "/.well-known/openid-configuration" req, err := http.NewRequest("GET", wellKnown, nil) diff --git a/oidc/oidc_test.go b/oidc/oidc_test.go index 66ce458..27415dd 100644 --- a/oidc/oidc_test.go +++ b/oidc/oidc_test.go @@ -344,6 +344,86 @@ func TestNewProvider(t *testing.T) { } } +func TestProviderConfigJSON(t *testing.T) { + // https://accounts.google.com/.well-known/openid-configuration + testCase := ` +{ + "issuer": "https://accounts.google.com", + "authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth", + "device_authorization_endpoint": "https://oauth2.googleapis.com/device/code", + "token_endpoint": "https://oauth2.googleapis.com/token", + "userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo", + "revocation_endpoint": "https://oauth2.googleapis.com/revoke", + "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs", + "response_types_supported": [ + "code", + "token", + "id_token", + "code token", + "code id_token", + "token id_token", + "code token id_token", + "none" + ], + "subject_types_supported": [ + "public" + ], + "id_token_signing_alg_values_supported": [ + "RS256" + ], + "scopes_supported": [ + "openid", + "email", + "profile" + ], + "token_endpoint_auth_methods_supported": [ + "client_secret_post", + "client_secret_basic" + ], + "claims_supported": [ + "aud", + "email", + "email_verified", + "exp", + "family_name", + "given_name", + "iat", + "iss", + "name", + "picture", + "sub" + ], + "code_challenge_methods_supported": [ + "plain", + "S256" + ], + "grant_types_supported": [ + "authorization_code", + "refresh_token", + "urn:ietf:params:oauth:grant-type:device_code", + "urn:ietf:params:oauth:grant-type:jwt-bearer" + ] +} +` + config := &ProviderConfig{} + if err := json.Unmarshal([]byte(testCase), config); err != nil { + t.Fatalf("Parsing provider config: %v", err) + } + + want := &ProviderConfig{ + IssuerURL: "https://accounts.google.com", + AuthURL: "https://accounts.google.com/o/oauth2/v2/auth", + TokenURL: "https://oauth2.googleapis.com/token", + DeviceAuthURL: "https://oauth2.googleapis.com/device/code", + UserInfoURL: "https://openidconnect.googleapis.com/v1/userinfo", + JWKSURL: "https://www.googleapis.com/oauth2/v3/certs", + Algorithms: []string{"RS256"}, + } + if !reflect.DeepEqual(config, want) { + t.Errorf("Parsing provider config returned unexpected result, got=%#v, want=%#v", config, want) + } +} + func TestGetClient(t *testing.T) { ctx := context.Background() if c := getClient(ctx); c != nil {