Clops SSO is a standard OpenID Connect provider. If your framework has an OIDC client, you are ten minutes from a working login.
In the SSO console, under Connected Products, add your product with the exact redirect URI your app will use. You get a client id and a client secret; the secret is displayed once and stored only as a hash, so put it in your secret manager immediately.
Redirect URIs must be absolute and use https, except on
localhost for development. They are matched exactly — no wildcards, no prefixes.
Use the discovery document rather than hard-coding endpoints:
https://sso.clopsai.com/.well-known/openid-configuration
// Node — npm i openid-client import * as client from 'openid-client'; const config = await client.discovery( new URL('https://sso.clopsai.com'), process.env.CLOPS_CLIENT_ID, process.env.CLOPS_CLIENT_SECRET); const verifier = client.randomPKCECodeVerifier(); // store `verifier` in the user's session — it must survive the round trip const url = client.buildAuthorizationUrl(config, { redirect_uri: 'https://your-app.com/auth/callback', scope: 'openid email profile', code_challenge: await client.calculatePKCECodeChallenge(verifier), code_challenge_method: 'S256' }); res.redirect(url.href);
Exchange the code for tokens. The library verifies the id_token signature
against the JWKS for you, and checks the nonce and audience.
const tokens = await client.authorizationCodeGrant(config, currentUrl, {
pkceCodeVerifier: req.session.verifier
});
const claims = tokens.claims();
// claims.sub — stable user id; key your local user on this, not the email
// claims.email — may change over time
// claims.tenant — which workspace the user belongs to
const user = await db.upsertUser({ ssoSub: claims.sub, email: claims.email });
req.session.userId = user.id;
# pip install authlib
oauth.register(
name="clops",
server_metadata_url="https://sso.clopsai.com/"
".well-known/openid-configuration",
client_id=CLOPS_CLIENT_ID,
client_secret=CLOPS_CLIENT_SECRET,
client_kwargs={
"scope": "openid email profile",
"code_challenge_method": "S256",
},
)
token = await oauth.clops.authorize_access_token(request)
user = token["userinfo"]
// Laravel — any OIDC package works
'clops' => [
'client_id' => env('CLOPS_CLIENT_ID'),
'client_secret' => env('CLOPS_CLIENT_SECRET'),
'redirect' => env('CLOPS_REDIRECT_URI'),
'issuer' => 'https://sso.clopsai.com',
],
$clopsUser = Socialite::driver('clops')->user();
User::updateOrCreate(
['clops_sub' => $clopsUser->getId()],
['email' => $clopsUser->getEmail()]
);
| Token | Lifetime | Use |
|---|---|---|
id_token | 1 hour | Who the user is. Verify against the JWKS, then create your own session. |
access_token | 1 hour | Call /sso/userinfo. Not meant for your own APIs. |
refresh_token | 14 days, rotating | Only issued when you request the offline_access scope. |
Refresh tokens rotate on every use. If an old one is presented again, we treat it as a leak and revoke the entire family — so always store the newest value you were given.
Clear your own session, then send the browser to the end-session endpoint so the SSO session ends too. Otherwise the next sign-in click goes straight back through.
GET https://sso.clopsai.com/sso/logout
?id_token_hint=<the id_token>
&post_logout_redirect_uri=https://your-app.com/goodbye
The redirect URI must be registered as a post-logout URI for your product, or the user simply lands on a Clops confirmation page.
Emails change. Store sub as the identity and treat the email as a mutable
attribute.
It is required, including for confidential clients. Requests without
code_challenge_method=S256 are rejected.
Always verify the signature, issuer and audience. A library does this; hand-rolled base64 decoding does not.
Send us the discovery URL you are pointing at and the error you see — that is usually enough to spot it.