Reference / Sentroy Auth

Sentroy Auth

Drop a "Sign in with Sentroy" button into your site. Standard OAuth 2.0 authorization-code flow + OIDC-compliant id_token — works with any spec-aware library, no custom integration code.

Overview#

Sentroy Auth is an OAuth 2.0 / OpenID Connect provider hosted at auth.sentroy.com. Users authenticate with their existing Sentroy account; your app gets back a verified profile (name, email) and a stateless id_token JWT.

Flow

1.User clicks "Sign in with Sentroy" on your site → redirected to https://auth.sentroy.com/oauth/authorize?...
2. If signed into Sentroy, consent screen appears immediately. If not, user logs in first (cross-subdomain cookie — single round-trip).
3. User clicks "Allow" → 302 back to your redirect_uri?code=...
4. Your backend exchanges the code at POST /oauth/token for an access_token + id_token.
5. Optional: call GET /oauth/userinfowith the access token for the user's profile.

Register an OAuth client#

Each site that signs users in needs a client_id + client_secret. One client per app, manage from your dashboard.

Open your Sentroy dashboard → company → OAuth clients → New OAuth client. You'll be asked for:

  • Name — shown to users on the consent screen.
  • Redirect URIs (one per line) — the URLs Sentroy is allowed to send the auth code back to. Add both your dev (http://localhost:3000/...) and prod (https://app.example.com/...) URLs.
  • Homepage URL(optional) — shown on the consent screen as "learn more" link.

On submit you get a one-time view of client_secret. Copy it into your app's deploy env immediately — it cannot be shown again. If you lose it, rotate it from the dashboard.

Quickstart (NextAuth)#

Most OAuth libraries auto-configure from the discovery document — one URL covers it.

// auth.ts (NextAuth v5)
import NextAuth from "next-auth"

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    {
      id: "sentroy",
      name: "Sentroy",
      type: "oidc",
      issuer: "https://auth.sentroy.com",
      clientId: process.env.SENTROY_CLIENT_ID!,
      clientSecret: process.env.SENTROY_CLIENT_SECRET!,
      authorization: { params: { scope: "openid profile email" } },
    },
  ],
})

That's it. NextAuth fetches https://auth.sentroy.com/.well-known/openid-configuration and learns every endpoint URL automatically. Sign-in button:

<form action={async () => {
  "use server"
  await signIn("sentroy", { redirectTo: "/dashboard" })
}}>
  <button type="submit">Sign in with Sentroy</button>
</form>

REST endpoints#

If you're rolling your own OAuth client (or your library doesn't read discovery), here are the raw endpoints.

GET /oauth/authorize

Required query: response_type=code, client_id, redirect_uri, scope (space-separated; must include openid).
Optional: state (CSRF token, echoed back), nonce (embedded in id_token).

https://auth.sentroy.com/oauth/authorize?
  response_type=code&
  client_id=client_abc123&
  redirect_uri=https%3A%2F%2Fapp.example.com%2Fapi%2Fauth%2Fcallback&
  scope=openid%20profile%20email&
  state=<random>&
  nonce=<random>

POST /oauth/token

Exchange the authorization code for tokens. Client authentication via Basic header (preferred) or form fields.

curl -X POST https://auth.sentroy.com/oauth/token \
  -u client_abc123:secret_xxx \
  -d grant_type=authorization_code \
  -d code=oac_xxxxxxxxxxxx \
  -d redirect_uri=https://app.example.com/api/auth/callback
{
  "access_token": "oat_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid profile email",
  "id_token": "eyJhbGciOiJIUzI1NiIs..."
}

GET /oauth/userinfo

Fetch the user's profile (claims depend on granted scopes).

curl -H "Authorization: Bearer oat_..." \
  https://auth.sentroy.com/oauth/userinfo
{
  "sub": "<user-id>",
  "name": "Aras Yilmaz",
  "preferred_username": "Aras Yilmaz",
  "email": "aras@example.com",
  "email_verified": true
}

GET /.well-known/openid-configuration

Discovery document — OAuth/OIDC libraries fetch this once at init to learn all endpoint URLs and supported parameters.

Scopes#

Pick only what you need — users see them on the consent screen.

  • openid — required by the spec. Returns sub (Sentroy user id) only.
  • profile — adds name, preferred_username, picture.
  • email — adds email + email_verified.
  • offline_access — issues a refresh_tokenalongside the access / id token, so the user stays signed in past the 1-hour TTL. Must be on the client's allow-list before it can be requested.

Each OAuth client has an allow-list of scopes (set on registration). Authorize requests for scopes outside the allow-list are rejected.

PKCE (RFC 7636)#

Proof Key for Code Exchange — recommended for all clients, required for SPA / mobile.

Sentroy Auth supports PKCE with the S256 method. Use it whenever you can — it adds zero friction for users and closes the "intercepted authorization code" attack window. Most libraries (NextAuth, oauth4webapi, Authlib, etc.) enable PKCE automatically when they see it in the discovery document.

// 1. Generate a verifier + challenge before redirecting
import { createHash, randomBytes } from "crypto"
const verifier = randomBytes(32).toString("base64url")
const challenge = createHash("sha256").update(verifier).digest("base64url")
// 2. Store verifier in your session, send challenge to authorize:
//    .../oauth/authorize?...&code_challenge=<challenge>&code_challenge_method=S256
// 3. On callback, send verifier to /oauth/token:
//    grant_type=authorization_code&code=...&code_verifier=<verifier>

Refresh tokens#

Keep users signed in past the 1-hour access token TTL — opt in via the offline_access scope.

