YouBothAgent▾
You — Business rules and flows you own. Read these yourself.
Both — Know the idea; your agent follows the details.
Agent — Conventions and references your agent follows. Look up as needed.
General▾
Interface▾
Observability▾
Single Sign-On
SSO lets users sign in with GitHub, Google, Facebook, Kakao or Naver instead of a password. With
libs/shared mounted, the routes and the sign-in logic already exist: you add keys, a button and the pages the user lands on.Words used on this page
TermDescription
provider
The service that confirms who the user is: GitHub, Google, Facebook, Kakao, Naver or Apple.
callback
The route the provider sends the user back to, carrying a one-time
code.accountId
The one value that identifies the user on every sign-in: an email or a GitHub username.
prepare user
A user in
prepare status, created for a newcomer. The signup page finishes it.What you do
- Add the keys. Put each provider's client ID and secret under
security.ssoin the server env file. - Register the redirect URI. Enter
<origin>/api/user/<provider>/callbackin each provider's developer console. - Place the buttons. Render
User.Util.SSOButtonson the sign-in page with three destinations. - Build the landing pages. A signup page that reads
userIdand an error page that readserrorfrom the query.
What happens when the user clicks
st.do.ssoSigninUsersaves the three destinations and the page origin in cookies, then opens/api/user/<provider>.- The start route sends the browser to the provider's consent screen.
- The provider returns the browser to
/api/user/<provider>/callbackwith acode. - The callback trades the
codefor a profile and picks theaccountId. userService.handleSsoCallbacksigns the user in, continues signup, or sends them to the error page.
Register Providers
Each provider gives you a client ID, and usually a secret, in its developer console. Put them under
security.sso in the server env file of each environment:apps/koyo/env/env.server.local.ts
- Only listed providers turn on. A provider missing here makes its
SSO.<Provider>guard refuse both routes withssoNotConfigured. - Keys stay out of git.
env.server.local.ts,env.server.main.tsand the other environment files are gitignored. - Setting
securityreplaces the whole object. Keepverifies, andjwtSecretif you use one, besidesso.
Credential fields
clientIDstringrequired
The app's client ID from the provider console. Kakao calls it the REST API key.
clientSecretstring
Sent with the code-for-token exchange, only when set.
teamIDstringapple
Your Apple developer team ID, the issuer of Apple's client secret.
keyIDstringapple
The ID of the Sign in with Apple key, sent as the secret's
kid.keyFilePathstringapple
Path to that key's private key file. Akan signs the client secret with it.
Redirect URI for the console
A provider only sends users back to a URI you registered. Akan builds it from the origin of the page where the user clicked:
https://<your-domain>/api/user/<provider>/callback- Register every origin. Local, staging and production each need their own entry, because the origin comes from the browser.
- Allow the scopes below. Consent items the console has not enabled come back empty, such as Kakao's email.
| Key in security.sso | Scope Akan requests |
|---|---|
| github | user |
| email profile | |
| kakao | account_email,profile_nickname |
| naver | (none) |
Write A Callback
You rarely write this yourself:
libs/shared/lib/user/user.signal.ts already pairs a start route with a callback for each provider. Read it when you add a provider or change what happens after sign-in.| endpoint | Path |
|---|---|
| ↳ What it does | |
| /api/user/google | |
| Redirects the browser to Google's consent screen. | |
| googleCallback | /api/user/google/callback |
Trades Google's code for a profile, then signs in or continues signup. | |
The Google pair, as shipped. Every other provider has the same shape:
libs/shared/lib/user/user.signal.ts
The pieces it uses. All but
handleSsoCallback, a user service method, come from @libs/shared/srvkit:HelperDescription
SSO.GoogleSSO.GithubSSO.Kakao…
Guards that refuse the call with
ssoNotConfigured when that provider has no keys.makeOAuthRedirectResponse
Builds the 302 to the provider's consent screen from the
ssoOrigin cookie.getSsoCodegetSsoOrigin
Read the
code query and the ssoOrigin cookie, and throw when either is missing.extractGoogleProfileextractGithubProfile…
One per provider. Trades the code for a token and fetches the profile.
handleSsoCallback
The user service's decision: sign in, continue signup or error. Returns
{ cookie, redirect }.makeSsoRedirectResponse
The final 302 to
redirect, setting the session cookies when there are any.- The callback stays small. It only turns the profile into an
accountIdand a nickname; every decision lives inhandleSsoCallback. - Apple is not wired yet.
SSO.Appleand the Apple keys exist, but the shippedappleandappleCallbackdo nothing. Build yours onverifyAppleUser.
Account Id
Each provider names the user differently. The callback turns every profile into one
accountId before it calls the service, and that value identifies the user from then on.| provider | accountId | Nickname seed |
|---|---|---|
| github | username | displayName |
| emails[0].value | displayName | |
| emails[0].value | givenName familyName | |
| kakao | name | |
| naver | name |
Writing your own callbacks? Keep the difference in one lookup:
apps/koyo/srvkit/accountIdOf.ts
- One provider per account. An existing
accountIdarriving from a provider it never signed in with goes to the error page withnoVerifiesInUser. - GitHub and Google make two users. A username and an email never match, even for the same person.
- The nickname is a first draft. A newcomer gets the profile name, or the
accountIdbefore@when it is empty, cut to 12 characters and made unique.
After The Callback
The callback always ends on one of three pages, and you name all three on the sign-in button:
apps/koyo/page/signin.tsx
| Outcome | When | Goes to |
|---|---|---|
| Signed in | The accountId belongs to an active, restricted or dormant user. | signinRedirect |
| Continue signup | No such user yet, so a prepare user is created with a unique nickname. | signupRedirect?userId=<id> |
| Error | Signing in or preparing the user fails, for example with noVerifiesInUser. | errorRedirect?error=<error key> |
SSOButtons props
signinRedirectstringrequired
Where an existing user lands, signed in.
signupRedirectstringrequired
Where a newcomer lands to finish signup. Gets
?userId=<id> appended.errorRedirectstringdefault "/404"
Where a failed sign-in lands. Gets
?error=<error key> appended.mainSsosSsoType["value"][]default []
Providers shown as full-width buttons with a label.
subSsosSsoType["value"][]default []
Providers shown as a row of round icon buttons below.
replacebooleandefault false
Replace the current history entry instead of pushing a new one.
- Your own button calls
st.do.ssoSigninUser(ssoType, { signinRedirect, signupRedirect, errorRedirect }), the same actionSSOButtonsuses. - Write paths as the app sees them. The action adds the basePath prefix when the app has one.


Start SSO only through
st.do.ssoSigninUser. A bare link to /api/user/google carries no ssoOrigin cookie, so the start route fails with invalidSsoCallbackMissingOrigin and no destination is known.Tips
- Provider differences go in the callback. Sign-in rules go in the service.
- One service method for every provider. Once the
accountIdis normalized, each callback calls the samehandleSsoCallback. - Build the three pages first. Have the signed-in, signup and error pages ready before you turn SSO on.