🎯

oauth2

🎯Skill

from oriolrius/pki-manager-web

VibeIndex|
What it does

Guides developers through implementing OAuth 2.0 authorization flows, token management, and secure authentication across various platforms and services.

oauth2

Installation

Install skill:
npx skills add https://github.com/oriolrius/pki-manager-web --skill oauth2
4
Last UpdatedDec 4, 2025

Skill Details

SKILL.md

Expert guidance for OAuth 2.0 protocol including authorization flows, grant types, token management, OpenID Connect, security best practices, and implementation patterns. Use this when implementing authentication/authorization, working with OAuth providers, securing APIs, or integrating with third-party services.

Overview

# OAuth 2.0

Expert assistance with OAuth 2.0 authorization framework and OpenID Connect.

Overview

OAuth 2.0 is an authorization framework that enables applications to obtain limited access to user accounts. Key concepts:

  • Resource Owner: User who owns the data
  • Client: Application requesting access
  • Authorization Server: Issues access tokens (e.g., Keycloak, Auth0)
  • Resource Server: API that holds protected resources
  • Access Token: Credentials to access protected resources
  • Refresh Token: Credentials to obtain new access tokens

OAuth 2.0 Flows

Authorization Code Flow (Most Secure)

Best for: Server-side web apps with backend

Flow:

```

  1. Client redirects user to authorization server

GET /authorize?

response_type=code

&client_id=CLIENT_ID

&redirect_uri=REDIRECT_URI

&scope=read write

&state=RANDOM_STATE

  1. User authenticates and grants permission
  1. Authorization server redirects back with code

REDIRECT_URI?code=AUTH_CODE&state=RANDOM_STATE

  1. Client exchanges code for tokens

POST /token

Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code

&code=AUTH_CODE

&redirect_uri=REDIRECT_URI

&client_id=CLIENT_ID

&client_secret=CLIENT_SECRET

  1. Authorization server responds with tokens

{

"access_token": "ACCESS_TOKEN",

"token_type": "Bearer",

"expires_in": 3600,

"refresh_token": "REFRESH_TOKEN",

"scope": "read write"

}

```

Implementation (Node.js):

```javascript

// Step 1: Redirect to authorization

app.get('/login', (req, res) => {

const state = crypto.randomBytes(16).toString('hex')

req.session.oauthState = state

const authUrl = new URL('https://auth.example.com/authorize')

authUrl.searchParams.set('response_type', 'code')

authUrl.searchParams.set('client_id', CLIENT_ID)

authUrl.searchParams.set('redirect_uri', REDIRECT_URI)

authUrl.searchParams.set('scope', 'read write')

authUrl.searchParams.set('state', state)

res.redirect(authUrl.toString())

})

// Step 3 & 4: Handle callback and exchange code

app.get('/callback', async (req, res) => {

const { code, state } = req.query

// Verify state

if (state !== req.session.oauthState) {

return res.status(400).send('Invalid state')

}

// Exchange code for token

const tokenResponse = await fetch('https://auth.example.com/token', {

method: 'POST',

headers: {

'Content-Type': 'application/x-www-form-urlencoded',

},

body: new URLSearchParams({

grant_type: 'authorization_code',

code,

redirect_uri: REDIRECT_URI,

client_id: CLIENT_ID,

client_secret: CLIENT_SECRET,

}),

})

const tokens = await tokenResponse.json()

// Store tokens securely

req.session.accessToken = tokens.access_token

req.session.refreshToken = tokens.refresh_token

res.redirect('/dashboard')

})

```

Authorization Code Flow with PKCE

Best for: Mobile apps, SPAs, any public client

PKCE adds security for public clients that can't keep secrets

Flow:

```javascript

// Step 1: Generate code verifier and challenge

const codeVerifier = base64URLEncode(crypto.randomBytes(32))

const codeChallenge = base64URLEncode(

crypto.createHash('sha256').update(codeVerifier).digest()

)

// Step 2: Authorization request

GET /authorize?

response_type=code

&client_id=CLIENT_ID

&redirect_uri=REDIRECT_URI

&scope=read

&state=STATE

&code_challenge=CODE_CHALLENGE

&code_challenge_method=S256

// Step 3: Token request (no client_secret needed)

POST /token

Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code

&code=AUTH_CODE

&redirect_uri=REDIRECT_URI

&client_id=CLIENT_ID

&code_verifier=CODE_VERIFIER

```

Implementation (React):

