Adds OIDC Token and UserInfo endpoints (#12711)
This commit is contained in:
parent
15fb265f85
commit
0551f91068
1
go.mod
1
go.mod
|
@ -59,6 +59,7 @@ require (
|
|||
github.com/google/go-github v17.0.0+incompatible
|
||||
github.com/google/go-metrics-stackdriver v0.2.0
|
||||
github.com/gorilla/mux v1.7.3 // indirect
|
||||
github.com/hashicorp/cap v0.1.0
|
||||
github.com/hashicorp/consul-template v0.27.1
|
||||
github.com/hashicorp/consul/api v1.11.0
|
||||
github.com/hashicorp/errwrap v1.1.0
|
||||
|
|
|
@ -411,9 +411,10 @@ func TestSysMounts_headerAuth(t *testing.T) {
|
|||
"type": "identity",
|
||||
"external_entropy_access": false,
|
||||
"config": map[string]interface{}{
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"passthrough_request_headers": []interface{}{"Authorization"},
|
||||
},
|
||||
"local": false,
|
||||
"seal_wrap": false,
|
||||
|
@ -465,9 +466,10 @@ func TestSysMounts_headerAuth(t *testing.T) {
|
|||
"type": "identity",
|
||||
"external_entropy_access": false,
|
||||
"config": map[string]interface{}{
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"passthrough_request_headers": []interface{}{"Authorization"},
|
||||
},
|
||||
"local": false,
|
||||
"seal_wrap": false,
|
||||
|
|
|
@ -510,10 +510,18 @@ WRITE_RESPONSE:
|
|||
w.Header().Set("Content-Type", contentType)
|
||||
}
|
||||
|
||||
if cacheControl, ok := resp.Data[logical.HTTPRawCacheControl].(string); ok {
|
||||
if cacheControl, ok := resp.Data[logical.HTTPCacheControlHeader].(string); ok {
|
||||
w.Header().Set("Cache-Control", cacheControl)
|
||||
}
|
||||
|
||||
if pragma, ok := resp.Data[logical.HTTPPragmaHeader].(string); ok {
|
||||
w.Header().Set("Pragma", pragma)
|
||||
}
|
||||
|
||||
if wwwAuthn, ok := resp.Data[logical.HTTPWWWAuthenticateHeader].(string); ok {
|
||||
w.Header().Set("WWW-Authenticate", wwwAuthn)
|
||||
}
|
||||
|
||||
w.WriteHeader(status)
|
||||
w.Write(body)
|
||||
}
|
||||
|
|
|
@ -73,9 +73,10 @@ func TestSysMounts(t *testing.T) {
|
|||
"type": "identity",
|
||||
"external_entropy_access": false,
|
||||
"config": map[string]interface{}{
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"passthrough_request_headers": []interface{}{"Authorization"},
|
||||
},
|
||||
"local": false,
|
||||
"seal_wrap": false,
|
||||
|
@ -127,9 +128,10 @@ func TestSysMounts(t *testing.T) {
|
|||
"type": "identity",
|
||||
"external_entropy_access": false,
|
||||
"config": map[string]interface{}{
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"passthrough_request_headers": []interface{}{"Authorization"},
|
||||
},
|
||||
"local": false,
|
||||
"seal_wrap": false,
|
||||
|
@ -241,9 +243,10 @@ func TestSysMount(t *testing.T) {
|
|||
"type": "identity",
|
||||
"external_entropy_access": false,
|
||||
"config": map[string]interface{}{
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"passthrough_request_headers": []interface{}{"Authorization"},
|
||||
},
|
||||
"local": false,
|
||||
"seal_wrap": false,
|
||||
|
@ -308,9 +311,10 @@ func TestSysMount(t *testing.T) {
|
|||
"type": "identity",
|
||||
"external_entropy_access": false,
|
||||
"config": map[string]interface{}{
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"passthrough_request_headers": []interface{}{"Authorization"},
|
||||
},
|
||||
"local": false,
|
||||
"seal_wrap": false,
|
||||
|
@ -441,9 +445,10 @@ func TestSysRemount(t *testing.T) {
|
|||
"type": "identity",
|
||||
"external_entropy_access": false,
|
||||
"config": map[string]interface{}{
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"passthrough_request_headers": []interface{}{"Authorization"},
|
||||
},
|
||||
"local": false,
|
||||
"seal_wrap": false,
|
||||
|
@ -508,9 +513,10 @@ func TestSysRemount(t *testing.T) {
|
|||
"type": "identity",
|
||||
"external_entropy_access": false,
|
||||
"config": map[string]interface{}{
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"passthrough_request_headers": []interface{}{"Authorization"},
|
||||
},
|
||||
"local": false,
|
||||
"seal_wrap": false,
|
||||
|
@ -609,9 +615,10 @@ func TestSysUnmount(t *testing.T) {
|
|||
"type": "identity",
|
||||
"external_entropy_access": false,
|
||||
"config": map[string]interface{}{
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"passthrough_request_headers": []interface{}{"Authorization"},
|
||||
},
|
||||
"local": false,
|
||||
"seal_wrap": false,
|
||||
|
@ -663,9 +670,10 @@ func TestSysUnmount(t *testing.T) {
|
|||
"type": "identity",
|
||||
"external_entropy_access": false,
|
||||
"config": map[string]interface{}{
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"passthrough_request_headers": []interface{}{"Authorization"},
|
||||
},
|
||||
"local": false,
|
||||
"seal_wrap": false,
|
||||
|
@ -863,9 +871,10 @@ func TestSysTuneMount(t *testing.T) {
|
|||
"type": "identity",
|
||||
"external_entropy_access": false,
|
||||
"config": map[string]interface{}{
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"passthrough_request_headers": []interface{}{"Authorization"},
|
||||
},
|
||||
"local": false,
|
||||
"seal_wrap": false,
|
||||
|
@ -930,9 +939,10 @@ func TestSysTuneMount(t *testing.T) {
|
|||
"type": "identity",
|
||||
"external_entropy_access": false,
|
||||
"config": map[string]interface{}{
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"passthrough_request_headers": []interface{}{"Authorization"},
|
||||
},
|
||||
"local": false,
|
||||
"seal_wrap": false,
|
||||
|
@ -1070,9 +1080,10 @@ func TestSysTuneMount(t *testing.T) {
|
|||
"type": "identity",
|
||||
"external_entropy_access": false,
|
||||
"config": map[string]interface{}{
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"passthrough_request_headers": []interface{}{"Authorization"},
|
||||
},
|
||||
"local": false,
|
||||
"seal_wrap": false,
|
||||
|
@ -1137,9 +1148,10 @@ func TestSysTuneMount(t *testing.T) {
|
|||
"type": "identity",
|
||||
"external_entropy_access": false,
|
||||
"config": map[string]interface{}{
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"passthrough_request_headers": []interface{}{"Authorization"},
|
||||
},
|
||||
"local": false,
|
||||
"seal_wrap": false,
|
||||
|
|
|
@ -34,9 +34,17 @@ const (
|
|||
// ignore errors.
|
||||
HTTPRawBodyAlreadyJSONDecoded = "http_raw_body_already_json_decoded"
|
||||
|
||||
// If set, HTTPRawCacheControl will replace the default Cache-Control=no-store header
|
||||
// If set, HTTPCacheControlHeader will replace the default Cache-Control=no-store header
|
||||
// set by the generic wrapping handler. The value must be a string.
|
||||
HTTPRawCacheControl = "http_raw_cache_control"
|
||||
HTTPCacheControlHeader = "http_raw_cache_control"
|
||||
|
||||
// If set, HTTPPragmaHeader will set the Pragma response header.
|
||||
// The value must be a string.
|
||||
HTTPPragmaHeader = "http_raw_pragma"
|
||||
|
||||
// If set, HTTPWWWAuthenticateHeader will set the WWW-Authenticate response header.
|
||||
// The value must be a string.
|
||||
HTTPWWWAuthenticateHeader = "http_www_authenticate"
|
||||
)
|
||||
|
||||
// Response is a struct that stores the response of a request.
|
||||
|
|
|
@ -0,0 +1,381 @@
|
|||
package identity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/cap/oidc"
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/builtin/credential/userpass"
|
||||
vaulthttp "github.com/hashicorp/vault/http"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
"github.com/hashicorp/vault/vault"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
testPassword = "testpassword"
|
||||
testRedirectURI = "https://127.0.0.1:8251/callback"
|
||||
testGroupScopeTemplate = `
|
||||
{
|
||||
"groups": {{identity.entity.groups.names}}
|
||||
}
|
||||
`
|
||||
testUserScopeTemplate = `
|
||||
{
|
||||
"username": {{identity.entity.aliases.%s.name}},
|
||||
"contact": {
|
||||
"email": {{identity.entity.metadata.email}},
|
||||
"phone_number": {{identity.entity.metadata.phone_number}}
|
||||
}
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
// TestOIDC_Auth_Code_Flow_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 requirements of the OIDC spec.
|
||||
func TestOIDC_Auth_Code_Flow_CAP_Client(t *testing.T) {
|
||||
cluster := setupOIDCTestCluster(t, 2)
|
||||
defer cluster.Cleanup()
|
||||
active := cluster.Cores[0].Client
|
||||
standby := cluster.Cores[1].Client
|
||||
|
||||
// Create an entity with some metadata
|
||||
resp, err := active.Logical().Write("identity/entity", map[string]interface{}{
|
||||
"name": "test-entity",
|
||||
"metadata": map[string]string{
|
||||
"email": "test@hashicorp.com",
|
||||
"phone_number": "123-456-7890",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
entityID := resp.Data["id"].(string)
|
||||
|
||||
// Create a group
|
||||
resp, err = active.Logical().Write("identity/group", map[string]interface{}{
|
||||
"name": "engineering",
|
||||
"member_entity_ids": []string{entityID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
groupID := resp.Data["id"].(string)
|
||||
|
||||
// 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)
|
||||
|
||||
// Get the userpass mount accessor
|
||||
mounts, err := active.Sys().ListAuth()
|
||||
require.NoError(t, err)
|
||||
var mountAccessor string
|
||||
for k, v := range mounts {
|
||||
if k == "userpass/" {
|
||||
mountAccessor = v.Accessor
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotEmpty(t, mountAccessor)
|
||||
|
||||
// Create an entity alias
|
||||
_, err = active.Logical().Write("identity/entity-alias", map[string]interface{}{
|
||||
"name": "end-user",
|
||||
"canonical_id": entityID,
|
||||
"mount_accessor": mountAccessor,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create some custom scopes
|
||||
_, err = active.Logical().Write("identity/oidc/scope/groups", map[string]interface{}{
|
||||
"template": testGroupScopeTemplate,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = active.Logical().Write("identity/oidc/scope/user", map[string]interface{}{
|
||||
"template": fmt.Sprintf(testUserScopeTemplate, mountAccessor),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a key
|
||||
_, err = active.Logical().Write("identity/oidc/key/test-key", map[string]interface{}{
|
||||
"allowed_client_ids": []string{"*"},
|
||||
"algorithm": "RS256",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an assignment
|
||||
_, err = active.Logical().Write("identity/oidc/assignment/test-assignment", map[string]interface{}{
|
||||
"entity_ids": []string{entityID},
|
||||
"group_ids": []string{groupID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a client
|
||||
_, err = active.Logical().Write("identity/oidc/client/test-client", map[string]interface{}{
|
||||
"key": "test-key",
|
||||
"redirect_uris": []string{testRedirectURI},
|
||||
"assignments": []string{"test-assignment"},
|
||||
"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/test-client")
|
||||
require.NoError(t, err)
|
||||
clientID := resp.Data["client_id"].(string)
|
||||
clientSecret := resp.Data["client_secret"].(string)
|
||||
|
||||
// Create the OIDC provider
|
||||
_, err = active.Logical().Write("identity/oidc/provider/test-provider", map[string]interface{}{
|
||||
"allowed_client_ids": []string{clientID},
|
||||
"scopes": []string{"user", "groups"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// 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
|
||||
|
||||
// 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/test-provider/.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()
|
||||
|
||||
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 user"),
|
||||
},
|
||||
},
|
||||
expected: fmt.Sprintf(`{
|
||||
"iss": "%s",
|
||||
"aud": "%s",
|
||||
"sub": "%s",
|
||||
"namespace": "root",
|
||||
"username": "end-user",
|
||||
"contact": {
|
||||
"email": "test@hashicorp.com",
|
||||
"phone_number": "123-456-7890"
|
||||
}
|
||||
}`, discovery.Issuer, clientID, entityID),
|
||||
},
|
||||
{
|
||||
name: "active: authorization code flow with additional scopes",
|
||||
args: args{
|
||||
options: []oidc.Option{
|
||||
oidc.WithScopes("openid user groups"),
|
||||
},
|
||||
},
|
||||
expected: fmt.Sprintf(`{
|
||||
"iss": "%s",
|
||||
"aud": "%s",
|
||||
"sub": "%s",
|
||||
"namespace": "root",
|
||||
"username": "end-user",
|
||||
"contact": {
|
||||
"email": "test@hashicorp.com",
|
||||
"phone_number": "123-456-7890"
|
||||
},
|
||||
"groups": ["engineering"]
|
||||
}`, 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: "standby: authorization code flow with additional scopes",
|
||||
args: args{
|
||||
useStandby: true,
|
||||
options: []oidc.Option{
|
||||
oidc.WithScopes("openid user groups"),
|
||||
},
|
||||
},
|
||||
expected: fmt.Sprintf(`{
|
||||
"iss": "%s",
|
||||
"aud": "%s",
|
||||
"sub": "%s",
|
||||
"namespace": "root",
|
||||
"username": "end-user",
|
||||
"contact": {
|
||||
"email": "test@hashicorp.com",
|
||||
"phone_number": "123-456-7890"
|
||||
},
|
||||
"groups": ["engineering"]
|
||||
}`, 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setupOIDCTestCluster(t *testing.T, numCores int) *vault.TestCluster {
|
||||
t.Helper()
|
||||
|
||||
coreConfig := &vault.CoreConfig{
|
||||
CredentialBackends: map[string]logical.Factory{
|
||||
"userpass": userpass.Factory,
|
||||
},
|
||||
}
|
||||
clusterOptions := &vault.TestClusterOptions{
|
||||
NumCores: numCores,
|
||||
HandlerFunc: vaulthttp.Handler,
|
||||
}
|
||||
cluster := vault.NewTestCluster(t, coreConfig, clusterOptions)
|
||||
cluster.Start()
|
||||
vault.TestWaitActive(t, cluster.Cores[0].Core)
|
||||
|
||||
return cluster
|
||||
}
|
||||
|
||||
func decodeRawRequest(t *testing.T, client *api.Client, method, path string, params url.Values, v interface{}) {
|
||||
t.Helper()
|
||||
|
||||
// Create the request and add query params if provided
|
||||
req := client.NewRequest(method, path)
|
||||
req.Params = params
|
||||
|
||||
// Send the raw request
|
||||
r, err := client.RawRequest(req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, r)
|
||||
require.Equal(t, http.StatusOK, r.StatusCode)
|
||||
defer r.Body.Close()
|
||||
|
||||
// Decode the body into v
|
||||
require.NoError(t, json.NewDecoder(r.Body).Decode(v))
|
||||
}
|
|
@ -70,12 +70,16 @@ type role struct {
|
|||
// include top-level keys, but those keys may not overwrite any of the
|
||||
// required OIDC fields.
|
||||
type idToken struct {
|
||||
Issuer string `json:"iss"` // api_addr or custom Issuer
|
||||
Namespace string `json:"namespace"` // Namespace of issuer
|
||||
Subject string `json:"sub"` // Entity ID
|
||||
Audience string `json:"aud"` // role ID will be used here.
|
||||
Expiry int64 `json:"exp"` // Expiration, as determined by the role.
|
||||
IssuedAt int64 `json:"iat"` // Time of token creation
|
||||
Issuer string `json:"iss"` // api_addr or custom Issuer
|
||||
Namespace string `json:"namespace"` // Namespace of issuer
|
||||
Subject string `json:"sub"` // Entity ID
|
||||
Audience string `json:"aud"` // Role or client ID will be used here.
|
||||
Expiry int64 `json:"exp"` // Expiration, as determined by the role or client.
|
||||
IssuedAt int64 `json:"iat"` // Time of token creation
|
||||
Nonce string `json:"nonce"` // Nonce given in OIDC authentication requests
|
||||
AuthTime int64 `json:"auth_time"` // AuthTime given in OIDC authentication requests
|
||||
AccessTokenHash string `json:"at_hash"` // Access token hash value
|
||||
CodeHash string `json:"c_hash"` // Authorization code hash value
|
||||
}
|
||||
|
||||
// discovery contains a subset of the required elements of OIDC discovery needed
|
||||
|
@ -107,8 +111,12 @@ const (
|
|||
)
|
||||
|
||||
var (
|
||||
requiredClaims = []string{"iat", "aud", "exp", "iss", "sub", "namespace"}
|
||||
supportedAlgs = []string{
|
||||
requiredClaims = []string{
|
||||
"iat", "aud", "exp", "iss",
|
||||
"sub", "namespace", "nonce",
|
||||
"auth_time", "at_hash", "c_hash",
|
||||
}
|
||||
supportedAlgs = []string{
|
||||
string(jose.RS256),
|
||||
string(jose.RS384),
|
||||
string(jose.RS512),
|
||||
|
@ -826,29 +834,14 @@ func (i *IdentityStore) pathOIDCGenerateToken(ctx context.Context, req *logical.
|
|||
return logical.ErrorResponse("role %q not found", roleName), nil
|
||||
}
|
||||
|
||||
var key *namedKey
|
||||
|
||||
keyRaw, found, err := i.oidcCache.Get(ns, "namedKeys/"+role.Key)
|
||||
key, err := i.getNamedKey(ctx, req.Storage, role.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if found {
|
||||
key = keyRaw.(*namedKey)
|
||||
} else {
|
||||
entry, _ := req.Storage.Get(ctx, namedKeyConfigPath+role.Key)
|
||||
if entry == nil {
|
||||
return logical.ErrorResponse("key %q not found", role.Key), nil
|
||||
}
|
||||
|
||||
if err := entry.DecodeJSON(&key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := i.oidcCache.SetDefault(ns, "namedKeys/"+role.Key, key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if key == nil {
|
||||
return logical.ErrorResponse("key %q not found", role.Key), nil
|
||||
}
|
||||
|
||||
// Validate that the role is allowed to sign with its key (the key could have been updated)
|
||||
if !strutil.StrListContains(key.AllowedClientIDs, "*") && !strutil.StrListContains(key.AllowedClientIDs, role.ClientID) {
|
||||
return logical.ErrorResponse("the key %q does not list the client ID of the role %q as an allowed client ID", role.Key, roleName), nil
|
||||
|
@ -897,7 +890,21 @@ func (i *IdentityStore) pathOIDCGenerateToken(ctx context.Context, req *logical.
|
|||
|
||||
groups = append(groups, inheritedGroups...)
|
||||
|
||||
payload, err := idToken.generatePayload(i.Logger(), role.Template, e, groups)
|
||||
// Parse and integrate the populated template. Structural errors with the template _should_
|
||||
// be caught during configuration. Error found during runtime will be logged, but they will
|
||||
// not block generation of the basic ID token. They should not be returned to the requester.
|
||||
_, populatedTemplate, err := identitytpl.PopulateString(identitytpl.PopulateStringInput{
|
||||
Mode: identitytpl.JSONTemplating,
|
||||
String: role.Template,
|
||||
Entity: identity.ToSDKEntity(e),
|
||||
Groups: identity.ToSDKGroups(groups),
|
||||
NamespaceID: ns.ID,
|
||||
})
|
||||
if err != nil {
|
||||
i.Logger().Warn("error populating OIDC token template", "template", role.Template, "error", err)
|
||||
}
|
||||
|
||||
payload, err := idToken.generatePayload(i.Logger(), populatedTemplate)
|
||||
if err != nil {
|
||||
i.Logger().Warn("error populating OIDC token template", "error", err)
|
||||
}
|
||||
|
@ -915,7 +922,43 @@ func (i *IdentityStore) pathOIDCGenerateToken(ctx context.Context, req *logical.
|
|||
return retResp, nil
|
||||
}
|
||||
|
||||
func (tok *idToken) generatePayload(logger hclog.Logger, template string, entity *identity.Entity, groups []*identity.Group) ([]byte, error) {
|
||||
func (i *IdentityStore) getNamedKey(ctx context.Context, s logical.Storage, name string) (*namedKey, error) {
|
||||
ns, err := namespace.FromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Attempt to get the key from the cache
|
||||
keyRaw, found, err := i.oidcCache.Get(ns, "namedKeys/"+name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if key, ok := keyRaw.(*namedKey); ok && found {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// Fall back to reading the key from storage
|
||||
entry, err := s.Get(ctx, namedKeyConfigPath+name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entry == nil {
|
||||
return nil, nil
|
||||
}
|
||||
var key namedKey
|
||||
if err := entry.DecodeJSON(&key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Cache the key
|
||||
if err := i.oidcCache.SetDefault(ns, "namedKeys/"+name, &key); err != nil {
|
||||
i.logger.Warn("failed to cache key", "error", err)
|
||||
}
|
||||
|
||||
return &key, nil
|
||||
}
|
||||
|
||||
func (tok *idToken) generatePayload(logger hclog.Logger, templates ...string) ([]byte, error) {
|
||||
output := map[string]interface{}{
|
||||
"iss": tok.Issuer,
|
||||
"namespace": tok.Namespace,
|
||||
|
@ -925,23 +968,41 @@ func (tok *idToken) generatePayload(logger hclog.Logger, template string, entity
|
|||
"iat": tok.IssuedAt,
|
||||
}
|
||||
|
||||
// Parse and integrate the populated role template. Structural errors with the template _should_
|
||||
// be caught during role configuration. Error found during runtime will be logged, but they will
|
||||
// not block generation of the basic ID token. They should not be returned to the requester.
|
||||
_, populatedTemplate, err := identitytpl.PopulateString(identitytpl.PopulateStringInput{
|
||||
Mode: identitytpl.JSONTemplating,
|
||||
String: template,
|
||||
Entity: identity.ToSDKEntity(entity),
|
||||
Groups: identity.ToSDKGroups(groups),
|
||||
// namespace?
|
||||
})
|
||||
if err != nil {
|
||||
logger.Warn("error populating OIDC token template", "template", template, "error", err)
|
||||
if len(tok.Nonce) > 0 {
|
||||
output["nonce"] = tok.Nonce
|
||||
}
|
||||
if tok.AuthTime > 0 {
|
||||
output["auth_time"] = tok.AuthTime
|
||||
}
|
||||
if len(tok.AccessTokenHash) > 0 {
|
||||
output["at_hash"] = tok.AccessTokenHash
|
||||
}
|
||||
if len(tok.CodeHash) > 0 {
|
||||
output["c_hash"] = tok.CodeHash
|
||||
}
|
||||
|
||||
if populatedTemplate != "" {
|
||||
// Merge each of the populated JSON templates into output
|
||||
err := mergeJSONTemplates(logger, output, templates...)
|
||||
if err != nil {
|
||||
logger.Error("failed to populate templates for ID token generation", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(output)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// mergeJSONTemplates will merge each of the given JSON templates into the given
|
||||
// output map. It will simply merge the top-level keys of the unmarshalled JSON
|
||||
// templates into output, which means that any conflicting keys will be overwritten.
|
||||
func mergeJSONTemplates(logger hclog.Logger, output map[string]interface{}, templates ...string) error {
|
||||
for _, template := range templates {
|
||||
var parsed map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(populatedTemplate), &parsed); err != nil {
|
||||
if err := json.Unmarshal([]byte(template), &parsed); err != nil {
|
||||
logger.Warn("error parsing OIDC template", "template", template, "err", err)
|
||||
}
|
||||
|
||||
|
@ -954,12 +1015,7 @@ func (tok *idToken) generatePayload(logger hclog.Logger, template string, entity
|
|||
}
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(output)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return payload, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *namedKey) signPayload(payload []byte) (string, error) {
|
||||
|
@ -1215,10 +1271,10 @@ func (i *IdentityStore) pathOIDCDiscovery(ctx context.Context, req *logical.Requ
|
|||
|
||||
resp := &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
logical.HTTPStatusCode: 200,
|
||||
logical.HTTPRawBody: data,
|
||||
logical.HTTPContentType: "application/json",
|
||||
logical.HTTPRawCacheControl: "max-age=3600",
|
||||
logical.HTTPStatusCode: 200,
|
||||
logical.HTTPRawBody: data,
|
||||
logical.HTTPContentType: "application/json",
|
||||
logical.HTTPCacheControlHeader: "max-age=3600",
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -1312,7 +1368,7 @@ func (i *IdentityStore) pathOIDCReadPublicKeys(ctx context.Context, req *logical
|
|||
}
|
||||
|
||||
if header != "" {
|
||||
resp.Data[logical.HTTPRawCacheControl] = header
|
||||
resp.Data[logical.HTTPCacheControlHeader] = header
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1878,6 +1934,15 @@ func (c *oidcCache) SetDefault(ns *namespace.Namespace, key string, obj interfac
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *oidcCache) Delete(ns *namespace.Namespace, key string) error {
|
||||
if ns == nil {
|
||||
return errNilNamespace
|
||||
}
|
||||
c.c.Delete(c.nskey(ns, key))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *oidcCache) Flush(ns *namespace.Namespace) error {
|
||||
if ns == nil {
|
||||
return errNilNamespace
|
||||
|
|
|
@ -2,10 +2,14 @@ package vault
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
|
@ -15,6 +19,7 @@ import (
|
|||
"github.com/hashicorp/go-memdb"
|
||||
"github.com/hashicorp/go-secure-stdlib/base62"
|
||||
"github.com/hashicorp/go-secure-stdlib/strutil"
|
||||
"github.com/hashicorp/vault/helper/identity"
|
||||
"github.com/hashicorp/vault/helper/namespace"
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/helper/identitytpl"
|
||||
|
@ -24,7 +29,10 @@ import (
|
|||
|
||||
const (
|
||||
// OIDC-related constants
|
||||
openIDScope = "openid"
|
||||
openIDScope = "openid"
|
||||
scopesDelimiter = " "
|
||||
accessTokenScopesMeta = "scopes"
|
||||
accessTokenClientIDMeta = "client_id"
|
||||
|
||||
// Storage path constants
|
||||
oidcProviderPrefix = "oidc_provider/"
|
||||
|
@ -47,6 +55,21 @@ const (
|
|||
ErrAuthRequestNotSupported = "request_not_supported"
|
||||
ErrAuthRequestURINotSupported = "request_uri_not_supported"
|
||||
|
||||
// Error constants used in the Token Endpoint. See details at
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#TokenErrorResponse
|
||||
ErrTokenInvalidRequest = "invalid_request"
|
||||
ErrTokenInvalidClient = "invalid_client"
|
||||
ErrTokenInvalidGrant = "invalid_grant"
|
||||
ErrTokenUnsupportedGrantType = "unsupported_grant_type"
|
||||
ErrTokenServerError = "server_error"
|
||||
|
||||
// Error constants used in the UserInfo Endpoint. See details at
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#UserInfoError
|
||||
ErrUserInfoServerError = "server_error"
|
||||
ErrUserInfoInvalidRequest = "invalid_request"
|
||||
ErrUserInfoInvalidToken = "invalid_token"
|
||||
ErrUserInfoAccessDenied = "access_denied"
|
||||
|
||||
// The following errors are used by the UI for specific behavior of
|
||||
// the OIDC specification. Any changes to their values must come with
|
||||
// a corresponding change in the UI code.
|
||||
|
@ -398,6 +421,62 @@ func oidcProviderPaths(i *IdentityStore) []*framework.Path {
|
|||
HelpSynopsis: "Provides the OIDC Authorization Endpoint.",
|
||||
HelpDescription: "The OIDC Authorization Endpoint performs authentication and authorization by using request parameters defined by OpenID Connect (OIDC).",
|
||||
},
|
||||
{
|
||||
Pattern: "oidc/provider/" + framework.GenericNameRegex("name") + "/token",
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"name": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Name of the provider",
|
||||
},
|
||||
"code": {
|
||||
Type: framework.TypeString,
|
||||
Description: "The authorization code received from the provider's authorization endpoint.",
|
||||
Required: true,
|
||||
},
|
||||
"grant_type": {
|
||||
Type: framework.TypeString,
|
||||
Description: "The authorization grant type. The following grant types are supported: 'authorization_code'.",
|
||||
Required: true,
|
||||
},
|
||||
"redirect_uri": {
|
||||
Type: framework.TypeString,
|
||||
Description: "The callback location where the authentication response was sent.",
|
||||
Required: true,
|
||||
},
|
||||
// The client_id and client_secret are provided to the token endpoint via
|
||||
// the client_secret_basic authentication method, which uses the HTTP Basic
|
||||
// authentication scheme. See the OIDC spec for details at:
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
|
||||
},
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.UpdateOperation: &framework.PathOperation{
|
||||
Callback: i.pathOIDCToken,
|
||||
ForwardPerformanceStandby: true,
|
||||
ForwardPerformanceSecondary: false,
|
||||
},
|
||||
},
|
||||
HelpSynopsis: "Provides the OIDC Token Endpoint.",
|
||||
HelpDescription: "The OIDC Token Endpoint allows a client to exchange its Authorization Grant for an Access Token and ID Token.",
|
||||
},
|
||||
{
|
||||
Pattern: "oidc/provider/" + framework.GenericNameRegex("name") + "/userinfo",
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"name": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Name of the provider",
|
||||
},
|
||||
},
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.ReadOperation: &framework.PathOperation{
|
||||
Callback: i.pathOIDCUserInfo,
|
||||
},
|
||||
logical.UpdateOperation: &framework.PathOperation{
|
||||
Callback: i.pathOIDCUserInfo,
|
||||
},
|
||||
},
|
||||
HelpSynopsis: "Provides the OIDC UserInfo Endpoint.",
|
||||
HelpDescription: "The OIDC UserInfo Endpoint returns claims about the authenticated end-user.",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -733,7 +812,24 @@ func (i *IdentityStore) pathOIDCListScope(ctx context.Context, req *logical.Requ
|
|||
func (i *IdentityStore) pathOIDCReadScope(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
name := d.Get("name").(string)
|
||||
|
||||
entry, err := req.Storage.Get(ctx, scopePath+name)
|
||||
scope, err := i.getOIDCScope(ctx, req.Storage, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if scope == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"template": scope.Template,
|
||||
"description": scope.Description,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (i *IdentityStore) getOIDCScope(ctx context.Context, s logical.Storage, name string) (*scope, error) {
|
||||
entry, err := s.Get(ctx, scopePath+name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -745,12 +841,8 @@ func (i *IdentityStore) pathOIDCReadScope(ctx context.Context, req *logical.Requ
|
|||
if err := entry.DecodeJSON(&scope); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"template": scope.Template,
|
||||
"description": scope.Description,
|
||||
},
|
||||
}, nil
|
||||
|
||||
return &scope, nil
|
||||
}
|
||||
|
||||
// pathOIDCDeleteScope is used to delete an scope
|
||||
|
@ -1052,24 +1144,19 @@ func (i *IdentityStore) pathOIDCCreateUpdateProvider(ctx context.Context, req *l
|
|||
|
||||
scopeTemplateKeyNames := make(map[string]string)
|
||||
for _, scopeName := range provider.Scopes {
|
||||
entry, err := req.Storage.Get(ctx, scopePath+scopeName)
|
||||
scope, err := i.getOIDCScope(ctx, req.Storage, scopeName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// enforce scope existence on provider create and update
|
||||
if entry == nil {
|
||||
if scope == nil {
|
||||
return logical.ErrorResponse("scope %q does not exist", scopeName), nil
|
||||
}
|
||||
|
||||
// ensure no two templates have the same top-level keys
|
||||
var storedScope scope
|
||||
if err := entry.DecodeJSON(&storedScope); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, populatedTemplate, err := identitytpl.PopulateString(identitytpl.PopulateStringInput{
|
||||
Mode: identitytpl.JSONTemplating,
|
||||
String: storedScope.Template,
|
||||
String: scope.Template,
|
||||
Entity: new(logical.Entity),
|
||||
Groups: make([]*logical.Group, 0),
|
||||
})
|
||||
|
@ -1223,10 +1310,10 @@ func (i *IdentityStore) pathOIDCProviderDiscovery(ctx context.Context, req *logi
|
|||
|
||||
resp := &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
logical.HTTPStatusCode: 200,
|
||||
logical.HTTPRawBody: data,
|
||||
logical.HTTPContentType: "application/json",
|
||||
logical.HTTPRawCacheControl: "max-age=3600",
|
||||
logical.HTTPStatusCode: 200,
|
||||
logical.HTTPRawBody: data,
|
||||
logical.HTTPContentType: "application/json",
|
||||
logical.HTTPCacheControlHeader: "max-age=3600",
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -1359,12 +1446,20 @@ func (i *IdentityStore) pathOIDCAuthorize(ctx context.Context, req *logical.Requ
|
|||
}
|
||||
|
||||
// Validate that a scope parameter is present and contains the openid scope value
|
||||
scopes := strutil.ParseStringSlice(d.Get("scope").(string), " ")
|
||||
if len(scopes) == 0 || !strutil.StrListContains(scopes, openIDScope) {
|
||||
requestedScopes := strutil.ParseDedupAndSortStrings(d.Get("scope").(string), scopesDelimiter)
|
||||
if len(requestedScopes) == 0 || !strutil.StrListContains(requestedScopes, openIDScope) {
|
||||
return authResponse("", state, ErrAuthInvalidRequest,
|
||||
fmt.Sprintf("scope parameter must contain the %q value", openIDScope))
|
||||
}
|
||||
|
||||
// Scope values that are not supported by the provider should be ignored
|
||||
scopes := make([]string, 0)
|
||||
for _, scope := range requestedScopes {
|
||||
if strutil.StrListContains(provider.Scopes, scope) && scope != openIDScope {
|
||||
scopes = append(scopes, scope)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the response type
|
||||
responseType := d.Get("response_type").(string)
|
||||
if responseType == "" {
|
||||
|
@ -1426,9 +1521,8 @@ func (i *IdentityStore) pathOIDCAuthorize(ctx context.Context, req *logical.Requ
|
|||
return authResponse("", state, ErrAuthAccessDenied, "identity entity must be associated with the request")
|
||||
}
|
||||
|
||||
// Validate that the identity entity associated with the request
|
||||
// is a member of the client assignments' groups or entities
|
||||
isMember, err := i.entityHasAssignment(ctx, req.Storage, entity.GetID(), client.Assignments)
|
||||
// Validate that the entity is a member of the client's assignments
|
||||
isMember, err := i.entityHasAssignment(ctx, req.Storage, entity, client.Assignments)
|
||||
if err != nil {
|
||||
return authResponse("", state, ErrAuthServerError, err.Error())
|
||||
}
|
||||
|
@ -1516,7 +1610,7 @@ func authResponse(code, state, errorCode, errorDescription string) (*logical.Res
|
|||
}
|
||||
}
|
||||
|
||||
data, err := json.Marshal(response)
|
||||
body, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1524,17 +1618,543 @@ func authResponse(code, state, errorCode, errorDescription string) (*logical.Res
|
|||
return &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
logical.HTTPStatusCode: statusCode,
|
||||
logical.HTTPRawBody: data,
|
||||
logical.HTTPRawBody: body,
|
||||
logical.HTTPContentType: "application/json",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// entityHasAssignment returns true if the entity is a member of any of the
|
||||
// assignments' groups or entities. Otherwise, returns false or an error.
|
||||
func (i *IdentityStore) entityHasAssignment(ctx context.Context, s logical.Storage, entityID string, assignments []string) (bool, error) {
|
||||
func (i *IdentityStore) pathOIDCToken(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
// Get the namespace
|
||||
ns, err := namespace.FromContext(ctx)
|
||||
if err != nil {
|
||||
return tokenResponse(nil, ErrTokenServerError, err.Error())
|
||||
}
|
||||
|
||||
// Get the OIDC provider
|
||||
name := d.Get("name").(string)
|
||||
provider, err := i.getOIDCProvider(ctx, req.Storage, name)
|
||||
if err != nil {
|
||||
return tokenResponse(nil, ErrTokenServerError, err.Error())
|
||||
}
|
||||
if provider == nil {
|
||||
return tokenResponse(nil, ErrTokenInvalidRequest, "provider not found")
|
||||
}
|
||||
|
||||
// Authenticate the client using the client_secret_basic authentication method.
|
||||
// The authentication method uses the HTTP Basic authentication scheme. Details at
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
|
||||
headerReq := &http.Request{Header: req.Headers}
|
||||
clientID, clientSecret, ok := headerReq.BasicAuth()
|
||||
if !ok {
|
||||
return tokenResponse(nil, ErrTokenInvalidRequest, "client failed to authenticate")
|
||||
}
|
||||
client, err := i.clientByID(ctx, req.Storage, clientID)
|
||||
if err != nil {
|
||||
return tokenResponse(nil, ErrTokenServerError, err.Error())
|
||||
}
|
||||
if client == nil {
|
||||
i.Logger().Debug("client failed to authenticate with client not found", "client_id", clientID)
|
||||
return tokenResponse(nil, ErrTokenInvalidClient, "client failed to authenticate")
|
||||
}
|
||||
if subtle.ConstantTimeCompare([]byte(client.ClientSecret), []byte(clientSecret)) == 0 {
|
||||
i.Logger().Debug("client failed to authenticate with invalid client secret", "client_id", clientID)
|
||||
return tokenResponse(nil, ErrTokenInvalidClient, "client failed to authenticate")
|
||||
}
|
||||
|
||||
// Validate that the client is authorized to use the provider
|
||||
if !strutil.StrListContains(provider.AllowedClientIDs, "*") &&
|
||||
!strutil.StrListContains(provider.AllowedClientIDs, clientID) {
|
||||
return tokenResponse(nil, ErrTokenInvalidClient, "client is not authorized to use the provider")
|
||||
}
|
||||
|
||||
// Get the key that the client uses to sign ID tokens
|
||||
key, err := i.getNamedKey(ctx, req.Storage, client.Key)
|
||||
if err != nil {
|
||||
return tokenResponse(nil, ErrTokenServerError, err.Error())
|
||||
}
|
||||
if key == nil {
|
||||
return tokenResponse(nil, ErrTokenServerError, fmt.Sprintf("client key %q not found", client.Key))
|
||||
}
|
||||
|
||||
// Validate that the client is authorized to use the key
|
||||
if !strutil.StrListContains(key.AllowedClientIDs, "*") &&
|
||||
!strutil.StrListContains(key.AllowedClientIDs, clientID) {
|
||||
return tokenResponse(nil, ErrTokenInvalidClient, "client is not authorized to use the key")
|
||||
}
|
||||
|
||||
// Validate the grant type
|
||||
grantType := d.Get("grant_type").(string)
|
||||
if grantType == "" {
|
||||
return tokenResponse(nil, ErrTokenInvalidRequest, "grant_type parameter is required")
|
||||
}
|
||||
if grantType != "authorization_code" {
|
||||
return tokenResponse(nil, ErrTokenUnsupportedGrantType, "unsupported grant_type value")
|
||||
}
|
||||
|
||||
// Validate the authorization code
|
||||
code := d.Get("code").(string)
|
||||
if code == "" {
|
||||
return tokenResponse(nil, ErrTokenInvalidRequest, "code parameter is required")
|
||||
}
|
||||
|
||||
// Get the authorization code entry and defer its deletion (single use)
|
||||
authCodeEntryRaw, ok, err := i.oidcAuthCodeCache.Get(ns, code)
|
||||
defer i.oidcAuthCodeCache.Delete(ns, code)
|
||||
if err != nil {
|
||||
return tokenResponse(nil, ErrTokenServerError, err.Error())
|
||||
}
|
||||
if !ok {
|
||||
return tokenResponse(nil, ErrTokenInvalidGrant, "authorization grant is invalid or expired")
|
||||
}
|
||||
authCodeEntry, ok := authCodeEntryRaw.(*authCodeCacheEntry)
|
||||
if !ok {
|
||||
return tokenResponse(nil, ErrTokenServerError, "authorization grant is invalid or expired")
|
||||
}
|
||||
|
||||
// Ensure the authorization code was issued to the authenticated client
|
||||
if authCodeEntry.clientID != clientID {
|
||||
return tokenResponse(nil, ErrTokenInvalidGrant, "authorization grant is invalid or expired")
|
||||
}
|
||||
|
||||
// Ensure that the redirect_uri parameter value is identical to the redirect_uri
|
||||
// parameter value that was included in the initial authorization request.
|
||||
redirectURI := d.Get("redirect_uri").(string)
|
||||
if redirectURI == "" {
|
||||
return tokenResponse(nil, ErrTokenInvalidRequest, "redirect_uri parameter is required")
|
||||
}
|
||||
if authCodeEntry.redirectURI != redirectURI {
|
||||
return tokenResponse(nil, ErrTokenInvalidGrant, "redirect_uri does not match the redirect_uri used in the authorization request")
|
||||
}
|
||||
|
||||
// Get the entity associated with the initial authorization request
|
||||
entity, err := i.MemDBEntityByID(authCodeEntry.entityID, true)
|
||||
if err != nil {
|
||||
return tokenResponse(nil, ErrTokenServerError, err.Error())
|
||||
}
|
||||
if entity == nil {
|
||||
return tokenResponse(nil, ErrTokenInvalidRequest, "identity entity associated with request not found")
|
||||
}
|
||||
|
||||
// Validate that the entity is a member of the client's assignments
|
||||
isMember, err := i.entityHasAssignment(ctx, req.Storage, entity, client.Assignments)
|
||||
if err != nil {
|
||||
return tokenResponse(nil, ErrTokenServerError, err.Error())
|
||||
}
|
||||
if !isMember {
|
||||
return tokenResponse(nil, ErrTokenInvalidRequest, "identity entity not authorized by client assignment")
|
||||
}
|
||||
|
||||
// The access token is a Vault batch token with a policy that only
|
||||
// provides access to the issuing provider's userinfo endpoint.
|
||||
accessTokenIssuedAt := time.Now()
|
||||
accessTokenExpiry := accessTokenIssuedAt.Add(client.AccessTokenTTL)
|
||||
accessToken := &logical.TokenEntry{
|
||||
Type: logical.TokenTypeBatch,
|
||||
NamespaceID: ns.ID,
|
||||
Path: req.Path,
|
||||
TTL: client.AccessTokenTTL,
|
||||
CreationTime: accessTokenIssuedAt.Unix(),
|
||||
EntityID: entity.ID,
|
||||
NoIdentityPolicies: true,
|
||||
Meta: map[string]string{
|
||||
"oidc_token_type": "access token",
|
||||
},
|
||||
InternalMeta: map[string]string{
|
||||
accessTokenClientIDMeta: client.ClientID,
|
||||
accessTokenScopesMeta: strings.Join(authCodeEntry.scopes, scopesDelimiter),
|
||||
},
|
||||
InlinePolicy: fmt.Sprintf(`
|
||||
path "identity/oidc/provider/%s/userinfo" {
|
||||
capabilities = ["read", "update"]
|
||||
}
|
||||
`, name),
|
||||
}
|
||||
err = i.tokenStorer.CreateToken(ctx, accessToken)
|
||||
if err != nil {
|
||||
return tokenResponse(nil, ErrTokenServerError, err.Error())
|
||||
}
|
||||
|
||||
// Compute the access token hash claim (at_hash)
|
||||
atHash, err := computeHashClaim(key.Algorithm, accessToken.ID)
|
||||
if err != nil {
|
||||
return tokenResponse(nil, ErrTokenServerError, err.Error())
|
||||
}
|
||||
|
||||
// Compute the authorization code hash claim (c_hash)
|
||||
cHash, err := computeHashClaim(key.Algorithm, code)
|
||||
if err != nil {
|
||||
return tokenResponse(nil, ErrTokenServerError, err.Error())
|
||||
}
|
||||
|
||||
// Set the ID token claims
|
||||
idTokenIssuedAt := time.Now()
|
||||
idTokenExpiry := idTokenIssuedAt.Add(client.IDTokenTTL)
|
||||
idToken := idToken{
|
||||
Namespace: ns.ID,
|
||||
Issuer: provider.effectiveIssuer,
|
||||
Subject: authCodeEntry.entityID,
|
||||
Audience: authCodeEntry.clientID,
|
||||
Nonce: authCodeEntry.nonce,
|
||||
Expiry: idTokenExpiry.Unix(),
|
||||
IssuedAt: idTokenIssuedAt.Unix(),
|
||||
AccessTokenHash: atHash,
|
||||
CodeHash: cHash,
|
||||
}
|
||||
|
||||
// Add the auth_time claim if it's not the zero time instant
|
||||
if !authCodeEntry.authTime.IsZero() {
|
||||
idToken.AuthTime = authCodeEntry.authTime.Unix()
|
||||
}
|
||||
|
||||
// Populate each of the requested scope templates
|
||||
templates, conflict, err := i.populateScopeTemplates(ctx, req.Storage, ns, entity, authCodeEntry.scopes...)
|
||||
if !conflict && err != nil {
|
||||
return tokenResponse(nil, ErrTokenServerError, err.Error())
|
||||
}
|
||||
if conflict && err != nil {
|
||||
return tokenResponse(nil, ErrTokenInvalidRequest, err.Error())
|
||||
}
|
||||
|
||||
// Generate the ID token payload
|
||||
payload, err := idToken.generatePayload(i.Logger(), templates...)
|
||||
if err != nil {
|
||||
return tokenResponse(nil, ErrTokenServerError, err.Error())
|
||||
}
|
||||
|
||||
// Sign the ID token using the client's key
|
||||
signedIDToken, err := key.signPayload(payload)
|
||||
if err != nil {
|
||||
return tokenResponse(nil, ErrTokenServerError, err.Error())
|
||||
}
|
||||
|
||||
return tokenResponse(map[string]interface{}{
|
||||
"token_type": "Bearer",
|
||||
"access_token": accessToken.ID,
|
||||
"id_token": signedIDToken,
|
||||
"expires_in": int64(accessTokenExpiry.Sub(accessTokenIssuedAt).Seconds()),
|
||||
}, "", "")
|
||||
}
|
||||
|
||||
// tokenResponse returns the OIDC Token Response. An error response is
|
||||
// returned if the given error code is non-empty. For details, see spec at
|
||||
// - https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse
|
||||
// - https://openid.net/specs/openid-connect-core-1_0.html#TokenErrorResponse
|
||||
func tokenResponse(response map[string]interface{}, errorCode, errorDescription string) (*logical.Response, error) {
|
||||
statusCode := http.StatusOK
|
||||
|
||||
// Set the error response and status code if error code isn't empty
|
||||
if errorCode != "" {
|
||||
switch errorCode {
|
||||
case ErrTokenInvalidClient:
|
||||
statusCode = http.StatusUnauthorized
|
||||
case ErrTokenServerError:
|
||||
statusCode = http.StatusInternalServerError
|
||||
default:
|
||||
statusCode = http.StatusBadRequest
|
||||
}
|
||||
|
||||
response = map[string]interface{}{
|
||||
"error": errorCode,
|
||||
"error_description": errorDescription,
|
||||
}
|
||||
}
|
||||
|
||||
body, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
logical.HTTPStatusCode: statusCode,
|
||||
logical.HTTPRawBody: body,
|
||||
logical.HTTPContentType: "application/json",
|
||||
|
||||
// Token responses must include the following HTTP response headers
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse
|
||||
logical.HTTPCacheControlHeader: "no-store",
|
||||
logical.HTTPPragmaHeader: "no-cache",
|
||||
}
|
||||
|
||||
// Set the WWW-Authenticate response header when returning the
|
||||
// invalid_client error code per the OAuth 2.0 spec at
|
||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
||||
if errorCode == ErrTokenInvalidClient {
|
||||
data[logical.HTTPWWWAuthenticateHeader] = "Basic"
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (i *IdentityStore) pathOIDCUserInfo(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
// Get the namespace
|
||||
ns, err := namespace.FromContext(ctx)
|
||||
if err != nil {
|
||||
return userInfoResponse(nil, ErrUserInfoServerError, err.Error())
|
||||
}
|
||||
|
||||
// Get the OIDC provider
|
||||
name := d.Get("name").(string)
|
||||
provider, err := i.getOIDCProvider(ctx, req.Storage, name)
|
||||
if err != nil {
|
||||
return userInfoResponse(nil, ErrUserInfoServerError, err.Error())
|
||||
}
|
||||
if provider == nil {
|
||||
return userInfoResponse(nil, ErrUserInfoInvalidRequest, "provider not found")
|
||||
}
|
||||
|
||||
// Validate that the access token was sent as a Bearer token
|
||||
if req.ClientTokenSource != logical.ClientTokenFromAuthzHeader {
|
||||
return userInfoResponse(nil, ErrUserInfoInvalidToken, "access token must be sent as a Bearer token")
|
||||
}
|
||||
|
||||
// Look up the access token
|
||||
te, err := i.tokenStorer.LookupToken(ctx, req.ClientToken)
|
||||
if err != nil {
|
||||
return userInfoResponse(nil, ErrUserInfoServerError, err.Error())
|
||||
}
|
||||
if te == nil {
|
||||
return userInfoResponse(nil, ErrUserInfoInvalidToken, "access token is expired")
|
||||
}
|
||||
if te.Type != logical.TokenTypeBatch {
|
||||
return userInfoResponse(nil, ErrUserInfoInvalidToken, "access token is malformed or invalid")
|
||||
}
|
||||
|
||||
// Get the client ID that originated the request from the token metadata
|
||||
clientID, ok := te.InternalMeta[accessTokenClientIDMeta]
|
||||
if !ok {
|
||||
return userInfoResponse(nil, ErrUserInfoServerError, "expected client ID in token metadata")
|
||||
}
|
||||
client, err := i.clientByID(ctx, req.Storage, clientID)
|
||||
if err != nil {
|
||||
return userInfoResponse(nil, ErrUserInfoServerError, err.Error())
|
||||
}
|
||||
if client == nil {
|
||||
return userInfoResponse(nil, ErrUserInfoAccessDenied, "client not found")
|
||||
}
|
||||
|
||||
// Get the entity associated with the request
|
||||
entity, err := i.MemDBEntityByID(req.EntityID, false)
|
||||
if err != nil {
|
||||
return userInfoResponse(nil, ErrUserInfoServerError, err.Error())
|
||||
}
|
||||
if entity == nil {
|
||||
return userInfoResponse(nil, ErrUserInfoAccessDenied, "identity entity must be associated with the request")
|
||||
}
|
||||
|
||||
// Validate that the entity is a member of the client's assignments
|
||||
isMember, err := i.entityHasAssignment(ctx, req.Storage, entity, client.Assignments)
|
||||
if err != nil {
|
||||
return userInfoResponse(nil, ErrUserInfoServerError, err.Error())
|
||||
}
|
||||
if !isMember {
|
||||
return userInfoResponse(nil, ErrUserInfoAccessDenied, "identity entity not authorized by client assignment")
|
||||
}
|
||||
|
||||
claims := map[string]interface{}{
|
||||
// The subject claim must always be in the response
|
||||
"sub": entity.ID,
|
||||
}
|
||||
|
||||
// Get the scopes for the access token
|
||||
scopes, ok := te.InternalMeta[accessTokenScopesMeta]
|
||||
if !ok || len(scopes) == 0 {
|
||||
return userInfoResponse(claims, "", "")
|
||||
}
|
||||
parsedScopes := strutil.ParseStringSlice(scopes, scopesDelimiter)
|
||||
|
||||
// Populate each of the token's scope templates
|
||||
templates, conflict, err := i.populateScopeTemplates(ctx, req.Storage, ns, entity, parsedScopes...)
|
||||
if !conflict && err != nil {
|
||||
return userInfoResponse(nil, ErrUserInfoServerError, err.Error())
|
||||
}
|
||||
if conflict && err != nil {
|
||||
return userInfoResponse(nil, ErrUserInfoInvalidRequest, err.Error())
|
||||
}
|
||||
|
||||
// Merge all of the populated JSON scope templates into claims
|
||||
if err := mergeJSONTemplates(i.Logger(), claims, templates...); err != nil {
|
||||
return userInfoResponse(nil, ErrUserInfoServerError, err.Error())
|
||||
}
|
||||
|
||||
return userInfoResponse(claims, "", "")
|
||||
}
|
||||
|
||||
// userInfoResponse returns the OIDC UserInfo Response. An error response is
|
||||
// returned if the given error code is non-empty. For details, see spec at
|
||||
// - https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
|
||||
// - https://openid.net/specs/openid-connect-core-1_0.html#UserInfoError
|
||||
func userInfoResponse(response map[string]interface{}, errorCode, errorDescription string) (*logical.Response, error) {
|
||||
statusCode := http.StatusOK
|
||||
|
||||
// Set the error response and status code if error code isn't empty
|
||||
if errorCode != "" {
|
||||
switch errorCode {
|
||||
case ErrUserInfoInvalidRequest:
|
||||
statusCode = http.StatusBadRequest
|
||||
case ErrUserInfoInvalidToken:
|
||||
statusCode = http.StatusUnauthorized
|
||||
case ErrUserInfoAccessDenied:
|
||||
statusCode = http.StatusForbidden
|
||||
case ErrUserInfoServerError:
|
||||
statusCode = http.StatusInternalServerError
|
||||
}
|
||||
|
||||
response = map[string]interface{}{
|
||||
"error": errorCode,
|
||||
"error_description": errorDescription,
|
||||
}
|
||||
}
|
||||
|
||||
body, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
logical.HTTPStatusCode: statusCode,
|
||||
logical.HTTPRawBody: body,
|
||||
logical.HTTPContentType: "application/json",
|
||||
}
|
||||
|
||||
// Set the WWW-Authenticate response header when returning error codes
|
||||
// defined in https://datatracker.ietf.org/doc/html/rfc6750#section-3
|
||||
if errorCode == ErrUserInfoInvalidRequest || errorCode == ErrUserInfoInvalidToken {
|
||||
data[logical.HTTPWWWAuthenticateHeader] = fmt.Sprintf("Bearer error=%q,error_description=%q",
|
||||
errorCode, errorDescription)
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getScopeTemplates returns a mapping from scope names to
|
||||
// their templates for each of the given scopes.
|
||||
func (i *IdentityStore) getScopeTemplates(ctx context.Context, s logical.Storage, scopes ...string) (map[string]string, error) {
|
||||
templates := make(map[string]string)
|
||||
for _, name := range scopes {
|
||||
if name == openIDScope {
|
||||
// No template for the openid scope
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the scope template
|
||||
scope, err := i.getOIDCScope(ctx, s, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if scope == nil {
|
||||
// Scope values used that are not understood by an implementation should be ignored.
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
||||
continue
|
||||
}
|
||||
templates[name] = scope.Template
|
||||
}
|
||||
|
||||
return templates, nil
|
||||
}
|
||||
|
||||
// populateScopeTemplates populates the templates for each of the passed scopes.
|
||||
// Returns a slice of the populated JSON template strings and a bool to indicate
|
||||
// if a conflict in scope template claims occurred.
|
||||
func (i *IdentityStore) populateScopeTemplates(ctx context.Context, s logical.Storage, ns *namespace.Namespace, entity *identity.Entity, scopes ...string) ([]string, bool, error) {
|
||||
// Gather the templates for each scope
|
||||
templates, err := i.getScopeTemplates(ctx, s, scopes...)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// Get the groups for the entity
|
||||
groups, inheritedGroups, err := i.groupsByEntityID(entity.ID)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
groups = append(groups, inheritedGroups...)
|
||||
|
||||
claimsToScopes := make(map[string]string)
|
||||
populatedTemplates := make([]string, 0)
|
||||
for scope, template := range templates {
|
||||
// Parse and integrate the populated template. Structural errors with the template
|
||||
// should be caught during configuration. Errors found during runtime will be logged.
|
||||
_, populatedTemplate, err := identitytpl.PopulateString(identitytpl.PopulateStringInput{
|
||||
Mode: identitytpl.JSONTemplating,
|
||||
String: template,
|
||||
Entity: identity.ToSDKEntity(entity),
|
||||
Groups: identity.ToSDKGroups(groups),
|
||||
NamespaceID: ns.ID,
|
||||
})
|
||||
if err != nil {
|
||||
i.Logger().Warn("error populating OIDC token template", "scope", scope,
|
||||
"template", template, "error", err)
|
||||
}
|
||||
|
||||
if populatedTemplate != "" {
|
||||
claimsMap := make(map[string]interface{})
|
||||
if err := json.Unmarshal([]byte(populatedTemplate), &claimsMap); err != nil {
|
||||
i.Logger().Warn("error parsing OIDC template", "template", template, "err", err)
|
||||
}
|
||||
|
||||
// Check top-level claim keys for conflicts with other scopes
|
||||
for claimKey := range claimsMap {
|
||||
if conflictScope, ok := claimsToScopes[claimKey]; ok {
|
||||
return nil, true, fmt.Errorf("found scopes with conflicting top-level claim: claim %q in scopes %q, %q",
|
||||
claimKey, scope, conflictScope)
|
||||
}
|
||||
claimsToScopes[claimKey] = scope
|
||||
}
|
||||
|
||||
populatedTemplates = append(populatedTemplates, populatedTemplate)
|
||||
}
|
||||
}
|
||||
|
||||
return populatedTemplates, false, nil
|
||||
}
|
||||
|
||||
// computeHashClaim computes the hash value to be used for the at_hash
|
||||
// and c_hash claims. For details on how this value is computed and the
|
||||
// class of attacks it's used to prevent, see the spec at
|
||||
// - https://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken
|
||||
// - https://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken
|
||||
// - https://openid.net/specs/openid-connect-core-1_0.html#TokenSubstitution
|
||||
func computeHashClaim(alg string, input string) (string, error) {
|
||||
signatureAlgToHash := map[jose.SignatureAlgorithm]func() hash.Hash{
|
||||
jose.RS256: sha256.New,
|
||||
jose.RS384: sha512.New384,
|
||||
jose.RS512: sha512.New,
|
||||
jose.ES256: sha256.New,
|
||||
jose.ES384: sha512.New384,
|
||||
jose.ES512: sha512.New,
|
||||
|
||||
// We use the Ed25519 curve key for EdDSA, which uses
|
||||
// SHA-512 for its digest algorithm. See details at
|
||||
// https://bitbucket.org/openid/connect/issues/1125.
|
||||
jose.EdDSA: sha512.New,
|
||||
}
|
||||
|
||||
newHash, ok := signatureAlgToHash[jose.SignatureAlgorithm(alg)]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unsupported signature algorithm: %q", alg)
|
||||
}
|
||||
h := newHash()
|
||||
|
||||
// Writing to the hash will never return an error
|
||||
_, _ = h.Write([]byte(input))
|
||||
sum := h.Sum(nil)
|
||||
return base64.RawURLEncoding.EncodeToString(sum[:len(sum)/2]), nil
|
||||
}
|
||||
|
||||
// entityHasAssignment returns true if the entity is enabled and a member of any
|
||||
// of the assignments' groups or entities. Otherwise, returns false or an error.
|
||||
func (i *IdentityStore) entityHasAssignment(ctx context.Context, s logical.Storage, entity *identity.Entity, assignments []string) (bool, error) {
|
||||
if entity.GetDisabled() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Get the group IDs that the entity is a member of
|
||||
entityGroups, err := i.MemDBGroupsByMemberEntityID(entityID, true, false)
|
||||
entityGroups, err := i.MemDBGroupsByMemberEntityID(entity.GetID(), true, false)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
@ -1560,7 +2180,7 @@ func (i *IdentityStore) entityHasAssignment(ctx context.Context, s logical.Stora
|
|||
}
|
||||
|
||||
// Check if the entity is a member of the assignment's entities
|
||||
if strutil.StrListContains(assignment.EntityIDs, entityID) {
|
||||
if strutil.StrListContains(assignment.EntityIDs, entity.GetID()) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -131,7 +131,8 @@ type GroupUpdater interface {
|
|||
var _ GroupUpdater = &Core{}
|
||||
|
||||
type TokenStorer interface {
|
||||
LookupToken(ctx context.Context, token string) (*logical.TokenEntry, error)
|
||||
LookupToken(context.Context, string) (*logical.TokenEntry, error)
|
||||
CreateToken(context.Context, *logical.TokenEntry) error
|
||||
}
|
||||
|
||||
var _ TokenStorer = &Core{}
|
||||
|
|
|
@ -207,9 +207,10 @@ func TestSystemBackend_mounts(t *testing.T) {
|
|||
"accessor": resp.Data["identity/"].(map[string]interface{})["accessor"],
|
||||
"uuid": resp.Data["identity/"].(map[string]interface{})["uuid"],
|
||||
"config": map[string]interface{}{
|
||||
"default_lease_ttl": resp.Data["identity/"].(map[string]interface{})["config"].(map[string]interface{})["default_lease_ttl"].(int64),
|
||||
"max_lease_ttl": resp.Data["identity/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64),
|
||||
"force_no_cache": false,
|
||||
"default_lease_ttl": resp.Data["identity/"].(map[string]interface{})["config"].(map[string]interface{})["default_lease_ttl"].(int64),
|
||||
"max_lease_ttl": resp.Data["identity/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64),
|
||||
"force_no_cache": false,
|
||||
"passthrough_request_headers": []string{"Authorization"},
|
||||
},
|
||||
"local": false,
|
||||
"seal_wrap": false,
|
||||
|
@ -308,9 +309,10 @@ func TestSystemBackend_mount(t *testing.T) {
|
|||
"accessor": resp.Data["identity/"].(map[string]interface{})["accessor"],
|
||||
"uuid": resp.Data["identity/"].(map[string]interface{})["uuid"],
|
||||
"config": map[string]interface{}{
|
||||
"default_lease_ttl": resp.Data["identity/"].(map[string]interface{})["config"].(map[string]interface{})["default_lease_ttl"].(int64),
|
||||
"max_lease_ttl": resp.Data["identity/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64),
|
||||
"force_no_cache": false,
|
||||
"default_lease_ttl": resp.Data["identity/"].(map[string]interface{})["config"].(map[string]interface{})["default_lease_ttl"].(int64),
|
||||
"max_lease_ttl": resp.Data["identity/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64),
|
||||
"force_no_cache": false,
|
||||
"passthrough_request_headers": []string{"Authorization"},
|
||||
},
|
||||
"local": false,
|
||||
"seal_wrap": false,
|
||||
|
@ -2476,9 +2478,10 @@ func TestSystemBackend_InternalUIMounts(t *testing.T) {
|
|||
"accessor": resp.Data["secret"].(map[string]interface{})["identity/"].(map[string]interface{})["accessor"],
|
||||
"uuid": resp.Data["secret"].(map[string]interface{})["identity/"].(map[string]interface{})["uuid"],
|
||||
"config": map[string]interface{}{
|
||||
"default_lease_ttl": resp.Data["secret"].(map[string]interface{})["identity/"].(map[string]interface{})["config"].(map[string]interface{})["default_lease_ttl"].(int64),
|
||||
"max_lease_ttl": resp.Data["secret"].(map[string]interface{})["identity/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64),
|
||||
"force_no_cache": false,
|
||||
"default_lease_ttl": resp.Data["secret"].(map[string]interface{})["identity/"].(map[string]interface{})["config"].(map[string]interface{})["default_lease_ttl"].(int64),
|
||||
"max_lease_ttl": resp.Data["secret"].(map[string]interface{})["identity/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64),
|
||||
"force_no_cache": false,
|
||||
"passthrough_request_headers": []string{"Authorization"},
|
||||
},
|
||||
"local": false,
|
||||
"seal_wrap": false,
|
||||
|
|
|
@ -1516,6 +1516,9 @@ func (c *Core) requiredMountTable() *MountTable {
|
|||
UUID: identityUUID,
|
||||
Accessor: identityAccessor,
|
||||
BackendAwareUUID: identityBackendUUID,
|
||||
Config: MountConfig{
|
||||
PassthroughRequestHeaders: []string{"Authorization"},
|
||||
},
|
||||
}
|
||||
|
||||
table.Entries = append(table.Entries, cubbyholeMount)
|
||||
|
|
|
@ -481,6 +481,15 @@ func (c *Core) LookupToken(ctx context.Context, token string) (*logical.TokenEnt
|
|||
return c.tokenStore.Lookup(ctx, token)
|
||||
}
|
||||
|
||||
// CreateToken creates the given token in the core's token store.
|
||||
func (c *Core) CreateToken(ctx context.Context, entry *logical.TokenEntry) error {
|
||||
if c.tokenStore == nil {
|
||||
return errors.New("unable to create token with nil token store")
|
||||
}
|
||||
|
||||
return c.tokenStore.create(ctx, entry)
|
||||
}
|
||||
|
||||
// TokenStore is used to manage client tokens. Tokens are used for
|
||||
// clients to authenticate, and each token is mapped to an applicable
|
||||
// set of policy which is used for authorization.
|
||||
|
|
Loading…
Reference in New Issue