본문으로 건너뛰기
cccskills
무료GitHub에 공개

oauth-patterns

OIDC flows, PKCE implementation, token refresh strategies, social login integration, and secure session management.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

설치 방법 보기

포함된 파일(1)

  • SKILL.md8.6 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

OAuth Patterns

Secure authentication and authorization patterns with OAuth 2.0 and OpenID Connect.

Authorization Code Flow with PKCE

// PKCE (Proof Key for Code Exchange): required for public clients (SPA, mobile)
import crypto from 'crypto'

// Step 1: Generate PKCE verifier and challenge
function generatePKCE(): { verifier: string; challenge: string } {
  const verifier = crypto.randomBytes(32).toString('base64url')
  const challenge = crypto
    .createHash('sha256')
    .update(verifier)
    .digest('base64url')
  return { verifier, challenge }
}

// Step 2: Build authorization URL
function getAuthorizationUrl(config: OAuthConfig): { url: string; state: string; pkce: PKCE } {
  const state = crypto.randomBytes(16).toString('hex')
  const pkce = generatePKCE()

  const params = new URLSearchParams({
    response_type: 'code',
    client_id: config.clientId,
    redirect_uri: config.redirectUri,
    scope: 'openid profile email',
    state,
    code_challenge: pkce.challenge,
    code_challenge_method: 'S256',
    prompt: 'consent',            // Force consent screen
    nonce: crypto.randomUUID(),   // Replay protection
  })

  return {
    url: `${config.authorizationEndpoint}?${params}`,
    state,
    pkce,
  }
}

// Step 3: Exchange code for tokens
async function exchangeCodeForTokens(
  code: string,
  verifier: string,
  config: OAuthConfig
): Promise<TokenSet> {
  const response = await fetch(config.tokenEndpoint, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code,
      redirect_uri: config.redirectUri,
      client_id: config.clientId,
      code_verifier: verifier,
    }),
  })

  if (!response.ok) {
    const error = await response.json()
    throw new Error(`Token exchange failed: ${error.error_description}`)
  }

  return response.json() as Promise<TokenSet>
}

Token Refresh Strategy

interface TokenSet {
  access_token: string
  refresh_token: string
  id_token: string
  expires_in: number        // seconds
  token_type: 'Bearer'
}

class TokenManager {
  private refreshTimer: NodeJS.Timeout | null = null

  async setTokens(tokens: TokenSet): Promise<void> {
    // Store tokens securely (httpOnly cookies or encrypted storage)
    await secureStore.set('access_token', tokens.access_token)
    await secureStore.set('refresh_token', tokens.refresh_token)

    // Schedule refresh before expiry (refresh at 75% of lifetime)
    const refreshIn = tokens.expires_in * 0.75 * 1000
    this.scheduleRefresh(refreshIn)
  }

  private scheduleRefresh(delayMs: number): void {
    if (this.refreshTimer) clearTimeout(this.refreshTimer)
    this.refreshTimer = setTimeout(() => this.refresh(), delayMs)
  }

  private async refresh(): Promise<void> {
    const refreshToken = await secureStore.get('refresh_token')
    if (!refreshToken) {
      this.emit('session_expired')
      return
    }

    try {
      const response = await fetch(config.tokenEndpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: new URLSearchParams({
          grant_type: 'refresh_token',
          refresh_token: refreshToken,
          client_id: config.clientId,
        }),
      })

      if (!response.ok) throw new Error('Refresh failed')

      const tokens = await response.json() as TokenSet
      await this.setTokens(tokens)
    } catch (err) {
      // Refresh token expired or revoked
      await this.clearTokens()
      this.emit('session_expired')
    }
  }

  async clearTokens(): Promise<void> {
    if (this.refreshTimer) clearTimeout(this.refreshTimer)
    await secureStore.delete('access_token')
    await secureStore.delete('refresh_token')
  }
}

Social Login Integration

// Provider-specific configurations
const OAUTH_PROVIDERS: Record<string, OAuthConfig> = {
  google: {
    clientId: process.env.GOOGLE_CLIENT_ID!,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
    tokenEndpoint: 'https://oauth2.googleapis.com/token',
    userInfoEndpoint: 'https://www.googleapis.com/oauth2/v3/userinfo',
    scopes: ['openid', 'profile', 'email'],
  },
  github: {
    clientId: process.env.GITHUB_CLIENT_ID!,
    clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    authorizationEndpoint: 'https://github.com/login/oauth/authorize',
    tokenEndpoint: 'https://github.com/login/oauth/access_token',
    userInfoEndpoint: 'https://api.github.com/user',
    scopes: ['user:email'],
  },
}

