package workos
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
"time"
)
type SessionData struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
User *User `json:"user,omitempty"`
Impersonator *AuthenticateResponseImpersonator `json:"impersonator,omitempty"`
}
type AuthenticateSessionResult struct {
Authenticated bool
SessionID string
OrganizationID string
Role string
Permissions []string
Entitlements []string
User *User
Impersonator *AuthenticateResponseImpersonator
NeedsRefresh bool
Reason string
}
type JWTClaims struct {
SessionID string `json:"sid"`
OrganizationID string `json:"org_id"`
Role string `json:"role"`
Permissions []string `json:"permissions"`
Entitlements []string `json:"entitlements"`
Exp int64 `json:"exp"`
}
type RefreshSessionResult struct {
Authenticated bool
SealedSession string
Session *SessionData
Reason string
}
type Session struct {
client *Client
cookiePassword string
sessionData string
}
func NewSession (client *Client , sessionData string , cookiePassword string ) *Session {
return &Session {
client : client ,
cookiePassword : cookiePassword ,
sessionData : sessionData ,
}
}
func (s *Session ) Authenticate () (*AuthenticateSessionResult , error ) {
if s .sessionData == "" {
return &AuthenticateSessionResult {
Authenticated : false ,
Reason : "no_session_cookie_provided" ,
}, nil
}
session , err := unsealSession (s .sessionData , s .cookiePassword )
if err != nil {
return &AuthenticateSessionResult {
Authenticated : false ,
Reason : "invalid_session_cookie" ,
}, nil
}
if session .AccessToken == "" {
return &AuthenticateSessionResult {
Authenticated : false ,
Reason : "invalid_jwt" ,
}, nil
}
claims , err := parseJWTPayload (session .AccessToken )
if err != nil {
return &AuthenticateSessionResult {
Authenticated : false ,
Reason : "invalid_jwt" ,
}, nil
}
if claims .Exp != 0 && time .Now ().Unix () >= claims .Exp {
return &AuthenticateSessionResult {
Authenticated : false ,
NeedsRefresh : true ,
SessionID : claims .SessionID ,
OrganizationID : claims .OrganizationID ,
Role : claims .Role ,
Permissions : claims .Permissions ,
Entitlements : claims .Entitlements ,
User : session .User ,
Impersonator : session .Impersonator ,
Reason : "session_expired" ,
}, nil
}
return &AuthenticateSessionResult {
Authenticated : true ,
SessionID : claims .SessionID ,
OrganizationID : claims .OrganizationID ,
Role : claims .Role ,
Permissions : claims .Permissions ,
Entitlements : claims .Entitlements ,
User : session .User ,
Impersonator : session .Impersonator ,
}, nil
}
func (s *Session ) Refresh (ctx context .Context , opts ...RequestOption ) (*RefreshSessionResult , error ) {
if s .sessionData == "" {
return &RefreshSessionResult {
Authenticated : false ,
Reason : "no_session_cookie_provided" ,
}, nil
}
session , err := unsealSession (s .sessionData , s .cookiePassword )
if err != nil {
return &RefreshSessionResult {
Authenticated : false ,
Reason : "invalid_session_cookie" ,
}, nil
}
if session .RefreshToken == "" {
return &RefreshSessionResult {
Authenticated : false ,
Reason : "no_refresh_token" ,
}, nil
}
if s .client == nil {
return nil , errors .New ("workos: client is required for session refresh" )
}
var orgID *string
if session .AccessToken != "" {
if claims , err := parseJWTPayload (session .AccessToken ); err == nil && claims .OrganizationID != "" {
orgID = &claims .OrganizationID
}
}
authResp , err := s .client .UserManagement ().AuthenticateWithRefreshToken (ctx , &UserManagementAuthenticateWithRefreshTokenParams {
RefreshToken : session .RefreshToken ,
OrganizationID : orgID ,
}, opts ...)
if err != nil {
reason := "refresh_failed"
var apiErr *APIError
if errors .As (err , &apiErr ) && apiErr .StatusCode == 401 && apiErr .ErrorCode == "invalid_grant" {
reason = "refresh_token_revoked"
}
return &RefreshSessionResult {
Authenticated : false ,
Reason : reason ,
}, err
}
newSession := &SessionData {
AccessToken : authResp .AccessToken ,
RefreshToken : authResp .RefreshToken ,
User : authResp .User ,
Impersonator : authResp .Impersonator ,
}
sealed , err := SealSession (newSession , s .cookiePassword )
if err != nil {
return nil , fmt .Errorf ("workos: failed to seal refreshed session: %w" , err )
}
return &RefreshSessionResult {
Authenticated : true ,
SealedSession : sealed ,
Session : newSession ,
}, nil
}
func (s *Session ) GetLogoutURL (ctx context .Context , returnTo string , opts ...RequestOption ) (string , error ) {
if s .sessionData == "" {
return "" , errors .New ("workos: no session data provided" )
}
result , err := s .Authenticate ()
if err != nil {
return "" , fmt .Errorf ("workos: failed to authenticate session: %w" , err )
}
if result .SessionID == "" {
return "" , errors .New ("workos: session has no session ID" )
}
baseURL := defaultBaseURL
if s .client != nil && s .client .baseURL != "" {
baseURL = s .client .baseURL
}
logoutURL := fmt .Sprintf ("%s/user_management/sessions/logout?session_id=%s" , baseURL , url .QueryEscape (result .SessionID ))
if returnTo != "" {
logoutURL += "&return_to=" + url .QueryEscape (returnTo )
}
return logoutURL , nil
}
func SealSessionFromAuthResponse (accessToken string , refreshToken string , user *User , impersonator *AuthenticateResponseImpersonator , cookiePassword string ) (string , error ) {
session := &SessionData {
AccessToken : accessToken ,
RefreshToken : refreshToken ,
User : user ,
Impersonator : impersonator ,
}
return SealSession (session , cookiePassword )
}
func AuthenticateSession (sealedSession string , cookiePassword string ) (*AuthenticateSessionResult , error ) {
session := NewSession (nil , sealedSession , cookiePassword )
return session .Authenticate ()
}
func (c *Client ) RefreshSession (ctx context .Context , sealedSession string , cookiePassword string , opts ...RequestOption ) (*RefreshSessionResult , error ) {
session := NewSession (c , sealedSession , cookiePassword )
return session .Refresh (ctx , opts ...)
}
func parseJWTPayload (token string ) (*JWTClaims , error ) {
parts := strings .Split (token , "." )
if len (parts ) != 3 {
return nil , fmt .Errorf ("workos: invalid JWT format: expected 3 parts, got %d" , len (parts ))
}
payload := parts [1 ]
decoded , err := base64 .RawURLEncoding .DecodeString (payload )
if err != nil {
return nil , fmt .Errorf ("workos: failed to decode JWT payload: %w" , err )
}
var claims JWTClaims
if err := json .Unmarshal (decoded , &claims ); err != nil {
return nil , fmt .Errorf ("workos: failed to unmarshal JWT claims: %w" , err )
}
return &claims , nil
}
The pages are generated with Golds v0.8.2 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .