Skip to main content Skills Marketplace 커뮤니티가 만든 AI 스킬을 발견하고 탐색하세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/InugamiDev/ultrathink-oss --skill oauth명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
Zip 다운로드 다운로드 중... name oauth description OAuth 2.0 authorization code flow, PKCE, token refresh, OpenID Connect, social login providers, and security considerations layer domain category security triggers ["oauth","oauth2","authorization code","PKCE","openid connect","oidc","social login","google login","github login","token refresh","oauth flow","access token","refresh token"] inputs ["Application type (SPA, server-side, mobile, CLI)","Identity providers needed (Google, GitHub, Apple, custom)","Scopes required (profile, email, custom API)","Token storage strategy","Session management approach"] outputs ["OAuth flow implementation","PKCE challenge generation","Token refresh logic","Provider configuration","Security checklist"] linksTo ["authentication","better-auth","api-designer","encryption","owasp"] linkedFrom ["nextjs","react","nodejs"] preferredNextSkills ["authentication","better-auth"] fallbackSkills ["api-designer","encryption"] riskLevel high memoryReadPolicy selective memoryWritePolicy selective sideEffects ["May add OAuth client libraries","May create OAuth callback routes","May store tokens in database or cookies"]
OAuth Domain Skill
Purpose
OAuth 2.0 is the industry standard for delegated authorization. Most "Login with Google/GitHub" flows use OAuth + OpenID Connect. This skill covers implementing OAuth correctly and securely -- the protocol has many subtle pitfalls that create real security vulnerabilities when done wrong.
When to Use What
Flow Use Case PKCE? Client Secret? Authorization Code + PKCE SPAs, mobile apps, any public client Yes No Authorization Code Server-side apps with secret storage Optional Yes Client Credentials Machine-to-machine, no user involved No Yes Device Code CLI tools, smart TVs, no browser No Yes
Always use PKCE -- even for server-side apps, PKCE adds defense-in-depth at zero cost.
Never use : Implicit flow (deprecated), Resource Owner Password (deprecated).
Key Concepts
OAuth 2.0 vs OpenID Connect
OAuth 2.0:
- Authorization protocol (what can you access?)
- Returns access_token (opaque or JWT)
- For accessing APIs on behalf of user
OpenID Connect (OIDC):
- Authentication layer ON TOP of OAuth 2.0 (who are you?)
- Returns id_token (always JWT) + access_token
- For identifying the user (name, email, picture)
- Adds standardized scopes: openid, profile, email
- Adds userinfo endpoint
- Adds discovery document (.well-known/openid-configuration)
TL;DR: Use OIDC when you need to know WHO the user is.
Use plain OAuth when you just need API access.
The Authorization Code Flow (with PKCE)
1. Client generates code_verifier (random 43-128 chars)
2. Client computes code_challenge = SHA256(code_verifier), base64url-encoded
3. Client redirects user to authorization server:
GET /authorize?
response_type=code&
client_id=xxx&
redirect_uri=https://app.com/callback&
scope=openid profile email&
state=random_csrf_token&
code_challenge=abc123&
code_challenge_method=S256
4. User authenticates and consents
5. Authorization server redirects back:
GET /callback?code=AUTH_CODE&state=random_csrf_token
6. Client verifies state matches (CSRF protection)
7. Client exchanges code for tokens:
POST /token
grant_type=authorization_code&
code=AUTH_CODE&
redirect_uri=https://app.com/callback&
client_id=xxx&
code_verifier=original_verifier
8. Authorization server verifies:
SHA256(code_verifier) === stored code_challenge
Returns: { access_token, refresh_token, id_token, expires_in }
Patterns
1. PKCE Challenge Generation
import { randomBytes, createHash } from 'crypto' ;
export function generatePKCE ( ) {
const verifier = randomBytes (32 )
.toString ('base64url' )
.slice (0 , 64 );
const challenge = createHash ('sha256' )
.update (verifier)
.digest ('base64url' );
return { verifier, challenge };
}
export async function generatePKCEBrowser ( ) {
const array = new Uint8Array (32 );
crypto.getRandomValues (array);
const verifier = btoa (String .fromCharCode (...array))
.replace (/\+/g , '-' )
.replace (/\//g , '_' )
.replace (/=+$/ , '' )
.slice (0 , 64 );
const encoder = new TextEncoder ();
const data = encoder.encode (verifier);
const hash = await crypto.subtle .digest ('SHA-256' , data);
const challenge = btoa (String .fromCharCode (...new Uint8Array (hash)))
.replace (/\+/g , '-' )
.replace (/\//g , '_' )
.replace (/=+$/ , '' );
return { verifier, challenge };
}
2. Server-Side OAuth (Next.js Route Handlers)
interface OAuthProvider {
name : string ;
authorizationUrl : string ;
tokenUrl : string ;
userInfoUrl : string ;
clientId : string ;
clientSecret : string ;
scopes : string [];
}
export const providers : Record <string , OAuthProvider > = {
google : {
name : 'Google' ,
authorizationUrl : 'https://accounts.google.com/o/oauth2/v2/auth' ,
tokenUrl : 'https://oauth2.googleapis.com/token' ,
userInfoUrl : 'https://www.googleapis.com/oauth2/v3/userinfo' ,
clientId : process.env .GOOGLE_CLIENT_ID !,
clientSecret : process.env .GOOGLE_CLIENT_SECRET !,
scopes : ['openid' , 'profile' , 'email' ],
},
github : {
name : 'GitHub' ,
authorizationUrl : 'https://github.com/login/oauth/authorize' ,
tokenUrl : 'https://github.com/login/oauth/access_token' ,
userInfoUrl : 'https://api.github.com/user' ,
clientId : process.env .GITHUB_CLIENT_ID !,
clientSecret : process.env .GITHUB_CLIENT_SECRET !,
scopes : ['read:user' , 'user:email' ],
},
};
import { NextRequest , NextResponse } from 'next/server' ;
import { cookies } from 'next/headers' ;
import { generatePKCE } from '@/lib/oauth/pkce' ;
import { providers } from '@/lib/oauth/providers' ;
export async function GET (
request : NextRequest ,
{ params }: { params: Promise <{ provider: string }> }
) {
const { provider : providerName } = await params;
const provider = providers[providerName];
if (!provider) {
return NextResponse .json ({ error : 'Unknown provider' }, { status : 400 });
}
const { verifier, challenge } = generatePKCE ();
const state = crypto.randomUUID ();
const cookieStore = await cookies ();
cookieStore.set ('oauth_verifier' , verifier, {
httpOnly : true ,
secure : true ,
sameSite : 'lax' ,
maxAge : 600 ,
path : '/' ,
});
cookieStore.set ('oauth_state' , state, {
httpOnly : true ,
secure : true ,
sameSite : 'lax' ,
maxAge : 600 ,
path : '/' ,
});
const redirectUri = `${process.env.NEXT_PUBLIC_APP_URL} /api/auth/${providerName} /callback` ;
const authUrl = new URL (provider.authorizationUrl );
authUrl.searchParams .set ('response_type' , 'code' );
authUrl.searchParams .set ('client_id' , provider.clientId );
authUrl.searchParams .set ('redirect_uri' , redirectUri);
authUrl.searchParams .set ('scope' , provider.scopes .join (' ' ));
authUrl.searchParams .set ('state' , state);
authUrl.searchParams .set ('code_challenge' , challenge);
authUrl.searchParams .set ('code_challenge_method' , 'S256' );
if (providerName === 'google' ) {
authUrl.searchParams .set ('prompt' , 'select_account' );
}
return NextResponse .redirect (authUrl.toString ());
}
import { NextRequest , NextResponse } from 'next/server' ;
import { cookies } from 'next/headers' ;
import { providers } from '@/lib/oauth/providers' ;
export async function GET (
request : NextRequest ,
{ params }: { params: Promise <{ provider: string }> }
) {
const { provider : providerName } = await params;
const provider = providers[providerName];
if (!provider) {
return NextResponse .redirect (`${process.env.NEXT_PUBLIC_APP_URL} /login?error=unknown_provider` );
}
const searchParams = request.nextUrl .searchParams ;
const code = searchParams.get ('code' );
const state = searchParams.get ('state' );
const error = searchParams.get ('error' );
if (error) {
return NextResponse .redirect (
`${process.env.NEXT_PUBLIC_APP_URL} /login?error=${error} `
);
}
const cookieStore = await cookies ();
const storedState = cookieStore.get ('oauth_state' )?.value ;
const verifier = cookieStore.get ('oauth_verifier' )?.value ;
if (!state || !storedState || state !== storedState) {
return NextResponse .redirect (
`${process.env.NEXT_PUBLIC_APP_URL} /login?error=state_mismatch`
);
}
if (!code || !verifier) {
return NextResponse .redirect (
`${process.env.NEXT_PUBLIC_APP_URL} /login?error=missing_params`
);
}
cookieStore.delete ('oauth_state' );
cookieStore.delete ('oauth_verifier' );
const redirectUri = `${process.env.NEXT_PUBLIC_APP_URL} /api/auth/${providerName} /callback` ;
const tokenResponse = await fetch (provider.tokenUrl , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/x-www-form-urlencoded' ,
Accept : 'application/json' ,
},
body : new URLSearchParams ({
grant_type : 'authorization_code' ,
code,
redirect_uri : redirectUri,
client_id : provider.clientId ,
client_secret : provider.clientSecret ,
code_verifier : verifier,
}),
});
if (!tokenResponse.ok ) {
console .error ('Token exchange failed:' , await tokenResponse.text ());
return NextResponse .redirect (
`${process.env.NEXT_PUBLIC_APP_URL} /login?error=token_exchange_failed`
);
}
const tokens = await tokenResponse.json ();
const userResponse = await fetch (provider.userInfoUrl , {
headers : { Authorization : `Bearer ${tokens.access_token} ` },
});
const userInfo = await userResponse.json ();
const normalizedUser = normalizeUser (providerName, userInfo);
const { user, session } = await findOrCreateUser (normalizedUser, {
provider : providerName,
providerAccountId : normalizedUser.providerId ,
accessToken : tokens.access_token ,
refreshToken : tokens.refresh_token ,
expiresAt : tokens.expires_in
? Math .floor (Date .now () / 1000 ) + tokens.expires_in
: undefined ,
});
cookieStore.set ('session_token' , session.token , {
httpOnly : true ,
secure : true ,
sameSite : 'lax' ,
maxAge : 60 * 60 * 24 * 30 ,
path : '/' ,
});
return NextResponse .redirect (`${process.env.NEXT_PUBLIC_APP_URL} /dashboard` );
}
function normalizeUser (provider : string , raw : Record <string , unknown > ) {
switch (provider) {
case 'google' :
return {
providerId : raw.sub as string ,
email : raw.email as string ,
name : raw.name as string ,
avatarUrl : raw.picture as string ,
emailVerified : raw.email_verified as boolean ,
};
case 'github' :
return {
providerId : String (raw.id ),
email : raw.email as string ,
name : (raw.name ?? raw.login ) as string ,
avatarUrl : raw.avatar_url as string ,
emailVerified : true ,
};
default :
throw new Error (`Unknown provider: ${provider} ` );
}
}
3. Token Refresh
interface StoredTokens {
accessToken : string ;
refreshToken : string | null ;
expiresAt : number ;
provider : string ;
}
export async function getValidAccessToken (
accountId : string
): Promise <string > {
const account = await db.query .oauthAccounts .findFirst ({
where : eq (oauthAccounts.id , accountId),
});
if (!account) throw new Error ('Account not found' );
const now = Math .floor (Date .now () / 1000 );
if (account.expiresAt && account.expiresAt > now + 300 ) {
return account.accessToken ;
}
if (!account.refreshToken ) {
throw new Error ('Token expired and no refresh token available' );
}
const provider = providers[account.provider ];
const response = await fetch (provider.tokenUrl , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/x-www-form-urlencoded' ,
Accept : 'application/json' ,
},
body : new URLSearchParams ({
grant_type : 'refresh_token' ,
refresh_token : account.refreshToken ,
client_id : provider.clientId ,
client_secret : provider.clientSecret ,
}),
});
if (!response.ok ) {
await db.update (oauthAccounts)
.set ({ refreshToken : null })
.where (eq (oauthAccounts.id , accountId));
throw new Error ('Token refresh failed -- re-authentication required' );
}
const tokens = await response.json ();
await db.update (oauthAccounts)
.set ({
accessToken : tokens.access_token ,
refreshToken : tokens.refresh_token ?? account.refreshToken ,
expiresAt : tokens.expires_in
? Math .floor (Date .now () / 1000 ) + tokens.expires_in
: null ,
})
.where (eq (oauthAccounts.id , accountId));
return tokens.access_token ;
}
4. Device Authorization Flow (CLI Tools)
async function deviceCodeLogin (provider : OAuthProvider ) {
const deviceResponse = await fetch (provider.deviceAuthorizationUrl !, {
method : 'POST' ,
headers : { 'Content-Type' : 'application/x-www-form-urlencoded' },
body : new URLSearchParams ({
client_id : provider.clientId ,
scope : provider.scopes .join (' ' ),
}),
});
const {
device_code,
user_code,
verification_uri,
verification_uri_complete,
expires_in,
interval,
} = await deviceResponse.json ();
console .log (`\nOpen this URL: ${verification_uri} ` );
console .log (`Enter code: ${user_code} \n` );
const deadline = Date .now () + expires_in * 1000 ;
const pollInterval = (interval ?? 5 ) * 1000 ;
while (Date .now () < deadline) {
await new Promise ((r ) => setTimeout (r, pollInterval));
const tokenResponse = await fetch (provider.tokenUrl , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/x-www-form-urlencoded' },
body : new URLSearchParams ({
grant_type : 'urn:ietf:params:oauth:grant-type:device_code' ,
device_code,
client_id : provider.clientId ,
}),
});
const body = await tokenResponse.json ();
if (body.error === 'authorization_pending' ) continue ;
if (body.error === 'slow_down' ) {
await new Promise ((r ) => setTimeout (r, 5000 ));
continue ;
}
if (body.error ) throw new Error (`OAuth error: ${body.error} ` );
return body;
}
throw new Error ('Device code expired' );
}
5. OIDC Discovery
async function discoverOIDCProvider (issuer : string ) {
const discoveryUrl = `${issuer} /.well-known/openid-configuration` ;
const response = await fetch (discoveryUrl);
const config = await response.json ();
return {
authorizationEndpoint : config.authorization_endpoint ,
tokenEndpoint : config.token_endpoint ,
userInfoEndpoint : config.userinfo_endpoint ,
jwksUri : config.jwks_uri ,
issuer : config.issuer ,
supportedScopes : config.scopes_supported ,
supportedResponseTypes : config.response_types_supported ,
supportedGrantTypes : config.grant_types_supported ,
};
}
6. ID Token Validation
import { createRemoteJWKSet, jwtVerify } from 'jose' ;
const JWKS = createRemoteJWKSet (
new URL ('https://www.googleapis.com/oauth2/v3/certs' )
);
async function validateIdToken (idToken : string , expectedNonce ?: string ) {
const { payload } = await jwtVerify (idToken, JWKS , {
issuer : 'https://accounts.google.com' ,
audience : process.env .GOOGLE_CLIENT_ID ,
});
if (expectedNonce && payload.nonce !== expectedNonce) {
throw new Error ('Invalid nonce' );
}
if (payload.exp && payload.exp < Math .floor (Date .now () / 1000 )) {
throw new Error ('ID token expired' );
}
return {
sub : payload.sub !,
email : payload.email as string ,
name : payload.name as string ,
picture : payload.picture as string ,
emailVerified : payload.email_verified as boolean ,
};
}
Security Checklist [x] Use Authorization Code flow with PKCE (never Implicit)
[x] Validate state parameter on callback (CSRF protection)
[x] Use code_challenge_method=S256 (never plain)
[x] Store tokens server-side (never in localStorage)
[x] Set cookies: httpOnly, secure, sameSite=lax
[x] Validate id_token signature and claims (issuer, audience, expiry)
[x] Use nonce for replay protection in OIDC flows
[x] Validate redirect_uri exactly matches registered URI
[x] Store client_secret server-side only (never in client code)
[x] Implement token refresh with rotation detection
[x] Handle revoked refresh tokens gracefully (re-auth flow)
[x] Use short-lived access tokens (< 1 hour)
[x] Log all authentication events for audit trail
[x] Rate-limit token exchange endpoint
Best Practices
Always use PKCE -- it is required for public clients and recommended for all clients
Validate state on every callback -- this is your CSRF protection
Store tokens in httpOnly cookies -- never localStorage (XSS vulnerable)
Use the jose library for JWT operations -- well-maintained, standards-compliant
Fetch user info from the userinfo endpoint -- do not solely trust the id_token for mutable data
Support account linking -- let users connect multiple OAuth providers to one account
Handle provider downtime -- show clear error messages, offer alternative login methods
Request minimal scopes -- ask only for what you need; users abandon consent screens with many scopes
Use OIDC discovery -- auto-configure endpoints from .well-known/openid-configuration
Rotate refresh tokens -- detect token reuse (indicates theft) and revoke all tokens for that grant
Common Pitfalls Pitfall Impact Fix Not validating state param CSRF attacks -- attacker can link their account to victim Always verify state matches stored value Storing tokens in localStorage XSS can steal all tokens Use httpOnly secure cookies Using Implicit flow Tokens in URL fragment, no refresh Use Authorization Code + PKCE Not checking iss and aud in ID token Token confusion attacks Validate issuer and audience on every ID token Hardcoding redirect URIs Breaks in different environments Use environment variables, validate against allow-list Not handling refresh token rotation Stale refresh tokens cause logout Store new refresh token when provider rotates Missing prompt=consent for scope changes Users denied new permissions silently Force consent when requesting additional scopes No CSRF on login initiation Login CSRF -- attacker forces victim to log into attacker's account Use state parameter + validate on callback
Unified design foundations — design system architecture, tokens, component specs, visual principles, creative vision, figma integration, plus brand design system loader (66 real brands via DESIGN.md). Absorbs design, design-system, design-systems, design-principles, design-router, creative-vision, figma, design-md.