feature: OIDC discovery endpoint (#12481)
* OIDC Provider: implement discovery endpoint * handle case when provider does not exist * refactor providerDiscover struct and add scopes_supported * fix authz endpoint
This commit is contained in:
parent
2cca67c96f
commit
01011973a3
|
@ -48,6 +48,18 @@ type provider struct {
|
|||
effectiveIssuer string
|
||||
}
|
||||
|
||||
type providerDiscovery struct {
|
||||
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
||||
IDTokenAlgs []string `json:"id_token_signing_alg_values_supported"`
|
||||
Issuer string `json:"issuer"`
|
||||
Keys string `json:"jwks_uri"`
|
||||
ResponseTypes []string `json:"response_types_supported"`
|
||||
Scopes []string `json:"scopes_supported"`
|
||||
Subjects []string `json:"subject_types_supported"`
|
||||
TokenEndpoint string `json:"token_endpoint"`
|
||||
UserinfoEndpoint string `json:"userinfo_endpoint"`
|
||||
}
|
||||
|
||||
const (
|
||||
oidcProviderPrefix = "oidc_provider/"
|
||||
assignmentPath = oidcProviderPrefix + "assignment/"
|
||||
|
@ -208,7 +220,7 @@ func oidcProviderPaths(i *IdentityStore) []*framework.Path {
|
|||
Fields: map[string]*framework.FieldSchema{
|
||||
"name": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Name of the assignment",
|
||||
Description: "Name of the provider",
|
||||
},
|
||||
"issuer": {
|
||||
Type: framework.TypeString,
|
||||
|
@ -251,9 +263,66 @@ func oidcProviderPaths(i *IdentityStore) []*framework.Path {
|
|||
HelpSynopsis: "List OIDC providers",
|
||||
HelpDescription: "List all configured OIDC providers in the identity backend.",
|
||||
},
|
||||
{
|
||||
Pattern: "oidc/provider/" + framework.GenericNameRegex("name") + "/.well-known/openid-configuration",
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"name": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Name of the provider",
|
||||
},
|
||||
},
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.ReadOperation: i.pathOIDCProviderDiscovery,
|
||||
},
|
||||
HelpSynopsis: "Query OIDC configurations",
|
||||
HelpDescription: "Query this path to retrieve the configured OIDC Issuer and Keys endpoints, response types, subject types, and signing algorithms used by the OIDC backend.",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (i *IdentityStore) pathOIDCProviderDiscovery(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
name := d.Get("name").(string)
|
||||
|
||||
p, err := i.getOIDCProvider(ctx, req.Storage, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// the "openid" scope is reserved and is included for every provider
|
||||
scopes := append(p.Scopes, "openid")
|
||||
|
||||
disc := providerDiscovery{
|
||||
AuthorizationEndpoint: strings.Replace(p.effectiveIssuer, "/v1/", "/ui/vault/", 1) + "/authorize",
|
||||
IDTokenAlgs: supportedAlgs,
|
||||
Issuer: p.effectiveIssuer,
|
||||
Keys: p.effectiveIssuer + "/.well-known/keys",
|
||||
ResponseTypes: []string{"code"},
|
||||
Scopes: scopes,
|
||||
Subjects: []string{"public"},
|
||||
TokenEndpoint: p.effectiveIssuer + "/token",
|
||||
UserinfoEndpoint: p.effectiveIssuer + "/userinfo",
|
||||
}
|
||||
|
||||
data, err := json.Marshal(disc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
logical.HTTPStatusCode: 200,
|
||||
logical.HTTPRawBody: data,
|
||||
logical.HTTPContentType: "application/json",
|
||||
logical.HTTPRawCacheControl: "max-age=3600",
|
||||
},
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// clientsReferencingTargetAssignmentName returns a map of client names to
|
||||
// clients referencing targetAssignmentName.
|
||||
func (i *IdentityStore) clientsReferencingTargetAssignmentName(ctx context.Context, req *logical.Request, targetAssignmentName string) (map[string]client, error) {
|
||||
|
|
|
@ -2,6 +2,7 @@ package vault
|
|||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
|
@ -891,7 +892,7 @@ func TestOIDC_Path_OIDC_ProviderScope_DeleteWithExistingProvider(t *testing.T) {
|
|||
expectSuccess(t, resp, err)
|
||||
|
||||
// Create a test provider "test-provider"
|
||||
c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/provider/test-provider",
|
||||
Operation: logical.CreateOperation,
|
||||
Data: map[string]interface{}{
|
||||
|
@ -1614,3 +1615,132 @@ func TestOIDC_Path_OIDC_Provider_List(t *testing.T) {
|
|||
delete(expectedStrings, "test-provider2")
|
||||
expectStrings(t, respListProvidersAfterDelete.Data["keys"].([]string), expectedStrings)
|
||||
}
|
||||
|
||||
// TestOIDC_Path_OpenIDProviderConfig tests read operations for the
|
||||
// openid-configuration path
|
||||
func TestOIDC_Path_OpenIDProviderConfig(t *testing.T) {
|
||||
c, _, _ := TestCoreUnsealed(t)
|
||||
ctx := namespace.RootContext(nil)
|
||||
storage := &logical.InmemStorage{}
|
||||
|
||||
// Create a test scope "test-scope-1" -- should succeed
|
||||
resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/scope/test-scope-1",
|
||||
Operation: logical.CreateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"template": `{"groups": "{{identity.entity.groups.names}}"}`,
|
||||
"description": "my-description",
|
||||
},
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, resp, err)
|
||||
|
||||
// Create a test provider "test-provider"
|
||||
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/provider/test-provider",
|
||||
Operation: logical.CreateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"scopes": []string{"test-scope-1"},
|
||||
},
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, resp, err)
|
||||
|
||||
// Expect defaults from .well-known/openid-configuration
|
||||
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/provider/test-provider/.well-known/openid-configuration",
|
||||
Operation: logical.ReadOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, resp, err)
|
||||
|
||||
basePath := "/v1/identity/oidc/provider/test-provider"
|
||||
expected := &providerDiscovery{
|
||||
Issuer: basePath,
|
||||
Keys: basePath + "/.well-known/keys",
|
||||
ResponseTypes: []string{"code"},
|
||||
Scopes: []string{"test-scope-1", "openid"},
|
||||
Subjects: []string{"public"},
|
||||
IDTokenAlgs: supportedAlgs,
|
||||
AuthorizationEndpoint: "/ui/vault/identity/oidc/provider/test-provider/authorize",
|
||||
TokenEndpoint: basePath + "/token",
|
||||
UserinfoEndpoint: basePath + "/userinfo",
|
||||
}
|
||||
discoveryResp := &providerDiscovery{}
|
||||
json.Unmarshal(resp.Data["http_raw_body"].([]byte), discoveryResp)
|
||||
if diff := deep.Equal(expected, discoveryResp); diff != nil {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
|
||||
// Create a test scope "test-scope-2" -- should succeed
|
||||
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/scope/test-scope-2",
|
||||
Operation: logical.CreateOperation,
|
||||
Data: map[string]interface{}{
|
||||
"template": `{"groups": "{{identity.entity.groups.names}}"}`,
|
||||
"description": "my-description",
|
||||
},
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, resp, err)
|
||||
|
||||
// Update provider issuer config
|
||||
testIssuer := "https://example.com:1234"
|
||||
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/provider/test-provider",
|
||||
Operation: logical.UpdateOperation,
|
||||
Storage: storage,
|
||||
Data: map[string]interface{}{
|
||||
"issuer": testIssuer,
|
||||
"scopes": []string{"test-scope-2"},
|
||||
},
|
||||
})
|
||||
expectSuccess(t, resp, err)
|
||||
|
||||
// Expect updates from .well-known/openid-configuration
|
||||
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/provider/test-provider/.well-known/openid-configuration",
|
||||
Operation: logical.ReadOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
expectSuccess(t, resp, err)
|
||||
// Validate
|
||||
basePath = testIssuer + basePath
|
||||
expected = &providerDiscovery{
|
||||
Issuer: basePath,
|
||||
Keys: basePath + "/.well-known/keys",
|
||||
ResponseTypes: []string{"code"},
|
||||
Scopes: []string{"test-scope-2", "openid"},
|
||||
Subjects: []string{"public"},
|
||||
IDTokenAlgs: supportedAlgs,
|
||||
AuthorizationEndpoint: testIssuer + "/ui/vault/identity/oidc/provider/test-provider/authorize",
|
||||
TokenEndpoint: basePath + "/token",
|
||||
UserinfoEndpoint: basePath + "/userinfo",
|
||||
}
|
||||
discoveryResp = &providerDiscovery{}
|
||||
json.Unmarshal(resp.Data["http_raw_body"].([]byte), discoveryResp)
|
||||
if diff := deep.Equal(expected, discoveryResp); diff != nil {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOIDC_Path_OpenIDProviderConfig_ProviderDoesNotExist tests read
|
||||
// operations for the openid-configuration path when the provider does not
|
||||
// exist
|
||||
func TestOIDC_Path_OpenIDProviderConfig_ProviderDoesNotExist(t *testing.T) {
|
||||
c, _, _ := TestCoreUnsealed(t)
|
||||
ctx := namespace.RootContext(nil)
|
||||
storage := &logical.InmemStorage{}
|
||||
|
||||
// Expect defaults from .well-known/openid-configuration
|
||||
// test-provider does not exist
|
||||
resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||
Path: "oidc/provider/test-provider/.well-known/openid-configuration",
|
||||
Operation: logical.ReadOperation,
|
||||
Storage: storage,
|
||||
})
|
||||
expectedResp := &logical.Response{}
|
||||
if resp != expectedResp && err != nil {
|
||||
t.Fatalf("expected empty response but got success; error:\n%v\nresp: %#v", err, resp)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue