feat(oidc): hashed client secrets (#4026)

Allow use of hashed OpenID Connect client secrets.
pull/4211/head
James Elliott 2022-10-20 14:21:45 +11:00 committed by GitHub
parent 3aaca0604f
commit 248f1d49d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 218 additions and 97 deletions

View File

@ -12,6 +12,7 @@ weight: 320
toc: true toc: true
aliases: aliases:
- /docs/contributing/commitmsg-guidelines.html - /docs/contributing/commitmsg-guidelines.html
- /contributing/development/guidelines-commit-message/
--- ---
The reasons for these conventions are as follows: The reasons for these conventions are as follows:

View File

@ -118,7 +118,7 @@ identity_providers:
clients: clients:
- id: myapp - id: myapp
description: My Application description: My Application
secret: this_is_a_secret secret: '$plaintext$this_is_a_secret'
sector_identifier: '' sector_identifier: ''
public: false public: false
authorization_policy: two_factor authorization_policy: two_factor
@ -352,7 +352,9 @@ A friendly description for this client shown in the UI. This defaults to the sam
{{< confkey type="string" required="situational" >}} {{< confkey type="string" required="situational" >}}
The shared secret between Authelia and the application consuming this client. This secret must match the secret The shared secret between Authelia and the application consuming this client. This secret must match the secret
configured in the application. Currently this is stored in plain text. configured in the application. This can either be stored in plain text (by prefixing the plain text secret with
`$plaintext$` or can be a hashed password generated with
[authelia crypto hash](../../reference/cli/authelia/authelia_hash-password.md).
This secret must be generated by the administrator and can be done by following the This secret must be generated by the administrator and can be done by following the
[Generating a Random Alphanumeric String](../miscellaneous/guides.md#generating-a-random-alphanumeric-string) guide. [Generating a Random Alphanumeric String](../miscellaneous/guides.md#generating-a-random-alphanumeric-string) guide.

View File

@ -12,6 +12,7 @@ weight: 320
toc: true toc: true
aliases: aliases:
- /docs/contributing/commitmsg-guidelines.html - /docs/contributing/commitmsg-guidelines.html
- /contributing/development/guidelines-commit-message/
--- ---
The reasons for these conventions are as follows: The reasons for these conventions are as follows:

View File

@ -11,7 +11,7 @@ menu:
weight: 320 weight: 320
toc: true toc: true
aliases: aliases:
- /contributing/development/pull-request/ - /contributing/development/guidelines-pull-request/
--- ---
[Pull Request] guidelines are in place in order to maintain consistency and clearly communicate our process for [Pull Request] guidelines are in place in order to maintain consistency and clearly communicate our process for

View File

@ -59,7 +59,7 @@ The following YAML configuration is an example __Authelia__
```yaml ```yaml
- id: guacamole - id: guacamole
description: Apache Guacamole description: Apache Guacamole
secret: guacamole_client_secret secret: '$plaintext$guacamole_client_secret'
public: false public: false
authorization_policy: two_factor authorization_policy: two_factor
redirect_uris: redirect_uris:

View File

@ -62,7 +62,7 @@ which will operate with the above example:
```yaml ```yaml
- id: argocd - id: argocd
description: Argo CD description: Argo CD
secret: argocd_client_secret secret: '$plaintext$argocd_client_secret'
public: false public: false
authorization_policy: two_factor authorization_policy: two_factor
redirect_uris: redirect_uris:

View File

@ -64,7 +64,7 @@ which will operate with the above example:
```yaml ```yaml
- id: bookstack - id: bookstack
description: BookStack description: BookStack
secret: bookstack_client_secret secret: '$plaintext$bookstack_client_secret'
public: false public: false
authorization_policy: two_factor authorization_policy: two_factor
redirect_uris: redirect_uris:

View File

@ -72,7 +72,7 @@ which will operate with the above example:
```yaml ```yaml
- id: cloudflare - id: cloudflare
description: Cloudflare ZeroTrust description: Cloudflare ZeroTrust
secret: cloudflare_client_secret secret: '$plaintext$cloudflare_client_secret'
public: false public: false
authorization_policy: two_factor authorization_policy: two_factor
redirect_uris: redirect_uris:

View File

@ -79,7 +79,7 @@ will operate with the above example:
```yaml ```yaml
- id: gitea - id: gitea
description: Gitea description: Gitea
secret: gitea_client_secret secret: '$plaintext$gitea_client_secret'
public: false public: false
authorization_policy: two_factor authorization_policy: two_factor
redirect_uris: redirect_uris:

View File

@ -75,7 +75,7 @@ which will operate with the above example:
```yaml ```yaml
- id: gitlab - id: gitlab
description: GitLab description: GitLab
secret: gitlab_client_secret secret: '$plaintext$gitlab_client_secret'
public: false public: false
authorization_policy: two_factor authorization_policy: two_factor
redirect_uris: redirect_uris:

View File

@ -93,7 +93,7 @@ which will operate with the above example:
```yaml ```yaml
- id: grafana - id: grafana
description: Grafana description: Grafana
secret: grafana_client_secret secret: '$plaintext$grafana_client_secret'
public: false public: false
authorization_policy: two_factor authorization_policy: two_factor
redirect_uris: redirect_uris:

View File

@ -66,7 +66,7 @@ which will operate with the above example:
```yaml ```yaml
- id: harbor - id: harbor
description: Harbor description: Harbor
secret: harbor_client_secret secret: '$plaintext$harbor_client_secret'
public: false public: false
authorization_policy: two_factor authorization_policy: two_factor
redirect_uris: redirect_uris:

View File

@ -49,7 +49,7 @@ which will operate with the above example:
```yaml ```yaml
- id: vault - id: vault
description: HashiCorp Vault description: HashiCorp Vault
secret: vault_client_secret secret: '$plaintext$vault_client_secret'
public: false public: false
authorization_policy: two_factor authorization_policy: two_factor
redirect_uris: redirect_uris:

View File

@ -71,7 +71,7 @@ which will operate with the above example:
```yaml ```yaml
- id: komga - id: komga
description: Komga description: Komga
secret: komga_client_secret secret: '$plaintext$komga_client_secret'
public: false public: false
authorization_policy: two_factor authorization_policy: two_factor
redirect_uris: redirect_uris:

View File

@ -87,7 +87,7 @@ which will operate with the above example:
```yaml ```yaml
- id: nextcloud - id: nextcloud
description: NextCloud description: NextCloud
secret: nextcloud_client_secret secret: '$plaintext$nextcloud_client_secret'
public: false public: false
authorization_policy: two_factor authorization_policy: two_factor
redirect_uris: redirect_uris:

View File

@ -66,7 +66,7 @@ which will operate with the above example:
```yaml ```yaml
- id: outline - id: outline
description: Outline description: Outline
secret: outline_client_secret secret: '$plaintext$outline_client_secret'
public: false public: false
authorization_policy: two_factor authorization_policy: two_factor
redirect_uris: redirect_uris:

View File

@ -67,7 +67,7 @@ which will operate with the above example:
```yaml ```yaml
- id: portainer - id: portainer
description: Portainer description: Portainer
secret: portainer_client_secret secret: '$plaintext$portainer_client_secret'
public: false public: false
authorization_policy: two_factor authorization_policy: two_factor
redirect_uris: redirect_uris:

View File

@ -69,7 +69,7 @@ which will operate with the above example:
```yaml ```yaml
- id: proxmox - id: proxmox
description: Proxmox description: Proxmox
secret: proxmox_client_secret secret: '$plaintext$proxmox_client_secret'
public: false public: false
authorization_policy: two_factor authorization_policy: two_factor
redirect_uris: redirect_uris:

View File

@ -75,7 +75,7 @@ which will operate with the above example:
```yaml ```yaml
- id: seafile - id: seafile
description: Seafile description: Seafile
secret: seafile_client_secret secret: '$plaintext$seafile_client_secret'
public: false public: false
authorization_policy: two_factor authorization_policy: two_factor
redirect_uris: redirect_uris:

View File

@ -69,7 +69,7 @@ which will operate with the above example:
```yaml ```yaml
- id: synapse - id: synapse
description: Synapse description: Synapse
secret: synapse_client_secret secret: '$plaintext$synapse_client_secret'
public: false public: false
authorization_policy: two_factor authorization_policy: two_factor
redirect_uris: redirect_uris:

View File

@ -2,7 +2,7 @@
title: "Access Control Rule Guide" title: "Access Control Rule Guide"
description: "A reference guide on access control rule operators" description: "A reference guide on access control rule operators"
lead: "This section contains a reference guide on access control rule operators." lead: "This section contains a reference guide on access control rule operators."
date: 2022-09-09T15:44:23+10:00 date: 2022-10-19T14:09:22+11:00
draft: false draft: false
images: [] images: []
menu: menu:

View File

@ -9,8 +9,10 @@ import (
"net/url" "net/url"
"reflect" "reflect"
"regexp" "regexp"
"strings"
"time" "time"
"github.com/go-crypt/crypt"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
@ -412,3 +414,53 @@ func StringToPrivateKeyHookFunc() mapstructure.DecodeHookFuncType {
} }
} }
} }
// StringToPasswordDigestHookFunc decodes a string into a crypt.Digest.
func StringToPasswordDigestHookFunc(plaintext bool) mapstructure.DecodeHookFuncType {
return func(f reflect.Type, t reflect.Type, data interface{}) (value interface{}, err error) {
var ptr bool
if f.Kind() != reflect.String {
return data, nil
}
prefixType := ""
if t.Kind() == reflect.Ptr {
ptr = true
prefixType = "*"
}
expectedType := reflect.TypeOf(schema.PasswordDigest{})
if ptr && t.Elem() != expectedType {
return data, nil
} else if !ptr && t != expectedType {
return data, nil
}
dataStr := data.(string)
var result *schema.PasswordDigest
if !strings.HasPrefix(dataStr, "$") {
dataStr = fmt.Sprintf(crypt.StorageFormatSimple, crypt.AlgorithmPrefixPlainText, dataStr)
}
if dataStr != "" {
if result, err = schema.NewPasswordDigest(dataStr, plaintext); err != nil {
return nil, fmt.Errorf(errFmtDecodeHookCouldNotParse, dataStr, prefixType, expectedType.String(), err)
}
}
if ptr {
return result, nil
}
if result == nil {
return nil, fmt.Errorf(errFmtDecodeHookCouldNotParseEmptyValue, prefixType, expectedType.String(), errDecodeNonPtrMustHaveValue)
}
return *result, nil
}
}

