go / workos
WorkOS provides SSO and AuthKit for OAuth flows.
I use a small client over the four WorkOS endpoints I need. The bulk of the auth security work happens in my own handler code.
Client
workos.Client is built from a Config so tests can swap the
base URL and HTTP client:
const defaultAPIBase = "https://api.workos.com"
type Config struct {
APIKey string
BaseURL string
ClientID string
Timeout time.Duration
}
type Client struct {
apiKey string
baseURL string
clientID string
httpClient *http.Client
}
func NewClient(cfg Config) *Client {
timeout := cfg.Timeout
if timeout <= 0 {
timeout = 5 * time.Second
}
baseURL := strings.TrimRight(cfg.BaseURL, "/")
if baseURL == "" {
baseURL = defaultAPIBase
}
return &Client{
apiKey: cfg.APIKey,
baseURL: baseURL,
clientID: cfg.ClientID,
httpClient: &http.Client{Timeout: timeout},
}
}
AuthorizationURL builds a string. No HTTP request:
func (c *Client) AuthorizationURL(organization, redirectURI, state string) string {
q := url.Values{
"client_id": []string{c.clientID},
"organization": []string{organization},
"redirect_uri": []string{redirectURI},
"response_type": []string{"code"},
"state": []string{state},
}
return c.baseURL + "/sso/authorize?" + q.Encode()
}
ProfileAndToken exchanges the callback code for a profile:
func (c *Client) ProfileAndToken(ctx context.Context, code string) (*ProfileAndTokenResponse, error) {
form := url.Values{
"client_id": []string{c.clientID},
"client_secret": []string{c.apiKey},
"code": []string{code},
"grant_type": []string{"authorization_code"},
}
req, err := http.NewRequestWithContext(ctx, "POST",
c.baseURL+"/sso/token", bytes.NewBufferString(form.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode/100 != 2 {
return nil, fmt.Errorf("workos profile_and_token status=%d body=%s",
resp.StatusCode, readBodyForError(resp.Body))
}
raw := map[string]any{}
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
return nil, err
}
out := &ProfileAndTokenResponse{Raw: map[string]any{}}
if v, ok := raw["access_token"].(string); ok {
out.AccessToken = v
}
if v, ok := raw["profile"].(map[string]any); ok {
out.Profile = v
} else {
out.Profile = map[string]any{}
}
maps.Copy(out.Raw, raw)
return out, nil
}
There are no retry loops. All calls happen inside the user's
login request cycle. Auth codes are single-use, so retrying
ProfileAndToken after a partial failure would compound the
problem rather than recover. Audit logging is best-effort.
Login
The login handler builds an authorization URL and stashes a CSRF state token in the session:
func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
state, err := webutil.RandomHex(16)
if err != nil {
h.WriteError(w, 500, "oauth state generation failed")
return
}
h.SessionSet(r, "oauth_state", state)
workosAuthURL := h.WorkOS().AuthorizationURL(
h.WorkOSOrganization,
h.AbsoluteURL(r, "/sso"),
state,
)
html, err := h.RenderPage(r, "sso/login", map[string]any{
"workos_auth_url": workosAuthURL,
})
if err != nil {
h.WriteError(w, 500, "render login failed")
return
}
h.WriteHTML(w, 200, html)
}
The handler takes WorkOS as an interface, so tests inject a
fake instead of hitting the network:
type WorkOSClient interface {
AuthorizationURL(organization, redirectURI, state string) string
ProfileAndToken(ctx context.Context, code string) (*workos.ProfileAndTokenResponse, error)
CompleteAuthkitFlow(ctx context.Context, externalAuthID string, user map[string]any) (string, error)
AuditEvent(ctx context.Context, organizationID string, event workos.AuditEvent) error
}
Callback
The callback verifies state, exchanges the code for a profile, and signs the user in:
func (h *Handler) SSOCallback(w http.ResponseWriter, r *http.Request) {
expectedState := h.SessionGet(r, "oauth_state")
h.SessionDelete(r, "oauth_state")
if expectedState == "" || r.URL.Query().Get("state") != expectedState {
h.FlashError(r, "Forbidden")
h.Redirect(w, r, "/login")
return
}
code := r.URL.Query().Get("code")
if code == "" {
h.FlashError(r, "Forbidden")
h.Redirect(w, r, "/login")
return
}
resp, err := h.WorkOS().ProfileAndToken(r.Context(), code)
if err != nil {
h.FlashError(r, "SSO error. Please try again.")
h.Redirect(w, r, "/login")
return
}
email, _ := resp.Profile["email"].(string)
user, err := h.lookupActiveLoginUserByEmail(r.Context(), email)
if err != nil || user == nil {
h.FlashError(r, "Forbidden")
h.Redirect(w, r, "/login")
return
}
h.SetRememberCookie(w, user.RememberToken, h.CookieSecret, h.AppEnv)
h.Redirect(w, r, h.PopReturnTo(r))
}
lookupActiveLoginUserByEmail queries WHERE active = true, so
deactivated users can't log in.
Audit logs
After a successful login, write a WorkOS audit event. The
Idempotency-Key header is required by the audit logs endpoint,
so retries (here or upstream) are safe:
func (c *Client) AuditEvent(ctx context.Context, organizationID string, event AuditEvent) error {
payload, err := json.Marshal(map[string]any{
"organization_id": organizationID,
"event": event,
})
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, "POST",
c.baseURL+"/audit_logs/events", bytes.NewReader(payload))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+c.apiKey)
idempotency, err := idempotencyKey()
if err != nil {
return err
}
req.Header.Set("Idempotency-Key", idempotency)
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode/100 != 2 {
return fmt.Errorf("workos audit_event status=%d body=%s",
resp.StatusCode, readBodyForError(resp.Body))
}
return nil
}
The callback logs the returned error to Sentry but does not fail the request. A missing audit log shouldn't block a login.
AuthKit bridge
The same login and callback handlers also serve an
MCP AuthKit flow. When a client starts OAuth with
AuthKit, AuthKit redirects to GET /login?external_auth_id=xxx.
If the user is signed in, stash the value in the session and render a consent page:
var externalAuthIDPattern = regexp.MustCompile(`^[a-zA-Z0-9_-]{1,255}$`)
func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
u, err := h.CurrentUser(r)
if err != nil {
h.WriteError(w, 500, "auth lookup failed")
return
}
externalAuthID := strings.TrimSpace(r.URL.Query().Get("external_auth_id"))
validExternalAuthID := externalAuthIDPattern.MatchString(externalAuthID)
if u != nil && u.Active && validExternalAuthID {
h.SessionSet(r, "pending_external_auth_id", externalAuthID)
html, _ := h.RenderPage(r, "sso/authkit_confirm", map[string]any{
"form_authenticity_token": h.SessionGet(r, "csrf_token"),
})
h.WriteHTML(w, 200, html)
return
}
// ... otherwise, normal SSO flow
}
A CSRF-protected POST from the consent page completes the bind:
func (h *Handler) SSOConfirm(w http.ResponseWriter, r *http.Request) {
u := h.RequestUser(r)
if u == nil || !u.Active {
h.Redirect(w, r, "/login")
return
}
externalAuthID := strings.TrimSpace(h.SessionGet(r, "pending_external_auth_id"))
h.SessionDelete(r, "pending_external_auth_id")
if !externalAuthIDPattern.MatchString(externalAuthID) {
h.FlashError(r, "MCP authentication session expired. Please try again.")
h.Redirect(w, r, "/login")
return
}
redirectURI, err := h.WorkOS().CompleteAuthkitFlow(r.Context(), externalAuthID, map[string]any{
"id": strconv.FormatInt(u.ID, 10),
"email": u.Email,
})
if err != nil || !AuthkitAllowedRedirectURI(redirectURI) {
h.FlashError(r, "MCP authentication error. Please try again.")
h.Redirect(w, r, "/login")
return
}
w.Header().Set("Location", redirectURI)
w.WriteHeader(303)
}
Without the consent step, an attacker could email a logged-in
victim a link with ?external_auth_id=ATTACKER_ID and silently
bind their AuthKit identity to the victim's account on click.
A CSRF-protected POST blocks that.
For guests, the callback path stashes external_auth_id in the
session and completes AuthKit there instead of ending the
request with a logged-in session. State already protects that
path.
The completion call returns an AuthKit-issued redirect URL. Check the host before redirecting:
func AuthkitAllowedRedirectURI(raw string) bool {
uri, err := url.Parse(raw)
if err != nil {
return false
}
if uri.Scheme != "https" {
return false
}
host := strings.ToLower(uri.Hostname())
return strings.HasSuffix(host, ".workos.com") || strings.HasSuffix(host, ".authkit.app")
}
The host check is defense in depth against open redirects.
Tests
Stub the HTTP boundary with httptest.NewServer and point the
client's base URL at it:
func TestProfileAndToken(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tu.OK(r.URL.Path == "/sso/token")
tu.OK(r.Header.Get("Content-Type") == "application/x-www-form-urlencoded")
w.Write([]byte(`{"access_token":"tok_123","profile":{"email":"[email protected]"}}`))
}))
defer s.Close()
c := NewClient(Config{APIKey: "key", ClientID: "cid", BaseURL: s.URL})
c.httpClient = s.Client()
out, err := c.ProfileAndToken(context.Background(), "code-123")
tu.OK(err == nil)
tu.OK(out.AccessToken == "tok_123")
tu.OK(out.Profile["email"] == "[email protected]")
}
The handler tests inject a fake WorkOSClient, so the login,
callback, and consent paths run without a network or a real
WorkOS account.