Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
module github.com/workos/workos-go/v4

go 1.13
go 1.22

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go 1.13 is fairly old, but changing this is a breaking change if I'm not mistaken.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of the dependencies I think won't work with versions of go less than 1.22. Would we consider a major release instead of a minor one for this?


toolchain go1.23.2

require (
github.com/MicahParks/keyfunc v1.9.0
github.com/fernet/fernet-go v0.0.0-20240119011108-303da6aec611
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/go-querystring v1.0.0
github.com/stretchr/testify v1.10.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
17 changes: 8 additions & 9 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fernet/fernet-go v0.0.0-20240119011108-303da6aec611 h1:JwYtKJ/DVEoIA5dH45OEU7uoryZY/gjd/BQiwwAOImM=
github.com/fernet/fernet-go v0.0.0-20240119011108-303da6aec611/go.mod h1:zHMNeYgqrTpKyjawjitDg0Osd1P/FmeA0SZLYK3RfLQ=
github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs=
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
139 changes: 104 additions & 35 deletions pkg/usermanagement/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,19 +261,21 @@ type DeleteUserOpts struct {
}

type AuthenticateWithPasswordOpts struct {
ClientID string `json:"client_id"`
Email string `json:"email"`
Password string `json:"password"`
IPAddress string `json:"ip_address,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
ClientID string `json:"client_id"`
Email string `json:"email"`
Password string `json:"password"`
IPAddress string `json:"ip_address,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
Session Session `json:"session,omitempty"`
}

type AuthenticateWithCodeOpts struct {
ClientID string `json:"client_id"`
Code string `json:"code"`
CodeVerifier string `json:"code_verifier,omitempty"`
IPAddress string `json:"ip_address,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
ClientID string `json:"client_id"`
Code string `json:"code"`
CodeVerifier string `json:"code_verifier,omitempty"`
IPAddress string `json:"ip_address,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
Session Session `json:"session,omitempty"`
}

type AuthenticateWithRefreshTokenOpts struct {
Expand All @@ -292,34 +294,38 @@ type AuthenticateWithMagicAuthOpts struct {
// An authorization code used in a previous authenticate request that resulted in an existing user error response.
// By specifying link_authorization_code, the Magic Auth authentication will link the credentials of the previous
// authorization code with this user.
LinkAuthorizationCode string `json:"link_authorization_code,omitempty"`
IPAddress string `json:"ip_address,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
LinkAuthorizationCode string `json:"link_authorization_code,omitempty"`
IPAddress string `json:"ip_address,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
Session Session `json:"session,omitempty"`
}

type AuthenticateWithTOTPOpts struct {
ClientID string `json:"client_id"`
Code string `json:"code"`
IPAddress string `json:"ip_address,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
PendingAuthenticationToken string `json:"pending_authentication_token"`
AuthenticationChallengeID string `json:"authentication_challenge_id"`
ClientID string `json:"client_id"`
Code string `json:"code"`
IPAddress string `json:"ip_address,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
PendingAuthenticationToken string `json:"pending_authentication_token"`
AuthenticationChallengeID string `json:"authentication_challenge_id"`
Session Session `json:"session,omitempty"`
}

type AuthenticateWithEmailVerificationCodeOpts struct {
ClientID string `json:"client_id"`
Code string `json:"code"`
PendingAuthenticationToken string `json:"pending_authentication_token"`
IPAddress string `json:"ip_address,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
ClientID string `json:"client_id"`
Code string `json:"code"`
PendingAuthenticationToken string `json:"pending_authentication_token"`
IPAddress string `json:"ip_address,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
Session Session `json:"session,omitempty"`
}

type AuthenticateWithOrganizationSelectionOpts struct {
ClientID string `json:"client_id"`
PendingAuthenticationToken string `json:"pending_authentication_token"`
OrganizationID string `json:"organization_id"`
IPAddress string `json:"ip_address,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
ClientID string `json:"client_id"`
PendingAuthenticationToken string `json:"pending_authentication_token"`
OrganizationID string `json:"organization_id"`
IPAddress string `json:"ip_address,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
Session Session `json:"session,omitempty"`
}

// AuthenticationMethod represents the authentication method used to authenticate the user.
Expand Down Expand Up @@ -384,6 +390,9 @@ type AuthenticateResponse struct {

// Third party OAuth provider tokens. Present if configured in the WorkOS Dashboard.
OAuthTokens *OAuthTokens `json:"oauth_tokens,omitempty"`

// The session data. Present if the session is sealed.
SealedSession string `json:"sealed_session,omitempty"`
}

type RefreshAuthenticationResponse struct {
Expand Down Expand Up @@ -1060,8 +1069,28 @@ func (c *Client) AuthenticateWithPassword(ctx context.Context, opts Authenticate
var body AuthenticateResponse
dec := json.NewDecoder(res.Body)
err = dec.Decode(&body)
if err != nil {
return AuthenticateResponse{}, err
}

return body, err
// Seal session if requested
if err := c.sealSessionIfRequested(&body, opts.Session); err != nil {
return AuthenticateResponse{}, err
}

return body, nil
}

// sealSessionIfRequested seals the session data if requested in the session options
func (c *Client) sealSessionIfRequested(response *AuthenticateResponse, session Session) error {
if session.SealSession {
sealedSession, err := SealData(*response, session.CookiePassword)
if err != nil {
return err
}
response.SealedSession = sealedSession
}
return nil
}

// AuthenticateWithCode authenticates an OAuth user or a managed SSO user that is logging in through SSO
Expand Down Expand Up @@ -1111,8 +1140,16 @@ func (c *Client) AuthenticateWithCode(ctx context.Context, opts AuthenticateWith
var body AuthenticateResponse
dec := json.NewDecoder(res.Body)
err = dec.Decode(&body)
if err != nil {
return AuthenticateResponse{}, err
}

return body, err
// Seal session if requested
if err := c.sealSessionIfRequested(&body, opts.Session); err != nil {
return AuthenticateResponse{}, err
}

return body, nil
}

// AuthenticateWithRefreshToken obtains a new AccessToken and RefreshToken for
Expand Down Expand Up @@ -1215,8 +1252,16 @@ func (c *Client) AuthenticateWithMagicAuth(ctx context.Context, opts Authenticat
var body AuthenticateResponse
dec := json.NewDecoder(res.Body)
err = dec.Decode(&body)
if err != nil {
return AuthenticateResponse{}, err
}

return body, err
// Seal session if requested
if err := c.sealSessionIfRequested(&body, opts.Session); err != nil {
return AuthenticateResponse{}, err
}

return body, nil
}

// AuthenticateWithTOTP authenticates a user by verifying a time-based one-time password (TOTP)
Expand Down Expand Up @@ -1266,8 +1311,16 @@ func (c *Client) AuthenticateWithTOTP(ctx context.Context, opts AuthenticateWith
var body AuthenticateResponse
dec := json.NewDecoder(res.Body)
err = dec.Decode(&body)
if err != nil {
return AuthenticateResponse{}, err
}

return body, err
// Seal session if requested
if err := c.sealSessionIfRequested(&body, opts.Session); err != nil {
return AuthenticateResponse{}, err
}

return body, nil
}

// AuthenticateWithEmailVerificationCode authenticates a user by verifying a code sent to their email address
Expand Down Expand Up @@ -1317,8 +1370,16 @@ func (c *Client) AuthenticateWithEmailVerificationCode(ctx context.Context, opts
var body AuthenticateResponse
dec := json.NewDecoder(res.Body)
err = dec.Decode(&body)
if err != nil {
return AuthenticateResponse{}, err
}

return body, err
// Seal session if requested
if err := c.sealSessionIfRequested(&body, opts.Session); err != nil {
return AuthenticateResponse{}, err
}

return body, nil
}

// AuthenticateWithOrganizationSelection completes authentication for a user given an organization they've selected.
Expand Down Expand Up @@ -1368,8 +1429,16 @@ func (c *Client) AuthenticateWithOrganizationSelection(ctx context.Context, opts
var body AuthenticateResponse
dec := json.NewDecoder(res.Body)
err = dec.Decode(&body)
if err != nil {
return AuthenticateResponse{}, err
}

return body, err
// Seal session if requested
if err := c.sealSessionIfRequested(&body, opts.Session); err != nil {
return AuthenticateResponse{}, err
}

return body, nil
}

// GetEmailVerification fetches an EmailVerification object by its ID.
Expand Down
124 changes: 124 additions & 0 deletions pkg/usermanagement/session.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package usermanagement

import (
"context"
)

type Session struct {
SealSession bool
CookiePassword string
}

type SealedSessionOpts struct {
ClientID string
SessionData string
CookiePassword string
}

type AuthenticateOpts struct {
ClientID string
SessionData string
}

// AuthenticateWithSessionCookieFailureReason is the set of allowed reasons.
type AuthenticateWithSessionCookieFailureReason string

const (
ReasonInvalidJWT AuthenticateWithSessionCookieFailureReason = "invalid_jwt"
ReasonInvalidSessionCookie AuthenticateWithSessionCookieFailureReason = "invalid_session_cookie"
ReasonNoSessionCookieProvided AuthenticateWithSessionCookieFailureReason = "no_session_cookie_provided"
)

// AuthenticateWithSessionCookieResponse is the JSON object returned by authenticate/refresh calls.
type AuthenticateWithSessionCookieResponse struct {
Authenticated bool `json:"authenticated"`
SessionID string `json:"session_id"`
User User `json:"user"`
OrganizationID string `json:"organization_id,omitempty"`
Role string `json:"role,omitempty"`
Permissions []string `json:"permissions,omitempty"`
Entitlements []string `json:"entitlements,omitempty"`
Impersonator Impersonator `json:"impersonator,omitempty"`
Reason *AuthenticateWithSessionCookieFailureReason `json:"reason,omitempty"`
}

// Authenticate authenticates a user session (package level function)
func Authenticate(ctx context.Context, opts AuthenticateOpts) (AuthenticateWithSessionCookieResponse, error) {
return DefaultClient.Authenticate(ctx, opts)
}

// LoadSealedSession loads a sealed session (package level function)
func LoadSealedSession(ctx context.Context, opts SealedSessionOpts) (*SessionHelper, error) {
return DefaultClient.LoadSealedSession(ctx, opts)
}

// Authenticate method on the client
func (c *Client) Authenticate(ctx context.Context, opts AuthenticateOpts) (AuthenticateWithSessionCookieResponse, error) {
// Create a temporary session helper for authentication
sessionHelper, err := NewSessionHelper(c, opts.ClientID, opts.SessionData, "")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: passing empty string for cookiePassword to NewSessionHelper contradicts the requirement in NewSessionHelper that cookiePassword cannot be empty

if err != nil {
return AuthenticateWithSessionCookieResponse{}, err
}

// Authenticate using the session helper
result := sessionHelper.Authenticate()

// Convert to the expected response format
response := AuthenticateWithSessionCookieResponse{
Authenticated: result.Authenticated,
SessionID: result.SessionID,
OrganizationID: result.OrganizationID,
Role: result.Role,
Permissions: result.Permissions,
Entitlements: result.Entitlements,
}

// Convert user if it exists
if result.User != nil {
if user, ok := result.User.(User); ok {
response.User = user
}
}

// Convert impersonator if it exists
if result.Impersonator != nil {
if impersonator, ok := result.Impersonator.(Impersonator); ok {
response.Impersonator = impersonator
}
}

// Set reason if authentication failed
if !result.Authenticated {
reason := mapFailureReason(result.Reason)
response.Reason = &reason
}

return response, nil
}

// LoadSealedSession method on the client
func (c *Client) LoadSealedSession(ctx context.Context, opts SealedSessionOpts) (*SessionHelper, error) {
return NewSessionHelper(c, opts.ClientID, opts.SessionData, opts.CookiePassword)
}

func (s *Session) Authenticate(ctx context.Context, opts SealedSessionOpts) (AuthenticateWithSessionCookieResponse, error) {
// Use the client's authenticate method
return DefaultClient.Authenticate(ctx, AuthenticateOpts{
ClientID: opts.ClientID,
SessionData: opts.SessionData,
})
}

// mapFailureReason maps internal failure reasons to the expected enum values
func mapFailureReason(reason string) AuthenticateWithSessionCookieFailureReason {
switch reason {
case "INVALID_JWT":
return ReasonInvalidJWT
case "INVALID_SESSION_COOKIE":
return ReasonInvalidSessionCookie
case "NO_SESSION_COOKIE_PROVIDED":
return ReasonNoSessionCookieProvided
default:
return ReasonInvalidSessionCookie
}
}
Loading