Skip to content

Commit 88c9638

Browse files
committed
Support optional acr_values for oidc (#275)
* 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
1 parent cca903f commit 88c9638

File tree

6 files changed

+166
-0
lines changed

6 files changed

+166
-0
lines changed

path_config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ func pathConfig(b *jwtAuthBackend) *framework.Path {
103103
Value: true,
104104
},
105105
},
106+
"acr_values": {
107+
Type: framework.TypeCommaStringSlice,
108+
Description: "Authentication Context Class Reference values for all the authentication requests made with this provider. Addition to possible 'acr_values' of a role. Optional.",
109+
},
106110
},
107111

108112
Operations: map[logical.Operation]framework.OperationHandler{
@@ -204,6 +208,7 @@ func (b *jwtAuthBackend) pathConfigRead(ctx context.Context, req *logical.Reques
204208
"bound_issuer": config.BoundIssuer,
205209
"provider_config": providerConfig,
206210
"namespace_in_state": config.NamespaceInState,
211+
"acr_values": config.ACRValues,
207212
},
208213
}
209214

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

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

422429
ParsedJWTPubKeys []crypto.PublicKey `json:"-"`
423430
}

path_config_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ func TestConfig_JWT_Read(t *testing.T) {
3333
"bound_issuer": "http://vault.example.com/",
3434
"provider_config": map[string]interface{}{},
3535
"namespace_in_state": false,
36+
"acr_values": []string{},
3637
}
3738

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

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

184187
req := &logical.Request{
@@ -354,6 +357,7 @@ func TestConfig_OIDC_Write(t *testing.T) {
354357
OIDCClientSecret: "def",
355358
ProviderConfig: map[string]interface{}{},
356359
NamespaceInState: true,
360+
ACRValues: []string{},
357361
}
358362

359363
conf, err := b.(*jwtAuthBackend).config(context.Background(), storage)
@@ -446,6 +450,7 @@ func TestConfig_OIDC_Write_ProviderConfig(t *testing.T) {
446450
"extraOptions": "abound",
447451
},
448452
NamespaceInState: true,
453+
ACRValues: []string{},
449454
}
450455

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

508514
conf, err := b.(*jwtAuthBackend).config(context.Background(), storage)
@@ -533,6 +539,7 @@ func TestConfig_OIDC_Create_Namespace(t *testing.T) {
533539
JWTSupportedAlgs: []string{},
534540
JWTValidationPubKeys: []string{},
535541
ProviderConfig: map[string]interface{}{},
542+
ACRValues: []string{},
536543
},
537544
},
538545
"namespace_in_state true": {
@@ -547,6 +554,7 @@ func TestConfig_OIDC_Create_Namespace(t *testing.T) {
547554
JWTSupportedAlgs: []string{},
548555
JWTValidationPubKeys: []string{},
549556
ProviderConfig: map[string]interface{}{},
557+
ACRValues: []string{},
550558
},
551559
},
552560
"namespace_in_state false": {
@@ -561,6 +569,7 @@ func TestConfig_OIDC_Create_Namespace(t *testing.T) {
561569
JWTSupportedAlgs: []string{},
562570
JWTValidationPubKeys: []string{},
563571
ProviderConfig: map[string]interface{}{},
572+
ACRValues: []string{},
564573
},
565574
},
566575
}
@@ -609,6 +618,7 @@ func TestConfig_OIDC_Update_Namespace(t *testing.T) {
609618
JWTSupportedAlgs: []string{},
610619
JWTValidationPubKeys: []string{},
611620
ProviderConfig: map[string]interface{}{},
621+
ACRValues: []string{},
612622
},
613623
},
614624
"existing false, update something else": {
@@ -628,6 +638,7 @@ func TestConfig_OIDC_Update_Namespace(t *testing.T) {
628638
JWTSupportedAlgs: []string{},
629639
JWTValidationPubKeys: []string{},
630640
ProviderConfig: map[string]interface{}{},
641+
ACRValues: []string{},
631642
},
632643
},
633644
"existing true, update to false": {
@@ -646,6 +657,7 @@ func TestConfig_OIDC_Update_Namespace(t *testing.T) {
646657
JWTSupportedAlgs: []string{},
647658
JWTValidationPubKeys: []string{},
648659
ProviderConfig: map[string]interface{}{},
660+
ACRValues: []string{},
649661
},
650662
},
651663
"existing true, update something else": {
@@ -665,6 +677,7 @@ func TestConfig_OIDC_Update_Namespace(t *testing.T) {
665677
JWTSupportedAlgs: []string{},
666678
JWTValidationPubKeys: []string{},
667679
ProviderConfig: map[string]interface{}{},
680+
ACRValues: []string{},
668681
},
669682
},
670683
}

