Skip to content

Commit

Permalink
Support optional acr_values for oidc (hashicorp#275)
Browse files Browse the repository at this point in the history
* Adding support for `acr_values` configuration of oidc endpoint

* Adding support for `acr_values` configuration of each role

Ref:
  https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
  • Loading branch information
matya committed Feb 3, 2024
1 parent cca903f commit 88c9638
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 0 deletions.
7 changes: 7 additions & 0 deletions path_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ func pathConfig(b *jwtAuthBackend) *framework.Path {
Value: true,
},
},
"acr_values": {
Type: framework.TypeCommaStringSlice,
Description: "Authentication Context Class Reference values for all the authentication requests made with this provider. Addition to possible 'acr_values' of a role. Optional.",
},
},

Operations: map[logical.Operation]framework.OperationHandler{
Expand Down Expand Up @@ -204,6 +208,7 @@ func (b *jwtAuthBackend) pathConfigRead(ctx context.Context, req *logical.Reques
"bound_issuer": config.BoundIssuer,
"provider_config": providerConfig,
"namespace_in_state": config.NamespaceInState,
"acr_values": config.ACRValues,
},
}

Expand All @@ -225,6 +230,7 @@ func (b *jwtAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Reque
JWTSupportedAlgs: d.Get("jwt_supported_algs").([]string),
BoundIssuer: d.Get("bound_issuer").(string),
ProviderConfig: d.Get("provider_config").(map[string]interface{}),
ACRValues: d.Get("acr_values").([]string),
}

// Check if the config already exists, to determine if this is a create or
Expand Down Expand Up @@ -418,6 +424,7 @@ type jwtConfig struct {
DefaultRole string `json:"default_role"`
ProviderConfig map[string]interface{} `json:"provider_config"`
NamespaceInState bool `json:"namespace_in_state"`
ACRValues []string `json:"acr_values"`

ParsedJWTPubKeys []crypto.PublicKey `json:"-"`
}
Expand Down
13 changes: 13 additions & 0 deletions path_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func TestConfig_JWT_Read(t *testing.T) {
"bound_issuer": "http://vault.example.com/",
"provider_config": map[string]interface{}{},
"namespace_in_state": false,
"acr_values": []string{},
}

req := &logical.Request{
Expand Down Expand Up @@ -142,6 +143,7 @@ func TestConfig_JWT_Write(t *testing.T) {
BoundIssuer: "http://vault.example.com/",
ProviderConfig: map[string]interface{}{},
NamespaceInState: true,
ACRValues: []string{},
}

conf, err := b.(*jwtAuthBackend).config(context.Background(), storage)
Expand Down Expand Up @@ -179,6 +181,7 @@ func TestConfig_JWKS_Update(t *testing.T) {
"bound_issuer": "",
"provider_config": map[string]interface{}{},
"namespace_in_state": false,
"acr_values": []string{},
}

req := &logical.Request{
Expand Down Expand Up @@ -354,6 +357,7 @@ func TestConfig_OIDC_Write(t *testing.T) {
OIDCClientSecret: "def",
ProviderConfig: map[string]interface{}{},
NamespaceInState: true,
ACRValues: []string{},
}

conf, err := b.(*jwtAuthBackend).config(context.Background(), storage)
Expand Down Expand Up @@ -446,6 +450,7 @@ func TestConfig_OIDC_Write_ProviderConfig(t *testing.T) {
"extraOptions": "abound",
},
NamespaceInState: true,
ACRValues: []string{},
}

conf, err := b.(*jwtAuthBackend).config(context.Background(), storage)
Expand Down Expand Up @@ -503,6 +508,7 @@ func TestConfig_OIDC_Write_ProviderConfig(t *testing.T) {
OIDCDiscoveryURL: "https://team-vault.auth0.com/",
ProviderConfig: map[string]interface{}{},
NamespaceInState: true,
ACRValues: []string{},
}

conf, err := b.(*jwtAuthBackend).config(context.Background(), storage)
Expand Down Expand Up @@ -533,6 +539,7 @@ func TestConfig_OIDC_Create_Namespace(t *testing.T) {
JWTSupportedAlgs: []string{},
JWTValidationPubKeys: []string{},
ProviderConfig: map[string]interface{}{},
ACRValues: []string{},
},
},
"namespace_in_state true": {
Expand All @@ -547,6 +554,7 @@ func TestConfig_OIDC_Create_Namespace(t *testing.T) {
JWTSupportedAlgs: []string{},
JWTValidationPubKeys: []string{},
ProviderConfig: map[string]interface{}{},
ACRValues: []string{},
},
},
"namespace_in_state false": {
Expand All @@ -561,6 +569,7 @@ func TestConfig_OIDC_Create_Namespace(t *testing.T) {
JWTSupportedAlgs: []string{},
JWTValidationPubKeys: []string{},
ProviderConfig: map[string]interface{}{},
ACRValues: []string{},
},
},
}
Expand Down Expand Up @@ -609,6 +618,7 @@ func TestConfig_OIDC_Update_Namespace(t *testing.T) {
JWTSupportedAlgs: []string{},
JWTValidationPubKeys: []string{},
ProviderConfig: map[string]interface{}{},
ACRValues: []string{},
},
},
"existing false, update something else": {
Expand All @@ -628,6 +638,7 @@ func TestConfig_OIDC_Update_Namespace(t *testing.T) {
JWTSupportedAlgs: []string{},
JWTValidationPubKeys: []string{},
ProviderConfig: map[string]interface{}{},
ACRValues: []string{},
},
},
"existing true, update to false": {
Expand All @@ -646,6 +657,7 @@ func TestConfig_OIDC_Update_Namespace(t *testing.T) {
JWTSupportedAlgs: []string{},
JWTValidationPubKeys: []string{},
ProviderConfig: map[string]interface{}{},
ACRValues: []string{},
},
},
"existing true, update something else": {
Expand All @@ -665,6 +677,7 @@ func TestConfig_OIDC_Update_Namespace(t *testing.T) {
JWTSupportedAlgs: []string{},
JWTValidationPubKeys: []string{},
ProviderConfig: map[string]interface{}{},
ACRValues: []string{},
},
},
}
Expand Down
12 changes: 12 additions & 0 deletions path_oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,18 @@ func (b *jwtAuthBackend) createOIDCRequest(config *jwtConfig, role *jwtRole, rol
options = append(options, oidc.WithMaxAge(uint(role.MaxAge.Seconds())))
}

