Summary
An authorization code minted by /sso/authorize cannot be redeemed at POST /user_management/authenticate. The two endpoints use different stores: /sso/authorize inserts into ssoAuthorizations (src/workos/routes/sso.ts) while the authorization_code grant reads authCodes (src/workos/routes/auth.ts), which only /user_management/authorize writes. So an app that starts SSO with sso.getAuthorizationUrl and finishes it with AuthKit's callback gets invalid_grant.
POST /sso/token does redeem the code, but it returns a profile and an access token without creating a session, so it can't back session helpers like saveSession / withAuth.
Related, and possibly the same fix: no code path records a session with auth_method: "sso". AUTH_METHOD_SESSION_VALUES maps SSO: 'sso' in src/workos/helpers.ts, but no grant in src/workos/routes/auth.ts ever assigns authMethod = 'SSO'. The string appears only in sso.ts, inside emitAuthenticationEvent calls. Anything that gates on the session's auth method, such as hiding password management for federated users, therefore can't be exercised against the emulator.
Reproduction (v0.7.1)
# seed.yaml
organizations:
- name: Acme
domains:
- domain: acme.test
state: verified
users:
- email: alice@acme.test
password: test123
email_verified: true
connections:
- name: Acme SSO
connection_type: GoogleOAuth
organization: Acme
domains: [acme.test]
profiles:
- email: alice@acme.test
workos-emulate --port 4100 --seed seed.yaml &
ORG=$(curl -s -H "Authorization: Bearer sk_test_default" \
http://localhost:4100/organizations | jq -r '.data[0].id')
# follow the authorize redirect and pull the code out of it
curl -s -o /dev/null -w '%{redirect_url}\n' \
"http://localhost:4100/sso/authorize?response_type=code&client_id=client_x&organization=$ORG&redirect_uri=http://localhost:3000/callback"
# http://localhost:3000/callback?code=sso_code_01M089ZB8KMCB9VFYPA2DNX7Z1
curl -s -w "\nHTTP %{http_code}\n" -X POST http://localhost:4100/user_management/authenticate \
-H "Content-Type: application/json" \
-d '{"grant_type":"authorization_code","code":"sso_code_01M089ZB8KMCB9VFYPA2DNX7Z1","client_id":"client_x","client_secret":"sk_test_default"}'
{"error":"invalid_grant","error_description":"The code 'sso_code_01M089ZB8KMCB9VFYPA2DNX7Z1' has expired or is invalid."}
HTTP 400
Expected
The code is accepted, and the response carries a session whose auth_method is sso.
Note on the workaround
Routing the user through /user_management/authorize?login_hint=... instead does work, and mints a code the authenticate endpoint accepts. It isn't equivalent though: it puts a hosted sign in screen in front of the user, which defeats setups that deliberately send people straight to their IdP. The resulting session also comes back as oauth rather than sso.
Happy to send a PR if you can confirm which way you'd want it done.
Summary
An authorization code minted by
/sso/authorizecannot be redeemed atPOST /user_management/authenticate. The two endpoints use different stores:/sso/authorizeinserts intossoAuthorizations(src/workos/routes/sso.ts) while theauthorization_codegrant readsauthCodes(src/workos/routes/auth.ts), which only/user_management/authorizewrites. So an app that starts SSO withsso.getAuthorizationUrland finishes it with AuthKit's callback getsinvalid_grant.POST /sso/tokendoes redeem the code, but it returns a profile and an access token without creating a session, so it can't back session helpers likesaveSession/withAuth.Related, and possibly the same fix: no code path records a session with
auth_method: "sso".AUTH_METHOD_SESSION_VALUESmapsSSO: 'sso'in src/workos/helpers.ts, but no grant in src/workos/routes/auth.ts ever assignsauthMethod = 'SSO'. The string appears only in sso.ts, insideemitAuthenticationEventcalls. Anything that gates on the session's auth method, such as hiding password management for federated users, therefore can't be exercised against the emulator.Reproduction (v0.7.1)
Expected
The code is accepted, and the response carries a session whose
auth_methodissso.Note on the workaround
Routing the user through
/user_management/authorize?login_hint=...instead does work, and mints a code the authenticate endpoint accepts. It isn't equivalent though: it puts a hosted sign in screen in front of the user, which defeats setups that deliberately send people straight to their IdP. The resulting session also comes back asoauthrather thansso.Happy to send a PR if you can confirm which way you'd want it done.