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. You redirect the person to Pabbel's consent screen.
- 2. They approve, and Pabbel redirects back to you with a one-time
code. - 3. Your server swaps that code for an access token.
- 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.
<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>.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:
| Parameter | Required | What it is |
|---|---|---|
| client_id | yes | From your app's page. |
| redirect_uri | yes | Must match one of your registered redirect URIs exactly. |
| scope | no | Space separated. Defaults to everything your app is registered for; asking for something it is not registered for is an error. |
| state | strongly advised | An unguessable value you store in the session and check on the way back. |
| code_challenge | yes when PKCE is on | base64url(SHA-256(code_verifier)). New apps require PKCE by default. |
| code_challenge_method | with a challenge | S256. Plain is not accepted. |
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=S2562. 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.
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
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.
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.
profileSee the person's name, avatar, email address and Pabbel account id.
drive.fileNot available yetSee and manage only the Drive files your app creates or the person opens with it.
drive.uploadNot available yetUpload 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
stateand 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.