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:
암냥 2025-07-13 17:08:55 +09:00
commit 7cd05b5c6a
29 changed files with 1962 additions and 0 deletions

View file

@ -0,0 +1,22 @@
<p>The application <strong>{{grant.client.client_name}}</strong> is requesting:
<strong>{{ grant.request.scope }}</strong>
</p>
<p>
from You - a.k.a. <strong>{{ user.username }}</strong>
</p>
<form action="" method="post">
<label>
<input type="checkbox" name="confirm">
<span>Consent?</span>
</label>
{% if not user %}
<p>You haven't logged in. Log in with:</p>
<div>
<input type="text" name="username">
</div>
{% endif %}
<br>
<button>Submit</button>
</form>

View file

@ -0,0 +1,42 @@
<style>
label, label > span { display: block; }
label { margin: 15px 0; }
</style>
<a href="/">Home</a>
<form action="" method="post">
<label>
<span>Client Name</span>
<input type="text" name="client_name">
</label>
<label>
<span>Client URI</span>
<input type="url" name="client_uri">
</label>
<label>
<span>Allowed Scope</span>
<input type="text" name="scope">
</label>
<label>
<span>Redirect URIs</span>
<textarea name="redirect_uri" cols="30" rows="10"></textarea>
</label>
<label>
<span>Allowed Grant Types</span>
<textarea name="grant_type" cols="30" rows="10"></textarea>
</label>
<label>
<span>Allowed Response Types</span>
<textarea name="response_type" cols="30" rows="10"></textarea>
</label>
<label>
<span>Token Endpoint Auth Method</span>
<select name="token_endpoint_auth_method">
<option value="client_secret_basic">client_secret_basic</option>
<option value="client_secret_post">client_secret_post</option>
<option value="none">none</option>
</select>
</label>
<button>Submit</button>
</form>

View file

