TatvaOS · Applications
SSO Integration Guide
Let people sign in to your application with their TatvaOS account. It uses OpenID Connect, the same standard as "Sign in with Google". If you have never done this before, read from the top. It is shorter than it looks.
- The idea in one minute
- Step 1: get registered
- Step 2: put these values in your app
- The rules
- What happens, request by request
- The ID token: who the person is
- Keeping people signed in
- Using a library (recommended)
- The sign-in button
- Errors and what they mean
- Lifetimes and limits
- When access is taken away
- Checklist before going live
- Try it without writing code
The idea in one minute
Your app never sees a password. Instead:
- Your app sends the person's browser to TatvaOS.
- TatvaOS signs them in (if they are not already) and asks once: "may this app receive your name, work email and organisation?"
- TatvaOS sends the browser back to your app with a one-time code.
- Your server swaps the code for tokens. One of them, the ID token, says who the person is.
If the organisation later removes or suspends the person, or removes your app, their tokens stop working. That is the whole point: one account, managed in one place.
Step 1: get registered
An administrator of the organisation registers your app in TatvaOS: Organisation → Applications → New application. Give them three things.
| They need | Give them |
|---|---|
| Name | What people see on the consent screen. For example Payroll. |
| Redirect URI | The exact https:// address in your app where TatvaOS sends people back (your callback). It must match character for character: no wildcards, and a trailing slash counts. For a developer's own machine, http://localhost… is allowed. |
| Server app or not? | Yes if your app has a server that can keep a secret (websites, backends). No if it runs only on a phone or in a browser. |
They get back a client id (starts with tos_) and, for a server app, a client secret (starts with toss_). The secret is shown only once. If it is lost, the app must be registered again.
Step 2: put these values in your app
Most libraries and frameworks have a settings page or config for exactly these.
| Setting | Value |
|---|---|
| Issuer (also called provider or authority) | https://core.tatvaos.com |
| Discovery URL | https://core.tatvaos.com/.well-known/openid-configuration |
| Client id | from the administrator |
| Client secret | from the administrator (server apps only) |
| Scopes | openid profile email, plus offline_access for a refresh token — server apps only (see below) |
| Response type | code |
| PKCE | on, method S256 |
Your library reads the endpoints from the discovery URL. For reference, they are:
| Endpoint | URL |
|---|---|
| Authorize (send the browser here) | https://core.tatvaos.com/oauth/authorize |
| Token (your server calls this) | https://core.tatvaos.com/api/oauth/token |
| Userinfo | https://core.tatvaos.com/api/oauth/userinfo |
| Keys (JWKS) | https://core.tatvaos.com/api/oauth/jwks |
| Introspection | https://core.tatvaos.com/api/oauth/introspect |
| Revocation | https://core.tatvaos.com/api/oauth/revoke |
The rules
- Code flow only.
response_type=code. Other flows are refused. - PKCE with S256, always. Even for server apps. Without it, or with
plain, the request is refused. - Exact redirect URI.
https://app.example.com/cbandhttps://app.example.com/cb/are different. - The secret stays on your server. Never in a browser, a phone app, a repository or a log.
- Send
stateandnonce, and check them when the browser comes back. - Check
tid. Every token names the organisation. If your app serves one organisation, refuse tokens from any other.
What happens, request by request
You only need this if you are not using a library, or want to understand what the library does.
1. Send the person to TatvaOS
Make a random code_verifier (43 to 128 characters). Compute code_challenge = base64url(sha256(code_verifier)). Make a random state and nonce. Keep all three in the person's session. Then redirect the browser to:
https://core.tatvaos.com/oauth/authorize
?response_type=code
&client_id=tos_…
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fauth%2Fcallback
&scope=openid%20profile%20email%20offline_access
&state=…
&nonce=…
&code_challenge=…
&code_challenge_method=S256
The person signs in to TatvaOS if needed. The first time, they see a consent screen naming your app. After that, TatvaOS remembers.
2. Get the code back
https://app.example.com/auth/callback?code=…&state=…&iss=https%3A%2F%2Fcore.tatvaos.com%2F
Check state matches what you stored, and iss is https://core.tatvaos.com/. If the person cancelled or is not allowed, you get error=access_denied instead. The code works once and only for 60 seconds, so do the next step right away.
3. Swap the code for tokens (from your server)
curl -X POST https://core.tatvaos.com/api/oauth/token \
-d grant_type=authorization_code \
-d code=… \
-d redirect_uri=https://app.example.com/auth/callback \
-d client_id=tos_… \
-d client_secret=toss_… \
-d code_verifier=…
A phone or browser app leaves out client_secret. The secret may also go in an HTTP Basic header as client_id:client_secret. You get back:
{
"access_token": "…",
"token_type": "Bearer",
"expires_in": 600,
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6…",
"refresh_token": "…",
"scope": "openid profile email offline_access"
}
refresh_token appears only if you asked for offline_access. The access and refresh tokens are opaque strings; do not try to read them. The ID token is explained next.
4. Find out who the person is
Read it from the ID token, or ask userinfo with the access token:
curl https://core.tatvaos.com/api/oauth/userinfo -H "Authorization: Bearer …"
{ "sub": "…", "tid": "…", "name": "Asha Rao", "email": "asha@example.com", "email_verified": true }
Userinfo checks every time. It answers 401 the moment the person is suspended, removed, or your app is revoked.
The ID token: who the person is
The ID token is a signed JWT (RS256). Your library verifies it against the keys at the JWKS URL, picking the key whose kid matches the token. Then read these claims:
| Claim | Meaning |
|---|---|
sub | The person's permanent id. Use this as your key for the person. Never the email; emails change. |
tid | The organisation's id. Same for everyone in that organisation. |
email, email_verified | Work address (with email scope). Verified means the organisation has proved it owns that domain. |
name | Display name (with profile scope). |
iss | Must be exactly https://core.tatvaos.com/. |
aud | Must be your client id. |
nonce | Must match the one you sent. |
exp | The ID token lives 5 minutes. Use it to start the person's session in your app, then rely on your own session. |
kid and fetch again when you meet an unknown one. Never hard-code a key.Keeping people signed in
offline_access: TatvaOS refuses it when the application is registered, and refuses it
again if it is asked for at sign-in. A refresh token grants weeks of access while nobody is
present, and anything running in a browser cannot keep one safe — any cross-site scripting flaw
in your own page would hand it over. Use short access tokens and send the person through TatvaOS
again when one expires; if they still have a TatvaOS session, they will not be asked anything.
Access tokens last 10 minutes. With offline_access you also get a refresh token. When the access token expires, swap the refresh token for a new set:
curl -X POST https://core.tatvaos.com/api/oauth/token \
-d grant_type=refresh_token \
-d refresh_token=… \
-d client_id=tos_… \
-d client_secret=toss_…
Three things to know:
- Every refresh returns a new refresh token. Store it. The old one is retired.
- Using a retired refresh token again cancels the whole chain. TatvaOS treats it as a sign the token was copied. The person must sign in through TatvaOS again.
- A refresh token lasts 14 days if unused. Every refresh re-checks that the person, the organisation and your app are all still active.
Using a library (recommended)
Do not write the requests above yourself. Give your framework's OpenID Connect client the issuer, client id, secret and scopes, and turn PKCE on. Three examples of the shape:
ASP.NET Core
builder.Services.AddAuthentication(o => { o.DefaultScheme = "Cookies"; o.DefaultChallengeScheme = "oidc"; })
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", o => {
o.Authority = "https://core.tatvaos.com";
o.ClientId = "tos_…";
o.ClientSecret = builder.Configuration["TatvaOS:ClientSecret"]; // never in code
o.ResponseType = "code";
o.UsePkce = true;
o.Scope.Clear();
foreach (var s in new[] { "openid", "profile", "email", "offline_access" }) o.Scope.Add(s);
o.CallbackPath = "/auth/callback"; // register https://your-app/auth/callback
o.SaveTokens = true;
});
Node.js (openid-client)
import * as client from 'openid-client';
const config = await client.discovery(new URL('https://core.tatvaos.com'), 'tos_…', process.env.TATVAOS_CLIENT_SECRET);
// send the person to TatvaOS
const codeVerifier = client.randomPKCECodeVerifier();
const url = client.buildAuthorizationUrl(config, {
redirect_uri: 'https://app.example.com/auth/callback',
scope: 'openid profile email offline_access',
code_challenge: await client.calculatePKCECodeChallenge(codeVerifier),
code_challenge_method: 'S256',
state, nonce,
});
// in the callback
const tokens = await client.authorizationCodeGrant(config, new URL(req.url, 'https://app.example.com'), {
pkceCodeVerifier: codeVerifier, expectedState: state, expectedNonce: nonce,
});
const who = tokens.claims(); // who.sub, who.email, who.name, who.tid
Python (Authlib with Flask)
oauth = OAuth(app)
oauth.register(
name='tatvaos',
server_metadata_url='https://core.tatvaos.com/.well-known/openid-configuration',
client_id='tos_…',
client_secret=os.environ['TATVAOS_CLIENT_SECRET'],
client_kwargs={'scope': 'openid profile email offline_access', 'code_challenge_method': 'S256'},
)
# send: return oauth.tatvaos.authorize_redirect(url_for('callback', _external=True))
# callback: token = oauth.tatvaos.authorize_access_token(); who = token['userinfo']
The sign-in button
Use the same button everyone else uses, so a person recognises what it does before they click it. It is self-contained: no font to load, no image to host, no stylesheet to link. Point href at your own endpoint that starts the flow.
This is the button, at the size it should be:
Sign in with TatvaOSCopy this. A dark-background version and an icon-only version for tight spaces are on the button assets page.
<style>
.tos-btn{display:inline-flex;align-items:center;gap:12px;height:44px;
padding:0 20px 0 16px;border-radius:8px;border:1px solid #D9DEE8;
background:#fff;color:#0B1B3F;text-decoration:none;cursor:pointer;
font:600 15px/1 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,
Helvetica,Arial,sans-serif;letter-spacing:-.01em}
.tos-btn:hover{background:#F4F7FC;border-color:#C3CBDA}
.tos-btn:focus-visible{outline:none;box-shadow:0 0 0 3px rgba(15,124,245,.35)}
.tos-btn svg{flex:none;width:20px;height:20px}
</style>
<a class="tos-btn" href="/auth/tatvaos/start">
<svg viewBox="0 0 48 48" aria-hidden="true">
<defs><linearGradient id="tosMark" x1="6" y1="4" x2="42" y2="44"
gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#1E52D9"/>
<stop offset=".55" stop-color="#2A7BEE"/>
<stop offset="1" stop-color="#3BA9F7"/>
</linearGradient></defs>
<path fill="url(#tosMark)" d="M17.94 3.5 Q24 0 30.06 3.5 L38.72 8.5
Q44.78 12 44.78 19 L44.78 29 Q44.78 36 38.72 39.5 L30.06 44.5
Q24 48 17.94 44.5 L9.28 39.5 Q3.22 36 3.22 29 L3.22 19
Q3.22 12 9.28 8.5 Z"/>
<rect x="13" y="13" width="22" height="6.5" rx="2" fill="#fff"/>
<rect x="20.75" y="13" width="6.5" height="23" rx="2" fill="#fff"/>
</svg>
Sign in with TatvaOS
</a>
Rules for using it
These exist so a person seeing the button knows what it does and can trust it.
- Do not recolour the mark. The gradient hexagon is the only version. No solid fills, no inverted colours, no tinting to match your brand.
- Do not change the wording. It reads Sign in with TatvaOS. Not "Login with", not "Continue with", not "TatvaOS SSO".
- Keep the mark's proportions. Scale it, do not stretch it, and do not crop the hexagon.
- Leave clear space of at least the mark's own height on every side.
- Minimum height 36px. Below that the mark stops being legible.
- Give it equal weight beside other sign-in providers. Do not make it smaller or quieter than the others on the same screen.
- Never use the mark or the name to suggest TatvaOS endorses, built, owns or has reviewed your application. The button means people can sign in with a TatvaOS account. It means nothing else.
- Never put the button on a page that collects a TatvaOS password. Sign-in happens on TatvaOS's own pages and nowhere else. An application that asks for a TatvaOS password is doing something wrong, whatever its intentions.
Errors and what they mean
| Error | Where | Meaning |
|---|---|---|
invalid_request "client_id is invalid" | authorize | Unknown client id, or the app was revoked. Shown on a TatvaOS page; the browser does not come back to you. |
invalid_request about the redirect URI | authorize | The redirect URI does not exactly match a registered one. |
invalid_request about code_challenge | authorize | PKCE missing, or not S256. |
access_denied | your callback | The person cancelled, belongs to another organisation, or cannot sign in. |
login_required, consent_required | your callback | You sent prompt=none and the person is not signed in or has not consented. |
invalid_client | token | Wrong secret, unknown client id, or the app was revoked. |
invalid_grant | token | The code expired, was already used, or the verifier is wrong. Or the refresh token was retired, reused, or its person or organisation is no longer active. |
temporarily_unavailable (HTTP 429) | token, authorize | Too many requests from your address in one minute. Wait a minute; do not retry in a loop. |
| HTTP 401 | userinfo | The access token expired or was revoked. Refresh, or sign the person in again. |
Lifetimes and limits
| Item | Value |
|---|---|
| Code | 60 seconds, single use |
| ID token | 5 minutes |
| Access token | 10 minutes, checked on every call |
| Refresh token | 14 days, replaced on every use |
| Token endpoint | 120 requests a minute per address |
| Authorize endpoint | 60 requests a minute per address |
When access is taken away
- The administrator revokes your app: new sign-ins are refused, its refresh tokens stop, its access tokens are refused on their next call.
- A person removes your app on their account page: the same, for that person only. Their next sign-in asks them again.
- A person is suspended or removed: their refresh and access tokens stop at their next use.
Checklist before going live
- Registered the exact callback address,
https://only (except a developer's own machine). - Client secret lives in server configuration. Never in a browser, a phone, a repository or a log.
- PKCE S256 on every request. Fresh
state,nonceand verifier every time. - ID token verified: signature by
kid, theniss,aud,nonce,exp. - People keyed on
sub, never onemail. tidchecked against the organisations you serve.- Each new refresh token stored as it arrives; a retired one never reused.
401from userinfo andinvalid_grantfrom refresh handled as "sign in again", not retried.- No codes, tokens, verifiers or full callback URLs in your logs.
Try it without writing code
- Ask the administrator to register a test app with redirect URI
https://oidcdebugger.com/debugand "Server application" unticked. Get the client id. - Go to oidcdebugger.com. Authorize URI:
https://core.tatvaos.com/oauth/authorize. Client ID: yours. Scope:openid profile email. Response type:code. Tick Use PKCE. Send request. - You see the TatvaOS consent screen, then a page showing the code and your state. That is the sign-in leg working. The code-for-tokens leg is your server's part, which the debugger does not do.
- Ask the administrator to revoke the test app when you are done.