Skip to content

Commit 91398e6

Browse files
authored
Add structured authentication error handling (#441)
* Add structured authentication error handling * Refactored to remove helper functions, adjust types, and handle nil user cases
1 parent d011017 commit 91398e6

File tree

6 files changed

+788
-47
lines changed

6 files changed

+788
-47
lines changed

pkg/common/user.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package common
2+
3+
// User contains data about a particular User.
4+
type User struct {
5+
// The User's unique identifier.
6+
ID string `json:"id"`
7+
8+
// The User's first name.
9+
FirstName string `json:"first_name"`
10+
11+
// The User's last name.
12+
LastName string `json:"last_name"`
13+
14+
// The User's email.
15+
Email string `json:"email"`
16+
17+
// The timestamp of when the User was created.
18+
CreatedAt string `json:"created_at"`
19+
20+
// The timestamp of when the User was updated.
21+
UpdatedAt string `json:"updated_at"`
22+
23+
// Whether the User email is verified.
24+
EmailVerified bool `json:"email_verified"`
25+
26+
// A URL reference to an image representing the User.
27+
ProfilePictureURL string `json:"profile_picture_url"`
28+
29+
// The timestamp when the user last signed in.
30+
LastSignInAt string `json:"last_sign_in_at"`
31+
32+
// The User's external id.
33+
ExternalID string `json:"external_id"`
34+
35+
// The User's metadata.
36+
Metadata map[string]string `json:"metadata"`
37+
}

pkg/common/user_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package common
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestUserMarshalUnmarshal(t *testing.T) {
11+
user := User{
12+
ID: "user_123",
13+
14+
FirstName: "John",
15+
LastName: "Doe",
16+
EmailVerified: true,
17+
ProfilePictureURL: "https://example.com/avatar.jpg",
18+
CreatedAt: "2023-01-01T00:00:00Z",
19+
UpdatedAt: "2023-01-02T00:00:00Z",
20+
LastSignInAt: "2023-01-03T00:00:00Z",
21+
ExternalID: "ext_123",
22+
Metadata: map[string]string{
23+
"key1": "value1",
24+
"key2": "value2",
25+
},
26+
}
27+
28+
// Marshal to JSON
29+
data, err := json.Marshal(user)
30+
require.NoError(t, err)
31+
32+
// Unmarshal back to struct
33+
var unmarshaledUser User
34+
err = json.Unmarshal(data, &unmarshaledUser)
35+
require.NoError(t, err)
36+
37+
// Verify all fields are preserved
38+
require.Equal(t, user.ID, unmarshaledUser.ID)
39+
require.Equal(t, user.Email, unmarshaledUser.Email)
40+
require.Equal(t, user.FirstName, unmarshaledUser.FirstName)
41+
require.Equal(t, user.LastName, unmarshaledUser.LastName)
42+
require.Equal(t, user.EmailVerified, unmarshaledUser.EmailVerified)
43+
require.Equal(t, user.ProfilePictureURL, unmarshaledUser.ProfilePictureURL)
44+
require.Equal(t, user.CreatedAt, unmarshaledUser.CreatedAt)
45+
require.Equal(t, user.UpdatedAt, unmarshaledUser.UpdatedAt)
46+
require.Equal(t, user.LastSignInAt, unmarshaledUser.LastSignInAt)
47+
require.Equal(t, user.ExternalID, unmarshaledUser.ExternalID)
48+
require.Equal(t, user.Metadata, unmarshaledUser.Metadata)
49+
}

pkg/usermanagement/client.go

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -139,40 +139,8 @@ type OrganizationMembership struct {
139139
}
140140

141141
// User contains data about a particular User.
142-
type User struct {
143-
// The User's unique identifier.
144-
ID string `json:"id"`
145-
146-
// The User's first name.
147-
FirstName string `json:"first_name"`
148-
149-
// The User's last name.
150-
LastName string `json:"last_name"`
151-
152-
// The User's email.
153-
Email string `json:"email"`
154-
155-
// The timestamp of when the User was created.
156-
CreatedAt string `json:"created_at"`
157-
158-
// The timestamp of when the User was updated.
159-
UpdatedAt string `json:"updated_at"`
160-
161-
// Whether the User email is verified.
162-
EmailVerified bool `json:"email_verified"`
163-
164-
// A URL reference to an image representing the User.
165-
ProfilePictureURL string `json:"profile_picture_url"`
166-
167-
// The timestamp when the user last signed in.
168-
LastSignInAt string `json:"last_sign_in_at"`
169-
170-
// The User's external id.
171-
ExternalID string `json:"external_id"`
172-
173-
// The User's metadata.
174-
Metadata map[string]string `json:"metadata"`
175-
}
142+
// User is an alias for common.User to maintain backwards compatibility
143+
type User = common.User
176144

177145
// Represents User identities obtained from external identity providers.
178146
type Identity struct {
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package workos_errors
2+
3+
import (
4+
"github.com/workos/workos-go/v4/pkg/common"
5+
)
6+
7+
// Authentication error code constants
8+
const (
9+
EmailVerificationRequiredCode = "email_verification_required"
10+
MFAEnrollmentCode = "mfa_enrollment"
11+
MFAChallengeCode = "mfa_challenge"
12+
OrganizationSelectionRequiredCode = "organization_selection_required"
13+
SSORequiredCode = "sso_required"
14+
OrganizationAuthenticationMethodsRequiredCode = "organization_authentication_methods_required"
15+
)
16+
17+
// FactorType represents the type of Authentication Factor
18+
type FactorType string
19+
20+
// Constants that enumerate the available Types (matching mfa.FactorType)
21+
const (
22+
SMS FactorType = "sms"
23+
TOTP FactorType = "totp"
24+
)
25+
26+
// AuthenticationFactor represents an MFA factor
27+
type AuthenticationFactor struct {
28+
ID string `json:"id"`
29+
Type FactorType `json:"type"`
30+
}
31+
32+
// PendingAuthenticationOrganizationInfo represents an organization in selection error
33+
type PendingAuthenticationOrganizationInfo struct {
34+
ID string `json:"id"`
35+
Name string `json:"name"`
36+
}
37+
38+
// User is an alias for common.User to maintain consistency
39+
type User = common.User
40+
41+
// EmailVerificationRequiredError occurs when a user with unverified email attempts authentication
42+
type EmailVerificationRequiredError struct {
43+
HTTPError
44+
Code string `json:"code"`
45+
Message string `json:"message"`
46+
Email string `json:"email"`
47+
EmailVerificationID string `json:"email_verification_id"`
48+
PendingAuthenticationToken string `json:"pending_authentication_token"`
49+
}
50+
51+
func (e EmailVerificationRequiredError) Error() string {
52+
return e.HTTPError.Error()
53+
}
54+
55+
// MFAEnrollmentError occurs when a user needs to enroll in MFA
56+
type MFAEnrollmentError struct {
57+
HTTPError
58+
Code string `json:"code"`
59+
Message string `json:"message"`
60+
User User `json:"user"`
61+
PendingAuthenticationToken string `json:"pending_authentication_token"`
62+
}
63+
64+
func (e MFAEnrollmentError) Error() string {
65+
return e.HTTPError.Error()
66+
}
67+
68+
// MFAChallengeError occurs when a user needs to complete MFA challenge
69+
type MFAChallengeError struct {
70+
HTTPError
71+
Code string `json:"code"`
72+
Message string `json:"message"`
73+
User User `json:"user"`
74+
AuthenticationFactors []AuthenticationFactor `json:"authentication_factors"`
75+
PendingAuthenticationToken string `json:"pending_authentication_token"`
76+
}
77+
78+
func (e MFAChallengeError) Error() string {
79+
return e.HTTPError.Error()
80+
}
81+
82+
// OrganizationSelectionRequiredError occurs when user must choose an organization
83+
type OrganizationSelectionRequiredError struct {
84+
HTTPError
85+
Code string `json:"code"`
86+
Message string `json:"message"`
87+
User User `json:"user"`
88+
Organizations []PendingAuthenticationOrganizationInfo `json:"organizations"`
89+
PendingAuthenticationToken string `json:"pending_authentication_token"`
90+
}
91+
92+
func (e OrganizationSelectionRequiredError) Error() string {
93+
return e.HTTPError.Error()
94+
}
95+
96+
// SSORequiredError occurs when user must authenticate via SSO
97+
type SSORequiredError struct {
98+
HTTPError
99+
ErrorCode string `json:"error"`
100+
ErrorDescription string `json:"error_description"`
101+
Email string `json:"email"`
102+
ConnectionIDs []string `json:"connection_ids"`
103+
PendingAuthenticationToken string `json:"pending_authentication_token"`
104+
}
105+
106+
func (e SSORequiredError) Error() string {
107+
return e.HTTPError.Error()
108+
}
109+
110+
// OrganizationAuthenticationMethodsRequiredError occurs when org restricts auth methods
111+
type OrganizationAuthenticationMethodsRequiredError struct {
112+
HTTPError
113+
ErrorCode string `json:"error"`
114+
ErrorDescription string `json:"error_description"`
115+
Email string `json:"email"`
116+
SSOConnectionIDs []string `json:"sso_connection_ids"`
117+
AuthMethods map[string]bool `json:"auth_methods"`
118+
PendingAuthenticationToken string `json:"pending_authentication_token"`
119+
}
120+
121+
func (e OrganizationAuthenticationMethodsRequiredError) Error() string {
122+
return e.HTTPError.Error()
123+
}

0 commit comments

Comments
 (0)