diff --git a/oidc/config.go b/oidc/config.go index 4f24b37..73ca6db 100644 --- a/oidc/config.go +++ b/oidc/config.go @@ -301,6 +301,7 @@ func (c *Config) Validate() error { return fmt.Errorf("%s: missing TokenURL: %w", op, ErrInvalidParameter) case c.ProviderConfig.UserInfoURL == "": return fmt.Errorf("%s: missing UserInfoURL: %w", op, ErrInvalidParameter) + // DeviceAuthURL is optional } } return nil @@ -400,6 +401,7 @@ type ProviderConfig struct { // TokenURL is the provider's OAuth2.0 token endpoint. TokenURL string + // UserInfoURL is the provider's OpenID UserInfo endpoint. // // See: https://openid.net/specs/openid-connect-core-1_0.html#UserInfo @@ -408,6 +410,9 @@ type ProviderConfig struct { // JWKSURL is the provider's OpenID JWKS endpoint (where it publishes the // pub keys. JWKSURL string + + // DeviceAuthURL is the provider's optional device authorization endpoint. + DeviceAuthURL string } // WithProviderConfig provides an optional ProviderConfig which supports diff --git a/oidc/config_test.go b/oidc/config_test.go index 85b576f..ddde003 100644 --- a/oidc/config_test.go +++ b/oidc/config_test.go @@ -77,10 +77,11 @@ func TestNewConfig(t *testing.T) { WithProviderCA(testCaPem), WithNow(testNow), WithProviderConfig(&ProviderConfig{ - AuthURL: "https://auth-endpoint", - JWKSURL: "https://jwks-endpoint", - TokenURL: "https://token-endpoint", - UserInfoURL: "https://userinfo-endpoint", + AuthURL: "https://auth-endpoint", + JWKSURL: "https://jwks-endpoint", + TokenURL: "https://token-endpoint", + UserInfoURL: "https://userinfo-endpoint", + DeviceAuthURL: "https://deviceauth-endpoint", }), }, }, @@ -99,10 +100,11 @@ func TestNewConfig(t *testing.T) { "http://redirect_url_three", }, ProviderConfig: &ProviderConfig{ - AuthURL: "https://auth-endpoint", - JWKSURL: "https://jwks-endpoint", - TokenURL: "https://token-endpoint", - UserInfoURL: "https://userinfo-endpoint", + AuthURL: "https://auth-endpoint", + JWKSURL: "https://jwks-endpoint", + TokenURL: "https://token-endpoint", + UserInfoURL: "https://userinfo-endpoint", + DeviceAuthURL: "https://deviceauth-endpoint", }, }, }, diff --git a/oidc/docs_test.go b/oidc/docs_test.go index 4b84ec5..de204df 100644 --- a/oidc/docs_test.go +++ b/oidc/docs_test.go @@ -107,10 +107,11 @@ func ExampleWithProviderConfig() { []oidc.Alg{oidc.RS256}, []string{"https://your_redirect_url/callback"}, oidc.WithProviderConfig(&oidc.ProviderConfig{ - AuthURL: "https://your_issuer/authorize", - TokenURL: "https://your_issuer/token", - JWKSURL: "https://your_issuer/.well-known/jwks.json", - UserInfoURL: "https://your_issuer/userinfo", + AuthURL: "https://your_issuer/authorize", + TokenURL: "https://your_issuer/token", + DeviceAuthURL: "https://your_issuer/authorize", + JWKSURL: "https://your_issuer/.well-known/jwks.json", + UserInfoURL: "https://your_issuer/userinfo", }), ) if err != nil { @@ -120,7 +121,7 @@ func ExampleWithProviderConfig() { fmt.Println(string(val)) // Output: - // {"ClientID":"your_client_id","ClientSecret":"[REDACTED: client secret]","Scopes":["openid"],"Issuer":"https://your_issuer/","SupportedSigningAlgs":["RS256"],"AllowedRedirectURLs":["https://your_redirect_url/callback"],"Audiences":null,"ProviderCA":"","RoundTripper":null,"ProviderConfig":{"AuthURL":"https://your_issuer/authorize","TokenURL":"https://your_issuer/token","UserInfoURL":"https://your_issuer/userinfo","JWKSURL":"https://your_issuer/.well-known/jwks.json"}} + // {"ClientID":"your_client_id","ClientSecret":"[REDACTED: client secret]","Scopes":["openid"],"Issuer":"https://your_issuer/","SupportedSigningAlgs":["RS256"],"AllowedRedirectURLs":["https://your_redirect_url/callback"],"Audiences":null,"ProviderCA":"","RoundTripper":null,"ProviderConfig":{"AuthURL":"https://your_issuer/authorize","TokenURL":"https://your_issuer/token","UserInfoURL":"https://your_issuer/userinfo","JWKSURL":"https://your_issuer/.well-known/jwks.json","DeviceAuthURL":"https://your_issuer/authorize"}} } func ExampleNewProvider() { diff --git a/oidc/provider.go b/oidc/provider.go index f8b342e..441a80d 100644 --- a/oidc/provider.go +++ b/oidc/provider.go @@ -93,12 +93,13 @@ func NewProvider(c *Config) (*Provider, error) { convertedAlgs = append(convertedAlgs, string(alg)) } cfg := oidc.ProviderConfig{ - IssuerURL: c.Issuer, - AuthURL: c.ProviderConfig.AuthURL, - JWKSURL: c.ProviderConfig.JWKSURL, - TokenURL: c.ProviderConfig.TokenURL, - UserInfoURL: c.ProviderConfig.UserInfoURL, - Algorithms: convertedAlgs, + IssuerURL: c.Issuer, + AuthURL: c.ProviderConfig.AuthURL, + JWKSURL: c.ProviderConfig.JWKSURL, + TokenURL: c.ProviderConfig.TokenURL, + DeviceAuthURL: c.ProviderConfig.DeviceAuthURL, + UserInfoURL: c.ProviderConfig.UserInfoURL, + Algorithms: convertedAlgs, } p.provider = cfg.NewProvider(oidcCtx) @@ -725,6 +726,9 @@ type DiscoveryInfo struct { // TokenURL (REQUIRED): URL of the OP's OAuth 2.0 Token Endpoint TokenURL string `json:"token_endpoint"` + // DeviceAuthURL (OPTIONAL, omitempty): URL of the OP's Device Authorization Endpoint + DeviceAuthURL string `json:"device_authorization_endpoint,omitempty"` + // UserInfoURL (OPTIONAL, omitempty): URL of the OP's UserInfo Endpoint UserInfoURL string `json:"userinfo_endpoint,omitempty"` @@ -826,3 +830,21 @@ func unmarshalRespJSON(r *http.Response, body []byte, v interface{}) error { } return fmt.Errorf("%s: expected Content-Type = application/json, got %q and could not unmarshal it as JSON: %w", op, ct, err) } + +// Config returns the ProviderConfig for this Provider. +// If it was supplied in the first place, just return it, but otherwise +// construct it from what was discovered by and available from go-oidc. +func (p *Provider) Config() ProviderConfig { + providerConfig := p.config.ProviderConfig + if providerConfig == nil { + // Note that JWKSURL is not available through the go-oidc api + endpoint := p.provider.Endpoint() + providerConfig = &ProviderConfig{ + AuthURL: endpoint.AuthURL, + TokenURL: endpoint.TokenURL, + DeviceAuthURL: endpoint.DeviceAuthURL, + UserInfoURL: p.provider.UserInfoEndpoint(), + } + } + return *providerConfig +} diff --git a/oidc/provider_test.go b/oidc/provider_test.go index b07b128..1a58ed0 100644 --- a/oidc/provider_test.go +++ b/oidc/provider_test.go @@ -1585,19 +1585,20 @@ func TestProvider_DiscoveryInfo(t *testing.T) { ctx := context.Background() tests := []struct { - name string - data string - trailingSlashIss bool - overrideHeader string - overrideData string - overrideIssuer string - wantAuthURL string - wantTokenURL string - wantUserInfoURL string - wantAlgorithms []string - wantScopes []string - wantErr bool - wantErrContains string + name string + data string + trailingSlashIss bool + overrideHeader string + overrideData string + overrideIssuer string + wantAuthURL string + wantTokenURL string + wantDeviceAuthURL string + wantUserInfoURL string + wantAlgorithms []string + wantScopes []string + wantErr bool + wantErrContains string }{ { name: "basic_case", @@ -1605,30 +1606,34 @@ func TestProvider_DiscoveryInfo(t *testing.T) { "issuer": "ISSUER", "authorization_endpoint": "https://example.com/auth", "token_endpoint": "https://example.com/token", + "device_authorization_endpoint": "https://example.com/deviceauth", "jwks_uri": "https://example.com/keys", "id_token_signing_alg_values_supported": ["RS256", "RS384"], "scopes_supported": ["openid", "profile"] }`, - wantScopes: []string{"openid", "profile"}, - wantAuthURL: "https://example.com/auth", - wantTokenURL: "https://example.com/token", - wantAlgorithms: []string{"RS256", "RS384"}, + wantScopes: []string{"openid", "profile"}, + wantAuthURL: "https://example.com/auth", + wantTokenURL: "https://example.com/token", + wantDeviceAuthURL: "https://example.com/deviceauth", + wantAlgorithms: []string{"RS256", "RS384"}, }, { - name: "basic_case", + name: "basic_case2", data: `{ "issuer": "ISSUER", "authorization_endpoint": "https://example.com/auth", "token_endpoint": "https://example.com/token", + "device_authorization_endpoint": "https://example.com/deviceauth", "jwks_uri": "https://example.com/keys", "id_token_signing_alg_values_supported": ["RS256", "RS384"], "scopes_supported": ["openid", "profile"] }`, - trailingSlashIss: true, - wantScopes: []string{"openid", "profile"}, - wantAuthURL: "https://example.com/auth", - wantTokenURL: "https://example.com/token", - wantAlgorithms: []string{"RS256", "RS384"}, + trailingSlashIss: true, + wantScopes: []string{"openid", "profile"}, + wantAuthURL: "https://example.com/auth", + wantTokenURL: "https://example.com/token", + wantDeviceAuthURL: "https://example.com/deviceauth", + wantAlgorithms: []string{"RS256", "RS384"}, }, { name: "mismatched_issuer", @@ -1636,6 +1641,7 @@ func TestProvider_DiscoveryInfo(t *testing.T) { "issuer": "ISSUER", "authorization_endpoint": "https://example.com/auth", "token_endpoint": "https://example.com/token", + "device_authorization_endpoint": "https://example.com/deviceauth", "jwks_uri": "https://example.com/keys", "id_token_signing_alg_values_supported": ["RS256"] }`, @@ -1643,6 +1649,7 @@ func TestProvider_DiscoveryInfo(t *testing.T) { "issuer": "https://example.com", "authorization_endpoint": "https://example.com/auth", "token_endpoint": "https://example.com/token", + "device_authorization_endpoint": "https://example.com/deviceauth", "jwks_uri": "https://example.com/keys", "id_token_signing_alg_values_supported": ["RS256"] }`, @@ -1655,6 +1662,7 @@ func TestProvider_DiscoveryInfo(t *testing.T) { "issuer": "ISSUER", "authorization_endpoint": "https://example.com/auth", "token_endpoint": "https://example.com/token", + "device_authorization_endpoint": "https://example.com/deviceauth", "jwks_uri": "https://example.com/keys", "id_token_signing_alg_values_supported": ["RS256"] }`, @@ -1668,6 +1676,7 @@ func TestProvider_DiscoveryInfo(t *testing.T) { "issuer": "ISSUER", "authorization_endpoint": "https://example.com/auth", "token_endpoint": "https://example.com/token", + "device_authorization_endpoint": "https://example.com/deviceauth", "jwks_uri": "https://example.com/keys", "id_token_signing_alg_values_supported": ["RS256"] }`, @@ -1725,9 +1734,23 @@ func TestProvider_DiscoveryInfo(t *testing.T) { require.NoError(err) assert.Equal(info.AuthURL, tt.wantAuthURL) assert.Equal(info.TokenURL, tt.wantTokenURL) + assert.Equal(info.DeviceAuthURL, tt.wantDeviceAuthURL) assert.Equal(info.UserInfoURL, tt.wantUserInfoURL) assert.Equal(info.IdTokenSigningAlgsSupported, tt.wantAlgorithms) assert.Equal(info.ScopesSupported, tt.wantScopes) }) } } + +func TestProvider_Config(t *testing.T) { + t.Parallel() + tp := StartTestProvider(t) + p := testNewProvider(t, "client-id", "client-secret", "redirect", tp) + + c := p.Config() + assert := assert.New(t) + assert.NotEqual(c.AuthURL, "") + assert.NotEqual(c.DeviceAuthURL, "") + assert.NotEqual(c.TokenURL, "") + assert.NotEqual(c.UserInfoURL, "") +} diff --git a/oidc/testing_provider.go b/oidc/testing_provider.go index a8c7e5c..e7072ed 100644 --- a/oidc/testing_provider.go +++ b/oidc/testing_provider.go @@ -1130,6 +1130,7 @@ func (p *TestProvider) ServeHTTP(w http.ResponseWriter, req *http.Request) { const ( openidConfiguration = "/.well-known/openid-configuration" authorize = "/authorize" + deviceAuthorize = "/deviceauthorize" token = "/token" userInfo = "/userinfo" wellKnownJwks = "/.well-known/jwks.json" @@ -1160,6 +1161,7 @@ func (p *TestProvider) ServeHTTP(w http.ResponseWriter, req *http.Request) { reply := struct { Issuer string `json:"issuer"` AuthEndpoint string `json:"authorization_endpoint"` + DeviceAuthEndpoint string `json:"device_authorization_endpoint"` TokenEndpoint string `json:"token_endpoint"` JWKSURI string `json:"jwks_uri"` UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"` @@ -1170,6 +1172,7 @@ func (p *TestProvider) ServeHTTP(w http.ResponseWriter, req *http.Request) { }{ Issuer: p.Addr(), AuthEndpoint: p.Addr() + authorize, + DeviceAuthEndpoint: p.Addr() + deviceAuthorize, TokenEndpoint: p.Addr() + token, JWKSURI: p.Addr() + wellKnownJwks, UserinfoEndpoint: p.Addr() + userInfo,