{"id":"87c94bca-2887-4e95-9716-f72f3d851649","shortId":"WZtdaC","kind":"skill","title":"auth-security","tagline":"OAuth 2.1 + JWT authentication security best practices. Use when implementing auth, API authorization, token management. Follows RFC 9700 (2025).","description":"# Auth Security\n\n## Core Principles\n\n- **OAuth 2.1** — Follow RFC 9700 (January 2025)\n- **PKCE Required** — All clients must use PKCE\n- **Short-lived Tokens** — Access tokens expire in 5-15 minutes\n- **Token Rotation** — Refresh tokens are single-use\n- **HttpOnly Storage** — Browser tokens in HttpOnly cookies\n- **Explicit Algorithm** — Never trust JWT header algorithm\n- **No backwards compatibility** — Delete deprecated auth flows\n\n---\n\n## OAuth 2.1 Key Changes\n\n### Deprecated Flows (DO NOT USE)\n\n| Flow | Status | Replacement |\n|------|--------|-------------|\n| Implicit Grant | Removed | Authorization Code + PKCE |\n| Password Grant | Removed | Authorization Code + PKCE |\n| Auth Code without PKCE | Removed | Must use PKCE |\n\n### Required: Authorization Code + PKCE\n\n```typescript\nimport crypto from 'crypto';\n\n// 1. Generate code verifier (43-128 chars)\nfunction generateCodeVerifier(): string {\n  return crypto.randomBytes(32).toString('base64url');\n}\n\n// 2. Generate code challenge\nfunction generateCodeChallenge(verifier: string): string {\n  return crypto\n    .createHash('sha256')\n    .update(verifier)\n    .digest('base64url');\n}\n\n// 3. Authorization request\nconst verifier = generateCodeVerifier();\nconst challenge = generateCodeChallenge(verifier);\n\nconst authUrl = new URL('https://auth.example.com/authorize');\nauthUrl.searchParams.set('response_type', 'code');\nauthUrl.searchParams.set('client_id', CLIENT_ID);\nauthUrl.searchParams.set('redirect_uri', REDIRECT_URI);\nauthUrl.searchParams.set('code_challenge', challenge);\nauthUrl.searchParams.set('code_challenge_method', 'S256');\nauthUrl.searchParams.set('scope', 'openid profile email');\nauthUrl.searchParams.set('state', generateState());\n\n// 4. Token exchange (after redirect)\nconst tokenResponse = await fetch('https://auth.example.com/token', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n  body: new URLSearchParams({\n    grant_type: 'authorization_code',\n    code: authorizationCode,\n    redirect_uri: REDIRECT_URI,\n    client_id: CLIENT_ID,\n    code_verifier: verifier, // Prove we initiated the request\n  }),\n});\n```\n\n---\n\n## JWT Best Practices\n\n### Algorithm Selection (2025)\n\n| Priority | Algorithm | Notes |\n|----------|-----------|-------|\n| 1 | EdDSA (Ed25519) | Most secure, quantum-resistant properties |\n| 2 | ES256 (ECDSA P-256) | Widely supported, compact signatures |\n| 3 | PS256 (RSA-PSS) | More secure than RS256 |\n| 4 | RS256 (RSA PKCS#1) | Best compatibility |\n\n```typescript\n// Recommended: ES256\nimport { SignJWT, jwtVerify } from 'jose';\n\nconst privateKey = await importPKCS8(PRIVATE_KEY_PEM, 'ES256');\nconst publicKey = await importSPKI(PUBLIC_KEY_PEM, 'ES256');\n\n// Sign\nconst token = await new SignJWT({ sub: userId, scope: 'read write' })\n  .setProtectedHeader({ alg: 'ES256', typ: 'JWT', kid: keyId })\n  .setIssuer('https://auth.example.com')\n  .setAudience('https://api.example.com')\n  .setExpirationTime('15m')\n  .setIssuedAt()\n  .setJti(crypto.randomUUID())\n  .sign(privateKey);\n```\n\n### Token Structure\n\n```typescript\ninterface AccessTokenPayload {\n  // Standard claims\n  iss: string;  // Issuer\n  sub: string;  // Subject (user ID)\n  aud: string;  // Audience\n  exp: number;  // Expiration (Unix timestamp)\n  iat: number;  // Issued at\n  jti: string;  // JWT ID (unique identifier)\n\n  // Custom claims\n  scope: string;      // Permissions\n  email?: string;     // User email\n  roles?: string[];   // User roles\n}\n```\n\n### Verification (Critical)\n\n```typescript\nimport { jwtVerify, errors } from 'jose';\n\nasync function verifyAccessToken(token: string): Promise<AccessTokenPayload> {\n  try {\n    const { payload } = await jwtVerify(token, publicKey, {\n      // CRITICAL: Explicitly specify allowed algorithms\n      algorithms: ['ES256'],\n\n      // Validate standard claims\n      issuer: 'https://auth.example.com',\n      audience: 'https://api.example.com',\n\n      // Clock tolerance for sync issues\n      clockTolerance: 30,\n    });\n\n    // Additional validation\n    if (!payload.scope?.includes('read')) {\n      throw new Error('Insufficient scope');\n    }\n\n    return payload as AccessTokenPayload;\n  } catch (err) {\n    if (err instanceof errors.JWTExpired) {\n      throw new AuthError('Token expired', 'TOKEN_EXPIRED');\n    }\n    if (err instanceof errors.JWTClaimValidationFailed) {\n      throw new AuthError('Invalid token claims', 'INVALID_CLAIMS');\n    }\n    throw new AuthError('Invalid token', 'INVALID_TOKEN');\n  }\n}\n```\n\n---\n\n## Token Storage\n\n### Web Applications\n\n```typescript\n// Set token in HttpOnly cookie (server-side)\nfunction setAuthCookie(res: Response, token: string) {\n  res.cookie('access_token', token, {\n    httpOnly: true,     // Not accessible via JavaScript\n    secure: true,       // HTTPS only\n    sameSite: 'strict', // CSRF protection\n    maxAge: 15 * 60 * 1000, // 15 minutes\n    path: '/api',       // Only sent to API routes\n  });\n}\n\n// Refresh token (longer-lived)\nfunction setRefreshCookie(res: Response, token: string) {\n  res.cookie('refresh_token', token, {\n    httpOnly: true,\n    secure: true,\n    sameSite: 'strict',\n    maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days\n    path: '/api/auth/refresh',  // Only for refresh endpoint\n  });\n}\n```\n\n### Single Page Applications (SPA)\n\n```typescript\n// Store in memory (NOT localStorage/sessionStorage)\nclass TokenManager {\n  private accessToken: string | null = null;\n\n  setToken(token: string) {\n    this.accessToken = token;\n  }\n\n  getToken(): string | null {\n    return this.accessToken;\n  }\n\n  clearToken() {\n    this.accessToken = null;\n  }\n}\n\n// Use with Refresh Token Rotation\n// Refresh token in HttpOnly cookie\n// Access token in memory\n```\n\n### Storage Comparison\n\n| Storage | XSS Safe | CSRF Safe | Persistence |\n|---------|----------|-----------|-------------|\n| HttpOnly Cookie | Yes | Needs SameSite | Yes |\n| Memory | Yes | Yes | No (lost on reload) |\n| localStorage | No | Yes | Yes |\n| sessionStorage | No | Yes | Tab only |\n\n---\n\n## Refresh Token Rotation\n\n### Flow\n\n```\n1. Client sends refresh_token\n2. Server validates refresh_token\n3. Server generates NEW access_token + NEW refresh_token\n4. Server INVALIDATES old refresh_token\n5. Server returns new tokens\n6. Client stores new tokens\n```\n\n### Implementation\n\n```typescript\nasync function refreshTokens(refreshToken: string) {\n  // Find token in database\n  const stored = await db.refreshToken.findUnique({\n    where: { token: hashToken(refreshToken) },\n    include: { user: true },\n  });\n\n  if (!stored) {\n    throw new AuthError('Invalid refresh token', 'INVALID_TOKEN');\n  }\n\n  // Check if already used (reuse detection)\n  if (stored.usedAt) {\n    // Potential token theft - revoke ALL user tokens\n    await db.refreshToken.deleteMany({\n      where: { userId: stored.userId },\n    });\n\n    // Alert security team\n    await alertSecurityTeam({\n      event: 'REFRESH_TOKEN_REUSE',\n      userId: stored.userId,\n      tokenId: stored.id,\n    });\n\n    throw new AuthError('Token reuse detected', 'TOKEN_REUSE');\n  }\n\n  // Check expiration\n  if (stored.expiresAt < new Date()) {\n    throw new AuthError('Refresh token expired', 'TOKEN_EXPIRED');\n  }\n\n  // Mark as used (but keep for reuse detection)\n  await db.refreshToken.update({\n    where: { id: stored.id },\n    data: { usedAt: new Date() },\n  });\n\n  // Generate new tokens\n  const newAccessToken = await generateAccessToken(stored.user);\n  const newRefreshToken = await generateRefreshToken(stored.user);\n\n  // Store new refresh token\n  await db.refreshToken.create({\n    data: {\n      token: hashToken(newRefreshToken),\n      userId: stored.userId,\n      expiresAt: addDays(new Date(), 7),\n      previousTokenId: stored.id, // Chain for audit\n    },\n  });\n\n  return {\n    accessToken: newAccessToken,\n    refreshToken: newRefreshToken,\n  };\n}\n```\n\n---\n\n## Attack Prevention\n\n### Algorithm Confusion\n\n```typescript\n// WRONG: Trusts header algorithm\njwt.verify(token, key); // Uses alg from header\n\n// CORRECT: Explicit algorithm\njwt.verify(token, key, { algorithms: ['ES256'] });\n```\n\n### CSRF Protection\n\n```typescript\n// Use SameSite cookies\nres.cookie('session', token, {\n  sameSite: 'strict', // or 'lax' for cross-site links\n});\n\n// Or double-submit cookie pattern\nconst csrfToken = crypto.randomBytes(32).toString('hex');\nres.cookie('csrf', csrfToken, { httpOnly: false });\n// Client sends csrf token in header\n```\n\n### XSS Protection\n\n```typescript\n// Content Security Policy\nres.setHeader('Content-Security-Policy', [\n  \"default-src 'self'\",\n  \"script-src 'self'\",\n  \"style-src 'self' 'unsafe-inline'\",\n].join('; '));\n\n// Use HttpOnly cookies for tokens\n// Never store tokens in localStorage\n```\n\n### Token Binding (DPoP)\n\n```typescript\n// Demonstration of Proof of Possession\n// Bind token to client's key pair\n\nconst dpopProof = await new SignJWT({\n  htm: 'POST',\n  htu: 'https://api.example.com/resource',\n  ath: await hashAccessToken(accessToken), // Access token hash\n})\n  .setProtectedHeader({ alg: 'ES256', typ: 'dpop+jwt', jwk: publicKey })\n  .setJti(crypto.randomUUID())\n  .setIssuedAt()\n  .sign(privateKey);\n\n// Send with request\nfetch('https://api.example.com/resource', {\n  headers: {\n    Authorization: `DPoP ${accessToken}`,\n    DPoP: dpopProof,\n  },\n});\n```\n\n---\n\n## Token Revocation\n\n```typescript\n// Revoke all user tokens (e.g., password change, logout all)\nasync function revokeAllUserTokens(userId: string) {\n  await db.refreshToken.deleteMany({\n    where: { userId },\n  });\n\n  // If using token blacklist for access tokens\n  await redis.sadd(`revoked:${userId}`, Date.now());\n  await redis.expire(`revoked:${userId}`, 15 * 60); // 15 min (access token lifetime)\n}\n\n// Check blacklist during verification\nasync function isTokenRevoked(userId: string, iat: number): Promise<boolean> {\n  const revokedAt = await redis.get(`revoked:${userId}`);\n  return revokedAt && parseInt(revokedAt) > iat * 1000;\n}\n```\n\n---\n\n## Checklist\n\n```markdown\n## OAuth 2.1\n- [ ] Using Authorization Code flow\n- [ ] PKCE enabled for all clients\n- [ ] No implicit or password grants\n- [ ] Redirect URI exact matching\n\n## JWT\n- [ ] Using ES256 or EdDSA algorithm\n- [ ] Explicit algorithm verification\n- [ ] Short expiration (≤15 min)\n- [ ] Unique jti for each token\n- [ ] Issuer and audience validation\n\n## Tokens\n- [ ] HttpOnly cookies for web apps\n- [ ] Refresh token rotation enabled\n- [ ] Reuse detection implemented\n- [ ] Token revocation mechanism\n\n## Security\n- [ ] HTTPS everywhere\n- [ ] SameSite cookies\n- [ ] CSP headers configured\n- [ ] Rate limiting on auth endpoints\n- [ ] Brute force protection\n```\n\n---\n\n## See Also\n\n- [reference/oauth2.1.md](reference/oauth2.1.md) — OAuth 2.1 deep dive\n- [reference/jwt.md](reference/jwt.md) — JWT patterns\n- [reference/attacks.md](reference/attacks.md) — Attack prevention","tags":["auth","security","claude","arsenal","majiayu000","agent-skills","ai-agents","ai-coding-assistant","automation","claude-code","code-review","developer-tools"],"capabilities":["skill","source-majiayu000","skill-auth-security","topic-agent-skills","topic-ai-agents","topic-ai-coding-assistant","topic-automation","topic-claude","topic-claude-code","topic-code-review","topic-developer-tools","topic-devops","topic-productivity","topic-prompt-engineering","topic-python"],"categories":["claude-arsenal"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/majiayu000/claude-arsenal/auth-security","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add majiayu000/claude-arsenal","source_repo":"https://github.com/majiayu000/claude-arsenal","install_from":"skills.sh"}},"qualityScore":"0.464","qualityRationale":"deterministic score 0.46 from registry signals: · indexed on github topic:agent-skills · 29 github stars · SKILL.md body (10,782 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-05-01T07:01:12.321Z","embedding":null,"createdAt":"2026-04-18T22:24:00.061Z","updatedAt":"2026-05-01T07:01:12.321Z","lastSeenAt":"2026-05-01T07:01:12.321Z","tsv":"'-128':127 '-15':50 '-256':268 '/api':521 '/api/auth/refresh':557 '/authorize'');':170 '/resource'',':947,974 '/token'',':213 '1':122,255,286,640 '1000':517,553,1048 '15':515,518,1018,1020,1082 '15m':336 '2':137,264,645 '2.1':5,28,82,1052,1130 '2025':22,33,251 '24':550 '3':154,273,650 '30':429 '32':134,870 '4':202,282,659 '43':126 '5':49,665 '6':670 '60':516,551,552,1019 '7':549,554,808 '9700':21,31 'access':45,497,503,602,654,952,1007,1022 'accesstoken':575,815,951,978 'accesstokenpayload':346,444 'addday':805 'addit':430 'alert':727 'alertsecurityteam':731 'alg':325,832,956 'algorithm':68,73,249,253,413,414,821,827,837,841,1076,1078 'allow':412 'alreadi':709 'also':1126 'api':15,525 'api.example.com':334,422,946,973 'api.example.com/resource'',':945,972 'app':1098 'applic':480,564 'application/x-www-form-urlencoded':220 'async':396,677,993,1029 'ath':948 'attack':819,1139 'aud':357 'audienc':359,421,1091 'audit':813 'auth':2,14,23,79,105,1120 'auth-secur':1 'auth.example.com':169,212,332,420 'auth.example.com/authorize'');':168 'auth.example.com/token'',':211 'authent':7 'autherror':453,464,472,701,742,756 'author':16,96,102,114,155,226,976,1054 'authorizationcod':229 'authurl':165 'authurl.searchparams.set':171,175,180,185,189,194,199 'await':209,299,307,316,405,688,722,730,770,784,789,796,939,949,998,1009,1014,1039 'backward':75 'base64url':136,153 'best':9,247,287 'bind':922,930 'blacklist':1005,1026 'bodi':221 'browser':62 'brute':1122 'catch':445 'chain':811 'challeng':140,161,187,188,191 'chang':84,990 'char':128 'check':707,748,1025 'checklist':1049 'claim':348,376,418,467,469 'class':572 'cleartoken':589 'client':37,176,178,234,236,641,671,878,933,1061 'clock':423 'clocktoler':428 'code':97,103,106,115,124,139,174,186,190,227,228,238,1055 'compact':271 'comparison':607 'compat':76,288 'configur':1116 'confus':822 'const':157,160,164,207,297,305,314,403,686,782,787,867,937,1037 'content':218,887,892 'content-security-polici':891 'content-typ':217 'cooki':66,486,601,615,848,865,913,1095,1113 'core':25 'correct':835 'createhash':148 'critic':389,409 'cross':858 'cross-sit':857 'crypto':119,121,147 'crypto.randombytes':133,869 'crypto.randomuuid':339,964 'csp':1114 'csrf':512,611,843,874,880 'csrftoken':868,875 'custom':375 'data':775,798 'databas':685 'date':753,778,807 'date.now':1013 'day':555 'db.refreshtoken.create':797 'db.refreshtoken.deletemany':723,999 'db.refreshtoken.findunique':689 'db.refreshtoken.update':771 'deep':1131 'default':896 'default-src':895 'delet':77 'demonstr':925 'deprec':78,85 'detect':712,745,769,1104 'digest':152 'dive':1132 'doubl':863 'double-submit':862 'dpop':923,959,977,979 'dpopproof':938,980 'e.g':988 'ecdsa':266 'ed25519':257 'eddsa':256,1075 'email':198,380,383 'enabl':1058,1102 'endpoint':561,1121 'err':446,448,459 'error':393,438 'errors.jwtclaimvalidationfailed':461 'errors.jwtexpired':450 'es256':265,291,304,312,326,415,842,957,1073 'event':732 'everywher':1111 'exact':1069 'exchang':204 'exp':360 'expir':47,362,455,457,749,759,761,1081 'expiresat':804 'explicit':67,410,836,1077 'fals':877 'fetch':210,971 'find':682 'flow':80,86,90,639,1056 'follow':19,29 'forc':1123 'function':129,141,397,490,532,678,994,1030 'generat':123,138,652,779 'generateaccesstoken':785 'generatecodechalleng':142,162 'generatecodeverifi':130,159 'generaterefreshtoken':790 'generatest':201 'gettoken':584 'grant':94,100,224,1066 'hash':954 'hashaccesstoken':950 'hashtoken':692,800 'header':72,216,826,834,883,975,1115 'hex':872 'htm':942 'httpon':60,65,485,500,542,600,614,876,912,1094 'https':508,1110 'htu':944 'iat':365,1034,1047 'id':177,179,235,237,356,372,773 'identifi':374 'implement':13,675,1105 'implicit':93,1063 'import':118,292,391 'importpkcs8':300 'importspki':308 'includ':434,694 'initi':243 'inlin':909 'instanceof':449,460 'insuffici':439 'interfac':345 'invalid':465,468,473,475,661,702,705 'iss':349 'issu':367,427 'issuer':351,419,1089 'istokenrevok':1031 'januari':32 'javascript':505 'join':910 'jose':296,395 'jti':369,1085 'jwk':961 'jwt':6,71,246,328,371,960,1071,1135 'jwt.verify':828,838 'jwtverifi':294,392,406 'keep':766 'key':83,302,310,830,840,935 'keyid':330 'kid':329 'lax':855 'lifetim':1024 'limit':1118 'link':860 'live':43,531 'localstorag':627,920 'localstorage/sessionstorage':571 'logout':991 'longer':530 'longer-liv':529 'lost':624 'manag':18 'mark':762 'markdown':1050 'match':1070 'maxag':514,548 'mechan':1108 'memori':569,605,620 'method':192,214 'min':1021,1083 'minut':51,519 'must':38,110 'need':617 'never':69,916 'new':166,222,317,437,452,463,471,653,656,668,673,700,741,752,755,777,780,793,806,940 'newaccesstoken':783,816 'newrefreshtoken':788,801,818 'note':254 'null':577,578,586,591 'number':361,366,1035 'oauth':4,27,81,1051,1129 'old':662 'openid':196 'p':267 'page':563 'pair':936 'parseint':1045 'password':99,989,1065 'path':520,556 'pattern':866,1136 'payload':404,442 'payload.scope':433 'pem':303,311 'permiss':379 'persist':613 'pkce':34,40,98,104,108,112,116,1057 'pkcs':285 'polici':889,894 'possess':929 'post':215,943 'potenti':715 'practic':10,248 'prevent':820,1140 'previoustokenid':809 'principl':26 'prioriti':252 'privat':301,574 'privatekey':298,341,967 'profil':197 'promis':401,1036 'proof':927 'properti':263 'protect':513,844,885,1124 'prove':241 'ps256':274 'pss':277 'public':309 'publickey':306,408,962 'quantum':261 'quantum-resist':260 'rate':1117 'read':322,435 'recommend':290 'redirect':181,183,206,230,232,1067 'redis.expire':1015 'redis.get':1040 'redis.sadd':1010 'reference/attacks.md':1137,1138 'reference/jwt.md':1133,1134 'reference/oauth2.1.md':1127,1128 'refresh':54,527,539,560,594,597,636,643,648,657,663,703,733,757,794,1099 'refreshtoken':679,680,693,817 'reload':626 'remov':95,101,109 'replac':92 'request':156,245,970 'requir':35,113 'res':492,534 'res.cookie':496,538,849,873 'res.setheader':890 'resist':262 'respons':172,493,535 'return':132,146,441,587,667,814,1043 'reus':711,735,744,747,768,1103 'revoc':982,1107 'revok':718,984,1011,1016,1041 'revokeallusertoken':995 'revokedat':1038,1044,1046 'rfc':20,30 'role':384,387 'rotat':53,596,638,1101 'rout':526 'rs256':281,283 'rsa':276,284 'rsa-pss':275 's256':193 'safe':610,612 'samesit':510,546,618,847,852,1112 'scope':195,321,377,440 'script':900 'script-src':899 'secur':3,8,24,259,279,506,544,728,888,893,1109 'see':1125 'select':250 'self':898,902,906 'send':642,879,968 'sent':523 'server':488,646,651,660,666 'server-sid':487 'session':850 'sessionstorag':631 'set':482 'setaudi':333 'setauthcooki':491 'setexpirationtim':335 'setissu':331 'setissuedat':337,965 'setjti':338,963 'setprotectedhead':324,955 'setrefreshcooki':533 'settoken':579 'sha256':149 'short':42,1080 'short-liv':41 'side':489 'sign':313,340,966 'signatur':272 'signjwt':293,318,941 'singl':58,562 'single-us':57 'site':859 'skill' 'skill-auth-security' 'source-majiayu000' 'spa':565 'specifi':411 'src':897,901,905 'standard':347,417 'state':200 'status':91 'storag':61,478,606,608 'store':567,672,687,698,792,917 'stored.expiresat':751 'stored.id':739,774,810 'stored.usedat':714 'stored.user':786,791 'stored.userid':726,737,803 'strict':511,547,853 'string':131,144,145,350,353,358,370,378,381,385,400,495,537,576,581,585,681,997,1033 'structur':343 'style':904 'style-src':903 'sub':319,352 'subject':354 'submit':864 'support':270 'sync':426 'tab':634 'team':729 'theft':717 'this.accesstoken':582,588,590 'throw':436,451,462,470,699,740,754 'timestamp':364 'token':17,44,46,52,55,63,203,315,342,399,407,454,456,466,474,476,477,483,494,498,499,528,536,540,541,580,583,595,598,603,637,644,649,655,658,664,669,674,683,691,704,706,716,721,734,743,746,758,760,781,795,799,829,839,851,881,915,918,921,931,953,981,987,1004,1008,1023,1088,1093,1100,1106 'tokenid':738 'tokenmanag':573 'tokenrespons':208 'toler':424 'topic-agent-skills' 'topic-ai-agents' 'topic-ai-coding-assistant' 'topic-automation' 'topic-claude' 'topic-claude-code' 'topic-code-review' 'topic-developer-tools' 'topic-devops' 'topic-productivity' 'topic-prompt-engineering' 'topic-python' 'tostr':135,871 'tri':402 'true':501,507,543,545,696 'trust':70,825 'typ':327,958 'type':173,219,225 'typescript':117,289,344,390,481,566,676,823,845,886,924,983 'uniqu':373,1084 'unix':363 'unsaf':908 'unsafe-inlin':907 'updat':150 'uri':182,184,231,233,1068 'url':167 'urlsearchparam':223 'use':11,39,59,89,111,592,710,764,831,846,911,1003,1053,1072 'usedat':776 'user':355,382,386,695,720,986 'userid':320,725,736,802,996,1001,1012,1017,1032,1042 'valid':416,431,647,1092 'verif':388,1028,1079 'verifi':125,143,151,158,163,239,240 'verifyaccesstoken':398 'via':504 'web':479,1097 'wide':269 'without':107 'write':323 'wrong':824 'xss':609,884 'yes':616,619,621,622,629,630,633","prices":[{"id":"859a8a32-d567-494c-b00d-a66ebfcc380c","listingId":"87c94bca-2887-4e95-9716-f72f3d851649","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"majiayu000","category":"claude-arsenal","install_from":"skills.sh"},"createdAt":"2026-04-18T22:24:00.061Z"}],"sources":[{"listingId":"87c94bca-2887-4e95-9716-f72f3d851649","source":"github","sourceId":"majiayu000/claude-arsenal/auth-security","sourceUrl":"https://github.com/majiayu000/claude-arsenal/tree/main/skills/auth-security","isPrimary":false,"firstSeenAt":"2026-04-18T22:24:00.061Z","lastSeenAt":"2026-05-01T07:01:12.321Z"}],"details":{"listingId":"87c94bca-2887-4e95-9716-f72f3d851649","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"majiayu000","slug":"auth-security","github":{"repo":"majiayu000/claude-arsenal","stars":29,"topics":["agent-skills","ai-agents","ai-coding-assistant","automation","claude","claude-code","code-review","developer-tools","devops","productivity","prompt-engineering","python","software-development","typescript","workflows"],"license":"mit","html_url":"https://github.com/majiayu000/claude-arsenal","pushed_at":"2026-04-29T04:12:22Z","description":"52 production-ready Claude Code skills and 7 specialized agents for software development, DevOps, product workflows, and automation.","skill_md_sha":"2cef89f360f74810fdab59226d29be155aa579d0","skill_md_path":"skills/auth-security/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/majiayu000/claude-arsenal/tree/main/skills/auth-security"},"layout":"multi","source":"github","category":"claude-arsenal","frontmatter":{"name":"auth-security","description":"OAuth 2.1 + JWT authentication security best practices. Use when implementing auth, API authorization, token management. Follows RFC 9700 (2025)."},"skills_sh_url":"https://skills.sh/majiayu000/claude-arsenal/auth-security"},"updatedAt":"2026-05-01T07:01:12.321Z"}}