Skip to content

Commit 3d02adb

Browse files
committed
Merge branch 'code-flow-to-server' into device-flow-after-server-code-flow
2 parents c733d6c + 77c1703 commit 3d02adb

File tree

4 files changed

+250
-12
lines changed

4 files changed

+250
-12
lines changed

path_login.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,22 @@ func (b *jwtAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d
9292
return logical.ErrorResponse("error configuring token validator: %s", err.Error()), nil
9393
}
9494

95+
// Validate JWT supported algorithms if they've been provided. Otherwise,
96+
// ensure that the signing algorithm is a member of the supported set.
97+
signingAlgorithms := toAlg(config.JWTSupportedAlgs)
98+
if len(signingAlgorithms) == 0 {
99+
signingAlgorithms = []jwt.Alg{
100+
jwt.RS256, jwt.RS384, jwt.RS512, jwt.ES256, jwt.ES384,
101+
jwt.ES512, jwt.PS256, jwt.PS384, jwt.PS512, jwt.EdDSA,
102+
}
103+
}
104+
95105
// Set expected claims values to assert on the JWT
96106
expected := jwt.Expected{
97107
Issuer: config.BoundIssuer,
98108
Subject: role.BoundSubject,
99109
Audiences: role.BoundAudiences,
100-
SigningAlgorithms: toAlg(config.JWTSupportedAlgs),
110+
SigningAlgorithms: signingAlgorithms,
101111
NotBeforeLeeway: role.NotBeforeLeeway,
102112
ExpirationLeeway: role.ExpirationLeeway,
103113
ClockSkewLeeway: role.ClockSkewLeeway,

path_login_test.go

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/go-test/deep"
1818
"github.com/hashicorp/cap/jwt"
1919
"github.com/hashicorp/vault/sdk/logical"
20+
"github.com/stretchr/testify/require"
2021
"golang.org/x/sync/errgroup"
2122
"gopkg.in/square/go-jose.v2"
2223
sqjwt "gopkg.in/square/go-jose.v2/jwt"
@@ -65,7 +66,6 @@ func setupBackend(t *testing.T, cfg testConfig) (closeableBackend, logical.Stora
6566
data = map[string]interface{}{
6667
"bound_issuer": "https://team-vault.auth0.com/",
6768
"jwt_validation_pubkeys": ecdsaPubKey,
68-
"jwt_supported_algs": []string{string(jwt.ES256)},
6969
}
7070
} else {
7171
p := newOIDCProvider(t)
@@ -77,9 +77,8 @@ func setupBackend(t *testing.T, cfg testConfig) (closeableBackend, logical.Stora
7777
}
7878

7979
data = map[string]interface{}{
80-
"jwks_url": p.server.URL + "/certs",
81-
"jwks_ca_pem": cert,
82-
"jwt_supported_algs": []string{string(jwt.ES256)},
80+
"jwks_url": p.server.URL + "/certs",
81+
"jwks_ca_pem": cert,
8382
}
8483
}
8584
}
@@ -955,6 +954,106 @@ func testLogin_NotBeforeClaims(t *testing.T, jwks bool) {
955954
}
956955
}
957956

