From d13b19cc3aacf28c311b76387f9898e497d5ce44 Mon Sep 17 00:00:00 2001 From: Aviad Lichtenstadt Date: Wed, 11 Sep 2024 10:05:16 +0300 Subject: [PATCH] Fix data race on fetchKeys (#459) Add mytenants call + test --- README.md | 20 +++++- descope/api/client.go | 6 ++ descope/internal/auth/auth.go | 34 ++++++++++ descope/internal/auth/auth_test.go | 64 +++++++++++++++++++ descope/sdk/auth.go | 6 ++ .../tests/mocks/auth/authenticationmock.go | 11 ++++ descope/types.go | 10 +++ 7 files changed, 150 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 898d5941..81940fe8 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ These sections show how to use the SDK to perform various authentication/authori 11. [Tenant selection](#tenant-selection) 12. [Logging Out](#logging-out) 13. [History](#history) +14. [My Tenants](#my-tenants) ## Management Functions @@ -648,7 +649,7 @@ The request requires a valid refresh token. ```go // Refresh token will be taken from the request header or cookies automatically -loginHistoryRes, err := descopeClient.Auth.History(request, r) +loginHistoryRes, err := descopeClient.Auth.History(request) if err == nil { for i := range loginHistoryRes { fmt.Println(loginHistoryRes[i].UserID) @@ -660,6 +661,23 @@ if err == nil { } ``` +### My Tenants + +You can get the current session user tenants. +The request requires a valid refresh token. +And either a boolean to receive the current selected tenant +Or a list of tenant IDs that this user is part of + +```go +// Refresh token will be taken from the request header or cookies automatically +tenants, err := descopeClient.Auth.MyTenants(context.Background(), request, true, nil) +if err == nil { + for i := range tenants.Tenants { + + } +} +``` + ## Management Functions It is very common for some form of management or automation to be required. These can be performed diff --git a/descope/api/client.go b/descope/api/client.go index bb275ad9..ade32694 100644 --- a/descope/api/client.go +++ b/descope/api/client.go @@ -205,6 +205,7 @@ var ( refresh: "auth/refresh", selectTenant: "auth/tenant/select", me: "auth/me", + meTenants: "auth/me/tenants", history: "auth/me/history", } ) @@ -220,6 +221,7 @@ type endpoints struct { refresh string selectTenant string me string + meTenants string history string } @@ -594,6 +596,10 @@ func (e *endpoints) Me() string { return path.Join(e.version, e.me) } +func (e *endpoints) MeTenants() string { + return path.Join(e.version, e.meTenants) +} + func (e *endpoints) History() string { return path.Join(e.version, e.history) } diff --git a/descope/internal/auth/auth.go b/descope/internal/auth/auth.go index 5a5f8d5d..5e69efde 100644 --- a/descope/internal/auth/auth.go +++ b/descope/internal/auth/auth.go @@ -255,6 +255,40 @@ func (auth *authenticationService) Me(request *http.Request) (*descope.UserRespo return auth.extractUserResponse(httpResponse.BodyStr) } +func (auth *authenticationService) MyTenants(ctx context.Context, request *http.Request, dct bool, tenantIDs []string) (*descope.TenantsResponse, error) { + if request == nil { + return nil, utils.NewInvalidArgumentError("request") + } + + if dct && len(tenantIDs) > 0 { + return nil, utils.NewInvalidArgumentError("Only one of dct or tenant ids should be provided") + } + + _, refreshToken := provideTokens(request) + if refreshToken == "" { + logger.LogDebug("Unable to find tokens from cookies") + return nil, descope.ErrRefreshToken.WithMessage("Unable to find tokens from cookies") + } + + _, err := auth.validateJWT(refreshToken) + if err != nil { + logger.LogDebug("Invalid refresh token") + return nil, descope.ErrRefreshToken.WithMessage("Invalid refresh token") + } + + httpResponse, err := auth.client.DoPostRequest(ctx, api.Routes.MeTenants(), map[string]any{"dct": dct, "ids": tenantIDs}, &api.HTTPRequest{}, refreshToken) + if err != nil { + return nil, err + } + res := descope.TenantsResponse{} + err = utils.Unmarshal([]byte(httpResponse.BodyStr), &res) + if err != nil { + logger.LogError("Unable to parse tenant response", err) + return nil, err + } + return &res, nil +} + func (auth *authenticationService) History(request *http.Request) ([]*descope.UserHistoryResponse, error) { if request == nil { return nil, utils.NewInvalidArgumentError("request") diff --git a/descope/internal/auth/auth_test.go b/descope/internal/auth/auth_test.go index 657f24e6..7a47c363 100644 --- a/descope/internal/auth/auth_test.go +++ b/descope/internal/auth/auth_test.go @@ -1214,6 +1214,70 @@ func TestMeEmptyResponse(t *testing.T) { assert.Nil(t, user) } +func TestTenants(t *testing.T) { + a, err := newTestAuth(nil, func(r *http.Request) (*http.Response, error) { + m := &map[string]any{} + readBody(r, m) + assert.EqualValues(t, map[string]any{"dct": true, "ids": nil}, *m) + res := descope.TenantsResponse{Tenants: []descope.MeTenant{{ID: "a"}}} + bs, err := utils.Marshal(res) + require.NoError(t, err) + return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBufferString(string(bs)))}, nil + }) + require.NoError(t, err) + request := &http.Request{Header: http.Header{}} + request.AddCookie(&http.Cookie{Name: descope.RefreshCookieName, Value: jwtRTokenValid}) + + tnts, err := a.MyTenants(context.Background(), request, true, nil) + require.NoError(t, err) + assert.Len(t, tnts.Tenants, 1) +} + +func TestTenantsInvalidArgs(t *testing.T) { + a, err := newTestAuth(nil, func(r *http.Request) (*http.Response, error) { + res := descope.TenantsResponse{Tenants: []descope.MeTenant{{ID: "a"}}} + bs, err := utils.Marshal(res) + require.NoError(t, err) + return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBufferString(string(bs)))}, nil + }) + require.NoError(t, err) + request := &http.Request{Header: http.Header{}} + request.AddCookie(&http.Cookie{Name: descope.RefreshCookieName, Value: jwtRTokenValid}) + + _, err = a.MyTenants(context.Background(), request, true, []string{"a"}) + require.Error(t, err) +} + +func TestTenantsNoRequest(t *testing.T) { + a, err := newTestAuth(nil, DoOk(nil)) + require.NoError(t, err) + user, err := a.MyTenants(context.Background(), nil, true, nil) + assert.ErrorIs(t, err, descope.ErrInvalidArguments) + assert.Nil(t, user) +} + +func TestTenantsNoToken(t *testing.T) { + a, err := newTestAuth(nil, DoOk(nil)) + require.NoError(t, err) + request := &http.Request{Header: http.Header{}} + user, err := a.MyTenants(context.Background(), request, true, nil) + assert.ErrorIs(t, err, descope.ErrRefreshToken) + assert.ErrorContains(t, err, "Unable to find tokens") + assert.Nil(t, user) +} + +func TestTenantsInvalidToken(t *testing.T) { + a, err := newTestAuth(nil, DoOk(nil)) + require.NoError(t, err) + request := &http.Request{Header: http.Header{}} + request.AddCookie(&http.Cookie{Name: descope.RefreshCookieName, Value: jwtTokenExpired}) + + user, err := a.MyTenants(context.Background(), request, true, nil) + assert.ErrorIs(t, err, descope.ErrRefreshToken) + assert.ErrorContains(t, err, "Invalid refresh token") + assert.Nil(t, user) +} + func TestHistory(t *testing.T) { a, err := newTestAuth(nil, func(r *http.Request) (*http.Response, error) { return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBufferString(mockUserHistoryResponseBody))}, nil diff --git a/descope/sdk/auth.go b/descope/sdk/auth.go index 45e4ce24..356a0e88 100644 --- a/descope/sdk/auth.go +++ b/descope/sdk/auth.go @@ -428,6 +428,12 @@ type Authentication interface { // returns the user details or error if the refresh token is not valid. Me(request *http.Request) (*descope.UserResponse, error) + // MyTenants - Use to retrieve current session user tenants. The request requires a valid refresh token. + // returns the tenant requested or error if the refresh token is not valid. + // set dct to true - if the required tenant is the one that is set on the dct claim + // or provide a list of tenant ids that user is part of + MyTenants(ctx context.Context, request *http.Request, dct bool, tenantIDs []string) (*descope.TenantsResponse, error) + // History - Use to retrieve current session user history. The request requires a valid refresh token. // returns the user authentication history or error if the refresh token is not valid. History(request *http.Request) ([]*descope.UserHistoryResponse, error) diff --git a/descope/tests/mocks/auth/authenticationmock.go b/descope/tests/mocks/auth/authenticationmock.go index cc58f3d3..281ec258 100644 --- a/descope/tests/mocks/auth/authenticationmock.go +++ b/descope/tests/mocks/auth/authenticationmock.go @@ -696,6 +696,10 @@ type MockSession struct { MeError error MeResponse *descope.UserResponse + MyTenantsAssert func(r *http.Request, dct bool, tenantIDs []string) + MyTenantsError error + MyTenantsResponse *descope.TenantsResponse + HistoryAssert func(r *http.Request) HistoryError error HistoryResponse []*descope.UserHistoryResponse @@ -888,6 +892,13 @@ func (m *MockSession) Me(r *http.Request) (*descope.UserResponse, error) { return m.MeResponse, m.MeError } +func (m *MockSession) MyTenants(_ context.Context, r *http.Request, dct bool, tenantIDs []string) (*descope.TenantsResponse, error) { + if m.MyTenantsAssert != nil { + m.MyTenantsAssert(r, dct, tenantIDs) + } + return m.MyTenantsResponse, m.MyTenantsError +} + func (m *MockSession) History(r *http.Request) ([]*descope.UserHistoryResponse, error) { if m.HistoryAssert != nil { m.HistoryAssert(r) diff --git a/descope/types.go b/descope/types.go index 7ee9ed4d..0ab2b261 100644 --- a/descope/types.go +++ b/descope/types.go @@ -486,6 +486,16 @@ type UserResponse struct { SSOAppIDs []string `json:"ssoAppIds,omitempty"` } +type MeTenant struct { + ID string `json:"id"` + Name string `json:"name"` + CustomAttributes map[string]any `json:"customAttributes,omitempty"` +} + +type TenantsResponse struct { + Tenants []MeTenant `json:"tenants,omitempty"` +} + type UserHistoryResponse struct { UserID string `json:"userId,omitempty"` LoginTime int32 `json:"loginTime,omitempty"`