Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add jumpcloud IdP #1124

Merged
merged 36 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
cbd574d
implement jumpcloud idp manager
bcmmbaga Sep 4, 2023
f70c838
Refactor JumpCloudManager for cleaner authentication context usage
bcmmbaga Sep 4, 2023
1b752d4
Merge branch 'main' into jumpcloud-idp
bcmmbaga Sep 4, 2023
3b0bd12
update user attributes without replacing other custom attributes
bcmmbaga Sep 4, 2023
8e95904
add support for OIDC providers that may not support the audience in a…
bcmmbaga Sep 5, 2023
fd4a976
Implement PKCE token verification via Client ID for providers lacking…
bcmmbaga Sep 5, 2023
45eeda4
include an option to turn off transmitting the audience to the IDP
bcmmbaga Sep 6, 2023
50ecf6f
wip: Handle user metadata without transmitting them to Identity Provider
bcmmbaga Sep 14, 2023
8293c95
wip: load user account into cache for idp with no GetAccount support
bcmmbaga Sep 14, 2023
b7665d5
Merge branch 'main' into jumpcloud-idp
bcmmbaga Sep 20, 2023
b36bcbc
Add DeleteUser method to JumpCloud IdP manager.
bcmmbaga Sep 20, 2023
9027271
Merge branch 'main' into idp-user-cache
bcmmbaga Sep 22, 2023
519f18b
Add compatibility for the IDP lacking AppMetadata update capabilities
bcmmbaga Sep 22, 2023
a3f6de0
Refactor Authentik IdP manager
bcmmbaga Sep 22, 2023
7aa72f6
cleanup
bcmmbaga Sep 22, 2023
1c7d4e9
Merge branch 'main' into idp-user-cache
bcmmbaga Sep 25, 2023
62d5853
Refactor Zitadel IDP manager
bcmmbaga Sep 25, 2023
7706319
Refactor Keycloak IDP manager
bcmmbaga Sep 25, 2023
5adebea
Refactor Okta IDP manager
bcmmbaga Sep 26, 2023
c10ebdb
Refactor Azure IDP manager
bcmmbaga Sep 26, 2023
de1fa8b
Remove unused types declarations
bcmmbaga Sep 26, 2023
1c4e6b2
Refactor Google Workspace IdP manager
bcmmbaga Sep 26, 2023
baf2546
Merge branch 'main' into idp-user-cache
bcmmbaga Sep 26, 2023
b155ee2
Remove unused function and variables
bcmmbaga Sep 26, 2023
cebf9d9
Initialize a user data slice with defined capacity
bcmmbaga Sep 27, 2023
7c5c9a2
Initialize a user data slice with defined capacity
bcmmbaga Sep 27, 2023
1f8407a
Merge remote-tracking branch 'origin/idp-user-cache' into idp-user-cache
bcmmbaga Sep 27, 2023
e04fb2e
Initialize a user data slice with defined capacity
bcmmbaga Sep 27, 2023
dd15de6
Merge branch 'idp-user-cache' into jumpcloud-idp
bcmmbaga Oct 1, 2023
940c09b
Refactor JumpCloud IdP with new user cache implementation for self-ho…
bcmmbaga Oct 1, 2023
cc215ac
Remove unused custom error and refactor unset account id into constant
bcmmbaga Oct 1, 2023
4c27a29
Merge branch 'idp-user-cache' into jumpcloud-idp
bcmmbaga Oct 1, 2023
21bf9b3
Refactor
bcmmbaga Oct 1, 2023
b834307
Merge branch 'main' into jumpcloud-idp
bcmmbaga Oct 3, 2023
39acc49
Update map key to correct constant
bcmmbaga Oct 3, 2023
fb368ae
Add missing IDP manager check for jumpcloud
mlsmaycon Oct 3, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion client/internal/auth/pkce_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,13 @@ func (p *PKCEAuthorizationFlow) parseOAuthToken(token *oauth2.Token) (TokenInfo,
tokenInfo.IDToken = idToken
}

