Developers
Sign in with Oshaani
Let people sign in to your website or app with their oshaani.com account. oshaani.com is a standard OpenID Connect provider, so any OIDC library works. You get verified profile details, centrally managed roles and permissions, and single logout across every connected site.
Overview
Sites that already use Sign in with Oshaani include social.oshaani.com. The integration is the same for every site:
What you get:
- One account everywhere — users don't create a new password for your site.
- Verified email — the
email_verifiedclaim tells you whether oshaani.com has confirmed the address. - Central authorization — roles and permissions are managed once in oshaani.com and sent to your site at every sign-in.
- Single logout — when a user signs out of oshaani.com, or loses access, your site is told immediately.
Endpoints
Most libraries only need the issuer or the discovery URL and will find everything else automatically.
| Issuer | https://oshaani.com/oMust match the iss claim exactly — no trailing slash. |
|---|---|
| Discovery document | https://oshaani.com/o/.well-known/openid-configuration |
| Authorization endpoint | https://oshaani.com/o/authorize/ |
| Token endpoint | https://oshaani.com/o/token/ |
| UserInfo endpoint | https://oshaani.com/o/userinfo/ |
| JWKS (signing keys) | https://oshaani.com/o/.well-known/jwks.json |
| End session (logout) | https://oshaani.com/o/logout/ |
| Token revocation | https://oshaani.com/o/revoke_token/ |
| Flow | Authorization code (response_type=code) only |
|---|---|
| PKCE | Required on every request. Use code_challenge_method=S256. |
| Client authentication | client_secret_basic (HTTP Basic) or client_secret_post |
| ID token signing | RS256, keys published at the JWKS URL |
| Scopes | openid (sign you in)profile (your name and username)email (your email address)roles (your oshaani roles and permissions) |
| Lifetimes | Authorization code 60 s · access token 3600 s · ID token 3600 s · refresh tokens rotate on every use |
1. Register your site
Clients are registered by the Oshaani team — there is no self-service sign-up, so every connected site is known and reviewed. Email support@oshaani.com with:
| Detail | Example |
|---|---|
| Site name (shown to admins) | Acme Helpdesk |
| Redirect URI(s) — where oshaani.com sends users after sign-in. Exact match, HTTPS only. | https://help.acme.com/auth/oshaani/callback |
| Post-logout redirect URI(s) — where users land after signing out. Optional. | https://help.acme.com/ |
| Back-channel logout URI — receives logout notifications. Optional but recommended. | https://help.acme.com/auth/oshaani/backchannel-logout |
| Who may sign in | Any oshaani.com user, or only users who have been given a role for your site |
| App type | Server-side web app (gets a client secret), or single-page / mobile app (public client, PKCE only) |
You'll receive a client_id and, for server-side apps, a client_secret. The secret is shown once — store it in your secrets manager or environment variables, never in source control or front-end code. If it leaks, ask us to rotate it; the old one stops working immediately.
2. Configure your app
Add the credentials to your app's environment:
OSHAANI_ISSUER=https://oshaani.com/o
OSHAANI_CLIENT_ID=your-client-id
OSHAANI_CLIENT_SECRET=your-client-secret
OSHAANI_REDIRECT_URI=https://help.acme.com/auth/oshaani/callbackThen add a “Sign in with Oshaani” button that links to your login route (step 3). If your site already has its own accounts, decide how to connect them: we recommend matching existing users by email only when email_verified is true, and otherwise letting signed-in users link their Oshaani account from their profile.
3. The sign-in flow
3a. Send the user to oshaani.com
Generate a random state, nonce and PKCE code_verifier, store them in the user's session, and redirect to the authorization endpoint:
GET https://oshaani.com/o/authorize/
?response_type=code
&client_id=your-client-id
&redirect_uri=https%3A%2F%2Fhelp.acme.com%2Fauth%2Foshaani%2Fcallback
&scope=openid%20profile%20email%20roles
&state=RANDOM_STATE
&nonce=RANDOM_NONCE
&code_challenge=BASE64URL(SHA256(code_verifier))
&code_challenge_method=S256redirect_urimust exactly match one you registered.- Ask only for the scopes you need.
openidis always required; addrolesif you use central authorization. - Optional:
prompt=loginforces the user to re-enter their password.
3b. Handle the callback
oshaani.com redirects back to your redirect_uri with ?code=…&state=…. Check that state equals the value you stored; if it doesn't, stop. If the user declined or something went wrong you'll get ?error=…&error_description=… instead.
3c. Exchange the code for tokens (server-side)
The code is single-use and expires after 60 seconds.
curl -X POST https://oshaani.com/o/token/ \
-u "$OSHAANI_CLIENT_ID:$OSHAANI_CLIENT_SECRET" \
-d grant_type=authorization_code \
-d code="CODE_FROM_CALLBACK" \
-d redirect_uri="$OSHAANI_REDIRECT_URI" \
-d code_verifier="CODE_VERIFIER_FROM_SESSION"{
"access_token": "…",
"expires_in": 3600,
"token_type": "Bearer",
"scope": "openid profile email roles",
"refresh_token": "…",
"id_token": "eyJhbGciOiJSUzI1NiIs…"
}Public clients (single-page and mobile apps) send client_id in the form body instead of a secret; PKCE protects the exchange.
4. Validate the ID token
The id_token is a JWT signed with RS256. Your OIDC library normally does this for you; if you validate it yourself, check all of the following before trusting it:
- The signature verifies with the key from the JWKS whose
kidmatches the token header, andalgisRS256. issis exactlyhttps://oshaani.com/o.audcontains yourclient_id.expis in the future (allow a minute of clock skew at most).nonceequals the value you stored in step 3a.
Cache the JWKS, but re-fetch it when you see a kid you don't know — that's how key rotation reaches you.
Identify users by sub, not by email. sub is stable and unique for each oshaani.com user; email addresses can change.
Claims
Claims appear in the ID token and from the UserInfo endpoint (GET https://oshaani.com/o/userinfo/ with Authorization: Bearer <access_token>), filtered by the scopes you were granted.
| Claim | Scope | Description |
|---|---|---|
sub | openid | Stable unique user ID (string). Use this as the key for the user in your database. |
name | profile | Full name, or the username if no name is set. |
given_name, family_name | profile | First and last name (may be empty). |
preferred_username | profile | oshaani.com username. Not guaranteed to stay the same. |
email | email | Primary email address. |
email_verified | email | true only if oshaani.com has confirmed the address (for example via Google or LinkedIn sign-in). Don't link existing accounts by email when this is false. |
roles | roles | Array of role slugs granted to this user for your site, including global roles. Example: ["staff"]. |
permissions | roles | Array of permission strings from those roles. Example: ["tickets.view", "tickets.*"]. |
{
"iss": "https://oshaani.com/o",
"sub": "1042",
"aud": "your-client-id",
"iat": 1791100000,
"exp": 1791103600,
"auth_time": 1791099950,
"nonce": "RANDOM_NONCE",
"name": "Ada Lovelace",
"given_name": "Ada",
"family_name": "Lovelace",
"preferred_username": "ada",
"email": "ada@example.com",
"email_verified": true,
"roles": ["staff"],
"permissions": ["admin.access"]
}Roles & permissions
Authorization is managed centrally on oshaani.com, so access is granted and revoked in one place for every connected site.
- Global roles apply to every site. Two exist by default:
admin(permission*) andstaff(permissionadmin.access). - Site roles are created for one site only, with whatever permission strings that site understands — for example
tickets.vieworbilling.*. - Wildcards: by convention
*means every permission andapp.*means every permission starting withapp.. Your site decides how to interpret them. - Restricted sites: if you asked for “only users with a role”, people without one see an access-denied page on oshaani.com and are never sent back to you.
- Freshness: roles are sent at every sign-in. When a role is removed or a user is deactivated, oshaani.com sends a back-channel logout so the user has to sign in again and picks up their new access.
def has_permission(granted, wanted):
for perm in granted:
if perm == "*" or perm == wanted:
return True
if perm.endswith(".*") and wanted.startswith(perm[:-1]):
return True
return False
has_permission(claims["permissions"], "tickets.close")Logout
Sign the user out everywhere (RP-initiated logout)
After clearing your own session, send the user to the end-session endpoint to sign them out of oshaani.com too:
GET https://oshaani.com/o/logout/
?id_token_hint=ID_TOKEN_FROM_SIGN_IN
&post_logout_redirect_uri=https%3A%2F%2Fhelp.acme.com%2F
&client_id=your-client-id
&state=OPTIONAL_STATEpost_logout_redirect_uri must be one you registered. Keep the ID token from sign-in in the session so you can send it as id_token_hint.
Get told when the user signs out elsewhere (back-channel logout)
If you registered a back-channel logout URI, oshaani.com sends a server-to-server POST to it when the user signs out of oshaani.com, has a role added or removed, or is deactivated. The body is form-encoded with a single field, logout_token, a signed JWT:
// header
{ "alg": "RS256", "typ": "logout+jwt", "kid": "…" }
// payload
{
"iss": "https://oshaani.com/o",
"aud": "your-client-id",
"iat": 1791100000,
"exp": 1791100120,
"jti": "5c0e5f5e-…",
"sub": "1042",
"events": { "http://schemas.openid.net/event/backchannel-logout": {} }
}To handle it:
- Verify the signature,
iss,audandexpexactly as for an ID token. - Check that
eventscontainshttp://schemas.openid.net/event/backchannel-logoutand that there is nononceclaim. - Reject a
jtiyou have already seen (keep them for a few minutes). - End every session on your site for that
sub— the token has nosid. - Respond
200 OK. Respond400if validation fails. The endpoint must not require a CSRF token or a session cookie.
Code examples
These use well-known OIDC libraries that handle discovery, PKCE, state, nonce and token validation for you.
Node.js — Express with openid-client 5.x
const { Issuer, generators } = require('openid-client');
const oshaani = await Issuer.discover('https://oshaani.com/o/.well-known/openid-configuration');
const client = new oshaani.Client({
client_id: process.env.OSHAANI_CLIENT_ID,
client_secret: process.env.OSHAANI_CLIENT_SECRET,
redirect_uris: [process.env.OSHAANI_REDIRECT_URI],
response_types: ['code'],
});
app.get('/auth/oshaani/login', (req, res) => {
const code_verifier = generators.codeVerifier();
const state = generators.state();
const nonce = generators.nonce();
req.session.oshaani = { code_verifier, state, nonce };
res.redirect(client.authorizationUrl({
scope: 'openid profile email roles',
code_challenge: generators.codeChallenge(code_verifier),
code_challenge_method: 'S256',
state,
nonce,
}));
});
app.get('/auth/oshaani/callback', async (req, res) => {
const { code_verifier, state, nonce } = req.session.oshaani || {};
const tokenSet = await client.callback(
process.env.OSHAANI_REDIRECT_URI,
client.callbackParams(req),
{ code_verifier, state, nonce },
);
const claims = tokenSet.claims(); // validated ID token claims
req.session.user = { id: claims.sub, email: claims.email, roles: claims.roles };
req.session.idToken = tokenSet.id_token; // keep for logout
res.redirect('/');
});Python — Flask with Authlib
from authlib.integrations.flask_client import OAuth
oauth = OAuth(app)
oauth.register(
name="oshaani",
server_metadata_url="https://oshaani.com/o/.well-known/openid-configuration",
client_id=os.environ["OSHAANI_CLIENT_ID"],
client_secret=os.environ["OSHAANI_CLIENT_SECRET"],
client_kwargs={"scope": "openid profile email roles", "code_challenge_method": "S256"},
)
@app.route("/auth/oshaani/login")
def oshaani_login():
return oauth.oshaani.authorize_redirect(os.environ["OSHAANI_REDIRECT_URI"])
@app.route("/auth/oshaani/callback")
def oshaani_callback():
token = oauth.oshaani.authorize_access_token() # checks state, nonce and the ID token
claims = token["userinfo"]
session["user"] = {"id": claims["sub"], "email": claims["email"], "roles": claims.get("roles", [])}
session["id_token"] = token["id_token"]
return redirect("/")Next.js — Auth.js (NextAuth v5)
// auth.ts — callback URL to register: https://your-app.com/api/auth/callback/oshaani
import NextAuth from "next-auth";
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
{
id: "oshaani",
name: "Oshaani",
type: "oidc",
issuer: "https://oshaani.com/o",
clientId: process.env.AUTH_OSHAANI_ID,
clientSecret: process.env.AUTH_OSHAANI_SECRET,
authorization: { params: { scope: "openid profile email roles" } },
checks: ["pkce", "state", "nonce"],
},
],
});Any other certified OpenID Connect client library works the same way: point it at the discovery URL, set the client ID, secret and redirect URI, and enable PKCE with S256.
Security checklist
- Use HTTPS for every redirect, post-logout and back-channel URI.
- Always send PKCE (S256),
stateandnonce, and check them on the way back. - Exchange codes and keep the client secret on your server only — never in browser JavaScript or a mobile app bundle.
- Validate every ID token and logout token as described above; don't just decode them.
- Key users by
sub. Only trustemailfor account linking whenemail_verifiedistrue. - Re-check permissions on the server for every protected action; don't rely on hiding buttons.
- Store refresh tokens encrypted, and revoke them at
https://oshaani.com/o/revoke_token/when a user signs out.
Troubleshooting
| What you see | Likely cause |
|---|---|
| Error page on oshaani.com mentioning the redirect URI | The redirect_uri isn't registered or differs slightly (http vs https, trailing slash, port). It must match exactly. |
invalid_request about code_challenge | PKCE is missing. Every request needs code_challenge and code_challenge_method=S256. |
invalid_client from the token endpoint | Wrong client ID or secret, or the secret was rotated. |
invalid_grant from the token endpoint | The code expired (60 s), was already used, the redirect_uri differs from step 3a, or the code_verifier doesn't match the challenge. |
| Issuer mismatch in your library | Configure the issuer as exactly https://oshaani.com/o — no trailing slash, no www.. |
| User sees “You don't have access to this site” | Your site only allows users with a role and this user has none. Ask an Oshaani admin to grant one. |
email_verified is false | The user registered with an unconfirmed email. Ask them to sign in to oshaani.com with Google or LinkedIn once, or link accounts manually. |
| Roles look out of date | Roles are read at sign-in. Handle back-channel logout, or ask the user to sign in again. |
Still stuck? Email support@oshaani.com with your client ID, the time of the attempt and the exact error. Never send your client secret.