path_oidc.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,18 @@ func (b *jwtAuthBackend) createOIDCRequest(config *jwtConfig, role *jwtRole, rol
493493
options = append(options, oidc.WithMaxAge(uint(role.MaxAge.Seconds())))
494494
}
495495

496+
acrValues := []string{}
497+
498+
if len(role.ACRValues) > 0 {
499+
acrValues = append(acrValues, role.ACRValues...)
500+
}
501+
if len(config.ACRValues) > 0 {
502+
acrValues = append(acrValues, config.ACRValues...)
503+
}
504+
if len(acrValues) > 0 {
505+
options = append(options, oidc.WithACRValues(strings.Join(acrValues[:], " ")))
506+
}
507+
496508
request, err := oidc.NewRequest(oidcRequestTimeout, redirectURI, options...)
497509
if err != nil {
498510
return nil, err

path_oidc_test.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1638,3 +1638,125 @@ func TestParseMount(t *testing.T) {
16381638
t.Fatalf("unexpected result: %s", result)
16391639
}
16401640
}
1641+
1642+
// The acr_values parameter refers to authentication context class reference.
1643+
func TestOIDC_AuthURL_acr_values(t *testing.T) {
1644+
b, storage := getBackend(t)
1645+
1646+
// Configure the backend without any ACRs, will be added later in the tests
1647+
req := &logical.Request{
1648+
Operation: logical.UpdateOperation,
1649+
Path: configPath,
1650+
Storage: storage,
1651+
Data: map[string]interface{}{
1652+
"oidc_discovery_url": "https://team-vault.auth0.com/",
1653+
"oidc_client_id": "abc",
1654+
"oidc_client_secret": "def",
1655+
},
1656+
}
1657+
resp, err := b.HandleRequest(context.Background(), req)
1658+
require.NoError(t, err)
1659+
require.False(t, resp.IsError())
1660+
1661+
// Configure the role without any ACRs, will be added later in the tests
1662+
req = &logical.Request{
1663+
Operation: logical.CreateOperation,
1664+
Path: "role/test",
1665+
Storage: storage,
1666+
Data: map[string]interface{}{
1667+
"user_claim": "email",
1668+
"allowed_redirect_uris": []string{"https://example.com"},
1669+
},
1670+
}
1671+
resp, err = b.HandleRequest(context.Background(), req)
1672+
require.NoError(t, err)
1673+
require.False(t, resp.IsError())
1674+
1675+
tests := map[string]struct {
1676+
config_acr_values []string
1677+
role_acr_values []string
1678+
expectedAcrValue string
1679+
shouldExist bool
1680+
}{
1681+
"auth URL with acr_values for role": {
1682+
role_acr_values: []string{"role_acr1", "role_acr2"},
1683+
config_acr_values: []string{},
1684+
expectedAcrValue: "role_acr1 role_acr2",
1685+
shouldExist: true,
1686+
},
1687+
"auth URL with acr_values for config": {
1688+
role_acr_values: []string{},
1689+
config_acr_values: []string{"config_acr1", "config_acr2"},
1690+
expectedAcrValue: "config_acr1 config_acr2",
1691+
shouldExist: true,
1692+
},
1693+
"auth URL with acr_values for both role and config": {
1694+
role_acr_values: []string{"role_acr1", "role_acr2"},
1695+
config_acr_values: []string{"config_acr1", "config_acr2"},
1696+
expectedAcrValue: "role_acr1 role_acr2 config_acr1 config_acr2",
1697+
shouldExist: true,
1698+
},
1699+
"auth URL for empty role acr_values": {
1700+
role_acr_values: []string{},
1701+
config_acr_values: []string{},
1702+
shouldExist: false,
1703+
},
1704+
}
1705+
for name, tt := range tests {
1706+
t.Run(name, func(t *testing.T) {
1707+
1708+
req = &logical.Request{
1709+
Operation: logical.UpdateOperation,
1710+
Path: configPath,
1711+
Storage: storage,
1712+
Data: map[string]interface{}{
1713+
"oidc_discovery_url": "https://team-vault.auth0.com/",
1714+
"oidc_client_id": "abc",
1715+
"oidc_client_secret": "def",
1716+
"acr_values": tt.config_acr_values,
1717+
},
1718+
}
1719+
resp, err = b.HandleRequest(context.Background(), req)
1720+
require.NoError(t, err)
1721+
require.False(t, resp.IsError())
1722+
1723+
req = &logical.Request{
1724+
Operation: logical.UpdateOperation,
1725+
Path: "role/test",
1726+
Storage: storage,
1727+
Data: map[string]interface{}{
1728+
"user_claim": "email",
1729+
"allowed_redirect_uris": []string{"https://example.com"},
1730+
"acr_values": tt.role_acr_values,
1731+
},
1732+
}
1733+
resp, err = b.HandleRequest(context.Background(), req)
1734+
require.NoError(t, err)
1735+
require.False(t, resp.IsError())
1736+
1737+
// Request for generation of an auth URL
1738+
req = &logical.Request{
1739+
Operation: logical.UpdateOperation,
1740+
Path: "oidc/auth_url",
1741+
Storage: storage,
1742+
Data: map[string]interface{}{
1743+
"role": "test",
1744+
"redirect_uri": "https://example.com",
1745+
},
1746+
}
1747+
resp, err = b.HandleRequest(context.Background(), req)
1748+
require.NoError(t, err)
1749+
require.False(t, resp.IsError())
1750+
1751+
// Parse the auth URL and assert the expected acr_values query parameter
1752+
parsedAuthURL, err := url.Parse(resp.Data["auth_url"].(string))
1753+
require.NoError(t, err)
1754+
queryParams := parsedAuthURL.Query()
1755+
if tt.shouldExist {
1756+
assert.Equal(t, tt.expectedAcrValue, queryParams.Get("acr_values"))
1757+
} else {
1758+
assert.Empty(t, queryParams.Get("acr_values"))
1759+
}
1760+
})
1761+
}
1762+
}

