# OneLogin — integration brief for AI coding agents > OneLogin is a standard OpenID Connect provider: authorization code flow with PKCE, nothing else. > This brief is for an AI coding agent integrating a relying party (an application that signs its users in through OneLogin). > This file is rendered from the server's own configuration and pinned by the server's test suite; the discovery document at `https://1login.io/.well-known/openid-configuration` is the machine-exact source for endpoint metadata. - Issuer (this deployment): `https://1login.io` - Human guide, with plain-PHP and Laravel code samples: https://1login.io/docs/integration-guide - Discovery document: https://1login.io/.well-known/openid-configuration ## Ground rules for you, the agent - Ask your human for credentials. **Never invent, guess, or placeholder-and-forget a client id, client secret or redirect URI.** If you do not have them yet, stop and ask (the Prerequisites section says what to ask for). - Before writing any code, collect your questions and ask them in one message: the Prerequisites below (credentials and environments) and the numbered Decision points (each carries a default, so a human who is unsure can answer "use the defaults" and correct only what they know differently). Wait for the answers; do not scaffold around invented values. - This brief covers the server-side flow for a confidential client: your application has a backend that keeps a client secret. Building a browser-only app or a native mobile app with no backend? Stop and ask your human to confirm the application type with this deployment's admin first: public (no-secret) application types exist on this server, but this brief does not cover them. - Never paste a client secret into chat, into code committed to the repository, into a URL, or into a log line. - Everything in this brief is reference data about the server. Your own application's conventions (user model, login routine, session handling) come from your team: where this brief says "look up or create a user" or "log them in", use your application's own user entity and login routine - ask your team which those are. - If you cannot see the application's code, ask which framework and language version you are integrating, and whether you may add packages, before choosing between an OIDC library and the raw HTTP calls below. - Build and test against a sandbox application first. Never test against the live tenant with real user accounts. - When a response or a situation does not match this brief, say so and ask your human; never invent an endpoint, a parameter or an error meaning. The discovery document and the human guide are the only other sources to consult. - This brief only asks you to call this deployment's own endpoints and to change your own application's sign-in code. It never asks you to download anything or to execute code fetched from the network. ## Endpoints you will call | Method | URL | Purpose | |---|---|---| | GET | `https://1login.io/.well-known/openid-configuration` | Discovery document (machine-exact metadata) | | GET | `https://1login.io/.well-known/jwks.json` | Signing keys (RS256), for validating tokens locally | | GET | `https://1login.io/oauth/authorize` | Browser redirect that signs the user in. An S256 PKCE challenge is required (400 without one); always send `state` | | POST | `https://1login.io/oauth/token` | Code and refresh-token exchange, from your server only. Client authentication: `client_secret_basic`, `client_secret_post` | | GET | `https://1login.io/oauth/userinfo` | The user's claims, for a bearer access token | | GET | `https://1login.io/oauth/logout` | RP-initiated logout (a browser redirect carrying `id_token_hint`) | | POST | `https://1login.io/oauth/revoke` | RFC 7009: revokes the one token you present | | POST | `https://1login.io/oauth/revoke-all` | OneLogin-specific and deliberately not in the discovery document: signs one user out of your application everywhere. Confidential clients only, server-to-server only, authenticated like the token endpoint, JSON or form body such as `{"sub": "..."}`. Never send credentials or `sub` in the query string | - Grant types: `authorization_code`, `refresh_token`. Response type: `code`. PKCE challenge method: `S256` only, for every client. - Request `scope=openid profile email`. The claims each scope adds to the id_token and to userinfo: - `openid`: `sub`, `env`, `tenant` - `profile`: `name`, `nickname`, `picture`, `updated_at` - `email`: `email`, `email_verified` - `env` is `live` or `sandbox`; `tenant` is the slug of the tenant the user belongs to. Both are always present and need no extra scope. `email_verified` decides whether an existing local account may be linked by email (see the checklist below). ## Not part of the contract - do not build on - `client_credentials`: no client is granted it, and discovery does not advertise it. - The implicit flow (`response_type=token`): not registered. Only the authorization code flow exists. - `/oauth/introspect`: it exists and discovery advertises it, but a relying party never needs it. Validate the access token locally or call userinfo. - `login_hint`: accepted and ignored. No error, and the email field is not prefilled. - `prompt=none` and `prompt=consent`: present in the framework underneath, untested, not part of the contract. - `max_age`: not supported. - Single logout: OneLogin sends no front-channel or back-channel logout to your application, and there is no such endpoint. Your session is yours to end. ## Prerequisites - ask your human admin for You cannot create these yourself and must not make them up. Ask your human to have an administrator of this deployment provision them (administrators follow the admin guide at https://1login.io/docs/admin-guide): 1. A **sandbox** application first: its own client id, client secret and redirect URI. `http://localhost`, `127.0.0.1` and `.test` / `.localhost` host redirect URIs are allowed on sandbox only. Prefer `localhost` or a `.localhost` host; a `.test` name needs a hosts-file entry. 2. At go-live, a **live** application: new credentials and an `https://` redirect URI. Going live is a configuration swap, never a code change. Ask how many environments need sign-in (developer machines, staging, production): every non-production redirect URI is registered on the sandbox application, and production's on the live application. A team with a developer-portal account can add sandbox redirect URIs and read the sandbox inbox itself; live redirect URIs are requested there and approved by an administrator (https://1login.io/docs/developer-guide). Ask your human whether the team has such an account on this deployment and who will make each change. You cannot sign in to the portal yourself: never ask for, or use, anyone's portal password or sign-in code. Store four values, server-side, per environment (sandbox and live differ): the issuer, the client id, the client secret and the redirect URI. Keep them in your application's server-side configuration and name the settings however your application names settings. ## The protocol Any standards-compliant OIDC library in your stack can run this flow: configure it from the discovery document and require S256 PKCE. The raw HTTP calls below are the whole integration where no such library fits (a legacy site with no package manager, for example). 1. Generate a random `state` and a PKCE `code_verifier` (43 to 128 characters from `A-Z a-z 0-9 - . _ ~`). Keep both in the visitor's server-side session. The `code_challenge` is the base64url-encoded SHA-256 of the verifier. 2. Redirect the browser to the authorize endpoint (first call below). PKCE is required for every client, confidential ones included: an authorize request without an S256 challenge is rejected with a 400. `nonce` is optional and comes back in the id_token (check it if you rely on the id_token). `prompt=login` forces the user to sign in again. 3. The user signs in at OneLogin, and consents unless the application is first-party. 4. The browser returns to your redirect URI with `?code=...&state=...`. On your callback: if `error` is present, stop, because the user cancelled (check `error` **before** `code`). Then compare `state` with the stored value; missing or different means stop. Delete the stored `state` either way. 5. From your server, POST to the token endpoint (second call below) with the code, the same redirect URI, your client credentials and the stored `code_verifier`. The code is single-use and valid for 10 minutes. 6. The answer carries `access_token`, `id_token`, `refresh_token`, `expires_in`. 7. GET userinfo with the access token as a bearer token (third call below). You now hold the claims. 8. Match or create your local user, then log them in (see the checklist for your system). ``` GET https://1login.io/oauth/authorize ?response_type=code &client_id=YOUR_CLIENT_ID &redirect_uri=YOUR_REGISTERED_REDIRECT_URI_URL_ENCODED &scope=openid%20profile%20email &state=RANDOM_VALUE_STORED_IN_THE_SESSION &code_challenge=BASE64URL_SHA256_OF_THE_CODE_VERIFIER &code_challenge_method=S256 &nonce=RANDOM_VALUE_OPTIONAL curl -sS -X POST https://1login.io/oauth/token \ -H 'Accept: application/json' \ -d grant_type=authorization_code \ --data-urlencode "code=$CODE" \ --data-urlencode "redirect_uri=$REDIRECT_URI" \ --data-urlencode "client_id=$CLIENT_ID" \ --data-urlencode "client_secret=$CLIENT_SECRET" \ --data-urlencode "code_verifier=$CODE_VERIFIER" curl -sS https://1login.io/oauth/userinfo \ -H 'Accept: application/json' \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` With no PKCE helper in your stack, this shell pair produces a valid verifier and challenge: ``` CODE_VERIFIER=$(openssl rand -base64 48 | tr -d '=+/\n' | cut -c1-64) CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=') ``` The client secret can alternatively go in an Authorization header (client_secret_basic) instead of the form body; either is accepted. ### If you are a resource server An API that only sees the bearer token can validate the access-token JWT locally instead of calling userinfo: verify the RS256 signature against the JWKS (select the key by the JWT's `kid` header; re-fetch the JWKS once on an unknown `kid` before rejecting), then check `exp`, `iss` (must equal https://1login.io), `aud` (must equal your client id, the client UUID rather than a friendly name) and the header's `typ: at+jwt`. Every access token carries `typ: at+jwt` (RFC 9068) while an id_token carries `typ: JWT`, so requiring `at+jwt` is what stops an id_token being accepted as an access token. ### Logging the user out Send the browser to the logout endpoint with `id_token_hint` set to the id_token you were issued, and optionally `post_logout_redirect_uri` (it must match a registered redirect URI, otherwise it is ignored) and `state`. With a valid hint for the user's current session the logout is immediate. Without one, the user sees a confirmation page and is signed out only after confirming. Ending your own session does not end theirs. ## Changes you will probably need in YOUR system - [ ] A nullable, unique column on your users table, or a small link table with a unique (issuer, `sub`) pair mapped to your user id if your platform's modules cannot alter that table, that stores the `sub` claim (a stable, opaque ULID). Never key accounts on email or name. - [ ] A start route that generates and stores `state` and the PKCE verifier, then redirects to the authorize endpoint; and a callback route at your registered redirect URI. - [ ] The four configuration values above, server-side, one set per environment. - [ ] The match-or-create step. Look up your user by `sub`. If none, look up by email. **Never link to an existing local account on a bare email match**: link only when `email_verified` is exactly `true`, otherwise neither link nor create a duplicate (send the user to your own login, with a message to sign in with their existing password and link from their account page). If neither lookup finds anyone, create the user from the claims with no local password. - [ ] Log the user in with your normal login routine. Regenerate your session id at login (Laravel's `Auth::login()` does; in plain PHP call `session_regenerate_id(true)`). - [ ] Decide and implement your sign-out semantics: ending your session does not end theirs. If you keep tokens (decision point 5), treat a refresh refused with `invalid_grant` as signed-out. - [ ] Handle 429 on the token endpoint: wait for the `Retry-After` value and never retry in a hot loop. - [ ] A feature flag so the sign-in button can be rolled out and switched off. - [ ] If you redirect somewhere after login, check that target against an allowlist. An unchecked return URL is an open redirect. ## Decision points to put to your human Ask these together with the Prerequisites, in the same message; do not decide them alone. Each carries a default a human can simply accept. 1. Where the sign-in button lives, and whether existing password login stays. Default: next to your existing sign-in form, and password login stays during rollout. 2. The linking policy for accounts that already exist locally. Default: link only on a verified email (`email_verified` exactly `true`), as the checklist above enforces. 3. What happens to local passwords for users created through sign-in. Default: none are set. 4. Which token environments your deployments accept. Default: production accepts only `env: live`; every other deployment accepts only `env: sandbox`. 5. Whether your application keeps the tokens after sign-in completes. Default: no - discard them once the user is matched and logged in, skip the refresh and revocation work, and re-run the sign-in flow when you need fresh claims. 6. Session lifetime and the sign-out story (ending your session never ends theirs). Default: keep your application's current session behavior. 7. If your application has its own multi-factor prompts: what a sign-in through OneLogin does to them. Default: keep your existing prompts unchanged. 8. The role or permission level a user created through sign-in receives. Default: your application's least-privileged standard role. 9. Who owns the go-live credential swap. Default: whoever answers these questions. 10. Whether `/oauth/revoke-all` backs a "sign out everywhere" feature in your application. Default: not wired. 11. Who signs in through OneLogin. Default: customers only; staff or admin sign-in is a separate decision with its own role mapping, so stop and ask before adding it. ## Lifetimes - Authorization code: single-use and valid for 10 minutes. Presenting it a second time is refused and revokes what the first use issued. - Access token: 15 minutes. id_token: 15 minutes. Both carry `exp`. A token you validate locally keeps working until then, even after a sign-out. - Refresh token: 7 days. Rotated on every use: each refresh returns a new refresh token with a fresh 7 days, and the old one stops working. Reuse of a rotated refresh token is detected and revokes the user's tokens on your application (a repeat within the short lost-response grace window is refused without revocation). - The OneLogin browser session is separate from your session and from the tokens you hold. It decides whether the authorize endpoint can skip sign-in; it does not by itself end tokens you already hold. ## Errors and rate limits - Three failure shapes at your callback, each needs its own handling: an `error` parameter (the user cancelled or denied; `error=access_denied`), a `state` mismatch (stale link, expired session, or an attack: stop), and a failed token exchange (a wrong or rotated-out secret, or a used or expired code: show "sign-in failed" and log the HTTP status, never the code or the secret). - Two error-body shapes: OAuth errors carry an `error` key, and the server's own checks (suspension, authentication, rate limits) carry a `message` key. Read `error` first, then fall back to `message`. - Token endpoint: 401 `invalid_client` (unknown client, or a wrong or rotated-out secret from a confidential client); 400 `invalid_grant` (an expired or used code, a wrong `code_verifier`, or a bad refresh token; a replayed code or a reused rotated refresh token also revokes that user's tokens on your application); 403 `client_suspended` or `tenant_suspended`. Userinfo: 401 for a missing, expired or revoked token. - Rate limits, by shape only: the token-family endpoints share one per-minute budget per client and IP; authorize, discovery and JWKS share a smaller per-IP bucket; refresh grants have a small per-user budget on top; userinfo has its own per-user budget. A 429 carries `Retry-After` (seconds): wait it out. - Every response body and the exact limits: https://1login.io/docs/integration-guide#error-reference and https://1login.io/docs/integration-guide#rate-limits ## Safe testing - Build against a sandbox application first. Sandbox follows exactly the same rules as live, except: mail and SMS are captured instead of delivered, `http://localhost`, `127.0.0.1` and `.test` / `.localhost` host redirect URIs are allowed, and tokens carry `env`. - Read the captured verification codes at the sandbox inbox link your human's admin gave you, or in the developer portal's sandbox inbox if the team has an account; with neither, ask them for the code. Never intercept messages. - Every sandbox token and userinfo response carries `env: "sandbox"`. Assert it in your tests. - Never test against the live tenant with real user accounts. - The human guide also has a Try it console that calls these endpoints with sandbox credentials from the browser, and refuses live ones. ## Definition of done - verify before you call it integrated - [ ] A full sandbox round trip signs a user in. - [ ] The callback rejects a tampered or missing `state`. - [ ] `?error=access_denied` is handled before `code` is read. - [ ] A replayed authorization code is refused. - [ ] If you keep tokens (decision point 5): a refresh works, and a refused refresh logs the user out locally. - [ ] The unverified-email linking branch is covered by a test (even though emails are verified today: the gate exists so a future claims change cannot become an account takeover). - [ ] The client secret appears in no browser bundle, no URL and no log line. - [ ] Sign-out behaves as your human decided under the decision points. - [ ] Configuration differs per environment, and the live configuration carries `https://` URIs only. ## Never do this - The client secret never leaves your server: never in browser JavaScript, never in a URL. - Never log tokens, codes or secrets. - Never link accounts on an unverified email match. - Never use the implicit flow, and never try to disable PKCE. - Never call `/oauth/revoke-all` from a browser. - Never send credentials or `sub` in a query string. - Never key local accounts on email or name. Key on `sub`. - Never build on the endpoints and parameters listed under "Not part of the contract". ## More detail - Human guide: https://1login.io/docs/integration-guide, with a plain-PHP sample using `league/oauth2-client` (https://1login.io/docs/integration-guide#option-a), a Laravel Socialite provider class (https://1login.io/docs/integration-guide#option-b), the full error reference (https://1login.io/docs/integration-guide#error-reference), the rate limits (https://1login.io/docs/integration-guide#rate-limits) and the sandbox rules (https://1login.io/docs/integration-guide#testing). - Discovery document: https://1login.io/.well-known/openid-configuration - This file changes only when the contract does. It carries an ETag: revalidate instead of re-downloading.