Sign in with Tahoe
Sign in with Tahoe
A standard OIDC provider. If your stack already speaks OpenID Connect, point it at the discovery document and you are most of the way there.
Start from discovery
Everything below is described by the discovery document, so configure your OIDC client from it rather than hard-coding endpoints.
curl -s https://tahoe.workonward.com/api/partner/v1/.well-known/openid-configuration | jq{
"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"],
"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"
],
"request_parameter_supported": false,
"request_uri_parameter_supported": false,
"claims_parameter_supported": false,
"service_documentation": "https://tahoe.workonward.com/developers"
}Registering your app
There is no self-serve registration endpoint. Ask your Tahoe contact for a client_id and secret, giving them your exact redirect URIs.
Self-serve dynamic registration would let anyone point an OIDC client at any redirect URI, which is a request-forgery primitive dressed as a convenience. Registration is a deliberate act by an operator.
The flow
Authorization code with PKCE. S256 only — plain is not advertised and is refused, so there is no downgrade path.
1. Send the user to authorize
GET https://tahoe.workonward.com/api/partner/v1/oauth/authorize
?client_id=tahoe_client_9tRc4mQx
&redirect_uri=https%3A%2F%2Fworkonward.com%2Fauth%2Ftahoe%2Fcallback
&response_type=code
&scope=openid%20profile%20email%20offline_access
&state=<random, bound to the user's session>
&nonce=<random, stored for the id_token check>
&code_challenge=<base64url(sha256(verifier))>
&code_challenge_method=S256| Parameter | Required | Notes |
|---|---|---|
client_id | Yes | Issued when your app was registered. |
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 any partner data scopes you need. |
state | Yes, in practice | Your CSRF defence. Bind it to the session and check it on return. |
nonce | Yes, in practice | Store it and verify it appears in the id_token. |
code_challenge | Yes | base64url of the SHA-256 of your verifier. |
code_challenge_method | Yes | S256. Nothing else is accepted. |
prompt | No | Standard OIDC prompt handling. |
2. The user approves, and comes back with a code
GET https://workonward.com/auth/tahoe/callback?code=<code>&state=<your state>Check state against the session before you do anything with the code. The code is single-use and expires in five minutes.
3. Exchange it 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://workonward.com/auth/tahoe/callback \
-d client_id=tahoe_client_9tRc4mQx \
-d client_secret=<secret> \
-d code_verifier=<the original verifier>{
"access_token": "...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "...",
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjNnY3M1ZGdoM01zcVJDeW5KZWNlOHNYaCJ9...",
"scope": "openid profile email offline_access jobs:read"
}| Token | Lifetime | Purpose |
|---|---|---|
access_token | 1 hour | Call the Partner API with it, exactly like an API key. |
id_token | 1 hour | An RS256 JWT of claims about the user. Verify it; do not send it to the API. |
refresh_token | 30 days | Only issued if you asked for offline_access. Rotates on every use. |
4. Verify the id_token
Standard checks: signature against jwks_uri, iss equals the issuer above, aud equals your client_id, exp is in the future, and nonce matches what you sent.
sub is pairwise, and what that costs you
Scopes on a delegated token
An access token from this flow can carry partner data scopes as well as the OIDC ones. Its reach is the intersection of what your app asked for and what that person can actually see — a delegated token never grants more than the human behind it has.
Audit rows for these requests record the acting user. A disclosure made under a delegated credential was authorised by a named person, and an audit line saying only “app X read this” would lose the fact that mattered most.
curl -s https://tahoe.workonward.com/api/partner/v1/me \
-H "Authorization: Bearer <access_token>"Refresh tokens rotate, and reuse is fatal
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 one> \
-d client_id=tahoe_client_9tRc4mQx \
-d client_secret=<secret>Every refresh returns a new refresh token and invalidates the one you sent. Store the new one atomically.
Endpoint reference
GET/.well-known/openid-configurationno authentication
Public and cacheable for 5 minutes. Configure from this.
GET/.well-known/jwks.jsonno authentication
Public keys. Two entries during a rotation — match on kid.
GET/oauth/authorizeno authentication
Starts a sign-in and hands the browser to the Tahoe consent screen.
POST/oauth/tokenno authentication
Exchanges a code, or refreshes. Supports client_secret_post, client_secret_basic and none for public clients using PKCE.
GET/oauth/userinfono authentication
Claims for the bearer of an access token. Gated field by field: with no email scope there is simply no email in the response, and that absence is not an error. sub is always present, because without it the response identifies nobody.
{
"sub": "9f4c1e...",
"name": "Priya Raman",
"given_name": "Priya",
"family_name": "Raman",
"picture": null,
"email": "[email protected]",
"email_verified": true,
"updated_at": 1789245645
}POST/oauth/revokeno authentication
Revoke a refresh token — on sign-out, or when a user disconnects your app. Do this rather than dropping the token silently, so the grant does not linger for its full 30 days.
GET/oauth/logoutno authentication
The end_session_endpoint. Ends the partner session at Tahoe.
Users can disconnect you
A Tahoe user can see the apps they have connected and revoke one at any time. When that happens your tokens stop working and refresh fails — treat a failed refresh as “this user has disconnected” and send them through the flow again rather than retrying.
Implementation checklist
- Configure from the discovery document; do not hard-code endpoints.
- PKCE with
S256. Fresh verifier per attempt. statebound to the session, checked on return.noncestored, checked in theid_token.- JWKS cached, key selected by
kid, re-fetched on an unknown one. - Refresh serialised per user; new token persisted before use.
- Revoke on sign-out.
- A failed refresh means re-authenticate, not retry.
