Adds OIDC Authorization Endpoint to OIDC providers (#12538)
This commit is contained in:
parent
b48debda2b
commit
b58913ad9f
|
@ -18,6 +18,7 @@ import (
|
|||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/helper/consts"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -56,6 +57,7 @@ func NewIdentityStore(ctx context.Context, core *Core, config *logical.BackendCo
|
|||
metrics: core.MetricSink(),
|
||||
totpPersister: core,
|
||||
groupUpdater: core,
|
||||
tokenStorer: core,
|
||||
}
|
||||
|
||||
// Create a memdb instance, which by default, operates on lower cased
|
||||
|
@ -96,7 +98,8 @@ func NewIdentityStore(ctx context.Context, core *Core, config *logical.BackendCo
|
|||
},
|
||||
}
|
||||
|
||||
iStore.oidcCache = newOIDCCache()
|
||||
iStore.oidcCache = newOIDCCache(cache.NoExpiration, cache.NoExpiration)
|
||||
iStore.oidcAuthCodeCache = newOIDCCache(5*time.Minute, 5*time.Minute)
|
||||
|
||||
err = iStore.Setup(ctx, config)
|
||||
if err != nil {
|
||||
|
@ -181,6 +184,10 @@ func (i *IdentityStore) Invalidate(ctx context.Context, key string) {
|
|||
i.logger.Error("failed to load groups during invalidation", "error", err)
|
||||
return
|
||||
}
|
||||
if err := i.loadOIDCClients(ctx); err != nil {
|
||||
i.logger.Error("failed to load OIDC clients during invalidation", "error", err)
|
||||
return
|
||||
}
|
||||
// Check if the key is a storage entry key for an entity bucket
|
||||
case strings.HasPrefix(key, storagepacker.StoragePackerBucketsPrefix):
|
||||
// Create a MemDB transaction
|
||||
|
@ -334,6 +341,14 @@ func (i *IdentityStore) Invalidate(ctx context.Context, key string) {
|
|||
if err := i.oidcCache.Flush(ns); err != nil {
|
||||
i.logger.Error("error flushing oidc cache", "error", err)
|
||||
}
|
||||
case strings.HasPrefix(key, clientPath):
|
||||
name := strings.TrimPrefix(key, clientPath)
|
||||
|
||||
// Invalidate the cached client in memdb
|
||||
if err := i.memDBDeleteClientByName(ctx, name); err != nil {
|
||||
i.logger.Error("error invalidating client", "error", err, "key", key)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1797,9 +1797,9 @@ func (i *IdentityStore) oidcPeriodicFunc(ctx context.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
func newOIDCCache() *oidcCache {
|
||||
func newOIDCCache(defaultExpiration, cleanupInterval time.Duration) *oidcCache {
|
||||
return &oidcCache{
|
||||
c: cache.New(cache.NoExpiration, cache.NoExpiration),
|
||||
c: cache.New(defaultExpiration, cleanupInterval),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -5,14 +5,786 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
"github.com/hashicorp/vault/helper/namespace"
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
)
|
||||
|
||||
func TestOIDC_Path_OIDC_Authorize(t *testing.T) {
|
||||
c, _, _ := TestCoreUnsealed(t)
|
||||
ctx := namespace.RootContext(nil)
|
||||
storage := new(logical.InmemStorage)
|
||||
|
||||
// Create a key
|
||||
resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/key/test-key",
|
||||
Operation: logical.CreateOperation,
|
||||
Data: map[string]interface{}{},
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, resp, err)
|
||||
|
||||
// Create an entity
|
||||
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "entity",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"name": "test-entity",
|
||||
},
|
||||
})
|
||||
expectSuccess(t, resp, err)
|
||||
assert.NotNil(t, resp.Data["id"])
|
||||
entityID := resp.Data["id"].(string)
|
||||
|
||||
// Create a group
|
||||
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "group",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"name": "test-group",
|
||||
"member_entity_ids": []string{entityID},
|
||||
},
|
||||
})
|
||||
expectSuccess(t, resp, err)
|
||||
assert.NotNil(t, resp.Data["id"])
|
||||
groupID := resp.Data["id"].(string)
|
||||
|
||||
type args struct {
|
||||
entityID string
|
||||
client client
|
||||
provider provider
|
||||
assignment assignment
|
||||
authorizeRequest *logical.Request
|
||||
tokenCreationTime func() time.Time
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "invalid authorize request with provider not found",
|
||||
args: args{
|
||||
entityID: entityID,
|
||||
assignment: assignment{
|
||||
EntityIDs: []string{entityID},
|
||||
},
|
||||
client: client{
|
||||
RedirectURIs: []string{"https://localhost:8251/callback"},
|
||||
Assignments: []string{"test-assignment"},
|
||||
Key: "test-key",
|
||||
},
|
||||
authorizeRequest: &logical.Request{
|
||||
Path: "oidc/provider/non-existent-provider/authorize",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"client_id": "",
|
||||
"scope": "openid",
|
||||
"redirect_uri": "https://localhost:8251/callback",
|
||||
"response_type": "code",
|
||||
"state": "abcdefg",
|
||||
"nonce": "hijklmn",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: ErrAuthInvalidRequest,
|
||||
},
|
||||
{
|
||||
name: "invalid authorize request with empty scopes",
|
||||
args: args{
|
||||
entityID: entityID,
|
||||
assignment: assignment{
|
||||
EntityIDs: []string{entityID},
|
||||
},
|
||||
client: client{
|
||||
RedirectURIs: []string{"https://localhost:8251/callback"},
|
||||
Assignments: []string{"test-assignment"},
|
||||
Key: "test-key",
|
||||
},
|
||||
authorizeRequest: &logical.Request{
|
||||
Path: "oidc/provider/test-provider/authorize",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"client_id": "",
|
||||
"scope": "",
|
||||
"redirect_uri": "https://localhost:8251/callback",
|
||||
"response_type": "code",
|
||||
"state": "abcdefg",
|
||||
"nonce": "hijklmn",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: ErrAuthInvalidRequest,
|
||||
},
|
||||
{
|
||||
name: "invalid authorize request with missing openid scope",
|
||||
args: args{
|
||||
entityID: entityID,
|
||||
assignment: assignment{
|
||||
EntityIDs: []string{entityID},
|
||||
},
|
||||
client: client{
|
||||
RedirectURIs: []string{"https://localhost:8251/callback"},
|
||||
Assignments: []string{"test-assignment"},
|
||||
Key: "test-key",
|
||||
},
|
||||
authorizeRequest: &logical.Request{
|
||||
Path: "oidc/provider/test-provider/authorize",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"client_id": "",
|
||||
"scope": "groups email profile",
|
||||
"redirect_uri": "https://localhost:8251/callback",
|
||||
"response_type": "code",
|
||||
"state": "abcdefg",
|
||||
"nonce": "hijklmn",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: ErrAuthInvalidRequest,
|
||||
},
|
||||
{
|
||||
name: "invalid authorize request with missing response_type",
|
||||
args: args{
|
||||
entityID: entityID,
|
||||
assignment: assignment{
|
||||
EntityIDs: []string{entityID},
|
||||
},
|
||||
client: client{
|
||||
RedirectURIs: []string{"https://localhost:8251/callback"},
|
||||
Assignments: []string{"test-assignment"},
|
||||
Key: "test-key",
|
||||
},
|
||||
authorizeRequest: &logical.Request{
|
||||
Path: "oidc/provider/test-provider/authorize",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"client_id": "",
|
||||
"scope": "openid",
|
||||
"redirect_uri": "https://localhost:8251/callback",
|
||||
"response_type": "",
|
||||
"state": "abcdefg",
|
||||
"nonce": "hijklmn",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: ErrAuthInvalidRequest,
|
||||
},
|
||||
{
|
||||
name: "invalid authorize request with unsupported response_type",
|
||||
args: args{
|
||||
entityID: entityID,
|
||||
assignment: assignment{
|
||||
EntityIDs: []string{entityID},
|
||||
},
|
||||
client: client{
|
||||
RedirectURIs: []string{"https://localhost:8251/callback"},
|
||||
Assignments: []string{"test-assignment"},
|
||||
Key: "test-key",
|
||||
},
|
||||
authorizeRequest: &logical.Request{
|
||||
Path: "oidc/provider/test-provider/authorize",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"client_id": "",
|
||||
"scope": "openid",
|
||||
"redirect_uri": "https://localhost:8251/callback",
|
||||
"response_type": "id_token",
|
||||
"state": "abcdefg",
|
||||
"nonce": "hijklmn",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: ErrAuthUnsupportedResponseType,
|
||||
},
|
||||
{
|
||||
name: "invalid authorize request with client_id not found",
|
||||
args: args{
|
||||
entityID: entityID,
|
||||
assignment: assignment{
|
||||
EntityIDs: []string{entityID},
|
||||
},
|
||||
client: client{
|
||||
RedirectURIs: []string{"https://localhost:8251/callback"},
|
||||
Assignments: []string{"test-assignment"},
|
||||
Key: "test-key",
|
||||
},
|
||||
authorizeRequest: &logical.Request{
|
||||
Path: "oidc/provider/test-provider/authorize",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"client_id": "non-existent-client-id",
|
||||
"scope": "openid",
|
||||
"redirect_uri": "https://localhost:8251/callback",
|
||||
"response_type": "code",
|
||||
"state": "abcdefg",
|
||||
"nonce": "hijklmn",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: ErrAuthInvalidClientID,
|
||||
},
|
||||
{
|
||||
name: "invalid authorize request with client_id not allowed by provider",
|
||||
args: args{
|
||||
entityID: entityID,
|
||||
assignment: assignment{
|
||||
EntityIDs: []string{entityID},
|
||||
},
|
||||
provider: provider{
|
||||
AllowedClientIDs: []string{"not-client-id"},
|
||||
},
|
||||
client: client{
|
||||
RedirectURIs: []string{"https://localhost:8251/callback"},
|
||||
Assignments: []string{"test-assignment"},
|
||||
Key: "test-key",
|
||||
},
|
||||
authorizeRequest: &logical.Request{
|
||||
Path: "oidc/provider/test-provider/authorize",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"client_id": "",
|
||||
"scope": "openid",
|
||||
"redirect_uri": "https://localhost:8251/callback",
|
||||
"response_type": "code",
|
||||
"state": "abcdefg",
|
||||
"nonce": "hijklmn",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: ErrAuthUnauthorizedClient,
|
||||
},
|
||||
{
|
||||
name: "invalid authorize request with missing redirect_uri",
|
||||
args: args{
|
||||
entityID: entityID,
|
||||
assignment: assignment{
|
||||
EntityIDs: []string{entityID},
|
||||
},
|
||||
client: client{
|
||||
RedirectURIs: []string{"https://localhost:8251/callback"},
|
||||
Assignments: []string{"test-assignment"},
|
||||
Key: "test-key",
|
||||
},
|
||||
authorizeRequest: &logical.Request{
|
||||
Path: "oidc/provider/test-provider/authorize",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"client_id": "",
|
||||
"scope": "openid",
|
||||
"redirect_uri": "",
|
||||
"response_type": "code",
|
||||
"state": "abcdefg",
|
||||
"nonce": "hijklmn",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: ErrAuthInvalidRequest,
|
||||
},
|
||||
{
|
||||
name: "invalid authorize request with redirect_uri not allowed by client",
|
||||
args: args{
|
||||
entityID: entityID,
|
||||
assignment: assignment{
|
||||
EntityIDs: []string{entityID},
|
||||
},
|
||||
client: client{
|
||||
RedirectURIs: []string{"https://not.redirect.uri:8251/callback"},
|
||||
Assignments: []string{"test-assignment"},
|
||||
Key: "test-key",
|
||||
},
|
||||
authorizeRequest: &logical.Request{
|
||||
Path: "oidc/provider/test-provider/authorize",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"client_id": "",
|
||||
"scope": "openid",
|
||||
"redirect_uri": "https://localhost:8251/callback",
|
||||
"response_type": "code",
|
||||
"state": "abcdefg",
|
||||
"nonce": "hijklmn",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: ErrAuthInvalidRedirectURI,
|
||||
},
|
||||
{
|
||||
name: "invalid authorize request with missing state",
|
||||
args: args{
|
||||
entityID: entityID,
|
||||
assignment: assignment{
|
||||
EntityIDs: []string{entityID},
|
||||
},
|
||||
client: client{
|
||||
RedirectURIs: []string{"https://localhost:8251/callback"},
|
||||
Assignments: []string{"test-assignment"},
|
||||
Key: "test-key",
|
||||
},
|
||||
authorizeRequest: &logical.Request{
|
||||
Path: "oidc/provider/test-provider/authorize",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"client_id": "",
|
||||
"scope": "openid",
|
||||
"redirect_uri": "https://localhost:8251/callback",
|
||||
"response_type": "code",
|
||||
"state": "",
|
||||
"nonce": "hijklmn",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: ErrAuthInvalidRequest,
|
||||
},
|
||||
{
|
||||
name: "invalid authorize request with missing nonce",
|
||||
args: args{
|
||||
entityID: entityID,
|
||||
assignment: assignment{
|
||||
EntityIDs: []string{entityID},
|
||||
},
|
||||
client: client{
|
||||
RedirectURIs: []string{"https://localhost:8251/callback"},
|
||||
Assignments: []string{"test-assignment"},
|
||||
Key: "test-key",
|
||||
},
|
||||
authorizeRequest: &logical.Request{
|
||||
Path: "oidc/provider/test-provider/authorize",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"client_id": "",
|
||||
"scope": "openid",
|
||||
"redirect_uri": "https://localhost:8251/callback",
|
||||
"response_type": "code",
|
||||
"state": "abcdefg",
|
||||
"nonce": "",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: ErrAuthInvalidRequest,
|
||||
},
|
||||
{
|
||||
name: "invalid authorize request with request parameter provided",
|
||||
args: args{
|
||||
entityID: entityID,
|
||||
assignment: assignment{
|
||||
EntityIDs: []string{entityID},
|
||||
},
|
||||
client: client{
|
||||
RedirectURIs: []string{"https://localhost:8251/callback"},
|
||||
Assignments: []string{"test-assignment"},
|
||||
Key: "test-key",
|
||||
},
|
||||
authorizeRequest: &logical.Request{
|
||||
Path: "oidc/provider/test-provider/authorize",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"client_id": "",
|
||||
"scope": "openid",
|
||||
"redirect_uri": "https://localhost:8251/callback",
|
||||
"response_type": "code",
|
||||
"state": "abcdefg",
|
||||
"nonce": "hijklmn",
|
||||
"request": "header.payload.signature",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: ErrAuthRequestNotSupported,
|
||||
},
|
||||
{
|
||||
name: "invalid authorize request with request_uri parameter provided",
|
||||
args: args{
|
||||
entityID: entityID,
|
||||
assignment: assignment{
|
||||
EntityIDs: []string{entityID},
|
||||
},
|
||||
client: client{
|
||||
RedirectURIs: []string{"https://localhost:8251/callback"},
|
||||
Assignments: []string{"test-assignment"},
|
||||
Key: "test-key",
|
||||
},
|
||||
authorizeRequest: &logical.Request{
|
||||
Path: "oidc/provider/test-provider/authorize",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"client_id": "",
|
||||
"scope": "openid",
|
||||
"redirect_uri": "https://localhost:8251/callback",
|
||||
"response_type": "code",
|
||||
"state": "abcdefg",
|
||||
"nonce": "hijklmn",
|
||||
"request_uri": "https://client.example.org/request.jwt",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: ErrAuthRequestURINotSupported,
|
||||
},
|
||||
{
|
||||
name: "invalid authorize request with identity entity ID not found",
|
||||
args: args{
|
||||
entityID: "non-existent-entity",
|
||||
assignment: assignment{
|
||||
EntityIDs: []string{entityID},
|
||||
},
|
||||
client: client{
|
||||
RedirectURIs: []string{"https://localhost:8251/callback"},
|
||||
Assignments: []string{"test-assignment"},
|
||||
Key: "test-key",
|
||||
},
|
||||
authorizeRequest: &logical.Request{
|
||||
Path: "oidc/provider/test-provider/authorize",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"client_id": "",
|
||||
"scope": "openid",
|
||||
"redirect_uri": "https://localhost:8251/callback",
|
||||
"response_type": "code",
|
||||
"state": "abcdefg",
|
||||
"nonce": "hijklmn",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: ErrAuthAccessDenied,
|
||||
},
|
||||
{
|
||||
name: "invalid authorize request with entity not found in client assignment",
|
||||
args: args{
|
||||
entityID: entityID,
|
||||
assignment: assignment{
|
||||
EntityIDs: []string{"not-entity-id"},
|
||||
},
|
||||
client: client{
|
||||
RedirectURIs: []string{"https://localhost:8251/callback"},
|
||||
Assignments: []string{"test-assignment"},
|
||||
Key: "test-key",
|
||||
},
|
||||
authorizeRequest: &logical.Request{
|
||||
Path: "oidc/provider/test-provider/authorize",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"client_id": "",
|
||||
"scope": "openid",
|
||||
"redirect_uri": "https://localhost:8251/callback",
|
||||
"response_type": "code",
|
||||
"state": "abcdefg",
|
||||
"nonce": "hijklmn",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: ErrAuthAccessDenied,
|
||||
},
|
||||
{
|
||||
name: "invalid authorize request with group not found in client assignment",
|
||||
args: args{
|
||||
entityID: entityID,
|
||||
assignment: assignment{
|
||||
GroupIDs: []string{"not-group-id"},
|
||||
},
|
||||
client: client{
|
||||
RedirectURIs: []string{"https://localhost:8251/callback"},
|
||||
Assignments: []string{"test-assignment"},
|
||||
Key: "test-key",
|
||||
},
|
||||
authorizeRequest: &logical.Request{
|
||||
Path: "oidc/provider/test-provider/authorize",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"client_id": "",
|
||||
"scope": "openid",
|
||||
"redirect_uri": "https://localhost:8251/callback",
|
||||
"response_type": "code",
|
||||
"state": "abcdefg",
|
||||
"nonce": "hijklmn",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: ErrAuthAccessDenied,
|
||||
},
|
||||
{
|
||||
name: "invalid authorize request with negative max_age",
|
||||
args: args{
|
||||
entityID: entityID,
|
||||
assignment: assignment{
|
||||
EntityIDs: []string{entityID},
|
||||
},
|
||||
client: client{
|
||||
RedirectURIs: []string{"https://localhost:8251/callback"},
|
||||
Assignments: []string{"test-assignment"},
|
||||
Key: "test-key",
|
||||
},
|
||||
authorizeRequest: &logical.Request{
|
||||
Path: "oidc/provider/test-provider/authorize",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"client_id": "",
|
||||
"scope": "openid",
|
||||
"redirect_uri": "https://localhost:8251/callback",
|
||||
"response_type": "code",
|
||||
"state": "abcdefg",
|
||||
"nonce": "hijklmn",
|
||||
"max_age": "-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: ErrAuthInvalidRequest,
|
||||
},
|
||||
{
|
||||
name: "active re-authentication required with token creation time exceeding max_age requirement",
|
||||
args: args{
|
||||
entityID: entityID,
|
||||
assignment: assignment{
|
||||
EntityIDs: []string{entityID},
|
||||
},
|
||||
client: client{
|
||||
RedirectURIs: []string{"https://localhost:8251/callback"},
|
||||
Assignments: []string{"test-assignment"},
|
||||
Key: "test-key",
|
||||
},
|
||||
authorizeRequest: &logical.Request{
|
||||
Path: "oidc/provider/test-provider/authorize",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"client_id": "",
|
||||
"scope": "openid",
|
||||
"redirect_uri": "https://localhost:8251/callback",
|
||||
"response_type": "code",
|
||||
"state": "abcdefg",
|
||||
"nonce": "hijklmn",
|
||||
"max_age": "30",
|
||||
},
|
||||
},
|
||||
tokenCreationTime: func() time.Time {
|
||||
return time.Now().Add(-time.Minute)
|
||||
},
|
||||
},
|
||||
wantErr: ErrAuthMaxAgeReAuthenticate,
|
||||
},
|
||||
{
|
||||
name: "valid authorize request with token creation time within max_age requirement",
|
||||
args: args{
|
||||
entityID: entityID,
|
||||
assignment: assignment{
|
||||
EntityIDs: []string{entityID},
|
||||
},
|
||||
client: client{
|
||||
RedirectURIs: []string{"https://localhost:8251/callback"},
|
||||
Assignments: []string{"test-assignment"},
|
||||
Key: "test-key",
|
||||
},
|
||||
authorizeRequest: &logical.Request{
|
||||
Path: "oidc/provider/test-provider/authorize",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"client_id": "",
|
||||
"scope": "openid",
|
||||
"redirect_uri": "https://localhost:8251/callback",
|
||||
"response_type": "code",
|
||||
"state": "abcdefg",
|
||||
"nonce": "hijklmn",
|
||||
"max_age": "30",
|
||||
},
|
||||
},
|
||||
tokenCreationTime: func() time.Time {
|
||||
return time.Now()
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid authorize request using update operation (HTTP PUT/POST)",
|
||||
args: args{
|
||||
entityID: entityID,
|
||||
assignment: assignment{
|
||||
EntityIDs: []string{entityID},
|
||||
},
|
||||
client: client{
|
||||
RedirectURIs: []string{"https://localhost:8251/callback"},
|
||||
Assignments: []string{"test-assignment"},
|
||||
Key: "test-key",
|
||||
},
|
||||
authorizeRequest: &logical.Request{
|
||||
Path: "oidc/provider/test-provider/authorize",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"client_id": "",
|
||||
"scope": "openid",
|
||||
"redirect_uri": "https://localhost:8251/callback",
|
||||
"response_type": "code",
|
||||
"state": "abcdefg",
|
||||
"nonce": "hijklmn",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid authorize request using read operation (HTTP GET)",
|
||||
args: args{
|
||||
entityID: entityID,
|
||||
assignment: assignment{
|
||||
EntityIDs: []string{entityID},
|
||||
},
|
||||
client: client{
|
||||
RedirectURIs: []string{"https://localhost:8251/callback"},
|
||||
Assignments: []string{"test-assignment"},
|
||||
Key: "test-key",
|
||||
},
|
||||
authorizeRequest: &logical.Request{
|
||||
Path: "oidc/provider/test-provider/authorize",
|
||||
Operation: logical.ReadOperation,
|
||||
Data: map[string]interface{}{
|
||||
"client_id": "",
|
||||
"scope": "openid",
|
||||
"redirect_uri": "https://localhost:8251/callback",
|
||||
"response_type": "code",
|
||||
"state": "abcdefg",
|
||||
"nonce": "hijklmn",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid authorize request using client assignment with group membership",
|
||||
args: args{
|
||||
entityID: entityID,
|
||||
assignment: assignment{
|
||||
GroupIDs: []string{groupID},
|
||||
},
|
||||
client: client{
|
||||
RedirectURIs: []string{"https://localhost:8251/callback"},
|
||||
Assignments: []string{"test-assignment"},
|
||||
Key: "test-key",
|
||||
},
|
||||
authorizeRequest: &logical.Request{
|
||||
Path: "oidc/provider/test-provider/authorize",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"client_id": "",
|
||||
"scope": "openid",
|
||||
"redirect_uri": "https://localhost:8251/callback",
|
||||
"response_type": "code",
|
||||
"state": "abcdefg",
|
||||
"nonce": "hijklmn",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create a token entry and associate with the authorize request
|
||||
creationTime := time.Now()
|
||||
if tt.args.tokenCreationTime != nil {
|
||||
creationTime = tt.args.tokenCreationTime()
|
||||
}
|
||||
te := &logical.TokenEntry{
|
||||
Path: "test",
|
||||
Policies: []string{"default"},
|
||||
TTL: time.Hour * 24,
|
||||
CreationTime: creationTime.Unix(),
|
||||
}
|
||||
testMakeTokenDirectly(t, c.tokenStore, te)
|
||||
assert.NotEmpty(t, te.ID)
|
||||
tt.args.authorizeRequest.ClientToken = te.ID
|
||||
|
||||
// Create an assignment
|
||||
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/assignment/test-assignment",
|
||||
Operation: logical.CreateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"group_ids": tt.args.assignment.GroupIDs,
|
||||
"entity_ids": tt.args.assignment.EntityIDs,
|
||||
},
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, resp, err)
|
||||
|
||||
// Create a client
|
||||
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/client/test-client",
|
||||
Operation: logical.CreateOperation,
|
||||
Storage: storage,
|
||||
Data: map[string]interface{}{
|
||||
"key": "test-key",
|
||||
"redirect_uris": tt.args.client.RedirectURIs,
|
||||
"assignments": tt.args.client.Assignments,
|
||||
"id_token_ttl": tt.args.client.IDTokenTTL,
|
||||
"access_token_ttl": tt.args.client.AccessTokenTTL,
|
||||
},
|
||||
})
|
||||
expectSuccess(t, resp, err)
|
||||
|
||||
// Read the client ID
|
||||
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/client/test-client",
|
||||
Operation: logical.ReadOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, resp, err)
|
||||
assert.NotNil(t, resp.Data["client_id"])
|
||||
clientID := resp.Data["client_id"].(string)
|
||||
|
||||
// Use allowed client IDs if set by test args
|
||||
if len(tt.args.provider.AllowedClientIDs) == 0 {
|
||||
tt.args.provider.AllowedClientIDs = []string{clientID}
|
||||
}
|
||||
|
||||
// Create a provider
|
||||
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/provider/test-provider",
|
||||
Operation: logical.CreateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"issuer": tt.args.provider.Issuer,
|
||||
"allowed_client_ids": tt.args.provider.AllowedClientIDs,
|
||||
"scopes": tt.args.provider.Scopes,
|
||||
},
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, resp, err)
|
||||
|
||||
// Use the client ID if set by test args
|
||||
if len(tt.args.authorizeRequest.Data["client_id"].(string)) == 0 {
|
||||
tt.args.authorizeRequest.Data["client_id"] = clientID
|
||||
}
|
||||
|
||||
// Send the request to the OIDC authorize endpoint
|
||||
tt.args.authorizeRequest.Storage = storage
|
||||
tt.args.authorizeRequest.EntityID = tt.args.entityID
|
||||
resp, err = c.identityStore.HandleRequest(ctx, tt.args.authorizeRequest)
|
||||
|
||||
// Parse the response
|
||||
var res struct {
|
||||
Code string `json:"code"`
|
||||
State string `json:"state"`
|
||||
Error string `json:"error"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
}
|
||||
assert.NotNil(t, resp)
|
||||
assert.NotNil(t, resp.Data[logical.HTTPRawBody])
|
||||
assert.NotNil(t, resp.Data[logical.HTTPContentType])
|
||||
assert.Equal(t, "application/json", resp.Data[logical.HTTPContentType].(string))
|
||||
assert.NoError(t, json.Unmarshal(resp.Data["http_raw_body"].([]byte), &res))
|
||||
|
||||
if tt.wantErr != "" {
|
||||
// Assert that we receive the expected error code
|
||||
assert.Equal(t, tt.wantErr, res.Error)
|
||||
assert.NotEmpty(t, res.ErrorDescription)
|
||||
return
|
||||
}
|
||||
|
||||
// Assert that we receive an authorization code (base62) and state
|
||||
expectSuccess(t, resp, err)
|
||||
assert.Regexp(t, "[a-zA-Z0-9]{32}", res.Code)
|
||||
assert.NotEmpty(t, res.State)
|
||||
assert.Empty(t, res.Error)
|
||||
assert.Empty(t, res.ErrorDescription)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestOIDC_Path_OIDC_ProviderReadPublicKey_ProviderDoesNotExist tests that the
|
||||
// path can handle the read operation when the provider does not exist
|
||||
func TestOIDC_Path_OIDC_ProviderReadPublicKey_ProviderDoesNotExist(t *testing.T) {
|
||||
|
@ -1091,8 +1863,8 @@ func TestOIDC_Path_OIDC_ProviderAssignment(t *testing.T) {
|
|||
})
|
||||
expectSuccess(t, resp, err)
|
||||
expected := map[string]interface{}{
|
||||
"groups": []string{},
|
||||
"entities": []string{},
|
||||
"group_ids": []string{},
|
||||
"entity_ids": []string{},
|
||||
}
|
||||
if diff := deep.Equal(expected, resp.Data); diff != nil {
|
||||
t.Fatal(diff)
|
||||
|
@ -1103,8 +1875,8 @@ func TestOIDC_Path_OIDC_ProviderAssignment(t *testing.T) {
|
|||
Path: "oidc/assignment/test-assignment",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"groups": "my-group",
|
||||
"entities": "my-entity",
|
||||
"group_ids": "my-group",
|
||||
"entity_ids": "my-entity",
|
||||
},
|
||||
Storage: storage,
|
||||
})
|
||||
|
@ -1118,8 +1890,8 @@ func TestOIDC_Path_OIDC_ProviderAssignment(t *testing.T) {
|
|||
})
|
||||
expectSuccess(t, resp, err)
|
||||
expected = map[string]interface{}{
|
||||
"groups": []string{"my-group"},
|
||||
"entities": []string{"my-entity"},
|
||||
"group_ids": []string{"my-group"},
|
||||
"entity_ids": []string{"my-entity"},
|
||||
}
|
||||
if diff := deep.Equal(expected, resp.Data); diff != nil {
|
||||
t.Fatal(diff)
|
||||
|
@ -1203,8 +1975,8 @@ func TestOIDC_Path_OIDC_ProviderAssignment_DeleteWithExistingClient(t *testing.T
|
|||
})
|
||||
expectSuccess(t, resp, err)
|
||||
expected := map[string]interface{}{
|
||||
"groups": []string{},
|
||||
"entities": []string{},
|
||||
"group_ids": []string{},
|
||||
"entity_ids": []string{},
|
||||
}
|
||||
if diff := deep.Equal(expected, resp.Data); diff != nil {
|
||||
t.Fatal(diff)
|
||||
|
@ -1223,8 +1995,8 @@ func TestOIDC_Path_OIDC_ProviderAssignment_Update(t *testing.T) {
|
|||
Operation: logical.CreateOperation,
|
||||
Storage: storage,
|
||||
Data: map[string]interface{}{
|
||||
"groups": "my-group",
|
||||
"entities": "my-entity",
|
||||
"group_ids": "my-group",
|
||||
"entity_ids": "my-entity",
|
||||
},
|
||||
})
|
||||
expectSuccess(t, resp, err)
|
||||
|
@ -1237,8 +2009,8 @@ func TestOIDC_Path_OIDC_ProviderAssignment_Update(t *testing.T) {
|
|||
})
|
||||
expectSuccess(t, resp, err)
|
||||
expected := map[string]interface{}{
|
||||
"groups": []string{"my-group"},
|
||||
"entities": []string{"my-entity"},
|
||||
"group_ids": []string{"my-group"},
|
||||
"entity_ids": []string{"my-entity"},
|
||||
}
|
||||
if diff := deep.Equal(expected, resp.Data); diff != nil {
|
||||
t.Fatal(diff)
|
||||
|
@ -1249,7 +2021,7 @@ func TestOIDC_Path_OIDC_ProviderAssignment_Update(t *testing.T) {
|
|||
Path: "oidc/assignment/test-assignment",
|
||||
Operation: logical.UpdateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"groups": "my-group2",
|
||||
"group_ids": "my-group2",
|
||||
},
|
||||
Storage: storage,
|
||||
})
|
||||
|
@ -1263,8 +2035,8 @@ func TestOIDC_Path_OIDC_ProviderAssignment_Update(t *testing.T) {
|
|||
})
|
||||
expectSuccess(t, resp, err)
|
||||
expected = map[string]interface{}{
|
||||
"groups": []string{"my-group2"},
|
||||
"entities": []string{"my-entity"},
|
||||
"group_ids": []string{"my-group2"},
|
||||
"entity_ids": []string{"my-entity"},
|
||||
}
|
||||
if diff := deep.Equal(expected, resp.Data); diff != nil {
|
||||
t.Fatal(diff)
|
||||
|
|
|
@ -1307,7 +1307,7 @@ func TestOIDC_isTargetNamespacedKey(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestOIDC_Flush(t *testing.T) {
|
||||
c := newOIDCCache()
|
||||
c := newOIDCCache(gocache.NoExpiration, gocache.NoExpiration)
|
||||
ns := []*namespace.Namespace{
|
||||
noNamespace, // ns[0] is nilNamespace
|
||||
{ID: "ns1"},
|
||||
|
@ -1367,7 +1367,7 @@ func TestOIDC_Flush(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestOIDC_CacheNamespaceNilCheck(t *testing.T) {
|
||||
cache := newOIDCCache()
|
||||
cache := newOIDCCache(gocache.NoExpiration, gocache.NoExpiration)
|
||||
|
||||
if _, _, err := cache.Get(nil, "foo"); err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
|
|
|
@ -11,6 +11,7 @@ const (
|
|||
entityAliasesTable = "entity_aliases"
|
||||
groupsTable = "groups"
|
||||
groupAliasesTable = "group_aliases"
|
||||
oidcClientsTable = "oidc_clients"
|
||||
)
|
||||
|
||||
func identityStoreSchema(lowerCaseName bool) *memdb.DBSchema {
|
||||
|
@ -23,6 +24,7 @@ func identityStoreSchema(lowerCaseName bool) *memdb.DBSchema {
|
|||
aliasesTableSchema,
|
||||
groupsTableSchema,
|
||||
groupAliasesTableSchema,
|
||||
oidcClientsTableSchema,
|
||||
}
|
||||
|
||||
for _, schemaFunc := range schemas {
|
||||
|
@ -213,3 +215,38 @@ func groupAliasesTableSchema(lowerCaseName bool) *memdb.TableSchema {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
func oidcClientsTableSchema(_ bool) *memdb.TableSchema {
|
||||
return &memdb.TableSchema{
|
||||
Name: oidcClientsTable,
|
||||
Indexes: map[string]*memdb.IndexSchema{
|
||||
"id": {
|
||||
Name: "id",
|
||||
Unique: true,
|
||||
Indexer: &memdb.StringFieldIndex{
|
||||
Field: "ClientID",
|
||||
},
|
||||
},
|
||||
"name": {
|
||||
Name: "name",
|
||||
Unique: true,
|
||||
Indexer: &memdb.CompoundIndex{
|
||||
Indexes: []memdb.Indexer{
|
||||
&memdb.StringFieldIndex{
|
||||
Field: "NamespaceID",
|
||||
},
|
||||
&memdb.StringFieldIndex{
|
||||
Field: "Name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"namespace_id": {
|
||||
Name: "namespace_id",
|
||||
Indexer: &memdb.StringFieldIndex{
|
||||
Field: "NamespaceID",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,6 +65,10 @@ type IdentityStore struct {
|
|||
// will invalidate the cache.
|
||||
oidcCache *oidcCache
|
||||
|
||||
// oidcAuthCodeCache stores OIDC authorization codes to be exchanged
|
||||
// for an ID token during an authorization code flow.
|
||||
oidcAuthCodeCache *oidcCache
|
||||
|
||||
// logger is the server logger copied over from core
|
||||
logger log.Logger
|
||||
|
||||
|
@ -87,6 +91,7 @@ type IdentityStore struct {
|
|||
metrics metricsutil.Metrics
|
||||
totpPersister TOTPPersister
|
||||
groupUpdater GroupUpdater
|
||||
tokenStorer TokenStorer
|
||||
}
|
||||
|
||||
type groupDiff struct {
|
||||
|
@ -124,3 +129,9 @@ type GroupUpdater interface {
|
|||
}
|
||||
|
||||
var _ GroupUpdater = &Core{}
|
||||
|
||||
type TokenStorer interface {
|
||||
LookupToken(ctx context.Context, token string) (*logical.TokenEntry, error)
|
||||
}
|
||||
|
||||
var _ TokenStorer = &Core{}
|
||||
|
|
|
@ -39,7 +39,11 @@ func (c *Core) loadIdentityStoreArtifacts(ctx context.Context) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.identityStore.loadGroups(ctx)
|
||||
err = c.identityStore.loadGroups(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.identityStore.loadOIDCClients(ctx)
|
||||
}
|
||||
|
||||
if !c.loadCaseSensitiveIdentityStore {
|
||||
|
@ -78,6 +82,39 @@ func (i *IdentityStore) sanitizeName(name string) string {
|
|||
return strings.ToLower(name)
|
||||
}
|
||||
|
||||
func (i *IdentityStore) loadOIDCClients(ctx context.Context) error {
|
||||
i.logger.Debug("identity loading OIDC clients")
|
||||
|
||||
clients, err := i.view.List(ctx, clientPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
txn := i.db.Txn(true)
|
||||
defer txn.Abort()
|
||||
for _, name := range clients {
|
||||
entry, err := i.view.Get(ctx, clientPath+name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if entry == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var client client
|
||||
if err := entry.DecodeJSON(&client); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := i.memDBUpsertClientInTxn(txn, &client); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
txn.Commit()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *IdentityStore) loadGroups(ctx context.Context) error {
|
||||
i.logger.Debug("identity loading groups")
|
||||
existing, err := i.groupPacker.View().List(ctx, groupBucketsPrefix)
|
||||
|
|
|
@ -3306,15 +3306,15 @@ func TestHandlePoliciesPasswordGenerate(t *testing.T) {
|
|||
t.Fatalf("no error expected, got: %s", err)
|
||||
}
|
||||
|
||||
assert(t, actualResp != nil, "response is nil")
|
||||
assert(t, actualResp.Data != nil, "expected data, got nil")
|
||||
assertTrue(t, actualResp != nil, "response is nil")
|
||||
assertTrue(t, actualResp.Data != nil, "expected data, got nil")
|
||||
assertHasKey(t, actualResp.Data, "password", "password key not found in data")
|
||||
assertIsString(t, actualResp.Data["password"], "password key should have a string value")
|
||||
password := actualResp.Data["password"].(string)
|
||||
|
||||
// Delete the password so the rest of the response can be compared
|
||||
delete(actualResp.Data, "password")
|
||||
assert(t, reflect.DeepEqual(actualResp, expectedResp), "Actual response: %#v\nExpected response: %#v", actualResp, expectedResp)
|
||||
assertTrue(t, reflect.DeepEqual(actualResp, expectedResp), "Actual response: %#v\nExpected response: %#v", actualResp, expectedResp)
|
||||
|
||||
// Check to make sure the password is correctly formatted
|
||||
passwordLength := len([]rune(password))
|
||||
|
@ -3331,7 +3331,7 @@ func TestHandlePoliciesPasswordGenerate(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func assert(t *testing.T, pass bool, f string, vals ...interface{}) {
|
||||
func assertTrue(t *testing.T, pass bool, f string, vals ...interface{}) {
|
||||
t.Helper()
|
||||
if !pass {
|
||||
t.Fatalf(f, vals...)
|
||||
|
|
|
@ -150,6 +150,11 @@ path "sys/tools/hash/*" {
|
|||
path "sys/control-group/request" {
|
||||
capabilities = ["update"]
|
||||
}
|
||||
|
||||
# Allow a token to make requests to the Authorization Endpoint for OIDC providers.
|
||||
path "identity/oidc/provider/+/authorize" {
|
||||
capabilities = ["read", "update"]
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
|
|
|
@ -563,6 +563,7 @@ func (r *Router) routeCommon(ctx context.Context, req *logical.Request, existenc
|
|||
switch {
|
||||
case strings.HasPrefix(originalPath, "auth/token/"):
|
||||
case strings.HasPrefix(originalPath, "sys/"):
|
||||
case strings.HasPrefix(originalPath, "identity/"):
|
||||
case strings.HasPrefix(originalPath, cubbyholeMountPath):
|
||||
if req.Operation == logical.RollbackOperation {
|
||||
// Backend doesn't support this and it can't properly look up a
|
||||
|
|
Loading…
Reference in New Issue