mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 07:16:38 +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:
@@ -78,16 +78,18 @@ func parseTime(timeString string) time.Time {
|
||||
return parsedTime
|
||||
}
|
||||
|
||||
func (c ClaimsExtractor) audienceClaim(claimName string) string {
|
||||
url, err := url.JoinPath(c.authAudience, claimName)
|
||||
func (c *ClaimsExtractor) audienceClaim(claimName string) string {
|
||||
audienceURL, err := url.JoinPath(c.authAudience, claimName)
|
||||
if err != nil {
|
||||
return c.authAudience + claimName // as it was previously
|
||||
}
|
||||
|
||||
return url
|
||||
return audienceURL
|
||||
}
|
||||
|
||||
// ToUserAuth extracts user authentication information from a JWT token
|
||||
// ToUserAuth extracts user authentication information from a JWT token.
|
||||
// The token should contain standard claims like email, name, preferred_username.
|
||||
// When using Dex, make sure to set getUserInfo: true to have these claims populated.
|
||||
func (c *ClaimsExtractor) ToUserAuth(token *jwt.Token) (auth.UserAuth, error) {
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
userAuth := auth.UserAuth{}
|
||||
@@ -120,6 +122,21 @@ func (c *ClaimsExtractor) ToUserAuth(token *jwt.Token) (auth.UserAuth, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Extract email from standard "email" claim
|
||||
if email, ok := claims["email"].(string); ok {
|
||||
userAuth.Email = email
|
||||
}
|
||||
|
||||
// Extract name from standard "name" claim
|
||||
if name, ok := claims["name"].(string); ok {
|
||||
userAuth.Name = name
|
||||
}
|
||||
|
||||
// Extract name from standard "preferred_username" claim
|
||||
if preferredName, ok := claims["preferred_username"].(string); ok {
|
||||
userAuth.PreferredName = preferredName
|
||||
}
|
||||
|
||||
return userAuth, nil
|
||||
}
|
||||
|
||||
|
||||
322
shared/auth/jwt/extractor_test.go
Normal file
322
shared/auth/jwt/extractor_test.go
Normal file
@@ -0,0 +1,322 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestClaimsExtractor_ToUserAuth_ExtractsEmailAndName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
claims jwt.MapClaims
|
||||
userIDClaim string
|
||||
audience string
|
||||
expectedUserID string
|
||||
expectedEmail string
|
||||
expectedName string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "extracts email and name from standard claims",
|
||||
claims: jwt.MapClaims{
|
||||
"sub": "user-123",
|
||||
"email": "test@example.com",
|
||||
"name": "Test User",
|
||||
},
|
||||
userIDClaim: "sub",
|
||||
expectedUserID: "user-123",
|
||||
expectedEmail: "test@example.com",
|
||||
expectedName: "Test User",
|
||||
},
|
||||
{
|
||||
name: "extracts Dex encoded user ID",
|
||||
claims: jwt.MapClaims{
|
||||
"sub": "CiQ3YWFkOGMwNS0zMjg3LTQ3M2YtYjQyYS0zNjU1MDRiZjI1ZTcSBWxvY2Fs",
|
||||
"email": "dex-user@example.com",
|
||||
"name": "Dex User",
|
||||
},
|
||||
userIDClaim: "sub",
|
||||
expectedUserID: "CiQ3YWFkOGMwNS0zMjg3LTQ3M2YtYjQyYS0zNjU1MDRiZjI1ZTcSBWxvY2Fs",
|
||||
expectedEmail: "dex-user@example.com",
|
||||
expectedName: "Dex User",
|
||||
},
|
||||
{
|
||||
name: "handles missing email claim",
|
||||
claims: jwt.MapClaims{
|
||||
"sub": "user-456",
|
||||
"name": "User Without Email",
|
||||
},
|
||||
userIDClaim: "sub",
|
||||
expectedUserID: "user-456",
|
||||
expectedEmail: "",
|
||||
expectedName: "User Without Email",
|
||||
},
|
||||
{
|
||||
name: "handles missing name claim",
|
||||
claims: jwt.MapClaims{
|
||||
"sub": "user-789",
|
||||
"email": "noname@example.com",
|
||||
},
|
||||
userIDClaim: "sub",
|
||||
expectedUserID: "user-789",
|
||||
expectedEmail: "noname@example.com",
|
||||
expectedName: "",
|
||||
},
|
||||
{
|
||||
name: "handles missing both email and name",
|
||||
claims: jwt.MapClaims{
|
||||
"sub": "user-minimal",
|
||||
},
|
||||
userIDClaim: "sub",
|
||||
expectedUserID: "user-minimal",
|
||||
expectedEmail: "",
|
||||
expectedName: "",
|
||||
},
|
||||
{
|
||||
name: "extracts preferred_username",
|
||||
claims: jwt.MapClaims{
|
||||
"sub": "user-pref",
|
||||
"email": "pref@example.com",
|
||||
"name": "Preferred User",
|
||||
"preferred_username": "prefuser",
|
||||
},
|
||||
userIDClaim: "sub",
|
||||
expectedUserID: "user-pref",
|
||||
expectedEmail: "pref@example.com",
|
||||
expectedName: "Preferred User",
|
||||
},
|
||||
{
|
||||
name: "fails when user ID claim is empty",
|
||||
claims: jwt.MapClaims{
|
||||
"email": "test@example.com",
|
||||
"name": "Test User",
|
||||
},
|
||||
userIDClaim: "sub",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "uses custom user ID claim",
|
||||
claims: jwt.MapClaims{
|
||||
"user_id": "custom-user-id",
|
||||
"email": "custom@example.com",
|
||||
"name": "Custom User",
|
||||
},
|
||||
userIDClaim: "user_id",
|
||||
expectedUserID: "custom-user-id",
|
||||
expectedEmail: "custom@example.com",
|
||||
expectedName: "Custom User",
|
||||
},
|
||||
{
|
||||
name: "extracts account ID with audience prefix",
|
||||
claims: jwt.MapClaims{
|
||||
"sub": "user-with-account",
|
||||
"email": "account@example.com",
|
||||
"name": "Account User",
|
||||
"https://api.netbird.io/wt_account_id": "account-123",
|
||||
"https://api.netbird.io/wt_account_domain": "example.com",
|
||||
},
|
||||
userIDClaim: "sub",
|
||||
audience: "https://api.netbird.io",
|
||||
expectedUserID: "user-with-account",
|
||||
expectedEmail: "account@example.com",
|
||||
expectedName: "Account User",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create extractor with options
|
||||
opts := []ClaimsExtractorOption{}
|
||||
if tt.userIDClaim != "" {
|
||||
opts = append(opts, WithUserIDClaim(tt.userIDClaim))
|
||||
}
|
||||
if tt.audience != "" {
|
||||
opts = append(opts, WithAudience(tt.audience))
|
||||
}
|
||||
extractor := NewClaimsExtractor(opts...)
|
||||
|
||||
// Create a mock token with the claims
|
||||
token := &jwt.Token{
|
||||
Claims: tt.claims,
|
||||
}
|
||||
|
||||
// Extract user auth
|
||||
userAuth, err := extractor.ToUserAuth(token)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedUserID, userAuth.UserId)
|
||||
assert.Equal(t, tt.expectedEmail, userAuth.Email)
|
||||
assert.Equal(t, tt.expectedName, userAuth.Name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaimsExtractor_ToUserAuth_PreferredUsername(t *testing.T) {
|
||||
extractor := NewClaimsExtractor(WithUserIDClaim("sub"))
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"sub": "user-123",
|
||||
"email": "test@example.com",
|
||||
"name": "Test User",
|
||||
"preferred_username": "testuser",
|
||||
}
|
||||
|
||||
token := &jwt.Token{Claims: claims}
|
||||
|
||||
userAuth, err := extractor.ToUserAuth(token)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "user-123", userAuth.UserId)
|
||||
assert.Equal(t, "test@example.com", userAuth.Email)
|
||||
assert.Equal(t, "Test User", userAuth.Name)
|
||||
assert.Equal(t, "testuser", userAuth.PreferredName)
|
||||
}
|
||||
|
||||
func TestClaimsExtractor_ToUserAuth_LastLogin(t *testing.T) {
|
||||
extractor := NewClaimsExtractor(
|
||||
WithUserIDClaim("sub"),
|
||||
WithAudience("https://api.netbird.io"),
|
||||
)
|
||||
|
||||
expectedTime := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC)
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"sub": "user-123",
|
||||
"email": "test@example.com",
|
||||
"https://api.netbird.io/nb_last_login": expectedTime.Format(time.RFC3339),
|
||||
}
|
||||
|
||||
token := &jwt.Token{Claims: claims}
|
||||
|
||||
userAuth, err := extractor.ToUserAuth(token)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expectedTime, userAuth.LastLogin)
|
||||
}
|
||||
|
||||
func TestClaimsExtractor_ToUserAuth_Invited(t *testing.T) {
|
||||
extractor := NewClaimsExtractor(
|
||||
WithUserIDClaim("sub"),
|
||||
WithAudience("https://api.netbird.io"),
|
||||
)
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"sub": "user-123",
|
||||
"email": "invited@example.com",
|
||||
"https://api.netbird.io/nb_invited": true,
|
||||
}
|
||||
|
||||
token := &jwt.Token{Claims: claims}
|
||||
|
||||
userAuth, err := extractor.ToUserAuth(token)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, userAuth.Invited)
|
||||
}
|
||||
|
||||
func TestClaimsExtractor_ToGroups(t *testing.T) {
|
||||
extractor := NewClaimsExtractor(WithUserIDClaim("sub"))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
claims jwt.MapClaims
|
||||
groupClaimName string
|
||||
expectedGroups []string
|
||||
}{
|
||||
{
|
||||
name: "extracts groups from claim",
|
||||
claims: jwt.MapClaims{
|
||||
"sub": "user-123",
|
||||
"groups": []interface{}{"admin", "users", "developers"},
|
||||
},
|
||||
groupClaimName: "groups",
|
||||
expectedGroups: []string{"admin", "users", "developers"},
|
||||
},
|
||||
{
|
||||
name: "returns empty slice when claim missing",
|
||||
claims: jwt.MapClaims{
|
||||
"sub": "user-123",
|
||||
},
|
||||
groupClaimName: "groups",
|
||||
expectedGroups: []string{},
|
||||
},
|
||||
{
|
||||
name: "handles custom claim name",
|
||||
claims: jwt.MapClaims{
|
||||
"sub": "user-123",
|
||||
"user_roles": []interface{}{"role1", "role2"},
|
||||
},
|
||||
groupClaimName: "user_roles",
|
||||
expectedGroups: []string{"role1", "role2"},
|
||||
},
|
||||
{
|
||||
name: "filters non-string values",
|
||||
claims: jwt.MapClaims{
|
||||
"sub": "user-123",
|
||||
"groups": []interface{}{"admin", 123, "users", true},
|
||||
},
|
||||
groupClaimName: "groups",
|
||||
expectedGroups: []string{"admin", "users"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
token := &jwt.Token{Claims: tt.claims}
|
||||
groups := extractor.ToGroups(token, tt.groupClaimName)
|
||||
assert.Equal(t, tt.expectedGroups, groups)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaimsExtractor_DefaultUserIDClaim(t *testing.T) {
|
||||
// When no user ID claim is specified, it should default to "sub"
|
||||
extractor := NewClaimsExtractor()
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"sub": "default-user-id",
|
||||
"email": "default@example.com",
|
||||
}
|
||||
|
||||
token := &jwt.Token{Claims: claims}
|
||||
|
||||
userAuth, err := extractor.ToUserAuth(token)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "default-user-id", userAuth.UserId)
|
||||
}
|
||||
|
||||
func TestClaimsExtractor_DexUserIDFormat(t *testing.T) {
|
||||
// Test that the extractor correctly handles Dex's encoded user ID format
|
||||
// Dex encodes user IDs as base64(protobuf{user_id, connector_id})
|
||||
extractor := NewClaimsExtractor(WithUserIDClaim("sub"))
|
||||
|
||||
// This is an actual Dex-encoded user ID
|
||||
dexEncodedID := "CiQ3YWFkOGMwNS0zMjg3LTQ3M2YtYjQyYS0zNjU1MDRiZjI1ZTcSBWxvY2Fs"
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"sub": dexEncodedID,
|
||||
"email": "dex@example.com",
|
||||
"name": "Dex User",
|
||||
}
|
||||
|
||||
token := &jwt.Token{Claims: claims}
|
||||
|
||||
userAuth, err := extractor.ToUserAuth(token)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The extractor should pass through the encoded ID as-is
|
||||
// Decoding is done elsewhere (e.g., in the Dex provider)
|
||||
assert.Equal(t, dexEncodedID, userAuth.UserId)
|
||||
assert.Equal(t, "dex@example.com", userAuth.Email)
|
||||
assert.Equal(t, "Dex User", userAuth.Name)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -18,6 +18,15 @@ type UserAuth struct {
|
||||
|
||||
// The user id
|
||||
UserId string
|
||||
// The user's email address
|
||||
// (optional, may be empty if not in token, make sure to set getUserInfo: true in Dex to have this field)
|
||||
Email string
|
||||
// The user's name
|
||||
// (optional, may be empty if not in token, make sure to set getUserInfo: true in Dex to have this field)
|
||||
Name string
|
||||
// The user's preferred name
|
||||
// (optional, may be empty if not in token, make sure to set getUserInfo: true in Dex to have this field)
|
||||
PreferredName string
|
||||
// Last login time for this user
|
||||
LastLogin time.Time
|
||||
// The Groups the user belongs to on this account
|
||||
|
||||
@@ -129,7 +129,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, secretsManager, nil, nil, mgmt.MockIntegratedValidator{}, networkMapController)
|
||||
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, secretsManager, nil, nil, mgmt.MockIntegratedValidator{}, networkMapController, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,10 @@ tags:
|
||||
- name: Ingress Ports
|
||||
description: Interact with and view information about the ingress peers and ports.
|
||||
x-cloud-only: true
|
||||
- name: Identity Providers
|
||||
description: Interact with and view information about identity providers.
|
||||
- name: Instance
|
||||
description: Instance setup and status endpoints for initial configuration.
|
||||
components:
|
||||
schemas:
|
||||
Account:
|
||||
@@ -149,6 +153,11 @@ components:
|
||||
description: Set Clients auto-update version. "latest", "disabled", or a specific version (e.g "0.50.1")
|
||||
type: string
|
||||
example: "0.51.2"
|
||||
embedded_idp_enabled:
|
||||
description: Indicates whether the embedded identity provider (Dex) is enabled for this account. This is a read-only field.
|
||||
type: boolean
|
||||
readOnly: true
|
||||
example: false
|
||||
required:
|
||||
- peer_login_expiration_enabled
|
||||
- peer_login_expiration
|
||||
@@ -206,6 +215,10 @@ components:
|
||||
description: User's email address
|
||||
type: string
|
||||
example: demo@netbird.io
|
||||
password:
|
||||
description: User's password. Only present when user is created (create user endpoint is called) and only when IdP supports user creation with password.
|
||||
type: string
|
||||
example: super_secure_password
|
||||
name:
|
||||
description: User's name from idp provider
|
||||
type: string
|
||||
@@ -252,6 +265,10 @@ components:
|
||||
description: How user was issued by API or Integration
|
||||
type: string
|
||||
example: api
|
||||
idp_id:
|
||||
description: Identity provider ID (connector ID) that the user authenticated with. Only populated for users with Dex-encoded user IDs.
|
||||
type: string
|
||||
example: okta-abc123
|
||||
permissions:
|
||||
$ref: '#/components/schemas/UserPermissions'
|
||||
required:
|
||||
@@ -2250,6 +2267,118 @@ components:
|
||||
- page_size
|
||||
- total_records
|
||||
- total_pages
|
||||
IdentityProviderType:
|
||||
type: string
|
||||
description: Type of identity provider
|
||||
enum:
|
||||
- oidc
|
||||
- zitadel
|
||||
- entra
|
||||
- google
|
||||
- okta
|
||||
- pocketid
|
||||
- microsoft
|
||||
example: oidc
|
||||
IdentityProvider:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
description: Identity provider ID
|
||||
type: string
|
||||
example: ch8i4ug6lnn4g9hqv7l0
|
||||
type:
|
||||
$ref: '#/components/schemas/IdentityProviderType'
|
||||
name:
|
||||
description: Human-readable name for the identity provider
|
||||
type: string
|
||||
example: My OIDC Provider
|
||||
issuer:
|
||||
description: OIDC issuer URL
|
||||
type: string
|
||||
example: https://accounts.google.com
|
||||
client_id:
|
||||
description: OAuth2 client ID
|
||||
type: string
|
||||
example: 123456789.apps.googleusercontent.com
|
||||
required:
|
||||
- type
|
||||
- name
|
||||
- issuer
|
||||
- client_id
|
||||
IdentityProviderRequest:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
$ref: '#/components/schemas/IdentityProviderType'
|
||||
name:
|
||||
description: Human-readable name for the identity provider
|
||||
type: string
|
||||
example: My OIDC Provider
|
||||
issuer:
|
||||
description: OIDC issuer URL
|
||||
type: string
|
||||
example: https://accounts.google.com
|
||||
client_id:
|
||||
description: OAuth2 client ID
|
||||
type: string
|
||||
example: 123456789.apps.googleusercontent.com
|
||||
client_secret:
|
||||
description: OAuth2 client secret
|
||||
type: string
|
||||
example: secret123
|
||||
required:
|
||||
- type
|
||||
- name
|
||||
- issuer
|
||||
- client_id
|
||||
- client_secret
|
||||
InstanceStatus:
|
||||
type: object
|
||||
description: Instance status information
|
||||
properties:
|
||||
setup_required:
|
||||
description: Indicates whether the instance requires initial setup
|
||||
type: boolean
|
||||
example: true
|
||||
required:
|
||||
- setup_required
|
||||
SetupRequest:
|
||||
type: object
|
||||
description: Request to set up the initial admin user
|
||||
properties:
|
||||
email:
|
||||
description: Email address for the admin user
|
||||
type: string
|
||||
example: admin@example.com
|
||||
password:
|
||||
description: Password for the admin user (minimum 8 characters)
|
||||
type: string
|
||||
format: password
|
||||
minLength: 8
|
||||
example: securepassword123
|
||||
name:
|
||||
description: Display name for the admin user (defaults to email if not provided)
|
||||
type: string
|
||||
example: Admin User
|
||||
required:
|
||||
- email
|
||||
- password
|
||||
- name
|
||||
SetupResponse:
|
||||
type: object
|
||||
description: Response after successful instance setup
|
||||
properties:
|
||||
user_id:
|
||||
description: The ID of the created user
|
||||
type: string
|
||||
example: abc123def456
|
||||
email:
|
||||
description: Email address of the created user
|
||||
type: string
|
||||
example: admin@example.com
|
||||
required:
|
||||
- user_id
|
||||
- email
|
||||
responses:
|
||||
not_found:
|
||||
description: Resource not found
|
||||
@@ -2287,6 +2416,48 @@ security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
paths:
|
||||
/api/instance:
|
||||
get:
|
||||
summary: Get Instance Status
|
||||
description: Returns the instance status including whether initial setup is required. This endpoint does not require authentication.
|
||||
tags: [ Instance ]
|
||||
security: [ ]
|
||||
responses:
|
||||
'200':
|
||||
description: Instance status information
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/InstanceStatus'
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
/api/setup:
|
||||
post:
|
||||
summary: Setup Instance
|
||||
description: Creates the initial admin user for the instance. This endpoint does not require authentication but only works when setup is required (no accounts exist and embedded IDP is enabled).
|
||||
tags: [ Instance ]
|
||||
security: [ ]
|
||||
requestBody:
|
||||
description: Initial admin user details
|
||||
required: true
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: '#/components/schemas/SetupRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Setup completed successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SetupResponse'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'412':
|
||||
description: Setup already completed
|
||||
content: { }
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
/api/accounts:
|
||||
get:
|
||||
summary: List all Accounts
|
||||
@@ -4877,3 +5048,147 @@ paths:
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
/api/identity-providers:
|
||||
get:
|
||||
summary: List all Identity Providers
|
||||
description: Returns a list of all identity providers configured for the account
|
||||
tags: [ Identity Providers ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
responses:
|
||||
'200':
|
||||
description: A JSON array of identity providers
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/IdentityProvider'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
post:
|
||||
summary: Create an Identity Provider
|
||||
description: Creates a new identity provider configuration
|
||||
tags: [ Identity Providers ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
requestBody:
|
||||
description: Identity provider configuration
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: '#/components/schemas/IdentityProviderRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: An Identity Provider object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IdentityProvider'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
/api/identity-providers/{idpId}:
|
||||
get:
|
||||
summary: Retrieve an Identity Provider
|
||||
description: Get information about a specific identity provider
|
||||
tags: [ Identity Providers ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: idpId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of an identity provider
|
||||
responses:
|
||||
'200':
|
||||
description: An Identity Provider object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IdentityProvider'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
put:
|
||||
summary: Update an Identity Provider
|
||||
description: Update an existing identity provider configuration
|
||||
tags: [ Identity Providers ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: idpId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of an identity provider
|
||||
requestBody:
|
||||
description: Identity provider update
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: '#/components/schemas/IdentityProviderRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: An Identity Provider object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IdentityProvider'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
delete:
|
||||
summary: Delete an Identity Provider
|
||||
description: Delete an identity provider configuration
|
||||
tags: [ Identity Providers ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: idpId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of an identity provider
|
||||
responses:
|
||||
'200':
|
||||
description: Delete status code
|
||||
content: { }
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
|
||||
@@ -83,6 +83,17 @@ const (
|
||||
GroupMinimumIssuedJwt GroupMinimumIssued = "jwt"
|
||||
)
|
||||
|
||||
// Defines values for IdentityProviderType.
|
||||
const (
|
||||
IdentityProviderTypeEntra IdentityProviderType = "entra"
|
||||
IdentityProviderTypeGoogle IdentityProviderType = "google"
|
||||
IdentityProviderTypeMicrosoft IdentityProviderType = "microsoft"
|
||||
IdentityProviderTypeOidc IdentityProviderType = "oidc"
|
||||
IdentityProviderTypeOkta IdentityProviderType = "okta"
|
||||
IdentityProviderTypePocketid IdentityProviderType = "pocketid"
|
||||
IdentityProviderTypeZitadel IdentityProviderType = "zitadel"
|
||||
)
|
||||
|
||||
// Defines values for IngressPortAllocationPortMappingProtocol.
|
||||
const (
|
||||
IngressPortAllocationPortMappingProtocolTcp IngressPortAllocationPortMappingProtocol = "tcp"
|
||||
@@ -298,8 +309,11 @@ type AccountSettings struct {
|
||||
AutoUpdateVersion *string `json:"auto_update_version,omitempty"`
|
||||
|
||||
// DnsDomain Allows to define a custom dns domain for the account
|
||||
DnsDomain *string `json:"dns_domain,omitempty"`
|
||||
Extra *AccountExtraSettings `json:"extra,omitempty"`
|
||||
DnsDomain *string `json:"dns_domain,omitempty"`
|
||||
|
||||
// EmbeddedIdpEnabled Indicates whether the embedded identity provider (Dex) is enabled for this account. This is a read-only field.
|
||||
EmbeddedIdpEnabled *bool `json:"embedded_idp_enabled,omitempty"`
|
||||
Extra *AccountExtraSettings `json:"extra,omitempty"`
|
||||
|
||||
// GroupsPropagationEnabled Allows propagate the new user auto groups to peers that belongs to the user
|
||||
GroupsPropagationEnabled *bool `json:"groups_propagation_enabled,omitempty"`
|
||||
@@ -520,6 +534,45 @@ type GroupRequest struct {
|
||||
Resources *[]Resource `json:"resources,omitempty"`
|
||||
}
|
||||
|
||||
// IdentityProvider defines model for IdentityProvider.
|
||||
type IdentityProvider struct {
|
||||
// ClientId OAuth2 client ID
|
||||
ClientId string `json:"client_id"`
|
||||
|
||||
// Id Identity provider ID
|
||||
Id *string `json:"id,omitempty"`
|
||||
|
||||
// Issuer OIDC issuer URL
|
||||
Issuer string `json:"issuer"`
|
||||
|
||||
// Name Human-readable name for the identity provider
|
||||
Name string `json:"name"`
|
||||
|
||||
// Type Type of identity provider
|
||||
Type IdentityProviderType `json:"type"`
|
||||
}
|
||||
|
||||
// IdentityProviderRequest defines model for IdentityProviderRequest.
|
||||
type IdentityProviderRequest struct {
|
||||
// ClientId OAuth2 client ID
|
||||
ClientId string `json:"client_id"`
|
||||
|
||||
// ClientSecret OAuth2 client secret
|
||||
ClientSecret string `json:"client_secret"`
|
||||
|
||||
// Issuer OIDC issuer URL
|
||||
Issuer string `json:"issuer"`
|
||||
|
||||
// Name Human-readable name for the identity provider
|
||||
Name string `json:"name"`
|
||||
|
||||
// Type Type of identity provider
|
||||
Type IdentityProviderType `json:"type"`
|
||||
}
|
||||
|
||||
// IdentityProviderType Type of identity provider
|
||||
type IdentityProviderType string
|
||||
|
||||
// IngressPeer defines model for IngressPeer.
|
||||
type IngressPeer struct {
|
||||
AvailablePorts AvailablePorts `json:"available_ports"`
|
||||
@@ -653,6 +706,12 @@ type IngressPortAllocationRequestPortRange struct {
|
||||
// IngressPortAllocationRequestPortRangeProtocol The protocol accepted by the port range
|
||||
type IngressPortAllocationRequestPortRangeProtocol string
|
||||
|
||||
// InstanceStatus Instance status information
|
||||
type InstanceStatus struct {
|
||||
// SetupRequired Indicates whether the instance requires initial setup
|
||||
SetupRequired bool `json:"setup_required"`
|
||||
}
|
||||
|
||||
// Location Describe geographical location information
|
||||
type Location struct {
|
||||
// CityName Commonly used English name of the city
|
||||
@@ -1833,6 +1892,27 @@ type SetupKeyRequest struct {
|
||||
Revoked bool `json:"revoked"`
|
||||
}
|
||||
|
||||
// SetupRequest Request to set up the initial admin user
|
||||
type SetupRequest struct {
|
||||
// Email Email address for the admin user
|
||||
Email string `json:"email"`
|
||||
|
||||
// Name Display name for the admin user (defaults to email if not provided)
|
||||
Name string `json:"name"`
|
||||
|
||||
// Password Password for the admin user (minimum 8 characters)
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// SetupResponse Response after successful instance setup
|
||||
type SetupResponse struct {
|
||||
// Email Email address of the created user
|
||||
Email string `json:"email"`
|
||||
|
||||
// UserId The ID of the created user
|
||||
UserId string `json:"user_id"`
|
||||
}
|
||||
|
||||
// User defines model for User.
|
||||
type User struct {
|
||||
// AutoGroups Group IDs to auto-assign to peers registered by this user
|
||||
@@ -1844,6 +1924,9 @@ type User struct {
|
||||
// Id User ID
|
||||
Id string `json:"id"`
|
||||
|
||||
// IdpId Identity provider ID (connector ID) that the user authenticated with. Only populated for users with Dex-encoded user IDs.
|
||||
IdpId *string `json:"idp_id,omitempty"`
|
||||
|
||||
// IsBlocked Is true if this user is blocked. Blocked users can't use the system
|
||||
IsBlocked bool `json:"is_blocked"`
|
||||
|
||||
@@ -1862,6 +1945,9 @@ type User struct {
|
||||
// Name User's name from idp provider
|
||||
Name string `json:"name"`
|
||||
|
||||
// Password User's password. Only present when user is created (create user endpoint is called) and only when IdP supports user creation with password.
|
||||
Password *string `json:"password,omitempty"`
|
||||
|
||||
// PendingApproval Is true if this user requires approval before being activated. Only applicable for users joining via domain matching when user_approval_required is enabled.
|
||||
PendingApproval bool `json:"pending_approval"`
|
||||
Permissions *UserPermissions `json:"permissions,omitempty"`
|
||||
@@ -2003,6 +2089,12 @@ type PostApiGroupsJSONRequestBody = GroupRequest
|
||||
// PutApiGroupsGroupIdJSONRequestBody defines body for PutApiGroupsGroupId for application/json ContentType.
|
||||
type PutApiGroupsGroupIdJSONRequestBody = GroupRequest
|
||||
|
||||
// PostApiIdentityProvidersJSONRequestBody defines body for PostApiIdentityProviders for application/json ContentType.
|
||||
type PostApiIdentityProvidersJSONRequestBody = IdentityProviderRequest
|
||||
|
||||
// PutApiIdentityProvidersIdpIdJSONRequestBody defines body for PutApiIdentityProvidersIdpId for application/json ContentType.
|
||||
type PutApiIdentityProvidersIdpIdJSONRequestBody = IdentityProviderRequest
|
||||
|
||||
// PostApiIngressPeersJSONRequestBody defines body for PostApiIngressPeers for application/json ContentType.
|
||||
type PostApiIngressPeersJSONRequestBody = IngressPeerCreateRequest
|
||||
|
||||
@@ -2057,6 +2149,9 @@ type PostApiRoutesJSONRequestBody = RouteRequest
|
||||
// PutApiRoutesRouteIdJSONRequestBody defines body for PutApiRoutesRouteId for application/json ContentType.
|
||||
type PutApiRoutesRouteIdJSONRequestBody = RouteRequest
|
||||
|
||||
// PostApiSetupJSONRequestBody defines body for PostApiSetup for application/json ContentType.
|
||||
type PostApiSetupJSONRequestBody = SetupRequest
|
||||
|
||||
// PostApiSetupKeysJSONRequestBody defines body for PostApiSetupKeys for application/json ContentType.
|
||||
type PostApiSetupKeysJSONRequestBody = CreateSetupKeyRequest
|
||||
|
||||
|
||||
Reference in New Issue
Block a user