feat(oidc): add additional config options, accurate token times, and refactoring (#1991)
* This gives admins more control over their OIDC installation exposing options that had defaults before. Things like lifespans for authorize codes, access tokens, id tokens, refresh tokens, a option to enable the debug client messages, minimum parameter entropy. It also allows admins to configure the response modes.
* Additionally this records specific values about a users session indicating when they performed a specific authz factor so this is represented in the token accurately.
* Lastly we also implemented a OIDC key manager which calculates the kid for jwk's using the SHA1 digest instead of being static, or more specifically the first 7 chars. As per https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key#section-8.1.1 the kid should not exceed 8 chars. While it's allowed to exceed 8 chars, it must only be done so with a compelling reason, which we do not have.
2021-07-04 06:44:30 +07:00
|
|
|
package oidc
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"crypto"
|
|
|
|
"crypto/rsa"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/ory/fosite/token/jwt"
|
|
|
|
"gopkg.in/square/go-jose.v2"
|
|
|
|
|
2021-08-11 08:04:35 +07:00
|
|
|
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
|
|
|
"github.com/authelia/authelia/v4/internal/utils"
|
feat(oidc): add additional config options, accurate token times, and refactoring (#1991)
* This gives admins more control over their OIDC installation exposing options that had defaults before. Things like lifespans for authorize codes, access tokens, id tokens, refresh tokens, a option to enable the debug client messages, minimum parameter entropy. It also allows admins to configure the response modes.
* Additionally this records specific values about a users session indicating when they performed a specific authz factor so this is represented in the token accurately.
* Lastly we also implemented a OIDC key manager which calculates the kid for jwk's using the SHA1 digest instead of being static, or more specifically the first 7 chars. As per https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key#section-8.1.1 the kid should not exceed 8 chars. While it's allowed to exceed 8 chars, it must only be done so with a compelling reason, which we do not have.
2021-07-04 06:44:30 +07:00
|
|
|
)
|
|
|
|
|
|
|
|
// NewKeyManagerWithConfiguration when provided a schema.OpenIDConnectConfiguration creates a new KeyManager and adds an
|
|
|
|
// initial key to the manager.
|
|
|
|
func NewKeyManagerWithConfiguration(configuration *schema.OpenIDConnectConfiguration) (manager *KeyManager, err error) {
|
|
|
|
manager = NewKeyManager()
|
|
|
|
|
|
|
|
_, _, err = manager.AddActivePrivateKeyData(configuration.IssuerPrivateKey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return manager, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewKeyManager creates a new empty KeyManager.
|
|
|
|
func NewKeyManager() (manager *KeyManager) {
|
|
|
|
manager = new(KeyManager)
|
|
|
|
manager.keys = map[string]*rsa.PrivateKey{}
|
|
|
|
manager.keySet = new(jose.JSONWebKeySet)
|
|
|
|
|
|
|
|
return manager
|
|
|
|
}
|
|
|
|
|
|
|
|
// Strategy returns the RS256JWTStrategy.
|
|
|
|
func (m KeyManager) Strategy() (strategy *RS256JWTStrategy) {
|
|
|
|
return m.strategy
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetKeySet returns the joseJSONWebKeySet containing the rsa.PublicKey types.
|
|
|
|
func (m KeyManager) GetKeySet() (keySet *jose.JSONWebKeySet) {
|
|
|
|
return m.keySet
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetActiveWebKey obtains the currently active jose.JSONWebKey.
|
|
|
|
func (m KeyManager) GetActiveWebKey() (webKey *jose.JSONWebKey, err error) {
|
|
|
|
webKeys := m.keySet.Key(m.activeKeyID)
|
|
|
|
if len(webKeys) == 1 {
|
|
|
|
return &webKeys[0], nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(webKeys) == 0 {
|
|
|
|
return nil, errors.New("could not find a key with the active key id")
|
|
|
|
}
|
|
|
|
|
|
|
|
return &webKeys[0], errors.New("multiple keys with the same key id")
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetActiveKeyID returns the key id of the currently active key.
|
|
|
|
func (m KeyManager) GetActiveKeyID() (keyID string) {
|
|
|
|
return m.activeKeyID
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetActiveKey returns the rsa.PublicKey of the currently active key.
|
|
|
|
func (m KeyManager) GetActiveKey() (key *rsa.PublicKey, err error) {
|
|
|
|
if key, ok := m.keys[m.activeKeyID]; ok {
|
|
|
|
return &key.PublicKey, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, errors.New("failed to retrieve active public key")
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetActivePrivateKey returns the rsa.PrivateKey of the currently active key.
|
|
|
|
func (m KeyManager) GetActivePrivateKey() (key *rsa.PrivateKey, err error) {
|
|
|
|
if key, ok := m.keys[m.activeKeyID]; ok {
|
|
|
|
return key, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, errors.New("failed to retrieve active private key")
|
|
|
|
}
|
|
|
|
|
|
|
|
// AddActivePrivateKeyData adds a rsa.PublicKey given the key in the PEM string format, then sets it to the active key.
|
|
|
|
func (m *KeyManager) AddActivePrivateKeyData(data string) (key *rsa.PrivateKey, webKey *jose.JSONWebKey, err error) {
|
|
|
|
key, err = utils.ParseRsaPrivateKeyFromPemStr(data)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
webKey, err = m.AddActivePrivateKey(key)
|
|
|
|
|
|
|
|
return key, webKey, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// AddActivePrivateKey adds a rsa.PublicKey, then sets it to the active key.
|
|
|
|
func (m *KeyManager) AddActivePrivateKey(key *rsa.PrivateKey) (webKey *jose.JSONWebKey, err error) {
|
|
|
|
wk := jose.JSONWebKey{
|
|
|
|
Key: &key.PublicKey,
|
|
|
|
Algorithm: "RS256",
|
|
|
|
Use: "sig",
|
|
|
|
}
|
|
|
|
|
|
|
|
keyID, err := wk.Thumbprint(crypto.SHA1)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
strKeyID := strings.ToLower(fmt.Sprintf("%x", keyID))
|
|
|
|
if len(strKeyID) >= 7 {
|
|
|
|
// Shorten the key if it's greater than 7 to a length of exactly 7.
|
|
|
|
strKeyID = strKeyID[0:6]
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, ok := m.keys[strKeyID]; ok {
|
|
|
|
return nil, fmt.Errorf("key id %s already exists", strKeyID)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Add Mutex here when implementing key rotation.
|
|
|
|
wk.KeyID = strKeyID
|
|
|
|
m.keySet.Keys = append(m.keySet.Keys, wk)
|
|
|
|
m.keys[strKeyID] = key
|
|
|
|
m.activeKeyID = strKeyID
|
|
|
|
|
|
|
|
m.strategy, err = NewRS256JWTStrategy(wk.KeyID, key)
|
|
|
|
if err != nil {
|
|
|
|
return &wk, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &wk, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewRS256JWTStrategy returns a new RS256JWTStrategy.
|
|
|
|
func NewRS256JWTStrategy(id string, key *rsa.PrivateKey) (strategy *RS256JWTStrategy, err error) {
|
|
|
|
strategy = new(RS256JWTStrategy)
|
|
|
|
strategy.JWTStrategy = new(jwt.RS256JWTStrategy)
|
|
|
|
|
|
|
|
strategy.SetKey(id, key)
|
|
|
|
|
|
|
|
return strategy, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// RS256JWTStrategy is a decorator struct for the fosite RS256JWTStrategy.
|
|
|
|
type RS256JWTStrategy struct {
|
|
|
|
JWTStrategy *jwt.RS256JWTStrategy
|
|
|
|
|
|
|
|
keyID string
|
|
|
|
}
|
|
|
|
|
|
|
|
// KeyID returns the key id.
|
|
|
|
func (s RS256JWTStrategy) KeyID() (id string) {
|
|
|
|
return s.keyID
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetKey sets the provided key id and key as the active key (this is what triggers fosite to use it).
|
|
|
|
func (s *RS256JWTStrategy) SetKey(id string, key *rsa.PrivateKey) {
|
|
|
|
s.keyID = id
|
|
|
|
s.JWTStrategy.PrivateKey = key
|
|
|
|
}
|
|
|
|
|
|
|
|
// Hash is a decorator func for the underlying fosite RS256JWTStrategy.
|
|
|
|
func (s *RS256JWTStrategy) Hash(ctx context.Context, in []byte) ([]byte, error) {
|
|
|
|
return s.JWTStrategy.Hash(ctx, in)
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetSigningMethodLength is a decorator func for the underlying fosite RS256JWTStrategy.
|
|
|
|
func (s *RS256JWTStrategy) GetSigningMethodLength() int {
|
|
|
|
return s.JWTStrategy.GetSigningMethodLength()
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetSignature is a decorator func for the underlying fosite RS256JWTStrategy.
|
|
|
|
func (s *RS256JWTStrategy) GetSignature(ctx context.Context, token string) (string, error) {
|
|
|
|
return s.JWTStrategy.GetSignature(ctx, token)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generate is a decorator func for the underlying fosite RS256JWTStrategy.
|
|
|
|
func (s *RS256JWTStrategy) Generate(ctx context.Context, claims jwt.MapClaims, header jwt.Mapper) (string, string, error) {
|
|
|
|
return s.JWTStrategy.Generate(ctx, claims, header)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate is a decorator func for the underlying fosite RS256JWTStrategy.
|
|
|
|
func (s *RS256JWTStrategy) Validate(ctx context.Context, token string) (string, error) {
|
|
|
|
return s.JWTStrategy.Validate(ctx, token)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Decode is a decorator func for the underlying fosite RS256JWTStrategy.
|
|
|
|
func (s *RS256JWTStrategy) Decode(ctx context.Context, token string) (*jwt.Token, error) {
|
|
|
|
return s.JWTStrategy.Decode(ctx, token)
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetPublicKeyID is a decorator func for the underlying fosite RS256JWTStrategy.
|
|
|
|
func (s *RS256JWTStrategy) GetPublicKeyID(_ context.Context) (string, error) {
|
|
|
|
return s.keyID, nil
|
|
|
|
}
|