957+
func TestLogin_JWTSupportedAlgs(t *testing.T) {
958+
tests := []struct {
959+
name string
960+
jwtSupportedAlgs []string
961+
wantErr bool
962+
}{
963+
{
964+
name: "JWT auth with empty signing algorithms",
965+
},
966+
{
967+
name: "JWT auth with valid signing algorithm",
968+
jwtSupportedAlgs: []string{string(jwt.ES256)},
969+
},
970+
{
971+
name: "JWT auth with valid signing algorithms",
972+
jwtSupportedAlgs: []string{string(jwt.RS256), string(jwt.ES256), string(jwt.EdDSA)},
973+
},
974+
{
975+
name: "JWT auth with invalid signing algorithm",
976+
jwtSupportedAlgs: []string{string(jwt.RS256)},
977+
wantErr: true,
978+
},
979+
{
980+
name: "JWT auth with invalid signing algorithms",
981+
jwtSupportedAlgs: []string{string(jwt.RS256), string(jwt.ES512), string(jwt.EdDSA)},
982+
wantErr: true,
983+
},
984+
}
985+
986+
for _, tt := range tests {
987+
t.Run(tt.name, func(t *testing.T) {
988+
b, storage := getBackend(t)
989+
990+
// Configure the backend with an ES256 public key
991+
data := map[string]interface{}{
992+
"jwt_validation_pubkeys": ecdsaPubKey,
993+
"jwt_supported_algs": tt.jwtSupportedAlgs,
994+
}
995+
req := &logical.Request{
996+
Operation: logical.UpdateOperation,
997+
Path: configPath,
998+
Storage: storage,
999+
Data: data,
1000+
}
1001+
resp, err := b.HandleRequest(context.Background(), req)
1002+
require.NoError(t, err)
1003+
require.False(t, resp.IsError())
1004+
1005+
// Configure a JWT role
1006+
data = map[string]interface{}{
1007+
"role_type": "jwt",
1008+
"bound_audiences": []string{"https://vault.plugin.auth.jwt.test"},
1009+
"user_claim": "email",
1010+
}
1011+
req = &logical.Request{
1012+
Operation: logical.CreateOperation,
1013+
Path: "role/plugin-test",
1014+
Storage: storage,
1015+
Data: data,
1016+
}
1017+
resp, err = b.HandleRequest(context.Background(), req)
1018+
require.NoError(t, err)
1019+
require.False(t, resp.IsError())
1020+
1021+
// Sign a JWT with the related ES256 private key
1022+
cl := sqjwt.Claims{
1023+
Subject: "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients",
1024+
Issuer: "https://team-vault.auth0.com/",
1025+
NotBefore: sqjwt.NewNumericDate(time.Now().Add(-5 * time.Second)),
1026+
Audience: sqjwt.Audience{"https://vault.plugin.auth.jwt.test"},
1027+
}
1028+
privateCl := struct {
1029+
Email string `json:"email"`
1030+
}{
1031+
1032+
}
1033+
jwtData, _ := getTestJWT(t, ecdsaPrivKey, cl, privateCl)
1034+
1035+
// Authenticate using the signed JWT
1036+
data = map[string]interface{}{
1037+
"role": "plugin-test",
1038+
"jwt": jwtData,
1039+
}
1040+
req = &logical.Request{
1041+
Operation: logical.UpdateOperation,
1042+
Path: "login",
1043+
Storage: storage,
1044+
Data: data,
1045+
}
1046+
resp, err = b.HandleRequest(context.Background(), req)
1047+
if tt.wantErr {
1048+
require.True(t, resp.IsError())
1049+
return
1050+
}
1051+
require.NoError(t, err)
1052+
require.False(t, resp.IsError())
1053+
})
1054+
}
1055+
}
1056+
9581057
func setupLogin(t *testing.T, iat, exp, nbf time.Time, b logical.Backend, storage logical.Storage) *logical.Request {
9591058
cl := sqjwt.Claims{
9601059
Audience: sqjwt.Audience{"https://vault.plugin.auth.jwt.test"},

path_oidc.go

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -287,12 +287,20 @@ func (b *jwtAuthBackend) pathCallback(ctx context.Context, req *logical.Request,
287287
if oidcReq.idToken == "" {
288288
return loginFailedResponse(useHttp, "No code or id_token received."), nil
289289
}
290-
token, err = oidc.NewToken(oidc.IDToken(oidcReq.idToken), nil)
290+
291+
// Verify the ID token received from the authentication response.
292+
rawToken = oidc.IDToken(oidcReq.idToken)
293+
if _, err := provider.VerifyIDToken(ctx, rawToken, oidcReq); err != nil {
294+
return logical.ErrorResponse("%s %s", errTokenVerification, err.Error()), nil
295+
}
296+
297+
token, err = oidc.NewToken(rawToken, nil)
291298
if err != nil {
292299
return nil, errwrap.Wrapf("error creating oidc token: {{err}}", err)
293300
}
294301
} else {
295-
// ID token verification takes place in exchange
302+
// Exchange the authorization code for an ID token and access token.
303+
// ID token verification takes place in provider.Exchange.
296304
token, err = provider.Exchange(ctx, oidcReq, stateID, code)
297305
if err != nil {
298306
return loginFailedResponse(useHttp, fmt.Sprintf("Error exchanging oidc code: %q.", err.Error())), nil
@@ -344,7 +352,15 @@ func (b *jwtAuthBackend) processToken(ctx context.Context, config *jwtConfig, oi
344352
return loginFailedResponse(useHttp, "sub claim does not match bound subject"), nil
345353
}
346354

347-
// If we have a tokenSource, attempt to fetch information from the /userinfo endpoint
355+
// Set the token source for the access token if it's available. It will only
356+
// be available for the authorization code flow (oidc_response_types=code).
357+
// The access token will be used for fetching additional user and group info.
358+
var tokenSource oauth2.TokenSource
359+
if token != nil {
360+
tokenSource = token.StaticTokenSource()
361+
}
362+
363+
// If we have a token, attempt to fetch information from the /userinfo endpoint
348364
// and merge it with the existing claims data. A failure to fetch additional information
349365
// from this endpoint will not invalidate the authorization flow.
350366
if tokenSource != nil {
@@ -365,15 +381,15 @@ func (b *jwtAuthBackend) processToken(ctx context.Context, config *jwtConfig, oi
365381
}
366382
}
367383

368-
if err := validateBoundClaims(b.Logger(), role.BoundClaimsType, role.BoundClaims, allClaims); err != nil {
369-
return loginFailedResponse(useHttp, fmt.Sprintf("error validating claims: %s", err.Error())), nil
370-
}
371-
372384
alias, groupAliases, err := b.createIdentity(ctx, allClaims, role, tokenSource)
373385
if err != nil {
374386
return loginFailedResponse(useHttp, err.Error()), nil
375387
}
376388

389+
if err := validateBoundClaims(b.Logger(), role.BoundClaimsType, role.BoundClaims, allClaims); err != nil {
390+
return loginFailedResponse(useHttp, fmt.Sprintf("error validating claims: %s", err.Error())), nil
391+
}
392+
377393
tokenMetadata := map[string]string{"role": roleName}
378394
for k, v := range alias.Metadata {
379395
tokenMetadata[k] = v

path_oidc_test.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,119 @@ func TestOIDC_AuthURL_max_age(t *testing.T) {
467467
}
468468
}
469469

470+
// TestOIDC_ResponseTypeIDToken tests authentication using an implicit flow
471+
// by setting oidc_response_types=id_token and oidc_response_mode=form_post.
472+
// This means that there is no exchange of an authorization code for tokens.
473+
// Instead, the OIDC provider's authorization endpoint responds with an ID
474+
// token, which will be verified to complete the authentication request.
475+
func TestOIDC_ResponseTypeIDToken(t *testing.T) {
476+
b, storage := getBackend(t)
477+
478+
// Start the test OIDC provider
479+
s := newOIDCProvider(t)
480+
t.Cleanup(s.server.Close)
481+
s.clientID = "abc"
482+
s.clientSecret = "def"
483+
cert, err := s.getTLSCert()
484+
require.NoError(t, err)
485+
486+
// Configure the backend
487+
data := map[string]interface{}{
488+
"oidc_discovery_url": s.server.URL,
489+
"oidc_client_id": s.clientID,
490+
"oidc_client_secret": s.clientSecret,
491+
"oidc_discovery_ca_pem": cert,
492+
"default_role": "test",
493+
"bound_issuer": "http://vault.example.com/",
494+
"jwt_supported_algs": []string{"ES256"},
495+
"oidc_response_mode": "form_post",
496+
"oidc_response_types": "id_token",
497+
}
498+
req := &logical.Request{
499+
Operation: logical.UpdateOperation,
500+
Path: configPath,
501+
Storage: storage,
502+
Data: data,
503+
}
504+
resp, err := b.HandleRequest(context.Background(), req)
505+
require.NoError(t, err)
506+
require.False(t, resp.IsError())
507+
508+
// Configure a role
509+
data = map[string]interface{}{
510+
"user_claim": "email",
511+
"bound_subject": "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients",
512+
"allowed_redirect_uris": []string{"https://example.com"},
513+
}
514+
req = &logical.Request{
515+
Operation: logical.CreateOperation,
516+
Path: "role/test",
517+
Storage: storage,
518+
Data: data,
519+
}
520+
resp, err = b.HandleRequest(context.Background(), req)
521+
require.NoError(t, err)
522+
require.False(t, resp.IsError())
523+
524+
// Generate an auth URL
525+
data = map[string]interface{}{
526+
"role": "test",
527+
"redirect_uri": "https://example.com",
528+
}
529+
req = &logical.Request{
530+
Operation: logical.UpdateOperation,
531+
Path: "oidc/auth_url",
532+
Storage: storage,
533+
Data: data,
534+
}
535+
resp, err = b.HandleRequest(context.Background(), req)
536+
require.NoError(t, err)
537+
require.False(t, resp.IsError())
538+
539+
// Parse the state and nonce from the auth URL
540+
authURL := resp.Data["auth_url"].(string)
541+
state := getQueryParam(t, authURL, "state")
542+
nonce := getQueryParam(t, authURL, "nonce")
543+
544+
// Create a signed JWT which will act as the ID token that would be
545+
// returned directly from the OIDC provider's authorization endpoint
546+
stdClaims := jwt.Claims{
547+
Subject: "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients",
548+
Issuer: s.server.URL,
549+
NotBefore: jwt.NewNumericDate(time.Now().Add(-5 * time.Second)),
550+
Expiry: jwt.NewNumericDate(time.Now().Add(2 * time.Minute)),
551+
Audience: jwt.Audience{s.clientID},
552+
}
553+
idToken, _ := getTestJWT(t, ecdsaPrivKey, stdClaims, sampleClaims(nonce))
554+
555+
// Invoke the POST callback handler with the ID token and state
556+
req = &logical.Request{
557+
Operation: logical.UpdateOperation,
558+
Path: "oidc/callback",
559+
Storage: storage,
560+
Data: map[string]interface{}{
561+
"id_token": idToken,
562+
"state": state,
563+
},
564+
}
565+
resp, err = b.HandleRequest(context.Background(), req)
566+
require.NoError(t, err)
567+
require.False(t, resp.IsError())
568+
569+
// Complete authentication by invoking the callback handler with the state
570+
req = &logical.Request{
571+
Operation: logical.ReadOperation,
572+
Path: "oidc/callback",
573+
Storage: storage,
574+
Data: map[string]interface{}{
575+
"state": state,
576+
},
577+
}
578+
resp, err = b.HandleRequest(context.Background(), req)
579+
require.NoError(t, err)
580+
require.False(t, resp.IsError())
581+
}
582+
470583
func TestOIDC_Callback(t *testing.T) {
471584
t.Run("successful login", func(t *testing.T) {
472585

0 commit comments

Comments
 (0)