OpsAPI signs people in once and hands each of your apps a token it can check with a single call. Your apps never store passwords, never talk to Google and never build their own reset, lockout or two-factor screens. This note walks through the flow the two live demo apps use, step by step.
The flow
- Log in. The app makes a random nonce, keeps it in the user's session and sends the browser to OpsAPI.
- Hosted sign-in.
GET /auth/signin?frontend_url=<app origin>&from=<nonce>shows OpsAPI's own page. If the browser already has an OpsAPI session it can go straight back with a fresh token. - Google, if chosen. OpsAPI redirects to Google, swaps the returned code server to server and reads the email. Unverified Google emails are refused.
- User and workspace. A first-time user is created and added to the sign-up workspace; then OpsAPI signs a JWT.
- Hand-back. The browser goes to
<app>/auth/callback?token=<JWT>&redirect=<nonce>, but only if the app's origin is on OpsAPI's trust list. - Check. The app compares the nonce with its session, then calls
GET /auth/mewith the token as a Bearer header and starts its own session from the answer.
Never verify the JWT inside your app. It's signed with a key that never leaves OpsAPI. Asking /auth/me is the check, and it also tells you the user's workspace and role.
What each side does
| Concern | OpsAPI | Your app |
|---|---|---|
| Passwords, Google, Apple | Handles all of it | Never sees them |
| Two-factor code | Emails and checks it (always for platform admins) | Nothing |
| Lockout and rate limits | Per account and per IP | Nothing |
| Who may receive a token | Trusted origins only | Has its origin allowed |
| Forged callback links | — | Nonce check, refuses with 400 |
| Session | Its own sign-in session | Its own cookie after /auth/me |
| Roles | Per workspace | Reads them from /auth/me |
The app side in four routes
The demo apps add /login, /auth/callback and /logout. This is the Spring Boot callback from OpsApiAuthController.java, trimmed:
@GetMapping("/auth/callback")
public String callback(@RequestParam String token,
@RequestParam(name = "redirect", required = false) String nonce,
HttpServletRequest request) {
HttpSession session = request.getSession(false);
Object expected = session == null ? null : session.getAttribute(NONCE);
if (expected == null || nonce == null || !MessageDigest.isEqual(
expected.toString().getBytes(UTF_8), nonce.getBytes(UTF_8))) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Sign-in was not started here. Go to /login.");
}
Map<String, Object> me = opsapi.me(token); // GET /auth/me with Authorization: Bearer <token>
request.changeSessionId(); // a session id seen before login must not survive it
session.removeAttribute(NONCE);
session.setAttribute(USER, me);
session.setAttribute(TOKEN, token);
return "redirect:" + appUrl + session.getAttribute(NEXT);
}/logout?everywhere=true also sends the browser to /auth/signout, so the next sign-in in any app prompts again.
The PHP app does the same in OpsApiAuth.php. Both live in bwalia/workflow-examples.
Settings that matter on the OpsAPI side
| Setting | What it does |
|---|---|
CORS_ALLOWED_DOMAINS | A domain and all its subdomains that may call the API and receive tokens |
PASSWORD_RESET_ALLOWED_ORIGINS | Extra exact origins allowed to receive tokens and reset links |
SIGNUP_NAMESPACE_SLUG | The workspace a first-time sign-in joins |
GOOGLE_CLIENT_ID / _SECRET / _REDIRECT_URI | The Google OAuth client; the redirect URI must be registered on it |
What it isn't
OpsAPI isn't an OpenID Connect or SAML provider: there are no client registrations and no /authorize or /token endpoints, so a stock OIDC client such as Spring Security's oauth2Login() can't use it directly. Apps integrate with the redirect, token and /auth/me contract above. Mobile apps post a Google or Apple ID token to POST /auth/oauth/validate instead.
Try it
The live demo runs exactly this flow with a Java app and a PHP app. Sign in to one, then open the other.