go / workos
WorkOS provides SSO and AuthKit for OAuth flows.
I use a small client over the four WorkOS endpoints I need.
Client
NewClient builds a client from a Config. Tests set 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. An auth code is single-use, so a retry of
ProfileAndToken cannot succeed.
Login
The login handler builds an authorization URL and stores 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 a test injects a fake:
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 a
deactivated user cannot log in.
Audit logs
A successful login writes a WorkOS audit event. The endpoint requires
an Idempotency-Key header, so a retry is 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
}
On error, the callback logs to Sentry and does not fail the login.
AuthKit bridge
The same handlers serve an MCP AuthKit flow. A client that
starts OAuth with AuthKit arrives at
GET /login?external_auth_id=xxx.
For a signed-in user, the handler stores the value in the session and renders 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, a link with
?external_auth_id=ATTACKER_ID binds the attacker's AuthKit identity
to the account that clicks it.
For a guest, the callback stores external_auth_id in the session
and completes AuthKit after the state check.
The handler checks the host of the returned redirect URL:
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")
}
Tests
Tests point the client's base URL at an httptest.NewServer:
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]")
}
Handler tests inject a fake WorkOSClient and run without a network.