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.

Discovery
curl -s https://tahoe.workonward.com/api/partner/v1/.well-known/openid-configuration | jq
Response (abridged)
{
  "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

Authorization request
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
ParameterRequiredNotes
client_idYesIssued when your app was registered.
redirect_uriYesMust match a registered URI exactly.
response_typeYesOnly code.
scopeYesMust include openid. Add profile, email, offline_access, and any partner data scopes you need.
stateYes, in practiceYour CSRF defence. Bind it to the session and check it on return.
nonceYes, in practiceStore it and verify it appears in the id_token.
code_challengeYesbase64url of the SHA-256 of your verifier.
code_challenge_methodYesS256. Nothing else is accepted.
promptNoStandard OIDC prompt handling.

2. The user approves, and comes back with a code

Callback
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

Token request
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>
Response
{
  "access_token": "...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "...",
  "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjNnY3M1ZGdoM01zcVJDeW5KZWNlOHNYaCJ9...",
  "scope": "openid profile email offline_access jobs:read"
}
TokenLifetimePurpose
access_token1 hourCall the Partner API with it, exactly like an API key.
id_token1 hourAn RS256 JWT of claims about the user. Verify it; do not send it to the API.
refresh_token30 daysOnly 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.

Calling the API with a delegated token
curl -s https://tahoe.workonward.com/api/partner/v1/me \
  -H "Authorization: Bearer <access_token>"

Refresh tokens rotate, and reuse is fatal

Refresh
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.

Response
{
  "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.
  • state bound to the session, checked on return.
  • nonce stored, checked in the id_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.