2021-08-17 20:55:06 +00:00
package vault
import (
"context"
2021-08-18 18:20:27 +00:00
"encoding/base64"
"encoding/json"
2021-08-23 13:42:31 +00:00
"fmt"
2021-08-26 17:13:51 +00:00
"net/url"
2021-08-23 13:42:31 +00:00
"sort"
2021-08-18 18:20:27 +00:00
"strings"
2021-09-27 17:05:28 +00:00
"time"
2021-08-17 20:55:06 +00:00
2021-08-23 13:42:31 +00:00
"github.com/hashicorp/go-secure-stdlib/base62"
2021-08-18 18:20:27 +00:00
"github.com/hashicorp/go-secure-stdlib/strutil"
2021-08-26 17:13:51 +00:00
"github.com/hashicorp/vault/helper/namespace"
2021-08-17 20:55:06 +00:00
"github.com/hashicorp/vault/sdk/framework"
2021-08-18 18:20:27 +00:00
"github.com/hashicorp/vault/sdk/helper/identitytpl"
2021-08-17 20:55:06 +00:00
"github.com/hashicorp/vault/sdk/logical"
2021-09-14 20:37:53 +00:00
"gopkg.in/square/go-jose.v2"
2021-08-17 20:55:06 +00:00
)
type assignment struct {
Groups [ ] string ` json:"groups" `
Entities [ ] string ` json:"entities" `
}
2021-08-18 18:20:27 +00:00
type scope struct {
Template string ` json:"template" `
Description string ` json:"description" `
}
2021-08-23 13:42:31 +00:00
type client struct {
2021-09-27 17:05:28 +00:00
RedirectURIs [ ] string ` json:"redirect_uris" `
Assignments [ ] string ` json:"assignments" `
Key string ` json:"key" `
IDTokenTTL time . Duration ` json:"id_token_ttl" `
AccessTokenTTL time . Duration ` json:"access_token_ttl" `
2021-08-23 13:42:31 +00:00
// used for OIDC endpoints
ClientID string ` json:"client_id" `
ClientSecret string ` json:"client_secret" `
}
2021-08-26 17:13:51 +00:00
type provider struct {
Issuer string ` json:"issuer" `
AllowedClientIDs [ ] string ` json:"allowed_client_ids" `
Scopes [ ] string ` json:"scopes" `
// effectiveIssuer is a calculated field and will be either Issuer (if
// that's set) or the Vault instance's api_addr.
effectiveIssuer string
}
2021-09-07 18:35:23 +00:00
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" `
}
2021-08-17 20:55:06 +00:00
const (
oidcProviderPrefix = "oidc_provider/"
assignmentPath = oidcProviderPrefix + "assignment/"
2021-08-18 18:20:27 +00:00
scopePath = oidcProviderPrefix + "scope/"
2021-08-23 13:42:31 +00:00
clientPath = oidcProviderPrefix + "client/"
2021-08-26 17:13:51 +00:00
providerPath = oidcProviderPrefix + "provider/"
2021-08-17 20:55:06 +00:00
)
func oidcProviderPaths ( i * IdentityStore ) [ ] * framework . Path {
return [ ] * framework . Path {
{
Pattern : "oidc/assignment/" + framework . GenericNameRegex ( "name" ) ,
Fields : map [ string ] * framework . FieldSchema {
"name" : {
Type : framework . TypeString ,
Description : "Name of the assignment" ,
} ,
"entities" : {
Type : framework . TypeCommaStringSlice ,
Description : "Comma separated string or array of identity entity names" ,
} ,
"groups" : {
Type : framework . TypeCommaStringSlice ,
Description : "Comma separated string or array of identity group names" ,
} ,
} ,
Operations : map [ logical . Operation ] framework . OperationHandler {
logical . UpdateOperation : & framework . PathOperation {
Callback : i . pathOIDCCreateUpdateAssignment ,
} ,
logical . CreateOperation : & framework . PathOperation {
Callback : i . pathOIDCCreateUpdateAssignment ,
} ,
logical . ReadOperation : & framework . PathOperation {
Callback : i . pathOIDCReadAssignment ,
} ,
logical . DeleteOperation : & framework . PathOperation {
Callback : i . pathOIDCDeleteAssignment ,
} ,
} ,
ExistenceCheck : i . pathOIDCAssignmentExistenceCheck ,
HelpSynopsis : "CRUD operations for OIDC assignments." ,
HelpDescription : "Create, Read, Update, and Delete OIDC assignments." ,
} ,
{
Pattern : "oidc/assignment/?$" ,
Operations : map [ logical . Operation ] framework . OperationHandler {
logical . ListOperation : & framework . PathOperation {
Callback : i . pathOIDCListAssignment ,
} ,
} ,
HelpSynopsis : "List OIDC assignments" ,
HelpDescription : "List all configured OIDC assignments in the identity backend." ,
} ,
2021-08-18 18:20:27 +00:00
{
Pattern : "oidc/scope/" + framework . GenericNameRegex ( "name" ) ,
Fields : map [ string ] * framework . FieldSchema {
"name" : {
Type : framework . TypeString ,
Description : "Name of the scope" ,
} ,
"template" : {
Type : framework . TypeString ,
Description : "The template string to use for the scope. This may be in string-ified JSON or base64 format." ,
} ,
"description" : {
Type : framework . TypeString ,
Description : "The description of the scope" ,
} ,
} ,
Operations : map [ logical . Operation ] framework . OperationHandler {
logical . UpdateOperation : & framework . PathOperation {
Callback : i . pathOIDCCreateUpdateScope ,
} ,
logical . CreateOperation : & framework . PathOperation {
Callback : i . pathOIDCCreateUpdateScope ,
} ,
logical . ReadOperation : & framework . PathOperation {
Callback : i . pathOIDCReadScope ,
} ,
logical . DeleteOperation : & framework . PathOperation {
Callback : i . pathOIDCDeleteScope ,
} ,
} ,
ExistenceCheck : i . pathOIDCScopeExistenceCheck ,
HelpSynopsis : "CRUD operations for OIDC scopes." ,
HelpDescription : "Create, Read, Update, and Delete OIDC scopes." ,
} ,
{
Pattern : "oidc/scope/?$" ,
Operations : map [ logical . Operation ] framework . OperationHandler {
logical . ListOperation : & framework . PathOperation {
Callback : i . pathOIDCListScope ,
} ,
} ,
HelpSynopsis : "List OIDC scopes" ,
HelpDescription : "List all configured OIDC scopes in the identity backend." ,
} ,
2021-08-23 13:42:31 +00:00
{
Pattern : "oidc/client/" + framework . GenericNameRegex ( "name" ) ,
Fields : map [ string ] * framework . FieldSchema {
"name" : {
Type : framework . TypeString ,
Description : "Name of the client." ,
} ,
"redirect_uris" : {
Type : framework . TypeCommaStringSlice ,
Description : "Comma separated string or array of redirect URIs used by the client. One of these values must exactly match the redirect_uri parameter value used in each authentication request." ,
} ,
"assignments" : {
Type : framework . TypeCommaStringSlice ,
Description : "Comma separated string or array of assignment resources." ,
} ,
"key" : {
Type : framework . TypeString ,
Description : "A reference to a named key resource. Cannot be modified after creation." ,
Required : true ,
} ,
"id_token_ttl" : {
Type : framework . TypeDurationSecond ,
Description : "The time-to-live for ID tokens obtained by the client." ,
2021-09-27 17:05:28 +00:00
Default : "24h" ,
2021-08-23 13:42:31 +00:00
} ,
"access_token_ttl" : {
Type : framework . TypeDurationSecond ,
Description : "The time-to-live for access tokens obtained by the client." ,
2021-09-27 17:05:28 +00:00
Default : "24h" ,
2021-08-23 13:42:31 +00:00
} ,
} ,
Operations : map [ logical . Operation ] framework . OperationHandler {
logical . UpdateOperation : & framework . PathOperation {
Callback : i . pathOIDCCreateUpdateClient ,
} ,
logical . CreateOperation : & framework . PathOperation {
Callback : i . pathOIDCCreateUpdateClient ,
} ,
logical . ReadOperation : & framework . PathOperation {
Callback : i . pathOIDCReadClient ,
} ,
logical . DeleteOperation : & framework . PathOperation {
Callback : i . pathOIDCDeleteClient ,
} ,
} ,
ExistenceCheck : i . pathOIDCClientExistenceCheck ,
HelpSynopsis : "CRUD operations for OIDC clients." ,
HelpDescription : "Create, Read, Update, and Delete OIDC clients." ,
} ,
{
Pattern : "oidc/client/?$" ,
Operations : map [ logical . Operation ] framework . OperationHandler {
logical . ListOperation : & framework . PathOperation {
Callback : i . pathOIDCListClient ,
} ,
} ,
HelpSynopsis : "List OIDC clients" ,
HelpDescription : "List all configured OIDC clients in the identity backend." ,
} ,
2021-08-26 17:13:51 +00:00
{
Pattern : "oidc/provider/" + framework . GenericNameRegex ( "name" ) ,
Fields : map [ string ] * framework . FieldSchema {
"name" : {
Type : framework . TypeString ,
2021-09-07 18:35:23 +00:00
Description : "Name of the provider" ,
2021-08-26 17:13:51 +00:00
} ,
"issuer" : {
Type : framework . TypeString ,
Description : "Specifies what will be used for the iss claim of ID tokens." ,
} ,
"allowed_client_ids" : {
Type : framework . TypeCommaStringSlice ,
Description : "The client IDs that are permitted to use the provider" ,
} ,
"scopes" : {
Type : framework . TypeCommaStringSlice ,
Description : "The scopes available for requesting on the provider" ,
} ,
} ,
Operations : map [ logical . Operation ] framework . OperationHandler {
logical . UpdateOperation : & framework . PathOperation {
Callback : i . pathOIDCCreateUpdateProvider ,
} ,
logical . CreateOperation : & framework . PathOperation {
Callback : i . pathOIDCCreateUpdateProvider ,
} ,
logical . ReadOperation : & framework . PathOperation {
Callback : i . pathOIDCReadProvider ,
} ,
logical . DeleteOperation : & framework . PathOperation {
Callback : i . pathOIDCDeleteProvider ,
} ,
} ,
ExistenceCheck : i . pathOIDCProviderExistenceCheck ,
HelpSynopsis : "CRUD operations for OIDC providers." ,
HelpDescription : "Create, Read, Update, and Delete OIDC named providers." ,
} ,
{
Pattern : "oidc/provider/?$" ,
Operations : map [ logical . Operation ] framework . OperationHandler {
logical . ListOperation : & framework . PathOperation {
Callback : i . pathOIDCListProvider ,
} ,
} ,
HelpSynopsis : "List OIDC providers" ,
HelpDescription : "List all configured OIDC providers in the identity backend." ,
} ,
2021-09-07 18:35:23 +00:00
{
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." ,
} ,
2021-09-14 20:37:53 +00:00
{
Pattern : "oidc/provider/" + framework . GenericNameRegex ( "name" ) + "/.well-known/keys" ,
Fields : map [ string ] * framework . FieldSchema {
"name" : {
Type : framework . TypeString ,
Description : "Name of the provider" ,
} ,
} ,
Callbacks : map [ logical . Operation ] framework . OperationFunc {
logical . ReadOperation : i . pathOIDCReadProviderPublicKeys ,
} ,
HelpSynopsis : "Retrieve public keys" ,
HelpDescription : "Returns the public portion of keys for a named OIDC provider. Clients can use them to validate the authenticity of an ID token." ,
} ,
}
}
func ( i * IdentityStore ) listClients ( ctx context . Context , s logical . Storage ) ( [ ] * client , error ) {
clientNames , err := s . List ( ctx , clientPath )
if err != nil {
return nil , err
2021-08-23 13:42:31 +00:00
}
2021-09-14 20:37:53 +00:00
var clients [ ] * client
for _ , name := range clientNames {
entry , err := s . Get ( ctx , clientPath + name )
if err != nil {
return nil , err
}
if entry == nil {
continue
}
var client client
if err := entry . DecodeJSON ( & client ) ; err != nil {
return nil , err
}
clients = append ( clients , & client )
}
return clients , nil
}
// TODO: load clients into memory (go-memdb) to look this up
func ( i * IdentityStore ) clientByID ( ctx context . Context , s logical . Storage , id string ) ( * client , error ) {
clients , err := i . listClients ( ctx , s )
if err != nil {
return nil , err
}
for _ , client := range clients {
if client . ClientID == id {
return client , nil
}
}
return nil , nil
}
// keyIDsReferencedByTargetClientIDs returns a slice of key IDs that are
// referenced by the clients' targetIDs.
// If targetIDs contains "*" then the IDs for all public keys are returned.
func ( i * IdentityStore ) keyIDsReferencedByTargetClientIDs ( ctx context . Context , s logical . Storage , targetIDs [ ] string ) ( [ ] string , error ) {
keyNames := make ( map [ string ] bool )
// Get all key names referenced by clients if wildcard "*" in target client IDs
if strutil . StrListContains ( targetIDs , "*" ) {
clients , err := i . listClients ( ctx , s )
if err != nil {
return nil , err
}
for _ , client := range clients {
keyNames [ client . Key ] = true
}
}
// Otherwise, get the key names referenced by each target client ID
if len ( keyNames ) == 0 {
for _ , clientID := range targetIDs {
client , err := i . clientByID ( ctx , s , clientID )
if err != nil {
return nil , err
}
if client != nil {
keyNames [ client . Key ] = true
}
}
}
// Collect the key IDs
var keyIDs [ ] string
for name , _ := range keyNames {
entry , err := s . Get ( ctx , namedKeyConfigPath + name )
if err != nil {
return nil , err
}
var key namedKey
if err := entry . DecodeJSON ( & key ) ; err != nil {
return nil , err
}
for _ , expirableKey := range key . KeyRing {
keyIDs = append ( keyIDs , expirableKey . KeyID )
}
}
return keyIDs , nil
}
// pathOIDCReadProviderPublicKeys is used to retrieve all public keys for a
// named provider so that clients can verify the validity of a signed OIDC token.
func ( i * IdentityStore ) pathOIDCReadProviderPublicKeys ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
providerName := d . Get ( "name" ) . ( string )
var provider provider
providerEntry , err := req . Storage . Get ( ctx , providerPath + providerName )
if err != nil {
return nil , err
}
if providerEntry == nil {
return nil , nil
}
if err := providerEntry . DecodeJSON ( & provider ) ; err != nil {
return nil , err
}
keyIDs , err := i . keyIDsReferencedByTargetClientIDs ( ctx , req . Storage , provider . AllowedClientIDs )
if err != nil {
return nil , err
}
jwks := & jose . JSONWebKeySet {
Keys : make ( [ ] jose . JSONWebKey , 0 , len ( keyIDs ) ) ,
}
for _ , keyID := range keyIDs {
key , err := loadOIDCPublicKey ( ctx , req . Storage , keyID )
if err != nil {
return nil , err
}
jwks . Keys = append ( jwks . Keys , * key )
}
data , err := json . Marshal ( jwks )
if err != nil {
return nil , err
}
resp := & logical . Response {
Data : map [ string ] interface { } {
logical . HTTPStatusCode : 200 ,
logical . HTTPRawBody : data ,
logical . HTTPContentType : "application/json" ,
} ,
}
return resp , nil
2021-08-23 13:42:31 +00:00
}
2021-09-07 18:35:23 +00:00
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
}
2021-08-23 13:42:31 +00:00
// 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 ) {
clientNames , err := req . Storage . List ( ctx , clientPath )
if err != nil {
return nil , err
}
var tempClient client
clients := make ( map [ string ] client )
for _ , clientName := range clientNames {
entry , err := req . Storage . Get ( ctx , clientPath + clientName )
if err != nil {
return nil , err
}
if entry != nil {
if err := entry . DecodeJSON ( & tempClient ) ; err != nil {
return nil , err
}
for _ , a := range tempClient . Assignments {
if a == targetAssignmentName {
clients [ clientName ] = tempClient
}
}
}
}
return clients , nil
}
// clientNamesReferencingTargetAssignmentName returns a slice of strings of client
// names referencing targetAssignmentName.
func ( i * IdentityStore ) clientNamesReferencingTargetAssignmentName ( ctx context . Context , req * logical . Request , targetAssignmentName string ) ( [ ] string , error ) {
clients , err := i . clientsReferencingTargetAssignmentName ( ctx , req , targetAssignmentName )
if err != nil {
return nil , err
}
var names [ ] string
2021-08-30 19:31:11 +00:00
for client := range clients {
2021-08-23 13:42:31 +00:00
names = append ( names , client )
2021-08-17 20:55:06 +00:00
}
2021-08-23 13:42:31 +00:00
sort . Strings ( names )
return names , nil
}
// clientsReferencingTargetKeyName returns a map of client names to
// clients referencing targetKeyName.
func ( i * IdentityStore ) clientsReferencingTargetKeyName ( ctx context . Context , req * logical . Request , targetKeyName string ) ( map [ string ] client , error ) {
clientNames , err := req . Storage . List ( ctx , clientPath )
if err != nil {
return nil , err
}
var tempClient client
clients := make ( map [ string ] client )
for _ , clientName := range clientNames {
entry , err := req . Storage . Get ( ctx , clientPath + clientName )
if err != nil {
return nil , err
}
if entry != nil {
if err := entry . DecodeJSON ( & tempClient ) ; err != nil {
return nil , err
}
if tempClient . Key == targetKeyName {
clients [ clientName ] = tempClient
}
}
}
return clients , nil
}
// clientNamesReferencingTargetKeyName returns a slice of strings of client
// names referencing targetKeyName.
func ( i * IdentityStore ) clientNamesReferencingTargetKeyName ( ctx context . Context , req * logical . Request , targetKeyName string ) ( [ ] string , error ) {
clients , err := i . clientsReferencingTargetKeyName ( ctx , req , targetKeyName )
if err != nil {
return nil , err
}
var names [ ] string
2021-08-30 19:31:11 +00:00
for client := range clients {
2021-08-23 13:42:31 +00:00
names = append ( names , client )
}
sort . Strings ( names )
return names , nil
2021-08-17 20:55:06 +00:00
}
2021-08-26 17:13:51 +00:00
// providersReferencingTargetScopeName returns a list of provider names referencing targetScopeName.
// Not threadsafe. To be called with lock already held.
func ( i * IdentityStore ) providersReferencingTargetScopeName ( ctx context . Context , req * logical . Request , targetScopeName string ) ( [ ] string , error ) {
providerNames , err := req . Storage . List ( ctx , providerPath )
if err != nil {
return nil , err
}
var tempProvider provider
var providers [ ] string
for _ , providerName := range providerNames {
entry , err := req . Storage . Get ( ctx , providerPath + providerName )
if err != nil {
return nil , err
}
if entry != nil {
if err := entry . DecodeJSON ( & tempProvider ) ; err != nil {
return nil , err
}
for _ , a := range tempProvider . Scopes {
if a == targetScopeName {
providers = append ( providers , providerName )
}
}
}
}
return providers , nil
}
2021-08-17 20:55:06 +00:00
// pathOIDCCreateUpdateAssignment is used to create a new assignment or update an existing one
func ( i * IdentityStore ) pathOIDCCreateUpdateAssignment ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
name := d . Get ( "name" ) . ( string )
var assignment assignment
if req . Operation == logical . UpdateOperation {
entry , err := req . Storage . Get ( ctx , assignmentPath + name )
if err != nil {
return nil , err
}
if entry != nil {
if err := entry . DecodeJSON ( & assignment ) ; err != nil {
return nil , err
}
}
}
if entitiesRaw , ok := d . GetOk ( "entities" ) ; ok {
assignment . Entities = entitiesRaw . ( [ ] string )
} else if req . Operation == logical . CreateOperation {
assignment . Entities = d . GetDefaultOrZero ( "entities" ) . ( [ ] string )
}
if groupsRaw , ok := d . GetOk ( "groups" ) ; ok {
assignment . Groups = groupsRaw . ( [ ] string )
} else if req . Operation == logical . CreateOperation {
assignment . Groups = d . GetDefaultOrZero ( "groups" ) . ( [ ] string )
}
// store assignment
entry , err := logical . StorageEntryJSON ( assignmentPath + name , assignment )
if err != nil {
return nil , err
}
if err := req . Storage . Put ( ctx , entry ) ; err != nil {
return nil , err
}
return nil , nil
}
// pathOIDCListAssignment is used to list assignments
func ( i * IdentityStore ) pathOIDCListAssignment ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
assignments , err := req . Storage . List ( ctx , assignmentPath )
if err != nil {
return nil , err
}
return logical . ListResponse ( assignments ) , nil
}
// pathOIDCReadAssignment is used to read an existing assignment
func ( i * IdentityStore ) pathOIDCReadAssignment ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
name := d . Get ( "name" ) . ( string )
entry , err := req . Storage . Get ( ctx , assignmentPath + name )
if err != nil {
return nil , err
}
if entry == nil {
return nil , nil
}
var assignment assignment
if err := entry . DecodeJSON ( & assignment ) ; err != nil {
return nil , err
}
return & logical . Response {
Data : map [ string ] interface { } {
"groups" : assignment . Groups ,
"entities" : assignment . Entities ,
} ,
} , nil
}
// pathOIDCDeleteAssignment is used to delete an assignment
func ( i * IdentityStore ) pathOIDCDeleteAssignment ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
name := d . Get ( "name" ) . ( string )
2021-08-23 13:42:31 +00:00
clientNames , err := i . clientNamesReferencingTargetAssignmentName ( ctx , req , name )
if err != nil {
return nil , err
}
if len ( clientNames ) > 0 {
errorMessage := fmt . Sprintf ( "unable to delete assignment %q because it is currently referenced by these clients: %s" ,
name , strings . Join ( clientNames , ", " ) )
return logical . ErrorResponse ( errorMessage ) , logical . ErrInvalidRequest
}
err = req . Storage . Delete ( ctx , assignmentPath + name )
2021-08-17 20:55:06 +00:00
if err != nil {
return nil , err
}
return nil , nil
}
func ( i * IdentityStore ) pathOIDCAssignmentExistenceCheck ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( bool , error ) {
name := d . Get ( "name" ) . ( string )
entry , err := req . Storage . Get ( ctx , assignmentPath + name )
if err != nil {
return false , err
}
return entry != nil , nil
}
2021-08-18 18:20:27 +00:00
// pathOIDCCreateUpdateScope is used to create a new scope or update an existing one
func ( i * IdentityStore ) pathOIDCCreateUpdateScope ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
name := d . Get ( "name" ) . ( string )
if name == "openid" {
return logical . ErrorResponse ( "the \"openid\" scope name is reserved" ) , nil
}
var scope scope
if req . Operation == logical . UpdateOperation {
entry , err := req . Storage . Get ( ctx , scopePath + name )
if err != nil {
return nil , err
}
if entry != nil {
if err := entry . DecodeJSON ( & scope ) ; err != nil {
return nil , err
}
}
}
if descriptionRaw , ok := d . GetOk ( "description" ) ; ok {
scope . Description = descriptionRaw . ( string )
} else if req . Operation == logical . CreateOperation {
scope . Description = d . GetDefaultOrZero ( "description" ) . ( string )
}
if templateRaw , ok := d . GetOk ( "template" ) ; ok {
scope . Template = templateRaw . ( string )
} else if req . Operation == logical . CreateOperation {
scope . Template = d . GetDefaultOrZero ( "template" ) . ( string )
}
// Attempt to decode as base64 and use that if it works
if decoded , err := base64 . StdEncoding . DecodeString ( scope . Template ) ; err == nil {
scope . Template = string ( decoded )
}
// Validate that template can be parsed and results in valid JSON
if scope . Template != "" {
_ , populatedTemplate , err := identitytpl . PopulateString ( identitytpl . PopulateStringInput {
Mode : identitytpl . JSONTemplating ,
String : scope . Template ,
Entity : new ( logical . Entity ) ,
Groups : make ( [ ] * logical . Group , 0 ) ,
} )
if err != nil {
return logical . ErrorResponse ( "error parsing template: %s" , err . Error ( ) ) , nil
}
var tmp map [ string ] interface { }
if err := json . Unmarshal ( [ ] byte ( populatedTemplate ) , & tmp ) ; err != nil {
return logical . ErrorResponse ( "error parsing template JSON: %s" , err . Error ( ) ) , nil
}
for key := range tmp {
if strutil . StrListContains ( requiredClaims , key ) {
return logical . ErrorResponse ( ` top level key %q not allowed. Restricted keys: %s ` ,
key , strings . Join ( requiredClaims , ", " ) ) , nil
}
}
}
// store scope
entry , err := logical . StorageEntryJSON ( scopePath + name , scope )
if err != nil {
return nil , err
}
if err := req . Storage . Put ( ctx , entry ) ; err != nil {
return nil , err
}
return nil , nil
}
// pathOIDCListScope is used to list scopes
func ( i * IdentityStore ) pathOIDCListScope ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
scopes , err := req . Storage . List ( ctx , scopePath )
if err != nil {
return nil , err
}
return logical . ListResponse ( scopes ) , nil
}
// pathOIDCReadScope is used to read an existing scope
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 )
if err != nil {
return nil , err
}
if entry == nil {
return nil , nil
}
var scope scope
if err := entry . DecodeJSON ( & scope ) ; err != nil {
return nil , err
}
return & logical . Response {
Data : map [ string ] interface { } {
"template" : scope . Template ,
"description" : scope . Description ,
} ,
} , nil
}
// pathOIDCDeleteScope is used to delete an scope
func ( i * IdentityStore ) pathOIDCDeleteScope ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
name := d . Get ( "name" ) . ( string )
2021-08-26 17:13:51 +00:00
targetScopeName := d . Get ( "name" ) . ( string )
providerNames , err := i . providersReferencingTargetScopeName ( ctx , req , targetScopeName )
2021-08-18 18:20:27 +00:00
if err != nil {
return nil , err
}
2021-08-26 17:13:51 +00:00
if len ( providerNames ) > 0 {
errorMessage := fmt . Sprintf ( "unable to delete scope %q because it is currently referenced by these providers: %s" ,
targetScopeName , strings . Join ( providerNames , ", " ) )
return logical . ErrorResponse ( errorMessage ) , logical . ErrInvalidRequest
}
err = req . Storage . Delete ( ctx , scopePath + name )
if err != nil {
return nil , err
}
2021-08-18 18:20:27 +00:00
return nil , nil
}
func ( i * IdentityStore ) pathOIDCScopeExistenceCheck ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( bool , error ) {
name := d . Get ( "name" ) . ( string )
entry , err := req . Storage . Get ( ctx , scopePath + name )
if err != nil {
return false , err
}
return entry != nil , nil
}
2021-08-23 13:42:31 +00:00
// pathOIDCCreateUpdateClient is used to create a new client or update an existing one
func ( i * IdentityStore ) pathOIDCCreateUpdateClient ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
name := d . Get ( "name" ) . ( string )
var client client
if req . Operation == logical . UpdateOperation {
entry , err := req . Storage . Get ( ctx , clientPath + name )
if err != nil {
return nil , err
}
if entry != nil {
if err := entry . DecodeJSON ( & client ) ; err != nil {
return nil , err
}
}
}
if redirectURIsRaw , ok := d . GetOk ( "redirect_uris" ) ; ok {
client . RedirectURIs = redirectURIsRaw . ( [ ] string )
} else if req . Operation == logical . CreateOperation {
client . RedirectURIs = d . Get ( "redirect_uris" ) . ( [ ] string )
}
if assignmentsRaw , ok := d . GetOk ( "assignments" ) ; ok {
client . Assignments = assignmentsRaw . ( [ ] string )
} else if req . Operation == logical . CreateOperation {
client . Assignments = d . Get ( "assignments" ) . ( [ ] string )
}
2021-08-30 20:57:28 +00:00
// remove duplicate assignments and redirect URIs
client . Assignments = strutil . RemoveDuplicates ( client . Assignments , false )
client . RedirectURIs = strutil . RemoveDuplicates ( client . RedirectURIs , false )
2021-08-23 13:42:31 +00:00
// enforce assignment existence
for _ , assignment := range client . Assignments {
entry , err := req . Storage . Get ( ctx , assignmentPath + assignment )
if err != nil {
return nil , err
}
if entry == nil {
return logical . ErrorResponse ( "assignment %q does not exist" , assignment ) , nil
}
}
if keyRaw , ok := d . GetOk ( "key" ) ; ok {
key := keyRaw . ( string )
if req . Operation == logical . UpdateOperation && client . Key != key {
return logical . ErrorResponse ( "key modification is not allowed" ) , nil
}
client . Key = key
} else if req . Operation == logical . CreateOperation {
client . Key = d . Get ( "key" ) . ( string )
}
if client . Key == "" {
return logical . ErrorResponse ( "the key parameter is required" ) , nil
}
// enforce key existence on client creation
entry , err := req . Storage . Get ( ctx , namedKeyConfigPath + client . Key )
if err != nil {
return nil , err
}
if entry == nil {
return logical . ErrorResponse ( "key %q does not exist" , client . Key ) , nil
}
if idTokenTTLRaw , ok := d . GetOk ( "id_token_ttl" ) ; ok {
2021-09-27 17:05:28 +00:00
client . IDTokenTTL = time . Duration ( idTokenTTLRaw . ( int ) ) * time . Second
2021-08-23 13:42:31 +00:00
} else if req . Operation == logical . CreateOperation {
2021-09-27 17:05:28 +00:00
client . IDTokenTTL = time . Duration ( d . Get ( "id_token_ttl" ) . ( int ) ) * time . Second
2021-08-23 13:42:31 +00:00
}
if accessTokenTTLRaw , ok := d . GetOk ( "access_token_ttl" ) ; ok {
2021-09-27 17:05:28 +00:00
client . AccessTokenTTL = time . Duration ( accessTokenTTLRaw . ( int ) ) * time . Second
2021-08-23 13:42:31 +00:00
} else if req . Operation == logical . CreateOperation {
2021-09-27 17:05:28 +00:00
client . AccessTokenTTL = time . Duration ( d . Get ( "access_token_ttl" ) . ( int ) ) * time . Second
2021-08-23 13:42:31 +00:00
}
if client . ClientID == "" {
// generate client_id
clientID , err := base62 . Random ( 32 )
if err != nil {
return nil , err
}
client . ClientID = clientID
}
if client . ClientSecret == "" {
// generate client_secret
clientSecret , err := base62 . Random ( 64 )
if err != nil {
return nil , err
}
client . ClientSecret = clientSecret
}
// store client
entry , err = logical . StorageEntryJSON ( clientPath + name , client )
if err != nil {
return nil , err
}
if err := req . Storage . Put ( ctx , entry ) ; err != nil {
return nil , err
}
return nil , nil
}
// pathOIDCListClient is used to list clients
func ( i * IdentityStore ) pathOIDCListClient ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
clients , err := req . Storage . List ( ctx , clientPath )
if err != nil {
return nil , err
}
return logical . ListResponse ( clients ) , nil
}
// pathOIDCReadClient is used to read an existing client
func ( i * IdentityStore ) pathOIDCReadClient ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
name := d . Get ( "name" ) . ( string )
entry , err := req . Storage . Get ( ctx , clientPath + name )
if err != nil {
return nil , err
}
if entry == nil {
return nil , nil
}
var client client
if err := entry . DecodeJSON ( & client ) ; err != nil {
return nil , err
}
return & logical . Response {
Data : map [ string ] interface { } {
"redirect_uris" : client . RedirectURIs ,
"assignments" : client . Assignments ,
"key" : client . Key ,
2021-09-27 17:05:28 +00:00
"id_token_ttl" : int64 ( client . IDTokenTTL . Seconds ( ) ) ,
"access_token_ttl" : int64 ( client . AccessTokenTTL . Seconds ( ) ) ,
2021-08-23 13:42:31 +00:00
"client_id" : client . ClientID ,
"client_secret" : client . ClientSecret ,
} ,
} , nil
}
// pathOIDCDeleteClient is used to delete an client
func ( i * IdentityStore ) pathOIDCDeleteClient ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
name := d . Get ( "name" ) . ( string )
err := req . Storage . Delete ( ctx , clientPath + name )
if err != nil {
return nil , err
}
return nil , nil
}
func ( i * IdentityStore ) pathOIDCClientExistenceCheck ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( bool , error ) {
name := d . Get ( "name" ) . ( string )
entry , err := req . Storage . Get ( ctx , clientPath + name )
if err != nil {
return false , err
}
return entry != nil , nil
}
2021-08-26 17:13:51 +00:00
// pathOIDCCreateUpdateProvider is used to create a new named provider or update an existing one
func ( i * IdentityStore ) pathOIDCCreateUpdateProvider ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
2021-09-17 16:41:08 +00:00
var resp logical . Response
2021-08-26 17:13:51 +00:00
name := d . Get ( "name" ) . ( string )
var provider provider
if req . Operation == logical . UpdateOperation {
entry , err := req . Storage . Get ( ctx , providerPath + name )
if err != nil {
return nil , err
}
if entry != nil {
if err := entry . DecodeJSON ( & provider ) ; err != nil {
return nil , err
}
}
}
if issuerRaw , ok := d . GetOk ( "issuer" ) ; ok {
provider . Issuer = issuerRaw . ( string )
} else if req . Operation == logical . CreateOperation {
provider . Issuer = d . GetDefaultOrZero ( "issuer" ) . ( string )
}
if allowedClientIDsRaw , ok := d . GetOk ( "allowed_client_ids" ) ; ok {
provider . AllowedClientIDs = allowedClientIDsRaw . ( [ ] string )
} else if req . Operation == logical . CreateOperation {
provider . AllowedClientIDs = d . GetDefaultOrZero ( "allowed_client_ids" ) . ( [ ] string )
}
if scopesRaw , ok := d . GetOk ( "scopes" ) ; ok {
provider . Scopes = scopesRaw . ( [ ] string )
} else if req . Operation == logical . CreateOperation {
provider . Scopes = d . GetDefaultOrZero ( "scopes" ) . ( [ ] string )
}
2021-08-30 20:57:28 +00:00
// remove duplicate allowed client IDs and scopes
provider . AllowedClientIDs = strutil . RemoveDuplicates ( provider . AllowedClientIDs , false )
provider . Scopes = strutil . RemoveDuplicates ( provider . Scopes , false )
2021-08-26 17:13:51 +00:00
if provider . Issuer != "" {
// verify that issuer is the correct format:
// - http or https
// - host name
// - optional port
// - nothing more
valid := false
if u , err := url . Parse ( provider . Issuer ) ; err == nil {
u2 := url . URL {
Scheme : u . Scheme ,
Host : u . Host ,
}
valid = ( * u == u2 ) &&
( u . Scheme == "http" || u . Scheme == "https" ) &&
u . Host != ""
}
if ! valid {
return logical . ErrorResponse (
"invalid issuer, which must include only a scheme, host, " +
"and optional port (e.g. https://example.com:8200)" ) , nil
}
resp . AddWarning ( ` If "issuer" is set explicitly, all tokens must be ` +
` validated against that address, including those issued by secondary ` +
` clusters. Setting issuer to "" will restore the default behavior of ` +
` using the cluster's api_addr as the issuer. ` )
}
scopeTemplateKeyNames := make ( map [ string ] string )
for _ , scopeName := range provider . Scopes {
entry , err := req . Storage . Get ( ctx , scopePath + scopeName )
if err != nil {
return nil , err
}
// enforce scope existence on provider create and update
if entry == 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 ,
Entity : new ( logical . Entity ) ,
Groups : make ( [ ] * logical . Group , 0 ) ,
} )
if err != nil {
return nil , fmt . Errorf ( "error parsing template for scope %q: %s" , scopeName , err . Error ( ) )
}
jsonTemplate := make ( map [ string ] interface { } )
if err = json . Unmarshal ( [ ] byte ( populatedTemplate ) , & jsonTemplate ) ; err != nil {
return nil , err
}
for keyName := range jsonTemplate {
val , ok := scopeTemplateKeyNames [ keyName ]
if ok && val != scopeName {
resp . AddWarning ( fmt . Sprintf ( "Found scope templates with conflicting top-level keys: " +
"conflict %q in scopes %q, %q. This may result in an error if the scopes are " +
"requested in an OIDC Authentication Request." , keyName , scopeName , val ) )
}
scopeTemplateKeyNames [ keyName ] = scopeName
}
}
// store named provider
entry , err := logical . StorageEntryJSON ( providerPath + name , provider )
if err != nil {
return nil , err
}
2021-09-17 16:41:08 +00:00
if err := req . Storage . Put ( ctx , entry ) ; err != nil {
return nil , err
}
if len ( resp . Warnings ) == 0 {
return nil , nil
}
return & resp , nil
2021-08-26 17:13:51 +00:00
}
// pathOIDCListProvider is used to list named providers
func ( i * IdentityStore ) pathOIDCListProvider ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
providers , err := req . Storage . List ( ctx , providerPath )
if err != nil {
return nil , err
}
return logical . ListResponse ( providers ) , nil
}
// pathOIDCReadProvider is used to read an existing provider
func ( i * IdentityStore ) pathOIDCReadProvider ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
name := d . Get ( "name" ) . ( string )
provider , err := i . getOIDCProvider ( ctx , req . Storage , name )
if err != nil {
return nil , err
}
if provider == nil {
return nil , nil
}
return & logical . Response {
Data : map [ string ] interface { } {
"issuer" : provider . Issuer ,
"allowed_client_ids" : provider . AllowedClientIDs ,
"scopes" : provider . Scopes ,
} ,
} , nil
}
func ( i * IdentityStore ) getOIDCProvider ( ctx context . Context , s logical . Storage , name string ) ( * provider , error ) {
ns , err := namespace . FromContext ( ctx )
if err != nil {
return nil , err
}
entry , err := s . Get ( ctx , providerPath + name )
if err != nil {
return nil , err
}
if entry == nil {
return nil , nil
}
var provider provider
if err := entry . DecodeJSON ( & provider ) ; err != nil {
return nil , err
}
provider . effectiveIssuer = provider . Issuer
if provider . effectiveIssuer == "" {
2021-08-30 19:31:11 +00:00
provider . effectiveIssuer = i . redirectAddr
2021-08-26 17:13:51 +00:00
}
provider . effectiveIssuer += "/v1/" + ns . Path + "identity/oidc/provider/" + name
return & provider , nil
}
// pathOIDCDeleteProvider is used to delete an assignment
func ( i * IdentityStore ) pathOIDCDeleteProvider ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
name := d . Get ( "name" ) . ( string )
return nil , req . Storage . Delete ( ctx , providerPath + name )
}
func ( i * IdentityStore ) pathOIDCProviderExistenceCheck ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( bool , error ) {
name := d . Get ( "name" ) . ( string )
entry , err := req . Storage . Get ( ctx , providerPath + name )
if err != nil {
return false , err
}
return entry != nil , nil
}