Skip to content

Commit

Permalink
VAULT-6727 Role resolution for JWT (#208)
Browse files Browse the repository at this point in the history
* VAULT-6727 Role resolution for JWT

* VAULT-6727 Add support for oidc also
  • Loading branch information
VioletHynes authored Jul 21, 2022
1 parent c817ca9 commit feb08af
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 11 deletions.
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ require (
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-hclog v1.0.0
github.com/hashicorp/go-secure-stdlib/base62 v0.1.2
github.com/hashicorp/go-secure-stdlib/strutil v0.1.1
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2
github.com/hashicorp/go-sockaddr v1.0.2
github.com/hashicorp/vault/api v1.3.0
github.com/hashicorp/vault/sdk v0.3.0
github.com/hashicorp/vault/sdk v0.5.3
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect
github.com/mitchellh/mapstructure v1.4.2
github.com/mitchellh/mapstructure v1.5.0
github.com/mitchellh/pointerstructure v1.2.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/ryanuber/go-glob v1.0.0
Expand Down
12 changes: 8 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -248,11 +248,13 @@ github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 h1:ET4pqyjiGmY09R5y+rSd70J2w
github.com/hashicorp/go-secure-stdlib/base62 v0.1.2/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw=
github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 h1:cCRo8gK7oq6A2L6LICkUZ+/a5rLiRXFMf1Qd4xSwxTc=
github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1 h1:78ki3QBevHwYrVxnyVeaEz+7WtifHhauYF23es/0KlI=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8=
github.com/hashicorp/go-secure-stdlib/password v0.1.1/go.mod h1:9hH302QllNwu1o2TGYtSk8I8kTAN0ca1EHpwhm5Mmzo=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.1 h1:nd0HIW15E6FG1MsnArYaHfuw9C2zgzM8LxkG5Ty/788=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.1/go.mod h1:l8slYwnJA26yBz+ErHpp2IRCLr0vuOMGBORIz4rRiAs=
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
Expand All @@ -269,8 +271,9 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/vault/api v1.3.0 h1:uDy39PLSvy6gtKyjOCRPizy2QdFiIYSWBR2pxCEzYL8=
github.com/hashicorp/vault/api v1.3.0/go.mod h1:EabNQLI0VWbWoGlA+oBLC8PXmR9D60aUVgQGvangFWQ=
github.com/hashicorp/vault/sdk v0.3.0 h1:kR3dpxNkhh/wr6ycaJYqp6AFT/i2xaftbfnwZduTKEY=
github.com/hashicorp/vault/sdk v0.3.0/go.mod h1:aZ3fNuL5VNydQk8GcLJ2TV8YCRVvyaakYkhZRoVuhj0=
github.com/hashicorp/vault/sdk v0.5.3 h1:PWY8sq/9pRrK9vUIy75qCH2Jd8oeENAgkaa/qbhzFrs=
github.com/hashicorp/vault/sdk v0.5.3/go.mod h1:DoGraE9kKGNcVgPmTuX357Fm6WAx1Okvde8Vp3dPDoU=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ=
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
Expand Down Expand Up @@ -316,8 +319,9 @@ github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdI
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo=
github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A=
github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
Expand Down
35 changes: 31 additions & 4 deletions path_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,36 +35,63 @@ func pathLogin(b *jwtAuthBackend) *framework.Path {
logical.AliasLookaheadOperation: &framework.PathOperation{
Callback: b.pathLogin,
},
logical.ResolveRoleOperation: &framework.PathOperation{
Callback: b.pathResolveRole,
},
},

HelpSynopsis: pathLoginHelpSyn,
HelpDescription: pathLoginHelpDesc,
}
}

