The OAuth2 surface is split into two hosts: protocol endpoints at the issuer (login.busha.io) and the resource API (api.busha.io). OAuth app management — creating apps, revealing secrets, editing redirect URIs — is dashboard-only at app.busha.io.
| Method | Endpoint | Purpose |
|---|
GET | /oauth2/auth | Start an authorization request (browser redirect). |
POST | /oauth2/token | Exchange a code for tokens, or refresh an existing session. |
POST | /oauth2/revoke | Revoke an access or refresh token. |
GET | /.well-known/openid-configuration | OIDC discovery document. |
GET | /.well-known/jwks.json | Public keys for JWT signature verification. |
GET /oauth2/auth
Browser redirect — this is where you send the user’s browser to start authorization. Never call this server-to-server.
Required query parameters:
| Parameter | Value |
|---|
response_type | code |
client_id | Your app’s client_id |
redirect_uri | Byte-identical to a registered URI |
scope | Space-separated scope list |
state | CSRF guard — at least 16 random bytes, bound to the user’s session |
code_challenge | base64url(sha256(code_verifier)) |
code_challenge_method | S256 — plain is not accepted; missing challenges are rejected |
Success: Redirects to your callback with ?code=...&state=...
User cancelled: Redirects with ?error=access_denied&state=...
POST /oauth2/token
Server-to-server. Authenticate with HTTP Basic using client_id:client_secret. Never call this from the browser.
Authorization code → tokens
curl -X POST https://login.busha.io/oauth2/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=$AUTH_CODE" \
--data-urlencode "redirect_uri=https://yourapp.example.com/oauth2/callback" \
--data-urlencode "code_verifier=$CODE_VERIFIER"
Refresh token → new pair
curl -X POST https://login.busha.io/oauth2/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=refresh_token" \
--data-urlencode "refresh_token=$REFRESH_TOKEN"
Important details:
id_token is returned only when openid is in the granted scopes.
refresh_token is returned only when offline_access is granted.
redirect_uri must exactly match a registered URI.
- Authorization codes are single-use and expire in 10 minutes.
- Token exchange is not idempotent — if your callback handler runs twice, the second call returns
invalid_grant.
Accepted grant types: authorization_code (with PKCE-S256) and refresh_token only. Implicit, resource-owner password, and client_credentials are not supported.
POST /oauth2/revoke
Revoke an access or refresh token. Call this when a user disconnects your integration.
curl -X POST https://login.busha.io/oauth2/revoke \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "token=$REFRESH_TOKEN" \
--data-urlencode "token_type_hint=refresh_token"
Returns 200 with an empty body regardless of whether the token was valid — this is intentional to avoid leaking token validity information.
Revoking a refresh token kills future refreshes immediately but does not invalidate a currently-issued access token. The access token continues to validate until exp.
GET /.well-known/openid-configuration
The OIDC discovery document. Contains the canonical issuer string and the URLs of all protocol endpoints.
curl https://login.busha.io/.well-known/openid-configuration
Read the issuer value from this document at startup and pin your JWT validation against it. Do not hardcode the issuer string.
GET /.well-known/jwks.json
The public key set used to verify JWT signatures.
curl https://login.busha.io/.well-known/jwks.json
Cache the response for ~5 minutes. Refetch when you encounter a kid in a JWT header that is not present in your cache.