// Callback handler: link social account to internal user
async function handleOAuthCallback(
  provider: string,
  code: string,
  state: string
): Promise<{ user: User; session: Session }> {
  // Verify state matches stored state (CSRF protection)
  const storedState = await sessionStore.get(`oauth_state_${state}`)
  if (!storedState) throw new Error('Invalid state parameter')

  const config = OAUTH_PROVIDERS[provider]
  if (!config) throw new Error(`Unknown provider: ${provider}`)

  // Exchange code for tokens
  const tokens = await exchangeCodeForTokens(code, storedState.verifier, config)

  // Fetch user profile from provider
  const profile = await fetchUserProfile(config.userInfoEndpoint, tokens.access_token)

  // Find or create user (link social identity)
  let user = await db.user.findFirst({
    where: {
      socialAccounts: {
        some: { provider, providerUserId: profile.sub }
      }
    }
  })

  if (!user) {
    user = await db.user.create({
      data: {
        email: profile.email,
        displayName: profile.name,
        socialAccounts: {
          create: {
            provider,
            providerUserId: profile.sub,
            email: profile.email,
          }
        }
      }
    })
  }

  const session = await createSession(user.id)
  return { user, session }
}

Session Management

import { SignJWT, jwtVerify } from 'jose'

const SESSION_SECRET = new TextEncoder().encode(process.env.SESSION_SECRET!)

async function createSession(userId: string): Promise<string> {
  const sessionId = crypto.randomUUID()

  // Store session server-side (not just JWT)
  await db.session.create({
    data: {
      id: sessionId,
      userId,
      expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),  // 24h
      createdAt: new Date(),
    }
  })

  // Issue signed session token
  const token = await new SignJWT({ sub: userId, sid: sessionId })
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('24h')
    .sign(SESSION_SECRET)

  return token
}

// Set session cookie (httpOnly, secure, sameSite)
function setSessionCookie(res: Response, token: string): void {
  res.cookie('session', token, {
    httpOnly: true,       // Not accessible via JavaScript
    secure: true,         // HTTPS only
    sameSite: 'lax',      // CSRF protection
    maxAge: 24 * 60 * 60 * 1000,
    path: '/',
    domain: '.example.com',
  })
}

// Validate session middleware
async function validateSession(req: Request, res: Response, next: NextFunction) {
  const token = req.cookies.session
  if (!token) return res.status(401).json({ error: 'No session' })

  try {
    const { payload } = await jwtVerify(token, SESSION_SECRET)
    const session = await db.session.findUnique({ where: { id: payload.sid as string } })

    if (!session || session.expiresAt < new Date()) {
      return res.status(401).json({ error: 'Session expired' })
    }

    req.user = { id: payload.sub as string, sessionId: session.id }
    next()
  } catch {
    return res.status(401).json({ error: 'Invalid session' })
  }
}

Checklist

  • PKCE enabled for all public clients (SPA, mobile, CLI)
  • State parameter validated on callback (CSRF protection)
  • Tokens stored in httpOnly secure cookies (not localStorage)
  • Refresh tokens rotated on each use (detect token theft)
  • Session validated server-side (not just JWT expiry)
  • Social login links to internal user account (not provider-dependent)
  • Nonce in OIDC requests for replay protection
  • Logout: revoke tokens + clear session + clear cookies

Anti-Patterns

  • Storing tokens in localStorage (XSS exposes all tokens)
  • Implicit grant flow (deprecated, tokens in URL fragment)
  • Not validating state parameter: vulnerable to CSRF
  • Long-lived access tokens without refresh mechanism
  • Trusting JWT without server-side session validation (no revocation)
  • Hardcoding client secrets in frontend code (use backend proxy)

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

WCAG 2.2 AA compliance, ARIA patterns, keyboard navigation, screen reader optimization

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

vibeeval/vibecosystem5382026년 8월 9일 업데이트

axe-core integration, WCAG 2.2 AA checklist, keyboard navigation testing, screen reader testing, and ARIA pattern validation.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

vibeeval/vibecosystem5382026년 8월 9일 업데이트

Steam-style achievement system with XP, levels, streaks, and skill trees. Gamifies the development workflow. 25 achievements across 5 categories.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

vibeeval/vibecosystem5382026년 8월 9일 업데이트

Framework for measuring and tracking agent response quality over time. Detects regressions before they reach production. Use when evaluating agent changes, auditing quality, or establishing performance baselines.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

vibeeval/vibecosystem5382026년 8월 9일 업데이트

Agent Context Isolation

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

vibeeval/vibecosystem5382026년 8월 9일 업데이트

Agent ve skill dosyalarinin yapisal dogrulamasi. Frontmatter kontrol, naming convention, zorunlu bolum kontrolu, tutarlilik denetimi. Yeni agent/skill eklendiginde veya mevcut dosyalar duzenlediginde otomatik calistirilir.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

vibeeval/vibecosystem5382026년 8월 9일 업데이트

vibeeval의 스킬 모두 보기

이 스킬의 문제 신고