Vetrix Docs

OAuth2 Developer Guide — building third-party apps for Vetrix

Vetrix is an OAuth2 / RFC 6749 authorization-code-flow authorization server. This guide walks through registering an OAuth2 app, completing the consent flow, and calling the Vetrix API on behalf of a user.

If you only need to authenticate users — without API calls — see the Vetrix-as-OAuth2-client guide (Part A) for "Sign in with Google / GitHub / GitLab / Microsoft" setup. This document covers Part B: third parties calling Vetrix.

TL;DR

  1. Register an app at /settings/applications.
  2. Send the user to /api/v1/oauth2/authorize with PKCE.
  3. Exchange the code at /api/v1/oauth2/token.
  4. Call the API with Authorization: Bearer vetrix_oat_….
  5. Rotate via /api/v1/oauth2/token grant_type=refresh_token.
  6. Revoke via /api/v1/oauth2/revoke.

1. Endpoints at a glance

Method Path Notes
GET /api/v1/oauth2/authorize Consent screen / auto-approve decision
POST /api/v1/oauth2/authorize Submit consent decision (Authorize / Cancel)
POST /api/v1/oauth2/token grant_type=authorization_code or refresh_token
POST /api/v1/oauth2/revoke RFC 7009 — public + confidential
POST /api/v1/oauth2/introspect RFC 7662 — confidential clients only
GET /api/v1/user/oauth2/apps Caller's registered apps
POST /api/v1/user/oauth2/apps Register; raw client_secret returned once
POST /api/v1/user/oauth2/apps/{id}/rotate-secret Rotate; new secret returned once
GET /api/v1/user/oauth2/authorizations Caller's live grants
DELETE /api/v1/user/oauth2/authorizations/{grant_id} Revoke grant (cascades to tokens)
GET /api/v1/admin/oauth2/apps Admin: instance-wide app list
POST /api/v1/admin/oauth2/apps/{id}/suspend / unsuspend Admin kill switch (immediate)
GET /api/v1/admin/oauth2/audit Admin: oauth2.* audit trail

2. Picking a client type

Type When Secret? PKCE?
public Mobile / desktop / single-page / CLI with loopback redirect ❌ No ✅ Required
confidential Server-side web apps that can keep a long-lived secret safe ✅ Yes ✅ Required (defence in depth)

Public clients must not present a client_secret and must provide a PKCE code_challenge. Confidential clients must present the secret over HTTP Basic or POST body, and may additionally provide PKCE.

admin:* scopes (admin:repo, read:audit) cannot be issued to a public client — register your tooling as confidential if you need them.


3. Scope catalog

Scope Allows
read:user Read the authenticated user's profile
read:repo Browse repos / branches / files / commits
write:repo Push commits, create branches
admin:repo Repo settings, collaborators, branch deletion (admin-only)
read:issue Read issues, comments, labels
write:issue Create / update issues + comments + labels
read:pipeline View pipelines + jobs + logs
write:pipeline Trigger / retry / cancel pipelines
read:package Pull packages from the registry
write:package Push packages
read:audit Read instance-wide audit log (admin-only, confidential client)

Scope enforcement is additive: a token must hold both the scope and the underlying ACL permission. Granting write:repo does not grant access to repos the user cannot already see.


