Two small, complete reference applications showing how a Java backend signs users in with WorkOS AuthKit using the official WorkOS Java/Kotlin SDK.
| Example | Stack | Best for |
|---|---|---|
authkit-javalin-example |
Javalin + jte, server-rendered | Seeing the whole AuthKit flow in one ~250-line file: sign-in, sign-up, code exchange, token claims, refresh, logout |
authkit-spring-boot-bff-example |
Spring Boot 3 + Spring Security | A React (or any SPA) frontend that logs in through a Spring backend and gets a plain session cookie. Replaces an existing oauth2Login() / Auth0 setup. Supports enterprise SSO (Okta, Entra, Google Workspace, ...) through AuthKit |
Which one should I run?
- Just want to see AuthKit work in Java, end to end, in one file? Run the Javalin example. It is a complete website on its own.
- Have a React (or Vue/Angular) frontend and a Spring Boot API, and want login to set a session
cookie the way Auth0 +
oauth2Login()did? Run the Spring Boot BFF example. It is the backend half only; a small placeholder page stands in for your React app. Its README starts with a numbered zero-to-logged-in quickstart.
Both examples use the same three SDK calls. Everything else is ordinary Java web code.
// 1. Send the browser to AuthKit's hosted sign-in page
String url = workos.userManagement
.getAuthorizationUrl(clientId, redirectUri)
.provider(UserManagementProviderEnumType.AuthKit)
.state(randomState)
.build();
// 2. On the callback, exchange the one-time code for the user and session tokens
Authentication auth = workos.userManagement.authenticateWithCode(clientId, code, null);
User user = auth.getUser(); // id, email, firstName, lastName, emailVerified, ...
String accessToken = auth.getAccessToken(); // JWT with sid, org_id, role, permissions
String refreshToken = auth.getRefreshToken();
// 3. (Optional) End the WorkOS session on logout
String logoutUrl = workos.userManagement.getLogoutUrl(sessionId, returnTo);- How AuthKit works
- Prerequisites
- WorkOS dashboard setup
- Configure credentials
- Run the Javalin example
- Run the Spring Boot BFF example
- Adding enterprise SSO
- Code walkthrough
- Taking this to production
- Troubleshooting
- FAQ
AuthKit is a hosted sign-in experience. Your app never renders a login form and never handles passwords, magic links, MFA codes or SAML assertions. It does two things:
- Redirect the user to an authorization URL that WorkOS generates for your client ID.
- Exchange the one-time
codeWorkOS sends back for aUserobject and session tokens, using your server-side API key.
Browser Your Java backend WorkOS AuthKit Identity provider
| | | |
| GET /login | | |
|----------------------------->| getAuthorizationUrl(...) | |
| 302 to AuthKit | | |
|<-----------------------------| | |
| GET /user_management/authorize?client_id=...&state=... | |
|------------------------------------------------------------------>| |
| hosted sign-in page (email + password, Google, Microsoft, magic link, SSO, MFA...) |
|<------------------------------------------------------------------| |
| user authenticates |<---------------------->|
| 302 to redirect_uri?code=...&state=... | |
|<------------------------------------------------------------------| |
| GET /callback?code=...&state=... | |
|----------------------------->| verify state | |
| | authenticateWithCode(clientId, code) (server to server) |
| |----------------------------------->| |
| | { user, access_token, refresh_token, organization_id } |
| |<-----------------------------------| |
| set your own session cookie, 302 to app | |
|<-----------------------------| | |
Key properties:
- The API key never leaves your server. Only the client ID appears in browser-visible URLs.
stateprotects the callback. Your app generates a random value, stores it in the session, and rejects any callback that doesn't echo it back.- Your session stays yours. AuthKit tells you who the user is; how you keep them signed in (servlet session, Spring Security, JWT of your own, ...) is up to you. Both examples use the server-side session so the browser only ever sees an opaque cookie.
- The access token is a JWT you can decode to get
sid(WorkOS session id),org_id,roleandpermissions. Verify its signature againstworkos.userManagement.getJwksUrl(clientId)before trusting it for authorization decisions.
- JDK 17 or newer.
macOS:
brew install openjdk@17thenexport JAVA_HOME=$(brew --prefix openjdk@17). Any Temurin / Corretto / Zulu 17+ works too. Check withjava -version. - A WorkOS account. Sign up free at https://dashboard.workos.com/signup. Sandbox environments are free with no user limit for development.
- No Gradle install needed. The repo ships the Gradle wrapper (
./gradlew).
All of this happens once per environment in https://dashboard.workos.com.
Every WorkOS project has a Sandbox and a Production environment. Each has its own API keys, client ID, users and organizations. Use Sandbox for this walkthrough.
Go to Authentication. Make sure AuthKit is enabled, then turn on the methods you want on the hosted page:
| Method | Notes |
|---|---|
| Email + Password | On by default |
| Magic Auth (email code) | No extra setup |
| Google OAuth | Works out of the box in Sandbox with WorkOS's shared credentials. Add your own Google client for Production |
| Microsoft OAuth | Same as Google |
| GitHub, Apple, ... | Same pattern |
| Enterprise SSO (SAML / OIDC) | Configured per organization, see Adding enterprise SSO |
| MFA | Authentication → Multi-Factor Auth. Optional or required |
Go to Redirects and add the callback URL of the example you are running:
| Example | Redirect URI |
|---|---|
| Javalin | http://localhost:7001/callback |
| Spring Boot BFF | http://localhost:8080/auth/callback |
The match is exact: scheme, host, port, path. http://localhost:7001/callback/ with a trailing
slash or http://127.0.0.1:7001/callback will be rejected with redirect_uri_invalid.
Go to API Keys.
- Client ID is shown at the top,
client_01.... It is public. - API key: click Create Key, copy the
sk_test_...value once. It is a server-side secret.
Sandbox keys start with sk_test_, Production keys with sk_live_. The API key and client ID must
come from the same environment.
Branding lets you set logo, colors and a custom domain (auth.yourcompany.com) so the AuthKit
page looks like your product. Nothing in the code changes.
Both examples read credentials from environment variables. For local development, copy the template and fill in the two required values:
cp .env.example .envWORKOS_API_KEY=sk_test_...
WORKOS_CLIENT_ID=client_01....env is git-ignored. The Javalin example reads .env directly. Spring Boot reads normal
environment variables, so export them first:
set -a; source .env; set +aexport JAVA_HOME=$(brew --prefix openjdk@17) # or wherever your JDK 17 lives
./gradlew :authkit-javalin-example:runOpen http://localhost:7001.
| Route | What it does |
|---|---|
GET / |
Sign-in page, or the profile page if a session exists |
GET /login |
Redirects to AuthKit. ?screen_hint=sign-up opens the sign-up screen, ?login_hint=a@b.com pre-fills the email |
GET /callback |
Validates state, exchanges the code, stores user and tokens in the servlet session |
GET /refresh |
Calls authenticateWithRefreshToken and rotates both tokens |
GET /logout |
Clears the local session and redirects to the WorkOS logout URL, which ends the AuthKit session |
After signing in you will see the user profile, linked identities (which OAuth/SSO providers the user has connected), the decoded access-token claims, the raw tokens, and buttons to refresh and log out.
Details: authkit-javalin-example/README.md.
This example is for the common shape where a React SPA talks to a Spring Boot API and login
is a full-page navigation to a backend route. React never sees a token. Spring sets a JSESSIONID
cookie. If you are migrating from Auth0 with spring-boot-starter-oauth2-client, this replaces
that with the WorkOS SDK and two controller methods.
set -a; source .env; set +a
export WORKOS_REDIRECT_URI=http://localhost:8080/auth/callback
export APP_FRONTEND_URL=http://localhost:8080/ # http://localhost:5173/ when using Vite
./gradlew :authkit-spring-boot-bff-example:bootRunOpen http://localhost:8080. The bundled index.html stands in for the React app.
| Route | What it does |
|---|---|
GET /auth/login |
Random state into the session, 302 to AuthKit. Optional ?organization_id=org_... forces a specific org's SSO connection |
GET /auth/callback |
Verifies state, authenticateWithCode, builds a Spring Security Authentication and saves it to the HttpSession, 302 to APP_FRONTEND_URL |
GET /api/me |
Returns { id, email, name, organizationId } as JSON. 401 without a session. This is what React calls on load |
POST /auth/logout |
Invalidates the Spring session, 204. Requires the CSRF header |
GET /auth/logout/workos |
Also ends the WorkOS/AuthKit session, so the next login prompts again |
The React change is one line:
- <a href="/oauth2/authorization/auth0">Log in</a>
+ <a href="/auth/login">Continue with Email</a>With a Vite dev server, proxy /auth and /api to the backend so cookies stay same-origin:
// vite.config.ts
export default defineConfig({
server: { proxy: { '/auth': 'http://localhost:8080', '/api': 'http://localhost:8080' } },
});Details, including CSRF handling and the security configuration:
authkit-spring-boot-bff-example/README.md.
AuthKit routes users to their company's identity provider automatically. No code changes are needed in either example.
- Create an Organization in the dashboard (Organizations → Create). One per customer company.
- Add the customer's email domain to the organization and verify it (Domains). Users whose email matches a verified domain are routed to that organization's connection.
- Create an SSO connection on the organization: Okta, Microsoft Entra, Google Workspace, OneLogin, JumpCloud, generic SAML or OIDC. You can configure it yourself or send the customer's IT admin a self-serve Admin Portal link from the same page.
- Wait for the connection to show Active.
Now when a user types alice@customer.com on the AuthKit page, they are sent to the customer's
IdP instead of seeing a password field.
To force a specific organization's connection (useful during a pilot, or when your app already
knows which tenant the user belongs to), add .organizationId("org_01...") to the authorization URL
builder. The Spring example does this when WORKOS_ORGANIZATION_ID is set or ?organization_id=
is passed to /auth/login.
var builder = workos.userManagement
.getAuthorizationUrl(clientId, redirectUri)
.provider(UserManagementProviderEnumType.AuthKit) // hosted UI with all enabled methods
.state(state); // random, stored in your session
// Optional refinements
builder.screenHint("sign-up"); // land on the sign-up screen
builder.loginHint("alice@example.com"); // pre-fill the email field
builder.organizationId("org_01..."); // skip email entry, go straight to this org's SSOprovider can also be GoogleOAuth, MicrosoftOAuth or GitHubOAuth to skip the AuthKit page
and go directly to that provider. For a single SSO connection you can pass .connectionId(...)
instead of a provider.
// 1. Reject errors and forged callbacks
if (ctx.queryParam("error") != null) { /* show error_description */ }
if (!storedState.equals(ctx.queryParam("state"))) { /* reject */ }
// 2. Exchange the code. Third argument: optional ip address / user agent / invitation token.
Authentication auth = workos.userManagement.authenticateWithCode(clientId, code, null);
// 3. Decide what to keep
User user = auth.getUser();
String organizationId = auth.getOrganizationId(); // null if the user is not in an org
String accessToken = auth.getAccessToken(); // short-lived JWT
String refreshToken = auth.getRefreshToken(); // long-lived, single useThe access token payload contains, among others:
| Claim | Meaning |
|---|---|
sub |
WorkOS user id (user_01...) |
sid |
WorkOS session id. Pass to getLogoutUrl to end the AuthKit session |
org_id |
Organization the user signed in to, if any |
role |
The user's role slug in that organization |
permissions |
Permissions granted by that role |
exp |
Expiry. Default lifetime is short (minutes); use the refresh token to get a new one |
Both examples decode the payload with Jackson for display. In production, verify the signature
with a JWT library against the JWKS at workos.userManagement.getJwksUrl(clientId).
RefreshAuthentication r = workos.userManagement
.authenticateWithRefreshToken(clientId, refreshToken, null, null);
// r.getAccessToken(), r.getRefreshToken() -- the old refresh token is now invalidClearing your own session is enough to sign the user out of your app. To also end the
AuthKit session (so the next /login prompts again instead of silently re-authenticating),
redirect the browser to:
workos.userManagement.getLogoutUrl(sidFromAccessToken, "https://yourapp.com/");- Switch to the Production environment. New
sk_live_API key, new client ID, register the production redirect URI (https://yourapp.com/callback). Configure your own Google/Microsoft OAuth credentials under Authentication. - Verify access tokens before making authorization decisions from
role/permissions. Use any JOSE library with the JWKS URL. Decoding without verification is fine for display only. - Store refresh tokens server-side (encrypted at rest) if you want sessions to outlive the access token. Never send the refresh token to the browser.
- Externalize sessions if you run more than one instance. Spring Session with Redis or JDBC
works unchanged with the BFF example because the principal is
Serializable. - Set cookie flags:
Secure,HttpOnly,SameSite=Lax. Lax is required so the cookie is sent on the top-level redirect back from WorkOS. - Custom AuthKit domain under Branding so the sign-in page is on your domain.
- Webhooks or Events API if you need to react to
user.created,session.created,organization_membership.*and similar.
| Symptom | Cause and fix |
|---|---|
AuthKit page says redirect URI is invalid (/redirect-uri-invalid) |
The redirect_uri in the URL is not in Redirects for this environment. Add it exactly. Check port, trailing slash, localhost vs 127.0.0.1. |
| State mismatch on the callback | The session cookie was not sent back. Start and finish on the same hostname; do not switch between localhost and 127.0.0.1. Do not set SameSite=Strict or Secure on plain http. Start again from /login. |
401 Unauthorized from authenticateWithCode |
API key and client ID are from different environments, or the key was deleted. Both must come from the same environment. |
400 / invalid_grant from authenticateWithCode |
The code was already used or expired (codes are single use, valid for a few minutes). Usually a double-submit or a page refresh on the callback URL. |
| Google button not shown on the AuthKit page | Google OAuth is disabled under Authentication. |
| Typing a company email shows a password form instead of SSO | The email domain is not verified on an organization with an Active connection, and no organizationId was passed. |
| "Organization has no active connection" | The organization_id points at an org whose connection is not Active, or the org lives in a different environment. |
App starts but Missing WORKOS_API_KEY |
.env is not in the repo root, or you are running Spring Boot without exporting the variables. |
Unable to locate a Java Runtime / Gradle toolchain error |
Install JDK 17+ and set JAVA_HOME. |
redirect_uri works locally but not behind a tunnel or proxy |
Set WORKOS_REDIRECT_URI (and BASE_URL / APP_FRONTEND_URL) to the public URL and register that URL under Redirects. |
Still stuck? Email support@workos.com with the environment client ID and a timestamp. The dashboard Events page shows every authentication attempt with its outcome.
Do I have to use Javalin or Spring?
No. The SDK is plain Java. Copy login and callback into any servlet, JAX-RS, Micronaut, Quarkus,
Vert.x or Ktor handler.
Can I use Spring Security's oauth2Login() pointed at WorkOS instead?
The BFF example deliberately does not. AuthKit's authorization endpoint is not a generic OIDC
provider for your own app; the SDK-based exchange is the supported integration and is two calls.
(WorkOS Connect exists for making WorkOS an IdP for third-party apps, which is a different use
case.)
Where do users live?
In WorkOS. Users in the dashboard lists every AuthKit user in the environment, with sessions,
identities, organization memberships and MFA factors. Use the SDK's userManagement methods or
webhooks to sync into your own database if needed.
How do I get roles and permissions?
Define roles under Roles in the dashboard, assign them to organization memberships, and read
role / permissions from the access token.
Is the hosted page customizable?
Logo, colors, custom domain and the set of sign-in methods, all from the dashboard. For a fully
custom UI, the same userManagement API supports password, magic auth, MFA and SSO flows directly
(authenticateWithPassword, authenticateWithMagicAuth, authenticateWithTotp, ...).
What SDK version is this?
com.workos:workos:4.14.0 on Maven Central. Method names above were checked against that version's
source.
.
├── authkit-javalin-example/ server-rendered walkthrough of the full flow
├── authkit-spring-boot-bff-example/ Spring Boot backend-for-frontend for a React SPA
├── .env.example credential template (copy to .env)
├── .github/workflows/build.yml CI: ./gradlew build on JDK 17
└── settings.gradle.kts Gradle multi-project root
./gradlew build # compiles both examples and runs their tests, no credentials needed- AuthKit docs: https://workos.com/docs/user-management
- API reference: https://workos.com/docs/reference/user-management
- Java/Kotlin SDK: https://github.com/workos/workos-kotlin
- Dashboard: https://dashboard.workos.com