Sign in with Tahoe
Let recruiters sign in to your app with their Tahoe account, and read or change their data.
Sign in with Tahoe lets recruiters log in to your app with the Tahoe account they already have. Your app can then call the Tahoe API on their behalf, reading only what they can see in Tahoe and only what they agreed to share.
Tahoe is a standard OpenID Connect (OIDC) provider using the authorization code flow with PKCE. If your stack already has an OIDC client library, point it at the discovery document below and most of the work is done.
Register your app
There are two ways to get an app, and the sign-in flow below is the same for both.
- An app for your own workspace. An owner or admin registers it in Settings, under Developer. It is confidential (it keeps a client secret on a server), it takes
httpsredirect URIs only, and it can be approved only for the workspace that registered it. Self-serve registration is being rolled out: the section appears in Settings when it is available for your workspace. Connect an app walks through it. - An app that serves many customers, or one that cannot keep a secret. Tahoe registers it for you. Email [email protected] with:
- your app’s name, as recruiters should see it on the consent screen;
- every redirect URI you will use, exactly, including the one for local development (for example
https://app.example.com/auth/tahoe/callback); - any address to return users to after they sign out;
- the scopes you need:
openid,profile,email,offline_access, and any API scopes from the scope table; - whether your app can keep a secret (a server-side app) or cannot (a mobile or single-page app).
Either way you get a client_id (it starts with thc_) and, for a server-side app, a client secret.
Discovery
The discovery document describes every endpoint below. Configure your client from it rather than typing the endpoints in by hand.
curl -s https://tahoe.workonward.com/api/partner/v1/.well-known/openid-configuration{
"issuer": "https://tahoe.workonward.com/api/partner/v1",
"authorization_endpoint": "https://tahoe.workonward.com/api/partner/v1/oauth/authorize",
"token_endpoint": "https://tahoe.workonward.com/api/partner/v1/oauth/token",
"userinfo_endpoint": "https://tahoe.workonward.com/api/partner/v1/oauth/userinfo",
"jwks_uri": "https://tahoe.workonward.com/api/partner/v1/.well-known/jwks.json",
"revocation_endpoint": "https://tahoe.workonward.com/api/partner/v1/oauth/revoke",
"end_session_endpoint": "https://tahoe.workonward.com/api/partner/v1/oauth/logout",
"response_types_supported": ["code"],
"response_modes_supported": ["query"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"subject_types_supported": ["pairwise"],
"id_token_signing_alg_values_supported": ["RS256"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": [
"client_secret_post", "client_secret_basic", "none"
]
}The sign-in flow
Send the user to Tahoe
Make a fresh PKCE code_verifier for each sign-in and keep it in the user’s session. Send its SHA-256 hash as the code_challenge.
import base64
import hashlib
import secrets
code_verifier = secrets.token_urlsafe(64)
code_challenge = (
base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest())
.rstrip(b"=")
.decode()
)Then redirect the browser to the authorization endpoint:
https://tahoe.workonward.com/api/partner/v1/oauth/authorize
?client_id=thc_7f3a9c21d84e4b6fa0c5e19b2d7a6e31
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fauth%2Ftahoe%2Fcallback
&response_type=code
&scope=openid%20profile%20email%20offline_access%20jobs%3Aread
&state=<random, tied to the user's session>
&nonce=<random, saved for the id_token check>
&code_challenge=<base64url(sha256(code_verifier))>
&code_challenge_method=S256| Parameter | Required | Notes |
|---|---|---|
client_id | Yes | The ID Tahoe gave you. |
redirect_uri | Yes | Must match a registered URI exactly. |
response_type | Yes | Only code. |
scope | Yes | Must include openid. Add profile, email, offline_access and the API scopes you need, separated by spaces. |
state | In practice, yes | Protects against cross-site request forgery. Tie it to the session and check it on return. |
nonce | In practice, yes | Save it and check that it comes back inside the id_token. |
code_challenge | Yes | The base64url SHA-256 hash of your code_verifier. |
code_challenge_method | Yes | S256. plain is refused. |
The user approves
Tahoe shows a consent screen titled “[Your app] wants to access your Tahoe account”. It shows which account is signed in, the address the user will be returned to, and what your app will be able to read. If you asked for contact details or resumes, it warns that the app wants personal data about candidates. If you asked for offline_access, it says the app will stay connected. The user picks Allow [your app] or Cancel.
An app that a customer registered is shown as unverified, with the name of the workspace that registered it, because Tahoe has not reviewed it. Scopes the user may not grant are shown unticked, with the reason. See Who can approve what.
On approval, the browser comes back to your redirect URI with a code:
GET https://app.example.com/auth/tahoe/callback?code=<code>&state=<your state>Check state against the session before you do anything with the code. The code works once and expires after five minutes.
Exchange the code for tokens
curl -s -X POST https://tahoe.workonward.com/api/partner/v1/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=authorization_code \
-d code=<code> \
-d redirect_uri=https://app.example.com/auth/tahoe/callback \
-d client_id=thc_7f3a9c21d84e4b6fa0c5e19b2d7a6e31 \
-d client_secret=<your client secret> \
-d code_verifier=<the code_verifier you made>{
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid profile email offline_access jobs:read",
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjNnY3M1ZGdoM01zcVJDeW5KZWNlOHNYaCJ9...",
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjNnY3M1ZGdoM01zcVJDeW5KZWNlOHNYaCJ9...",
"refresh_token": "..."
}A server-side app authenticates with client_secret_post (as above) or client_secret_basic. An app without a secret sends only its client_id and must not send a secret at all.
| Token | Lifetime | Use |
|---|---|---|
access_token | 1 hour | Call the Tahoe API with it, like an API key. |
id_token | 1 hour | A signed (RS256) JWT with claims about the user. Verify it; do not send it to the API. |
refresh_token | 30 days unused, 90 days at most | Only if you asked for offline_access. A new one comes back on every refresh. |
Verify the id_token
Run the standard checks: the signature against jwks_uri, iss equal to the issuer, aud equal to your client_id, exp in the future, and nonce equal to the one you sent.
The sub claim is different in every app
Subject identifiers are pairwise: the same Tahoe user has a different sub in your app than in any other app. Two apps cannot match up a user by comparing identifiers.
For you, sub is a good primary key for your own user record, and it stays the same for your app. It means nothing anywhere else: do not log it as a Tahoe-wide user ID, and do not expect Tahoe support to recognise one.
Calling the API as the user
Send the access token exactly like an API key, to the same base URL:
curl -s https://tahoe.workonward.com/api/partner/v1/jobs \
-H "Authorization: Bearer <access_token>"- What it can read is the overlap of the API scopes your app asked for and what the user can see in Tahoe. A token never reaches further than the person behind it.
- Which workspace: the one the user was signed in to when they approved your app. The token reads only that workspace, so you do not need to name it. If the user is removed from that workspace, the next refresh fails. When the token carries a write scope or a sensitive scope, it acts only in workspaces where the user is still an owner or admin. A user who was demoted keeps the app’s read access and loses the rest on their next request.
- Writing is off unless it is switched on. Every write with a token from this flow returns
403 write_requires_api_keyuntil writes for connected apps are switched on. When they are, the write is attributed to the person who signed in, never to the app. See Connect an app. - It is recorded as the user. Every personal-data read made with the token is recorded against that user as well as your app.
Refreshing tokens
curl -s -X POST https://tahoe.workonward.com/api/partner/v1/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=refresh_token \
-d refresh_token=<the current refresh token> \
-d client_id=thc_7f3a9c21d84e4b6fa0c5e19b2d7a6e31 \
-d client_secret=<your client secret>Every refresh returns a new refresh token and ends the one you sent. Save the new one before you use it. You may send a scope parameter to ask for fewer scopes than the original grant, but never more.
A refresh token expires after 30 days without use. However often it is used, the chain of refresh tokens from one sign-in ends 90 days after that sign-in, and the user signs in again.
A refresh token that was already exchanged cannot be exchanged again. If the response to a refresh is lost, for example by a timeout, the new token is lost with it. A short grace period, when Tahoe has switched one on, answers a retry made right after the first use with the same new token instead of ending the session. It is off unless Tahoe tells you otherwise, so build as if it did not exist.
When access ends
Your tokens stop working, and the next refresh fails, when:
- the user disconnects your app in Tahoe, under Settings → Connected apps in the section Apps you signed in to with Tahoe;
- the user resets their Tahoe password;
- the user is removed from the workspace the sign-in was for, or their account is closed;
- a refresh token is reused, as described above.
Treat a failed refresh (invalid_grant) as “send this user through sign-in again”, not as something to retry.
Endpoint reference
GET/.well-known/openid-configurationpublic
The discovery document. Public, and cacheable for 5 minutes.
GET/.well-known/jwks.jsonpublic
The public signing keys, also cacheable for 5 minutes. Two entries appear during a key rotation: match on kid.
GET/oauth/authorizebrowser redirect
Starts a sign-in and takes the browser to the Tahoe consent screen.
POST/oauth/tokenclient authentication
Exchanges a code for tokens, or refreshes them. Accepts client_secret_post, client_secret_basic, or none for apps without a secret. Only the authorization_code and refresh_token grants exist.
GET/oauth/userinfoaccess token
Claims about the user behind an access token. Each claim depends on the scopes granted: without email there is no email in the response, and that is not an error. Claims with no value are left out. sub is always present. If the user has disconnected your app, this returns 401 even while the access token has not yet expired.
curl -s https://tahoe.workonward.com/api/partner/v1/oauth/userinfo \
-H "Authorization: Bearer <access_token>"{
"sub": "9f4c1e7a2b...",
"name": "Jordan Rivera",
"given_name": "Jordan",
"family_name": "Rivera",
"updated_at": 1789245645,
"email": "[email protected]",
"email_verified": true
}POST/oauth/revokeclient authentication
Revokes a refresh token. Call it when the user signs out of your app or disconnects Tahoe, so the grant does not linger. It answers 200 whether or not the token existed.
curl -s -X POST https://tahoe.workonward.com/api/partner/v1/oauth/revoke \
-H "Content-Type: application/x-www-form-urlencoded" \
-d token=<refresh token> \
-d client_id=thc_7f3a9c21d84e4b6fa0c5e19b2d7a6e31 \
-d client_secret=<your client secret>GET/oauth/logoutbrowser redirect
Signs the user out of Tahoe in that browser. Pass your client_id and a registered post_logout_redirect_uri (matched exactly) to bring the user back to your app afterwards, with an optional state.
https://tahoe.workonward.com/api/partner/v1/oauth/logout
?client_id=thc_7f3a9c21d84e4b6fa0c5e19b2d7a6e31
&post_logout_redirect_uri=https%3A%2F%2Fapp.example.com%2Fsigned-out
&state=<optional>Checklist
- Configure from the discovery document; do not type endpoints in by hand.
- PKCE with
S256, and a freshcode_verifierfor every sign-in. statetied to the session and checked on return.noncesaved and checked inside theid_token.- JWKS cached, key chosen by
kid, fetched again on an unknown one. - Refreshes one at a time per user, with the new token saved before use.
- Refresh tokens revoked on sign-out.
- A failed refresh sends the user to sign in again, without retrying.