Repository: authelia/authelia (go)

Issue:
OIDC: redirect_uris for owncloud iOS app
## Feature Request
<!--
1. IMPORTANT: Do not report security vulnerabilities via GitHub issues. Please click 'choose a different type' and see 
the dedicated report a security vulnerability link.
2. Remember to customize the title above.
3. Replace the below sections (not this section) starting with <!- - and ending with - -> with relevant information, 
making sure to remove the <!- - and - ->.
4. Sections that start with _N/A_ are optional, but if you populate them with values please remove the _N/A_.
-->

### Description
ownCloud iOS app expects client `redirect_uris` to be `oc://ios.owncloud.com` according to the [doc](https://doc.owncloud.com/server/next/admin_manual/configuration/user/oidc/oidc.html#redirect-uris).
Authelia accepts only http and https in `redirect_uris`, according to the [doc](https://www.authelia.com/docs/configuration/identity-providers/oidc.html#redirect_uris) .
<!--
Describe the bug here.
-->

### Expected Behaviour
OIDC login from iOS app

<!--
Describe the behaviour your expect here.
-->

### Reproduction Steps
Configure owncloud to use authelia as IDP, add default iOS client as described [here](https://doc.owncloud.com/server/next/admin_manual/configuration/user/oidc/oidc.html#owncloud-desktop-and-mobile-clients) and start login flow from iOS app

<!--
1. Replace this step with your first step. Add additional steps below this.
-->

### Additional Information

Adding the `oc` uri sheme must resolve the issue, here is the full request coming from iOS app:
`GET/api/oidc/authorize?prompt=consent&response_type=code&code_challenge_method=S256&code_challenge=z7d2cS0bP6Sz03hDpeaiiJXMjQ3RgiAVfFEhVgQ4KFg&scope=openid offline_access email profile&redirect_uri=oc://ios.owncloud.com&client_id=mxd5OQDk6es5LzOzRvidJNfXLUZS2oN3oUFeXPP8LpPrhx3UroJFduGEYIBOxkY1HTTP/2.0 "Mozilla/5.0(iPhone;CPUiPhoneOS15_1likeMacOSX)AppleWebKit/605.1.15(KHTML,likeGecko)Version/15.1Mobile/15E148Safari/604.1"`

thank you
<!--
This section is for relevant additional information like; logs, configurations, environment information, etc. 
-->

Pull request:
feat(configuration): support private-use redirect uris in oidc
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

Patch:
diff --git a/internal/configuration/validator/identity_providers.go b/internal/configuration/validator/identity_providers.go
index 7a50a3db1..b119e1643 100644
--- a/internal/configuration/validator/identity_providers.go
+++ b/internal/configuration/validator/identity_providers.go
@@ -190,7 +190,7 @@ func validateOIDCClientRedirectURIs(client schema.OpenIDConnectClientConfigurati
 			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))
 		}
 	}


Test patch:
diff --git a/internal/configuration/validator/identity_providers_test.go b/internal/configuration/validator/identity_providers_test.go
index 11bb63caf..49f9de11f 100644
--- a/internal/configuration/validator/identity_providers_test.go
+++ b/internal/configuration/validator/identity_providers_test.go
@@ -46,36 +46,61 @@ func TestShouldRaiseErrorWhenOIDCServerIssuerPrivateKeyPathInvalid(t *testing.T)
 }
 
 func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
-	validator := schema.NewStructValidator()
-	config := &schema.IdentityProvidersConfiguration{
-		OIDC: &schema.OpenIDConnectConfiguration{
-			HMACSecret:       "rLABDrx87et5KvRHVUgTm3pezWWd8LMN",
-			IssuerPrivateKey: "key-material",
+	testCases := []struct {
+		Name    string
+		Clients []schema.OpenIDConnectClientConfiguration
+		Errors  []error
+	}{
+		{
+			Name: "empty",
 			Clients: []schema.OpenIDConnectClientConfiguration{
 				{
-					ID:     "",
-					Secret: "",
-					Policy: "",
-					RedirectURIs: []string{
-						"tcp://google.com",
-					},
+					ID:           "",
+					Secret:       "",
+					Policy:       "",
+					RedirectURIs: []string{},
 				},
+			},
+			Errors: []error{
+				fmt.Errorf(errFmtOIDCClientInvalidSecret, ""),
+				errors.New(errFmtOIDCClientsWithEmptyID),
+			},
+		},
+		{
+			Name: "client-1",
+			Clients: []schema.OpenIDConnectClientConfiguration{
 				{
-					ID:     "a-client",
+					ID:     "client-1",
 					Secret: "a-secret",
 					Policy: "a-policy",
 					RedirectURIs: []string{
 						"https://google.com",
 					},
 				},
+			},
+			Errors: []error{fmt.Errorf(errFmtOIDCClientInvalidPolicy, "client-1", "a-policy")},
+		},
+		{
+			Name: "client-duplicate",
+			Clients: []schema.OpenIDConnectClientConfiguration{
 				{
-					ID:     "a-client",
-					Secret: "a-secret",
-					Policy: "a-policy",
-					RedirectURIs: []string{
-						"https://google.com",
-					},
+					ID:           "client-x",
+					Secret:       "a-secret",
+					Policy:       policyTwoFactor,
+					RedirectURIs: []string{},
+				},
+				{
+					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",
 					Secret: "a-secret",
@@ -84,6 +109,14 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
 						"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",
 					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)
-
-	assert.Equal(t, schema.DefaultOpenIDConnectClientConfiguration.Policy, config.OIDC.Clients[0].Policy)
-	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)
+			ValidateIdentityProviders(config, validator)
+
+			assert.ElementsMatch(t, validator.Errors(), tc.Errors)
+		})
+	}
 }
 
 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.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"),
+		})
+	})
+}