Skip to content

Commit aec3db1

Browse files
Add users.EnrollAuthFactor() method
1 parent e0ebf21 commit aec3db1

File tree

4 files changed

+221
-32
lines changed

4 files changed

+221
-32
lines changed

pkg/users/client.go

Lines changed: 69 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/google/go-querystring/query"
99
"github.com/workos/workos-go/v2/internal/workos"
1010
"github.com/workos/workos-go/v2/pkg/common"
11+
"github.com/workos/workos-go/v2/pkg/mfa"
1112
"github.com/workos/workos-go/v2/pkg/workos_errors"
1213
"net/http"
1314
"time"
@@ -160,7 +161,8 @@ type AuthenticateWithMagicAuthOpts struct {
160161
}
161162

162163
type AuthenticationResponse struct {
163-
User User `json:"user"`
164+
Factor mfa.Factor `json:"authentication_factor"`
165+
Challenge mfa.Challenge `json:"authentication_challenge"`
164166
}
165167

166168
type SendVerificationEmailOpts struct {
@@ -210,6 +212,11 @@ type RemoveUserFromOrganizationOpts struct {
210212
Organization string `json:"organization_id"`
211213
}
212214

215+
type EnrollAuthFactorOpts struct {
216+
User string
217+
Type mfa.FactorType `json:"type"`
218+
}
219+
213220
func NewClient(apiKey string) *Client {
214221
return &Client{
215222
APIKey: apiKey,
@@ -547,7 +554,7 @@ func (c *Client) RemoveUserFromOrganization(ctx context.Context, opts RemoveUser
547554
}
548555

549556
// AuthenticateWithPassword authenticates a user with Email and Password
550-
func (c *Client) AuthenticateWithPassword(ctx context.Context, opts AuthenticateWithPasswordOpts) (AuthenticationResponse, error) {
557+
func (c *Client) AuthenticateWithPassword(ctx context.Context, opts AuthenticateWithPasswordOpts) (UserResponse, error) {
551558
payload := struct {
552559
AuthenticateWithPasswordOpts
553560
ClientSecret string `json:"client_secret"`
@@ -560,7 +567,7 @@ func (c *Client) AuthenticateWithPassword(ctx context.Context, opts Authenticate
560567

561568
jsonData, err := json.Marshal(payload)
562569
if err != nil {
563-
return AuthenticationResponse{}, err
570+
return UserResponse{}, err
564571
}
565572

566573
req, err := http.NewRequest(
@@ -570,7 +577,7 @@ func (c *Client) AuthenticateWithPassword(ctx context.Context, opts Authenticate
570577
)
571578

572579
if err != nil {
573-
return AuthenticationResponse{}, err
580+
return UserResponse{}, err
574581
}
575582

576583
// Add headers and context to the request
@@ -581,24 +588,24 @@ func (c *Client) AuthenticateWithPassword(ctx context.Context, opts Authenticate
581588
// Execute the request
582589
res, err := c.HTTPClient.Do(req)
583590
if err != nil {
584-
return AuthenticationResponse{}, err
591+
return UserResponse{}, err
585592
}
586593
defer res.Body.Close()
587594

588595
if err = workos_errors.TryGetHTTPError(res); err != nil {
589-
return AuthenticationResponse{}, err
596+
return UserResponse{}, err
590597
}
591598

592599
// Parse the JSON response
593-
var body AuthenticationResponse
600+
var body UserResponse
594601
dec := json.NewDecoder(res.Body)
595602
err = dec.Decode(&body)
596603

597604
return body, err
598605
}
599606

600607
// AuthenticateWithCode authenticates an OAuth user or a managed SSO user that is logging in through SSO
601-
func (c *Client) AuthenticateWithCode(ctx context.Context, opts AuthenticateWithCodeOpts) (AuthenticationResponse, error) {
608+
func (c *Client) AuthenticateWithCode(ctx context.Context, opts AuthenticateWithCodeOpts) (UserResponse, error) {
602609
payload := struct {
603610
AuthenticateWithCodeOpts
604611
ClientSecret string `json:"client_secret"`
@@ -611,7 +618,7 @@ func (c *Client) AuthenticateWithCode(ctx context.Context, opts AuthenticateWith
611618

612619
jsonData, err := json.Marshal(payload)
613620
if err != nil {
614-
return AuthenticationResponse{}, err
621+
return UserResponse{}, err
615622
}
616623

617624
req, err := http.NewRequest(
@@ -621,7 +628,7 @@ func (c *Client) AuthenticateWithCode(ctx context.Context, opts AuthenticateWith
621628
)
622629

623630
if err != nil {
624-
return AuthenticationResponse{}, err
631+
return UserResponse{}, err
625632
}
626633

627634
// Add headers and context to the request
@@ -632,16 +639,16 @@ func (c *Client) AuthenticateWithCode(ctx context.Context, opts AuthenticateWith
632639
// Execute the request
633640
res, err := c.HTTPClient.Do(req)
634641
if err != nil {
635-
return AuthenticationResponse{}, err
642+
return UserResponse{}, err
636643
}
637644
defer res.Body.Close()
638645

639646
if err = workos_errors.TryGetHTTPError(res); err != nil {
640-
return AuthenticationResponse{}, err
647+
return UserResponse{}, err
641648
}
642649

643650
// Parse the JSON response
644-
var body AuthenticationResponse
651+
var body UserResponse
645652
dec := json.NewDecoder(res.Body)
646653
err = dec.Decode(&body)
647654

@@ -650,7 +657,7 @@ func (c *Client) AuthenticateWithCode(ctx context.Context, opts AuthenticateWith
650657

651658
// AuthenticateWithMagicAuth authenticates a user by verifying a one-time code sent to the user's email address by
652659
// the Magic Auth Send Code endpoint.
653-
func (c *Client) AuthenticateWithMagicAuth(ctx context.Context, opts AuthenticateWithMagicAuthOpts) (AuthenticationResponse, error) {
660+
func (c *Client) AuthenticateWithMagicAuth(ctx context.Context, opts AuthenticateWithMagicAuthOpts) (UserResponse, error) {
654661
payload := struct {
655662
AuthenticateWithMagicAuthOpts
656663
ClientSecret string `json:"client_secret"`
@@ -663,7 +670,7 @@ func (c *Client) AuthenticateWithMagicAuth(ctx context.Context, opts Authenticat
663670

664671
jsonData, err := json.Marshal(payload)
665672
if err != nil {
666-
return AuthenticationResponse{}, err
673+
return UserResponse{}, err
667674
}
668675

669676
req, err := http.NewRequest(
@@ -673,7 +680,7 @@ func (c *Client) AuthenticateWithMagicAuth(ctx context.Context, opts Authenticat
673680
)
674681

675682
if err != nil {
676-
return AuthenticationResponse{}, err
683+
return UserResponse{}, err
677684
}
678685

679686
// Add headers and context to the request
@@ -684,16 +691,16 @@ func (c *Client) AuthenticateWithMagicAuth(ctx context.Context, opts Authenticat
684691
// Execute the request
685692
res, err := c.HTTPClient.Do(req)
686693
if err != nil {
687-
return AuthenticationResponse{}, err
694+
return UserResponse{}, err
688695
}
689696
defer res.Body.Close()
690697

691698
if err = workos_errors.TryGetHTTPError(res); err != nil {
692-
return AuthenticationResponse{}, err
699+
return UserResponse{}, err
693700
}
694701

695702
// Parse the JSON response
696-
var body AuthenticationResponse
703+
var body UserResponse
697704
dec := json.NewDecoder(res.Body)
698705
err = dec.Decode(&body)
699706

@@ -906,3 +913,46 @@ func (c *Client) SendMagicAuthCode(ctx context.Context, opts SendMagicAuthCodeOp
906913

907914
return body, err
908915
}
916+
917+
// EnrollAuthFactor enrolls an authentication factor for the user.
918+
func (c *Client) EnrollAuthFactor(ctx context.Context, opts EnrollAuthFactorOpts) (AuthenticationResponse, error) {
919+
endpoint := fmt.Sprintf(
920+
"%s/users/%s/auth/factors",
921+
c.Endpoint,
922+
opts.User,
923+
)
924+
925+
data, err := c.JSONEncode(opts)
926+
if err != nil {
927+
return AuthenticationResponse{}, err
928+
}
929+
930+
req, err := http.NewRequest(
931+
http.MethodPost,
932+
endpoint,
933+
bytes.NewBuffer(data),
934+
)
935+
if err != nil {
936+
return AuthenticationResponse{}, err
937+
}
938+
req = req.WithContext(ctx)
939+
req.Header.Set("User-Agent", "workos-go/"+workos.Version)
940+
req.Header.Set("Authorization", "Bearer "+c.APIKey)
941+
req.Header.Set("Content-Type", "application/json")
942+
943+
res, err := c.HTTPClient.Do(req)
944+
if err != nil {
945+
return AuthenticationResponse{}, err
946+
}
947+
defer res.Body.Close()
948+
949+
if err = workos_errors.TryGetHTTPError(res); err != nil {
950+
return AuthenticationResponse{}, err
951+
}
952+
953+
var body AuthenticationResponse
954+
dec := json.NewDecoder(res.Body)
955+
err = dec.Decode(&body)
956+
957+
return body, err
958+
}

pkg/users/client_test.go

Lines changed: 103 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"github.com/stretchr/testify/require"
77
"github.com/workos/workos-go/v2/pkg/common"
8+
"github.com/workos/workos-go/v2/pkg/mfa"
89
"net/http"
910
"net/http/httptest"
1011
"strings"
@@ -699,7 +700,7 @@ func TestAuthenticateUserWithPassword(t *testing.T) {
699700
scenario string
700701
client *Client
701702
options AuthenticateWithPasswordOpts
702-
expected AuthenticationResponse
703+
expected UserResponse
703704
err bool
704705
}{{
705706
scenario: "Request without API Key returns an error",
@@ -714,7 +715,7 @@ func TestAuthenticateUserWithPassword(t *testing.T) {
714715
715716
Password: "test_123",
716717
},
717-
expected: AuthenticationResponse{
718+
expected: UserResponse{
718719
User: User{
719720
ID: "testUserID",
720721
FirstName: "John",
@@ -749,7 +750,7 @@ func TestAuthenticateUserWithCode(t *testing.T) {
749750
scenario string
750751
client *Client
751752
options AuthenticateWithCodeOpts
752-
expected AuthenticationResponse
753+
expected UserResponse
753754
err bool
754755
}{{
755756
scenario: "Request without API Key returns an error",
@@ -763,7 +764,7 @@ func TestAuthenticateUserWithCode(t *testing.T) {
763764
ClientID: "project_123",
764765
Code: "test_123",
765766
},
766-
expected: AuthenticationResponse{
767+
expected: UserResponse{
767768
User: User{
768769
ID: "testUserID",
769770
FirstName: "John",
@@ -798,7 +799,7 @@ func TestAuthenticateUserWithMagicAuth(t *testing.T) {
798799
scenario string
799800
client *Client
800801
options AuthenticateWithMagicAuthOpts
801-
expected AuthenticationResponse
802+
expected UserResponse
802803
err bool
803804
}{{
804805
scenario: "Request without API Key returns an error",
@@ -813,7 +814,7 @@ func TestAuthenticateUserWithMagicAuth(t *testing.T) {
813814
Code: "test_123",
814815
User: "user_123",
815816
},
816-
expected: AuthenticationResponse{
817+
expected: UserResponse{
817818
User: User{
818819
ID: "testUserID",
819820
FirstName: "John",
@@ -851,7 +852,7 @@ func authenticationResponseTestHandler(w http.ResponseWriter, r *http.Request) {
851852
return
852853
}
853854
if secret, exists := payload["client_secret"].(string); exists && secret != "" {
854-
response := AuthenticationResponse{
855+
response := UserResponse{
855856
User: User{
856857
ID: "testUserID",
857858
FirstName: "John",
@@ -1282,3 +1283,98 @@ func sendMagicAuthCodeTestHandler(w http.ResponseWriter, r *http.Request) {
12821283
w.WriteHeader(http.StatusOK)
12831284
w.Write(body)
12841285
}
1286+
1287+
func TestEnrollAuthFactor(t *testing.T) {
1288+
tests := []struct {
1289+
scenario string
1290+
client *Client
1291+
options EnrollAuthFactorOpts
1292+
expected AuthenticationResponse
1293+
err bool
1294+
}{
1295+
{
1296+
scenario: "Request without API Key returns an error",
1297+
client: NewClient(""),
1298+
err: true,
1299+
},
1300+
{
1301+
scenario: "Request returns User",
1302+
client: NewClient("test"),
1303+
options: EnrollAuthFactorOpts{
1304+
User: "user_01E3JC5F5Z1YJNPGVYWV9SX6GH",
1305+
Type: mfa.TOTP,
1306+
},
1307+
expected: AuthenticationResponse{
1308+
Factor: mfa.Factor{
1309+
ID: "auth_factor_test123",
1310+
CreatedAt: "2022-02-17T22:39:26.616Z",
1311+
UpdatedAt: "2022-02-17T22:39:26.616Z",
1312+
Type: "generic_otp",
1313+
},
1314+
Challenge: mfa.Challenge{
1315+
ID: "auth_challenge_test123",
1316+
CreatedAt: "2022-02-17T22:39:26.616Z",
1317+
UpdatedAt: "2022-02-17T22:39:26.616Z",
1318+
FactorID: "auth_factor_test123",
1319+
ExpiresAt: "2022-02-17T22:39:26.616Z",
1320+
},
1321+
},
1322+
},
1323+
}
1324+
1325+
for _, test := range tests {
1326+
t.Run(test.scenario, func(t *testing.T) {
1327+
server := httptest.NewServer(http.HandlerFunc(enrollAuthFactorTestHandler))
1328+
defer server.Close()
1329+
1330+
client := test.client
1331+
client.Endpoint = server.URL
1332+
client.HTTPClient = server.Client()
1333+
1334+
user, err := client.EnrollAuthFactor(context.Background(), test.options)
1335+
if test.err {
1336+
require.Error(t, err)
1337+
return
1338+
}
1339+
require.NoError(t, err)
1340+
require.Equal(t, test.expected, user)
1341+
})
1342+
}
1343+
}
1344+
1345+
func enrollAuthFactorTestHandler(w http.ResponseWriter, r *http.Request) {
1346+
auth := r.Header.Get("Authorization")
1347+
if auth != "Bearer test" {
1348+
http.Error(w, "bad auth", http.StatusUnauthorized)
1349+
return
1350+
}
1351+
1352+
var body []byte
1353+
var err error
1354+
1355+
if r.URL.Path == "/users/user_01E3JC5F5Z1YJNPGVYWV9SX6GH/auth/factors" {
1356+
body, err = json.Marshal(AuthenticationResponse{
1357+
Factor: mfa.Factor{
1358+
ID: "auth_factor_test123",
1359+
CreatedAt: "2022-02-17T22:39:26.616Z",
1360+
UpdatedAt: "2022-02-17T22:39:26.616Z",
1361+
Type: "generic_otp",
1362+
},
1363+
Challenge: mfa.Challenge{
1364+
ID: "auth_challenge_test123",
1365+
CreatedAt: "2022-02-17T22:39:26.616Z",
1366+
UpdatedAt: "2022-02-17T22:39:26.616Z",
1367+
FactorID: "auth_factor_test123",
1368+
ExpiresAt: "2022-02-17T22:39:26.616Z",
1369+
},
1370+
})
1371+
}
1372+
1373+
if err != nil {
1374+
w.WriteHeader(http.StatusInternalServerError)
1375+
return
1376+
}
1377+
1378+
w.WriteHeader(http.StatusOK)
1379+
w.Write(body)
1380+
}

0 commit comments

Comments
 (0)