feat(configuration): support private-use redirect uris in oidc (#2796)

Private-use redirect URIs are used to redirect the user to native apps initiating the authentication flow on a device as described in https://datatracker.ietf.org/doc/html/rfc8252#section-7.1

Fix #2742
pull/2697/head^2
Clément Michaud 2022-01-21 12:05:53 +01:00 committed by GitHub
parent 8d5a29117e
commit a7a2bc63fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 105 additions and 30 deletions

View File

@ -190,7 +190,7 @@ func validateOIDCClientRedirectURIs(client schema.OpenIDConnectClientConfigurati
return return
} }
if parsedURL.Scheme != schemeHTTPS && parsedURL.Scheme != schemeHTTP { if !client.Public && parsedURL.Scheme != schemeHTTPS && parsedURL.Scheme != schemeHTTP {
validator.Push(fmt.Errorf(errFmtOIDCClientRedirectURI, client.ID, redirectURI, parsedURL.Scheme)) validator.Push(fmt.Errorf(errFmtOIDCClientRedirectURI, client.ID, redirectURI, parsedURL.Scheme))
} }
} }

View File

@ -46,36 +46,61 @@ func TestShouldRaiseErrorWhenOIDCServerIssuerPrivateKeyPathInvalid(t *testing.T)
} }
func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
validator := schema.NewStructValidator() testCases := []struct {
config := &schema.IdentityProvidersConfiguration{ Name string
OIDC: &schema.OpenIDConnectConfiguration{ Clients []schema.OpenIDConnectClientConfiguration
HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", Errors []error
IssuerPrivateKey: "key-material", }{
{
Name: "empty",
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "", ID: "",
Secret: "", Secret: "",
Policy: "", Policy: "",
RedirectURIs: []string{ RedirectURIs: []string{},
"tcp://google.com",
},
}, },
},
Errors: []error{
fmt.Errorf(errFmtOIDCClientInvalidSecret, ""),
errors.New(errFmtOIDCClientsWithEmptyID),
},
},
{
Name: "client-1",
Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "a-client", ID: "client-1",
Secret: "a-secret", Secret: "a-secret",
Policy: "a-policy", Policy: "a-policy",
RedirectURIs: []string{ RedirectURIs: []string{
"https://google.com", "https://google.com",
}, },
}, },
},
Errors: []error{fmt.Errorf(errFmtOIDCClientInvalidPolicy, "client-1", "a-policy")},
},
{
Name: "client-duplicate",
Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "a-client", ID: "client-x",
Secret: "a-secret", Secret: "a-secret",
Policy: "a-policy", Policy: policyTwoFactor,
RedirectURIs: []string{ RedirectURIs: []string{},
"https://google.com",
},
}, },
{
ID: "client-x",
Secret: "a-secret",
Policy: policyTwoFactor,
RedirectURIs: []string{},
},
},
Errors: []error{errors.New(errFmtOIDCClientsDuplicateID)},
},
{
Name: "client-check-uri-parse",
Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "client-check-uri-parse", ID: "client-check-uri-parse",
Secret: "a-secret", Secret: "a-secret",
@ -84,6 +109,14 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
"http://abc@%two", "http://abc@%two",
}, },
}, },
},
Errors: []error{
fmt.Errorf(errFmtOIDCClientRedirectURICantBeParsed, "client-check-uri-parse", "http://abc@%two", errors.New("parse \"http://abc@%two\": invalid URL escape \"%tw\"")),
},
},
{
Name: "client-check-uri-abs",
Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "client-check-uri-abs", ID: "client-check-uri-abs",
Secret: "a-secret", Secret: "a-secret",
@ -93,22 +126,28 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
}, },
}, },
}, },
Errors: []error{
fmt.Errorf(errFmtOIDCClientRedirectURIAbsolute, "client-check-uri-abs", "google.com"),
},
}, },
} }
ValidateIdentityProviders(config, validator) for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
validator := schema.NewStructValidator()
config := &schema.IdentityProvidersConfiguration{
OIDC: &schema.OpenIDConnectConfiguration{
HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN",
IssuerPrivateKey: "key-material",
Clients: tc.Clients,
},
}
require.Len(t, validator.Errors(), 8) ValidateIdentityProviders(config, validator)
assert.Equal(t, schema.DefaultOpenIDConnectClientConfiguration.Policy, config.OIDC.Clients[0].Policy) assert.ElementsMatch(t, validator.Errors(), tc.Errors)
assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtOIDCClientInvalidSecret, "")) })
assert.EqualError(t, validator.Errors()[1], fmt.Sprintf(errFmtOIDCClientRedirectURI, "", "tcp://google.com", "tcp")) }
assert.EqualError(t, validator.Errors()[2], fmt.Sprintf(errFmtOIDCClientInvalidPolicy, "a-client", "a-policy"))
assert.EqualError(t, validator.Errors()[3], fmt.Sprintf(errFmtOIDCClientInvalidPolicy, "a-client", "a-policy"))
assert.EqualError(t, validator.Errors()[4], fmt.Sprintf(errFmtOIDCClientRedirectURICantBeParsed, "client-check-uri-parse", "http://abc@%two", errors.New("parse \"http://abc@%two\": invalid URL escape \"%tw\"")))
assert.EqualError(t, validator.Errors()[5], fmt.Sprintf(errFmtOIDCClientRedirectURIAbsolute, "client-check-uri-abs", "google.com"))
assert.EqualError(t, validator.Errors()[6], errFmtOIDCClientsWithEmptyID)
assert.EqualError(t, validator.Errors()[7], errFmtOIDCClientsDuplicateID)
} }
func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadScopes(t *testing.T) { func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadScopes(t *testing.T) {
@ -432,3 +471,39 @@ func TestValidateIdentityProvidersShouldSetDefaultValues(t *testing.T) {
assert.Equal(t, time.Hour, config.OIDC.IDTokenLifespan) assert.Equal(t, time.Hour, config.OIDC.IDTokenLifespan)
assert.Equal(t, time.Minute*90, config.OIDC.RefreshTokenLifespan) assert.Equal(t, time.Minute*90, config.OIDC.RefreshTokenLifespan)
} }
// All valid schemes are supported as defined in https://datatracker.ietf.org/doc/html/rfc8252#section-7.1
func TestValidateOIDCClientRedirectURIsSupportingPrivateUseURISchemes(t *testing.T) {
conf := schema.OpenIDConnectClientConfiguration{
ID: "owncloud",
RedirectURIs: []string{
"https://www.mywebsite.com",
"http://www.mywebsite.com",
"oc://ios.owncloud.com",
// example given in the RFC https://datatracker.ietf.org/doc/html/rfc8252#section-7.1
"com.example.app:/oauth2redirect/example-provider",
},
}
t.Run("public", func(t *testing.T) {
validator := schema.NewStructValidator()
conf.Public = true
validateOIDCClientRedirectURIs(conf, validator)
assert.Len(t, validator.Warnings(), 0)
assert.Len(t, validator.Errors(), 0)
})
t.Run("not public", func(t *testing.T) {
validator := schema.NewStructValidator()
conf.Public = false
validateOIDCClientRedirectURIs(conf, validator)
assert.Len(t, validator.Warnings(), 0)
assert.Len(t, validator.Errors(), 2)
assert.ElementsMatch(t, validator.Errors(), []error{
errors.New("openid connect provider: client with ID 'owncloud' redirect URI oc://ios.owncloud.com has an invalid scheme oc, should be http or https"),
errors.New("openid connect provider: client with ID 'owncloud' redirect URI com.example.app:/oauth2redirect/example-provider has an invalid scheme com.example.app, should be http or https"),
})
})
}