AlchemyEngine 向けの ユーザー認証 API サービス。
登録・ログイン・セッション検証・ログアウトを担い、ゲームエンジン本体やアセット管理サービスから 認証責務を分離 する。
| 責務 | 説明 |
|---|---|
| ユーザーアイデンティティ | UUID ベースのユーザー ID(JWT sub) |
| 登録 / ログイン | ユーザー名 or メール + パスワード(Argon2) |
| セッション | JWT(Bearer)の発行・検証 |
| Remember Me | opaque リフレッシュトークン(7 日非アクティブで失効・スライディング) |
| ログアウト | トークン失効(jti + リフレッシュトークン) |
本リポジトリが担わないもの
- ゲームロジック・ルーム管理(alchemy-engine)
- アセットメタデータ・BLOB 管理(将来の alchemy-assets)
- ログイン UI(alchemy-engine クライアント側)
┌─────────────────┐ JWT (Bearer) ┌─────────────────┐
│ alchemy-engine │ ◄──────────────────── │ alchemy-auth │
│ (ゲーム/UI) │ JWKS 公開鍵参照 │ (本リポ) │
└─────────────────┘ └─────────────────┘
│ │
│ 将来 │ 将来
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ alchemy-assets │ ◄── JWT sub = owner ──│ PostgreSQL │
│ (Ash + メタ) │ │ (users 等) │
└─────────────────┘ └─────────────────┘
- User の SSoT は本サービスのみ。他サービスは JWT の
sub(ユーザー UUID)だけを参照する。 - ルーム参加用の短命トークン(Room Token)は alchemy-engine 側が発行する(User Session → Room Token の 2 段階)。
| 項目 | バージョン |
|---|---|
| Elixir | ~> 1.19 |
| Erlang/OTP | 28 |
| Phoenix | ~> 1.8 |
| Ash + ash_postgres | ~> 3.0 / ~> 2.0 |
| PostgreSQL | 16+ |
| パスワードハッシュ | Argon2 |
| セッション | JWT(RS256) |
usersテーブル(idUUID,username,email,password_hash,status,birthday,promo_code,tos_agreed_at,tos_version)- ユーザー登録(username / email 一意制約、パスワード複雑性、利用規約同意、生年月日)
- ユーザー名 or メール + パスワードログイン
- JWT セッション発行
- Remember Me(リフレッシュトークン。7 日非アクティブで失効、
POST /refresh) - 認証済み API ガード(
GET /me) - ログアウト(アクセストークン失効 + リフレッシュトークン失効)
- メール確認・パスワードリセット
- OAuth / OIDC
- alchemy-engine / alchemy-assets との本番統合
- 管理画面
| Method | Path | 認証 | 説明 |
|---|---|---|---|
POST |
/api/v1/auth/register |
なし | ユーザー登録 → セッション発行 |
POST |
/api/v1/auth/login |
なし | ログイン(username or email)→ セッション発行 |
POST |
/api/v1/auth/refresh |
なし | リフレッシュトークン → 新規アクセストークン |
POST |
/api/v1/auth/logout |
Bearer | ログアウト(リフレッシュトークンも失効可) |
GET |
/api/v1/auth/me |
Bearer | 認証済みユーザー情報 |
GET |
/health |
なし | ヘルスチェック |
GET |
/.well-known/jwks.json |
なし | JWT 検証用公開鍵 |
Register
POST /api/v1/auth/register
Content-Type: application/json
{
"username": "frick",
"email": "user@example.com",
"password": "Secret123",
"birthday": "2000-01-31",
"promo_code": "ABC123",
"tos_agreed": true,
"remember_me": true
}username: 3〜20 文字、英数字とアンダースコアのみ(大文字小文字非区別で一意)password: 8 文字以上、数字・小文字・大文字を各 1 以上birthday:YYYY-MM-DD(過去日付)promo_code: 任意tos_agreed:true必須(同意時刻と規約バージョンを記録)remember_me: 任意。trueでリフレッシュトークンを発行
HTTP/1.1 201 Created
{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 86400,
"refresh_token": "opaque...",
"user": {"user_id": "550e8400-...", "username": "frick", "email": "user@example.com"}
}バリデーションエラーはフィールド別に返す:
HTTP/1.1 422 Unprocessable Entity
{"errors": {"detail": "validation failed", "fields": {"password": ["must contain at least 1 uppercase letter"]}}}Login
POST /api/v1/auth/login
Content-Type: application/json
{"identifier": "frick", "password": "Secret123", "remember_me": true}identifier: username または email(@の有無で判定)
HTTP/1.1 200 OK
{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 86400,
"refresh_token": "opaque...",
"user": {"user_id": "550e8400-...", "username": "frick", "email": "user@example.com"}
}Refresh(Remember Me)
POST /api/v1/auth/refresh
Content-Type: application/json
{"refresh_token": "opaque..."}- 最終使用から 7 日以内なら新しいアクセストークンを発行し、使用時刻を更新(スライディング)
- 7 日超過・失効済みは
401
Logout
POST /api/v1/auth/logout
Authorization: Bearer eyJ...
Content-Type: application/json
{"refresh_token": "opaque..."}- アクセストークン(
jti)を失効。refresh_tokenを渡すとそれも失効(任意)
Me
GET /api/v1/auth/me
Authorization: Bearer eyJ...HTTP/1.1 200 OK
{"user_id": "550e8400-...", "username": "frick", "email": "user@example.com", "status": "active"}ログイン成功時に発行する Payload の固定項目:
| クレーム | 説明 | 例 |
|---|---|---|
sub |
ユーザー UUID(他サービスの owner_user_id と一致) |
"550e8400-..." |
status |
ユーザー状態 | "active" |
iss |
発行者 | "alchemy-auth" |
aud |
受信者 | "alchemy-platform" |
iat |
発行時刻(Unix 秒) | 1719148167 |
exp |
失効時刻(Unix 秒) | 1719234567 |
jti |
トークン一意 ID(ログアウト失効用) | "7c9e6679-..." |
- 署名方式: RS256(本サービスのみ秘密鍵を保持)
- 他サービス(alchemy-engine, alchemy-assets)は
/.well-known/jwks.jsonの公開鍵で検証する - パスワード・password_hash は JWT に含めない
- パスワードは Argon2 ハッシュのみ保存(平文禁止)
- ログイン失敗時は常に同じエラーメッセージ(ユーザー列挙の防止)
- ログイン / 登録にレート制限(目標: 5 回 / 15 分 / IP)
statusがsuspended/deletedのユーザーはログイン・JWT 検証の両方で拒否
- Elixir 1.19 / OTP 28
- Docker(PostgreSQL 用)
PostgreSQL と Phoenix をまとめて起動する場合:
docker compose up -d --build初回はイメージビルドと mix ash.setup / ecto.create / ecto.migrate が走るため、数分かかることがあります。http://localhost:4002/health で確認できます。
ソースはコンテナに bind mount されるため、コード変更はホスト側で保存すれば mix phx.server が再コンパイルします(deps / _build は名前付きボリュームで永続化)。
PostgreSQL のみ Docker、Elixir は WSL / ローカルで動かす場合:
# PostgreSQL のみ
docker compose up -d postgres
# 依存関係・DB
mix deps.get
mix ecto.create
mix ecto.migrate
# サーバー起動(デフォルト port 4002)
mix phx.servercurl -X POST http://localhost:4002/api/v1/auth/register `
-H "Content-Type: application/json" `
-d '{"username":"tester","email":"test@example.com","password":"Secret123","birthday":"2000-01-31","tos_agreed":true}'
curl -X POST http://localhost:4002/api/v1/auth/login `
-H "Content-Type: application/json" `
-d '{"identifier":"tester","password":"Secret123"}'Docker 起動時はシード(priv/repo/seeds.exs)によりデバッグ用アカウント(alice / bob / carol / admin、パスワードはいずれも Password1)が自動作成される。
| 変数 | 説明 | 例 |
|---|---|---|
DATABASE_URL |
PostgreSQL 接続 URL | ホスト: ...@localhost:5433/... / Docker 内: ...@postgres:5432/... |
SECRET_KEY_BASE |
Phoenix 秘密鍵 | (mix phx.gen.secret で生成) |
JWT_PRIVATE_KEY_PATH |
RS256 秘密鍵 PEM パス | priv/jwt_private.pem |
PORT |
HTTP ポート | 4002 |
BIND_ALL |
0.0.0.0 で待ち受け(Docker 用) |
true |
PHX_SERVER |
サーバープロセスを起動 | true |
| サービス | ポート |
|---|---|
| alchemy-auth(本サービス) | 4002 |
| alchemy-engine | 4000 |
mix testCI では PostgreSQL 16 サービスコンテナ上で mix ecto.create → mix ecto.migrate → mix test を実行する。
| リポジトリ | 関係 |
|---|---|
| alchemy-engine | ゲームエンジン。JWT 検証後に Room Token を発行 |
| alchemy-protocol | ゲームワイヤ契約(Protobuf) |
| alchemy-assets(予定) | アセットメタデータ。JWT sub を owner_user_id として参照 |
(alchemy-engine と同じライセンスを適用予定)
🚧 初期開発中 — Ash リソース・JWT・認証 API まで実装済み。CI 整備中。