DevToolBoxฟรี
บล็อก

OAuth 2.0 & Authentication Guide: PKCE, JWT, OpenID Connect, RBAC & Security Best Practices

22 min readโดย DevToolBox Team

OAuth 2.0 is the industry standard protocol for authorization, powering social logins, API integrations, and secure resource sharing across the web. This comprehensive guide walks you through OAuth fundamentals, JWT tokens, OpenID Connect, session management, and production security patterns with practical code examples for Node.js and Next.js applications.

TL;DR

OAuth 2.0 delegates authorization without sharing credentials. Use Authorization Code + PKCE for web apps, JWT for stateless APIs, OpenID Connect for identity, and always store tokens in httpOnly cookies. Implement RBAC for permissions and MFA for sensitive operations.

Key Takeaways

  • Use Authorization Code + PKCE flow; Implicit grant is deprecated
  • Store tokens in httpOnly cookies, never in localStorage
  • Use OpenID Connect for user identity; OAuth is for authorization only
  • Implement refresh token rotation to detect token theft
  • Use RBAC for fine-grained permission control
  • Enable MFA (TOTP or WebAuthn) for sensitive operations
  • Follow OWASP guidelines for redirect URI validation and injection prevention

1. OAuth 2.0 Fundamentals

OAuth 2.0 is a delegation protocol that allows a third-party application to access user resources on a server without the user sharing their credentials. It defines four roles: Resource Owner (the user), Client (the application), Authorization Server (issues tokens), and Resource Server (hosts protected resources).

OAuth 2.0 supports multiple grant types for different scenarios: Authorization Code (server-side apps), Client Credentials (machine-to-machine), Device Code (smart TVs, CLI tools), and Refresh Token (silent token renewal). The Implicit grant is deprecated in OAuth 2.1 in favor of Authorization Code with PKCE.

Key terminology: access_token grants resource access (short-lived, 5-60 minutes), refresh_token obtains new access tokens (long-lived, days to months), scope limits what the token can do (read:user, write:repos), and state prevents CSRF attacks during the authorization flow.

# OAuth 2.0 Grant Types Overview

1. Authorization Code  (server-side web apps)
   User -> Auth Server -> Code -> Token Exchange

2. Client Credentials   (machine-to-machine)
   Client -> Auth Server -> Token (no user)

3. Device Code          (smart TV, CLI)
   Device shows code -> User enters on phone

4. Refresh Token        (token renewal)
   Expired Token -> Refresh Token -> New Token

# Deprecated in OAuth 2.1:
5. Implicit             -> Use Auth Code + PKCE
6. Password (ROPC)      -> Use Auth Code + PKCE

2. Authorization Code Flow with PKCE

The Authorization Code flow is the most secure OAuth grant for web and mobile applications. The client redirects the user to the authorization server, receives an authorization code via callback, then exchanges it for tokens via a secure back-channel request. The access token never touches the browser.

PKCE (Proof Key for Code Exchange) adds protection against authorization code interception. The client generates a random code_verifier, derives a code_challenge via SHA-256, sends the challenge in the auth request, and proves possession of the verifier during token exchange. PKCE is now required for all public clients and recommended for confidential clients.

The state parameter is a random string that prevents CSRF attacks. The client generates a unique state value, includes it in the authorization request, and verifies it matches when the callback is received. Always validate state before exchanging the authorization code.

// Authorization Code Flow with PKCE
const crypto = require('crypto');

// Step 1: Generate PKCE pair
const codeVerifier = crypto
  .randomBytes(32).toString('base64url');
const codeChallenge = crypto
  .createHash('sha256')
  .update(codeVerifier).digest('base64url');

// Step 2: Build authorization URL
const authUrl = 'https://auth.example.com/authorize'
  + '?response_type=code'
  + '&client_id=my_app'
  + '&redirect_uri=https://app.com/callback'
  + '&scope=openid%20email%20profile'
  + '&state=' + csrfToken
  + '&code_challenge=' + codeChallenge
  + '&code_challenge_method=S256';

3. JWT Deep Dive

JSON Web Tokens (JWT) are compact, URL-safe tokens consisting of three Base64URL-encoded parts separated by dots: Header (algorithm and type), Payload (claims), and Signature (integrity proof). JWTs enable stateless authentication — the server verifies the signature without a database lookup.

Standard claims include: iss (issuer), sub (subject/user ID), aud (audience), exp (expiration), iat (issued at), nbf (not before), and jti (unique token ID). Custom claims like role, permissions, and email can be added but keep payloads small since they are sent with every request.