```typescript

import { useEffect } from 'react'

import { useRouter } from 'next/router'

// Generate PKCE challenge

function generatePKCE() {

const codeVerifier = generateRandomString(128)

const codeChallenge = base64URLEncode(

sha256(codeVerifier)

)

return { codeVerifier, codeChallenge }

}

function LoginButton() {

const router = useRouter()

const handleLogin = () => {

const { codeVerifier, codeChallenge } = generatePKCE()

const state = generateRandomString(32)

// Store for later use

sessionStorage.setItem('pkce_verifier', codeVerifier)

sessionStorage.setItem('oauth_state', state)

const authUrl = new URL('https://auth.example.com/authorize')

authUrl.searchParams.set('response_type', 'code')

authUrl.searchParams.set('client_id', CLIENT_ID)

authUrl.searchParams.set('redirect_uri', REDIRECT_URI)

authUrl.searchParams.set('scope', 'openid profile email')

authUrl.searchParams.set('state', state)

authUrl.searchParams.set('code_challenge', codeChallenge)

authUrl.searchParams.set('code_challenge_method', 'S256')

window.location.href = authUrl.toString()

}

return

}

// Callback page

function CallbackPage() {

const router = useRouter()

useEffect(() => {

async function handleCallback() {

const { code, state } = router.query

// Verify state

const savedState = sessionStorage.getItem('oauth_state')

if (state !== savedState) {

throw new Error('Invalid state')

}

// Get code verifier

const codeVerifier = sessionStorage.getItem('pkce_verifier')

// Exchange code for token

const response = await fetch('https://auth.example.com/token', {

method: 'POST',

headers: {

'Content-Type': 'application/x-www-form-urlencoded',

},

body: new URLSearchParams({

grant_type: 'authorization_code',

code: code as string,

redirect_uri: REDIRECT_URI,

client_id: CLIENT_ID,

code_verifier: codeVerifier!,

}),

})

const tokens = await response.json()

// Store tokens

localStorage.setItem('access_token', tokens.access_token)

localStorage.setItem('refresh_token', tokens.refresh_token)

// Clean up

sessionStorage.removeItem('pkce_verifier')

sessionStorage.removeItem('oauth_state')

router.push('/dashboard')

}

handleCallback()

}, [router.query])

return

Logging in...

}

```

Client Credentials Flow

Best for: Machine-to-machine, service accounts, server-to-server

Flow:

```bash

# Request token

POST /token

Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials

&client_id=CLIENT_ID

&client_secret=CLIENT_SECRET

&scope=api.read api.write

# Response

{

"access_token": "ACCESS_TOKEN",

"token_type": "Bearer",

"expires_in": 3600,

"scope": "api.read api.write"

}

```

Implementation:

```javascript

async function getServiceToken() {

const response = await fetch('https://auth.example.com/token', {

method: 'POST',

headers: {

'Content-Type': 'application/x-www-form-urlencoded',

},

body: new URLSearchParams({

grant_type: 'client_credentials',

client_id: process.env.CLIENT_ID,

client_secret: process.env.CLIENT_SECRET,

scope: 'api.read api.write',

}),

})

return await response.json()

}

// Use token

async function callProtectedAPI() {

const { access_token } = await getServiceToken()

const response = await fetch('https://api.example.com/data', {

headers: {

'Authorization': Bearer ${access_token},

},

})

return await response.json()

}

```

Resource Owner Password Credentials (Legacy)

⚠️ Not recommended - Only use when no other flow works

```bash

POST /token

Content-Type: application/x-www-form-urlencoded

grant_type=password

&username=USER

&password=PASSWORD

&client_id=CLIENT_ID

&client_secret=CLIENT_SECRET

&scope=read

```

Implicit Flow (Deprecated)

⚠️ Deprecated - Use Authorization Code Flow with PKCE instead

Token Management

Access Tokens

Characteristics:

  • Short-lived (5-15 minutes typical)
  • Used to access protected resources
  • Should be treated as opaque strings
  • Often JWT format but not required

Usage:

```javascript

// Make API request with access token

fetch('https://api.example.com/user/profile', {

headers: {

'Authorization': Bearer ${accessToken},

},

})

```

Refresh Tokens

Purpose: Obtain new access tokens without re-authentication

Usage:

