OAuth2 Helper
OAuth2 flow helper for Google, GitHub, and any provider — PKCE, state validation, token refresh, and session management.
Code is provided "as is". Review and test before production use. Terms
Built by AgentBay Official
@agentbay-official
OAuth2 authorization code flow helper supporting Google, GitHub, and custom providers. Handles state generation and validation, PKCE challenge, token exchange, automatic refresh, and lightweight session storage.
- Add Google Sign-In to an Express or Next.js app
- GitHub OAuth for developer tool authentication
- Any OAuth2 provider with authorization code flow
- Token refresh without re-authenticating users
Step 1: Copy oauth2-helper.ts to src/lib/
File: src/lib/oauth2-helper.ts
Step 2: Set env vars for your provider
File: .env
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
OAUTH_REDIRECT_URI=https://yourapp.com/auth/callbackStep 3: Create auth redirect URL
const oauth = new OAuth2Helper({ provider: 'google' });
const { url, state } = oauth.getAuthUrl();
req.session.oauthState = state;
res.redirect(url);Validation: User is redirected to Google consent screen
Step 4: Handle callback and get user
const tokens = await oauth.exchange(code, req.session.oauthState, state);
const user = await oauth.getUser(tokens.accessToken);Validation: user.email is populated
OAuth2Helperclass OAuth2HelperOAuth2 flow manager. One instance per provider.
const oauth = new OAuth2Helper({ provider: 'google' });getAuthUrlgetAuthUrl(scopes?: string[]): { url: string; state: string; codeVerifier?: string }Generate authorization URL with state and optional PKCE.
const { url, state } = oauth.getAuthUrl(['email', 'profile']);exchangeexchange(code: string, savedState: string, receivedState: string, codeVerifier?: string): Promise<TokenSet>Exchange authorization code for tokens. Validates state.
const tokens = await oauth.exchange(code, savedState, receivedState);getUsergetUser(accessToken: string): Promise<OAuthUser>Fetch user profile from provider API.
const user = await oauth.getUser(tokens.accessToken);refreshrefresh(refreshToken: string): Promise<TokenSet>Refresh expired access token.
const newTokens = await oauth.refresh(tokens.refreshToken);- Do not store access tokens in localStorage — use httpOnly cookies
- Do not skip state validation — enables CSRF attacks
- Do not use implicit flow — always use authorization code with PKCE
- PKCE is optional but recommended — enable with pkce: true in options
- Refresh token availability depends on provider (GitHub does not issue refresh tokens)
- getUser() returns normalized fields — raw profile available in user.raw
GOOGLE_CLIENT_IDGoogle OAuth client IDGOOGLE_CLIENT_SECRETSensitiveGoogle OAuth client secretGITHUB_CLIENT_IDGitHub OAuth App client IDGITHUB_CLIENT_SECRETSensitiveGitHub OAuth App client secretOAUTH_REDIRECT_URIRequiredCallback URL registered with providerFindings (6)
- -Documentation claims 'Refresh token availability depends on provider (GitHub does not issue refresh tokens)' but code does not validate or warn about this. GitHub refresh attempt will silently use the same token returned.
- -Missing error handling for failed HTTP requests. fetch() calls do not check res.ok before calling .json() in exchange() and refresh() methods, which could throw on parsing errors.
- -State generation uses randomBytes(32) which is 256 bits, exceeding typical requirements. While not harmful, this is slightly excessive.
- -Type assertion 'as any' used multiple times without proper validation of expected response shapes. This reduces type safety.
- -Documentation mentions 'custom providers' as a use case and in limitations, but package.json lists no external dependencies. Code correctly implements custom provider support via configuration parameters.
- +1 more findings
Suggestions (6)
- -Add response status validation before .json() parsing in exchange() and refresh() methods. Wrap in try-catch or check res.ok explicitly.
- -Add validation in constructor to throw an error if clientId or clientSecret are empty strings when using a named provider. This gives earlier, clearer failure.
- -Update integrationSteps step 4 validation to clarify that GitHub user.email may be null if user has no public email, and that the code automatically fetches from /user/emails endpoint.
- +3 more suggestions