mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-19 16:56:39 +00:00
[management, infrastructure, idp] Simplified IdP Management - Embedded IdP (#5008)
Embed Dex as a built-in IdP to simplify self-hosting setup. Adds an embedded OIDC Identity Provider (Dex) with local user management and optional external IdP connectors (Google/GitHub/OIDC/SAML), plus device-auth flow for CLI login. Introduces instance onboarding/setup endpoints (including owner creation), field-level encryption for sensitive user data, a streamlined self-hosting provisioning script, and expanded APIs + test coverage for IdP management. more at https://github.com/netbirdio/netbird/pull/5008#issuecomment-3718987393
This commit is contained in:
@@ -60,6 +60,7 @@ type Validator struct {
|
||||
keysLocation string
|
||||
idpSignkeyRefreshEnabled bool
|
||||
keys *Jwks
|
||||
lastForcedRefresh time.Time
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -84,26 +85,17 @@ func NewValidator(issuer string, audienceList []string, keysLocation string, idp
|
||||
}
|
||||
}
|
||||
|
||||
// forcedRefreshCooldown is the minimum time between forced key refreshes
|
||||
// to prevent abuse from invalid tokens with fake kid values
|
||||
const forcedRefreshCooldown = 30 * time.Second
|
||||
|
||||
func (v *Validator) getKeyFunc(ctx context.Context) jwt.Keyfunc {
|
||||
return func(token *jwt.Token) (interface{}, error) {
|
||||
// If keys are rotated, verify the keys prior to token validation
|
||||
if v.idpSignkeyRefreshEnabled {
|
||||
// If the keys are invalid, retrieve new ones
|
||||
// @todo propose a separate go routine to regularly check these to prevent blocking when actually
|
||||
// validating the token
|
||||
if !v.keys.stillValid() {
|
||||
v.lock.Lock()
|
||||
defer v.lock.Unlock()
|
||||
|
||||
refreshedKeys, err := getPemKeys(v.keysLocation)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Debugf("cannot get JSONWebKey: %v, falling back to old keys", err)
|
||||
refreshedKeys = v.keys
|
||||
}
|
||||
|
||||
log.WithContext(ctx).Debugf("keys refreshed, new UTC expiration time: %s", refreshedKeys.expiresInTime.UTC())
|
||||
|
||||
v.keys = refreshedKeys
|
||||
v.refreshKeys(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +104,18 @@ func (v *Validator) getKeyFunc(ctx context.Context) jwt.Keyfunc {
|
||||
return publicKey, nil
|
||||
}
|
||||
|
||||
// If key not found and refresh is enabled, try refreshing keys and retry once.
|
||||
// This handles the case where keys were rotated but cache hasn't expired yet.
|
||||
// Use a cooldown to prevent abuse from tokens with fake kid values.
|
||||
if errors.Is(err, errKeyNotFound) && v.idpSignkeyRefreshEnabled {
|
||||
if v.forceRefreshKeys(ctx) {
|
||||
publicKey, err = getPublicKey(token, v.keys)
|
||||
if err == nil {
|
||||
return publicKey, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("getPublicKey error: %s", err)
|
||||
if errors.Is(err, errKeyNotFound) && !v.idpSignkeyRefreshEnabled {
|
||||
msg = fmt.Sprintf("getPublicKey error: %s. You can enable key refresh by setting HttpServerConfig.IdpSignKeyRefreshEnabled to true in your management.json file and restart the service", err)
|
||||
@@ -123,6 +127,46 @@ func (v *Validator) getKeyFunc(ctx context.Context) jwt.Keyfunc {
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Validator) refreshKeys(ctx context.Context) {
|
||||
v.lock.Lock()
|
||||
defer v.lock.Unlock()
|
||||
|
||||
refreshedKeys, err := getPemKeys(v.keysLocation)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Debugf("cannot get JSONWebKey: %v, falling back to old keys", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.WithContext(ctx).Debugf("keys refreshed, new UTC expiration time: %s", refreshedKeys.expiresInTime.UTC())
|
||||
v.keys = refreshedKeys
|
||||
}
|
||||
|
||||
// forceRefreshKeys refreshes keys if the cooldown period has passed.
|
||||
// Returns true if keys were refreshed, false if cooldown prevented refresh.
|
||||
// The cooldown check is done inside the lock to prevent race conditions.
|
||||
func (v *Validator) forceRefreshKeys(ctx context.Context) bool {
|
||||
v.lock.Lock()
|
||||
defer v.lock.Unlock()
|
||||
|
||||
// Check cooldown inside lock to prevent multiple goroutines from refreshing
|
||||
if time.Since(v.lastForcedRefresh) <= forcedRefreshCooldown {
|
||||
return false
|
||||
}
|
||||
|
||||
log.WithContext(ctx).Debugf("key not found in cache, forcing JWKS refresh")
|
||||
|
||||
refreshedKeys, err := getPemKeys(v.keysLocation)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Debugf("cannot get JSONWebKey: %v, falling back to old keys", err)
|
||||
return false
|
||||
}
|
||||
|
||||
log.WithContext(ctx).Debugf("keys refreshed, new UTC expiration time: %s", refreshedKeys.expiresInTime.UTC())
|
||||
v.keys = refreshedKeys
|
||||
v.lastForcedRefresh = time.Now()
|
||||
return true
|
||||
}
|
||||
|
||||
// ValidateAndParse validates the token and returns the parsed token
|
||||
func (v *Validator) ValidateAndParse(ctx context.Context, token string) (*jwt.Token, error) {
|
||||
// If the token is empty...
|
||||
@@ -165,12 +209,12 @@ func (jwks *Jwks) stillValid() bool {
|
||||
func getPemKeys(keysLocation string) (*Jwks, error) {
|
||||
jwks := &Jwks{}
|
||||
|
||||
url, err := url.ParseRequestURI(keysLocation)
|
||||
requestURI, err := url.ParseRequestURI(keysLocation)
|
||||
if err != nil {
|
||||
return jwks, err
|
||||
}
|
||||
|
||||
resp, err := http.Get(url.String())
|
||||
resp, err := http.Get(requestURI.String())
|
||||
if err != nil {
|
||||
return jwks, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user