open-vault/vault/identity_store_oidc.go
David Adams 98a6a428c0 Add response_types_supported to OIDC configuration (#7533)
The OIDC Discovery standard requires the response_types_supported field
to be returned in the .well-known/openid-configuration response.

Also, the AWS IAM OIDC consumer won't accept Vault as an identity
provider without this field.

Based on examples in the OIDC Core documentation, it appears Vault
supports only the `id_token` flow, and thus that is the only value that
makes sense to be set in this field. See:

https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationExamples
2019-10-02 08:59:57 -07:00

1581 lines
43 KiB
Go

package vault
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"strings"
"time"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/helper/identity"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/base62"
"github.com/hashicorp/vault/sdk/helper/strutil"
"github.com/hashicorp/vault/sdk/logical"
"github.com/patrickmn/go-cache"
"golang.org/x/crypto/ed25519"
"gopkg.in/square/go-jose.v2"
"gopkg.in/square/go-jose.v2/jwt"
)
type oidcConfig struct {
Issuer string `json:"issuer"`
// effectiveIssuer is a calculated field and will be either Issuer (if
// that's set) or the Vault instance's api_addr.
effectiveIssuer string
}
type expireableKey struct {
KeyID string `json:"key_id"`
ExpireAt time.Time `json:"expire_at"`
}
type namedKey struct {
name string
Algorithm string `json:"signing_algorithm"`
VerificationTTL time.Duration `json:"verification_ttl"`
RotationPeriod time.Duration `json:"rotation_period"`
KeyRing []*expireableKey `json:"key_ring"`
SigningKey *jose.JSONWebKey `json:"signing_key"`
NextRotation time.Time `json:"next_rotation"`
AllowedClientIDs []string `json:"allowed_client_ids"`
}
type role struct {
TokenTTL time.Duration `json:"token_ttl"`
Key string `json:"key"`
Template string `json:"template"`
ClientID string `json:"client_id"`
}
// idToken contains the required OIDC fields.
//
// Templated claims will be merged into the final output. Those claims may
// include top-level keys, but those keys may not overwrite any of the
// required OIDC fields.
type idToken struct {
Issuer string `json:"iss"` // api_addr or custom Issuer
Namespace string `json:"namespace"` // Namespace of issuer
Subject string `json:"sub"` // Entity ID
Audience string `json:"aud"` // role ID will be used here.
Expiry int64 `json:"exp"` // Expiration, as determined by the role.
IssuedAt int64 `json:"iat"` // Time of token creation
}
// discovery contains a subset of the required elements of OIDC discovery needed
// for JWT verification libraries to use the .well-known endpoint.
//
// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
type discovery struct {
Issuer string `json:"issuer"`
Keys string `json:"jwks_uri"`
ResponseTypes []string `json:"response_types_supported"`
Subjects []string `json:"subject_types_supported"`
IDTokenAlgs []string `json:"id_token_signing_alg_values_supported"`
}
// oidcCache is a thin wrapper around go-cache to partition by namespace
type oidcCache struct {
c *cache.Cache
}
const (
issuerPath = "identity/oidc"
oidcTokensPrefix = "oidc_tokens/"
oidcConfigStorageKey = oidcTokensPrefix + "config/"
namedKeyConfigPath = oidcTokensPrefix + "named_keys/"
publicKeysConfigPath = oidcTokensPrefix + "public_keys/"
roleConfigPath = oidcTokensPrefix + "roles/"
)
var requiredClaims = []string{"iat", "aud", "exp", "iss", "sub", "namespace"}
var supportedAlgs = []string{
string(jose.RS256),
string(jose.RS384),
string(jose.RS512),
string(jose.ES256),
string(jose.ES384),
string(jose.ES512),
string(jose.EdDSA),
}
// pseudo-namespace for cache items that don't belong to any real namespace.
var nilNamespace = &namespace.Namespace{ID: "__NIL_NAMESPACE"}
func oidcPaths(i *IdentityStore) []*framework.Path {
return []*framework.Path{
{
Pattern: "oidc/config/?$",
Fields: map[string]*framework.FieldSchema{
"issuer": {
Type: framework.TypeString,
Description: "Issuer URL to be used in the iss claim of the token. If not set, Vault's app_addr will be used.",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: i.pathOIDCReadConfig,
logical.UpdateOperation: i.pathOIDCUpdateConfig,
},
HelpSynopsis: "OIDC configuration",
HelpDescription: "Update OIDC configuration in the identity backend",
},
{
Pattern: "oidc/key/" + framework.GenericNameRegex("name"),
Fields: map[string]*framework.FieldSchema{
"name": {
Type: framework.TypeString,
Description: "Name of the key",
},
"rotation_period": {
Type: framework.TypeDurationSecond,
Description: "How often to generate a new keypair.",
Default: "24h",
},
"verification_ttl": {
Type: framework.TypeDurationSecond,
Description: "Controls how long the public portion of a key will be available for verification after being rotated.",
Default: "24h",
},
"algorithm": {
Type: framework.TypeString,
Description: "Signing algorithm to use. This will default to RS256.",
Default: "RS256",
},
"allowed_client_ids": &framework.FieldSchema{
Type: framework.TypeCommaStringSlice,
Description: "Comma separated string or array of role client ids allowed to use this key for signing. If empty no roles are allowed. If \"*\" all roles are allowed.",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.CreateOperation: i.pathOIDCCreateUpdateKey,
logical.UpdateOperation: i.pathOIDCCreateUpdateKey,
logical.ReadOperation: i.pathOIDCReadKey,
logical.DeleteOperation: i.pathOIDCDeleteKey,
},
ExistenceCheck: i.pathOIDCKeyExistenceCheck,
HelpSynopsis: "CRUD operations for OIDC keys.",
HelpDescription: "Create, Read, Update, and Delete OIDC named keys.",
},
{
Pattern: "oidc/key/" + framework.GenericNameRegex("name") + "/rotate/?$",
Fields: map[string]*framework.FieldSchema{
"name": {
Type: framework.TypeString,
Description: "Name of the key",
},
"verification_ttl": {
Type: framework.TypeDurationSecond,
Description: "Controls how long the public portion of a key will be available for verification after being rotated. Setting verification_ttl here will override the verification_ttl set on the key.",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: i.pathOIDCRotateKey,
},
HelpSynopsis: "Rotate a named OIDC key.",
HelpDescription: "Manually rotate a named OIDC key. Rotating a named key will cause a new underlying signing key to be generated. The public portion of the underlying rotated signing key will continue to live for the verification_ttl duration.",
},
{
Pattern: "oidc/key/?$",
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ListOperation: i.pathOIDCListKey,
},
HelpSynopsis: "List OIDC keys",
HelpDescription: "List all named OIDC keys",
},
{
Pattern: "oidc/.well-known/openid-configuration/?$",
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: i.pathOIDCDiscovery,
},
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.",
},
{
Pattern: "oidc/.well-known/keys/?$",
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: i.pathOIDCReadPublicKeys,
},
HelpSynopsis: "Retrieve public keys",
HelpDescription: "Query this path to retrieve the public portion of keys used to sign OIDC tokens. Clients can use this to validate the authenticity of the OIDC token claims.",
},
{
Pattern: "oidc/token/" + framework.GenericNameRegex("name"),
Fields: map[string]*framework.FieldSchema{
"name": {
Type: framework.TypeString,
Description: "Name of the role",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: i.pathOIDCGenerateToken,
},
HelpSynopsis: "Generate an OIDC token",
HelpDescription: "Generate an OIDC token against a configured role. The vault token used to call this path must have a corresponding entity.",
},
{
Pattern: "oidc/role/" + framework.GenericNameRegex("name"),
Fields: map[string]*framework.FieldSchema{
"name": {
Type: framework.TypeString,
Description: "Name of the role",
},
"key": {
Type: framework.TypeString,
Description: "The OIDC key to use for generating tokens. The specified key must already exist.",
},
"template": {
Type: framework.TypeString,
Description: "The template string to use for generating tokens. This may be in string-ified JSON or base64 format.",
},
"ttl": {
Type: framework.TypeDurationSecond,
Description: "TTL of the tokens generated against the role.",
Default: "24h",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: i.pathOIDCCreateUpdateRole,
logical.CreateOperation: i.pathOIDCCreateUpdateRole,
logical.ReadOperation: i.pathOIDCReadRole,
logical.DeleteOperation: i.pathOIDCDeleteRole,
},
ExistenceCheck: i.pathOIDCRoleExistenceCheck,
HelpSynopsis: "CRUD operations on OIDC Roles",
HelpDescription: "Create, Read, Update, and Delete OIDC Roles. OIDC tokens are generated against roles which can be configured to determine how OIDC tokens are generated.",
},
{
Pattern: "oidc/role/?$",
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ListOperation: i.pathOIDCListRole,
},
HelpSynopsis: "List configured OIDC roles",
HelpDescription: "List all configured OIDC roles in the identity backend.",
},
{
Pattern: "oidc/introspect/?$",
Fields: map[string]*framework.FieldSchema{
"token": {
Type: framework.TypeString,
Description: "Token to verify",
},
"client_id": {
Type: framework.TypeString,
Description: "Optional client_id to verify",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: i.pathOIDCIntrospect,
},
HelpSynopsis: "Verify the authenticity of an OIDC token",
HelpDescription: "Use this path to verify the authenticity of an OIDC token and whether the associated entity is active and enabled.",
},
}
}
func (i *IdentityStore) pathOIDCReadConfig(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
c, err := i.getOIDCConfig(ctx, req.Storage)
if err != nil {
return nil, err
}
if c == nil {
return nil, nil
}
resp := &logical.Response{
Data: map[string]interface{}{
"issuer": c.Issuer,
},
}
if i.core.redirectAddr == "" && c.Issuer == "" {
resp.AddWarning(`Both "issuer" and Vault's "api_addr" are empty. ` +
`The issuer claim in generated tokens will not be network reachable.`)
}
return resp, nil
}
func (i *IdentityStore) pathOIDCUpdateConfig(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
var resp *logical.Response
ns, err := namespace.FromContext(ctx)
if err != nil {
return nil, err
}
issuerRaw, ok := d.GetOk("issuer")
if !ok {
return nil, nil
}
issuer := issuerRaw.(string)
if issuer != "" {
// verify that issuer is the correct format:
// - http or https
// - host name
// - optional port
// - nothing more
valid := false
if u, err := url.Parse(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 = &logical.Response{
Warnings: []string{`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.`},
}
}
c := oidcConfig{
Issuer: issuer,
}
entry, err := logical.StorageEntryJSON(oidcConfigStorageKey, c)
if err != nil {
return nil, err
}
if err := req.Storage.Put(ctx, entry); err != nil {
return nil, err
}
i.oidcCache.Flush(ns)
return resp, nil
}
func (i *IdentityStore) getOIDCConfig(ctx context.Context, s logical.Storage) (*oidcConfig, error) {
ns, err := namespace.FromContext(ctx)
if err != nil {
return nil, err
}
if v, ok := i.oidcCache.Get(ns, "config"); ok {
return v.(*oidcConfig), nil
}
var c oidcConfig
entry, err := s.Get(ctx, oidcConfigStorageKey)
if err != nil {
return nil, err
}
if entry != nil {
if err := entry.DecodeJSON(&c); err != nil {
return nil, err
}
}
c.effectiveIssuer = c.Issuer
if c.effectiveIssuer == "" {
c.effectiveIssuer = i.core.redirectAddr
}
c.effectiveIssuer += "/v1/" + ns.Path + issuerPath
i.oidcCache.SetDefault(ns, "config", &c)
return &c, nil
}
// handleOIDCCreateKey is used to create a new named key or update an existing one
func (i *IdentityStore) pathOIDCCreateUpdateKey(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
ns, err := namespace.FromContext(ctx)
if err != nil {
return nil, err
}
defer i.oidcCache.Flush(ns)
name := d.Get("name").(string)
i.oidcLock.Lock()
defer i.oidcLock.Unlock()
var key namedKey
if req.Operation == logical.UpdateOperation {
entry, err := req.Storage.Get(ctx, namedKeyConfigPath+name)
if err != nil {
return nil, err
}
if entry != nil {
if err := entry.DecodeJSON(&key); err != nil {
return nil, err
}
}
}
if rotationPeriodRaw, ok := d.GetOk("rotation_period"); ok {
key.RotationPeriod = time.Duration(rotationPeriodRaw.(int)) * time.Second
} else if req.Operation == logical.CreateOperation {
key.RotationPeriod = time.Duration(d.Get("rotation_period").(int)) * time.Second
}
if key.RotationPeriod < 1*time.Minute {
return logical.ErrorResponse("rotation_period must be at least one minute"), nil
}
if verificationTTLRaw, ok := d.GetOk("verification_ttl"); ok {
key.VerificationTTL = time.Duration(verificationTTLRaw.(int)) * time.Second
} else if req.Operation == logical.CreateOperation {
key.VerificationTTL = time.Duration(d.Get("verification_ttl").(int)) * time.Second
}
if key.VerificationTTL > 10*key.RotationPeriod {
return logical.ErrorResponse("verification_ttl cannot be longer than 10x rotation_period"), nil
}
if allowedClientIDsRaw, ok := d.GetOk("allowed_client_ids"); ok {
key.AllowedClientIDs = allowedClientIDsRaw.([]string)
} else if req.Operation == logical.CreateOperation {
key.AllowedClientIDs = d.Get("allowed_client_ids").([]string)
}
prevAlgorithm := key.Algorithm
if algorithm, ok := d.GetOk("algorithm"); ok {
key.Algorithm = algorithm.(string)
} else if req.Operation == logical.CreateOperation {
key.Algorithm = d.Get("algorithm").(string)
}
if !strutil.StrListContains(supportedAlgs, key.Algorithm) {
return logical.ErrorResponse("unknown signing algorithm %q", key.Algorithm), nil
}
// Update next rotation time if it is unset or now earlier than previously set.
nextRotation := time.Now().Add(key.RotationPeriod)
if key.NextRotation.IsZero() || nextRotation.Before(key.NextRotation) {
key.NextRotation = nextRotation
}
// generate keys if creating a new key or changing algorithms
if key.Algorithm != prevAlgorithm {
signingKey, err := generateKeys(key.Algorithm)
if err != nil {
return nil, err
}
key.SigningKey = signingKey
key.KeyRing = append(key.KeyRing, &expireableKey{KeyID: signingKey.Public().KeyID})
if err := saveOIDCPublicKey(ctx, req.Storage, signingKey.Public()); err != nil {
return nil, err
}
}
// store named key
entry, err := logical.StorageEntryJSON(namedKeyConfigPath+name, key)
if err != nil {
return nil, err
}
if err := req.Storage.Put(ctx, entry); err != nil {
return nil, err
}
return nil, nil
}
// handleOIDCReadKey is used to read an existing key
func (i *IdentityStore) pathOIDCReadKey(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := d.Get("name").(string)
i.oidcLock.RLock()
defer i.oidcLock.RUnlock()
entry, err := req.Storage.Get(ctx, namedKeyConfigPath+name)
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
var storedNamedKey namedKey
if err := entry.DecodeJSON(&storedNamedKey); err != nil {
return nil, err
}
return &logical.Response{
Data: map[string]interface{}{
"rotation_period": int64(storedNamedKey.RotationPeriod.Seconds()),
"verification_ttl": int64(storedNamedKey.VerificationTTL.Seconds()),
"algorithm": storedNamedKey.Algorithm,
"allowed_client_ids": storedNamedKey.AllowedClientIDs,
},
}, nil
}
// handleOIDCDeleteKey is used to delete a key
func (i *IdentityStore) pathOIDCDeleteKey(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
ns, err := namespace.FromContext(ctx)
if err != nil {
return nil, err
}
targetKeyName := d.Get("name").(string)
i.oidcLock.Lock()
// it is an error to delete a key that is actively referenced by a role
roleNames, err := req.Storage.List(ctx, roleConfigPath)
if err != nil {
return nil, err
}
var role *role
rolesReferencingTargetKeyName := make([]string, 0)
for _, roleName := range roleNames {
entry, err := req.Storage.Get(ctx, roleConfigPath+roleName)
if err != nil {
return nil, err
}
if entry != nil {
if err := entry.DecodeJSON(&role); err != nil {
return nil, err
}
if role.Key == targetKeyName {
rolesReferencingTargetKeyName = append(rolesReferencingTargetKeyName, roleName)
}
}
}
if len(rolesReferencingTargetKeyName) > 0 {
errorMessage := fmt.Sprintf("unable to delete key %q because it is currently referenced by these roles: %s",
targetKeyName, strings.Join(rolesReferencingTargetKeyName, ", "))
i.oidcLock.Unlock()
return logical.ErrorResponse(errorMessage), logical.ErrInvalidRequest
}
// key can safely be deleted now
err = req.Storage.Delete(ctx, namedKeyConfigPath+targetKeyName)
if err != nil {
return nil, err
}
i.oidcLock.Unlock()
_, err = i.expireOIDCPublicKeys(ctx, req.Storage)
if err != nil {
return nil, err
}
i.oidcCache.Flush(ns)
return nil, nil
}
// handleOIDCListKey is used to list named keys
func (i *IdentityStore) pathOIDCListKey(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
i.oidcLock.RLock()
defer i.oidcLock.RUnlock()
keys, err := req.Storage.List(ctx, namedKeyConfigPath)
if err != nil {
return nil, err
}
return logical.ListResponse(keys), nil
}
// pathOIDCRotateKey is used to manually trigger a rotation on the named key
func (i *IdentityStore) pathOIDCRotateKey(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
ns, err := namespace.FromContext(ctx)
if err != nil {
return nil, err
}
name := d.Get("name").(string)
i.oidcLock.Lock()
defer i.oidcLock.Unlock()
// load the named key and perform a rotation
entry, err := req.Storage.Get(ctx, namedKeyConfigPath+name)
if err != nil {
return nil, err
}
if entry == nil {
return logical.ErrorResponse("no named key found at %q", name), logical.ErrInvalidRequest
}
var storedNamedKey namedKey
if err := entry.DecodeJSON(&storedNamedKey); err != nil {
return nil, err
}
storedNamedKey.name = name
// call rotate with an appropriate overrideTTL where < 0 means no override
verificationTTLOverride := -1 * time.Second
if ttlRaw, ok := d.GetOk("verification_ttl"); ok {
verificationTTLOverride = time.Duration(ttlRaw.(int)) * time.Second
}
if err := storedNamedKey.rotate(ctx, req.Storage, verificationTTLOverride); err != nil {
return nil, err
}
i.oidcCache.Flush(ns)
return nil, nil
}
func (i *IdentityStore) pathOIDCKeyExistenceCheck(ctx context.Context, req *logical.Request, d *framework.FieldData) (bool, error) {
name := d.Get("name").(string)
i.oidcLock.RLock()
defer i.oidcLock.RUnlock()
entry, err := req.Storage.Get(ctx, namedKeyConfigPath+name)
if err != nil {
return false, err
}
return entry != nil, nil
}
// handleOIDCGenerateSignToken generates and signs an OIDC token
func (i *IdentityStore) pathOIDCGenerateToken(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
ns, err := namespace.FromContext(ctx)
if err != nil {
return nil, err
}
roleName := d.Get("name").(string)
role, err := i.getOIDCRole(ctx, req.Storage, roleName)
if err != nil {
return nil, err
}
if role == nil {
return logical.ErrorResponse("role %q not found", roleName), nil
}
var key *namedKey
if keyRaw, found := i.oidcCache.Get(ns, "namedKeys/"+role.Key); found {
key = keyRaw.(*namedKey)
} else {
entry, _ := req.Storage.Get(ctx, namedKeyConfigPath+role.Key)
if entry == nil {
return logical.ErrorResponse("key %q not found", role.Key), nil
}
if err := entry.DecodeJSON(&key); err != nil {
return nil, err
}
i.oidcCache.SetDefault(ns, "namedKeys/"+role.Key, key)
}
// Validate that the role is allowed to sign with its key (the key could have been updated)
if !strutil.StrListContains(key.AllowedClientIDs, "*") && !strutil.StrListContains(key.AllowedClientIDs, role.ClientID) {
return logical.ErrorResponse("the key %q does not list the client ID of the role %q as an allowed client ID", role.Key, roleName), nil
}
// generate an OIDC token from entity data
if req.EntityID == "" {
return logical.ErrorResponse("no entity associated with the request's token"), nil
}
config, err := i.getOIDCConfig(ctx, req.Storage)
if err != nil {
return nil, err
}
now := time.Now()
idToken := idToken{
Issuer: config.effectiveIssuer,
Namespace: ns.ID,
Subject: req.EntityID,
Audience: role.ClientID,
Expiry: now.Add(role.TokenTTL).Unix(),
IssuedAt: now.Unix(),
}
e, err := i.MemDBEntityByID(req.EntityID, true)
if err != nil {
return nil, err
}
if e == nil {
return nil, fmt.Errorf("error loading entity ID %q", req.EntityID)
}
groups, inheritedGroups, err := i.groupsByEntityID(e.ID)
if err != nil {
return nil, err
}
groups = append(groups, inheritedGroups...)
payload, err := idToken.generatePayload(i.Logger(), role.Template, e, groups)
if err != nil {
i.Logger().Warn("error populating OIDC token template", "error", err)
}
signedIdToken, err := key.signPayload(payload)
if err != nil {
return nil, errwrap.Wrapf("error signing OIDC token: {{err}}", err)
}
return &logical.Response{
Data: map[string]interface{}{
"token": signedIdToken,
"client_id": role.ClientID,
"ttl": int64(role.TokenTTL.Seconds()),
},
}, nil
}
func (tok *idToken) generatePayload(logger hclog.Logger, template string, entity *identity.Entity, groups []*identity.Group) ([]byte, error) {
output := map[string]interface{}{
"iss": tok.Issuer,
"namespace": tok.Namespace,
"sub": tok.Subject,
"aud": tok.Audience,
"exp": tok.Expiry,
"iat": tok.IssuedAt,
}
// Parse and integrate the populated role template. Structural errors with the template _should_
// be caught during role configuration. Error found during runtime will be logged, but they will
// not block generation of the basic ID token. They should not be returned to the requester.
_, populatedTemplate, err := identity.PopulateString(identity.PopulateStringInput{
Mode: identity.JSONTemplating,
String: template,
Entity: entity,
Groups: groups,
// namespace?
})
if err != nil {
logger.Warn("error populating OIDC token template", "template", template, "error", err)
}
if populatedTemplate != "" {
var parsed map[string]interface{}
if err := json.Unmarshal([]byte(populatedTemplate), &parsed); err != nil {
logger.Warn("error parsing OIDC template", "template", template, "err", err)
}
for k, v := range parsed {
if !strutil.StrListContains(requiredClaims, k) {
output[k] = v
} else {
logger.Warn("invalid top level OIDC template key", "template", template, "key", k)
}
}
}
payload, err := json.Marshal(output)
if err != nil {
return nil, err
}
return payload, nil
}
func (k *namedKey) signPayload(payload []byte) (string, error) {
signingKey := jose.SigningKey{Key: k.SigningKey, Algorithm: jose.SignatureAlgorithm(k.Algorithm)}
signer, err := jose.NewSigner(signingKey, &jose.SignerOptions{})
if err != nil {
return "", err
}
signature, err := signer.Sign(payload)
if err != nil {
return "", err
}
signedIdToken, err := signature.CompactSerialize()
if err != nil {
return "", err
}
return signedIdToken, nil
}
func (i *IdentityStore) pathOIDCRoleExistenceCheck(ctx context.Context, req *logical.Request, d *framework.FieldData) (bool, error) {
role, err := i.getOIDCRole(ctx, req.Storage, d.Get("name").(string))
if err != nil {
return false, err
}
return role != nil, nil
}
// handleOIDCCreateRole is used to create a new role or update an existing one
func (i *IdentityStore) pathOIDCCreateUpdateRole(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
ns, err := namespace.FromContext(ctx)
if err != nil {
return nil, err
}
name := d.Get("name").(string)
var role role
if req.Operation == logical.UpdateOperation {
entry, err := req.Storage.Get(ctx, roleConfigPath+name)
if err != nil {
return nil, err
}
if entry != nil {
if err := entry.DecodeJSON(&role); err != nil {
return nil, err
}
}
}
if key, ok := d.GetOk("key"); ok {
role.Key = key.(string)
} else if req.Operation == logical.CreateOperation {
role.Key = d.Get("key").(string)
}
if template, ok := d.GetOk("template"); ok {
role.Template = template.(string)
} else if req.Operation == logical.CreateOperation {
role.Template = d.Get("template").(string)
}
// Attempt to decode as base64 and use that if it works
if decoded, err := base64.StdEncoding.DecodeString(role.Template); err == nil {
role.Template = string(decoded)
}
// Validate that template can be parsed and results in valid JSON
if role.Template != "" {
_, populatedTemplate, err := identity.PopulateString(identity.PopulateStringInput{
Mode: identity.JSONTemplating,
String: role.Template,
Entity: new(identity.Entity),
Groups: make([]*identity.Group, 0),
// namespace?
})
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
}
}
}
if ttl, ok := d.GetOk("ttl"); ok {
role.TokenTTL = time.Duration(ttl.(int)) * time.Second
} else if req.Operation == logical.CreateOperation {
role.TokenTTL = time.Duration(d.Get("ttl").(int)) * time.Second
}
// create role path
if role.ClientID == "" {
clientID, err := base62.Random(26)
if err != nil {
return nil, err
}
role.ClientID = clientID
}
// store role (which was either just created or updated)
entry, err := logical.StorageEntryJSON(roleConfigPath+name, role)
if err != nil {
return nil, err
}
if err := req.Storage.Put(ctx, entry); err != nil {
return nil, err
}
i.oidcCache.Flush(ns)
return nil, nil
}
// handleOIDCReadRole is used to read an existing role
func (i *IdentityStore) pathOIDCReadRole(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := d.Get("name").(string)
role, err := i.getOIDCRole(ctx, req.Storage, name)
if err != nil {
return nil, err
}
if role == nil {
return nil, nil
}
return &logical.Response{
Data: map[string]interface{}{
"client_id": role.ClientID,
"key": role.Key,
"template": role.Template,
"ttl": int64(role.TokenTTL.Seconds()),
},
}, nil
}
func (i *IdentityStore) getOIDCRole(ctx context.Context, s logical.Storage, roleName string) (*role, error) {
entry, err := s.Get(ctx, roleConfigPath+roleName)
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
var role role
if err := entry.DecodeJSON(&role); err != nil {
return nil, err
}
return &role, nil
}
// handleOIDCDeleteRole is used to delete a role if it exists
func (i *IdentityStore) pathOIDCDeleteRole(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := d.Get("name").(string)
err := req.Storage.Delete(ctx, roleConfigPath+name)
if err != nil {
return nil, err
}
return nil, nil
}
// handleOIDCListRole is used to list stored a roles
func (i *IdentityStore) pathOIDCListRole(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
roles, err := req.Storage.List(ctx, roleConfigPath)
if err != nil {
return nil, err
}
return logical.ListResponse(roles), nil
}
func (i *IdentityStore) pathOIDCDiscovery(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
var data []byte
ns, err := namespace.FromContext(ctx)
if err != nil {
return nil, err
}
if v, ok := i.oidcCache.Get(ns, "discoveryResponse"); ok {
data = v.([]byte)
} else {
c, err := i.getOIDCConfig(ctx, req.Storage)
if err != nil {
return nil, err
}
disc := discovery{
Issuer: c.effectiveIssuer,
Keys: c.effectiveIssuer + "/.well-known/keys",
ResponseTypes: []string{"id_token"},
Subjects: []string{"public"},
IDTokenAlgs: supportedAlgs,
}
data, err = json.Marshal(disc)
if err != nil {
return nil, err
}
i.oidcCache.SetDefault(ns, "discoveryResponse", data)
}
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
}
// pathOIDCReadPublicKeys is used to retrieve all public keys so that clients can
// verify the validity of a signed OIDC token.
func (i *IdentityStore) pathOIDCReadPublicKeys(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
var data []byte
ns, err := namespace.FromContext(ctx)
if err != nil {
return nil, err
}
if v, ok := i.oidcCache.Get(ns, "jwksResponse"); ok {
data = v.([]byte)
} else {
jwks, err := i.generatePublicJWKS(ctx, req.Storage)
if err != nil {
return nil, err
}
data, err = json.Marshal(jwks)
if err != nil {
return nil, err
}
i.oidcCache.SetDefault(ns, "jwksResponse", data)
}
resp := &logical.Response{
Data: map[string]interface{}{
logical.HTTPStatusCode: 200,
logical.HTTPRawBody: data,
logical.HTTPContentType: "application/json",
},
}
// set a Cache-Control header only if there are keys, if there aren't keys
// then nextRun should not be used to set Cache-Control header because it chooses
// a time in the future that isn't based on key rotation/expiration values
keys, err := listOIDCPublicKeys(ctx, req.Storage)
if err != nil {
return nil, err
}
if len(keys) > 0 {
if v, ok := i.oidcCache.Get(nilNamespace, "nextRun"); ok {
now := time.Now()
expireAt := v.(time.Time)
if expireAt.After(now) {
expireInSeconds := expireAt.Sub(time.Now()).Seconds()
expireInString := fmt.Sprintf("max-age=%.0f", expireInSeconds)
resp.Data[logical.HTTPRawCacheControl] = expireInString
}
}
}
return resp, nil
}
func (i *IdentityStore) pathOIDCIntrospect(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
var claims jwt.Claims
// helper for preparing the non-standard introspection response
introspectionResp := func(errorMsg string) (*logical.Response, error) {
response := map[string]interface{}{
"active": true,
}
if errorMsg != "" {
response["active"] = false
response["error"] = errorMsg
}
data, err := json.Marshal(response)
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
}
rawIDToken := d.Get("token").(string)
clientID := d.Get("client_id").(string)
// validate basic JWT structure
parsedJWT, err := jwt.ParseSigned(rawIDToken)
if err != nil {
return introspectionResp(fmt.Sprintf("error parsing token: %s", err.Error()))
}
// validate signature
jwks, err := i.generatePublicJWKS(ctx, req.Storage)
if err != nil {
return nil, err
}
var valid bool
for _, key := range jwks.Keys {
if err := parsedJWT.Claims(key, &claims); err == nil {
valid = true
break
}
}
if !valid {
return introspectionResp("unable to validate the token signature")
}
// validate claims
c, err := i.getOIDCConfig(ctx, req.Storage)
if err != nil {
return nil, err
}
expected := jwt.Expected{
Issuer: c.effectiveIssuer,
Time: time.Now(),
}
if clientID != "" {
expected.Audience = []string{clientID}
}
if claimsErr := claims.Validate(expected); claimsErr != nil {
return introspectionResp(fmt.Sprintf("error validating claims: %s", claimsErr.Error()))
}
// validate entity exists and is active
entity, err := i.MemDBEntityByID(claims.Subject, true)
if err != nil {
return nil, err
}
if entity == nil {
return introspectionResp("entity was not found")
} else if entity.Disabled {
return introspectionResp("entity is disabled")
}
return introspectionResp("")
}
// namedKey.rotate(overrides) performs a key rotation on a namedKey and returns the
// verification_ttl that was applied. verification_ttl can be overridden with an
// overrideVerificationTTL value >= 0
func (k *namedKey) rotate(ctx context.Context, s logical.Storage, overrideVerificationTTL time.Duration) error {
verificationTTL := k.VerificationTTL
if overrideVerificationTTL >= 0 {
verificationTTL = overrideVerificationTTL
}
// generate new key
signingKey, err := generateKeys(k.Algorithm)
if err != nil {
return err
}
if err := saveOIDCPublicKey(ctx, s, signingKey.Public()); err != nil {
return err
}
now := time.Now()
// set the previous public key's expiry time
for _, key := range k.KeyRing {
if key.KeyID == k.SigningKey.KeyID {
key.ExpireAt = now.Add(verificationTTL)
break
}
}
k.SigningKey = signingKey
k.KeyRing = append(k.KeyRing, &expireableKey{KeyID: signingKey.KeyID})
k.NextRotation = now.Add(k.RotationPeriod)
// store named key (it was modified when rotate was called on it)
entry, err := logical.StorageEntryJSON(namedKeyConfigPath+k.name, k)
if err != nil {
return err
}
if err := s.Put(ctx, entry); err != nil {
return err
}
return nil
}
// generateKeys returns a signingKey and publicKey pair
func generateKeys(algorithm string) (*jose.JSONWebKey, error) {
var key interface{}
var err error
switch algorithm {
case "RS256", "RS384", "RS512":
// 2048 bits is recommended by RSA Laboratories as a minimum post 2015
if key, err = rsa.GenerateKey(rand.Reader, 2048); err != nil {
return nil, err
}
case "ES256", "ES384", "ES512":
var curve elliptic.Curve
switch algorithm {
case "ES256":
curve = elliptic.P256()
case "ES384":
curve = elliptic.P384()
case "ES512":
curve = elliptic.P521()
}
if key, err = ecdsa.GenerateKey(curve, rand.Reader); err != nil {
return nil, err
}
case "EdDSA":
_, key, err = ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unknown algorithm %q", algorithm)
}
id, err := uuid.GenerateUUID()
if err != nil {
return nil, err
}
jwk := &jose.JSONWebKey{
Key: key,
KeyID: id,
Algorithm: algorithm,
Use: "sig",
}
return jwk, nil
}
func saveOIDCPublicKey(ctx context.Context, s logical.Storage, key jose.JSONWebKey) error {
entry, err := logical.StorageEntryJSON(publicKeysConfigPath+key.KeyID, key)
if err != nil {
return err
}
if err := s.Put(ctx, entry); err != nil {
return err
}
return nil
}
func loadOIDCPublicKey(ctx context.Context, s logical.Storage, keyID string) (*jose.JSONWebKey, error) {
entry, err := s.Get(ctx, publicKeysConfigPath+keyID)
if err != nil {
return nil, err
}
var key jose.JSONWebKey
if err := entry.DecodeJSON(&key); err != nil {
return nil, err
}
return &key, nil
}
func listOIDCPublicKeys(ctx context.Context, s logical.Storage) ([]string, error) {
keys, err := s.List(ctx, publicKeysConfigPath)
if err != nil {
return nil, err
}
return keys, nil
}
func (i *IdentityStore) generatePublicJWKS(ctx context.Context, s logical.Storage) (*jose.JSONWebKeySet, error) {
ns, err := namespace.FromContext(ctx)
if err != nil {
return nil, err
}
if jwksRaw, ok := i.oidcCache.Get(ns, "jwks"); ok {
return jwksRaw.(*jose.JSONWebKeySet), nil
}
if _, err := i.expireOIDCPublicKeys(ctx, s); err != nil {
return nil, err
}
keyIDs, err := listOIDCPublicKeys(ctx, s)
if err != nil {
return nil, err
}
jwks := &jose.JSONWebKeySet{
Keys: make([]jose.JSONWebKey, 0, len(keyIDs)),
}
for _, keyID := range keyIDs {
key, err := loadOIDCPublicKey(ctx, s, keyID)
if err != nil {
return nil, err
}
jwks.Keys = append(jwks.Keys, *key)
}
i.oidcCache.SetDefault(ns, "jwks", jwks)
return jwks, nil
}
func (i *IdentityStore) expireOIDCPublicKeys(ctx context.Context, s logical.Storage) (time.Time, error) {
var didUpdate bool
i.oidcLock.Lock()
defer i.oidcLock.Unlock()
ns, err := namespace.FromContext(ctx)
if err != nil {
return time.Time{}, err
}
// nextExpiration will be the soonest expiration time of all keys. Initialize
// here to a relatively distant time.
nextExpiration := time.Now().Add(24 * time.Hour)
now := time.Now()
publicKeyIDs, err := listOIDCPublicKeys(ctx, s)
if err != nil {
return now, err
}
namedKeys, err := s.List(ctx, namedKeyConfigPath)
if err != nil {
return now, err
}
usedKeys := make([]string, 0, 2*len(namedKeys))
for _, k := range namedKeys {
entry, err := s.Get(ctx, namedKeyConfigPath+k)
if err != nil {
return now, err
}
var key namedKey
if err := entry.DecodeJSON(&key); err != nil {
return now, err
}
// Remove any expired keys from the keyring.
keyRing := key.KeyRing
var keyringUpdated bool
for i := 0; i < len(keyRing); i++ {
k := keyRing[i]
if !k.ExpireAt.IsZero() && k.ExpireAt.Before(now) {
keyRing[i] = keyRing[len(keyRing)-1]
keyRing = keyRing[:len(keyRing)-1]
keyringUpdated = true
i--
continue
}
// Save a remaining key's next expiration if it is the earliest we've
// seen (for use by the periodicFunc for scheduling).
if !k.ExpireAt.IsZero() && k.ExpireAt.Before(nextExpiration) {
nextExpiration = k.ExpireAt
}
// Mark the KeyID as in use so it doesn't get deleted in the next step
usedKeys = append(usedKeys, k.KeyID)
}
// Persist any keyring updates if necessary
if keyringUpdated {
key.KeyRing = keyRing
entry, err := logical.StorageEntryJSON(entry.Key, key)
if err != nil {
i.Logger().Error("error updating key", "key", key.name, "error", err)
}
if err := s.Put(ctx, entry); err != nil {
i.Logger().Error("error saving key", "key", key.name, "error", err)
}
didUpdate = true
}
}
// Delete all public keys that were not determined to be not expired and in
// use by some role.
for _, keyID := range publicKeyIDs {
if !strutil.StrListContains(usedKeys, keyID) {
didUpdate = true
if err := s.Delete(ctx, publicKeysConfigPath+keyID); err != nil {
i.Logger().Error("error deleting OIDC public key", "key_id", keyID, "error", err)
nextExpiration = now
}
i.Logger().Debug("deleted OIDC public key", "key_id", keyID)
}
}
if didUpdate {
i.oidcCache.Flush(ns)
}
return nextExpiration, nil
}
func (i *IdentityStore) oidcKeyRotation(ctx context.Context, s logical.Storage) (time.Time, error) {
// soonestRotation will be the soonest rotation time of all keys. Initialize
// here to a relatively distant time.
now := time.Now()
soonestRotation := now.Add(24 * time.Hour)
i.oidcLock.Lock()
defer i.oidcLock.Unlock()
keys, err := s.List(ctx, namedKeyConfigPath)
if err != nil {
return now, err
}
for _, k := range keys {
entry, err := s.Get(ctx, namedKeyConfigPath+k)
if err != nil {
return now, err
}
if entry == nil {
continue
}
var key namedKey
if err := entry.DecodeJSON(&key); err != nil {
return now, err
}
key.name = k
// Future key rotation that is the earliest we've seen.
if now.Before(key.NextRotation) && key.NextRotation.Before(soonestRotation) {
soonestRotation = key.NextRotation
}
// Key that is due to be rotated.
if now.After(key.NextRotation) {
i.Logger().Debug("rotating OIDC key", "key", key.name)
if err := key.rotate(ctx, s, -1); err != nil {
return now, err
}
// Possibly save the new rotation time
if key.NextRotation.Before(soonestRotation) {
soonestRotation = key.NextRotation
}
}
}
return soonestRotation, nil
}
// oidcPeriodFunc is invoked by the backend's periodFunc and runs regular key
// rotations and expiration actions.
func (i *IdentityStore) oidcPeriodicFunc(ctx context.Context) {
var nextRun time.Time
now := time.Now()
nsPaths := i.listNamespacePaths()
if v, ok := i.oidcCache.Get(nilNamespace, "nextRun"); ok {
nextRun = v.(time.Time)
}
// The condition here is for performance, not precise timing. The actions can
// be run at any time safely, but there is no need to invoke them (which
// might be somewhat expensive if there are many roles/keys) if we're not
// past any rotation/expiration TTLs.
if now.After(nextRun) {
// Initialize to a fairly distant next run time. This will be brought in
// based on key rotation times.
nextRun = now.Add(24 * time.Hour)
for _, nsPath := range nsPaths {
s := i.core.router.MatchingStorageByAPIPath(ctx, nsPath+"identity/oidc")
if s == nil {
continue
}
nextRotation, err := i.oidcKeyRotation(ctx, s)
if err != nil {
i.Logger().Warn("error rotating OIDC keys", "err", err)
}
nextExpiration, err := i.expireOIDCPublicKeys(ctx, s)
if err != nil {
i.Logger().Warn("error expiring OIDC public keys", "err", err)
}
i.oidcCache.Flush(nilNamespace)
// re-run at the soonest expiration or rotation time
if nextRotation.Before(nextRun) {
nextRun = nextRotation
}
if nextExpiration.Before(nextRun) {
nextRun = nextExpiration
}
}
i.oidcCache.SetDefault(nilNamespace, "nextRun", nextRun)
}
}
func newOIDCCache() *oidcCache {
return &oidcCache{
c: cache.New(cache.NoExpiration, cache.NoExpiration),
}
}
func (c *oidcCache) nskey(ns *namespace.Namespace, key string) string {
return fmt.Sprintf("v0:%s:%s", ns.ID, key)
}
func (c *oidcCache) Get(ns *namespace.Namespace, key string) (interface{}, bool) {
return c.c.Get(c.nskey(ns, key))
}
func (c *oidcCache) SetDefault(ns *namespace.Namespace, key string, obj interface{}) {
c.c.SetDefault(c.nskey(ns, key), obj)
}
func (c *oidcCache) Flush(ns *namespace.Namespace) {
for itemKey := range c.c.Items() {
if isTargetNamespacedKey(itemKey, []string{nilNamespace.ID, ns.ID}) {
c.c.Delete(itemKey)
}
}
}
// isTargetNamespacedKey returns true for a properly constructed namespaced key (<version>:<nsID>:<key>)
// where <nsID> matches any targeted nsID
func isTargetNamespacedKey(nskey string, nsTargets []string) bool {
split := strings.Split(nskey, ":")
return len(split) >= 3 && strutil.StrListContains(nsTargets, split[1])
}