4. Registering an app

  1. Sign in to Vetrix.
  2. Visit /settings/applications.
  3. Click "Register new application".
  4. Fill in:
    • Name (2–60 chars, shown on the consent screen).
    • Description (≤1000 chars).
    • Homepage URL (optional, http(s)://).
    • Logo URL (optional, http(s)://).
    • Redirect URIs — one per line. Allowed schemes:
      • https://…
      • http://localhost[:port]/…, http://127.0.0.1[:port]/…, http://[::1][:port]/… (RFC 8252 §7.3 loopback carve-out — port differences are accepted by Vetrix for these hosts).
      • Rejected: arbitrary http://…, javascript:, file:, data:.
    • Scopes — least-privilege; you can request fewer at /authorize time but never more than the registered set without re-consent.
    • Client typepublic or confidential.
  5. On save, you receive a client_id. For confidential clients, the raw client_secret appears in the response exactly once. Store it now; subsequent reads return <set>.

Per-user cap: oauth2.max_apps_per_user (default 20). The 21st create returns 409 Conflict with {"error":"too_many_apps"}.


5. Authorization code flow (curl walkthrough)

The walkthrough uses https://api.gitvetrix.com. Substitute the local dev host (https://api.gitvetrix.test) when working against the .test stack.

# Compute PKCE locally.
CV=$(openssl rand -base64 32 | tr -d '=' | tr '/+' '_-')   # code_verifier
CC=$(printf "%s" "$CV" | openssl dgst -sha256 -binary | openssl base64 | tr -d '=' | tr '/+' '_-')

CID="vetrix_…"          # from /settings/applications
CS="…"                 # only for confidential clients
REDIRECT="http://127.0.0.1:8123/cb"

# Step 1 — send the user to consent.
xdg-open "https://api.gitvetrix.com/api/v1/oauth2/authorize\
?response_type=code\
&client_id=$CID\
&redirect_uri=$REDIRECT\
&scope=read:repo%20read:user\
&state=$(openssl rand -hex 8)\
&code_challenge=$CC\
&code_challenge_method=S256"

# Step 2 — Vetrix redirects to $REDIRECT?code=…&state=… after consent.
# Capture $CODE, then exchange:
curl -u "$CID:$CS" https://api.gitvetrix.com/api/v1/oauth2/token \
  -d grant_type=authorization_code \
  -d code="$CODE" \
  -d redirect_uri="$REDIRECT" \
  -d code_verifier="$CV"
# Response:
# {
#   "access_token":  "vetrix_oat_…",
#   "refresh_token": "vetrix_ort_…",
#   "token_type":    "Bearer",
#   "expires_in":    3600,
#   "scope":         "read:repo read:user"
# }

# Step 3 — call the Vetrix API.
curl -H "Authorization: Bearer $ACCESS" https://api.gitvetrix.com/api/v1/auth/me

# Step 4 — refresh when the access token expires.
curl -u "$CID:$CS" https://api.gitvetrix.com/api/v1/oauth2/token \
  -d grant_type=refresh_token \
  -d refresh_token="$REFRESH"

# Step 5 — revoke when the user signs out / removes your app.
curl -u "$CID:$CS" https://api.gitvetrix.com/api/v1/oauth2/revoke \
  -d token="$ACCESS"

Public clients omit the -u "$CID:$CS" HTTP Basic auth and pass -d client_id="$CID" in the POST body instead.


6. PKCE S256 math

// Go
verifier := base64.RawURLEncoding.EncodeToString(randomBytes(32))
sum := sha256.Sum256([]byte(verifier))
challenge := base64.RawURLEncoding.EncodeToString(sum[:])
# Python
import base64, hashlib, secrets
verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).rstrip(b"=").decode()
challenge = base64.urlsafe_b64encode(
    hashlib.sha256(verifier.encode()).digest()
).rstrip(b"=").decode()
// Browser / Node 18+
const buf = crypto.getRandomValues(new Uint8Array(32));
const verifier = btoa(String.fromCharCode(...buf))
  .replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
const challenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
  .replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');

code_challenge_method=S256 is the only accepted method. Vetrix rejects the legacy plain PKCE method.


7. Refresh-rotation semantics

Every successful refresh exchange:

  1. Returns a new access token and a new refresh token.
  2. Marks the old refresh token's used_at. The old refresh is no longer exchangeable, but its access-token sibling continues to work until its own expires_in lapses.
  3. Tracks a family_id shared by every refresh in the rotation chain.

If a token thief replays an already-used refresh:

  • Vetrix revokes the entire family of refresh tokens.
  • Vetrix revokes every access token tied to the grant.
  • The replay returns 400 Bad Request with {"error":"invalid_grant"}.
  • An oauth2.refresh_reuse_detected audit entry is emitted.

The legitimate user can re-authorize the app at /settings/applications to restore access. Always store the new refresh token after every successful exchange.


8. /revoke and /introspect

POST /oauth2/revoke accepts both access (vetrix_oat_…) and refresh (vetrix_ort_…) tokens, optionally with a token_type_hint. Per RFC 7009 §2.2 the response is always 200 OK — Vetrix never reveals whether the token existed before the request, so you cannot probe the token space by revoking randomly-generated values.

POST /oauth2/introspect is restricted to confidential clients (Vetrix enforces RFC 7662's protected-resource model strictly). Public clients receive 401 invalid_client. Active tokens return:

{
  "active":     true,
  "scope":      "read:repo read:user",
  "user_id":    "…",
  "app_id":     "…",
  "exp":        1745300800,
  "token_type": "Bearer"
}

Inactive / unknown / expired / revoked tokens return:

{ "active": false }

…and nothing else.


9. Error catalogue

Code When it occurs How to recover
invalid_request Missing required parameter, double-presented client_secret, bad form Inspect error_description and fix the request shape
invalid_client Wrong / missing secret, suspended app, public client at /introspect Re-check credentials; check whether your app was suspended at /admin/oauth2/apps
invalid_grant Code expired / re-used, redirect_uri mismatch, refresh re-used or expired Restart the consent flow
unauthorized_client Client not allowed to use the requested grant type Use authorization_code or refresh_token
unsupported_grant_type Unknown grant_type Use authorization_code or refresh_token
invalid_scope Unknown scope, or admin-only scope requested by a public client Check the scope catalog (§3); use a confidential client for admin:* scopes
access_denied User pressed Cancel Show the user a "permission required" page; offer to retry
insufficient_scope Token lacks the scope required by the called endpoint Re-request consent with the additional scope; the user will see only the delta
service_unavailable oauth2.server.enabled=false (operator kill switch) Wait; Vetrix user JWTs and PATs continue to work
rate_limited 60/min/IP at /authorize, 30/min/client_id at /token, etc. Retry after Retry-After seconds

10. Token hygiene checklist

  • ✅ Treat vetrix_oat_… and vetrix_ort_… like passwords — never log them, never bake them into client-side bundles.
  • ✅ Store refresh tokens in a server-side secret store; the bearer is a password-equivalent.
  • ✅ Rotate client_secret periodically via /settings/applications → Rotate Secret. Rotation does not invalidate live access tokens; you must also revoke them or wait out their TTL.
  • ✅ On compromise, revoke the abused token immediately (/oauth2/revoke) and rotate client_secret.
  • ✅ Watch for oauth2.refresh_reuse_detected in the admin audit log — one of these means a token thief has replayed a refresh you already used.
  • ❌ Don't store raw secrets in environment variables on shared CI runners without using the secret-mask feature.
  • ❌ Don't share a single client_id across multiple deployments — register one app per environment so suspension can target a single tenant.

11. Operator runbook (admins)

Task Endpoint / UI
Audit every registered app GET /api/v1/admin/oauth2/apps or /admin/oauth2/apps
Suspend a misbehaving app (kill switch) POST /api/v1/admin/oauth2/apps/{id}/suspend
Inspect an app's grants GET /api/v1/admin/oauth2/apps/{id}/grants
Pull the OAuth-only audit trail GET /api/v1/admin/oauth2/audit
Disable the entire OAuth2 server Set oauth2.server.enabled=false in admin settings
Tune access-token TTL oauth2.access_token_ttl (10m–24h, default 1h)
Tune refresh-token TTL oauth2.refresh_token_ttl (24h–8760h, default 720h)
Refuse admin scopes for public clients oauth2.require_confidential_for_admin_scopes (default true)
Cap apps per developer oauth2.max_apps_per_user (default 20)

Suspension takes effect on the next request to the OAuth2 access-token middleware — there is no need to revoke each token individually. Existing sessions for the suspended app start failing with 401 Unauthorized within milliseconds of flipping the flag.


12. References

  • RFC 6749 — The OAuth 2.0 Authorization Framework
  • RFC 6750 — Bearer Token Usage
  • RFC 7009 — Token Revocation
  • RFC 7636 — PKCE
  • RFC 7662 — Token Introspection
  • RFC 8252 — OAuth 2.0 for Native Apps (loopback redirect carve-out)
  • RFC 9700 — OAuth 2.0 Security Best Current Practice (refresh-token rotation requirements that informed Vetrix's reuse-detection algorithm)
  • Internal design: plans/oauth.md (full Part B specification)

See also