ruby / workos

We use WorkOS for SSO and AuthKit for our MCP server. The workos gem is one of the few SDKs we depend on, which is at odds with my thin HTTP client preference, but auth is a security boundary worth treating carefully.

Boot

Configure the gem once at boot:

require "workos"

WorkOS.configure do |config|
  config.api_key = ENV.fetch("WORKOS_API_KEY")
  config.client_id = ENV.fetch("WORKOS_CLIENT_ID")
  config.timeout = 5
end

Fork safety

Puma forks workers from a preloaded master. The WorkOS client holds open sockets to api.workos.com. Sharing sockets across forked workers causes errors, so reset the client in each worker on boot:

Puma::Configuration.new do |c|
  c.app APP
  c.workers ENV.fetch("WEB_CONCURRENCY").to_i
  c.preload_app!

  c.before_worker_boot do
    DB.reset_pool!
    WorkOS.reset_client
  end
end

Login

The login handler builds an authorization URL, stores a CSRF state token in the session, and renders a page with a "Login" button:

class Login < PublicHandler
  def handle
    state = SecureRandom.hex(16)
    @session[:oauth_state] = state

    workos_auth_url = WorkOS.client.sso.get_authorization_url(
      organization: ENV.fetch("WORKOS_ORGANIZATION"),
      redirect_uri: "#{base_url}/sso",
      state: state
    )

    page "sso/login", workos_auth_url: workos_auth_url
  end
end

get_authorization_url builds a string. No HTTP request.

Callback

The callback verifies the state parameter, exchanges the code for a profile, looks up the user by email, and remembers them via an encrypted cookie:

class Callback < PublicHandler
  def handle
    state = @session.delete(:oauth_state)
    if params["state"].nil? || params["state"] != state
      flash_next(:error, "Forbidden")
      return redirect_to("/login")
    end
    if params["code"].nil?
      flash_next(:error, "Forbidden")
      return redirect_to("/login")
    end

    profile = WorkOS.client.sso.get_profile_and_token(
      code: params["code"]
    ).profile

    user = Users::ByEmail.new(db).call(profile.email)
    if user == {}
      flash_next(:error, "Forbidden")
      return redirect_to("/login")
    end

    remember user["remember_token"]
    redirect_to(safe_return_to)
  end
end

Users::ByEmail queries WHERE active = true, so deactivated users can't log in.

Audit logs

Successful logins write to WorkOS audit logs. The call is best-effort: a failure shouldn't block the login.

begin
  WorkOS.client.audit_logs.create_event(
    organization_id: ENV.fetch("WORKOS_ORGANIZATION"),
    event: {
      action: "user_logged_in",
      occurred_at: Time.now.utc.iso8601,
      actor: {id: user["id"].to_s, type: "user", name: user["name"]},
      targets: [{id: user["id"].to_s, type: "user"}],
      context: {location: @req.ip, user_agent: @req.user_agent}
    }
  )
rescue WorkOS::Error => err
  Sentry.capture_message("WorkOS::AuditLogs.create_event #{err}")
end

Open redirect guard

After login, redirect to a return_to path stored before login. Only allow internal paths:

def valid_return_path?(path)
  if path.nil?
    return false
  end
  if !path.start_with?("/")
    return false
  end
  if path.start_with?("//") # protocol-relative URL
    return false
  end
  true
end

AuthKit bridge

The same Login and Callback handlers also serve our MCP AuthKit flow. When Claude starts OAuth with AuthKit, AuthKit redirects to GET /login?external_auth_id=xxx.

If the user is already signed in, complete the flow immediately:

if current_user["id"] && valid_external_auth_id
  return complete_authkit_flow(external_auth_id, {
    id: current_user["id"].to_s,
    email: current_user["email"]
  })
end

Otherwise stash external_auth_id in the session and run normal SSO. The callback detects it and completes AuthKit instead of normal login.

The completion call is plain HTTP, not the SDK, since the gem doesn't cover this endpoint:

resp = HTTP
  .auth("Bearer #{ENV.fetch("WORKOS_API_KEY")}")
  .post("https://api.workos.com/authkit/oauth2/complete", json: {
    external_auth_id: external_auth_id,
    user: user_payload
  })

body = JSON.parse(resp.body.to_s)
redirect_uri = body["redirect_uri"]

uri = URI.parse(redirect_uri.to_s)
if uri.scheme != "https" || !uri.host&.end_with?(".workos.com", ".authkit.app")
  Sentry.capture_message("AuthKit redirect_uri rejected", extra: {
    redirect_uri: redirect_uri
  })
  flash_next(:error, "MCP authentication error. Please try again.")
  return redirect_to("/login")
end

[303, {"Location" => redirect_uri}, []]

The host check on redirect_uri is defense in depth against open redirects.

Audit logs fire on normal SSO logins but not on the AuthKit path, since the AuthKit branches return before reaching the audit log call.

← All articles