Skip to content

Commit

Permalink
oidc: add JSON tags to ProviderConfig
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ericchiang committed Jan 4, 2025
1 parent 0fe9887 commit 4b5f82d
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 10 deletions.
50 changes: 40 additions & 10 deletions oidc/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
Expand Down
80 changes: 80 additions & 0 deletions oidc/oidc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 4b5f82d

Please sign in to comment.