Add offline_accessto the requested scopes (and to the client's allow-list in the dashboard). On consent the token endpoint returns a refresh_token alongside the access/id tokens. When the access token expires, exchange the refresh token for a new pair:

curl -X POST https://auth.sentroy.com/oauth/token \
  -u client_abc123:secret_xxx \
  -d grant_type=refresh_token \
  -d refresh_token=ort_xxxxxxxxxxxx

Signing keys (RS256 + JWKS)#

Public-key id_token signing — RPs verify locally without round-tripping to userinfo.

Set OAUTH_RSA_PRIVATE_KEY on the auth deploy to a PEM-encoded RSA private key. Sentroy switches id_token signing to RS256, publishes the public key at /.well-known/jwks.json, and advertises the JWKS URI in the discovery document. RP libraries auto-detect the new mode on next discovery refresh.

# Generate a fresh RSA key (one-time)
node -e "console.log(require('crypto').generateKeyPairSync('rsa',{modulusLength:2048}).privateKey.export({type:'pkcs8',format:'pem'}))" \
  > oauth_rsa_private.pem
# Then set the contents as OAUTH_RSA_PRIVATE_KEY on the auth Coolify env

The kid is derived from the key (RFC 7638 JWK SHA-256 thumbprint) — no extra env required. WithoutOAUTH_RSA_PRIVATE_KEY, Sentroy falls back to HS256 (OAUTH_ID_TOKEN_SECRET); JWKS stays empty and discovery omits jwks_uri.

Zero-downtime rotation

Sentroy supports two simultaneous keys for graceful rotation — OAUTH_RSA_PRIVATE_KEY for signing, OAUTH_RSA_PRIVATE_KEY_PREVIOUS kept in JWKS for verification. RPs see both public keys, look up the right one by kid in each id_token header, and verify both old and new tokens during the grace window.

# Step 1: shift the current key to the PREVIOUS slot
#   On the auth deploy's Coolify env, copy the value of
#   OAUTH_RSA_PRIVATE_KEY into OAUTH_RSA_PRIVATE_KEY_PREVIOUS.

# Step 2: generate a fresh key, set as PRIMARY
node -e "console.log(require('crypto').generateKeyPairSync('rsa',{modulusLength:2048}).privateKey.export({type:'pkcs8',format:'pem'}))" \
  > new_oauth_rsa_private.pem
#   Set OAUTH_RSA_PRIVATE_KEY to the new key's contents.

# Step 3: deploy
#   New id_tokens are signed with the new key (kid changes); existing
#   tokens stay verifiable via the previous key in JWKS.

# Step 4: wait for the access_token TTL to elapse (60 min default + margin),
#   then remove OAUTH_RSA_PRIVATE_KEY_PREVIOUS and redeploy.

Token revocation (RFC 7009)#

POST /oauth/revoke — invalidate an access or refresh token from the RP side.

curl -X POST https://auth.sentroy.com/oauth/revoke \
  -u client_abc123:secret_xxx \
  -d token=oat_xxxxxxxxxxxx
# → 200 OK (always, even for unknown tokens — spec)

Optional token_type_hint=access_token or token_type_hint=refresh_token shortcuts the lookup. Sentroy returns 200 unconditionally per RFC §2.2 — never reveals whether the token existed.

Token introspection (RFC 7662)#

POST /oauth/introspect — check whether a token is currently valid.

curl -X POST https://auth.sentroy.com/oauth/introspect \
  -u client_abc123:secret_xxx \
  -d token=oat_xxxxxxxxxxxx
{
  "active": true,
  "scope": "openid profile email",
  "client_id": "client_abc123",
  "sub": "<user-id>",
  "token_type": "Bearer",
  "exp": 1733000000,
  "iat": 1732996400
}

A token introspected by a different client returns {"active": false} regardless of actual validity — strict client binding. Useful for stateless services that need a live-check without parsing the token themselves.

End session#

GET/POST /oauth/end-session — RP-initiated logout (OIDC).

Send the user here when they log out of your site. Sentroy revokes all access + refresh tokens issued to your client for that user, then redirects back to your post_logout_redirect_uri(must be on the client's allow-list — same security boundary as the authorize redirect).

https://auth.sentroy.com/oauth/end-session?
  id_token_hint=eyJhbGciOiJSUzI1NiIs...&
  post_logout_redirect_uri=https%3A%2F%2Fapp.example.com%2Floggedout&
  state=<random>

Connected apps (user-side)#

End users see + revoke their authorizations from their Sentroy profile.

Users visit https://sentroy.com/[lang]/profile/connected-apps to see every app they've signed into with their Sentroy account, what scopes they granted, and a one-click Revoke button. Revoke triggers a cascade:

  • Consent record deleted → next authorize re-prompts.
  • All access tokens for the (user, client) pair revoked → /oauth/userinfo 401s instantly.
  • All refresh tokens for the pair revoked → refresh attempts return invalid_grant.

Security notes#

What v1 enforces, what to mind when integrating.

  • state is your CSRF guard. Generate a random value before redirecting to authorize, store it in a session cookie, verify on the callback. Most libraries do this for you.
  • nonce defends against id_token replay. Generate a random value, send in authorize, verify it matches nonce claim in the returned id_token.
  • PKCE is recommended for every client and required for any client without a confidential client_secret (SPA, mobile, native).
  • id_tokenis signed HS256 with a Sentroy-side secret. The spec recommends RS256 + JWKS — that's on the roadmap. Today, treat the id_token as a userinfo shortcut, not a self-issued JWT you can hand to other services.
  • access_token is opaque (not a JWT). Validate by calling /oauth/userinfo — a 200 response means the token is live + the user still exists.
  • code lifetime is 10 minutes, single-use. Replays return invalid_grant.
  • refresh_token lifetime is 30 days. Rotation on every use. Family revocation on replay.