View File

@ -64,6 +64,7 @@ func unmarshal(ko *koanf.Koanf, val *schema.StructValidator, path string, o any)
StringToX509CertificateHookFunc(), StringToX509CertificateHookFunc(),
StringToX509CertificateChainHookFunc(), StringToX509CertificateChainHookFunc(),
StringToPrivateKeyHookFunc(), StringToPrivateKeyHookFunc(),
StringToPasswordDigestHookFunc(true),
ToTimeDurationHookFunc(), ToTimeDurationHookFunc(),
), ),
Metadata: nil, Metadata: nil,

View File

@ -45,7 +45,7 @@ type OpenIDConnectCORSConfiguration struct {
type OpenIDConnectClientConfiguration struct { type OpenIDConnectClientConfiguration struct {
ID string `koanf:"id"` ID string `koanf:"id"`
Description string `koanf:"description"` Description string `koanf:"description"`
Secret string `koanf:"secret"` Secret *PasswordDigest `koanf:"secret"`
SectorIdentifier url.URL `koanf:"sector_identifier"` SectorIdentifier url.URL `koanf:"sector_identifier"`
Public bool `koanf:"public"` Public bool `koanf:"public"`

View File

@ -13,6 +13,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/go-crypt/crypt"
) )
// NewAddressFromString returns an *Address and error depending on the ability to parse the string as an Address. // NewAddressFromString returns an *Address and error depending on the ability to parse the string as an Address.
@ -107,6 +109,29 @@ func (a Address) Listener() (net.Listener, error) {
return net.Listen(a.Scheme, a.HostPort()) return net.Listen(a.Scheme, a.HostPort())
} }
// NewPasswordDigest returns a new PasswordDigest.
func NewPasswordDigest(value string, plaintext bool) (digest *PasswordDigest, err error) {
var d crypt.Digest
switch {
case plaintext:
d, err = crypt.DecodeWithPlainText(value)
default:
d, err = crypt.Decode(value)
}
if err != nil {
return nil, err
}
return &PasswordDigest{d}, err
}
// PasswordDigest is a configuration type for the crypt.Digest.
type PasswordDigest struct {
crypt.Digest
}
// NewX509CertificateChain creates a new *X509CertificateChain from a given string, parsing each PEM block one by one. // NewX509CertificateChain creates a new *X509CertificateChain from a given string, parsing each PEM block one by one.
func NewX509CertificateChain(in string) (chain *X509CertificateChain, err error) { func NewX509CertificateChain(in string) (chain *X509CertificateChain, err error) {
if in == "" { if in == "" {

View File

@ -132,6 +132,6 @@ identity_providers:
- https://example.com - https://example.com
clients: clients:
- id: abc - id: abc
secret: 123 secret: '123'
consent_mode: explicit consent_mode: explicit
... ...

View File

@ -161,11 +161,11 @@ func validateOIDCClients(config *schema.OpenIDConnectConfiguration, validator *s
} }
if client.Public { if client.Public {
if client.Secret != "" { if client.Secret != nil {
validator.Push(fmt.Errorf(errFmtOIDCClientPublicInvalidSecret, client.ID)) validator.Push(fmt.Errorf(errFmtOIDCClientPublicInvalidSecret, client.ID))
} }
} else { } else {
if client.Secret == "" { if client.Secret == nil {
validator.Push(fmt.Errorf(errFmtOIDCClientInvalidSecret, client.ID)) validator.Push(fmt.Errorf(errFmtOIDCClientInvalidSecret, client.ID))
} }
} }

View File

@ -46,7 +46,7 @@ func TestShouldNotRaiseErrorWhenCORSEndpointsValid(t *testing.T) {
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "example", ID: "example",
Secret: "example", Secret: MustDecodeSecret("$plaintext$example"),
}, },
}, },
}, },
@ -69,7 +69,7 @@ func TestShouldRaiseErrorWhenCORSEndpointsNotValid(t *testing.T) {
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "example", ID: "example",
Secret: "example", Secret: MustDecodeSecret("$plaintext$example"),
}, },
}, },
}, },
@ -114,7 +114,7 @@ func TestShouldRaiseErrorWhenOIDCCORSOriginsHasInvalidValues(t *testing.T) {
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "myclient", ID: "myclient",
Secret: "jk12nb3klqwmnelqkwenm", Secret: MustDecodeSecret("$plaintext$jk12nb3klqwmnelqkwenm"),
Policy: "two_factor", Policy: "two_factor",
RedirectURIs: []string{"https://example.com/oauth2_callback", "https://localhost:566/callback", "http://an.example.com/callback", "file://a/file"}, RedirectURIs: []string{"https://example.com/oauth2_callback", "https://localhost:566/callback", "http://an.example.com/callback", "file://a/file"},
}, },
@ -173,7 +173,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "", ID: "",
Secret: "", Secret: nil,
Policy: "", Policy: "",
RedirectURIs: []string{}, RedirectURIs: []string{},
}, },
@ -188,7 +188,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "client-1", ID: "client-1",
Secret: "a-secret", Secret: MustDecodeSecret("$plaintext$a-secret"),
Policy: "a-policy", Policy: "a-policy",
RedirectURIs: []string{ RedirectURIs: []string{
"https://google.com", "https://google.com",
@ -202,13 +202,13 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "client-x", ID: "client-x",
Secret: "a-secret", Secret: MustDecodeSecret("$plaintext$a-secret"),
Policy: policyTwoFactor, Policy: policyTwoFactor,
RedirectURIs: []string{}, RedirectURIs: []string{},
}, },
{ {
ID: "client-x", ID: "client-x",
Secret: "a-secret", Secret: MustDecodeSecret("$plaintext$a-secret"),
Policy: policyTwoFactor, Policy: policyTwoFactor,
RedirectURIs: []string{}, RedirectURIs: []string{},
}, },
@ -220,7 +220,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "client-check-uri-parse", ID: "client-check-uri-parse",
Secret: "a-secret", Secret: MustDecodeSecret("$plaintext$a-secret"),
Policy: policyTwoFactor, Policy: policyTwoFactor,
RedirectURIs: []string{ RedirectURIs: []string{
"http://abc@%two", "http://abc@%two",
@ -236,7 +236,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "client-check-uri-abs", ID: "client-check-uri-abs",
Secret: "a-secret", Secret: MustDecodeSecret("$plaintext$a-secret"),
Policy: policyTwoFactor, Policy: policyTwoFactor,
RedirectURIs: []string{ RedirectURIs: []string{
"google.com", "google.com",
@ -252,7 +252,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "client-valid-sector", ID: "client-valid-sector",
Secret: "a-secret", Secret: MustDecodeSecret("$plaintext$a-secret"),
Policy: policyTwoFactor, Policy: policyTwoFactor,
RedirectURIs: []string{ RedirectURIs: []string{
"https://google.com", "https://google.com",
@ -266,7 +266,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "client-valid-sector", ID: "client-valid-sector",
Secret: "a-secret", Secret: MustDecodeSecret("$plaintext$a-secret"),
Policy: policyTwoFactor, Policy: policyTwoFactor,
RedirectURIs: []string{ RedirectURIs: []string{
"https://google.com", "https://google.com",
@ -280,7 +280,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "client-invalid-sector", ID: "client-invalid-sector",
Secret: "a-secret", Secret: MustDecodeSecret("$plaintext$a-secret"),
Policy: policyTwoFactor, Policy: policyTwoFactor,
RedirectURIs: []string{ RedirectURIs: []string{
"https://google.com", "https://google.com",
@ -302,7 +302,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "client-invalid-sector", ID: "client-invalid-sector",
Secret: "a-secret", Secret: MustDecodeSecret("$plaintext$a-secret"),
Policy: policyTwoFactor, Policy: policyTwoFactor,
RedirectURIs: []string{ RedirectURIs: []string{
"https://google.com", "https://google.com",
@ -350,7 +350,7 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadScopes(t *testing.T) {
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "good_id", ID: "good_id",
Secret: "good_secret", Secret: MustDecodeSecret("$plaintext$good_secret"),
Policy: "two_factor", Policy: "two_factor",
Scopes: []string{"openid", "bad_scope"}, Scopes: []string{"openid", "bad_scope"},
RedirectURIs: []string{ RedirectURIs: []string{
@ -376,7 +376,7 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadGrantTypes(t *testing.T)
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "good_id", ID: "good_id",
Secret: "good_secret", Secret: MustDecodeSecret("$plaintext$good_secret"),
Policy: "two_factor", Policy: "two_factor",
GrantTypes: []string{"bad_grant_type"}, GrantTypes: []string{"bad_grant_type"},
RedirectURIs: []string{ RedirectURIs: []string{
@ -403,7 +403,7 @@ func TestShouldNotErrorOnCertificateValid(t *testing.T) {
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "good_id", ID: "good_id",
Secret: "good_secret", Secret: MustDecodeSecret("$plaintext$good_secret"),
Policy: "two_factor", Policy: "two_factor",
RedirectURIs: []string{ RedirectURIs: []string{
"https://google.com/callback", "https://google.com/callback",
@ -429,7 +429,7 @@ func TestShouldRaiseErrorOnCertificateNotValid(t *testing.T) {
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "good_id", ID: "good_id",
Secret: "good_secret", Secret: MustDecodeSecret("$plaintext$good_secret"),
Policy: "two_factor", Policy: "two_factor",
RedirectURIs: []string{ RedirectURIs: []string{
"https://google.com/callback", "https://google.com/callback",
@ -456,7 +456,7 @@ func TestShouldRaiseErrorOnKeySizeTooSmall(t *testing.T) {
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "good_id", ID: "good_id",
Secret: "good_secret", Secret: MustDecodeSecret("$plaintext$good_secret"),
Policy: "two_factor", Policy: "two_factor",
RedirectURIs: []string{ RedirectURIs: []string{
"https://google.com/callback", "https://google.com/callback",
@ -483,7 +483,7 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadResponseModes(t *testing
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "good_id", ID: "good_id",
Secret: "good_secret", Secret: MustDecodeSecret("$plaintext$good_secret"),
Policy: "two_factor", Policy: "two_factor",
ResponseModes: []string{"bad_responsemode"}, ResponseModes: []string{"bad_responsemode"},
RedirectURIs: []string{ RedirectURIs: []string{
@ -509,7 +509,7 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadUserinfoAlg(t *testing.T
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "good_id", ID: "good_id",
Secret: "good_secret", Secret: MustDecodeSecret("$plaintext$good_secret"),
Policy: "two_factor", Policy: "two_factor",
UserinfoSigningAlgorithm: "rs256", UserinfoSigningAlgorithm: "rs256",
RedirectURIs: []string{ RedirectURIs: []string{
@ -536,7 +536,7 @@ func TestValidateIdentityProvidersShouldRaiseWarningOnSecurityIssue(t *testing.T
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "good_id", ID: "good_id",
Secret: "good_secret", Secret: MustDecodeSecret("$plaintext$good_secret"),
Policy: "two_factor", Policy: "two_factor",
RedirectURIs: []string{ RedirectURIs: []string{
"https://google.com/callback", "https://google.com/callback",
@ -563,7 +563,7 @@ func TestValidateIdentityProvidersShouldRaiseErrorsOnInvalidClientTypes(t *testi
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "client-with-invalid-secret", ID: "client-with-invalid-secret",
Secret: "a-secret", Secret: MustDecodeSecret("$plaintext$a-secret"),
Public: true, Public: true,
Policy: "two_factor", Policy: "two_factor",
RedirectURIs: []string{ RedirectURIs: []string{
@ -572,7 +572,7 @@ func TestValidateIdentityProvidersShouldRaiseErrorsOnInvalidClientTypes(t *testi
}, },
{ {
ID: "client-with-bad-redirect-uri", ID: "client-with-bad-redirect-uri",
Secret: "a-secret", Secret: MustDecodeSecret("$plaintext$a-secret"),
Public: false, Public: false,
Policy: "two_factor", Policy: "two_factor",
RedirectURIs: []string{ RedirectURIs: []string{
@ -642,7 +642,7 @@ func TestValidateIdentityProvidersShouldSetDefaultValues(t *testing.T) {
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "a-client", ID: "a-client",
Secret: "a-client-secret", Secret: MustDecodeSecret("$plaintext$a-client-secret"),
RedirectURIs: []string{ RedirectURIs: []string{
"https://google.com", "https://google.com",
}, },
@ -650,7 +650,7 @@ func TestValidateIdentityProvidersShouldSetDefaultValues(t *testing.T) {
{ {
ID: "b-client", ID: "b-client",
Description: "Normal Description", Description: "Normal Description",
Secret: "b-client-secret", Secret: MustDecodeSecret("$plaintext$b-client-secret"),
Policy: policyOneFactor, Policy: policyOneFactor,
UserinfoSigningAlgorithm: "RS256", UserinfoSigningAlgorithm: "RS256",
RedirectURIs: []string{ RedirectURIs: []string{
@ -775,6 +775,14 @@ func TestValidateOIDCClientRedirectURIsSupportingPrivateUseURISchemes(t *testing
}) })
} }
func MustDecodeSecret(value string) *schema.PasswordDigest {
if secret, err := schema.NewPasswordDigest(value, true); err != nil {
panic(err)
} else {
return secret
}
}
func MustParseRSAPrivateKey(data string) *rsa.PrivateKey { func MustParseRSAPrivateKey(data string) *rsa.PrivateKey {
block, _ := pem.Decode([]byte(data)) block, _ := pem.Decode([]byte(data))
if block == nil || block.Bytes == nil || len(block.Bytes) == 0 { if block == nil || block.Bytes == nil || len(block.Bytes) == 0 {

View File

@ -14,7 +14,7 @@ func NewClient(config schema.OpenIDConnectClientConfiguration) (client *Client)
client = &Client{ client = &Client{
ID: config.ID, ID: config.ID,
Description: config.Description, Description: config.Description,
Secret: []byte(config.Secret), Secret: config.Secret,
SectorIdentifier: config.SectorIdentifier.String(), SectorIdentifier: config.SectorIdentifier.String(),
Public: config.Public, Public: config.Public,
@ -76,7 +76,11 @@ func (c *Client) GetConsentResponseBody(consent *model.OAuth2ConsentSession) Con
// GetHashedSecret returns the Secret. // GetHashedSecret returns the Secret.
func (c *Client) GetHashedSecret() []byte { func (c *Client) GetHashedSecret() []byte {
return c.Secret if c.Secret == nil {
return []byte(nil)
}
return []byte(c.Secret.Encode())
} }
// GetRedirectURIs returns the RedirectURIs. // GetRedirectURIs returns the RedirectURIs.

View File

@ -26,7 +26,7 @@ func TestNewClient(t *testing.T) {
ID: "myapp", ID: "myapp",
Description: "My App", Description: "My App",
Policy: "two_factor", Policy: "two_factor",
Secret: "abcdef", Secret: MustDecodeSecret("$plaintext$abcdef"),
RedirectURIs: []string{"https://google.com/callback"}, RedirectURIs: []string{"https://google.com/callback"},
Scopes: schema.DefaultOpenIDConnectClientConfiguration.Scopes, Scopes: schema.DefaultOpenIDConnectClientConfiguration.Scopes,
ResponseTypes: schema.DefaultOpenIDConnectClientConfiguration.ResponseTypes, ResponseTypes: schema.DefaultOpenIDConnectClientConfiguration.ResponseTypes,
@ -68,7 +68,7 @@ func TestIsAuthenticationLevelSufficient(t *testing.T) {
assert.False(t, c.IsAuthenticationLevelSufficient(authentication.TwoFactor)) assert.False(t, c.IsAuthenticationLevelSufficient(authentication.TwoFactor))
} }
func TestInternalClient_GetConsentResponseBody(t *testing.T) { func TestClient_GetConsentResponseBody(t *testing.T) {
c := Client{} c := Client{}
consentRequestBody := c.GetConsentResponseBody(nil) consentRequestBody := c.GetConsentResponseBody(nil)
@ -95,7 +95,7 @@ func TestInternalClient_GetConsentResponseBody(t *testing.T) {
assert.Equal(t, expectedAudiences, consentRequestBody.Audience) assert.Equal(t, expectedAudiences, consentRequestBody.Audience)
} }
func TestInternalClient_GetAudience(t *testing.T) { func TestClient_GetAudience(t *testing.T) {
c := Client{} c := Client{}
audience := c.GetAudience() audience := c.GetAudience()
@ -108,7 +108,7 @@ func TestInternalClient_GetAudience(t *testing.T) {
assert.Equal(t, "https://example.com", audience[0]) assert.Equal(t, "https://example.com", audience[0])
} }
func TestInternalClient_GetScopes(t *testing.T) { func TestClient_GetScopes(t *testing.T) {
c := Client{} c := Client{}
scopes := c.GetScopes() scopes := c.GetScopes()
@ -121,7 +121,7 @@ func TestInternalClient_GetScopes(t *testing.T) {
assert.Equal(t, "openid", scopes[0]) assert.Equal(t, "openid", scopes[0])
} }
func TestInternalClient_GetGrantTypes(t *testing.T) { func TestClient_GetGrantTypes(t *testing.T) {
c := Client{} c := Client{}
grantTypes := c.GetGrantTypes() grantTypes := c.GetGrantTypes()
@ -135,19 +135,30 @@ func TestInternalClient_GetGrantTypes(t *testing.T) {
assert.Equal(t, "device_code", grantTypes[0]) assert.Equal(t, "device_code", grantTypes[0])
} }
func TestInternalClient_GetHashedSecret(t *testing.T) { func TestClient_Hashing(t *testing.T) {
c := Client{} c := Client{}
hashedSecret := c.GetHashedSecret() hashedSecret := c.GetHashedSecret()
assert.Equal(t, []byte(nil), hashedSecret) assert.Equal(t, []byte(nil), hashedSecret)
c.Secret = []byte("a_bad_secret") c.Secret = MustDecodeSecret("$plaintext$a_bad_secret")
hashedSecret = c.GetHashedSecret() assert.True(t, c.Secret.MatchBytes([]byte("a_bad_secret")))
assert.Equal(t, []byte("a_bad_secret"), hashedSecret)
} }
func TestInternalClient_GetID(t *testing.T) { func TestClient_GetHashedSecret(t *testing.T) {
c := Client{}
hashedSecret := c.GetHashedSecret()
assert.Equal(t, []byte(nil), hashedSecret)
c.Secret = MustDecodeSecret("$plaintext$a_bad_secret")
hashedSecret = c.GetHashedSecret()
assert.Equal(t, []byte("$plaintext$a_bad_secret"), hashedSecret)
}
func TestClient_GetID(t *testing.T) {
c := Client{} c := Client{}
id := c.GetID() id := c.GetID()
@ -159,7 +170,7 @@ func TestInternalClient_GetID(t *testing.T) {
assert.Equal(t, "myid", id) assert.Equal(t, "myid", id)
} }
func TestInternalClient_GetRedirectURIs(t *testing.T) { func TestClient_GetRedirectURIs(t *testing.T) {
c := Client{} c := Client{}
redirectURIs := c.GetRedirectURIs() redirectURIs := c.GetRedirectURIs()
@ -172,7 +183,7 @@ func TestInternalClient_GetRedirectURIs(t *testing.T) {
assert.Equal(t, "https://example.com/oauth2/callback", redirectURIs[0]) assert.Equal(t, "https://example.com/oauth2/callback", redirectURIs[0])
} }
func TestInternalClient_GetResponseModes(t *testing.T) { func TestClient_GetResponseModes(t *testing.T) {
c := Client{} c := Client{}
responseModes := c.GetResponseModes() responseModes := c.GetResponseModes()
@ -191,7 +202,7 @@ func TestInternalClient_GetResponseModes(t *testing.T) {
assert.Equal(t, fosite.ResponseModeFragment, responseModes[3]) assert.Equal(t, fosite.ResponseModeFragment, responseModes[3])
} }
func TestInternalClient_GetResponseTypes(t *testing.T) { func TestClient_GetResponseTypes(t *testing.T) {
c := Client{} c := Client{}
responseTypes := c.GetResponseTypes() responseTypes := c.GetResponseTypes()
@ -206,7 +217,7 @@ func TestInternalClient_GetResponseTypes(t *testing.T) {
assert.Equal(t, "id_token", responseTypes[1]) assert.Equal(t, "id_token", responseTypes[1])
} }
func TestInternalClient_IsPublic(t *testing.T) { func TestClient_IsPublic(t *testing.T) {
c := Client{} c := Client{}
assert.False(t, c.IsPublic()) assert.False(t, c.IsPublic())
@ -214,3 +225,11 @@ func TestInternalClient_IsPublic(t *testing.T) {
c.Public = true c.Public = true
assert.True(t, c.IsPublic()) assert.True(t, c.IsPublic())
} }
func MustDecodeSecret(value string) *schema.PasswordDigest {
if secret, err := schema.NewPasswordDigest(value, true); err != nil {
panic(err)
} else {
return secret
}
}

View File

@ -2,19 +2,26 @@ package oidc
import ( import (
"context" "context"
"crypto/subtle"
"github.com/go-crypt/crypt"
) )
// Compare compares the hash with the data and returns an error if they don't match. // Compare compares the hash with the data and returns an error if they don't match.
func (h PlainTextHasher) Compare(_ context.Context, hash, data []byte) (err error) { func (h AdaptiveHasher) Compare(_ context.Context, hash, data []byte) (err error) {
if subtle.ConstantTimeCompare(hash, data) == 0 { var digest crypt.Digest
return errPasswordsDoNotMatch
if digest, err = crypt.DecodeWithPlainText(string(hash)); err != nil {
return err
} }
if digest.MatchBytes(data) {
return nil return nil
}
return errPasswordsDoNotMatch
} }
// Hash creates a new hash from data. // Hash creates a new hash from data.
func (h PlainTextHasher) Hash(_ context.Context, data []byte) (hash []byte, err error) { func (h AdaptiveHasher) Hash(_ context.Context, data []byte) (hash []byte, err error) {
return data, nil return data, nil
} }

View File

@ -8,9 +8,9 @@ import (
) )
func TestShouldNotRaiseErrorOnEqualPasswordsPlainText(t *testing.T) { func TestShouldNotRaiseErrorOnEqualPasswordsPlainText(t *testing.T) {
hasher := PlainTextHasher{} hasher := AdaptiveHasher{}
a := []byte("abc") a := []byte("$plaintext$abc")
b := []byte("abc") b := []byte("abc")
ctx := context.Background() ctx := context.Background()
@ -21,9 +21,9 @@ func TestShouldNotRaiseErrorOnEqualPasswordsPlainText(t *testing.T) {
} }
func TestShouldRaiseErrorOnNonEqualPasswordsPlainText(t *testing.T) { func TestShouldRaiseErrorOnNonEqualPasswordsPlainText(t *testing.T) {
hasher := PlainTextHasher{} hasher := AdaptiveHasher{}
a := []byte("abc") a := []byte("$plaintext$abc")
b := []byte("abcd") b := []byte("abcd")
ctx := context.Background() ctx := context.Background()
@ -34,7 +34,7 @@ func TestShouldRaiseErrorOnNonEqualPasswordsPlainText(t *testing.T) {
} }
func TestShouldHashPassword(t *testing.T) { func TestShouldHashPassword(t *testing.T) {
hasher := PlainTextHasher{} hasher := AdaptiveHasher{}
data := []byte("abc") data := []byte("abc")

View File

@ -69,7 +69,7 @@ func NewOpenIDConnectProvider(config *schema.OpenIDConnectConfiguration, store s
cconfig, cconfig,
provider.Store, provider.Store,
strategy, strategy,
PlainTextHasher{}, AdaptiveHasher{},
/* /*
These are the OAuth2 and OpenIDConnect factories. Order is important (the OAuth2 factories at the top must These are the OAuth2 and OpenIDConnect factories. Order is important (the OAuth2 factories at the top must

View File

@ -31,7 +31,7 @@ func TestNewOpenIDConnectProvider_ShouldEnableOptionalDiscoveryValues(t *testing
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "a-client", ID: "a-client",
Secret: "a-client-secret", Secret: MustDecodeSecret("$plaintext$a-client-secret"),
SectorIdentifier: url.URL{Host: "google.com"}, SectorIdentifier: url.URL{Host: "google.com"},
Policy: "one_factor", Policy: "one_factor",
RedirectURIs: []string{ RedirectURIs: []string{
@ -62,7 +62,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GoodConfiguration(t *tes
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "a-client", ID: "a-client",
Secret: "a-client-secret", Secret: MustDecodeSecret("$plaintext$a-client-secret"),
Policy: "one_factor", Policy: "one_factor",
RedirectURIs: []string{ RedirectURIs: []string{
"https://google.com", "https://google.com",
@ -71,7 +71,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GoodConfiguration(t *tes
{ {
ID: "b-client", ID: "b-client",
Description: "Normal Description", Description: "Normal Description",
Secret: "b-client-secret", Secret: MustDecodeSecret("$plaintext$b-client-secret"),
Policy: "two_factor", Policy: "two_factor",
RedirectURIs: []string{ RedirectURIs: []string{
"https://google.com", "https://google.com",
@ -102,7 +102,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "a-client", ID: "a-client",
Secret: "a-client-secret", Secret: MustDecodeSecret("$plaintext$a-client-secret"),
Policy: "one_factor", Policy: "one_factor",
RedirectURIs: []string{ RedirectURIs: []string{
"https://google.com", "https://google.com",
@ -192,7 +192,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOAuth2WellKnownConfig
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "a-client", ID: "a-client",
Secret: "a-client-secret", Secret: MustDecodeSecret("$plaintext$a-client-secret"),
Policy: "one_factor", Policy: "one_factor",
RedirectURIs: []string{ RedirectURIs: []string{
"https://google.com", "https://google.com",
@ -271,7 +271,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClientConfiguration{
{ {
ID: "a-client", ID: "a-client",
Secret: "a-client-secret", Secret: MustDecodeSecret("$plaintext$a-client-secret"),
Policy: "one_factor", Policy: "one_factor",
RedirectURIs: []string{ RedirectURIs: []string{
"https://google.com", "https://google.com",

View File

@ -21,14 +21,14 @@ func TestOpenIDConnectStore_GetClientPolicy(t *testing.T) {
Description: "myclient desc", Description: "myclient desc",
Policy: "one_factor", Policy: "one_factor",
Scopes: []string{ScopeOpenID, ScopeProfile}, Scopes: []string{ScopeOpenID, ScopeProfile},
Secret: "mysecret", Secret: MustDecodeSecret("$plaintext$mysecret"),
}, },
{ {
ID: "myotherclient", ID: "myotherclient",
Description: "myclient desc", Description: "myclient desc",
Policy: "two_factor", Policy: "two_factor",
Scopes: []string{ScopeOpenID, ScopeProfile}, Scopes: []string{ScopeOpenID, ScopeProfile},
Secret: "mysecret", Secret: MustDecodeSecret("$plaintext$mysecret"),
}, },
}, },
}, nil) }, nil)
@ -53,7 +53,7 @@ func TestOpenIDConnectStore_GetInternalClient(t *testing.T) {
Description: "myclient desc", Description: "myclient desc",
Policy: "one_factor", Policy: "one_factor",
Scopes: []string{ScopeOpenID, ScopeProfile}, Scopes: []string{ScopeOpenID, ScopeProfile},
Secret: "mysecret", Secret: MustDecodeSecret("$plaintext$mysecret"),
}, },
}, },
}, nil) }, nil)
@ -74,7 +74,7 @@ func TestOpenIDConnectStore_GetInternalClient_ValidClient(t *testing.T) {
Description: "myclient desc", Description: "myclient desc",
Policy: "one_factor", Policy: "one_factor",
Scopes: []string{ScopeOpenID, ScopeProfile}, Scopes: []string{ScopeOpenID, ScopeProfile},
Secret: "mysecret", Secret: MustDecodeSecret("$plaintext$mysecret"),
} }
s := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{ s := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{
@ -93,7 +93,7 @@ func TestOpenIDConnectStore_GetInternalClient_ValidClient(t *testing.T) {
assert.Equal(t, client.ResponseTypes, c1.ResponseTypes) assert.Equal(t, client.ResponseTypes, c1.ResponseTypes)
assert.Equal(t, client.RedirectURIs, c1.RedirectURIs) assert.Equal(t, client.RedirectURIs, c1.RedirectURIs)
assert.Equal(t, client.Policy, authorization.OneFactor) assert.Equal(t, client.Policy, authorization.OneFactor)
assert.Equal(t, client.Secret, []byte(c1.Secret)) assert.Equal(t, client.Secret.Encode(), "$plaintext$mysecret")
} }
func TestOpenIDConnectStore_GetInternalClient_InvalidClient(t *testing.T) { func TestOpenIDConnectStore_GetInternalClient_InvalidClient(t *testing.T) {
@ -102,7 +102,7 @@ func TestOpenIDConnectStore_GetInternalClient_InvalidClient(t *testing.T) {
Description: "myclient desc", Description: "myclient desc",
Policy: "one_factor", Policy: "one_factor",
Scopes: []string{ScopeOpenID, ScopeProfile}, Scopes: []string{ScopeOpenID, ScopeProfile},
Secret: "mysecret", Secret: MustDecodeSecret("$plaintext$mysecret"),
} }
s := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{ s := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{
@ -126,7 +126,7 @@ func TestOpenIDConnectStore_IsValidClientID(t *testing.T) {
Description: "myclient desc", Description: "myclient desc",
Policy: "one_factor", Policy: "one_factor",
Scopes: []string{ScopeOpenID, ScopeProfile}, Scopes: []string{ScopeOpenID, ScopeProfile},
Secret: "mysecret", Secret: MustDecodeSecret("$plaintext$mysecret"),
}, },
}, },
}, nil) }, nil)

View File

@ -4,6 +4,7 @@ import (
"net/url" "net/url"
"time" "time"
"github.com/go-crypt/crypt"
"github.com/ory/fosite" "github.com/ory/fosite"
"github.com/ory/fosite/handler/openid" "github.com/ory/fosite/handler/openid"
"github.com/ory/fosite/token/jwt" "github.com/ory/fosite/token/jwt"
@ -101,7 +102,7 @@ type Store struct {
type Client struct { type Client struct {
ID string ID string
Description string Description string
Secret []byte Secret crypt.Digest
SectorIdentifier string SectorIdentifier string
Public bool Public bool
@ -182,8 +183,8 @@ type KeyManager struct {
jwks *jose.JSONWebKeySet jwks *jose.JSONWebKeySet
} }
// PlainTextHasher implements the fosite.Hasher interface without an actual hashing algo. // AdaptiveHasher implements the fosite.Hasher interface without an actual hashing algo.
type PlainTextHasher struct{} type AdaptiveHasher struct{}
// ConsentGetResponseBody schema of the response body of the consent GET endpoint. // ConsentGetResponseBody schema of the response body of the consent GET endpoint.
type ConsentGetResponseBody struct { type ConsentGetResponseBody struct {