Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

identity/oidc: Adds default provider, key, and allow_all assignment #14119

Merged
merged 8 commits into from
Feb 22, 2022
9 changes: 9 additions & 0 deletions changelog/14119.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
```release-note:improvement
identity/oidc: Adds a default OIDC provider
```
```release-note:improvement
identity/oidc: Adds a default key for OIDC clients
```
```release-note:improvement
identity/oidc: Adds an `allow_all` assignment that permits all entities to authenticate via an OIDC client
```
222 changes: 222 additions & 0 deletions vault/external_tests/identity/oidc_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,228 @@ const (
`
)

// TestOIDC_Auth_Code_Flow_Default_Resources tests the authorization
// code flow using the default OIDC provider, default key, and allow_all
// assignment. This ensures that the resources are created and usable with
// an initial setup of Vault.
func TestOIDC_Auth_Code_Flow_Default_Resources(t *testing.T) {
cluster := setupOIDCTestCluster(t, 2)
defer cluster.Cleanup()
active := cluster.Cores[0].Client
standby := cluster.Cores[1].Client

// Enable userpass auth and create a user
err := active.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{
Type: "userpass",
})
require.NoError(t, err)
_, err = active.Logical().Write("auth/userpass/users/end-user", map[string]interface{}{
"password": testPassword,
})
require.NoError(t, err)

// Create a confidential client
_, err = active.Logical().Write("identity/oidc/client/confidential", map[string]interface{}{
"redirect_uris": []string{testRedirectURI},
"assignments": []string{"allow_all"},
"id_token_ttl": "1h",
"access_token_ttl": "30m",
})
require.NoError(t, err)

// Read the client ID and secret in order to configure the OIDC client
resp, err := active.Logical().Read("identity/oidc/client/confidential")
require.NoError(t, err)
clientID := resp.Data["client_id"].(string)
clientSecret := resp.Data["client_secret"].(string)

// We aren't going to open up a browser to facilitate the login and redirect
// from this test, so we'll log in via userpass and set the client's token as
// the token that results from the authentication.
resp, err = active.Logical().Write("auth/userpass/login/end-user", map[string]interface{}{
"password": testPassword,
})
require.NoError(t, err)
clientToken := resp.Auth.ClientToken
entityID := resp.Auth.EntityID

// Look up the token to get its creation time. This will be used for test
// cases that make assertions on the max_age parameter and auth_time claim.
resp, err = active.Logical().Write("auth/token/lookup", map[string]interface{}{
"token": clientToken,
})
require.NoError(t, err)
expectedAuthTime, err := strconv.Atoi(string(resp.Data["creation_time"].(json.Number)))
require.NoError(t, err)

// Read the issuer from the OIDC provider's discovery document
var discovery struct {
Issuer string `json:"issuer"`
}
decodeRawRequest(t, active, http.MethodGet,
"/v1/identity/oidc/provider/default/.well-known/openid-configuration",
nil, &discovery)

// Create the client-side OIDC provider config
pc, err := oidc.NewConfig(discovery.Issuer, clientID,
oidc.ClientSecret(clientSecret), []oidc.Alg{oidc.RS256},
[]string{testRedirectURI}, oidc.WithProviderCA(string(cluster.CACertPEM)))
require.NoError(t, err)

// Create the client-side OIDC provider
p, err := oidc.NewProvider(pc)
require.NoError(t, err)
defer p.Done()

// Create the client-side PKCE code verifier
v, err := oidc.NewCodeVerifier()
require.NoError(t, err)

type args struct {
useStandby bool
options []oidc.Option
}
tests := []struct {
name string
args args
expected string
}{
{
name: "active: authorization code flow",
args: args{
options: []oidc.Option{
oidc.WithScopes("openid"),
},
},
expected: fmt.Sprintf(`{
"iss": "%s",
"aud": "%s",
"sub": "%s",
"namespace": "root"
}`, discovery.Issuer, clientID, entityID),
},
{
name: "active: authorization code flow with max_age parameter",
args: args{
options: []oidc.Option{
oidc.WithScopes("openid"),
oidc.WithMaxAge(60),
},
},
expected: fmt.Sprintf(`{
"iss": "%s",
"aud": "%s",
"sub": "%s",
"namespace": "root",
"auth_time": %d
}`, discovery.Issuer, clientID, entityID, expectedAuthTime),
},
{
name: "active: authorization code flow with Proof Key for Code Exchange (PKCE)",
args: args{
options: []oidc.Option{
oidc.WithScopes("openid"),
oidc.WithPKCE(v),
},
},
expected: fmt.Sprintf(`{
"iss": "%s",
"aud": "%s",
"sub": "%s",
"namespace": "root"
}`, discovery.Issuer, clientID, entityID),
},
{
name: "standby: authorization code flow",
args: args{
useStandby: true,
options: []oidc.Option{
oidc.WithScopes("openid"),
},
},
expected: fmt.Sprintf(`{
"iss": "%s",
"aud": "%s",
"sub": "%s",
"namespace": "root"
}`, discovery.Issuer, clientID, entityID),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := active
if tt.args.useStandby {
client = standby
}
client.SetToken(clientToken)

// Create the client-side OIDC request state
oidcRequest, err := oidc.NewRequest(10*time.Minute, testRedirectURI, tt.args.options...)
require.NoError(t, err)

// Get the URL for the authorization endpoint from the OIDC client
authURL, err := p.AuthURL(context.Background(), oidcRequest)
require.NoError(t, err)
parsedAuthURL, err := url.Parse(authURL)
require.NoError(t, err)

// This replace only occurs because we're not using the browser in this test
authURLPath := strings.Replace(parsedAuthURL.Path, "/ui/vault/", "/v1/", 1)

// Kick off the authorization code flow
var authResp struct {
Code string `json:"code"`
State string `json:"state"`
}
decodeRawRequest(t, client, http.MethodGet, authURLPath, parsedAuthURL.Query(), &authResp)

// The returned state must match the OIDC client state
require.Equal(t, oidcRequest.State(), authResp.State)

// Exchange the authorization code for an ID token and access token.
// The ID token signature is verified using the provider's public keys after
// the exchange takes place. The ID token is also validated according to the
// client-side requirements of the OIDC spec. See the validation code at:
// - https://github.com/hashicorp/cap/blob/main/oidc/provider.go#L240
// - https://github.com/hashicorp/cap/blob/main/oidc/provider.go#L441
token, err := p.Exchange(context.Background(), oidcRequest, authResp.State, authResp.Code)
require.NoError(t, err)
require.NotNil(t, token)
idToken := token.IDToken()
accessToken := token.StaticTokenSource()

// Get the ID token claims
allClaims := make(map[string]interface{})
require.NoError(t, idToken.Claims(&allClaims))

// Get the sub claim for userinfo validation
require.NotEmpty(t, allClaims["sub"])
subject := allClaims["sub"].(string)

// Request userinfo using the access token
err = p.UserInfo(context.Background(), accessToken, subject, &allClaims)
require.NoError(t, err)

// Assert that claims computed during the flow (i.e., not known
// ahead of time in this test) are present as top-level keys
for _, claim := range []string{"iat", "exp", "nonce", "at_hash", "c_hash"} {
_, ok := allClaims[claim]
require.True(t, ok)
}

// Assert that all other expected claims are populated
expectedClaims := make(map[string]interface{})
require.NoError(t, json.Unmarshal([]byte(tt.expected), &expectedClaims))
for k, expectedVal := range expectedClaims {
actualVal, ok := allClaims[k]
require.True(t, ok)
require.EqualValues(t, expectedVal, actualVal)
}
})
}
}

// TestOIDC_Auth_Code_Flow_Confidential_CAP_Client tests the authorization code
// flow using a Vault OIDC provider. The test uses the CAP OIDC client to verify
// that the Vault OIDC provider's responses pass the various client-side validation
Expand Down
4 changes: 4 additions & 0 deletions vault/identity_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ func (i *IdentityStore) initialize(ctx context.Context, req *logical.Initializat
return nil
}

if err := i.storeOIDCDefaultResources(ctx, req.Storage); err != nil {
return err
}

entry, err := logical.StorageEntryJSON(caseSensitivityKey, &casesensitivity{
DisableLowerCasedNames: i.disableLowerCasedNames,
})
Expand Down
5 changes: 5 additions & 0 deletions vault/identity_store_oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,11 @@ func (i *IdentityStore) pathOIDCDeleteKey(ctx context.Context, req *logical.Requ

targetKeyName := d.Get("name").(string)

if targetKeyName == defaultKeyName {
return logical.ErrorResponse("deletion of key %q not allowed",
defaultKeyName), nil
}

i.oidcLock.Lock()

roleNames, err := i.roleNamesReferencingTargetKeyName(ctx, req, targetKeyName)
Expand Down
Loading