diff --git a/go.mod b/go.mod index 64651d3c..6e92040d 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/auth0/go-auth0 -go 1.21.9 +go 1.21 require ( github.com/PuerkitoBio/rehttp v1.4.0 diff --git a/management/management.gen.go b/management/management.gen.go index c74c66a4..2c486c19 100644 --- a/management/management.gen.go +++ b/management/management.gen.go @@ -8227,6 +8227,101 @@ func (p *PromptPartials) String() string { return Stringify(p) } +// GetClientID returns the ClientID field if it's non-nil, zero value otherwise. +func (r *RefreshToken) GetClientID() string { + if r == nil || r.ClientID == nil { + return "" + } + return *r.ClientID +} + +// GetCreatedAt returns the CreatedAt field if it's non-nil, zero value otherwise. +func (r *RefreshToken) GetCreatedAt() time.Time { + if r == nil || r.CreatedAt == nil { + return time.Time{} + } + return *r.CreatedAt +} + +// GetExpiresAt returns the ExpiresAt field if it's non-nil, zero value otherwise. +func (r *RefreshToken) GetExpiresAt() time.Time { + if r == nil || r.ExpiresAt == nil { + return time.Time{} + } + return *r.ExpiresAt +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (r *RefreshToken) GetID() string { + if r == nil || r.ID == nil { + return "" + } + return *r.ID +} + +// GetIdleExpiresAt returns the IdleExpiresAt field if it's non-nil, zero value otherwise. +func (r *RefreshToken) GetIdleExpiresAt() time.Time { + if r == nil || r.IdleExpiresAt == nil { + return time.Time{} + } + return *r.IdleExpiresAt +} + +// GetRotating returns the Rotating field if it's non-nil, zero value otherwise. +func (r *RefreshToken) GetRotating() bool { + if r == nil || r.Rotating == nil { + return false + } + return *r.Rotating +} + +// GetSessionID returns the SessionID field if it's non-nil, zero value otherwise. +func (r *RefreshToken) GetSessionID() string { + if r == nil || r.SessionID == nil { + return "" + } + return *r.SessionID +} + +// GetUserID returns the UserID field if it's non-nil, zero value otherwise. +func (r *RefreshToken) GetUserID() string { + if r == nil || r.UserID == nil { + return "" + } + return *r.UserID +} + +// String returns a string representation of RefreshToken. +func (r *RefreshToken) String() string { + return Stringify(r) +} + +// String returns a string representation of RefreshTokenList. +func (r *RefreshTokenList) String() string { + return Stringify(r) +} + +// GetAudience returns the Audience field if it's non-nil, zero value otherwise. +func (r *RefreshTokenResourceServer) GetAudience() string { + if r == nil || r.Audience == nil { + return "" + } + return *r.Audience +} + +// GetScopes returns the Scopes field if it's non-nil, zero value otherwise. +func (r *RefreshTokenResourceServer) GetScopes() string { + if r == nil || r.Scopes == nil { + return "" + } + return *r.Scopes +} + +// String returns a string representation of RefreshTokenResourceServer. +func (r *RefreshTokenResourceServer) String() string { + return Stringify(r) +} + // GetAllowOfflineAccess returns the AllowOfflineAccess field if it's non-nil, zero value otherwise. func (r *ResourceServer) GetAllowOfflineAccess() bool { if r == nil || r.AllowOfflineAccess == nil { diff --git a/management/management.gen_test.go b/management/management.gen_test.go index 4e58a080..98f2c6dd 100644 --- a/management/management.gen_test.go +++ b/management/management.gen_test.go @@ -10345,6 +10345,130 @@ func TestPromptPartials_String(t *testing.T) { } } +func TestRefreshToken_GetClientID(tt *testing.T) { + var zeroValue string + r := &RefreshToken{ClientID: &zeroValue} + r.GetClientID() + r = &RefreshToken{} + r.GetClientID() + r = nil + r.GetClientID() +} + +func TestRefreshToken_GetCreatedAt(tt *testing.T) { + var zeroValue time.Time + r := &RefreshToken{CreatedAt: &zeroValue} + r.GetCreatedAt() + r = &RefreshToken{} + r.GetCreatedAt() + r = nil + r.GetCreatedAt() +} + +func TestRefreshToken_GetExpiresAt(tt *testing.T) { + var zeroValue time.Time + r := &RefreshToken{ExpiresAt: &zeroValue} + r.GetExpiresAt() + r = &RefreshToken{} + r.GetExpiresAt() + r = nil + r.GetExpiresAt() +} + +func TestRefreshToken_GetID(tt *testing.T) { + var zeroValue string + r := &RefreshToken{ID: &zeroValue} + r.GetID() + r = &RefreshToken{} + r.GetID() + r = nil + r.GetID() +} + +func TestRefreshToken_GetIdleExpiresAt(tt *testing.T) { + var zeroValue time.Time + r := &RefreshToken{IdleExpiresAt: &zeroValue} + r.GetIdleExpiresAt() + r = &RefreshToken{} + r.GetIdleExpiresAt() + r = nil + r.GetIdleExpiresAt() +} + +func TestRefreshToken_GetRotating(tt *testing.T) { + var zeroValue bool + r := &RefreshToken{Rotating: &zeroValue} + r.GetRotating() + r = &RefreshToken{} + r.GetRotating() + r = nil + r.GetRotating() +} + +func TestRefreshToken_GetSessionID(tt *testing.T) { + var zeroValue string + r := &RefreshToken{SessionID: &zeroValue} + r.GetSessionID() + r = &RefreshToken{} + r.GetSessionID() + r = nil + r.GetSessionID() +} + +func TestRefreshToken_GetUserID(tt *testing.T) { + var zeroValue string + r := &RefreshToken{UserID: &zeroValue} + r.GetUserID() + r = &RefreshToken{} + r.GetUserID() + r = nil + r.GetUserID() +} + +func TestRefreshToken_String(t *testing.T) { + var rawJSON json.RawMessage + v := &RefreshToken{} + if err := json.Unmarshal([]byte(v.String()), &rawJSON); err != nil { + t.Errorf("failed to produce a valid json") + } +} + +func TestRefreshTokenList_String(t *testing.T) { + var rawJSON json.RawMessage + v := &RefreshTokenList{} + if err := json.Unmarshal([]byte(v.String()), &rawJSON); err != nil { + t.Errorf("failed to produce a valid json") + } +} + +func TestRefreshTokenResourceServer_GetAudience(tt *testing.T) { + var zeroValue string + r := &RefreshTokenResourceServer{Audience: &zeroValue} + r.GetAudience() + r = &RefreshTokenResourceServer{} + r.GetAudience() + r = nil + r.GetAudience() +} + +func TestRefreshTokenResourceServer_GetScopes(tt *testing.T) { + var zeroValue string + r := &RefreshTokenResourceServer{Scopes: &zeroValue} + r.GetScopes() + r = &RefreshTokenResourceServer{} + r.GetScopes() + r = nil + r.GetScopes() +} + +func TestRefreshTokenResourceServer_String(t *testing.T) { + var rawJSON json.RawMessage + v := &RefreshTokenResourceServer{} + if err := json.Unmarshal([]byte(v.String()), &rawJSON); err != nil { + t.Errorf("failed to produce a valid json") + } +} + func TestResourceServer_GetAllowOfflineAccess(tt *testing.T) { var zeroValue bool r := &ResourceServer{AllowOfflineAccess: &zeroValue} diff --git a/management/user.go b/management/user.go index fc4a68d4..7fd50a38 100644 --- a/management/user.go +++ b/management/user.go @@ -388,6 +388,31 @@ type AuthenticationMethodList struct { Authenticators []*AuthenticationMethod `json:"authenticators,omitempty"` } +// RefreshTokenList represents a list of user refresh tokens. +type RefreshTokenList struct { + List + Tokens []*RefreshToken `json:"tokens,omitempty"` +} + +// RefreshToken represents a refresh token for a user. +type RefreshToken struct { + ID *string `json:"id,omitempty"` + UserID *string `json:"user_id,omitempty"` + CreatedAt *time.Time `json:"created_at,omitempty"` + IdleExpiresAt *time.Time `json:"idle_expires_at,omitempty"` + ExpiresAt *time.Time `json:"expires_at,omitempty"` + ClientID *string `json:"client_id,omitempty"` + SessionID *string `json:"session_id,omitempty"` + Rotating *bool `json:"rotating,omitempty"` + ResourceServer []*RefreshTokenResourceServer `json:"resource_servers,omitempty"` +} + +// RefreshTokenResourceServer represents the resource server associated with a refresh token. +type RefreshTokenResourceServer struct { + Audience *string `json:"audience,omitempty"` + Scopes *string `json:"scopes,omitempty"` +} + // UserManager manages Auth0 User resources. type UserManager manager @@ -724,3 +749,22 @@ func (m *UserManager) DeleteAllAuthenticationMethods(ctx context.Context, userID err = m.management.Request(ctx, "DELETE", m.management.URI("users", userID, "authentication-methods"), nil, opts...) return } + +// ListRefreshTokens retrieves details for a user's refresh tokens. +// +// It allows pagination using the provided options. For more information on pagination, refer to: +// https://pkg.go.dev/github.com/auth0/go-auth0/management#hdr-Page_Based_Pagination +// +// See: https://auth0.com/docs/api/management/v2#!/Users/get-refresh-tokens-for-user +func (m *UserManager) ListRefreshTokens(ctx context.Context, userID string, opts ...RequestOption) (r *RefreshTokenList, err error) { + err = m.management.Request(ctx, "GET", m.management.URI("users", userID, "refresh-tokens"), &r, applyListDefaults(opts)) + return +} + +// DeleteRefreshTokens deletes all refresh tokens for a user. +// +// See: https://auth0.com/docs/api/management/v2#!/Users/delete-refresh-tokens-for-user +func (m *UserManager) DeleteRefreshTokens(ctx context.Context, userID string, opts ...RequestOption) (err error) { + err = m.management.Request(ctx, "DELETE", m.management.URI("users", userID, "refresh-tokens"), nil, opts...) + return +} diff --git a/management/user_test.go b/management/user_test.go index 30486758..4de17c48 100644 --- a/management/user_test.go +++ b/management/user_test.go @@ -444,6 +444,98 @@ func TestUserManager_Organizations(t *testing.T) { assert.Equal(t, org.GetID(), orgs.Organizations[0].GetID()) } +// TestUserManager_ListRefreshTokens tests the ListRefreshTokens method of UserManager. +// This E2E test is skipped because refresh tokens cannot be created without UI interaction. +func TestUserManager_ListRefreshTokens(t *testing.T) { + skipTestIfRunningE2E(t) + configureHTTPTestRecordings(t) + + // RecordingNote: This test recording was manually generated to match these details. + // If any changes occur here, the test recording will need manual modification. + user := &User{ID: auth0.String("UserID")} + expectedToken1 := RefreshToken{ + ID: auth0.String("RefreshTokenID"), + UserID: auth0.String("UserID"), + CreatedAt: auth0.Time(time.Date(2024, 5, 1, 13, 0, 30, 38000000, time.UTC)), + ClientID: auth0.String("CLIENTID"), + Rotating: auth0.Bool(false), + ResourceServer: []*RefreshTokenResourceServer{ + { + Audience: auth0.String("https://go-auth0-dev.eu.auth0.com.us.auth0.com/api/v2/"), + Scopes: auth0.String("openid profile offline_access"), + }, + }, + } + expectedToken2 := RefreshToken{ + ID: auth0.String("RefreshTokenID"), + UserID: auth0.String("UserID"), + CreatedAt: auth0.Time(time.Date(2024, 5, 3, 11, 58, 27, 35000000, time.UTC)), + ClientID: auth0.String("CLIENTID"), + Rotating: auth0.Bool(false), + ResourceServer: []*RefreshTokenResourceServer{ + { + Audience: auth0.String("https://go-auth0-dev.eu.auth0.com.us.auth0.com/api/v2/"), + Scopes: auth0.String("openid profile email address phone delete:current_user_device_credentials create:current_user_device_credentials offline_access"), + }, + }, + } + + expectedTokens := []*RefreshToken{&expectedToken1, &expectedToken2} + + tokens, err := api.User.ListRefreshTokens(context.Background(), user.GetID()) + require.NoError(t, err) + assert.Equal(t, expectedTokens, tokens.Tokens) + assert.Equal(t, "RefreshTokenID", tokens.Next) +} + +// TestUserManager_DeleteRefreshTokens tests the DeleteRefreshTokens method of UserManager. +// This E2E test is skipped because refresh tokens cannot be created without UI interaction. +func TestUserManager_DeleteRefreshTokens(t *testing.T) { + skipTestIfRunningE2E(t) + configureHTTPTestRecordings(t) + + // RecordingNote: This test recording was manually generated to match these details. + // If any changes occur here, the test recording will need manual modification. + user := &User{ID: auth0.String("UserID")} + expectedToken1 := RefreshToken{ + ID: auth0.String("RefreshTokenID"), + UserID: auth0.String("UserID"), + CreatedAt: auth0.Time(time.Date(2024, 5, 1, 13, 0, 30, 38000000, time.UTC)), + ClientID: auth0.String("CLIENTID"), + Rotating: auth0.Bool(false), + ResourceServer: []*RefreshTokenResourceServer{ + { + Audience: auth0.String("https://go-auth0-dev.eu.auth0.com.us.auth0.com/api/v2/"), + Scopes: auth0.String("openid profile offline_access"), + }, + }, + } + expectedToken2 := RefreshToken{ + ID: auth0.String("RefreshTokenID"), + UserID: auth0.String("UserID"), + CreatedAt: auth0.Time(time.Date(2024, 5, 3, 11, 58, 27, 35000000, time.UTC)), + ClientID: auth0.String("CLIENTID"), + Rotating: auth0.Bool(false), + ResourceServer: []*RefreshTokenResourceServer{ + { + Audience: auth0.String("https://go-auth0-dev.eu.auth0.com.us.auth0.com/api/v2/"), + Scopes: auth0.String("openid profile email address phone delete:current_user_device_credentials create:current_user_device_credentials offline_access"), + }, + }, + } + expectedTokens := []*RefreshToken{&expectedToken1, &expectedToken2} + + tokens := retrieveRefreshTokens(t) + assert.Equal(t, expectedTokens, tokens.Tokens) + + err := api.User.DeleteRefreshTokens(context.Background(), user.GetID()) + require.NoError(t, err) + + tokensAfterDeletion := retrieveRefreshTokens(t) + assert.Empty(t, tokensAfterDeletion.Tokens) + assert.Empty(t, tokensAfterDeletion.Next) +} + func givenAUser(t *testing.T) *User { t.Helper() @@ -483,9 +575,34 @@ func givenAUser(t *testing.T) *User { return user } +// retrieveRefreshTokens retrieves refresh tokens associated with a user. +// +// This function is responsible for fetching refresh tokens from the user. +// It does not create new refresh tokens but rather retrieves existing ones. +func retrieveRefreshTokens(t *testing.T) *RefreshTokenList { + t.Helper() + user := &User{ID: auth0.String("UserID")} + + tokens, err := api.User.ListRefreshTokens(context.Background(), user.GetID()) + require.NoError(t, err) + return tokens +} + func cleanupUser(t *testing.T, userID string) { t.Helper() err := api.User.Delete(context.Background(), userID) require.NoError(t, err) } + +// skipTestIfRunningE2E skips the test if running in an end-to-end (E2E) scenario. +// +// This function is used to skip a test if it's being executed in an end-to-end (E2E) scenario +// where HTTP recordings are not enabled. +func skipTestIfRunningE2E(t *testing.T) { + t.Helper() + + if !httpRecordingsEnabled { + t.Skip("Skipped due to inability of setting this up for an E2E scenario") + } +} diff --git a/test/data/recordings/TestUserManager_DeleteRefreshTokens.yaml b/test/data/recordings/TestUserManager_DeleteRefreshTokens.yaml new file mode 100644 index 00000000..c3840492 --- /dev/null +++ b/test/data/recordings/TestUserManager_DeleteRefreshTokens.yaml @@ -0,0 +1,108 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: go-auth0-dev.eu.auth0.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Content-Type: + - application/json + User-Agent: + - Go-Auth0/1.5.0 + url: https://go-auth0-dev.eu.auth0.com/api/v2/users/UserID/refresh-tokens?include_totals=true&per_page=50 + method: GET + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: true + body: '{"tokens":[{"id":"RefreshTokenID","user_id":"UserID","created_at":"2024-05-01T13:00:30.038Z","idle_expires_at":null,"expires_at":null,"client_id":"CLIENTID","session_id":null,"rotating":false,"resource_servers":[{"audience":"https://go-auth0-dev.eu.auth0.com.us.auth0.com/api/v2/","scopes":"openid profile offline_access"}]},{"id":"RefreshTokenID","user_id":"UserID","created_at":"2024-05-03T11:58:27.035Z","idle_expires_at":null,"expires_at":null,"client_id":"CLIENTID","session_id":null,"rotating":false,"resource_servers":[{"audience":"https://go-auth0-dev.eu.auth0.com.us.auth0.com/api/v2/","scopes":"openid profile email address phone delete:current_user_device_credentials create:current_user_device_credentials offline_access"}]}],"next":"RefreshTokenID","total":2}' + headers: + Content-Type: + - application/json; charset=utf-8 + status: 200 OK + code: 200 + duration: 899.232666ms + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: go-auth0-dev.eu.auth0.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Content-Type: + - application/json + User-Agent: + - Go-Auth0/1.5.0 + url: https://go-auth0-dev.eu.auth0.com/api/v2/users/UserID/refresh-tokens + method: DELETE + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + transfer_encoding: [] + trailer: {} + content_length: 2 + uncompressed: false + body: '{}' + headers: + Content-Type: + - application/json; charset=utf-8 + status: 202 Accepted + code: 202 + duration: 375.384375ms + - id: 2 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: go-auth0-dev.eu.auth0.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Content-Type: + - application/json + User-Agent: + - Go-Auth0/1.5.0 + url: https://go-auth0-dev.eu.auth0.com/api/v2/users/UserID/refresh-tokens?include_totals=true&per_page=50 + method: GET + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + transfer_encoding: [] + trailer: {} + content_length: 23 + uncompressed: false + body: '{"tokens":[],"total":0}' + headers: + Content-Type: + - application/json; charset=utf-8 + status: 200 OK + code: 200 + duration: 432.324416ms diff --git a/test/data/recordings/TestUserManager_ListRefreshTokens.yaml b/test/data/recordings/TestUserManager_ListRefreshTokens.yaml new file mode 100644 index 00000000..7ff7b271 --- /dev/null +++ b/test/data/recordings/TestUserManager_ListRefreshTokens.yaml @@ -0,0 +1,38 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: go-auth0-dev.eu.auth0.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Content-Type: + - application/json + User-Agent: + - Go-Auth0/1.5.0 + url: https://go-auth0-dev.eu.auth0.com/api/v2/users/UserID/refresh-tokens?include_totals=true&per_page=50 + method: GET + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: true + body: '{"tokens":[{"id":"RefreshTokenID","user_id":"UserID","created_at":"2024-05-01T13:00:30.038Z","idle_expires_at":null,"expires_at":null,"client_id":"CLIENTID","session_id":null,"rotating":false,"resource_servers":[{"audience":"https://go-auth0-dev.eu.auth0.com.us.auth0.com/api/v2/","scopes":"openid profile offline_access"}]},{"id":"RefreshTokenID","user_id":"UserID","created_at":"2024-05-03T11:58:27.035Z","idle_expires_at":null,"expires_at":null,"client_id":"CLIENTID","session_id":null,"rotating":false,"resource_servers":[{"audience":"https://go-auth0-dev.eu.auth0.com.us.auth0.com/api/v2/","scopes":"openid profile email address phone delete:current_user_device_credentials create:current_user_device_credentials offline_access"}]}],"next":"RefreshTokenID","total":2}' + headers: + Content-Type: + - application/json; charset=utf-8 + status: 200 OK + code: 200 + duration: 948.362375ms