```javascript

async function refreshAccessToken(refreshToken) {

const response = await fetch('https://auth.example.com/token', {

method: 'POST',

headers: {

'Content-Type': 'application/x-www-form-urlencoded',

},

body: new URLSearchParams({

grant_type: 'refresh_token',

refresh_token: refreshToken,

client_id: CLIENT_ID,

client_secret: CLIENT_SECRET,

}),

})

const tokens = await response.json()

return tokens

}

// Automatic token refresh

async function apiCall(url, options = {}) {

let accessToken = localStorage.getItem('access_token')

let response = await fetch(url, {

...options,

headers: {

...options.headers,

'Authorization': Bearer ${accessToken},

},

})

// Token expired

if (response.status === 401) {

const refreshToken = localStorage.getItem('refresh_token')

const newTokens = await refreshAccessToken(refreshToken)

localStorage.setItem('access_token', newTokens.access_token)

if (newTokens.refresh_token) {

localStorage.setItem('refresh_token', newTokens.refresh_token)

}

// Retry request with new token

response = await fetch(url, {

...options,

headers: {

...options.headers,

'Authorization': Bearer ${newTokens.access_token},

},

})

}

return response

}

```

Token Storage

Best Practices:

Browser (SPA):

  • βœ… Memory (most secure, lost on refresh)
  • βœ… Session storage (cleared on tab close)
  • ⚠️ Local storage (XSS risk, but convenient)
  • ❌ Cookies without httpOnly (XSS risk)
  • βœ… httpOnly cookies (CSRF protection needed)

Server-side:

  • βœ… Encrypted session storage
  • βœ… Secure database with encryption
  • ❌ Plain text files
  • ❌ Environment variables (for user tokens)

Implementation:

```javascript

// Secure token storage (Next.js)

import { serialize, parse } from 'cookie'

// Set token in httpOnly cookie

export function setTokenCookie(res, token) {

const cookie = serialize('token', token, {

httpOnly: true,

secure: process.env.NODE_ENV === 'production',

sameSite: 'lax',

maxAge: 3600,

path: '/',

})

res.setHeader('Set-Cookie', cookie)

}

// Get token from cookie

export function getTokenCookie(req) {

const cookies = parse(req.headers.cookie || '')

return cookies.token

}

```

OpenID Connect (OIDC)

OAuth 2.0 extension for authentication

ID Token

JWT containing user information:

```json

{

"iss": "https://auth.example.com",

"sub": "user-123",

"aud": "client-id",

"exp": 1234567890,

"iat": 1234567890,

"name": "John Doe",

"email": "john@example.com",

"email_verified": true,

"picture": "https://example.com/photo.jpg"

}

```

OIDC Flow

```javascript

// Authorization request with openid scope

GET /authorize?

response_type=code

&client_id=CLIENT_ID

&redirect_uri=REDIRECT_URI

&scope=openid profile email

&state=STATE

// Token response includes ID token

{

"access_token": "ACCESS_TOKEN",

"id_token": "ID_TOKEN_JWT",

"refresh_token": "REFRESH_TOKEN",

"token_type": "Bearer",

"expires_in": 3600

}

// Validate ID token

import jwt from 'jsonwebtoken'

import jwksClient from 'jwks-rsa'

const client = jwksClient({

jwksUri: 'https://auth.example.com/.well-known/jwks.json'

})

function getKey(header, callback) {

client.getSigningKey(header.kid, (err, key) => {

const signingKey = key.publicKey || key.rsaPublicKey

callback(null, signingKey)

})

}

jwt.verify(idToken, getKey, {

audience: CLIENT_ID,

issuer: 'https://auth.example.com',

algorithms: ['RS256']

}, (err, decoded) => {

if (err) {

console.error('Invalid token')

} else {

console.log('User:', decoded)

}

})

```

UserInfo Endpoint

```javascript

// Get additional user info

async function getUserInfo(accessToken) {

const response = await fetch('https://auth.example.com/userinfo', {

headers: {

'Authorization': Bearer ${accessToken},

},

})

return await response.json()

}

```

Discovery

```bash

# Get provider configuration

GET https://auth.example.com/.well-known/openid-configuration

# Response includes:

{

"issuer": "https://auth.example.com",

"authorization_endpoint": "https://auth.example.com/authorize",

"token_endpoint": "https://auth.example.com/token",

"userinfo_endpoint": "https://auth.example.com/userinfo",

"jwks_uri": "https://auth.example.com/.well-known/jwks.json",

"response_types_supported": ["code", "token", "id_token"],

"grant_types_supported": ["authorization_code", "refresh_token"],

"token_endpoint_auth_methods_supported": ["client_secret_post"]

}

```

Scopes

Standard Scopes

  • openid - Required for OIDC
  • profile - User's profile info (name, picture, etc.)
  • email - User's email address
  • address - User's address
  • phone - User's phone number
  • offline_access - Request refresh token

Custom Scopes

```

API-specific permissions:

  • api:read - Read access to API
  • api:write - Write access to API
  • api:delete - Delete access to API
  • admin - Admin access

```

Requesting Scopes

