feat: Add OAuth2 server and client implementation with PKCE support
- Implemented OAuth2 server with client registration, authorization, and token endpoints. - Created HTML templates for client authorization, client creation, and client editing. - Developed an OAuth2 client application using Hono.js and Bun, supporting authorization code grant flow. - Integrated PKCE (Proof Key for Code Exchange) for enhanced security during authorization. - Added session management using cookies for user authentication. - Included detailed README documentation for setup and usage instructions.
This commit is contained in:
commit
7cd05b5c6a
29 changed files with 1962 additions and 0 deletions
34
oauth2-attacker/.gitignore
vendored
Normal file
34
oauth2-attacker/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
15
oauth2-attacker/README.md
Normal file
15
oauth2-attacker/README.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# oauth2-attacker
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
bun run index.ts
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.2.18. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
||||
34
oauth2-attacker/bun.lock
Normal file
34
oauth2-attacker/bun.lock
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "oauth2-attacker",
|
||||
"dependencies": {
|
||||
"hono": "^4.8.4",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="],
|
||||
|
||||
"@types/node": ["@types/node@24.0.13", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"hono": ["hono@4.8.4", "", {}, "sha512-KOIBp1+iUs0HrKztM4EHiB2UtzZDTBihDtOF5K6+WaJjCPeaW4Q92R8j63jOhvJI5+tZSMuKD9REVEXXY9illg=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||
}
|
||||
}
|
||||
108
oauth2-attacker/index.ts
Normal file
108
oauth2-attacker/index.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import { Hono } from 'hono'
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
// define config
|
||||
const OAUTH_CONFIG = {
|
||||
authServerUrl: 'http://localhost:3020',
|
||||
clientId: process.env.CLIENT_ID, // 클라이언트 등록 후 설정 필요
|
||||
clientSecret: process.env.CLIENT_SECRET, // 필요한 경우
|
||||
redirectUri: process.env.REDIRECT_URI,
|
||||
scope: 'profile'
|
||||
}
|
||||
|
||||
app.get('/', (c) => {
|
||||
|
||||
return c.html(`
|
||||
<h1>Attacker</h1>
|
||||
|
||||
<h2>Profile</h2>
|
||||
<form action="/profile" method="get">
|
||||
<input type="text" name="access_token" placeholder="Access Token" style="width:300px; padding:5px;" required />
|
||||
<button type="submit" style="padding:5px 10px;">프로필 보기</button>
|
||||
</form>
|
||||
`)
|
||||
})
|
||||
|
||||
|
||||
app.get("/profile", async (c) => {
|
||||
const accessToken = c.req.query('access_token')
|
||||
|
||||
try {
|
||||
// 바로 프로필 조회
|
||||
const profileResponse = await fetch(`${OAUTH_CONFIG.authServerUrl}/api/me`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
}
|
||||
})
|
||||
|
||||
if (!profileResponse.ok) {
|
||||
return c.html(`
|
||||
<h1>프로필 조회 실패</h1>
|
||||
<p>토큰은 발급되었지만 프로필 조회에 실패했습니다.</p>
|
||||
<p>상태 코드: ${profileResponse.status}</p>
|
||||
<a href="/">홈으로</a>
|
||||
`)
|
||||
}
|
||||
|
||||
const profile = await profileResponse.json() as any
|
||||
|
||||
// 토큰 정보와 프로필을 함께 표시
|
||||
return c.html(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>인증 완료 - 프로필</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 40px; }
|
||||
.container { max-width: 800px; margin: 0 auto; }
|
||||
.button { background: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block; margin: 10px 5px; }
|
||||
.success { color: #28a745; font-size: 18px; margin-bottom: 20px; }
|
||||
.section { background: #f8f9fa; padding: 20px; border-radius: 5px; margin: 20px 0; border-left: 4px solid #007bff; }
|
||||
.profile-item { margin: 10px 0; }
|
||||
.label { font-weight: bold; color: #495057; }
|
||||
.value { color: #212529; }
|
||||
.token-info { background: #e3f2fd; border-left-color: #2196f3; }
|
||||
.profile-info { background: #e8f5e8; border-left-color: #28a745; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="section profile-info">
|
||||
<h3>👤 사용자 프로필</h3>
|
||||
<div class="profile-item">
|
||||
<span class="label">사용자 ID:</span>
|
||||
<span class="value">${profile.id || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="profile-item">
|
||||
<span class="label">사용자명:</span>
|
||||
<span class="value">${profile.username || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="profile-item">
|
||||
<code>
|
||||
${JSON.stringify(profile, null, 2)}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-top: 30px;">
|
||||
<a href="/" class="button">홈으로</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
|
||||
} catch (error: any) {
|
||||
return c.html(`
|
||||
<h1>오류 발생</h1>
|
||||
<p>토큰 교환 또는 프로필 조회 중 오류가 발생했습니다: ${error?.message || '알 수 없는 오류'}</p>
|
||||
<a href="/">홈으로</a>
|
||||
`)
|
||||
}
|
||||
})
|
||||
|
||||
export default {
|
||||
port: 5002,
|
||||
fetch: app.fetch,
|
||||
}
|
||||
19
oauth2-attacker/package.json
Normal file
19
oauth2-attacker/package.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "oauth2-attacker",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "bun run --hot index.ts",
|
||||
"start": "bun run index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"hono": "^4.8.4"
|
||||
}
|
||||
}
|
||||
29
oauth2-attacker/tsconfig.json
Normal file
29
oauth2-attacker/tsconfig.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue