Add Sign in with Pabbel

Pabbel is an OAuth 2.0 provider using the authorization code flow with PKCE. People sign in with the Pabbel account they already have, agree to the permissions you ask for, and your server gets an access token.

How it works

  1. 1. You redirect the person to Pabbel's consent screen.
  2. 2. They approve, and Pabbel redirects back to you with a one-time code.
  3. 3. Your server swaps that code for an access token.
  4. 4. You call the API with the token.

The button

Point it at whatever route on your server starts step 1. The mark is inlined, so there is nothing to host and nothing to wait for.

Continue with Pabbel
Continue with Pabbel
button.html
<a class="pabbel-btn" href="/auth/pabbel">
  <svg viewBox="0 0 108 108" aria-hidden="true">
    <rect width="108" height="108" rx="24" fill="#E0612A"/>
    <g transform="translate(54 54) scale(1.32) translate(-59.5 -54)">
      <path fill="#fff" d="M44,76 L44,32 L60,32 a15,15 0 0 1 0,30 L52,62 L52,76 Z M52,40 L52,54 L59,54 a7,7 0 0 0 0,-14 Z"/>
      <circle cx="64" cy="70" r="5" fill="#FFD9C4"/>
    </g>
  </svg>
  Continue with Pabbel
</a>
button.css
.pabbel-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.625rem;
  padding: 0.625rem 1rem;
  border: 1px solid #dcdcdc;
  border-radius: 0.5rem;
  background: #fff;
  color: #1a1a1a;
  font: 500 0.875rem/1 system-ui, -apple-system, sans-serif;
  text-decoration: none;
}
.pabbel-btn:hover { background: #f6f6f6; }
.pabbel-btn svg { width: 20px; height: 20px; border-radius: 5px; }

@media (prefers-color-scheme: dark) {
  .pabbel-btn { background: #1b1b1b; color: #f2f2f2; border-color: #333; }
  .pabbel-btn:hover { background: #242424; }
}

Prefer to link the mark? It is served, unversioned and permanently, from https://accounts.pabbel.com/brand/pabbel-mark.svg.

  • · Keep the mark's colours. Do not recolour it, outline it, or drop it into a coloured pill.
  • · Say “Continue with Pabbel” or “Sign in with Pabbel”. Not “Login with Pabbel”, and never just “Pabbel”.
  • · Give the mark at least 16px, and leave it square - it has its own rounded corners.
  • · Do not imply Pabbel endorses your app, and do not use the mark as your own icon.

1. Send them to Pabbel

Redirect the browser to https://accounts.pabbel.com/authorize with:

ParameterRequiredWhat it is
client_idyesFrom your app's page.
redirect_uriyesMust match one of your registered redirect URIs exactly.
scopenoSpace separated. Defaults to everything your app is registered for; asking for something it is not registered for is an error.
statestrongly advisedAn unguessable value you store in the session and check on the way back.
code_challengeyes when PKCE is onbase64url(SHA-256(code_verifier)). New apps require PKCE by default.
code_challenge_methodwith a challengeS256. Plain is not accepted.
redirect
https://accounts.pabbel.com/authorize
  ?client_id=YOUR_CLIENT_ID
  &redirect_uri=https%3A%2F%2Fyour-app.com%2Fauth%2Fcallback
  &scope=profile
  &state=RANDOM_PER_REQUEST
  &code_challenge=BASE64URL_SHA256_OF_VERIFIER
  &code_challenge_method=S256

2. Swap the code, on your server

Pabbel redirects back with ?code=…&state=…. Check the state matches what you stored, then exchange the code. The client secret must never reach the browser.

POST /oauth/token
await fetch("https://accounts.pabbel.com/api/oauth/token", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    clientId: "YOUR_CLIENT_ID",
    clientSecret: process.env.PABBEL_CLIENT_SECRET,
    code,
    codeVerifier,          // the verifier you kept in the session
  }),
});

// → { accessToken, tokenType, scope, expiresIn, refreshToken, user }

3. Call the API

GET /auth/me
const me = await fetch("https://accounts.pabbel.com/api/auth/me", {
  headers: { Authorization: `Bearer ${accessToken}` },
}).then((r) => r.json());

A token only opens what its scopes name. Anything else answers 403, and an expired token answers 401 so you know to refresh rather than to give up.

Refreshing

If your app has a token lifetime set, the exchange also returns a refreshToken. Refresh tokens rotate: each one can be spent once, and using it gives you a new pair.

refresh
await fetch("https://accounts.pabbel.com/api/oauth/token", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    grantType: "refresh_token",
    clientId: "YOUR_CLIENT_ID",
    clientSecret: process.env.PABBEL_CLIENT_SECRET,
    refreshToken,
  }),
});

Scopes

A token only reaches the surfaces its scopes name; everything else answers 403. Today that means profile. The Drive scopes are described here so you can plan for them, but they cannot be registered yet.

profile

See the person's name, avatar, email address and Pabbel account id.

drive.fileNot available yet

See and manage only the Drive files your app creates or the person opens with it.

drive.uploadNot available yet

Upload new files to the person's Pabbel Drive.

Getting it right

  • · Redirect URIs match exactly. A trailing slash or http/https swap is the usual cause of “works locally, fails in production”.
  • · Always send state and check it on the way back. It is what stops someone else's code being planted in your callback.
  • · Use PKCE. New apps require it by default; it makes a stolen code useless.
  • · Keep the secret on the server. If it leaks, rotate it from your app's page.
  • · Codes are single-use and expire in two minutes.