2019-06-21 17:23:39 +00:00
package vault
import (
"context"
2019-06-27 15:34:48 +00:00
"crypto/ecdsa"
"crypto/elliptic"
2019-06-21 17:23:39 +00:00
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"time"
2019-06-27 15:34:48 +00:00
"golang.org/x/crypto/ed25519"
2019-06-21 17:23:39 +00:00
"github.com/hashicorp/vault/sdk/helper/base62"
"gopkg.in/square/go-jose.v2/jwt"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/sdk/helper/strutil"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/helper/identity"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
"gopkg.in/square/go-jose.v2"
)
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 ` json:"name" `
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" `
}
type role struct {
Name string ` json:"name" ` // TODO: do we need/want this?
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
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
}
type discovery struct {
Issuer string ` json:"issuer" `
Auth string ` json:"authorization_endpoint" `
Token string ` json:"token_endpoint" `
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" `
Scopes [ ] string ` json:"scopes_supported" `
AuthMethods [ ] string ` json:"token_endpoint_auth_methods_supported" `
Claims [ ] string ` json:"claims_supported" `
}
const (
issuerPath = "/v1/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" }
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 ,
2019-06-27 15:34:48 +00:00
Description : "Signing algorithm to use. This will default to RS256." ,
2019-06-21 17:23:39 +00:00
Default : "RS256" ,
} ,
} ,
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, Subjects, 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 ) {
value , ok := d . GetOk ( "issuer" )
if ! ok {
return nil , nil
}
c := oidcConfig {
Issuer : value . ( string ) ,
}
entry , err := logical . StorageEntryJSON ( oidcConfigStorageKey , c )
if err != nil {
return nil , err
}
if err := req . Storage . Put ( ctx , entry ) ; err != nil {
return nil , err
}
var resp logical . Response
if c . Issuer != "" {
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. ` )
}
i . oidcCache . Flush ( )
return & resp , nil
}
func ( i * IdentityStore ) getOIDCConfig ( ctx context . Context , s logical . Storage ) ( * oidcConfig , error ) {
if v , ok := i . oidcCache . Get ( "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 + issuerPath
}
i . oidcCache . SetDefault ( "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 ) {
defer i . oidcCache . Flush ( )
name := d . Get ( "name" ) . ( string )
var key namedKey
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
}
} else {
key . Name = name
}
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 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 . RotationPeriod < 1 * time . Minute {
return logical . ErrorResponse ( "rotation_period must be at least one minute" ) , nil
}
if key . VerificationTTL > 10 * key . RotationPeriod {
return logical . ErrorResponse ( "verification_ttl cannot be longer than 10x rotation_period" ) , nil
}
2019-06-27 15:34:48 +00:00
prevAlgorithm := key . Algorithm
2019-06-21 17:23:39 +00:00
if algorithm , ok := d . GetOk ( "algorithm" ) ; ok {
key . Algorithm = algorithm . ( string )
} else if req . Operation == logical . CreateOperation {
key . Algorithm = d . Get ( "algorithm" ) . ( string )
}
2019-06-27 15:34:48 +00:00
switch key . Algorithm {
case "RS256" , "RS384" , "RS512" ,
"ES256" , "ES384" , "ES512" ,
"EdDSA" :
default :
2019-06-21 17:23:39 +00:00
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
}
2019-06-27 15:34:48 +00:00
// generate keys if creating a new key or changing algorithms
if key . Algorithm != prevAlgorithm {
2019-06-21 17:23:39 +00:00
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 )
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 ) , 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 ,
} ,
} , nil
}
// handleOIDCDeleteKey is used to delete a key
func ( i * IdentityStore ) pathOIDCDeleteKey ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
targetKeyName := d . Get ( "name" ) . ( string )
// 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 , role . Name )
}
}
}
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 , ", " ) )
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
}
_ , err = i . expireOIDCPublicKeys ( ctx , req . Storage )
if err != nil {
return nil , err
}
i . oidcCache . Flush ( )
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 ) {
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 ) {
name := d . Get ( "name" ) . ( string )
// 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
}
// 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 ( )
return nil , nil
}
func ( i * IdentityStore ) pathOIDCKeyExistenceCheck ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( bool , error ) {
name := d . Get ( "name" ) . ( string )
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 ) {
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 ( "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 ( "namedKeys/" + role . Key , key )
}
// 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 ,
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 ,
"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 ) {
name := d . Get ( "name" ) . ( string )
var role role
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 role . Key == "" {
return logical . ErrorResponse ( "key must be provided" ) , nil
}
// validate that key exists
entry , err = req . Storage . Get ( ctx , namedKeyConfigPath + role . Key )
if err != nil {
return nil , err
}
if entry == nil {
return logical . ErrorResponse ( "key %q does not exist" , role . Key ) , nil
}
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
}
role . Name = name // TODO: needed???
// 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 ( )
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
if v , ok := i . oidcCache . Get ( "discoveryResponse" ) ; ok {
data = v . ( [ ] byte )
} else {
c , err := i . getOIDCConfig ( ctx , req . Storage )
if err != nil {
return nil , err
}
// TODO: review required contents
disc := discovery {
Issuer : c . effectiveIssuer ,
Keys : c . effectiveIssuer + "/.well-known/keys" ,
Subjects : [ ] string { "public" } ,
IDTokenAlgs : [ ] string { string ( jose . RS256 ) } ,
}
data , err = json . Marshal ( disc )
if err != nil {
return nil , err
}
i . oidcCache . SetDefault ( "discoveryResponse" , data )
}
resp := & logical . Response {
Data : map [ string ] interface { } {
logical . HTTPStatusCode : 200 ,
logical . HTTPRawBody : data ,
logical . HTTPContentType : "application/json" ,
} ,
}
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
if v , ok := i . oidcCache . Get ( "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 ( "jwksResponse" , data )
}
resp := & logical . Response {
Data : map [ string ] interface { } {
logical . HTTPStatusCode : 200 ,
logical . HTTPRawBody : data ,
logical . HTTPContentType : "application/json" ,
} ,
}
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 overriden 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 ) {
2019-06-27 15:34:48 +00:00
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 )
2019-06-21 17:23:39 +00:00
}
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 ) {
if jwksRaw , ok := i . oidcCache . Get ( "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 ( "jwks" , jwks )
return jwks , nil
}
func ( i * IdentityStore ) expireOIDCPublicKeys ( ctx context . Context , s logical . Storage ) ( time . Time , error ) {
var didUpdate bool
// 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 ( )
}
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 )
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
}
var key namedKey
if err := entry . DecodeJSON ( & key ) ; err != nil {
return now , err
}
// 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 , s logical . Storage ) {
nextRun := time . Time { }
if v , ok := i . oidcCache . Get ( "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 time . Now ( ) . After ( nextRun ) {
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 ( )
// re-run at the soonest expiration or rotation time
if nextRotation . Before ( nextExpiration ) {
i . oidcCache . SetDefault ( "nextRun" , nextRotation )
} else {
i . oidcCache . SetDefault ( "nextRun" , nextExpiration )
}
}
}