mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 03:37:34 +00:00
feat: dynamic issuer (#3481)
* feat: dynamic issuer * dynamic domain handling * key rotation durations * feat: dynamic issuer * make webauthn displayname dynamic
This commit is contained in:
@@ -5,7 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
"github.com/caos/oidc/pkg/oidc"
|
||||
"github.com/caos/oidc/v2/pkg/oidc"
|
||||
"golang.org/x/text/language"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
|
||||
|
@@ -2,6 +2,7 @@ package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/securecookie"
|
||||
|
||||
@@ -20,7 +21,6 @@ type CookieHandler struct {
|
||||
sameSite http.SameSite
|
||||
path string
|
||||
maxAge int
|
||||
domain string
|
||||
}
|
||||
|
||||
func NewCookieHandler(opts ...CookieHandlerOpt) *CookieHandler {
|
||||
@@ -76,12 +76,6 @@ func WithMaxAge(maxAge int) CookieHandlerOpt {
|
||||
}
|
||||
}
|
||||
|
||||
func WithDomain(domain string) CookieHandlerOpt {
|
||||
return func(c *CookieHandler) {
|
||||
c.domain = domain
|
||||
}
|
||||
}
|
||||
|
||||
func SetCookiePrefix(name, domain, path string, secureOnly bool) string {
|
||||
if !secureOnly {
|
||||
return name
|
||||
@@ -101,7 +95,7 @@ func (c *CookieHandler) GetCookieValue(r *http.Request, name string) (string, er
|
||||
}
|
||||
|
||||
func (c *CookieHandler) GetEncryptedCookieValue(r *http.Request, name string, value interface{}) error {
|
||||
cookie, err := r.Cookie(SetCookiePrefix(name, c.domain, c.path, c.secureOnly))
|
||||
cookie, err := r.Cookie(SetCookiePrefix(name, r.Host, c.path, c.secureOnly))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -111,11 +105,11 @@ func (c *CookieHandler) GetEncryptedCookieValue(r *http.Request, name string, va
|
||||
return c.securecookie.Decode(name, cookie.Value, value)
|
||||
}
|
||||
|
||||
func (c *CookieHandler) SetCookie(w http.ResponseWriter, name string, value string) {
|
||||
c.httpSet(w, name, value, c.maxAge)
|
||||
func (c *CookieHandler) SetCookie(w http.ResponseWriter, name, domain, value string) {
|
||||
c.httpSet(w, name, domain, value, c.maxAge)
|
||||
}
|
||||
|
||||
func (c *CookieHandler) SetEncryptedCookie(w http.ResponseWriter, name string, value interface{}) error {
|
||||
func (c *CookieHandler) SetEncryptedCookie(w http.ResponseWriter, name, domain string, value interface{}) error {
|
||||
if c.securecookie == nil {
|
||||
return errors.ThrowInternal(nil, "HTTP-s2HUtx", "securecookie not configured")
|
||||
}
|
||||
@@ -123,19 +117,19 @@ func (c *CookieHandler) SetEncryptedCookie(w http.ResponseWriter, name string, v
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.httpSet(w, name, encoded, c.maxAge)
|
||||
c.httpSet(w, name, domain, encoded, c.maxAge)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CookieHandler) DeleteCookie(w http.ResponseWriter, name string) {
|
||||
c.httpSet(w, name, "", -1)
|
||||
func (c *CookieHandler) DeleteCookie(w http.ResponseWriter, r *http.Request, name string) {
|
||||
c.httpSet(w, name, r.Host, "", -1)
|
||||
}
|
||||
|
||||
func (c *CookieHandler) httpSet(w http.ResponseWriter, name, value string, maxage int) {
|
||||
func (c *CookieHandler) httpSet(w http.ResponseWriter, name, domain, value string, maxage int) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: SetCookiePrefix(name, c.domain, c.path, c.secureOnly),
|
||||
Name: SetCookiePrefix(name, domain, c.path, c.secureOnly),
|
||||
Value: value,
|
||||
Domain: c.domain,
|
||||
Domain: strings.Split(domain, ":")[0],
|
||||
Path: c.path,
|
||||
MaxAge: maxage,
|
||||
HttpOnly: c.httpOnly,
|
||||
|
@@ -34,7 +34,7 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func (csp *CSP) Value(nonce string) string {
|
||||
func (csp *CSP) Value(nonce string, host string) string {
|
||||
valuesMap := csp.asMap()
|
||||
|
||||
values := make([]string, 0, len(valuesMap))
|
||||
@@ -43,7 +43,7 @@ func (csp *CSP) Value(nonce string) string {
|
||||
continue
|
||||
}
|
||||
|
||||
values = append(values, fmt.Sprintf("%v %v", k, v.String(nonce)))
|
||||
values = append(values, fmt.Sprintf("%v %v", k, v.String(nonce, host)))
|
||||
}
|
||||
|
||||
return strings.Join(values, ";")
|
||||
@@ -99,24 +99,33 @@ func (srcOpts CSPSourceOptions) AddHost(h ...string) CSPSourceOptions {
|
||||
return append(srcOpts, h...)
|
||||
}
|
||||
|
||||
func (srcOpts CSPSourceOptions) AddOwnHost() CSPSourceOptions {
|
||||
return append(srcOpts, placeHolderHost)
|
||||
}
|
||||
|
||||
func (srcOpts CSPSourceOptions) AddScheme(s ...string) CSPSourceOptions {
|
||||
return srcOpts.add(s, "%v:")
|
||||
}
|
||||
|
||||
func (srcOpts CSPSourceOptions) AddNonce() CSPSourceOptions {
|
||||
return append(srcOpts, "'nonce-%v'")
|
||||
return append(srcOpts, fmt.Sprintf("'nonce-%s'", placeHolderNonce))
|
||||
}
|
||||
|
||||
const (
|
||||
placeHolderNonce = "{{nonce}}"
|
||||
placeHolderHost = "{{host}}"
|
||||
)
|
||||
|
||||
func (srcOpts CSPSourceOptions) AddHash(alg, b64v string) CSPSourceOptions {
|
||||
return append(srcOpts, fmt.Sprintf("'%v-%v'", alg, b64v))
|
||||
}
|
||||
|
||||
func (srcOpts CSPSourceOptions) String(nonce string) string {
|
||||
func (srcOpts CSPSourceOptions) String(nonce, host string) string {
|
||||
value := strings.Join(srcOpts, " ")
|
||||
if !strings.Contains(value, "%v") {
|
||||
if !strings.Contains(value, placeHolderNonce) && !strings.Contains(value, placeHolderHost) {
|
||||
return value
|
||||
}
|
||||
return fmt.Sprintf(value, nonce)
|
||||
return strings.ReplaceAll(strings.ReplaceAll(value, placeHolderHost, host), placeHolderNonce, nonce)
|
||||
}
|
||||
|
||||
func (srcOpts CSPSourceOptions) add(values []string, format string) CSPSourceOptions {
|
||||
|
@@ -63,7 +63,7 @@ func (h *headers) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
r = saveContext(r, nonceKey, nonce)
|
||||
}
|
||||
headers := w.Header()
|
||||
headers.Set(http_utils.ContentSecurityPolicy, h.csp.Value(nonce))
|
||||
headers.Set(http_utils.ContentSecurityPolicy, h.csp.Value(nonce, r.Host))
|
||||
headers.Set(http_utils.XXSSProtection, "1; mode=block")
|
||||
headers.Set(http_utils.StrictTransportSecurity, "max-age=31536000; includeSubDomains")
|
||||
headers.Set(http_utils.XFrameOptions, "DENY")
|
||||
|
@@ -21,10 +21,6 @@ func UserAgentIDFromCtx(ctx context.Context) (string, bool) {
|
||||
return userAgentID, ok
|
||||
}
|
||||
|
||||
func InstanceIDFromCtx(ctx context.Context) string {
|
||||
return "" //TODO: implement
|
||||
}
|
||||
|
||||
type UserAgent struct {
|
||||
ID string
|
||||
}
|
||||
@@ -41,10 +37,9 @@ type UserAgentCookieConfig struct {
|
||||
MaxAge time.Duration
|
||||
}
|
||||
|
||||
func NewUserAgentHandler(config *UserAgentCookieConfig, cookieKey []byte, domain string, idGenerator id.Generator, externalSecure bool) (func(http.Handler) http.Handler, error) {
|
||||
func NewUserAgentHandler(config *UserAgentCookieConfig, cookieKey []byte, idGenerator id.Generator, externalSecure bool) (func(http.Handler) http.Handler, error) {
|
||||
opts := []http_utils.CookieHandlerOpt{
|
||||
http_utils.WithEncryption(cookieKey, cookieKey),
|
||||
http_utils.WithDomain(domain),
|
||||
http_utils.WithMaxAge(int(config.MaxAge.Seconds())),
|
||||
}
|
||||
if !externalSecure {
|
||||
@@ -68,7 +63,7 @@ func (ua *userAgentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if err == nil {
|
||||
ctx := context.WithValue(r.Context(), userAgentKey, agent.ID)
|
||||
r = r.WithContext(ctx)
|
||||
ua.setUserAgent(w, agent)
|
||||
ua.setUserAgent(w, r.Host, agent)
|
||||
}
|
||||
ua.nextHandler.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -90,8 +85,8 @@ func (ua *userAgentHandler) getUserAgent(r *http.Request) (*UserAgent, error) {
|
||||
return userAgent, nil
|
||||
}
|
||||
|
||||
func (ua *userAgentHandler) setUserAgent(w http.ResponseWriter, agent *UserAgent) error {
|
||||
err := ua.cookieHandler.SetEncryptedCookie(w, ua.cookieName, agent)
|
||||
func (ua *userAgentHandler) setUserAgent(w http.ResponseWriter, host string, agent *UserAgent) error {
|
||||
err := ua.cookieHandler.SetEncryptedCookie(w, ua.cookieName, host, agent)
|
||||
if err != nil {
|
||||
return errors.ThrowPermissionDenied(err, "HTTP-AqgqdA", "cannot set user agent cookie")
|
||||
}
|
||||
|
@@ -32,9 +32,13 @@ func IsOrigin(rawOrigin string) bool {
|
||||
}
|
||||
|
||||
func BuildHTTP(hostname string, externalPort uint16, secure bool) string {
|
||||
return BuildOrigin(fmt.Sprintf("%s:%d", hostname, externalPort), secure)
|
||||
}
|
||||
|
||||
func BuildOrigin(host string, secure bool) string {
|
||||
schema := "https"
|
||||
if !secure {
|
||||
schema = "http"
|
||||
}
|
||||
return fmt.Sprintf("%s://%s:%d", schema, hostname, externalPort)
|
||||
return fmt.Sprintf("%s://%s", schema, host)
|
||||
}
|
||||
|
@@ -6,8 +6,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
"github.com/caos/oidc/pkg/oidc"
|
||||
"github.com/caos/oidc/pkg/op"
|
||||
"github.com/caos/oidc/v2/pkg/oidc"
|
||||
"github.com/caos/oidc/v2/pkg/op"
|
||||
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
"github.com/caos/zitadel/internal/api/http/middleware"
|
||||
|
@@ -6,8 +6,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/caos/oidc/pkg/oidc"
|
||||
"github.com/caos/oidc/pkg/op"
|
||||
"github.com/caos/oidc/v2/pkg/oidc"
|
||||
"github.com/caos/oidc/v2/pkg/op"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
|
@@ -5,8 +5,8 @@ import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
|
||||
"github.com/caos/oidc/pkg/oidc"
|
||||
"github.com/caos/oidc/pkg/op"
|
||||
"github.com/caos/oidc/v2/pkg/oidc"
|
||||
"github.com/caos/oidc/v2/pkg/op"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
@@ -43,7 +43,7 @@ func (o *OPStorage) GetClientByClientID(ctx context.Context, id string) (_ op.Cl
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInternal(err, "OIDC-mPxqP", "Errors.Internal")
|
||||
}
|
||||
projectRoles, err := o.query.SearchProjectRoles(context.TODO(), &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}})
|
||||
projectRoles, err := o.query.SearchProjectRoles(ctx, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -4,8 +4,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/caos/oidc/pkg/oidc"
|
||||
"github.com/caos/oidc/pkg/op"
|
||||
"github.com/caos/oidc/v2/pkg/oidc"
|
||||
"github.com/caos/oidc/v2/pkg/op"
|
||||
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
|
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
"github.com/caos/oidc/v2/pkg/op"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
@@ -20,99 +21,121 @@ import (
|
||||
const (
|
||||
locksTable = "projections.locks"
|
||||
signingKey = "signing_key"
|
||||
oidcUser = "OIDC"
|
||||
|
||||
retryBackoff = 500 * time.Millisecond
|
||||
retryCount = 3
|
||||
lockDuration = retryCount * retryBackoff * 5
|
||||
gracefulPeriod = 10 * time.Minute
|
||||
)
|
||||
|
||||
func (o *OPStorage) GetKeySet(ctx context.Context) (_ *jose.JSONWebKeySet, err error) {
|
||||
//SigningKey wraps the query.PrivateKey to implement the op.SigningKey interface
|
||||
type SigningKey struct {
|
||||
algorithm jose.SignatureAlgorithm
|
||||
id string
|
||||
key interface{}
|
||||
}
|
||||
|
||||
func (s *SigningKey) SignatureAlgorithm() jose.SignatureAlgorithm {
|
||||
return s.algorithm
|
||||
}
|
||||
|
||||
func (s *SigningKey) Key() interface{} {
|
||||
return s.key
|
||||
}
|
||||
|
||||
func (s *SigningKey) ID() string {
|
||||
return s.id
|
||||
}
|
||||
|
||||
//PublicKey wraps the query.PublicKey to implement the op.Key interface
|
||||
type PublicKey struct {
|
||||
key query.PublicKey
|
||||
}
|
||||
|
||||
func (s *PublicKey) Algorithm() jose.SignatureAlgorithm {
|
||||
return jose.SignatureAlgorithm(s.key.Algorithm())
|
||||
}
|
||||
|
||||
func (s *PublicKey) Use() string {
|
||||
return s.key.Use().String()
|
||||
}
|
||||
|
||||
func (s *PublicKey) Key() interface{} {
|
||||
return s.key.Key()
|
||||
}
|
||||
|
||||
func (s *PublicKey) ID() string {
|
||||
return s.key.ID()
|
||||
}
|
||||
|
||||
//KeySet implements the op.Storage interface
|
||||
func (o *OPStorage) KeySet(ctx context.Context) (keys []op.Key, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
keys, err := o.query.ActivePublicKeys(ctx, time.Now())
|
||||
err = retry(func() error {
|
||||
publicKeys, err := o.query.ActivePublicKeys(ctx, time.Now())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keys = make([]op.Key, len(publicKeys.Keys))
|
||||
for i, key := range publicKeys.Keys {
|
||||
keys[i] = &PublicKey{key}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return keys, err
|
||||
}
|
||||
|
||||
//SignatureAlgorithms implements the op.Storage interface
|
||||
func (o *OPStorage) SignatureAlgorithms(ctx context.Context) ([]jose.SignatureAlgorithm, error) {
|
||||
key, err := o.SigningKey(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
webKeys := make([]jose.JSONWebKey, len(keys.Keys))
|
||||
for i, key := range keys.Keys {
|
||||
webKeys[i] = jose.JSONWebKey{
|
||||
KeyID: key.ID(),
|
||||
Algorithm: key.Algorithm(),
|
||||
Use: key.Use().String(),
|
||||
Key: key.Key(),
|
||||
}
|
||||
}
|
||||
return &jose.JSONWebKeySet{Keys: webKeys}, nil
|
||||
return []jose.SignatureAlgorithm{key.SignatureAlgorithm()}, nil
|
||||
}
|
||||
|
||||
func (o *OPStorage) GetSigningKey(ctx context.Context, keyCh chan<- jose.SigningKey) {
|
||||
renewTimer := time.NewTimer(0)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-o.keyChan:
|
||||
if !renewTimer.Stop() {
|
||||
<-renewTimer.C
|
||||
}
|
||||
checkAfter := o.resetTimer(renewTimer, true)
|
||||
logging.Infof("requested next signing key check in %s", checkAfter)
|
||||
case <-renewTimer.C:
|
||||
o.getSigningKey(ctx, renewTimer, keyCh)
|
||||
}
|
||||
//SigningKey implements the op.Storage interface
|
||||
func (o *OPStorage) SigningKey(ctx context.Context) (key op.SigningKey, err error) {
|
||||
err = retry(func() error {
|
||||
key, err = o.getSigningKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}()
|
||||
if key == nil {
|
||||
return errors.ThrowInternal(nil, "test", "test")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return key, err
|
||||
}
|
||||
|
||||
func (o *OPStorage) getSigningKey(ctx context.Context, renewTimer *time.Timer, keyCh chan<- jose.SigningKey) {
|
||||
keys, err := o.query.ActivePrivateSigningKey(ctx, time.Now().Add(o.signingKeyGracefulPeriod))
|
||||
func (o *OPStorage) getSigningKey(ctx context.Context) (op.SigningKey, error) {
|
||||
keys, err := o.query.ActivePrivateSigningKey(ctx, time.Now().Add(gracefulPeriod))
|
||||
if err != nil {
|
||||
checkAfter := o.resetTimer(renewTimer, true)
|
||||
logging.Infof("next signing key check in %s", checkAfter)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
if len(keys.Keys) == 0 {
|
||||
var sequence uint64
|
||||
if keys.LatestSequence != nil {
|
||||
sequence = keys.LatestSequence.Sequence
|
||||
}
|
||||
o.refreshSigningKey(ctx, keyCh, o.signingKeyAlgorithm, sequence)
|
||||
checkAfter := o.resetTimer(renewTimer, true)
|
||||
logging.Infof("next signing key check in %s", checkAfter)
|
||||
return
|
||||
if len(keys.Keys) > 0 {
|
||||
return o.privateKeyToSigningKey(selectSigningKey(keys.Keys))
|
||||
}
|
||||
err = o.exchangeSigningKey(selectSigningKey(keys.Keys), keyCh)
|
||||
logging.OnError(err).Error("could not exchange signing key")
|
||||
checkAfter := o.resetTimer(renewTimer, err != nil)
|
||||
logging.Infof("next signing key check in %s", checkAfter)
|
||||
var sequence uint64
|
||||
if keys.LatestSequence != nil {
|
||||
sequence = keys.LatestSequence.Sequence
|
||||
}
|
||||
return nil, o.refreshSigningKey(ctx, o.signingKeyAlgorithm, sequence)
|
||||
}
|
||||
|
||||
func (o *OPStorage) resetTimer(timer *time.Timer, shortRefresh bool) (nextCheck time.Duration) {
|
||||
nextCheck = o.signingKeyRotationCheck
|
||||
defer func() { timer.Reset(nextCheck) }()
|
||||
if shortRefresh || o.currentKey == nil {
|
||||
return nextCheck
|
||||
}
|
||||
maxLifetime := time.Until(o.currentKey.Expiry())
|
||||
if maxLifetime < o.signingKeyGracefulPeriod+2*o.signingKeyRotationCheck {
|
||||
return nextCheck
|
||||
}
|
||||
return maxLifetime - o.signingKeyGracefulPeriod - o.signingKeyRotationCheck
|
||||
}
|
||||
|
||||
func (o *OPStorage) refreshSigningKey(ctx context.Context, keyCh chan<- jose.SigningKey, algorithm string, sequence uint64) {
|
||||
if o.currentKey != nil && o.currentKey.Expiry().Before(time.Now().UTC()) {
|
||||
logging.Info("unset current signing key")
|
||||
keyCh <- jose.SigningKey{}
|
||||
}
|
||||
func (o *OPStorage) refreshSigningKey(ctx context.Context, algorithm string, sequence uint64) error {
|
||||
ok, err := o.ensureIsLatestKey(ctx, sequence)
|
||||
if err != nil {
|
||||
logging.New().WithError(err).Error("could not ensure latest key")
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
logging.Warn("view not up to date, retrying later")
|
||||
return
|
||||
if err != nil || !ok {
|
||||
return errors.ThrowInternal(err, "OIDC-ASfh3", "cannot ensure that projection is up to date")
|
||||
}
|
||||
err = o.lockAndGenerateSigningKeyPair(ctx, algorithm)
|
||||
logging.OnError(err).Warn("could not create signing key")
|
||||
if err != nil {
|
||||
return errors.ThrowInternal(err, "OIDC-ADh31", "could not create signing key")
|
||||
}
|
||||
return errors.ThrowInternal(nil, "OIDC-Df1bh", "")
|
||||
}
|
||||
|
||||
func (o *OPStorage) ensureIsLatestKey(ctx context.Context, sequence uint64) (bool, error) {
|
||||
@@ -123,29 +146,20 @@ func (o *OPStorage) ensureIsLatestKey(ctx context.Context, sequence uint64) (boo
|
||||
return sequence == maxSequence, nil
|
||||
}
|
||||
|
||||
func (o *OPStorage) exchangeSigningKey(key query.PrivateKey, keyCh chan<- jose.SigningKey) (err error) {
|
||||
if o.currentKey != nil && o.currentKey.ID() == key.ID() {
|
||||
logging.Info("no new signing key")
|
||||
return nil
|
||||
}
|
||||
func (o *OPStorage) privateKeyToSigningKey(key query.PrivateKey) (_ op.SigningKey, err error) {
|
||||
keyData, err := crypto.Decrypt(key.Key(), o.encAlg)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
privateKey, err := crypto.BytesToPrivateKey(keyData)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
keyCh <- jose.SigningKey{
|
||||
Algorithm: jose.SignatureAlgorithm(key.Algorithm()),
|
||||
Key: jose.JSONWebKey{
|
||||
KeyID: key.ID(),
|
||||
Key: privateKey,
|
||||
},
|
||||
}
|
||||
o.currentKey = key
|
||||
logging.WithFields("keyID", key.ID()).Info("exchanged signing key")
|
||||
return nil
|
||||
return &SigningKey{
|
||||
algorithm: jose.SignatureAlgorithm(key.Algorithm()),
|
||||
key: privateKey,
|
||||
id: key.ID(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (o *OPStorage) lockAndGenerateSigningKeyPair(ctx context.Context, algorithm string) error {
|
||||
@@ -154,7 +168,7 @@ func (o *OPStorage) lockAndGenerateSigningKeyPair(ctx context.Context, algorithm
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
errs := o.locker.Lock(ctx, o.signingKeyRotationCheck*2, authz.GetInstance(ctx).InstanceID())
|
||||
errs := o.locker.Lock(ctx, lockDuration, authz.GetInstance(ctx).InstanceID())
|
||||
err, ok := <-errs
|
||||
if err != nil || !ok {
|
||||
if errors.IsErrorAlreadyExists(err) {
|
||||
@@ -164,13 +178,13 @@ func (o *OPStorage) lockAndGenerateSigningKeyPair(ctx context.Context, algorithm
|
||||
return err
|
||||
}
|
||||
|
||||
return o.command.GenerateSigningKeyPair(ctx, algorithm)
|
||||
return o.command.GenerateSigningKeyPair(setOIDCCtx(ctx), algorithm)
|
||||
}
|
||||
|
||||
func (o *OPStorage) getMaxKeySequence(ctx context.Context) (uint64, error) {
|
||||
return o.eventstore.LatestSequence(ctx,
|
||||
eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxSequence).
|
||||
ResourceOwner("system"). //TODO: change with multi issuer
|
||||
ResourceOwner(authz.GetInstance(ctx).InstanceID()).
|
||||
AddQuery().
|
||||
AggregateTypes(keypair.AggregateType).
|
||||
Builder(),
|
||||
@@ -180,3 +194,18 @@ func (o *OPStorage) getMaxKeySequence(ctx context.Context) (uint64, error) {
|
||||
func selectSigningKey(keys []query.PrivateKey) query.PrivateKey {
|
||||
return keys[len(keys)-1]
|
||||
}
|
||||
|
||||
func setOIDCCtx(ctx context.Context) context.Context {
|
||||
return authz.SetCtxData(ctx, authz.CtxData{UserID: oidcUser, OrgID: authz.GetInstance(ctx).InstanceID()})
|
||||
}
|
||||
|
||||
func retry(retryable func() error) (err error) {
|
||||
for i := 0; i < retryCount; i++ {
|
||||
time.Sleep(retryBackoff)
|
||||
err = retryable()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/caos/oidc/pkg/op"
|
||||
"github.com/caos/oidc/v2/pkg/op"
|
||||
"github.com/rakyll/statik/fs"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"github.com/caos/zitadel/internal/api/ui/login"
|
||||
"github.com/caos/zitadel/internal/auth/repository"
|
||||
"github.com/caos/zitadel/internal/command"
|
||||
"github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
@@ -74,26 +73,23 @@ type OPStorage struct {
|
||||
defaultRefreshTokenIdleExpiration time.Duration
|
||||
defaultRefreshTokenExpiration time.Duration
|
||||
encAlg crypto.EncryptionAlgorithm
|
||||
keyChan <-chan interface{}
|
||||
currentKey query.PrivateKey
|
||||
signingKeyRotationCheck time.Duration
|
||||
signingKeyGracefulPeriod time.Duration
|
||||
locker crdb.Locker
|
||||
assetAPIPrefix string
|
||||
}
|
||||
|
||||
func NewProvider(ctx context.Context, config Config, issuer, defaultLogoutRedirectURI string, command *command.Commands, query *query.Queries, repo repository.Repository, keyConfig systemdefaults.KeyConfig, encryptionAlg crypto.EncryptionAlgorithm, cryptoKey []byte, es *eventstore.Eventstore, projections *sql.DB, keyChan <-chan interface{}, userAgentCookie, instanceHandler func(http.Handler) http.Handler) (op.OpenIDProvider, error) {
|
||||
opConfig, err := createOPConfig(config, issuer, defaultLogoutRedirectURI, cryptoKey)
|
||||
func NewProvider(ctx context.Context, config Config, defaultLogoutRedirectURI string, externalSecure bool, command *command.Commands, query *query.Queries, repo repository.Repository, encryptionAlg crypto.EncryptionAlgorithm, cryptoKey []byte, es *eventstore.Eventstore, projections *sql.DB, userAgentCookie, instanceHandler func(http.Handler) http.Handler) (op.OpenIDProvider, error) {
|
||||
opConfig, err := createOPConfig(config, defaultLogoutRedirectURI, cryptoKey)
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowInternal(err, "OIDC-EGrqd", "cannot create op config: %w")
|
||||
}
|
||||
storage := newStorage(config, command, query, repo, keyConfig, encryptionAlg, es, projections, keyChan)
|
||||
options, err := createOptions(config, userAgentCookie, instanceHandler)
|
||||
storage := newStorage(config, command, query, repo, encryptionAlg, es, projections)
|
||||
options, err := createOptions(config, externalSecure, userAgentCookie, instanceHandler)
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowInternal(err, "OIDC-D3gq1", "cannot create options: %w")
|
||||
}
|
||||
provider, err := op.NewOpenIDProvider(
|
||||
provider, err := op.NewDynamicOpenIDProvider(
|
||||
ctx,
|
||||
HandlerPrefix,
|
||||
opConfig,
|
||||
storage,
|
||||
options...,
|
||||
@@ -104,17 +100,12 @@ func NewProvider(ctx context.Context, config Config, issuer, defaultLogoutRedire
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
func Issuer(domain string, port uint16, externalSecure bool) string {
|
||||
return http_utils.BuildHTTP(domain, port, externalSecure) + HandlerPrefix
|
||||
}
|
||||
|
||||
func createOPConfig(config Config, issuer, defaultLogoutRedirectURI string, cryptoKey []byte) (*op.Config, error) {
|
||||
func createOPConfig(config Config, defaultLogoutRedirectURI string, cryptoKey []byte) (*op.Config, error) {
|
||||
supportedLanguages, err := getSupportedLanguages()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opConfig := &op.Config{
|
||||
Issuer: issuer,
|
||||
DefaultLogoutRedirectURI: defaultLogoutRedirectURI,
|
||||
CodeMethodS256: config.CodeMethodS256,
|
||||
AuthMethodPost: config.AuthMethodPost,
|
||||
@@ -130,21 +121,26 @@ func createOPConfig(config Config, issuer, defaultLogoutRedirectURI string, cryp
|
||||
return opConfig, nil
|
||||
}
|
||||
|
||||
func createOptions(config Config, userAgentCookie, instanceHandler func(http.Handler) http.Handler) ([]op.Option, error) {
|
||||
func createOptions(config Config, externalSecure bool, userAgentCookie, instanceHandler func(http.Handler) http.Handler) ([]op.Option, error) {
|
||||
metricTypes := []metrics.MetricType{metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode, metrics.MetricTypeTotalCount}
|
||||
interceptor := op.WithHttpInterceptors(
|
||||
middleware.MetricsHandler(metricTypes),
|
||||
middleware.TelemetryHandler(),
|
||||
middleware.NoCacheInterceptor,
|
||||
instanceHandler,
|
||||
userAgentCookie,
|
||||
http_utils.CopyHeadersToContext,
|
||||
)
|
||||
endpoints := customEndpoints(config.CustomEndpoints)
|
||||
if len(endpoints) == 0 {
|
||||
return []op.Option{interceptor}, nil
|
||||
options := []op.Option{
|
||||
op.WithHttpInterceptors(
|
||||
middleware.MetricsHandler(metricTypes),
|
||||
middleware.TelemetryHandler(),
|
||||
middleware.NoCacheInterceptor,
|
||||
instanceHandler,
|
||||
userAgentCookie,
|
||||
http_utils.CopyHeadersToContext,
|
||||
),
|
||||
}
|
||||
return append(endpoints, interceptor), nil
|
||||
if !externalSecure {
|
||||
options = append(options, op.WithAllowInsecure())
|
||||
}
|
||||
endpoints := customEndpoints(config.CustomEndpoints)
|
||||
if len(endpoints) != 0 {
|
||||
options = append(options, endpoints...)
|
||||
}
|
||||
return options, nil
|
||||
}
|
||||
|
||||
func customEndpoints(endpointConfig *EndpointConfig) []op.Option {
|
||||
@@ -176,7 +172,7 @@ func customEndpoints(endpointConfig *EndpointConfig) []op.Option {
|
||||
return options
|
||||
}
|
||||
|
||||
func newStorage(config Config, command *command.Commands, query *query.Queries, repo repository.Repository, keyConfig systemdefaults.KeyConfig, encAlg crypto.EncryptionAlgorithm, es *eventstore.Eventstore, projections *sql.DB, keyChan <-chan interface{}) *OPStorage {
|
||||
func newStorage(config Config, command *command.Commands, query *query.Queries, repo repository.Repository, encAlg crypto.EncryptionAlgorithm, es *eventstore.Eventstore, projections *sql.DB) *OPStorage {
|
||||
return &OPStorage{
|
||||
repo: repo,
|
||||
command: command,
|
||||
@@ -189,10 +185,7 @@ func newStorage(config Config, command *command.Commands, query *query.Queries,
|
||||
defaultRefreshTokenIdleExpiration: config.DefaultRefreshTokenIdleExpiration,
|
||||
defaultRefreshTokenExpiration: config.DefaultRefreshTokenExpiration,
|
||||
encAlg: encAlg,
|
||||
signingKeyGracefulPeriod: keyConfig.SigningKeyGracefulPeriod,
|
||||
signingKeyRotationCheck: keyConfig.SigningKeyRotationCheck,
|
||||
locker: crdb.NewLocker(projections, locksTable, signingKey),
|
||||
keyChan: keyChan,
|
||||
assetAPIPrefix: assets.HandlerPrefix,
|
||||
}
|
||||
}
|
||||
|
@@ -8,13 +8,13 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
"github.com/caos/oidc/v2/pkg/op"
|
||||
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
|
||||
http_util "github.com/caos/zitadel/internal/api/http"
|
||||
"github.com/caos/zitadel/internal/api/http/middleware"
|
||||
)
|
||||
|
||||
@@ -59,7 +59,7 @@ func (i *spaHandler) Open(name string) (http.File, error) {
|
||||
return i.fileSystem.Open("/index.html")
|
||||
}
|
||||
|
||||
func Start(config Config, domain, url, issuer string, instanceHandler func(http.Handler) http.Handler) (http.Handler, error) {
|
||||
func Start(config Config, externalSecure bool, issuer op.IssuerFromRequest, instanceHandler func(http.Handler) http.Handler) (http.Handler, error) {
|
||||
fSys, err := fs.Sub(static, "static")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -70,7 +70,7 @@ func Start(config Config, domain, url, issuer string, instanceHandler func(http.
|
||||
config.LongCache.MaxAge,
|
||||
config.LongCache.SharedMaxAge,
|
||||
)
|
||||
security := middleware.SecurityHeaders(csp(domain), nil)
|
||||
security := middleware.SecurityHeaders(csp(), nil)
|
||||
|
||||
handler := &http.ServeMux{}
|
||||
handler.Handle("/", cache(security(http.FileServer(&spaHandler{http.FS(fSys)}))))
|
||||
@@ -80,7 +80,8 @@ func Start(config Config, domain, url, issuer string, instanceHandler func(http.
|
||||
http.Error(w, "empty instanceID", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
environmentJSON, err := createEnvironmentJSON(url, issuer, instance.ConsoleClientID())
|
||||
url := http_util.BuildOrigin(r.Host, externalSecure)
|
||||
environmentJSON, err := createEnvironmentJSON(url, issuer(r), instance.ConsoleClientID())
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("unable to marshal env for console: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -91,15 +92,12 @@ func Start(config Config, domain, url, issuer string, instanceHandler func(http.
|
||||
return handler, nil
|
||||
}
|
||||
|
||||
func csp(zitadelDomain string) *middleware.CSP {
|
||||
if !strings.HasPrefix(zitadelDomain, "*.") {
|
||||
zitadelDomain = "*." + zitadelDomain
|
||||
}
|
||||
func csp() *middleware.CSP {
|
||||
csp := middleware.DefaultSCP
|
||||
csp.StyleSrc = csp.StyleSrc.AddInline()
|
||||
csp.ScriptSrc = csp.ScriptSrc.AddEval()
|
||||
csp.ConnectSrc = csp.ConnectSrc.AddHost(zitadelDomain)
|
||||
csp.ImgSrc = csp.ImgSrc.AddHost(zitadelDomain).AddScheme("blob")
|
||||
csp.ConnectSrc = csp.ConnectSrc.AddOwnHost()
|
||||
csp.ImgSrc = csp.ImgSrc.AddOwnHost().AddScheme("blob")
|
||||
return &csp
|
||||
}
|
||||
|
||||
|
@@ -3,10 +3,10 @@ package login
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/caos/oidc/pkg/oidc"
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
"github.com/caos/oidc/v2/pkg/oidc"
|
||||
|
||||
"github.com/caos/zitadel/internal/actions"
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
iam_model "github.com/caos/zitadel/internal/iam/model"
|
||||
)
|
||||
|
@@ -7,8 +7,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/caos/oidc/pkg/client/rp"
|
||||
"github.com/caos/oidc/pkg/oidc"
|
||||
"github.com/caos/oidc/v2/pkg/client/rp"
|
||||
"github.com/caos/oidc/v2/pkg/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
|
||||
|
@@ -4,8 +4,8 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/caos/oidc/pkg/client/rp"
|
||||
"github.com/caos/oidc/pkg/oidc"
|
||||
"github.com/caos/oidc/v2/pkg/client/rp"
|
||||
"github.com/caos/oidc/v2/pkg/oidc"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
|
||||
|
@@ -9,8 +9,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
"github.com/caos/oidc/pkg/client/rp"
|
||||
"github.com/caos/oidc/pkg/oidc"
|
||||
"github.com/caos/oidc/v2/pkg/client/rp"
|
||||
"github.com/caos/oidc/v2/pkg/oidc"
|
||||
|
||||
http_util "github.com/caos/zitadel/internal/api/http"
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
|
@@ -25,18 +25,17 @@ import (
|
||||
)
|
||||
|
||||
type Login struct {
|
||||
endpoint string
|
||||
router http.Handler
|
||||
renderer *Renderer
|
||||
parser *form.Parser
|
||||
command *command.Commands
|
||||
query *query.Queries
|
||||
staticStorage static.Storage
|
||||
//staticCache cache.Cache //TODO: enable when storage is implemented again
|
||||
endpoint string
|
||||
router http.Handler
|
||||
renderer *Renderer
|
||||
parser *form.Parser
|
||||
command *command.Commands
|
||||
query *query.Queries
|
||||
staticStorage static.Storage
|
||||
authRepo auth_repository.Repository
|
||||
baseURL string
|
||||
consolePath string
|
||||
oidcAuthCallbackURL func(string) string
|
||||
oidcAuthCallbackURL func(context.Context, string) string
|
||||
idpConfigAlg crypto.EncryptionAlgorithm
|
||||
userCodeAlg crypto.EncryptionAlgorithm
|
||||
iamDomain string
|
||||
@@ -46,7 +45,6 @@ type Config struct {
|
||||
LanguageCookieName string
|
||||
CSRFCookieName string
|
||||
Cache middleware.CacheConfig
|
||||
//StaticCache cache_config.CacheConfig //TODO: enable when storage is implemented again
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -64,9 +62,10 @@ func CreateLogin(config Config,
|
||||
consolePath,
|
||||
domain,
|
||||
baseURL string,
|
||||
oidcAuthCallbackURL func(string) string,
|
||||
oidcAuthCallbackURL func(context.Context, string) string,
|
||||
externalSecure bool,
|
||||
userAgentCookie,
|
||||
issuerInterceptor,
|
||||
instanceHandler mux.MiddlewareFunc,
|
||||
userCodeAlg crypto.EncryptionAlgorithm,
|
||||
idpConfigAlg crypto.EncryptionAlgorithm,
|
||||
@@ -85,12 +84,6 @@ func CreateLogin(config Config,
|
||||
idpConfigAlg: idpConfigAlg,
|
||||
userCodeAlg: userCodeAlg,
|
||||
}
|
||||
//TODO: enable when storage is implemented again
|
||||
//login.staticCache, err = config.StaticCache.Config.NewCache()
|
||||
//if err != nil {
|
||||
// return nil, fmt.Errorf("unable to create storage cache: %w", err)
|
||||
//}
|
||||
|
||||
statikFS, err := fs.NewWithNamespace("login")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create filesystem: %w", err)
|
||||
@@ -105,7 +98,8 @@ func CreateLogin(config Config,
|
||||
return nil, fmt.Errorf("unable to create cacheInterceptor: %w", err)
|
||||
}
|
||||
security := middleware.SecurityHeaders(csp(), login.cspErrorHandler)
|
||||
login.router = CreateRouter(login, statikFS, instanceHandler, csrfInterceptor, cacheInterceptor, security, userAgentCookie, middleware.TelemetryHandler(EndpointResources))
|
||||
|
||||
login.router = CreateRouter(login, statikFS, instanceHandler, csrfInterceptor, cacheInterceptor, security, userAgentCookie, middleware.TelemetryHandler(EndpointResources), issuerInterceptor)
|
||||
login.renderer = CreateRenderer(HandlerPrefix, statikFS, staticStorage, config.LanguageCookieName, systemDefaults.DefaultLanguage)
|
||||
login.parser = form.NewParser()
|
||||
return login, nil
|
||||
|
@@ -43,11 +43,11 @@ func (l *Login) renderSuccessAndCallback(w http.ResponseWriter, r *http.Request,
|
||||
userData: l.getUserData(r, authReq, "Login Successful", errID, errMessage),
|
||||
}
|
||||
if authReq != nil {
|
||||
data.RedirectURI = l.oidcAuthCallbackURL("") //the id will be set via the html (maybe change this with the login refactoring)
|
||||
data.RedirectURI = l.oidcAuthCallbackURL(r.Context(), "") //the id will be set via the html (maybe change this with the login refactoring)
|
||||
}
|
||||
l.renderer.RenderTemplate(w, r, l.getTranslator(authReq), l.renderer.Templates[tmplLoginSuccess], data, nil)
|
||||
}
|
||||
|
||||
func (l *Login) redirectToCallback(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
|
||||
http.Redirect(w, r, l.oidcAuthCallbackURL(authReq.ID), http.StatusFound)
|
||||
http.Redirect(w, r, l.oidcAuthCallbackURL(r.Context(), authReq.ID), http.StatusFound)
|
||||
}
|
||||
|
Reference in New Issue
Block a user