@ -0,0 +1,120 @@
<!DOCTYPE html>
<html>
<head>
<title>OAuth Client 수정</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input[type="text"], input[type="url"], textarea, select {
width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;
}
textarea { height: 60px; resize: vertical; }
button {
background-color: #007bff; color: white; padding: 10px 20px;
border: none; border-radius: 4px; cursor: pointer; margin-right: 10px;
}
button:hover { background-color: #0056b3; }
.danger { background-color: #dc3545; }
.danger:hover { background-color: #c82333; }
.client-info {
background-color: #f8f9fa; padding: 15px; margin-bottom: 20px;
border-radius: 4px; border: 1px solid #dee2e6;
}
.checkbox-group { display: flex; align-items: center; }
.checkbox-group input[type="checkbox"] { width: auto; margin-right: 10px; }
</style>
</head>
<body>
<h1>OAuth Client 수정</h1>
<div class="client-info">
<h3>현재 클라이언트 정보</h3>
<p><strong>Client ID:</strong> {{ client.client_id }}</p>
<p><strong>Client Secret:</strong>
{% if client.client_secret %}
{{ client.client_secret[:8] }}...
{% else %}
(없음 - Public Client)
{% endif %}
</p>
<p><strong>생성일:</strong> {{ client.client_id_issued_at }}</p>
</div>
<form method="post">
<div class="form-group">
<label for="client_name">클라이언트 이름:</label>
<input type="text" id="client_name" name="client_name"
value="{{ client.client_metadata.get('client_name', '') }}" required>
</div>
<div class="form-group">
<label for="client_uri">클라이언트 URI:</label>
<input type="url" id="client_uri" name="client_uri"
value="{{ client.client_metadata.get('client_uri', '') }}">
</div>
<div class="form-group">
<label for="redirect_uri">Redirect URIs (각 줄에 하나씩):</label>
<textarea id="redirect_uri" name="redirect_uri" required>{% for uri in client.client_metadata.get('redirect_uris', []) %}{{ uri }}
{% endfor %}</textarea>
</div>
<div class="form-group">
<label for="scope">Scope:</label>
<input type="text" id="scope" name="scope"
value="{{ client.client_metadata.get('scope', '') }}">
</div>
<div class="form-group">
<label for="grant_type">Grant Types (각 줄에 하나씩):</label>
<textarea id="grant_type" name="grant_type" required>{% for grant in client.client_metadata.get('grant_types', []) %}{{ grant }}
{% endfor %}</textarea>
</div>
<div class="form-group">
<label for="response_type">Response Types (각 줄에 하나씩):</label>
<textarea id="response_type" name="response_type" required>{% for response in client.client_metadata.get('response_types', []) %}{{ response }}
{% endfor %}</textarea>
</div>
<div class="form-group">
<label for="token_endpoint_auth_method">Token Endpoint Auth Method:</label>
<select id="token_endpoint_auth_method" name="token_endpoint_auth_method" required>
<option value="client_secret_basic"
{% if client.client_metadata.get('token_endpoint_auth_method') == 'client_secret_basic' %}selected{% endif %}>
client_secret_basic
</option>
<option value="client_secret_post"
{% if client.client_metadata.get('token_endpoint_auth_method') == 'client_secret_post' %}selected{% endif %}>
client_secret_post
</option>
<option value="none"
{% if client.client_metadata.get('token_endpoint_auth_method') == 'none' %}selected{% endif %}>
none
</option>
</select>
</div>
<div class="form-group">
<div class="checkbox-group">
<input type="checkbox" id="regenerate_secret" name="regenerate_secret">
<label for="regenerate_secret">Client Secret 재생성 (기존 토큰들이 무효화됩니다)</label>
</div>
</div>
<button type="submit">클라이언트 수정</button>
<a href="/" style="text-decoration: none;">
<button type="button">취소</button>
</a>
</form>
<hr style="margin: 30px 0;">
<h3>위험 영역</h3>
<form method="post" action="{{ url_for('.delete_client', client_id=client.id) }}"
onsubmit="return confirm('정말로 이 클라이언트를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.')">
<button type="submit" class="danger">클라이언트 삭제</button>
</form>
</body>
</html>

View file

@ -0,0 +1,82 @@
{% if user %}
<style>
pre{white-space:wrap}
.client-container {
border: 1px solid #ddd;
padding: 15px;
margin-bottom: 20px;
border-radius: 5px;
background-color: #f9f9f9;
}
.client-actions {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #ccc;
}
.client-actions a {
margin-right: 10px;
padding: 5px 10px;
text-decoration: none;
border-radius: 3px;
}
.edit-btn {
background-color: #007bff;
color: white;
}
.edit-btn:hover {
background-color: #0056b3;
}
.delete-btn {
background-color: #dc3545;
color: white;
}
.delete-btn:hover {
background-color: #c82333;
}
.create-btn {
background-color: #28a745;
color: white;
padding: 10px 20px;
text-decoration: none;
border-radius: 3px;
display: inline-block;
margin-top: 20px;
}
.create-btn:hover {
background-color: #218838;
}
</style>
<div>Logged in as <strong>{{user}}</strong> (<a href="{{ url_for('.logout') }}">Log Out</a>)</div>
{% for client in clients %}
<div class="client-container">
<pre>
<strong>Client Info</strong>
{%- for key in client.client_info %}
<strong>{{ key }}: </strong>{{ client.client_info[key] }}
{%- endfor %}
<strong>Client Metadata</strong>
{%- for key in client.client_metadata %}
<strong>{{ key }}: </strong>{{ client.client_metadata[key] }}
{%- endfor %}
</pre>
<div class="client-actions">
<a href="{{ url_for('.edit_client', client_id=client.id) }}" class="edit-btn">수정</a>
<form style="display: inline;" method="POST"
action="{{ url_for('.delete_client', client_id=client.id) }}"
onsubmit="return confirm('정말로 이 클라이언트를 삭제하시겠습니까?')">
<button type="submit" class="delete-btn"
style="border: none; cursor: pointer; padding: 5px 10px; border-radius: 3px;">삭제</button>
</form>
</div>
</div>
{% endfor %}
<a href="{{ url_for('.create_client') }}" class="create-btn">새 클라이언트 생성</a>
{% else %}
<form action="" method="post">
<input type="text" name="username" placeholder="username">
<button type="submit">Login / Signup</button>
</form>
{% endif %}