Repository: coreos/go-oidc
Pull request #365 (2023-02-09 04:10:26)

support provider device_authorization_endpoint
`DeviceAuthURL` was added to `oauth2.Endpoint` in `golang.org/x/oauth2` `v0.13.0`

This PR adds support for parsing the provider `device_authorization_endpoint` from the OIDC discovery endpoint and populates `DeviceAuthURL` for the `oauth2.Endpoint` returned by `Provider.Endpoint()`.

Related issue: https://github.com/coreos/go-oidc/issues/357

ericchiang: I don't see device_authorization_endpoint as a field provided by OpenID Connect discovery https://openid.net/specs/openid-connect-discovery-1_0.html

Is there another spec I should be looking at?
ericchiang: An to be clear, you can retrieve any additional discovery fields through https://pkg.go.dev/github.com/coreos/go-oidc/v3/oidc#Provider.Claims

So users can already access device_authorization_endpoint if a provider returns it today
somersf: > I don't see device_authorization_endpoint as a field provided by OpenID Connect discovery https://openid.net/specs/openid-connect-discovery-1_0.html
> 
> Is there another spec I should be looking at?

[RFC-8628: OAuth 2.0 Device Authorization Grant -(section 4)](https://www.rfc-editor.org/rfc/rfc8628.html#section-4) defines `device_authorization_endpoint`, and says it can optionally be included in [RFC-8414: OAuth 2.0 Authorization Server Metadata]( https://www.rfc-editor.org/rfc/rfc8414).  The latter RFC's introduction says: _This specification generalizes the metadata format defined by "OpenID Connect Discovery 1.0" [OpenID.Discovery] in a way that is compatible with OpenID Connect Discovery while being applicable to a wider set of OAuth 2.0 use cases._

So, whilst `device_authorization_endpoint` might not be _specifically_ mentioned in an _OIDC_ specification, it can be included/found in `.well-known/openid-configuration` - for example see the final paragraph of [RFC-8414: OAuth 2.0 Authorization Server Metadata - Section 3](https://www.rfc-editor.org/rfc/rfc8414#section-3) says: _Some OAuth applications will choose to use the well-known URI suffix "openid-configuration".  As described in Section 5, despite the identifier "/.well-known/openid-configuration", appearing to be OpenID specific, its usage in this specification is actually referring to a general OAuth 2.0 feature that is not specific to OpenID Connect._


I also felt that because go-oidc's `Provider.Endpoint()` populates an `oauth2.Endpoint`, that it should ideally populate _all_ of the fields in the `oauth2.Endpoint`.  If go-oidc had instead defined/populated its own Endpoint type, I wouldn't have felt that way, as it would have been something independent of the oauth2 struct / OIDC  spec-defined only.

Hope that helps.
somersf: > An to be clear, you can retrieve any additional discovery fields through https://pkg.go.dev/github.com/coreos/go-oidc/v3/oidc#Provider.Claims
> 
> So users can already access device_authorization_endpoint if a provider returns it today

Yes, thanks, I was aware that it could be extracted this way.  I've tried to provide some references / perspective as to why I felt it should be filled out by `Endpoint()` (now that it is part of `oauth2.Endpoint`) in my reply to your first comment.
cbodonnell: Thanks @somersf - I see both sides here since it's technically not part of the OIDC spec, but I also see how it fits in given the change to `oauth2.Endpoint`. I'll rebase this PR and address your comments in https://github.com/coreos/go-oidc/pull/365#pullrequestreview-1690831931 this week and we can go from there.
cbodonnell: @somersf the PR has been updated.

@ericchiang Let us know if this is something reasonable for this project based on the thoughts above. If not, I'd suggest we add a small snippet to the linked issue with the recommended way to retrieve this information for reference.
cbodonnell: > lgtm, can you please squash and resolve conflicts? Also do we need to update our go.mod to support newer versions of oauth2 with the added endpoint field?

@ericchiang done! the `go.mod` is already on the latest version `v0.13.0` of `golang.org/x/oauth2` as of https://github.com/coreos/go-oidc/commit/3ebd0c9e4dcfceac8e1b23b1fb5ff8e7b8b61c2a, so we are good there.
cbodonnell: @ericchiang anything else we need to do for this one based on my last comment?

Patch:
diff --git a/oidc/oidc.go b/oidc/oidc.go
index 6e2b0e5..b7db3c7 100644
--- a/oidc/oidc.go
+++ b/oidc/oidc.go
@@ -94,12 +94,13 @@ func doRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
 
 // Provider represents an OpenID Connect server's configuration.
 type Provider struct {
-	issuer      string
-	authURL     string
-	tokenURL    string
-	userInfoURL string
-	jwksURL     string
-	algorithms  []string
+	issuer        string
+	authURL       string
+	tokenURL      string
+	deviceAuthURL string
+	userInfoURL   string
+	jwksURL       string
+	algorithms    []string
 
 	// Raw claims returned by the server.
 	rawClaims []byte
@@ -128,12 +129,13 @@ func (p *Provider) remoteKeySet() KeySet {
 }
 
 type providerJSON struct {
-	Issuer      string   `json:"issuer"`
-	AuthURL     string   `json:"authorization_endpoint"`
-	TokenURL    string   `json:"token_endpoint"`
-	JWKSURL     string   `json:"jwks_uri"`
-	UserInfoURL string   `json:"userinfo_endpoint"`
-	Algorithms  []string `json:"id_token_signing_alg_values_supported"`
+	Issuer        string   `json:"issuer"`
+	AuthURL       string   `json:"authorization_endpoint"`
+	TokenURL      string   `json:"token_endpoint"`
+	DeviceAuthURL string   `json:"device_authorization_endpoint"`
+	JWKSURL       string   `json:"jwks_uri"`
+	UserInfoURL   string   `json:"userinfo_endpoint"`
+	Algorithms    []string `json:"id_token_signing_alg_values_supported"`
 }
 
 // supportedAlgorithms is a list of algorithms explicitly supported by this
@@ -165,6 +167,9 @@ type ProviderConfig struct {
 	// TokenURL is the endpoint used by the provider to support the OAuth 2.0
 	// token endpoint.
 	TokenURL string
+	// DeviceAuthURL is the endpoint used by the provider to support the OAuth 2.0
+	// device authorization endpoint.
+	DeviceAuthURL string
 	// UserInfoURL is the endpoint used by the provider to support the OpenID
 	// Connect UserInfo flow.
 	//
@@ -185,13 +190,14 @@ type ProviderConfig struct {
 // through discovery.
 func (p *ProviderConfig) NewProvider(ctx context.Context) *Provider {
 	return &Provider{
-		issuer:      p.IssuerURL,
-		authURL:     p.AuthURL,
-		tokenURL:    p.TokenURL,
-		userInfoURL: p.UserInfoURL,
-		jwksURL:     p.JWKSURL,
-		algorithms:  p.Algorithms,
-		client:      getClient(ctx),
+		issuer:        p.IssuerURL,
+		authURL:       p.AuthURL,
+		tokenURL:      p.TokenURL,
+		deviceAuthURL: p.DeviceAuthURL,
+		userInfoURL:   p.UserInfoURL,
+		jwksURL:       p.JWKSURL,
+		algorithms:    p.Algorithms,
+		client:        getClient(ctx),
 	}
 }
 
@@ -240,14 +246,15 @@ func NewProvider(ctx context.Context, issuer string) (*Provider, error) {
 		}
 	}
 	return &Provider{
-		issuer:      issuerURL,
-		authURL:     p.AuthURL,
-		tokenURL:    p.TokenURL,
-		userInfoURL: p.UserInfoURL,
-		jwksURL:     p.JWKSURL,
-		algorithms:  algs,
-		rawClaims:   body,
-		client:      getClient(ctx),
+		issuer:        issuerURL,
+		authURL:       p.AuthURL,
+		tokenURL:      p.TokenURL,
+		deviceAuthURL: p.DeviceAuthURL,
+		userInfoURL:   p.UserInfoURL,
+		jwksURL:       p.JWKSURL,
+		algorithms:    algs,
+		rawClaims:     body,
+		client:        getClient(ctx),
 	}, nil
 }
 
@@ -273,7 +280,7 @@ func (p *Provider) Claims(v interface{}) error {
 
 // Endpoint returns the OAuth2 auth and token endpoints for the given provider.
 func (p *Provider) Endpoint() oauth2.Endpoint {
-	return oauth2.Endpoint{AuthURL: p.authURL, TokenURL: p.tokenURL}
+	return oauth2.Endpoint{AuthURL: p.authURL, DeviceAuthURL: p.deviceAuthURL, TokenURL: p.tokenURL}
 }
 
 // UserInfoEndpoint returns the OpenID Connect userinfo endpoint for the given


Test patch:
diff --git a/oidc/oidc_test.go b/oidc/oidc_test.go
index 8a813e3..b19cac6 100644
--- a/oidc/oidc_test.go
+++ b/oidc/oidc_test.go
@@ -116,6 +116,7 @@ func TestNewProvider(t *testing.T) {
 		trailingSlash     bool
 		wantAuthURL       string
 		wantTokenURL      string
+		wantDeviceAuthURL string
 		wantUserInfoURL   string
 		wantIssuerURL     string
 		wantAlgorithms    []string
@@ -205,11 +206,12 @@ func TestNewProvider(t *testing.T) {
 		{
 			// Test case taken directly from:
 			// https://accounts.google.com/.well-known/openid-configuration
-			name:            "google",
-			wantAuthURL:     "https://accounts.google.com/o/oauth2/v2/auth",
-			wantTokenURL:    "https://oauth2.googleapis.com/token",
-			wantUserInfoURL: "https://openidconnect.googleapis.com/v1/userinfo",
-			wantAlgorithms:  []string{"RS256"},
+			name:              "google",
+			wantAuthURL:       "https://accounts.google.com/o/oauth2/v2/auth",
+			wantTokenURL:      "https://oauth2.googleapis.com/token",
+			wantDeviceAuthURL: "https://oauth2.googleapis.com/device/code",
+			wantUserInfoURL:   "https://openidconnect.googleapis.com/v1/userinfo",
+			wantAlgorithms:    []string{"RS256"},
 			data: `{
  "issuer": "ISSUER",
  "authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
@@ -320,6 +322,10 @@ func TestNewProvider(t *testing.T) {
 				t.Errorf("NewProvider() unexpected tokenURL value, got=%s, want=%s",
 					p.tokenURL, test.wantTokenURL)
 			}
+			if p.deviceAuthURL != test.wantDeviceAuthURL {
+				t.Errorf("NewProvider() unexpected deviceAuthURL value, got=%s, want=%s",
+					p.deviceAuthURL, test.wantDeviceAuthURL)
+			}
 			if p.userInfoURL != test.wantUserInfoURL {
 				t.Errorf("NewProvider() unexpected userInfoURL value, got=%s, want=%s",
 					p.userInfoURL, test.wantUserInfoURL)