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:
John-Michael Faircloth 2021-09-07 13:35:23 -05:00 committed by GitHub
parent 2cca67c96f
commit 01011973a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 201 additions and 2 deletions

View File

@ -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) {

View File

@ -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)
}
}