func (b *jwtAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
func (b *jwtAuthBackend) pathResolveRole(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
config, err := b.config(ctx, req.Storage)
if err != nil {
return nil, err
}
if config == nil {
return logical.ErrorResponse("could not load configuration"), nil
}
roleName, _, resp, err := b.getRoleNameAndRoleFromLoginRequest(config, ctx, req, d)
if resp != nil || err != nil {
return resp, err
}
return logical.ResolveRoleResponse(roleName)
}

func (b *jwtAuthBackend) getRoleNameAndRoleFromLoginRequest(config *jwtConfig, ctx context.Context, req *logical.Request, d *framework.FieldData) (string, *jwtRole, *logical.Response, error) {
roleName := d.Get("role").(string)
if roleName == "" {
roleName = config.DefaultRole
}
if roleName == "" {
return logical.ErrorResponse("missing role"), nil
return "", nil, logical.ErrorResponse("missing role"), nil
}

role, err := b.role(ctx, req.Storage, roleName)
if err != nil {
return nil, err
return "", nil, nil, err
}
if role == nil {
return logical.ErrorResponse("role %q could not be found", roleName), nil
return "", nil, logical.ErrorResponse("role %q could not be found", roleName), nil
}

return roleName, role, nil, nil
}

func (b *jwtAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
config, err := b.config(ctx, req.Storage)
if err != nil {
return nil, err
}
if config == nil {
return logical.ErrorResponse("could not load configuration"), nil
}

roleName, role, resp, err := b.getRoleNameAndRoleFromLoginRequest(config, ctx, req, d)
if resp != nil || err != nil {
return resp, err
}

if role.RoleType == "oidc" {
Expand Down
148 changes: 148 additions & 0 deletions path_login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1381,6 +1381,154 @@ func TestLogin_JWKS_Concurrent(t *testing.T) {
}
}

func TestResolveRole(t *testing.T) {
cfg := testConfig{
audience: true,
jwks: true,
defaultLeeway: -1,
}
b, storage := setupBackend(t, cfg)
role := "testrole"

dummyRoleData := map[string]interface{}{
"role_type": "jwt",
"bound_subject": "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients",
"user_claim": "https://vault/user",
"policies": "test",
"period": "3s",
"ttl": "1s",
"num_uses": 12,
"max_ttl": "5s",
}

req := &logical.Request{
Operation: logical.CreateOperation,
Path: "role/" + role,
Storage: storage,
Data: dummyRoleData,
}

resp, err := b.HandleRequest(context.Background(), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}

loginData := map[string]interface{}{
"role": role,
}
loginReq := &logical.Request{
Operation: logical.ResolveRoleOperation,
Path: "login",
Storage: storage,
Data: loginData,
Connection: &logical.Connection{
RemoteAddr: "127.0.0.1",
},
}

resp, err = b.HandleRequest(context.Background(), loginReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}

if resp.Data["role"] != role {
t.Fatalf("Role was not as expected. Expected %s, received %s", role, resp.Data["role"])
}
}

func TestResolveRole_OIDC(t *testing.T) {
cfg := testConfig{
audience: true,
jwks: true,
defaultLeeway: -1,
}
b, storage := setupBackend(t, cfg)
role := "testrole"

dummyRoleData := map[string]interface{}{
"role_type": "oidc",
"bound_subject": "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients",
"user_claim": "https://vault/user",
"policies": "test",
"allowed_redirect_uris": "false",
"period": "3s",
"ttl": "1s",
"num_uses": 12,
"max_ttl": "5s",
}

req := &logical.Request{
Operation: logical.CreateOperation,
Path: "role/" + role,
Storage: storage,
Data: dummyRoleData,
}

resp, err := b.HandleRequest(context.Background(), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}

loginData := map[string]interface{}{
"role": role,
}
loginReq := &logical.Request{
Operation: logical.ResolveRoleOperation,
Path: "login",
Storage: storage,
Data: loginData,
Connection: &logical.Connection{
RemoteAddr: "127.0.0.1",
},
}

resp, err = b.HandleRequest(context.Background(), loginReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}

if resp.Data["role"] != role {
t.Fatalf("Role was not as expected. Expected %s, received %s", role, resp.Data["role"])
}
}

func TestResolveRole_RoleDoesNotExist(t *testing.T) {
cfg := testConfig{
audience: true,
jwks: true,
defaultLeeway: -1,
}
b, storage := setupBackend(t, cfg)
role := "testrole"

loginData := map[string]interface{}{
"role": role,
}
loginReq := &logical.Request{
Operation: logical.ResolveRoleOperation,
Path: "login",
Storage: storage,
Data: loginData,
Connection: &logical.Connection{
RemoteAddr: "127.0.0.1",
},
}

resp, err := b.HandleRequest(context.Background(), loginReq)
if resp == nil && !resp.IsError() {
t.Fatalf("Response was not an error: err:%v resp:%#v", err, resp)
}

errString, ok := resp.Data["error"].(string)
if !ok {
t.Fatal("Error not part of response.")
}

if !strings.Contains(errString, "role \"testrole\" could not be found") {
t.Fatalf("Error was not due to invalid role name. Error: %s", errString)
}
}

const (
ecdsaPrivKey string = `-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIKfldwWLPYsHjRL9EVTsjSbzTtcGRu6icohNfIqcb6A+oAoGCCqGSM49
Expand Down

0 comments on commit feb08af

Please sign in to comment.