diff --git a/changelog/13231.txt b/changelog/13231.txt new file mode 100644 index 000000000000..3af52338134e --- /dev/null +++ b/changelog/13231.txt @@ -0,0 +1,3 @@ +```release-note:bug +identity/oidc: Make the `nonce` parameter optional for the Authorization Endpoint of OIDC providers. +``` diff --git a/go.sum b/go.sum index 2698877f7386..ad0cf3cd62f7 100644 --- a/go.sum +++ b/go.sum @@ -830,7 +830,6 @@ github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jU github.com/hashicorp/go-memdb v1.3.2 h1:RBKHOsnSszpU6vxq80LzC2BaQjuuvoyaQbkLTf7V7g8= github.com/hashicorp/go-memdb v1.3.2/go.mod h1:Mluclgwib3R93Hk5fxEfiRhB+6Dar64wWh71LpNSe3g= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-msgpack v1.1.5 h1:9byZdVjKTe5mce63pRVNP1L7UAmdHOTEMGehn6KvJWs= github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4= diff --git a/vault/identity_store_oidc.go b/vault/identity_store_oidc.go index cc66e63194af..e9dbd18242c0 100644 --- a/vault/identity_store_oidc.go +++ b/vault/identity_store_oidc.go @@ -111,7 +111,7 @@ const ( ) var ( - requiredClaims = []string{ + reservedClaims = []string{ "iat", "aud", "exp", "iss", "sub", "namespace", "nonce", "auth_time", "at_hash", "c_hash", @@ -970,6 +970,7 @@ func (tok *idToken) generatePayload(logger hclog.Logger, templates ...string) ([ "iat": tok.IssuedAt, } + // Copy optional claims into output if len(tok.Nonce) > 0 { output["nonce"] = tok.Nonce } @@ -1009,7 +1010,7 @@ func mergeJSONTemplates(logger hclog.Logger, output map[string]interface{}, temp } for k, v := range parsed { - if !strutil.StrListContains(requiredClaims, k) { + if !strutil.StrListContains(reservedClaims, k) { output[k] = v } else { logger.Warn("invalid top level OIDC template key", "template", template, "key", k) @@ -1114,9 +1115,9 @@ func (i *IdentityStore) pathOIDCCreateUpdateRole(ctx context.Context, req *logic } for key := range tmp { - if strutil.StrListContains(requiredClaims, key) { + if strutil.StrListContains(reservedClaims, key) { return logical.ErrorResponse(`top level key %q not allowed. Restricted keys: %s`, - key, strings.Join(requiredClaims, ", ")), nil + key, strings.Join(reservedClaims, ", ")), nil } } } diff --git a/vault/identity_store_oidc_provider.go b/vault/identity_store_oidc_provider.go index cff96505bfa7..6a8e6787a2aa 100644 --- a/vault/identity_store_oidc_provider.go +++ b/vault/identity_store_oidc_provider.go @@ -403,7 +403,6 @@ func oidcProviderPaths(i *IdentityStore) []*framework.Path { "nonce": { Type: framework.TypeString, Description: "The value that will be returned in the ID token nonce claim after a token exchange.", - Required: true, }, "max_age": { Type: framework.TypeInt, @@ -793,9 +792,9 @@ func (i *IdentityStore) pathOIDCCreateUpdateScope(ctx context.Context, req *logi } for key := range tmp { - if strutil.StrListContains(requiredClaims, key) { + if strutil.StrListContains(reservedClaims, key) { return logical.ErrorResponse(`top level key %q not allowed. Restricted keys: %s`, - key, strings.Join(requiredClaims, ", ")), nil + key, strings.Join(reservedClaims, ", ")), nil } } } @@ -1518,12 +1517,6 @@ func (i *IdentityStore) pathOIDCAuthorize(ctx context.Context, req *logical.Requ return authResponse("", state, ErrAuthInvalidRedirectURI, "redirect_uri is not allowed for the client") } - // Validate the nonce - nonce := d.Get("nonce").(string) - if nonce == "" { - return authResponse("", state, ErrAuthInvalidRequest, "nonce parameter is required") - } - // We don't support the request or request_uri parameters. If they're provided, // the appropriate errors must be returned. For details, see the spec at: // https://openid.net/specs/openid-connect-core-1_0.html#RequestObject @@ -1556,6 +1549,10 @@ func (i *IdentityStore) pathOIDCAuthorize(ctx context.Context, req *logical.Requ return authResponse("", state, ErrAuthAccessDenied, "identity entity not authorized by client assignment") } + // A nonce is optional for the authorization code flow. If not + // provided, the nonce claim will be omitted from the ID token. + nonce := d.Get("nonce").(string) + // Create the auth code cache entry authCodeEntry := &authCodeCacheEntry{ provider: name, @@ -1567,10 +1564,9 @@ func (i *IdentityStore) pathOIDCAuthorize(ctx context.Context, req *logical.Requ } // Validate the optional max_age parameter to check if an active re-authentication - // of the user should occur. Re-authentication will be requested if max_age=0 or the - // last time the token actively authenticated exceeds the given max_age requirement. - // Returning ErrAuthMaxAgeReAuthenticate will enforce the user to re-authenticate via - // the user agent. + // of the user should occur. Re-authentication will be requested if the last time + // the token actively authenticated exceeds the given max_age requirement. Returning + // ErrAuthMaxAgeReAuthenticate will enforce the user to re-authenticate via the user agent. if maxAgeRaw, ok := d.GetOk("max_age"); ok { maxAge := maxAgeRaw.(int) if maxAge < 1 { diff --git a/vault/identity_store_oidc_provider_test.go b/vault/identity_store_oidc_provider_test.go index 02499b3727c6..e279b21438bd 100644 --- a/vault/identity_store_oidc_provider_test.go +++ b/vault/identity_store_oidc_provider_test.go @@ -287,6 +287,20 @@ func TestOIDC_Path_OIDC_Token(t *testing.T) { }, }, }, + { + name: "valid token request with empty nonce in authorize request", + args: args{ + clientReq: testClientReq(s), + providerReq: testProviderReq(s, clientID), + assignmentReq: testAssignmentReq(s, entityID, groupID), + authorizeReq: func() *logical.Request { + req := testAuthorizeReq(s, clientID) + delete(req.Data, "nonce") + return req + }(), + tokenReq: testTokenReq(s, "", clientID, clientSecret), + }, + }, { name: "valid token request", args: args{ @@ -404,9 +418,39 @@ func TestOIDC_Path_OIDC_Token(t *testing.T) { require.Equal(t, "Bearer", tokenRes.TokenType) require.NotEmpty(t, tokenRes.AccessToken) require.NotEmpty(t, tokenRes.IDToken) - require.NotEmpty(t, tokenRes.ExpiresIn) + require.Equal(t, int64(86400), tokenRes.ExpiresIn) require.Empty(t, tokenRes.Error) require.Empty(t, tokenRes.ErrorDescription) + + // Parse the claims from the ID token payload + parts := strings.Split(tokenRes.IDToken, ".") + require.Equal(t, 3, len(parts)) + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + require.NoError(t, err) + claims := make(map[string]interface{}) + require.NoError(t, json.Unmarshal(payload, &claims)) + + // Assert that reserved claims are present in the ID token. + // Optional reserved claims are asserted on conditionally. + for _, c := range reservedClaims { + switch c { + case "nonce": + // nonce must equal the nonce provided in the authorize request (including empty) + require.EqualValues(t, tt.args.authorizeReq.Data[c], claims[c]) + + case "auth_time": + // auth_time must exist if max_age provided in the authorize request + if _, ok := tt.args.authorizeReq.Data["max_age"]; ok { + require.EqualValues(t, creationTime.Unix(), claims[c]) + } else { + require.Empty(t, claims[c]) + } + + default: + // other reserved claims must be present in all cases + require.NotEmpty(t, claims[c]) + } + } }) } } @@ -579,21 +623,6 @@ func TestOIDC_Path_OIDC_Authorize(t *testing.T) { }, wantErr: ErrAuthInvalidRequest, }, - { - name: "invalid authorize request with missing nonce", - args: args{ - entityID: entityID, - clientReq: testClientReq(s), - providerReq: testProviderReq(s, clientID), - assignmentReq: testAssignmentReq(s, entityID, groupID), - authorizeReq: func() *logical.Request { - req := testAuthorizeReq(s, clientID) - req.Data["nonce"] = "" - return req - }(), - }, - wantErr: ErrAuthInvalidRequest, - }, { name: "invalid authorize request with request parameter provided", args: args{ @@ -683,6 +712,20 @@ func TestOIDC_Path_OIDC_Authorize(t *testing.T) { }, wantErr: ErrAuthInvalidRequest, }, + { + name: "valid authorize request with empty nonce", + args: args{ + entityID: entityID, + clientReq: testClientReq(s), + providerReq: testProviderReq(s, clientID), + assignmentReq: testAssignmentReq(s, entityID, groupID), + authorizeReq: func() *logical.Request { + req := testAuthorizeReq(s, clientID) + delete(req.Data, "nonce") + return req + }(), + }, + }, { name: "active re-authentication required with token creation time exceeding max_age requirement", args: args{ @@ -842,7 +885,7 @@ func TestOIDC_Path_OIDC_Authorize(t *testing.T) { } // setupOIDCCommon creates all of the resources needed to test a Vault OIDC provider. -// Returns the entity ID, group ID, and client ID to be used in tests. +// Returns the entity ID, group ID, client ID, client secret to be used in tests. func setupOIDCCommon(t *testing.T, c *Core, s logical.Storage) (string, string, string, string) { t.Helper() ctx := namespace.RootContext(nil) diff --git a/website/content/api-docs/secret/identity/oidc-provider.mdx b/website/content/api-docs/secret/identity/oidc-provider.mdx index fe017e9b215f..4de357852bc9 100644 --- a/website/content/api-docs/secret/identity/oidc-provider.mdx +++ b/website/content/api-docs/secret/identity/oidc-provider.mdx @@ -17,7 +17,7 @@ This endpoint creates or updates a Provider. - `name` `(string: )` – The name of the provider. This parameter is specified as part of the URL. -- `issuer` `(string: )` - Specifies what will be used as the `scheme://host:port` component for the `iss` claim of ID tokens. Defaults to a URL with +- `issuer` `(string: )` - Specifies what will be used as the `scheme://host:port` component for the `iss` claim of ID tokens. This defaults to a URL with Vault's `api_addr` as the `scheme://host:port` component and `/v1/:namespace/identity/oidc/provider/:name` as the path component. If provided explicitly, it must point to a Vault instance that is network reachable by clients for ID token validation. @@ -37,7 +37,7 @@ This endpoint creates or updates a Provider. ### Sample Request ```shell-session -$ curl \ +$ curl \ --header "X-Vault-Token: ..." \ --request POST \ --data @payload.json \ @@ -154,7 +154,7 @@ This endpoint creates or updates a scope. ### Sample Request ```shell-session -$ curl \ +$ curl \ --header "X-Vault-Token: ..." \ --request POST \ --data @payload.json \ @@ -240,4 +240,509 @@ $ curl \ --header "X-Vault-Token: ..." \ --request DELETE \ http://127.0.0.1:8200/v1/identity/oidc/scope/test-scope -``` \ No newline at end of file +``` + +## Create or Update a Client + +This endpoint creates or updates a client. + +| Method | Path | +| :----- | :----------------- | +| `POST` | `identity/oidc/client/:name` | + +### Parameters + +- `name` `(string: )` – The name of the client. This parameter is specified as part of the URL. + +- `key` `(string: )` – A reference to a named key resource. This cannot be modified after creation. + +- `redirect_uris` `([]string: )` - Redirection URI values used by the client. One of these values + must exactly match the `redirect_uri` parameter value used in each [authentication request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + +- `assignments` `([]string: )` – A list of assignment resources associated with the client. + +- `id_token_ttl` `(int or duration: )` – The time-to-live for ID tokens obtained by the client. + This can be specified as a number of seconds or as a [Go duration format string](https://golang.org/pkg/time/#ParseDuration) + like `"30m"` or `"6h"`. The value should be less than the `verification_ttl` on the key. + +- `access_token_ttl` `(int or duration: )` – The time-to-live for access tokens obtained by the client. + This can be specified as a number of seconds or as a [Go duration format string](https://golang.org/pkg/time/#ParseDuration) like `"30m"` or `"6h"`. + +### Sample Payload + +```json +{ + "key":"test-key", + "access_token_ttl":"30m", + "id_token_ttl":"1h" +} +``` + +### Sample Request + +```shell-session +$ curl \ + --header "X-Vault-Token: ..." \ + --request POST \ + --data @payload.json \ + http://127.0.0.1:8200/v1/identity/oidc/client/test-client +``` + +## Read Client by Name + +This endpoint queries a client by its name. + +| Method | Path | +| :----- | :------------------------ | +| `GET` | `/identity/oidc/client/:name` | + +### Parameters + +- `name` `(string: )` – The name of the client. + +### Sample Request + +```shell-session +$ curl \ + --header "X-Vault-Token: ..." \ + http://127.0.0.1:8200/v1/identity/oidc/client/test-client +``` + +### Sample Response + +```json +{ + "data":{ + "access_token_ttl":1800, + "assignments":[], + "client_id":"014zXvcvbvIZWwD5NfD1Uzmv7c5JBRMb", + "client_secret":"hvo_secret_bZtgQPBZaJXK7F5vOI7JlvEuLOfOUS7DmwynFjE3xKcsen7TyowqPFfYFXG2tbWM", + "id_token_ttl":3600, + "key":"test-key", + "redirect_uris":[] + } +} +``` + +## List Clients + +This endpoint returns a list of all configured clients. + +| Method | Path | +| :----- | :------------------------------ | +| `LIST` | `/identity/oidc/client` | + +### Sample Request + +```shell-session +$ curl \ + --header "X-Vault-Token: ..." \ + --request LIST \ + http://127.0.0.1:8200/v1/identity/oidc/client +``` + +### Sample Response + +```json +{ + "data": { + "keys":[ + "test-client" + ] + } +} +``` + +## Delete Client by Name + +This endpoint deletes a client. + +| Method | Path | +| :------- | :------------------------ | +| `DELETE` | `/identity/oidc/client/:name` | + +### Parameters + +- `name` `(string: )` – The name of the client. + +### Sample Request + +```shell-session +$ curl \ + --header "X-Vault-Token: ..." \ + --request DELETE \ + http://127.0.0.1:8200/v1/identity/oidc/client/test-client +``` + +## Create or Update an Assignment + +This endpoint creates or updates an assignment. + +| Method | Path | +| :----- | :----------------- | +| `POST` | `identity/oidc/assignment/:name` | + +### Parameters + +- `name` `(string: )` – The name of the assignment. This parameter is specified as part of the URL. + +- `entity_ids` `([]string: )` - A list of Vault [entity](https://www.vaultproject.io/docs/secrets/identity#entities-and-aliases) IDs. + +- `group_ids` `([]string: )` – A list of Vault [group](https://www.vaultproject.io/docs/secrets/identity#identity-groups) IDs. + +### Sample Payload + +```json +{ + "group_ids":["my-group"], + "entity_ids":["my-entity"] +} +``` + +### Sample Request + +```shell-session +$ curl \ + --header "X-Vault-Token: ..." \ + --request POST \ + --data @payload.json \ + http://127.0.0.1:8200/v1/identity/oidc/assignment/test-assignment +``` + +## Read Assignment by Name + +This endpoint queries an assignment by its name. + +| Method | Path | +| :----- | :------------------------ | +| `GET` | `/identity/oidc/assignment/:name` | + +### Parameters + +- `name` `(string: )` – The name of the assignment. + +### Sample Request + +```shell-session +$ curl \ + --header "X-Vault-Token: ..." \ + http://127.0.0.1:8200/v1/identity/oidc/assignment/test-assignment +``` + +### Sample Response + +```json +{ + "data":{ + "entity_ids":[ + "my-entity" + ], + "group_ids":[ + "my-group" + ] + } +} +``` + +## List Assignments + +This endpoint returns a list of all configured assignments. + +| Method | Path | +| :----- | :------------------------------ | +| `LIST` | `/identity/oidc/assignment` | + +### Sample Request + +```shell-session +$ curl \ + --header "X-Vault-Token: ..." \ + --request LIST \ + http://127.0.0.1:8200/v1/identity/oidc/assignment +``` + +### Sample Response + +```json +{ + "data": { + "keys":[ + "test-assignment" + ] + } +} +``` + +## Delete Assignment by Name + +This endpoint deletes an assignment. + +| Method | Path | +| :------- | :------------------------ | +| `DELETE` | `/identity/oidc/assignment/:name` | + +### Parameters + +- `name` `(string: )` – The name of the assignment. + +### Sample Request + +```shell-session +$ curl \ + --header "X-Vault-Token: ..." \ + --request DELETE \ + http://127.0.0.1:8200/v1/identity/oidc/assignment/test-assignment +``` + +## Read Provider OpenID Configuration + +Returns OpenID Connect Metadata for a named OIDC provider. The response is a +compliant [OpenID Provider Configuration Response](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse). + +| Method | Path | +| :----- | :--------------------------------------------------------------- | +| `GET` | `/identity/oidc/provider/:name/.well-known/openid-configuration` | + +### Parameters + +- `name` `(string: )` – The name of the provider. This parameter is specified as part of the URL. + +### Sample Request + +```shell-session +$ curl \ + --request GET \ + http://127.0.0.1:8200/v1/identity/oidc/provider/test-provider/.well-known/openid-configuration +``` + +### Sample Response + +```json +{ + "issuer": "http://127.0.0.1:8200/v1/identity/oidc/provider/test-provider", + "jwks_uri": "http://127.0.0.1:8200/v1/identity/oidc/provider/test-provider/.well-known/keys", + "authorization_endpoint": "http://127.0.0.1:8200/ui/vault/identity/oidc/provider/test-provider/authorize", + "token_endpoint": "http://127.0.0.1:8200/v1/identity/oidc/provider/test-provider/token", + "userinfo_endpoint": "http://127.0.0.1:8200/v1/identity/oidc/provider/test-provider/userinfo", + "request_uri_parameter_supported": false, + "id_token_signing_alg_values_supported": [ + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES512", + "EdDSA" + ], + "response_types_supported": [ + "code" + ], + "scopes_supported": [ + "openid" + ], + "subject_types_supported": [ + "public" + ], + "grant_types_supported": [ + "authorization_code" + ], + "token_endpoint_auth_methods_supported": [ + "client_secret_basic" + ]} +``` + +## Read Provider Public Keys + +Query this path to retrieve the public portion of keys for an OIDC provider. +Clients can use them to validate the authenticity of an identity token. + +| Method | Path | +| :----- | :----------------------------------------------- | +| `GET` | `/identity/oidc/provider/:name/.well-known/keys` | + +### Parameters + +- `name` `(string: )` – The name of the provider. This parameter is specified as part of the URL. + +### Sample Request + +```shell-session +$ curl \ + --request GET \ + http://127.0.0.1:8200/v1/identity/oidc/provider/test-provider/.well-known/keys +``` + +### Sample Response + +```json +{ + "keys": [ + { + "use": "sig", + "kty": "RSA", + "kid": "ee7c0920-fdb9-5c1a-9c69-6dab710d1a09", + "alg": "RS256", + "n": "zdFjUV9lBw5nQPvTtwH-gzKgRG7iepvYbFoc2hNB0-inJL25oh-mvNW3GS8jPY5XHLsiWa_1TKKE99JrKQgane2C96soFeOvR7SozbCeH8_FpZelH1Pym1NV038j05Vp87uB9FeKPsy1PNOLPTs_Fp42JIAenly7ojYwPp1s61p9V0U9rOhtldY7GkXHLN9s8v3aJjxqrTS3Puhs9MFS7EgRrEDAc69uiLXCoYXKygjXddvJi6j446XxnO2eTRMGl1f2t04s_vDgVnFQgjQSKYWPbOMhf2slkeR47fqE3qqUDzINxauqMbkW-PlLP9IN0crR2uC07cG2os4RxN4YHw", + "e": "AQAB" + }, + { + "use": "sig", + "kty": "RSA", + "kid": "6e468221-b7c2-9d2d-744d-33b7ae0357cb", + "alg": "RS256", + "n": "rMaucILJKiFg_lkCE8ZEV_8jiYdaVDjKkc-8XPBW8S34wIRl1EbsgCYfMHtJnIJ_3eUgOVorW5KVeN9C8W16LR3lhqRWS9y4qlt0AcWpOvsmxr5q5dS_QqgCjeftCKwJzUsMi5bMW8wKjRZdd-qLz6X1rVSZWX82G0So8nRBg9d3MNJbKcdIJrRbrxWkm8U9xMqRouzbyQ2Hsp2rRVgGh7yjEA6daI5Ao8UsPdBmlCM9oKZ1_Kje5JTfZKeHlT-58vn_ylCjMVlapLuUsDN6He2kPVyOzGbie297VOfjmB7QX0ah1f7Ni1UJFJYHrVK9wMfCLTltSFZBcQ9--FlVdQ", + "e": "AQAB" + } + ]} +``` + +## Authorization Endpoint + +Provides the [Authorization Endpoint](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint) +for an OIDC provider. This allows OIDC clients to request an authorization code +to be used for the [Authorization Code Flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth). + +| Method | Path | +| :---------- | :---------------------------------------- | +| `GET/POST` | `/identity/oidc/provider/:name/authorize` | + +### Parameters + +- `name` `(string: )` - The name of the provider. This parameter is specified as part of the URL. + +- `scope` `(string: )` - A space-delimited list of scopes to be requested. The `openid` scope is required. + +- `response_type` `(string: )` - The OIDC authentication flow to be used. The following response types are supported: `code`. + +- `client_id` `(string: )` - The ID of the requesting client. + +- `redirect_uri` `(string: )` - The redirection URI to which the response will be sent. + +- `state` `(string: )` - A value used to maintain state between the authentication request and client. + +- `nonce` `(string: )` - A value that is returned in the ID token nonce claim. It is used to mitigate replay attacks, so we *strongly encourage* providing this optional parameter. + +### Sample Request + +```shell-session +$ curl \ + --request GET \ + --header "X-Vault-Token: ..." \ + -G \ + -d "response_type=code" \ + -d "client_id=$CLIENT_ID" \ + -d "state=af0ifjsldkj" \ + -d "nonce=abcdefghijk" \ + --data-urlencode "scope=openid" \ + --data-urlencode "redirect_uri=http://127.0.0.1:8251/callback" \ + http://127.0.0.1:8200/v1/identity/oidc/provider/test-provider/authorize +``` + +### Sample Response + +```json +{ + "code": "BDSc9kVYljxND93YpveBuJtSvguM3AWe", + "state": "af0ifjsldkj" +} +``` + +## Token Endpoint + +Provides the [Token Endpoint](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint) +for an OIDC provider. + +| Method | Path | +| :------ | :------------------------------------ | +| `POST` | `/identity/oidc/provider/:name/token` | + +### Parameters + +- `name` `(string: )` - The name of the provider. This parameter is +specified as part of the URL. + +- `code` `(string: )` - The authorization code received from the +provider's authorization endpoint. + +- `grant_type` `(string: )` - The authorization grant type. The +following grant types are supported: `authorization_code`. + +- `redirect_uri` `(string: )` - The callback location where the +authorization request was sent. This must match the `redirect_uri` used when the +original authorization code was generated. + +### Headers + +- Basic Auth `(string: )` - Authenticate the client using the `client_id` +and `client_secret` as described in the [client_secret_basic authentication method](https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication). +The authentication method uses the HTTP Basic authentication scheme. + +### Sample Request + +```shell-session +$ BASIC_AUTH_CREDS=$(printf "%s:%s" "$CLIENT_ID" "$CLIENT_SECRET" | base64) +$ curl \ + --request POST \ + --header "Authorization: Basic $BASIC_AUTH_CREDS" \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + -d "code=4RL50r78p8HsNJY0GVUNGfjLHnpkRf3N" \ + -d "grant_type=authorization_code" \ + -d "redirect_uri=http://127.0.0.1:8251/callback" \ + http://127.0.0.1:8200/v1/identity/oidc/provider/test-provider/token +``` + +### Sample Response + +```json +{ + "access_token": "b.AAAAAQJEH5VXjfjUESCwySTKk2MS1MGVNc9oU-N2EyoLKVo9SYa-NnOWAXloYfrlO45UWC3R1PC5ZShl3JdmRJ0264julNnlBduSNXJkYjgCQsFQwXTKHcjhqdNsmJNMWiPaHPn5NLSpNQVtzAxfHADt4r9rmX-UEG5seOWbmK_Z5WwS_4a8-wcVPB7FpOGzfBydP7yMxHu-3H1TWyQvYVr28XUfYxcBbdlzxhJn0yqkWItgmZ25xEOp7SW7Pg4tYB7AXfk", + "expires_in": 3600, + "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImEzMjk5ZWVmLTllNDEtOGNiYS1kNWExLTZmZWM2NjIyODRjYyJ9.eyJhdF9oYXNoIjoiMUdlQlEzUFdtUjJ2ajZVU2swSW42USIsImF1ZCI6InpTSktMVmk0R1BYS1o3TTZzUUEwY3FNc05VaHNPYkVTIiwiY19oYXNoIjoiN09SOUszNmhNdllENzJkUkFLUHhNdyIsImNvbnRhY3QiOnsiZW1haWwiOiJ2YXVsdEBoYXNoaWNvcnAuY29tIiwicGhvbmVfbnVtYmVyIjoiMTIzLTQ1Ni03ODkwIn0sImV4cCI6MTYzMzEwNjI5NCwiZ3JvdXBzIjpbImVuZ2luZWVyaW5nIl0sImlhdCI6MTYzMzEwNDQ5NCwiaXNzIjoiaHR0cDovLzEyNy4wLjAuMTo4MjAwL3YxL2lkZW50aXR5L29pZGMvcHJvdmlkZXIvbXktcHJvdmlkZXIiLCJuYW1lc3BhY2UiOiJyb290Iiwibm9uY2UiOiJhYmNkZWZnaGlqayIsInN1YiI6IjUwMDA3OTZlLTM2ZGYtMGQ4Yy02NDYwLTgxODUzZDliMjY2NyIsInVzZXJuYW1lIjoiZW5kLXVzZXIifQ.ehdLj6jnrJvltar1kkVSyNK48w2M5vkh5DTFJFZDqatnDWhQbbKGLZnVgd3wD6KPboXRaUwhGe4jDiTIiSoJaovOhsia77NKukym_ROLvGZw-LG7xaYkzJLnmEfeQhelLxWe0DHPROB7VXcFqBx8vX5hkuoVyqrB87vwiobK42pDPZ9MRsmbM2yzBC3wrnT7RQFtT4q2Bbyt9YIAHUaq9rU0PwJRoNISw6of1uQHo3_UzLdpwth7PEOEcI47OBGFA5vR_Gw3ocREfSrUWfCWOInAKCT43cImvg4Bts6qiZYfv9n-iNBq4AihGqq_VEF-hB1Hrprn7VgnEZ1VjUHaQQ", + "token_type": "Bearer" +} +``` + +## UserInfo Endpoint + +Provides the [UserInfo Endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) +for an OIDC provider. The UserInfo Endpoint is an OAuth 2.0 Protected +Resource that returns Claims about the authenticated End-User. + +| Method | Path | +| :------ | :--------------------------------------- | +| `POST` | `/identity/oidc/provider/:name/userinfo` | + +### Parameters + +- `name` `(string: )` - The name of the provider. This parameter is +specified as part of the URL. + +### Headers + +- Access Token `(string: )` - The access token provided by the +`Authorization: Bearer ` HTTP header acquired from the authorization +endpoint. + +### Sample Request + +```shell-session +$ curl \ + -X GET \ + --header "Authorization: Bearer $ACCESS_TOKEN" \ + http://127.0.0.1:8200/v1/identity/oidc/provider/test-provider/userinfo +``` + +### Sample Response + +```json +{ + "contact": { + "email": "vault@hashicorp.com", + "phone_number": "123-456-7890" + }, + "groups": [ + "engineering" + ], + "sub": "5000796e-36df-0d8c-6460-81853d9b2667", + "username": "end-user"} +``` diff --git a/website/content/docs/concepts/oidc-provider.mdx b/website/content/docs/concepts/oidc-provider.mdx new file mode 100644 index 000000000000..ab6b5836941d --- /dev/null +++ b/website/content/docs/concepts/oidc-provider.mdx @@ -0,0 +1,156 @@ +--- +layout: docs +page_title: OIDC Provider +description: >- + Describes how Vault can be an OIDC identity provider. +--- + +# OIDC Provider + +~> **Note:** This feature is currently a ***Tech Preview*** and not recommended for deployment in production. + + +This document describes how Vault can be an **OpenID Connect (OIDC) identity provider** by enabling applications to leverage Vault as a source of identity using the OIDC protocol. + +This feature allows clients speaking the OIDC protocol to take advantage of Vault's various authentication methods and source of identity. Clients can configure their authentication logic to talk to Vault. Once enabled, Vault will act as the bridge to identity providers via its existing authentication methods. Clients will also obtain identity information for their end-users by leveraging custom templating of Vault identity information. + +Vault as an OIDC provider allows mutual Vault and Boundary customers to leverage Vault's identity system to delegate authentication and authorization to Vault. Vault, therefore, acts as an identity provider for Boundary. Other HashiCorp products such as Consul can also leverage Vault's identity system and provide delegated authentication and authorization to its users. Having Vault as an OIDC provider allows a single sign-on experience to their end-users for organizations that want to leverage Vault as an identity provider. + +## Configuration Options + +The next few sections of the document provide implementation details for each resource that permits Vault configuration as an OIDC identity provider. + +### Providers + +A Vault namespace may contain several provider resources. Each configured provider will publish the APIs listed within the OIDC flow. The APIs will be served via backend path-based routing on Vault's listen [address](/docs/configuration/listener/tcp#address). + +A provider must have the following configuration parameters: + +* **Issuer URL**: used in the `iss` claim of ID tokens +* **Allowed client IDs**: limits which clients can access the provider +* **Scopes supported**: limits what identity information is available as claims + +The issuer URL parameter is necessary for the validation of ID tokens by clients. If an URL parameter is not provided explicitly, it will default to a URL with Vault's [api_addr](/docs/configuration#api_addr) as the `scheme://host:port` component and `/v1/:namespace/identity/oidc/provider/:name` as the path component. This means tokens issued by a provider in a specified Vault cluster must be validated within that same cluster. If the issuer URL is provided explicitly, it must point to a Vault instance that is network-reachable by clients for ID token validation. + +The allowed client IDs parameter utilizes the list of client IDs that have been generated by Vault as a part of client registration. By default, all clients will be *disallowed*. Providing an asterisk(*) as the parameter value will allow all clients to use the provider. The scopes parameter employs a list of references to named scope resources. The values provided are discoverable by the `scopes_supported` key in the OIDC discovery document of the provider. By default, a provider will have the `openid` scope available. See the scopes section below for more details on the `openid` scope. + +### Scopes + +Providers may reference scope resources via the `scopes_supported` parameter to make specific identity information available as claims. + +A scope will have the following configuration parameters: + +* **Description**: identity information captured by the scope +* **Template**: maps individual claims to Vault identity information + + +The template parameter takes advantage of the [JSON-based templating](/api-docs/secret/identity/tokens#template) used by identity tokens for claims mapping. This means the parameter will take a JSON string of arbitrary structure where the values may be replaced with specific identity information. Template parameters that are not present for a Vault identity are omitted from the resulting claims without an error. + + +Example of a JSON template for a scope: + +``` +{ + "username": {{identity.entity.aliases.$MOUNT_ACCESSOR.name}}, + "contact": { + "email": {{identity.entity.metadata.email}}, + "phone_number": {{identity.entity.metadata.phone_number}} + }, + "groups": {{identity.entity.groups.names}} +} +``` + +The full list of template parameters are included in the following table: + +| Name | Description | +| :------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------- | +| `identity.entity.id` | The entity's ID | +| `identity.entity.name` | The entity's name | +| `identity.entity.groups.ids` | The IDs of the groups the entity is a member of | +| `identity.entity.groups.names` | The names of the groups the entity is a member of | +| `identity.entity.metadata` | Metadata associated with the entity | +| `identity.entity.metadata.` | Metadata associated with the entity for the given key | +| `identity.entity.aliases..id` | Entity alias ID for the given mount | +| `identity.entity.aliases..name` | Entity alias name for the given mount | +| `identity.entity.aliases..metadata` | Metadata associated with the alias for the given mount | +| `identity.entity.aliases..metadata.` | Metadata associated with the alias for the given mount and metadata key | +| `identity.entity.aliases..custom_metadata` | Custom metadata associated with the alias for the given mount | +| `identity.entity.aliases..custom_metadata.` | Custom metadata associated with the alias for the given mount and custom metadata key | +| `time.now` | Current time as integral seconds since the Epoch | +| `time.now.plus.` | Current time plus a Go-parsable [duration](https://golang.org/pkg/time/#ParseDuration) | +| `time.now.minus.` | Current time minus a Go-parsable [duration](https://golang.org/pkg/time/#ParseDuration) | + + +Several named scopes can be made available on an individual provider. Note that the top-level keys in a JSON template may conflict with those in another scope. When scopes are made available on a provider, their templates are checked for top-level conflicts. A warning will be issued to the Vault operator if any conflicts are found. This may result in an error if the scopes are requested in an OIDC Authentication Request. + +The `openid` scope is a unique case scope that may not be modified or deleted. The scope will exist in Vault and supported by each provider by default. The scope represents the minimum set of claims required by the OIDC specification for inclusion in ID tokens. As such, templates may not contain top-level keys that overwrite the claims populated by the openid scope. + +The following defines the claims key and value mapping for the openid scope: + +* `iss`- configured issuer of the provider +* `sub`- unique entity ID of the Vault user +* `aud`- ID of the client +* `iat`- time of token issue +* `exp`- time of token issue + ID token TTL + +### Client registration + +A client resource allows the relying party to [dynamically register](https://openid.net/specs/openid-connect-registration-1_0.html) by providing metadata about itself to Vault. + +The client must have the following configuration parameters: + +* **Redirect URIs**: limits the valid redirect URIs in an authentication request +* **Assignments**: determines who can authenticate with the client +* **Key**: used to sign the ID tokens +* **ID token TTL**: specifies the time-to-live for ID tokens +* **Access token TTL**: establishes the time-to-live for access tokens + +A `client_id` and `client_secret` are generated and returned after a successful client registration. Their values are strings using the base62 character set. The `client_id` will have 32 characters, and the `client_secret` will have a prefix of `hvo_secret`. The `client_id` uniquely identifies the client. The `client_secret` will be used to authenticate to the token endpoint as described in [client authentication](https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication). + +The `key` parameter is required. The user must create a `key` as a required parameter of the client configuration. + +~> **Note**: At least one of the redirect URIs of a client must exactly match the `redirect_uri` parameter used in an authentication request initiated by the client. + +### Assignments + +Assignment resources are referenced by clients via the `assignments` parameter. This parameter limits the set of Vault users allowed to authenticate. The assignments of an associated client are validated during the authentication request, ensuring that the Vault identity associated with the request is a member of the assignment's entities or groups. + +### Keys + +Key resources are referenced by clients via the key parameter. This parameter specifies the key that will be used to sign ID tokens for the client. See existing [documentation](/api-docs/secret/identity/tokens#create-a-named-key) for details on keyring management, supported signing algorithms, rotation periods, and verification TTLs. Currently, a key referenced by a client cannot be changed. + +## OIDC flow + +~> **Note**: The Vault OIDC Provider feature currently only supports the [authorization code flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth). + +The following sections provide implementation details for the OIDC compliant APIs provided by Vault OIDC providers. + +Vault OIDC providers enable registered clients to authenticate and obtain identity information (or "claims") for their end-users. They do this by providing the APIs and behavior required to satisfy the OIDC specification for the [authorization code flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth). All clients are treated as first-party. This means that end-users will not be required to provide consent to the provider as detailed in section [3.1.2.4](https://openid.net/specs/openid-connect-core-1_0.html#Consent) of the OIDC specification. The provider will release information to clients as long as the end-user has ACL access to the provider and their identity has been authorized via an assignment. + +### OpenID configuration + +Each provider offers an unauthenticated endpoint that facilitates OIDC Discovery. All required metadata listed in [OpenID Provider Metadata](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata) is included in the discovery document. Additionally, the recommended `userinfo_endpoint` and `scopes_supported` metadata are included. + +### Keys + +Each provider offers an unauthenticated endpoint that provides the public portion of keys used to sign ID tokens. The keys are published in a JSON Web Key Set [(JWKS)](https://datatracker.ietf.org/doc/html/rfc7517) format. The keyset for an individual provider contains the keys referenced by all clients via the `allowed_client_ids` configuration parameter. A `Cache-Control` header to set based on responses, allowing clients to refresh their keys upon rotation. The `max-age` of the header is set based on the earliest rotation time of any of the keys in the keyset. + +### Authorization Endpoint + +Each provider offers an authenticated [authorization endpoint](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint). The authorization endpoint for each provider is added to Vault's [default policy](/docs/concepts/policies#default-policy) using the `identity/oidc/provider/+/authorize` path. The endpoint incorporates all required [authentication request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest) parameters as input. Additionally, the `state` parameter is required. + +The endpoint [validates](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequestValidation) client requests and ensures that all required parameters are present and valid. The `redirect_uri` of the request is validated against the client's `redirect_uris`. The requesting Vault entity will be validated against the client's `assignments`. An appropriate [error code](https://openid.net/specs/openid-connect-core-1_0.html#AuthError) is returned for invalid requests. + +An authorization code is generated with a successful validation of the request. The authorization code is single-use and cached with a lifetime of approximately 5 minutes, which mitigates the risk of leaks. A response including the original `state` presented by the client and `code` will be returned to the Vault UI which initiated the request. Vault will issue an HTTP 302 redirect to the `redirect_uri` of the request, which includes the `code` and `state` as query parameters. + +### Token Endpoint + +Each provider will offer a [token endpoint](/api-docs/secret/identity/oidc-provider#token-endpoint). The endpoint may be unauthenticated in Vault but is authenticated by requiring a `client_secret` as described in [client authentication](https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication). The endpoint ingests all required [token request](/api-docs/secret/identity/oidc-provider#parameters-15) parameters as input. The endpoint [validates](https://openid.net/specs/openid-connect-core-1_0.html#TokenRequestValidation) the client requests and exchanges an authorization code for the ID token and access token. The cache of authorization codes will be verified against the code presented in the exchange. The appropriate [error codes](https://openid.net/specs/openid-connect-core-1_0.html#TokenErrorResponse) are returned for all invalid requests. + +The ID token is generated and returned upon successful client authentication and request validation. The ID token will contain a combination of required and configurable claims. The required claims are enumerated in the scopes section above for the `openid` scope. The configurable claims are populated by templates associated with the scopes provided in the authentication request that generated the authorization code. + +An access token is also generated and returned upon successful client authentication and request validation. The access token is a Vault [batch token](/docs/concepts/tokens#batch-tokens) with a policy that only provides read access to the issuing provider's [userinfo endpoint](/api-docs/secret/identity/oidc-provider#userinfo-endpoint). The access token is also a TTL as defined by the `access_token_ttl` of the requesting client. + +### UserInfo Endpoint + +Each provider provides an authenticated [userinfo endpoint](/api-docs/secret/identity/oidc-provider#userinfo-endpoint). The endpoint accepts the access token obtained from the token endpoint as a [bearer token](/api-docs#authentication). The userinfo response is a JSON object with the `application/json` content type. The JSON object contains claims for the Vault entity associated with the access token. The claims returned are determined by the scopes requested in the authentication request that produced the access token. The `sub` claim is always returned as the entity ID in the userinfo response. diff --git a/website/content/docs/secrets/identity/oidc-provider.mdx b/website/content/docs/secrets/identity/oidc-provider.mdx new file mode 100644 index 000000000000..05225109d01b --- /dev/null +++ b/website/content/docs/secrets/identity/oidc-provider.mdx @@ -0,0 +1,138 @@ +--- +layout: docs +page_title: OIDC Identity Provider +description: >- + Setup and configuration for Vault as an OpenID Connect (OIDC) identity provider. +--- + +# OIDC Identity Provider + +~> **Note:** This feature is currently a ***Tech Preview*** and not recommended +for deployment in production. + +Vault as an OIDC identity provider allows clients speaking the OIDC protocol to +take advantage of Vault's various authentication methods and source of +identity. Clients can configure their authentication logic to talk to Vault. +Once enabled, Vault will act as the bridge to identity providers via its +existing authentication methods. Clients will also obtain identity information +for their end-users by leveraging custom templating of Vault identity +information. For more information on the configuration resources and OIDC endpoints, +please visit the [OIDC provider](/docs/concepts/oidc-provider) concepts page. + +The Vault OIDC provider feature currently only supports the +[authorization code flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth). + +## OIDC Provider Configuration + +The Vault OIDC provider system is built on top of the identity secrets engine. +This secrets engine is mounted by default and cannot be disabled or moved. + +Most secrets engines must be configured in advance before they can perform +their functions. These steps are usually completed by an operator or +configuration management tool. + +1. Create a key that will be used to sign/verify ID tokens: + ```text + $ vault write identity/oidc/key/my-key \ + allowed_client_ids="xxAQWBYzD2WXsB8GiZqwq4jsUwfG0hJV" \ + verification_ttl="1h" \ + rotation_period="1h" \ + algorithm="RS256" + Success! Data written to: identity/oidc/key/my-key + ``` + +1. Create an assignment. This specifies which Vault entities and groups are +authorized to use a specific OIDC client for authentication flows: + + ```text + $ vault write identity/oidc/assignment/my-assignment \ + group_ids="b6ea7804-acbd-e866-7c51-0896456bd4bb" \ + entity_ids="aa786a7a-da2f-dca7-3680-0710771cca51" + Success! Data written to: identity/oidc/assignment/my-assignment + ``` + +1. Create the 'user' custom scope: + + ```text + $ TOKEN_TEMPLATE=$(cat << EOF + { + "username": {{identity.entity.aliases.$MOUNT_ACCESSOR.name}}, + "contact": { + "email": {{identity.entity.metadata.email}}, + "phone_number": {{identity.entity.metadata.phone_number}} + }, + "groups": {{identity.entity.groups.names}} + } + EOF + ) + $ vault write identity/oidc/scope/user \ + description="Scope for user metadata" \ + template="$(echo $TOKEN_TEMPLATE | base64 -)" + Success! Data written to: identity/oidc/scope/user + ``` + +1. Create an OIDC client: + + ```text + $ vault write identity/oidc/client/my-webapp \ + redirect_uris="http://127.0.0.1:8251/callback,http://127.0.0.1:8500/ui/oidc/callback" \ + assignments="my-assignment" \ + key="my-key" \ + id_token_ttl="30m" \ + access_token_ttl="1h" + Success! Data written to: identity/oidc/client/my-webapp + ``` + +1. Create an OIDC provider: + + ```text + $ vault write identity/oidc/provider/my-provider \ + allowed_client_ids="xxAQWBYzD2WXsB8GiZqwq4jsUwfG0hJV" \ + scopes_supported="user" + Success! Data written to: identity/oidc/provider/my-provider + ``` + +1. Query the OIDC provider configuration: + + ```text + $ curl -s http://127.0.0.1:8200/v1/identity/oidc/provider/my-provider/.well-known/openid-configuration + { + "issuer": "http://127.0.0.1:8200/v1/identity/oidc/provider/my-provider", + "jwks_uri": "http://127.0.0.1:8200/v1/identity/oidc/provider/my-provider/.well-known/keys", + "authorization_endpoint": "http://127.0.0.1:8200/ui/vault/identity/oidc/provider/my-provider/authorize", + "token_endpoint": "http://127.0.0.1:8200/v1/identity/oidc/provider/my-provider/token", + "userinfo_endpoint": "http://127.0.0.1:8200/v1/identity/oidc/provider/my-provider/userinfo", + "request_uri_parameter_supported": false, + "id_token_signing_alg_values_supported": [ + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES512", + "EdDSA" + ], + "response_types_supported": [ + "code" + ], + "scopes_supported": [ + "user", + "openid" + ], + "subject_types_supported": [ + "public" + ], + "grant_types_supported": [ + "authorization_code" + ], + "token_endpoint_auth_methods_supported": [ + "client_secret_basic" + ] + } + ``` + +## API + +The Vault OIDC provider feature has a full HTTP API. Please see the +[OIDC identity provider API](/api-docs/secret/identity/oidc-provider) for more +details.