Repository: coreos/go-oidc (go)

Issue:
Auth provider creation does not allow for a URL query string
This is foundamentally another issue with Azure, but different from the ones raised in the past regarding the problem with a different issuer returned by Azure.

It would be useful to be able to use a query string in the issuer URL, this is necessary with Azure B2C when policies are used .

In more details when a Azure policy is used the well known URL needs to be in this format:

https://login.microsoftonline.com//{tenantID}/v2.0/.well-known/openid-configuration?p={policyName}

Due to [how the well known URL is built at the moment](https://github.com/coreos/go-oidc/blob/v3/oidc/oidc.go#L125) it is unfortunately not possible to add a parameter to the URL.

My proposal would be to allow the issuer to contain a query string (EG: https://login.microsoftonline.com/organizations/v2.0/?p={policyName}) or allow a third parameter to the `NewProvider` method. The second  option would break backward compatibility so it might be more problematic.

I can work on this change myself if you think it might OK to include it in this library.


Pull request description:
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

Patch:
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)


Test patch:
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 {