Always validate JWT tokens thoroughly: verify the signature algorithm matches expectations (prevent algorithm confusion attacks), check exp and nbf timestamps, validate iss and aud claims, and reject tokens with unexpected or missing claims. Use asymmetric keys (RS256/ES256) for distributed systems.

// JWT verification with jose library
const { jwtVerify } = require('jose');

async function verifyToken(token, publicKey) {
  const { payload } = await jwtVerify(
    token,
    publicKey,
    {
      algorithms: ['RS256'],
      issuer: 'https://auth.example.com',
      audience: 'my-api',
      maxTokenAge: '15m',
    }
  );
  // payload.sub = user ID
  // payload.role = user role
  return payload;
}

JWT Structure

# JWT = Header.Payload.Signature

# Header (Base64URL)
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "key-id-2024"
}

# Payload (Base64URL)
{
  "iss": "https://auth.example.com",
  "sub": "user_123",
  "aud": "my-api",
  "exp": 1709251200,
  "iat": 1709250300,
  "role": "editor"
}

# Signature
# RS256(base64(header) + "." + base64(payload),
#   privateKey)

4. OpenID Connect (OIDC)

OpenID Connect is an identity layer built on top of OAuth 2.0. While OAuth handles authorization (what can you access), OIDC handles authentication (who are you). It introduces the ID Token — a JWT containing user identity claims — and standardizes the UserInfo endpoint for retrieving profile data.

OIDC defines standard scopes: openid (required, returns sub claim), profile (name, picture, locale), email (email, email_verified), address, and phone. The ID token is always a JWT and must be validated by the client. The access token is used to call the UserInfo endpoint for additional claims.

OIDC Discovery allows clients to automatically configure endpoints by fetching /.well-known/openid-configuration from the provider. This returns the authorization endpoint, token endpoint, UserInfo endpoint, supported scopes, and JWKS URI for token verification.

// Fetch OIDC Discovery configuration
const discoveryUrl =
  'https://accounts.google.com'
  + '/.well-known/openid-configuration';

const config = await fetch(discoveryUrl)
  .then(r => r.json());

// config.authorization_endpoint
// config.token_endpoint
// config.userinfo_endpoint
// config.jwks_uri
// config.scopes_supported:
//   ["openid","email","profile"]
// config.id_token_signing_alg_values:
//   ["RS256"]

5. Session Management

Cookie-based sessions store a session ID in an httpOnly cookie. The server maintains session state in a store (Redis, database). Cookies are sent automatically by the browser, making them ideal for traditional web apps. Set secure, httpOnly, sameSite=lax, and appropriate maxAge for session cookies.

Token-based sessions store JWT access tokens client-side. The client includes the token in Authorization headers. This approach is stateless and works well for SPAs and mobile apps. Store tokens in httpOnly cookies (not localStorage) to prevent XSS token theft.

Refresh token rotation issues a new refresh token with each use and invalidates the old one. If a refresh token is used twice, it indicates theft — revoke all tokens for that user. Set refresh tokens with longer expiry (7-30 days) and store them securely server-side or in httpOnly cookies.

// Refresh token rotation middleware
app.post('/auth/refresh', async (req, res) => {
  const { refreshToken } = req.cookies;
  const stored = await db.tokens.findOne({
    token: refreshToken, revoked: false
  });
  if (!stored) {
    // Token reuse detected - revoke all
    await db.tokens.updateMany(
      { userId: stored?.userId },
      { revoked: true }
    );
    return res.status(401).json({
      error: 'Token reuse detected'
    });
  }
  await db.tokens.updateOne(
    { _id: stored._id }, { revoked: true }
  );
  // Issue new token pair...
});

6. OAuth with Node.js (Passport.js)

Passport.js is the most popular authentication middleware for Node.js with 500+ strategies. It integrates with Express via passport.initialize() and passport.session() middleware. Each strategy handles a specific authentication method (local, OAuth, SAML). Configure strategies with provider credentials and a verify callback.

The verify callback receives the access token, refresh token, and user profile from the OAuth provider. Use it to find or create the user in your database, associate the OAuth account, and return the user object. Passport serializes and deserializes the user for session management.

// Passport.js GitHub OAuth strategy
const passport = require('passport');
const GitHubStrategy =
  require('passport-github2').Strategy;