path_role.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,10 @@ in OIDC responses.`,
165165
Description: `Specifies the allowable elapsed time in seconds since the last time the
166166
user was actively authenticated.`,
167167
},
168+
"acr_values": {
169+
Type: framework.TypeCommaStringSlice,
170+
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.`,
171+
},
168172
},
169173
ExistenceCheck: b.pathRoleExistenceCheck,
170174
Operations: map[logical.Operation]framework.OperationHandler{
@@ -225,6 +229,7 @@ type jwtRole struct {
225229
VerboseOIDCLogging bool `json:"verbose_oidc_logging"`
226230
MaxAge time.Duration `json:"max_age"`
227231
UserClaimJSONPointer bool `json:"user_claim_json_pointer"`
232+
ACRValues []string `json:"acr_values"`
228233

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

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

479+
if acrValues, ok := data.GetOk("acr_values"); ok {
480+
role.ACRValues = acrValues.([]string)
481+
}
482+
473483
boundClaimsType := data.Get("bound_claims_type").(string)
474484
switch boundClaimsType {
475485
case boundClaimsTypeString, boundClaimsTypeGlob:

path_role_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ func TestPath_Create(t *testing.T) {
9292
BoundCIDRs: []*sockaddr.SockAddrMarshaler{{SockAddr: expectedSockAddr}},
9393
AllowedRedirectURIs: []string(nil),
9494
MaxAge: 60 * time.Second,
95+
ACRValues: []string(nil),
9596
}
9697

9798
req := &logical.Request{
@@ -792,6 +793,7 @@ func TestPath_Read(t *testing.T) {
792793
"token_no_default_policy": false,
793794
"token_explicit_max_ttl": int64(0),
794795
"max_age": int64(0),
796+
"acr_values": []string(nil),
795797
}
796798

797799
req := &logical.Request{

0 commit comments

Comments
 (0)