acrValues := []string{}

if len(role.ACRValues) > 0 {
acrValues = append(acrValues, role.ACRValues...)
}
if len(config.ACRValues) > 0 {
acrValues = append(acrValues, config.ACRValues...)
}
if len(acrValues) > 0 {
options = append(options, oidc.WithACRValues(strings.Join(acrValues[:], " ")))
}

request, err := oidc.NewRequest(oidcRequestTimeout, redirectURI, options...)
if err != nil {
return nil, err
Expand Down
122 changes: 122 additions & 0 deletions path_oidc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1638,3 +1638,125 @@ func TestParseMount(t *testing.T) {
t.Fatalf("unexpected result: %s", result)
}
}

// The acr_values parameter refers to authentication context class reference.
func TestOIDC_AuthURL_acr_values(t *testing.T) {
b, storage := getBackend(t)

// Configure the backend without any ACRs, will be added later in the tests
req := &logical.Request{
Operation: logical.UpdateOperation,
Path: configPath,
Storage: storage,
Data: map[string]interface{}{
"oidc_discovery_url": "https://team-vault.auth0.com/",
"oidc_client_id": "abc",
"oidc_client_secret": "def",
},
}
resp, err := b.HandleRequest(context.Background(), req)
require.NoError(t, err)
require.False(t, resp.IsError())

// Configure the role without any ACRs, will be added later in the tests
req = &logical.Request{
Operation: logical.CreateOperation,
Path: "role/test",
Storage: storage,
Data: map[string]interface{}{
"user_claim": "email",
"allowed_redirect_uris": []string{"https://example.com"},
},
}
resp, err = b.HandleRequest(context.Background(), req)
require.NoError(t, err)
require.False(t, resp.IsError())

tests := map[string]struct {
config_acr_values []string
role_acr_values []string
expectedAcrValue string
shouldExist bool
}{
"auth URL with acr_values for role": {
role_acr_values: []string{"role_acr1", "role_acr2"},
config_acr_values: []string{},
expectedAcrValue: "role_acr1 role_acr2",
shouldExist: true,
},
"auth URL with acr_values for config": {
role_acr_values: []string{},
config_acr_values: []string{"config_acr1", "config_acr2"},
expectedAcrValue: "config_acr1 config_acr2",
shouldExist: true,
},
"auth URL with acr_values for both role and config": {
role_acr_values: []string{"role_acr1", "role_acr2"},
config_acr_values: []string{"config_acr1", "config_acr2"},
expectedAcrValue: "role_acr1 role_acr2 config_acr1 config_acr2",
shouldExist: true,
},
"auth URL for empty role acr_values": {
role_acr_values: []string{},
config_acr_values: []string{},
shouldExist: false,
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {

req = &logical.Request{
Operation: logical.UpdateOperation,
Path: configPath,
Storage: storage,
Data: map[string]interface{}{
"oidc_discovery_url": "https://team-vault.auth0.com/",
"oidc_client_id": "abc",
"oidc_client_secret": "def",
"acr_values": tt.config_acr_values,
},
}
resp, err = b.HandleRequest(context.Background(), req)
require.NoError(t, err)
require.False(t, resp.IsError())

req = &logical.Request{
Operation: logical.UpdateOperation,
Path: "role/test",
Storage: storage,
Data: map[string]interface{}{
"user_claim": "email",
"allowed_redirect_uris": []string{"https://example.com"},
"acr_values": tt.role_acr_values,
},
}
resp, err = b.HandleRequest(context.Background(), req)
require.NoError(t, err)
require.False(t, resp.IsError())

// Request for generation of an auth URL
req = &logical.Request{
Operation: logical.UpdateOperation,
Path: "oidc/auth_url",
Storage: storage,
Data: map[string]interface{}{
"role": "test",
"redirect_uri": "https://example.com",
},
}
resp, err = b.HandleRequest(context.Background(), req)
require.NoError(t, err)
require.False(t, resp.IsError())

// Parse the auth URL and assert the expected acr_values query parameter
parsedAuthURL, err := url.Parse(resp.Data["auth_url"].(string))
require.NoError(t, err)
queryParams := parsedAuthURL.Query()
if tt.shouldExist {
assert.Equal(t, tt.expectedAcrValue, queryParams.Get("acr_values"))
} else {
assert.Empty(t, queryParams.Get("acr_values"))
}
})
}
}
10 changes: 10 additions & 0 deletions path_role.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ in OIDC responses.`,
Description: `Specifies the allowable elapsed time in seconds since the last time the
user was actively authenticated.`,
},
"acr_values": {
Type: framework.TypeCommaStringSlice,
Description: `Specifies the Authentication Context Class Reference values for the authentication request made for this role. Addition to possible 'acr_values' of global config. Optional.`,
},
},
ExistenceCheck: b.pathRoleExistenceCheck,
Operations: map[logical.Operation]framework.OperationHandler{
Expand Down Expand Up @@ -225,6 +229,7 @@ type jwtRole struct {
VerboseOIDCLogging bool `json:"verbose_oidc_logging"`
MaxAge time.Duration `json:"max_age"`
UserClaimJSONPointer bool `json:"user_claim_json_pointer"`
ACRValues []string `json:"acr_values"`

// Deprecated by TokenParams
Policies []string `json:"policies"`
Expand Down Expand Up @@ -333,6 +338,7 @@ func (b *jwtAuthBackend) pathRoleRead(ctx context.Context, req *logical.Request,
"oidc_scopes": role.OIDCScopes,
"verbose_oidc_logging": role.VerboseOIDCLogging,
"max_age": int64(role.MaxAge.Seconds()),
"acr_values": role.ACRValues,
}

role.PopulateTokenData(d)
Expand Down Expand Up @@ -470,6 +476,10 @@ func (b *jwtAuthBackend) pathRoleCreateUpdate(ctx context.Context, req *logical.
role.MaxAge = time.Duration(maxAgeRaw.(int)) * time.Second
}

if acrValues, ok := data.GetOk("acr_values"); ok {
role.ACRValues = acrValues.([]string)
}

boundClaimsType := data.Get("bound_claims_type").(string)
switch boundClaimsType {
case boundClaimsTypeString, boundClaimsTypeGlob:
Expand Down
2 changes: 2 additions & 0 deletions path_role_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ func TestPath_Create(t *testing.T) {
BoundCIDRs: []*sockaddr.SockAddrMarshaler{{SockAddr: expectedSockAddr}},
AllowedRedirectURIs: []string(nil),
MaxAge: 60 * time.Second,
ACRValues: []string(nil),
}

req := &logical.Request{
Expand Down Expand Up @@ -792,6 +793,7 @@ func TestPath_Read(t *testing.T) {
"token_no_default_policy": false,
"token_explicit_max_ttl": int64(0),
"max_age": int64(0),
"acr_values": []string(nil),
}

req := &logical.Request{
Expand Down

0 comments on commit 88c9638

Please sign in to comment.