Quick Start

Get “Sign in with Blah” working in your application in under 10 minutes.

Prerequisites

  • A blah.dev account
  • Your application's callback URL (e.g., https://myapp.com/auth/callback)
Step 1

Create an OAuth Client

Option A: Dashboard (Recommended)

  1. Log in to blah.dev
  2. Navigate to DashboardOAuth Clients
  3. Click Create Application
  4. Enter your application name and redirect URI(s)
  5. Copy and securely store the client secret (shown only once)

Option B: API

Create client via API
curl -X POST https://blah.dev/api/oauth/clients \ -H "Content-Type: application/json" \ -H "Cookie: session=YOUR_SESSION_COOKIE" \ -d '{ "name": "My App", "redirect_uris": ["https://myapp.com/auth/callback"] }'

Response:

json
{
  "client_id": "jY5Tx2K4Qp8...",
  "client_secret": "s3cr3t_7x9..."
}
Step 2

Redirect to Authorization

Send your users to the authorization endpoint. We recommend using PKCE for all clients.

auth.js
const crypto = require('crypto');

// Generate PKCE values
const verifier = crypto.randomBytes(32).toString('base64url');
const challenge = crypto
  .createHash('sha256')
  .update(verifier)
  .digest('base64url');
const state = crypto.randomBytes(16).toString('hex');

// Store verifier and state in session
req.session.pkceVerifier = verifier;
req.session.oauthState = state;

// Build authorization URL
const params = new URLSearchParams({
  response_type: 'code',
  client_id: BLAH_CLIENT_ID,
  redirect_uri: 'https://myapp.com/auth/callback',
  scope: 'openid email',
  state,
  code_challenge: challenge,
  code_challenge_method: 'S256',
});

// Redirect user
res.redirect(`https://blah.dev/oauth/authorize?${params}`);
Step 3

Exchange Code for Tokens

After the user approves, they're redirected back to your callback URL with an authorization code. Exchange it for tokens:

callback.js
// Verify state
if (req.query.state !== req.session.oauthState) {
  return res.status(400).send('Invalid state');
}

// Exchange code for tokens
const tokenResponse = await fetch('https://blah.dev/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: req.query.code,
    redirect_uri: 'https://myapp.com/auth/callback',
    client_id: BLAH_CLIENT_ID,
    client_secret: BLAH_CLIENT_SECRET,
    code_verifier: req.session.pkceVerifier,
  }),
});

const tokens = await tokenResponse.json();
// { access_token, token_type, expires_in, refresh_token, scope }
Step 4

Get User Info

Use the access token to fetch user information:

Fetch user info
curl https://blah.dev/oauth/userinfo \ -H "Authorization: Bearer ACCESS_TOKEN"
json
{
  "sub": "user_abc123",
  "email": "user@example.com"
}
Step 5

Handle Token Refresh

Access tokens expire after 1 hour. Use the refresh token to get new ones:

refresh.js
async function refreshTokens(refreshToken) {
  const response = await fetch('https://blah.dev/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: BLAH_CLIENT_ID,
      client_secret: BLAH_CLIENT_SECRET,
    }),
  });

  if (!response.ok) {
    throw new Error('Token refresh failed');
  }

  return response.json();
}

Troubleshooting

"Invalid redirect_uri"

Ensure the redirect URI exactly matches what you registered. Check for trailing slashes and verify you're using HTTPS (required except for localhost).

"Invalid state"

State is stored in session/cookies — check cookie settings. Ensure cookies are being set correctly (SameSite, Secure flags). Session may have expired.

"Invalid code_verifier"

Ensure the verifier is stored and retrieved correctly. Verify you're using base64url encoding (not standard base64). Check that the challenge method matches.

Token refresh failing

Check that the refresh token hasn't expired (30-day lifetime). Verify client credentials are correct. Ensure the token hasn't been revoked.

Next Steps