if err := isValidAccessToken(tokenInfo.GetTokenToUse(), p.providerConfig.Audience); err != nil {
// if a provider doesn't support an audience, use the Client ID for token verification
audience := p.providerConfig.Audience
if audience == "" {
audience = p.providerConfig.ClientID
}

if err := isValidAccessToken(tokenInfo.GetTokenToUse(), audience); err != nil {
return TokenInfo{}, fmt.Errorf("validate access token failed with error: %v", err)
}

Expand Down
3 changes: 0 additions & 3 deletions client/internal/pkce_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,6 @@ func GetPKCEAuthorizationFlowInfo(ctx context.Context, privateKey string, mgmURL

func isPKCEProviderConfigValid(config PKCEAuthProviderConfig) error {
errorMSGFormat := "invalid provider configuration received from management: %s value is empty. Contact your NetBird administrator"
if config.Audience == "" {
return fmt.Errorf(errorMSGFormat, "Audience")
}
if config.ClientID == "" {
return fmt.Errorf(errorMSGFormat, "Client ID")
}
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ require (

require (
fyne.io/fyne/v2 v2.1.4
github.com/TheJumpCloud/jcapi-go v3.0.0+incompatible
github.com/c-robinson/iplib v1.0.3
github.com/cilium/ebpf v0.10.0
github.com/coreos/go-iptables v0.7.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/TheJumpCloud/jcapi-go v3.0.0+incompatible h1:hqcTK6ZISdip65SR792lwYJTa/axESA0889D3UlZbLo=
github.com/TheJumpCloud/jcapi-go v3.0.0+incompatible/go.mod h1:6B1nuc1MUs6c62ODZDl7hVE5Pv7O2XGSkgg2olnq34I=
github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2 h1:pami0oPhVosjOu/qRHepRmdjD6hGILF7DBr+qQZeP10=
github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2/go.mod h1:jNIx5ykW1MroBuaTja9+VpglmaJOUzezumfhLlER3oY=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
Expand Down
13 changes: 12 additions & 1 deletion infrastructure_files/base.setup.env
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ NETBIRD_TOKEN_SOURCE=${NETBIRD_TOKEN_SOURCE:-accessToken}
# PKCE authorization flow
NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS=${NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS:-"53000"}
NETBIRD_AUTH_PKCE_USE_ID_TOKEN=${NETBIRD_AUTH_PKCE_USE_ID_TOKEN:-false}
NETBIRD_AUTH_PKCE_AUDIENCE=$NETBIRD_AUTH_AUDIENCE

# Dashboard

# The default setting is to transmit the audience to the IDP during authorization. However,
# if your IDP does not have this capability, you can turn this off by setting it to false.
NETBIRD_DASH_AUTH_USE_AUDIENCE=${NETBIRD_DASH_AUTH_USE_AUDIENCE:-true}
NETBIRD_DASH_AUTH_AUDIENCE=$NETBIRD_AUTH_AUDIENCE

# exports
export NETBIRD_DOMAIN
Expand Down Expand Up @@ -86,4 +94,7 @@ export NETBIRD_TOKEN_SOURCE
export NETBIRD_AUTH_DEVICE_AUTH_SCOPE
export NETBIRD_AUTH_DEVICE_AUTH_USE_ID_TOKEN
export NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT
export NETBIRD_AUTH_PKCE_USE_ID_TOKEN
export NETBIRD_AUTH_PKCE_USE_ID_TOKEN
export NETBIRD_AUTH_PKCE_AUDIENCE
export NETBIRD_DASH_AUTH_USE_AUDIENCE
export NETBIRD_DASH_AUTH_AUDIENCE
6 changes: 6 additions & 0 deletions infrastructure_files/configure.sh
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,12 @@ done

export NETBIRD_AUTH_PKCE_REDIRECT_URLS=${REDIRECT_URLS%,}

# Remove audience for providers that do not support it
if [ "$NETBIRD_DASH_AUTH_USE_AUDIENCE" = "false" ]; then
export NETBIRD_DASH_AUTH_AUDIENCE=none
export NETBIRD_AUTH_PKCE_AUDIENCE=
fi

env | grep NETBIRD

envsubst <docker-compose.yml.tmpl >docker-compose.yml
Expand Down
2 changes: 1 addition & 1 deletion infrastructure_files/docker-compose.yml.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ services:
- NETBIRD_MGMT_API_ENDPOINT=$NETBIRD_MGMT_API_ENDPOINT
- NETBIRD_MGMT_GRPC_API_ENDPOINT=$NETBIRD_MGMT_API_ENDPOINT
# OIDC
- AUTH_AUDIENCE=$NETBIRD_AUTH_AUDIENCE
- AUTH_AUDIENCE=$NETBIRD_DASH_AUTH_AUDIENCE
- AUTH_CLIENT_ID=$NETBIRD_AUTH_CLIENT_ID
- AUTH_CLIENT_SECRET=$NETBIRD_AUTH_CLIENT_SECRET
- AUTH_AUTHORITY=$NETBIRD_AUTH_AUTHORITY
Expand Down
2 changes: 1 addition & 1 deletion infrastructure_files/docker-compose.yml.tmpl.traefik
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ services:
- NETBIRD_MGMT_API_ENDPOINT=$NETBIRD_MGMT_API_ENDPOINT
- NETBIRD_MGMT_GRPC_API_ENDPOINT=$NETBIRD_MGMT_API_ENDPOINT
# OIDC
- AUTH_AUDIENCE=$NETBIRD_AUTH_AUDIENCE
- AUTH_AUDIENCE=$NETBIRD_DASH_AUTH_AUDIENCE
- AUTH_CLIENT_ID=$NETBIRD_AUTH_CLIENT_ID
- AUTH_CLIENT_SECRET=$NETBIRD_AUTH_CLIENT_SECRET
- AUTH_AUTHORITY=$NETBIRD_AUTH_AUTHORITY
Expand Down
2 changes: 1 addition & 1 deletion infrastructure_files/management.json.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
},
"PKCEAuthorizationFlow": {
"ProviderConfig": {
"Audience": "$NETBIRD_AUTH_AUDIENCE",
"Audience": "$NETBIRD_AUTH_PKCE_AUDIENCE",
"ClientID": "$NETBIRD_AUTH_CLIENT_ID",
"ClientSecret": "$NETBIRD_AUTH_CLIENT_SECRET",
"AuthorizationEndpoint": "$NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT",
Expand Down
3 changes: 3 additions & 0 deletions infrastructure_files/setup.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ NETBIRD_DOMAIN=""
# e.g., https://example.eu.auth0.com/.well-known/openid-configuration
# -------------------------------------------
NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT=""
# The default setting is to transmit the audience to the IDP during authorization. However,
# if your IDP does not have this capability, you can turn this off by setting it to false.
#NETBIRD_DASH_AUTH_USE_AUDIENCE=false
NETBIRD_AUTH_AUDIENCE=""
# e.g. netbird-client
NETBIRD_AUTH_CLIENT_ID=""
Expand Down
6 changes: 5 additions & 1 deletion management/server/idp/idp.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,11 @@ func NewManager(config Config, appMetrics telemetry.AppMetrics) (Manager, error)
CustomerID: config.ExtraConfig["CustomerId"],
}
return NewGoogleWorkspaceManager(googleClientConfig, appMetrics)

case "jumpcloud":
jumpcloudConfig := JumpCloudClientConfig{
APIToken: config.ExtraConfig["ApiToken"],
}
return NewJumpCloudManager(jumpcloudConfig, appMetrics)
default:
return nil, fmt.Errorf("invalid manager type: %s", config.ManagerType)
}
Expand Down
257 changes: 257 additions & 0 deletions management/server/idp/jumpcloud.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
package idp

import (
"context"
"fmt"
"net/http"
"strings"
"time"

v1 "github.com/TheJumpCloud/jcapi-go/v1"

"github.com/netbirdio/netbird/management/server/telemetry"
)

const (
contentType = "application/json"
accept = "application/json"
)

// JumpCloudManager JumpCloud manager client instance.
type JumpCloudManager struct {
client *v1.APIClient
apiToken string
httpClient ManagerHTTPClient
credentials ManagerCredentials
helper ManagerHelper
appMetrics telemetry.AppMetrics
}

// JumpCloudClientConfig JumpCloud manager client configurations.
type JumpCloudClientConfig struct {
APIToken string
}

// JumpCloudCredentials JumpCloud authentication information.
type JumpCloudCredentials struct {
clientConfig JumpCloudClientConfig
helper ManagerHelper
httpClient ManagerHTTPClient
appMetrics telemetry.AppMetrics
}

// NewJumpCloudManager creates a new instance of the JumpCloudManager.
func NewJumpCloudManager(config JumpCloudClientConfig, appMetrics telemetry.AppMetrics) (*JumpCloudManager, error) {
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
httpTransport.MaxIdleConns = 5

httpClient := &http.Client{
Timeout: 10 * time.Second,
Transport: httpTransport,
}
helper := JsonParser{}

if config.APIToken == "" {
return nil, fmt.Errorf("jumpCloud IdP configuration is incomplete, ApiToken is missing")
}

client := v1.NewAPIClient(v1.NewConfiguration())
credentials := &JumpCloudCredentials{
clientConfig: config,
httpClient: httpClient,
helper: helper,
appMetrics: appMetrics,
}

return &JumpCloudManager{
client: client,
apiToken: config.APIToken,
httpClient: httpClient,
credentials: credentials,
helper: helper,
appMetrics: appMetrics,
}, nil
}

// Authenticate retrieves access token to use the JumpCloud user API.
func (jc *JumpCloudCredentials) Authenticate() (JWTToken, error) {
return JWTToken{}, nil
}

func (jm *JumpCloudManager) authenticationContext() context.Context {
return context.WithValue(context.Background(), v1.ContextAPIKey, v1.APIKey{
Key: jm.apiToken,
})
}

// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
func (jm *JumpCloudManager) UpdateUserAppMetadata(_ string, _ AppMetadata) error {
return nil
}

// GetUserDataByID requests user data from JumpCloud via ID.
func (jm *JumpCloudManager) GetUserDataByID(userID string, appMetadata AppMetadata) (*UserData, error) {
authCtx := jm.authenticationContext()
user, resp, err := jm.client.SystemusersApi.SystemusersGet(authCtx, userID, contentType, accept, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
if jm.appMetrics != nil {
jm.appMetrics.IDPMetrics().CountRequestStatusError()
}
return nil, fmt.Errorf("unable to get user %s, statusCode %d", userID, resp.StatusCode)
}

if jm.appMetrics != nil {
jm.appMetrics.IDPMetrics().CountGetUserDataByID()
}

userData := parseJumpCloudUser(user)
userData.AppMetadata = appMetadata

return userData, nil
}

// GetAccount returns all the users for a given profile.
func (jm *JumpCloudManager) GetAccount(accountID string) ([]*UserData, error) {
authCtx := jm.authenticationContext()
userList, resp, err := jm.client.SearchApi.SearchSystemusersPost(authCtx, contentType, accept, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
if jm.appMetrics != nil {
jm.appMetrics.IDPMetrics().CountRequestStatusError()
}
return nil, fmt.Errorf("unable to get account %s users, statusCode %d", accountID, resp.StatusCode)
}

if jm.appMetrics != nil {
jm.appMetrics.IDPMetrics().CountGetAccount()
}

users := make([]*UserData, 0)
for _, user := range userList.Results {
userData := parseJumpCloudUser(user)
userData.AppMetadata.WTAccountID = accountID

users = append(users, userData)
}

return users, nil
}

// GetAllAccounts gets all registered accounts with corresponding user data.
// It returns a list of users indexed by accountID.
func (jm *JumpCloudManager) GetAllAccounts() (map[string][]*UserData, error) {
authCtx := jm.authenticationContext()
userList, resp, err := jm.client.SearchApi.SearchSystemusersPost(authCtx, contentType, accept, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
if jm.appMetrics != nil {
jm.appMetrics.IDPMetrics().CountRequestStatusError()
}
return nil, fmt.Errorf("unable to get all accounts, statusCode %d", resp.StatusCode)
}

if jm.appMetrics != nil {
jm.appMetrics.IDPMetrics().CountGetAllAccounts()
}

indexedUsers := make(map[string][]*UserData)
for _, user := range userList.Results {
userData := parseJumpCloudUser(user)
indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], userData)
}

return indexedUsers, nil
}

// CreateUser creates a new user in JumpCloud Idp and sends an invitation.
func (jm *JumpCloudManager) CreateUser(_, _, _, _ string) (*UserData, error) {
return nil, fmt.Errorf("method CreateUser not implemented")
}

// GetUserByEmail searches users with a given email.
// If no users have been found, this function returns an empty list.
func (jm *JumpCloudManager) GetUserByEmail(email string) ([]*UserData, error) {
searchFilter := map[string]interface{}{
"searchFilter": map[string]interface{}{
"filter": []string{email},
"fields": []string{"email"},
},
}

authCtx := jm.authenticationContext()
userList, resp, err := jm.client.SearchApi.SearchSystemusersPost(authCtx, contentType, accept, searchFilter)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
if jm.appMetrics != nil {
jm.appMetrics.IDPMetrics().CountRequestStatusError()
}
return nil, fmt.Errorf("unable to get user %s, statusCode %d", email, resp.StatusCode)
}

if jm.appMetrics != nil {
jm.appMetrics.IDPMetrics().CountGetUserByEmail()
}

usersData := make([]*UserData, 0)
for _, user := range userList.Results {
usersData = append(usersData, parseJumpCloudUser(user))
}

return usersData, nil
}

// InviteUserByID resend invitations to users who haven't activated,
// their accounts prior to the expiration period.
func (jm *JumpCloudManager) InviteUserByID(_ string) error {
return fmt.Errorf("method InviteUserByID not implemented")
}

// DeleteUser from jumpCloud directory
func (jm *JumpCloudManager) DeleteUser(userID string) error {
authCtx := jm.authenticationContext()
_, resp, err := jm.client.SystemusersApi.SystemusersDelete(authCtx, userID, contentType, accept, nil)
if err != nil {
return err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
if jm.appMetrics != nil {
jm.appMetrics.IDPMetrics().CountRequestStatusError()
}
return fmt.Errorf("unable to delete user, statusCode %d", resp.StatusCode)
}

if jm.appMetrics != nil {
jm.appMetrics.IDPMetrics().CountDeleteUser()
}

return nil
}

// parseJumpCloudUser parse JumpCloud system user returned from API V1 to UserData.
func parseJumpCloudUser(user v1.Systemuserreturn) *UserData {
names := []string{user.Firstname, user.Middlename, user.Lastname}
return &UserData{
Email: user.Email,
Name: strings.Join(names, " "),
ID: user.Id,
}
}
Loading
Loading