Skip to content

Commit

Permalink
feature: OIDC discovery endpoint (hashicorp#12481)
Browse files Browse the repository at this point in the history
* OIDC Provider: implement discovery endpoint

* handle case when provider does not exist

* refactor providerDiscover struct and add scopes_supported

* fix authz endpoint
  • Loading branch information
fairclothjm authored and jartek committed Sep 11, 2021
1 parent 47d250c commit d57e8e9
Show file tree
Hide file tree
Showing 2 changed files with 201 additions and 2 deletions.
71 changes: 70 additions & 1 deletion vault/identity_store_oidc_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -251,7 +263,64 @@ 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
Expand Down
132 changes: 131 additions & 1 deletion vault/identity_store_oidc_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package vault

import (
"encoding/base64"
"encoding/json"
"fmt"
"testing"

Expand Down Expand Up @@ -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{}{
Expand Down Expand Up @@ -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)
}
}

0 comments on commit d57e8e9

Please sign in to comment.