passport.use(new GitHubStrategy({
    clientID: process.env.GITHUB_ID,
    clientSecret: process.env.GITHUB_SECRET,
    callbackURL: '/auth/github/callback'
  },
  async (accessToken, refresh, profile, done) => {
    let user = await db.users.findOne({
      githubId: profile.id
    });
    if (!user) {
      user = await db.users.create({
        githubId: profile.id,
        name: profile.displayName,
        email: profile.emails?.[0]?.value
      });
    }
    return done(null, user);
  }
));

7. OAuth with Next.js (Auth.js)

Auth.js (formerly NextAuth.js) provides a complete authentication solution for Next.js applications. It supports 50+ OAuth providers out of the box, handles session management, CSRF protection, and token rotation automatically. Configuration is centralized in a single auth configuration file.

Auth.js supports multiple session strategies: JWT (default, stateless) and database sessions (via Prisma, Drizzle, or other adapters). Use callbacks to customize the JWT payload, session object, and sign-in behavior. The middleware protects routes at the edge for optimal performance.

// auth.ts - Auth.js configuration
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';
import Google from 'next-auth/providers/google';

export const { handlers, auth } = NextAuth({
  providers: [
    GitHub({ clientId: '', clientSecret: '' }),
    Google({ clientId: '', clientSecret: '' }),
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) token.role = user.role;
      return token;
    },
    async session({ session, token }) {
      session.user.role = token.role;
      return session;
    },
  },
});

8. Social Login Integration

Google OAuth requires creating credentials in the Google Cloud Console. Configure the OAuth consent screen with scopes (openid, email, profile), set authorized redirect URIs, and obtain client_id and client_secret. Google returns an ID token with verified email, name, and profile picture.

GitHub OAuth is configured in Settings > Developer Settings > OAuth Apps. GitHub provides user profile, email, and repository access via scopes (read:user, user:email, repo). Apple Sign In requires an Apple Developer account and provides minimal user data (email, name) — the name is only sent on first login, so store it immediately.

// Multi-provider social login config
const providers = {
  google: {
    authUrl: 'https://accounts.google.com'
      + '/o/oauth2/v2/auth',
    tokenUrl: 'https://oauth2.googleapis.com'
      + '/token',
    scopes: ['openid', 'email', 'profile'],
    userInfoUrl: 'https://www.googleapis.com'
      + '/oauth2/v3/userinfo',
  },
  github: {
    authUrl: 'https://github.com'
      + '/login/oauth/authorize',
    tokenUrl: 'https://github.com'
      + '/login/oauth/access_token',
    scopes: ['read:user', 'user:email'],
    userInfoUrl: 'https://api.github.com/user',
  },
};

9. Role-Based Access Control (RBAC)

RBAC assigns permissions to roles, then assigns roles to users. Common roles include admin, editor, viewer, and custom roles. Define permissions as granular actions: users:read, users:write, posts:delete. Store roles in the database and include the role in JWT claims for stateless authorization checks.

Implement authorization middleware that extracts the user role from the JWT, looks up the role permissions, and checks if the required permission is granted. Use a hierarchical permission model where admin inherits all editor permissions, and editor inherits all viewer permissions.

// RBAC middleware with permission check
const ROLES = {
  admin:  ['users:*', 'posts:*', 'settings:*'],
  editor: ['posts:read', 'posts:write',
           'posts:delete', 'users:read'],
  viewer: ['posts:read', 'users:read'],
};

function authorize(permission) {
  return (req, res, next) => {
    const userRole = req.user?.role;
    const perms = ROLES[userRole] || [];
    const allowed = perms.some(p =>
      p === permission ||
      p === permission.split(':')[0] + ':*'
    );
    if (!allowed) return res.status(403)
      .json({ error: 'Forbidden' });
    next();
  };
}

10. Token Security

Never store tokens in localStorage or sessionStorage — they are accessible to any JavaScript on the page, making them vulnerable to XSS attacks. Instead, store access tokens in httpOnly cookies with secure and sameSite flags. For SPAs, use the Backend-for-Frontend (BFF) pattern where the server handles token storage.

Security Warning: Tokens in localStorage and sessionStorage are readable by any injected script. A single XSS vulnerability can steal all user tokens. Always use httpOnly cookies.

Implement CSRF protection for cookie-based tokens using the SameSite cookie attribute (lax or strict), custom request headers (X-Requested-With), and synchronizer token pattern. For APIs, use short-lived access tokens (5-15 minutes) and rotate refresh tokens on each use.

