From 709ab52f35f48aaaf6289017f6b4fb62b0d14828 Mon Sep 17 00:00:00 2001 From: Jonas Hiltl <71456708+JonasHiltl@users.noreply.github.com> Date: Mon, 3 Jun 2024 15:37:43 +0200 Subject: [PATCH] Add Identity functionality to User Management (#343) * Add ListIdentities function to user management * fix method: POST -> GET --- pkg/usermanagement/client.go | 62 +++++++++++++++++++++- pkg/usermanagement/client_test.go | 86 +++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 1 deletion(-) diff --git a/pkg/usermanagement/client.go b/pkg/usermanagement/client.go index fb1ef44e..9133a06d 100644 --- a/pkg/usermanagement/client.go +++ b/pkg/usermanagement/client.go @@ -66,7 +66,7 @@ type Invitation struct { AcceptedAt string `json:"accepted_at,omitempty"` RevokedAt string `json:"revoked_at,omitempty"` Token string `json:"token"` - AcceptInvitationUrl string `json:"accept_invitation_url` + AcceptInvitationUrl string `json:"accept_invitation_url"` OrganizationID string `json:"organization_id,omitempty"` InviterUserID string `json:"inviter_user_id,omitempty"` ExpiresAt string `json:"expires_at"` @@ -165,6 +165,16 @@ type User struct { ProfilePictureURL string `json:"profile_picture_url"` } +// Represents User identities obtained from external identity providers. +type Identity struct { + // The unique ID of the user in the external identity provider. + IdpID string `json:"idp_id"` + // The type of the identity. + Type string `json:"type"` + // The type of OAuth provider for the identity. + Provider string `json:"provider"` +} + // GetUserOpts contains the options to pass in order to get a user profile. type GetUserOpts struct { // User unique identifier @@ -530,6 +540,14 @@ type RevokeSessionOpts struct { SessionID string `json:"session_id"` } +type ListIdentitiesResult struct { + Identities []Identity `json:"identities"` +} + +type ListIdentitiesOpts struct { + ID string `json:"id"` +} + func NewClient(apiKey string) *Client { return &Client{ APIKey: apiKey, @@ -745,6 +763,48 @@ func (c *Client) DeleteUser(ctx context.Context, opts DeleteUserOpts) error { return workos_errors.TryGetHTTPError(res) } +func (c *Client) ListIdentities(ctx context.Context, opts ListIdentitiesOpts) (ListIdentitiesResult, error) { + endpoint := fmt.Sprintf( + "%s/user_management/users/%s/identities", + c.Endpoint, + opts.ID, + ) + + data, err := c.JSONEncode(opts) + if err != nil { + return ListIdentitiesResult{}, err + } + + req, err := http.NewRequest( + http.MethodGet, + endpoint, + bytes.NewBuffer(data), + ) + if err != nil { + return ListIdentitiesResult{}, err + } + req = req.WithContext(ctx) + req.Header.Set("User-Agent", "workos-go/"+workos.Version) + req.Header.Set("Authorization", "Bearer "+c.APIKey) + req.Header.Set("Content-Type", "application/json") + + res, err := c.HTTPClient.Do(req) + if err != nil { + return ListIdentitiesResult{}, err + } + defer res.Body.Close() + + if err = workos_errors.TryGetHTTPError(res); err != nil { + return ListIdentitiesResult{}, err + } + + var body ListIdentitiesResult + dec := json.NewDecoder(res.Body) + err = dec.Decode(&body) + + return body, err +} + // GetAuthorizationURLOpts contains the options to pass in order to generate // an authorization url. type GetAuthorizationURLOpts struct { diff --git a/pkg/usermanagement/client_test.go b/pkg/usermanagement/client_test.go index 3d94d2b9..1f386241 100644 --- a/pkg/usermanagement/client_test.go +++ b/pkg/usermanagement/client_test.go @@ -509,6 +509,92 @@ func deleteUserTestHandler(w http.ResponseWriter, r *http.Request) { w.Write(body) } +func TestListIdentities(t *testing.T) { + tests := []struct { + scenario string + client *Client + options ListIdentitiesOpts + expected ListIdentitiesResult + err bool + }{ + { + scenario: "Request without API Key returns an error", + client: NewClient(""), + err: true, + }, + { + scenario: "Request returns identities", + client: NewClient("test"), + options: ListIdentitiesOpts{ + ID: "user_01E3JC5F5Z1YJNPGVYWV9SX6GH", + }, + expected: ListIdentitiesResult{ + Identities: []Identity{ + { + IdpID: "13966412", + Type: "OAuth", + Provider: "GitHubOAuth", + }, + }, + }, + err: false, + }, + } + + for _, test := range tests { + t.Run(test.scenario, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(listIdentitiesTestHandler)) + defer server.Close() + + client := test.client + client.Endpoint = server.URL + client.HTTPClient = server.Client() + + identities, err := client.ListIdentities(context.Background(), test.options) + if test.err { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, test.expected, identities) + }) + } +} + +func listIdentitiesTestHandler(w http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + if auth != "Bearer test" { + http.Error(w, "bad auth", http.StatusUnauthorized) + return + } + + if r.Method != http.MethodGet { + http.Error(w, "bad method", http.StatusBadRequest) + } + + if userAgent := r.Header.Get("User-Agent"); !strings.Contains(userAgent, "workos-go/") { + w.WriteHeader(http.StatusBadRequest) + return + } + + body, err := json.Marshal(ListIdentitiesResult{ + Identities: []Identity{ + { + IdpID: "13966412", + Type: "OAuth", + Provider: "GitHubOAuth", + }, + }, + }) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Write(body) +} + func TestClientAuthorizeURL(t *testing.T) { tests := []struct { scenario string