```javascript

// Request multiple scopes

const authUrl = new URL('https://auth.example.com/authorize')

authUrl.searchParams.set('scope', 'openid profile email api:read api:write')

```

Security Best Practices

1. Always Use State Parameter

```javascript

// Generate random state

const state = crypto.randomBytes(32).toString('hex')

sessionStorage.setItem('oauth_state', state)

// Include in authorization request

authUrl.searchParams.set('state', state)

// Verify in callback

if (receivedState !== sessionStorage.getItem('oauth_state')) {

throw new Error('CSRF attack detected')

}

```

2. Use PKCE for Public Clients

Always use PKCE for SPAs and mobile apps - no exceptions.

3. Validate Tokens

```javascript

// Validate JWT

  • Verify signature using public key
  • Check issuer (iss)
  • Check audience (aud)
  • Check expiration (exp)
  • Check not before (nbf)

```

4. Short-Lived Access Tokens

```

Access token: 5-15 minutes

Refresh token: Days to months (with rotation)

```

5. Token Rotation

```javascript

// Refresh token rotation

When using refresh token:

  1. Issue new access token
  2. Issue new refresh token
  3. Invalidate old refresh token

```

6. Secure Token Storage

```javascript

// βœ… Best practices

  • httpOnly cookies (server-side)
  • Encrypted session storage (server-side)
  • Memory only (SPA, lost on refresh)

// ❌ Avoid

  • Local storage (XSS vulnerable)
  • Session storage without proper sanitization
  • URL parameters
  • Plain text anywhere

```

7. Redirect URI Validation

```javascript

// Server must validate exact match

Registered: https://app.example.com/callback

Valid: https://app.example.com/callback

Invalid: https://app.example.com/callback/extra

Invalid: https://evil.com/callback

```

8. HTTPS Only

All OAuth communication must use HTTPS in production.

9. Rate Limiting

Implement rate limiting on:

  • Token endpoint
  • Authorization endpoint
  • UserInfo endpoint

10. Audit Logging

Log all OAuth events:

  • Authorization requests
  • Token issuances
  • Token refreshes
  • Failed attempts

Common Patterns

API Protection

```javascript

// Express middleware

function requireAuth(req, res, next) {

const token = req.headers.authorization?.split(' ')[1]

if (!token) {

return res.status(401).json({ error: 'No token provided' })

}

try {

const decoded = jwt.verify(token, PUBLIC_KEY)

req.user = decoded

next()

} catch (error) {

return res.status(401).json({ error: 'Invalid token' })

}

}

// Protected route

app.get('/api/protected', requireAuth, (req, res) => {

res.json({ data: 'Protected data', user: req.user })

})

// Role-based protection

function requireRole(role) {

return (req, res, next) => {

if (!req.user?.roles?.includes(role)) {

return res.status(403).json({ error: 'Insufficient permissions' })

}

next()

}

}

app.delete('/api/users/:id', requireAuth, requireRole('admin'), (req, res) => {

// Delete user

})

```

Silent Authentication

```javascript

// Attempt silent authentication in hidden iframe

function silentAuthentication() {

return new Promise((resolve, reject) => {

const iframe = document.createElement('iframe')

iframe.style.display = 'none'

const authUrl = new URL('https://auth.example.com/authorize')

authUrl.searchParams.set('prompt', 'none')

authUrl.searchParams.set('response_type', 'code')

authUrl.searchParams.set('client_id', CLIENT_ID)

authUrl.searchParams.set('redirect_uri', SILENT_REDIRECT_URI)

iframe.src = authUrl.toString()

window.addEventListener('message', (event) => {

if (event.origin !== 'https://app.example.com') return

if (event.data.code) {

resolve(event.data.code)

} else {

reject(new Error('Silent auth failed'))

}

document.body.removeChild(iframe)

})

document.body.appendChild(iframe)

})

}

```

Troubleshooting

Invalid Grant

  • Expired authorization code
  • Code already used
  • Mismatched redirect URI
  • Invalid code verifier (PKCE)

Invalid Client

  • Wrong client credentials
  • Client not found
  • Client disabled

Unauthorized Client

  • Client not allowed for grant type
  • Client not allowed for scope

Access Denied

  • User denied permission
  • Invalid scope requested

Resources

  • RFC 6749 (OAuth 2.0): https://tools.ietf.org/html/rfc6749
  • RFC 7636 (PKCE): https://tools.ietf.org/html/rfc7636
  • OpenID Connect: https://openid.net/connect/
  • OAuth 2.0 Security Best Practices: https://tools.ietf.org/html/draft-ietf-oauth-security-topics