// Secure cookie token configuration
function setAuthCookies(res, tokens) {
  res.cookie('access_token', tokens.access, {
    httpOnly: true,   // No JS access
    secure: true,     // HTTPS only
    sameSite: 'lax',  // CSRF protection
    maxAge: 15 * 60 * 1000,  // 15 min
    path: '/',
  });
  res.cookie('refresh_token', tokens.refresh, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000, // 7d
    path: '/auth/refresh', // Limit scope
  });
}

11. Multi-Factor Authentication (MFA)

TOTP (Time-based One-Time Password) generates 6-digit codes using a shared secret and current timestamp. The server generates a secret, the user scans a QR code with an authenticator app (Google Authenticator, Authy), and enters codes during login. Always provide backup codes in case the user loses their device.

WebAuthn (FIDO2) enables passwordless authentication using hardware security keys (YubiKey) or platform authenticators (Touch ID, Windows Hello). It uses public-key cryptography — the private key never leaves the device. WebAuthn is phishing-resistant because the browser verifies the origin before signing the challenge.

// TOTP setup and verification
const { authenticator } = require('otplib');
const QRCode = require('qrcode');

// Generate secret for user
app.post('/mfa/setup', auth, async (req, res) => {
  const secret = authenticator.generateSecret();
  const otpauth = authenticator.keyuri(
    req.user.email, 'MyApp', secret
  );
  const qrCode = await QRCode.toDataURL(
    otpauth
  );
  await db.users.updateOne(
    { _id: req.user.id },
    { mfaSecret: secret, mfaEnabled: false }
  );
  res.json({ qrCode, secret });
});

// Verify TOTP code
const isValid = authenticator.verify({
  token: userCode, secret: user.mfaSecret
});

12. API Authentication Patterns

API keys identify the calling application, not the user. Use them for server-to-server communication, rate limiting, and usage tracking. Send keys in the X-API-Key header (never in query parameters). Implement key rotation, per-key rate limits, and scope restrictions.

OAuth scopes limit what an access token can do. Define granular scopes (read:users, write:posts, admin:settings) and validate them in middleware. Use rate limiting per client/user with token bucket or sliding window algorithms. Return standard 429 responses with Retry-After headers.

// API key + OAuth scope middleware
function apiAuth(requiredScope) {
  return async (req, res, next) => {
    const apiKey = req.headers['x-api-key'];
    const bearer = req.headers.authorization;
    if (apiKey) {
      const client = await db.apiKeys.findOne({
        key: apiKey, active: true
      });
      if (!client)
        return res.status(401)
          .json({ error: 'Invalid API key' });
      if (!client.scopes.includes(requiredScope))
        return res.status(403)
          .json({ error: 'Insufficient scope' });
      req.client = client;
    } else if (bearer) {
      // Verify OAuth bearer token + scope
      req.user = verifyBearer(bearer);
    }
    next();
  };
}

13. Security Best Practices

Follow OWASP guidelines: validate all redirect URIs against a whitelist (prevent open redirect attacks), use exact string matching for redirect URIs (no wildcards), enforce HTTPS everywhere, implement proper token revocation, and log all authentication events for audit trails.

Common vulnerabilities to prevent: authorization code injection (use PKCE), token leakage via referrer headers (use fragment-based redirects), insufficient redirect URI validation (use exact match), cross-site request forgery (use state parameter), and insecure token storage (use httpOnly cookies).

Security Checklist: Always use PKCE, validate state parameter, exact-match redirect URIs, store tokens in httpOnly cookies, use short-lived access tokens (15min), rotate refresh tokens, enforce HTTPS everywhere, log all auth events.

// Security headers middleware
app.use((req, res, next) => {
  // Prevent clickjacking
  res.setHeader('X-Frame-Options', 'DENY');
  // Strict transport security
  res.setHeader(
    'Strict-Transport-Security',
    'max-age=31536000; includeSubDomains'
  );
  // Content security policy
  res.setHeader(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self'"
  );
  // Prevent MIME sniffing
  res.setHeader(
    'X-Content-Type-Options', 'nosniff'
  );
  next();
});

Authentication Decision Guide

  • Server-side web app: Auth Code flow + server sessions + httpOnly cookies
  • Single-page app (SPA): Auth Code + PKCE + BFF pattern + httpOnly cookies
  • Mobile app: Auth Code + PKCE + secure storage (Keychain/Keystore)
  • Microservice-to-microservice: Client Credentials flow + JWT + mTLS
  • Third-party API integration: API keys + OAuth scopes + rate limiting
  • IoT / CLI tools: Device Code flow + short-lived tokens

OAuth Flow Comparison

FlowUse CaseSecurityPKCE
Auth CodeServer-side web appsHighRecommended
Auth Code + PKCESPAs, mobile appsHighRequired
Client CredentialsMachine-to-machineMediumN/A
Device CodeSmart TV, CLIMediumN/A
Implicit (Deprecated)Do not useLowN/A

Token Storage Comparison

StorageXSS SafeCSRF SafeRecommended
localStorageNoYesNo
sessionStorageNoYesNo
httpOnly CookieYesWith SameSiteYes
In-memoryYesYesLost on refresh

Frequently Asked Questions

What is the difference between OAuth 2.0 and OpenID Connect?

OAuth 2.0 is an authorization framework that grants access to resources. OpenID Connect is an identity layer on top of OAuth 2.0 that adds authentication — verifying who the user is. OAuth answers "what can you access" while OIDC answers "who are you" by providing ID tokens with user identity claims.

Should I use JWT or session cookies for authentication?

Use session cookies for traditional server-rendered web apps where you control the server. Use JWTs for stateless APIs, microservices, and mobile apps. For SPAs, consider the BFF pattern — store JWTs in httpOnly cookies on a backend proxy to get the security benefits of cookies with the flexibility of JWTs.

Is the Implicit grant type still safe to use?

No. The Implicit grant is deprecated in OAuth 2.1 because it exposes access tokens in the URL fragment, making them vulnerable to browser history attacks and referrer leakage. Use Authorization Code flow with PKCE instead, which is secure for both public and confidential clients.

How should I store OAuth tokens in a browser application?

Never store tokens in localStorage or sessionStorage as they are vulnerable to XSS attacks. Store them in httpOnly, secure, sameSite cookies. For SPAs, use the Backend-for-Frontend pattern where the server manages tokens and sets secure cookies for the browser.

What is PKCE and do I need it?

PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks. The client creates a random verifier, sends a SHA-256 hash as a challenge during authorization, and proves possession of the verifier during token exchange. Yes, you need it — PKCE is required for all public clients and recommended for all OAuth flows.

How do I implement token refresh without logging out the user?

Use a refresh token stored in an httpOnly cookie. When the access token expires (typically after 5-15 minutes), the client or backend proxy sends the refresh token to the token endpoint to get a new access token. Implement silent refresh with an interceptor that retries failed requests after refreshing the token.

What scopes should I request for social login?

Request the minimum scopes needed. For social login, typically openid, email, and profile are sufficient. For Google, these provide verified email, name, and avatar. For GitHub, use read:user and user:email. Avoid requesting write scopes unless your app genuinely needs them — users are more likely to approve minimal scope requests.

How do I protect against CSRF attacks with OAuth?

Use the state parameter in all OAuth authorization requests. Generate a cryptographically random string, store it in the session, include it in the auth request, and verify it matches when the callback is received. Additionally, set SameSite=lax or strict on session cookies and validate the Origin header on token exchange requests.

𝕏 Twitterin LinkedIn
บทความนี้มีประโยชน์ไหม?

อัปเดตข่าวสาร

รับเคล็ดลับการพัฒนาและเครื่องมือใหม่ทุกสัปดาห์

ไม่มีสแปม ยกเลิกได้ตลอดเวลา

ลองเครื่องมือที่เกี่ยวข้อง

JWTJWT Decoder{ }JSON Formatter#Hash Generator

บทความที่เกี่ยวข้อง

Next.js Advanced Guide: App Router, Server Components, Data Fetching, Middleware & Performance

Complete Next.js advanced guide covering App Router architecture, React Server Components, streaming SSR, data fetching patterns, middleware, route handlers, parallel and intercepting routes, caching strategies, ISR, image optimization, and deployment best practices.

Advanced TypeScript Guide: Generics, Conditional Types, Mapped Types, Decorators, and Type Narrowing

Master advanced TypeScript patterns. Covers generic constraints, conditional types with infer, mapped types (Partial/Pick/Omit), template literal types, discriminated unions, utility types deep dive, decorators, module augmentation, type narrowing, covariance/contravariance, and satisfies operator.

Microservices Patterns Guide: Saga, CQRS, Event Sourcing, Service Mesh & Domain-Driven Design

Complete microservices patterns guide covering Saga pattern, CQRS, event sourcing, service mesh with Istio, API gateway patterns, circuit breaker, distributed tracing, domain-driven design, and microservices testing strategies.