クリーンアーキテクチャ設計書¶
| 項目 | 値 |
|---|---|
| モジュール/サービス名 | Authentication Service (recerdo-auth) |
| 作成者 | Akira |
| 作成日 | 2026-04-13 |
| ステータス | ドラフト |
| バージョン | 1.0 |
1. 概要¶
1.1 目的¶
Authentication Service は Recuerdo プラットフォーム全体の認証・トークン管理を司る。AWS Cognito とローカルデータベースを統合し、ユーザーログイン、トークン更新、デバイス追跡、トークン無効化を提供する。マイクロサービス間の信頼基盤として、JWT (RS256) による安全なトークン発行と検証をサポートする。
1.2 ビジネスコンテキスト¶
Recuerdo では、ユーザー認証は基本的な要件。複数デバイス、セッション管理、アクセストークン/リフレッシュトークンの分離、ユーザー停止チェック等、複雑な認証フローが必要。AWS Cognito をアイデンティティプロバイダーとして使用し、ローカルDB で追加のメタデータ(デバイス、セッション)を管理する。
1.3 アーキテクチャ原則¶
- 単一責任の原則: 認証・トークン管理に専念し、ビジネスロジック非依存
- 依存性の逆転: リポジトリ・外部サービスはインターフェース経由で依存
- 層間の厳密な分離: Entities → UseCases → Adapters → Frameworks の一方向依存
- テスト可能性: すべてのユースケースはインターフェース依存、モック可能
- セキュリティ重視: トークン署名検証、パスワードハッシング、デバイストラッキング
2. レイヤーアーキテクチャ¶
2.1 アーキテクチャ図¶
┌─────────────────────────────────────────────────────┐
│ Frameworks & Drivers (フレームワーク&ドライバ) │
│ Gin, MySQL (MariaDB互換), Redis, AWS Cognito, QueuePort (Beta: Redis+BullMQ / Prod: OCI Queue) │
└─────────────────────────────────────────────────────┘
▲
│ (依存)
┌─────────────────────────────────────────────────────┐
│ Interface Adapters (インターフェースアダプタ) │
│ HTTP Controllers, Repository Impl, Presenters │
└─────────────────────────────────────────────────────┘
▲
│ (依存)
┌─────────────────────────────────────────────────────┐
│ Application Business Rules (アプリケーション) │
│ Use Cases, DTOs, Port Interfaces │
└─────────────────────────────────────────────────────┘
▲
│ (依存)
┌─────────────────────────────────────────────────────┐
│ Enterprise Business Rules (エンティティ/ドメイン) │
│ Domain Models, Value Objects, Domain Events │
└─────────────────────────────────────────────────────┘
2.2 依存性ルール¶
外側のレイヤーは内側に依存し、内側は外側に依存しない。データ流は入力アダプタ → ユースケース → 出力アダプタ の一方向。外側のレイヤー間通信はポート(インターフェース)経由のみ。
3. エンティティ層(ドメイン)¶
3.1 ドメインモデル¶
| エンティティ名 | 説明 | 主要フィールド |
|---|---|---|
| User | ローカルユーザー キャッシュ | userId, cognitoSub, email, createdAt, updatedAt |
| UserSession | ユーザーセッション | sessionId, userId, refreshTokenJti, createdAt, expiresAt |
| DeviceRegistration | ユーザーデバイス情報 | deviceId, userId, fingerprint, deviceName, lastSeenAt |
| BlockedToken | 無効化されたトークン | tokenJti, userId, revokedAt, expiresAt |
| RefreshTokenGrant | リフレッシュトークンの払い出し記録 | grantId, userId, jti, issuedAt, expiresAt, revokedAt |
3.2 値オブジェクト¶
| 値オブジェクト | 説明 | 不変性 |
|---|---|---|
| AccessToken | アクセストークン (1h TTL) | Yes |
| RefreshToken | リフレッシュトークン (30d TTL) | Yes |
| DeviceFingerprint | デバイス識別子 (User-Agent + IP hash) | Yes |
| CognitoUserAttributes | AWS Cognito ユーザー属性 | Yes |
| TokenClaims | JWT クレーム (sub, exp, iat, aud) | Yes |
3.3 ドメインルール / 不変条件¶
- 各 User は一意の cognitoSub を保持し、複数登録は禁止
- AccessToken の TTL は常に 3600 秒
- RefreshToken の TTL は常に 2592000 秒 (30 日)
- BlockedToken に含まれるトークンはいかなる場合でも再使用不可
- ユーザーが SUSPENDED 状態の場合、ログイン不可
- DeviceRegistration の lastSeenAt は各リクエスト時に更新
- RefreshTokenGrant は revokedAt なしで一度だけ有効
3.4 ドメインイベント¶
| イベント名 | 発火条件 | ペイロード |
|---|---|---|
| auth.user_login | ログイン成功 | userId, sessionId, deviceId, timestamp |
| auth.user_logout | ログアウト成功 | userId, sessionId, timestamp |
| auth.token_revoked | トークン無効化 | jti, userId, revokedAt, expiresAt |
| auth.device_registered | 新デバイス登録 | userId, deviceId, fingerprint, timestamp |
| auth.cognito_synced | Cognito ユーザー同期 | userId, cognitoSub, email, timestamp |
3.5 エンティティ定義¶
// Domain Entities
package domain
import "time"
// User represents a locally cached user from Cognito
type User struct {
UserID string
CognitoSub string // Cognito's user UUID
Email string
CreatedAt time.Time
UpdatedAt time.Time
}
// UserSession represents an active login session
type UserSession struct {
SessionID string
UserID string
RefreshTokenJti string // JWT ID of associated refresh token
CreatedAt time.Time
ExpiresAt time.Time
}
// DeviceRegistration tracks user devices
type DeviceRegistration struct {
DeviceID string
UserID string
Fingerprint string // Hash of User-Agent + IP
DeviceName string // e.g., "Chrome on MacOS"
LastSeenAt time.Time
RegisteredAt time.Time
}
// BlockedToken represents a revoked JWT
type BlockedToken struct {
TokenJti string
UserID string
RevokedAt time.Time
ExpiresAt time.Time
}
// RefreshTokenGrant tracks refresh token issuance
type RefreshTokenGrant struct {
GrantID string
UserID string
JTI string // JWT ID
IssuedAt time.Time
ExpiresAt time.Time
RevokedAt *time.Time
}
// Value Objects
type AccessToken struct {
Token string
Claims TokenClaims
ExpiresAt time.Time
}
type RefreshToken struct {
Token string
JTI string
ExpiresAt time.Time
}
type TokenClaims struct {
Sub string // Cognito Sub
Exp int64 // Expiration (unix)
Iat int64 // Issued at (unix)
Aud string // Audience
Custom map[string]interface{} // Custom claims
}
type DeviceFingerprint struct {
Value string
}
type CognitoUserAttributes struct {
Sub string
Email string
EmailVerified bool
Name string
UpdatedAt int64
}
4. ユースケース層(アプリケーション)¶
4.1 ユースケース一覧¶
| ユースケース | アクター | 説明 | 優先度 |
|---|---|---|---|
| Login (main) | User | メールとパスワードでログイン、トークン発行 | HIGH |
| Logout | User | セッション無効化、トークン無効化 | HIGH |
| RefreshToken | User | リフレッシュトークンからアクセストークン再発行 | HIGH |
| RegisterDevice | System | 新デバイス登録またはlastSeenAt更新 | HIGH |
| SyncCognito | System | Cognito ユーザーをローカルDBに同期 | MEDIUM |
| RevokeToken | System | トークンをブロックリストに追加 | HIGH |
| ValidateToken | System (API Gateway) | トークン署名・有効性検証 (gRPC) | HIGH |
| ListActiveSessions | User | ユーザーのアクティブセッション一覧取得 | MEDIUM |
4.2 ユースケース詳細 - Login¶
アクター: User (Web/Mobile client)
事前条件: - ユーザーが Cognito に登録済み - Username/Password が有効 - User が SUSPENDED 状態ではない
フロー: 1. HostedUIにログイン要求 (InitiateAuth API) 2. AWS Cognito に認証要求 (InitiateAuth API) 3. Cognito から ID Token + Access Token + Refresh Token を受け取る 4. ID Token から Cognito User Attributes を抽出 5. ローカル DB で User が存在するか確認 6. User が存在しない場合、SyncCognito ユースケース実行 (新規作成) 7. Permission Service で User が SUSPENDED かチェック 8. SUSPENDED の場合、ユースケース終了、エラー返却 9. DeviceRegistration テーブルで deviceFingerprint をチェック 10. 既存デバイスの場合、lastSeenAt 更新; 新規デバイスの場合、RegisterDevice ユースケース実行 11. UserSession 新規作成 12. RefreshTokenGrant レコード作成(Refresh Token JTI を記録) 13. AccessToken・RefreshToken を Response DTO で返す 14. auth.user_login イベント発行(QueuePort: Beta=Redis+BullMQ / Prod=OCI Queue)
事後条件: - ユーザーがセッション取得 - トークン (AccessToken + RefreshToken) が返される - デバイス登録完了
エラーケース: - Cognito 認証失敗 → 401 Unauthorized - User が SUSPENDED → 403 Forbidden - デバイス登録失敗 → 500 Internal Server Error
4.3 入出力DTO¶
// Application DTOs
package application
import "time"
// ===== Login Use Case =====
type LoginInput struct {
Email string
Password string
DeviceID string // Client-provided device identifier
Fingerprint string // User-Agent + IP hash
}
type LoginOutput struct {
UserID string
SessionID string
AccessToken string
RefreshToken string
ExpiresIn int // Access token TTL in seconds
TokenType string // "Bearer"
}
// ===== Logout Use Case =====
type LogoutInput struct {
UserID string
SessionID string
RefreshTokenJti string
}
type LogoutOutput struct {
Success bool
RevokedAt time.Time
}
// ===== RefreshToken Use Case =====
type RefreshTokenInput struct {
RefreshToken string
}
type RefreshTokenOutput struct {
AccessToken string
ExpiresIn int
TokenType string
}
// ===== RegisterDevice Use Case =====
type RegisterDeviceInput struct {
UserID string
DeviceID string
Fingerprint string
DeviceName string
}
type RegisterDeviceOutput struct {
DeviceID string
RegisteredAt time.Time
}
// ===== SyncCognito Use Case =====
type SyncCognitoInput struct {
CognitoSub string
Email string
Name string
}
type SyncCognitoOutput struct {
UserID string
CreatedAt time.Time
IsNewUser bool
}
// ===== RevokeToken Use Case =====
type RevokeTokenInput struct {
TokenJti string
UserID string
ExpiresAt time.Time
}
type RevokeTokenOutput struct {
Success bool
RevokedAt time.Time
}
4.4 リポジトリインターフェース(ポート)¶
// Application Ports - Repository Interfaces
package ports
import (
"context"
"time"
)
// UserRepository ユーザー管理
type UserRepository interface {
FindByCognitoSub(ctx context.Context, cognitoSub string) (*User, error)
FindByUserID(ctx context.Context, userID string) (*User, error)
Save(ctx context.Context, user *User) error
Update(ctx context.Context, user *User) error
}
// UserSessionRepository セッション管理
type UserSessionRepository interface {
FindBySessionID(ctx context.Context, sessionID string) (*UserSession, error)
FindActiveByUserID(ctx context.Context, userID string) ([]*UserSession, error)
Save(ctx context.Context, session *UserSession) error
Delete(ctx context.Context, sessionID string) error
}
// DeviceRegistrationRepository デバイス管理
type DeviceRegistrationRepository interface {
FindByUserIDAndFingerprint(ctx context.Context, userID, fingerprint string) (*DeviceRegistration, error)
FindByUserID(ctx context.Context, userID string) ([]*DeviceRegistration, error)
Save(ctx context.Context, device *DeviceRegistration) error
UpdateLastSeen(ctx context.Context, deviceID string) error
}
// BlockedTokenRepository トークンブロックリスト
type BlockedTokenRepository interface {
IsBlocked(ctx context.Context, jti string) (bool, error)
Add(ctx context.Context, jti string, expiresAt time.Time) error
RemoveExpired(ctx context.Context) error
}
// RefreshTokenGrantRepository リフレッシュトークン払い出し記録
type RefreshTokenGrantRepository interface {
FindByJTI(ctx context.Context, jti string) (*RefreshTokenGrant, error)
Save(ctx context.Context, grant *RefreshTokenGrant) error
RevokeByUserID(ctx context.Context, userID string) error
}
// EventPublisher イベント発行
type EventPublisher interface {
Publish(ctx context.Context, eventType string, payload map[string]interface{}) error
}
4.5 外部サービスインターフェース(ポート)¶
// Application Ports - External Service Interfaces
package ports
import "context"
// CognitoAuthProvider AWS Cognito 認証
type CognitoAuthProvider interface {
InitiateAuth(ctx context.Context, email, password string) (*CognitoAuthResponse, error)
GetUserAttributes(ctx context.Context, accessToken string) (*CognitoUserAttributes, error)
}
type CognitoAuthResponse struct {
IDToken string
AccessToken string
RefreshToken string
ExpiresIn int
}
// PermissionServiceClient Permission Service (gRPC) 権限チェック
type PermissionServiceClient interface {
CheckUserSuspended(ctx context.Context, userID string) (bool, error)
}
// TokenSigningProvider JWT トークン署名
type TokenSigningProvider interface {
GenerateAccessToken(ctx context.Context, claims TokenClaims) (string, error)
GenerateRefreshToken(ctx context.Context, userID, jti string) (string, error)
VerifyAccessToken(ctx context.Context, token string) (*TokenClaims, error)
VerifyRefreshToken(ctx context.Context, token string) (*TokenClaims, error)
}
5. インターフェースアダプタ層¶
5.1 コントローラ / ハンドラ¶
| コントローラ | HTTPメソッド | エンドポイント | 説明 |
|---|---|---|---|
| LoginHandler | POST | /api/auth/login | HostedUIにリダイレクトでログイン |
| LogoutHandler | POST | /api/auth/logout | ログアウト、トークン無効化 |
| RefreshTokenHandler | POST | /api/auth/refresh | リフレッシュトークン使用、アクセストークン再発行 |
| SyncHandler | POST | /api/auth/sync | Cognito user を同期(内部用) |
| SessionListHandler | GET | /api/auth/sessions | アクティブセッション一覧取得 |
| HealthCheckHandler | GET | /health | サービス稼働確認 |
5.2 プレゼンター / レスポンスマッパー¶
プレゼンター役割: アプリケーション層の Output DTO を HTTP レスポンスに変換。
レスポンスマッパー例:
{
"user_id": "user-123",
"session_id": "sess-456",
"access_token": "eyJhbGc...",
"refresh_token": "eyJhbGc...",
"token_type": "Bearer",
"expires_in": 3600
}
エラーレスポンス:
{
"error": "INVALID_CREDENTIALS",
"message": "Email or password is incorrect",
"timestamp": "2026-04-13T10:30:45Z"
}
5.3 リポジトリ実装(アダプタ)¶
| リポジトリ実装 | 技術スタック | 説明 |
|---|---|---|
| MySQLUserRepository | Database/sql + sqlc | ユーザーレコード CRUD |
| MySQLUserSessionRepository | Database/sql + sqlc | セッションレコード CRUD |
| MySQLDeviceRegistrationRepository | Database/sql + sqlc | デバイスレコード CRUD |
| RedisBlockedTokenRepository | Redis (go-redis/v9) | トークンブロックリスト |
| MySQLRefreshTokenGrantRepository | Database/sql + sqlc | リフレッシュトークン払い出し記録 |
| QueueEventPublisher | QueuePort (Beta: Redis+BullMQ/asynq / Prod: OCI Queue) | イベント発行 |
5.4 外部サービスアダプタ¶
| アダプタ | 外部サービス | 説明 |
|---|---|---|
| CognitoAuthAdapter | AWS Cognito (AuthPort 実装) | AWS SDK で InitiateAuth, GetUser 実行。AWS で唯一許可されたサービス |
| GRPCPermissionServiceAdapter | gRPC (Permission Service) | CheckUserSuspended gRPC 呼び出し |
| RSATokenSigningAdapter | RS256 署名 | JWKS 秘密鍵で JWT 署名、検証 |
5.5 マッパー¶
// Mappers - Domain ↔ DTO conversion
package adapters
import "domain"
import "application"
// MapLoginInputToDomain HTTP request → Domain login context
func MapLoginInputToDomain(input *LoginInput) *domain.LoginContext {
return &domain.LoginContext{
Email: input.Email,
DeviceID: input.DeviceID,
Fingerprint: input.Fingerprint,
}
}
// MapCognitoResponseToUser Cognito response → Domain User entity
func MapCognitoResponseToUser(cognitoAttrs *domain.CognitoUserAttributes) *domain.User {
return &domain.User{
CognitoSub: cognitoAttrs.Sub,
Email: cognitoAttrs.Email,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
}
// MapUserSessionToOutput Domain session → HTTP response DTO
func MapUserSessionToOutput(session *domain.UserSession, accessToken, refreshToken string) *LoginOutput {
return &LoginOutput{
SessionID: session.SessionID,
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresIn: 3600,
TokenType: "Bearer",
}
}
6. フレームワーク&ドライバ層(インフラストラクチャ)¶
6.1 Webフレームワーク¶
- Framework: Gin Web Framework (github.com/gin-gonic/gin)
- Port: 8080 (HTTP)
- TLS: 本番環境では mTLS (Kubernetes ServiceAccount)
- Health Check: /health エンドポイント (Kubernetes liveness probe)
- Request Logging: 構造化ログ (JSON format, OpenTelemetry)
6.2 データベース¶
Primary Database: MySQL 8.x (Beta) / OCI MySQL HeatWave (Prod)。スキーマは MariaDB 10.6+ 互換(PostgreSQL 固有型 UUID / INET / gen_random_uuid() は使用しない)。
MySQL / MariaDB 互換テーブルスキーマ:
-- Users table
CREATE TABLE users (
user_id CHAR(36) PRIMARY KEY, -- UUID() をアプリ側で生成
cognito_sub VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_cognito_sub (cognito_sub),
INDEX idx_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- User Sessions table
CREATE TABLE user_sessions (
session_id CHAR(36) PRIMARY KEY,
user_id CHAR(36) NOT NULL,
refresh_token_jti VARCHAR(255) NOT NULL UNIQUE,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME NOT NULL,
CONSTRAINT fk_sessions_user FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE,
INDEX idx_user_id (user_id),
INDEX idx_refresh_token_jti (refresh_token_jti),
INDEX idx_expires_at (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Device Registrations table
CREATE TABLE device_registrations (
device_id CHAR(36) PRIMARY KEY,
user_id CHAR(36) NOT NULL,
fingerprint VARCHAR(255) NOT NULL,
device_name VARCHAR(255),
last_seen_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
registered_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_user_fp (user_id, fingerprint),
CONSTRAINT fk_devices_user FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE,
INDEX idx_user_id (user_id),
INDEX idx_last_seen_at (last_seen_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Refresh Token Grants table
CREATE TABLE refresh_token_grants (
grant_id CHAR(36) PRIMARY KEY,
user_id CHAR(36) NOT NULL,
jti VARCHAR(255) UNIQUE NOT NULL,
issued_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME NOT NULL,
revoked_at DATETIME NULL,
CONSTRAINT fk_grants_user FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE,
INDEX idx_user_id (user_id),
INDEX idx_jti (jti),
INDEX idx_expires_at (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Blocked Tokens table (for archival; Redis が primary)
CREATE TABLE blocked_tokens (
token_jti VARCHAR(255) PRIMARY KEY,
user_id CHAR(36) NOT NULL,
revoked_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME NOT NULL,
CONSTRAINT fk_blocked_user FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE,
INDEX idx_user_id (user_id),
INDEX idx_expires_at (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Audit Log table
CREATE TABLE auth_audit_log (
log_id CHAR(36) PRIMARY KEY,
user_id CHAR(36) NULL,
action VARCHAR(50) NOT NULL, -- LOGIN, LOGOUT, TOKEN_REFRESH, DEVICE_REGISTER, etc.
result VARCHAR(20) NOT NULL, -- SUCCESS, FAILURE
detail TEXT,
ip_address VARCHAR(45), -- IPv4/IPv6 文字列 (INET は未使用)
user_agent TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_audit_user FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE SET NULL,
INDEX idx_user_id (user_id),
INDEX idx_action (action),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Redis スキーマ (Blocked Tokens キャッシュ):
# Blocked tokens (Set で高速チェック)
SET blocked_tokens:jti-uuid-123 1 EX 86400
# Refresh token blacklist (Optional for faster revocation)
SET revoked_refresh_tokens:jti-456 1 EX 2592000
6.3 メッセージブローカー¶
- Broker: QueuePort 抽象
- Beta: Redis + BullMQ / asynq (
RedisBullMQAdapter) - Prod: OCI Queue (
OCIQueueAdapter) - AWS SQS / SNS は使用しない
- Event Topics (論理名):
auth.user_login- ログイン成功auth.user_logout- ログアウトauth.token_revoked- トークン無効化auth.device_registered- デバイス登録auth.cognito_synced- Cognito 同期
Queue Event Message Format (JSON):
{
"event_type": "auth.user_login",
"timestamp": "2026-04-13T10:30:45Z",
"payload": {
"user_id": "user-123",
"session_id": "sess-456",
"device_id": "dev-789",
"ip_address": "203.0.113.42"
}
}
6.4 外部ライブラリ&SDK¶
| ライブラリ | バージョン | 用途 |
|---|---|---|
| github.com/gin-gonic/gin | v1.9.1 | HTTP Web Framework |
| github.com/go-sql-driver/mysql | v1.8.0 | MySQL / MariaDB driver |
| github.com/redis/go-redis/v9 | v9.0.5 | Redis client |
| github.com/golang-jwt/jwt/v5 | v5.0.0 | JWT 生成・検証 |
| github.com/lestrrat-go/jwx/v2 | v2.0.0 | JWKS 処理 |
| github.com/aws/aws-sdk-go-v2 | v1.17.0 | AWS SDK (Cognito のみ利用) |
| github.com/hibiken/asynq | v0.24.1 | Redis キュー (Beta QueuePort) |
| github.com/oracle/oci-go-sdk/v65 | v65.0.0 | OCI SDK (Prod Queue) |
| google.golang.org/grpc | v1.56.0 | gRPC client (Permission Service) |
| github.com/sqlc-dev/sqlc | v1.19.0 | SQL code generation |
| go.opentelemetry.io/api | v1.16.0 | OpenTelemetry (Logging, Tracing) |
| github.com/uber-go/fx | v1.19.0 | Dependency injection framework |
6.5 依存性注入¶
// Infrastructure - Dependency Injection (uber-go/fx)
package infrastructure
import (
"context"
"go.uber.org/fx"
"database/sql"
"github.com/redis/go-redis/v9"
"github.com/gin-gonic/gin"
"google.golang.org/grpc"
)
// Module provides all infrastructure dependencies
var Module = fx.Module("infrastructure",
fx.Provide(
// MySQL
provideDatabaseConnection,
// Redis
provideRedisClient,
// AWS Cognito (AuthPort)
provideCognitoClient,
// QueuePort (Beta: Redis+BullMQ/asynq, Prod: OCI Queue)
provideQueueAdapter,
// gRPC clients
providePermissionServiceClient,
// Repositories
provideUserRepository,
provideUserSessionRepository,
provideDeviceRegistrationRepository,
provideBlockedTokenRepository,
provideRefreshTokenGrantRepository,
// External Service Adapters
provideCognitoAuthAdapter,
providePermissionServiceAdapter,
provideTokenSigningAdapter,
// Event Publisher (QueuePort-backed)
provideQueueEventPublisher,
// Gin Engine
provideGinEngine,
// Controllers/Handlers
provideLoginHandler,
provideLogoutHandler,
provideRefreshTokenHandler,
provideSyncHandler,
// Use Cases
provideLoginUseCase,
provideLogoutUseCase,
provideRefreshTokenUseCase,
provideRegisterDeviceUseCase,
provideSyncCognitoUseCase,
provideRevokeTokenUseCase,
),
)
func provideDatabaseConnection() (*sql.DB, error) {
// go-sql-driver/mysql 用 DSN。Beta: XServer VPS 上の MySQL 8.x、Prod: OCI MySQL HeatWave
return sql.Open("mysql", "user:pass@tcp(db:3306)/recuerdo?parseTime=true&charset=utf8mb4")
}
// provideQueueAdapter は QueuePort の実装を環境に応じて返す
func provideQueueAdapter(cfg *Config) (application.QueuePort, error) {
switch cfg.Queue.Provider {
case "redis-bullmq":
return adapter.NewRedisBullMQAdapter(cfg.Queue.RedisAddr), nil
case "oci-queue":
return adapter.NewOCIQueueAdapter(cfg.Queue.OCIQueueID)
default:
return nil, fmt.Errorf("unknown queue provider: %s", cfg.Queue.Provider)
}
}
func provideRedisClient() *redis.Client {
return redis.NewClient(&redis.Options{
Addr: "redis:6379",
})
}
func provideGinEngine(
loginHandler *LoginHandler,
logoutHandler *LogoutHandler,
refreshHandler *RefreshTokenHandler,
) *gin.Engine {
engine := gin.Default()
// Routes
engine.POST("/api/auth/login", loginHandler.Handle)
engine.POST("/api/auth/logout", logoutHandler.Handle)
engine.POST("/api/auth/refresh", refreshHandler.Handle)
engine.GET("/api/auth/sessions", sessionListHandler)
engine.GET("/health", healthCheckHandler)
return engine
}
func main() {
app := fx.New(
fx.Module("recerdo-auth",
infrastructure.Module,
),
fx.Invoke(startServer),
)
app.Run()
}
7. ディレクトリ構成¶
recerdo-auth/
├── cmd/
│ └── main.go # エントリーポイント
├── domain/
│ ├── entities.go # User, UserSession, Device, etc.
│ ├── value_objects.go # AccessToken, RefreshToken, etc.
│ └── events.go # Domain events
├── application/
│ ├── dto/
│ │ └── dto.go # Input/Output DTOs
│ ├── ports/
│ │ ├── repository.go # Repository interfaces
│ │ └── external.go # External service interfaces
│ └── usecases/
│ ├── login.go # Login use case
│ ├── logout.go # Logout use case
│ ├── refresh_token.go # RefreshToken use case
│ ├── register_device.go # RegisterDevice use case
│ ├── sync_cognito.go # SyncCognito use case
│ ├── revoke_token.go # RevokeToken use case
│ └── validate_token.go # ValidateToken use case (gRPC)
├── adapters/
│ ├── handlers/
│ │ ├── login_handler.go
│ │ ├── logout_handler.go
│ │ ├── refresh_handler.go
│ │ └── session_handler.go
│ ├── repositories/
│ │ ├── MySQL_user.go
│ │ ├── MySQL_session.go
│ │ ├── MySQL_device.go
│ │ ├── redis_blocked_token.go
│ │ └── MySQL_refresh_grant.go
│ ├── external/
│ │ ├── cognito_adapter.go
│ │ ├── permission_grpc.go
│ │ └── token_signing.go
│ ├── mappers/
│ │ └── mappers.go # Domain ↔ DTO mappings
│ └── presenters/
│ └── response_mapper.go # Output → HTTP response
├── infrastructure/
│ ├── config.go # Configuration loading
│ ├── db.go # MySQL setup
│ ├── redis.go # Redis client setup
│ ├── aws.go # AWS SDK setup
│ ├── grpc.go # gRPC client setup
│ └── di.go # Dependency injection (fx module)
├── go.mod
├── go.sum
├── Dockerfile
└── k8s/
├── deployment.yaml # Kubernetes Deployment
├── service.yaml # Service definition
├── configmap.yaml # Configuration ConfigMap
└── statefulset-MySQL.yaml # MySQL StatefulSet (optional)
8. 依存性ルールと境界¶
8.1 許可される依存関係¶
| ソース層 | ターゲット層 | 許可 | 理由 |
|---|---|---|---|
| Frameworks | Adapters | Yes | アダプタはフレームワークを使用 |
| Frameworks | UseCases | No | ビジネスロジックはフレームワーク非依存 |
| Adapters | UseCases | Yes | ハンドラはユースケース呼び出し |
| Adapters | Entities | Yes | マッパーがドメインモデルを使用 |
| UseCases | Entities | Yes | ビジネスロジック実行 |
| UseCases | Frameworks | No | 外部フレームワーク非依存 |
| Entities | 他すべて | No | エンティティはビジネスロジックのみ |
8.2 境界の横断¶
許可される方法: 1. ポート/インターフェース: 内側のレイヤーがインターフェース定義、外側が実装 2. DTO: レイヤー間のデータ転送用(ドメインモデルは未公開) 3. ドメインイベント: 非同期・疎結合通信
禁止される方法: - 直接 import(例:usecase が gin.Context 直接使用) - DB モデルの外部公開 - フレームワーク型の内部レイヤーへの波及
8.3 ルールの強制¶
アーキテクチャ監視: - GitHub Actions で import グラフの循環参照検出 - staticcheck で禁止パターン検出 - Code review での手動チェック
9. テスト戦略¶
9.1 テストピラミッド¶
| テストレベル | 割合 | テスト対象 | ツール |
|---|---|---|---|
| Unit Tests | 70% | ユースケース、値オブジェクト、マッパー | testing, testify/assert |
| Integration Tests | 20% | リポジトリ実装、外部サービスアダプタ | testcontainers |
| E2E Tests | 10% | HTTP エンドポイント、完全フロー | Go httptest, Docker Compose |
9.2 テスト例¶
// Unit Tests - Login Use Case
package application
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// Test Case 1: Login - Successful authentication and session creation
func TestLogin_Success(t *testing.T) {
// Arrange
mockCognitoAuth := new(MockCognitoAuthProvider)
mockUserRepo := new(MockUserRepository)
mockSessionRepo := new(MockUserSessionRepository)
mockDeviceRepo := new(MockDeviceRegistrationRepository)
mockPermissionClient := new(MockPermissionServiceClient)
mockTokenSigner := new(MockTokenSigningProvider)
mockEventPublisher := new(MockEventPublisher)
cognitoAttrs := &domain.CognitoUserAttributes{
Sub: "cognito-sub-123",
Email: "user@example.com",
}
mockCognitoAuth.On("InitiateAuth", mock.Anything, "user@example.com", "password").
Return(&domain.CognitoAuthResponse{
IDToken: "id-token",
AccessToken: "cognito-access-token",
RefreshToken: "refresh-token",
ExpiresIn: 3600,
}, nil)
mockCognitoAuth.On("GetUserAttributes", mock.Anything, "cognito-access-token").
Return(cognitoAttrs, nil)
mockUserRepo.On("FindByCognitoSub", mock.Anything, "cognito-sub-123").
Return(&domain.User{
UserID: "user-123",
CognitoSub: "cognito-sub-123",
Email: "user@example.com",
}, nil)
mockPermissionClient.On("CheckUserSuspended", mock.Anything, "user-123").
Return(false, nil)
mockDeviceRepo.On("FindByUserIDAndFingerprint", mock.Anything, "user-123", "fingerprint-xyz").
Return(nil, nil) // New device
mockDeviceRepo.On("Save", mock.Anything, mock.Anything).Return(nil)
mockSessionRepo.On("Save", mock.Anything, mock.Anything).Return(nil)
mockTokenSigner.On("GenerateAccessToken", mock.Anything, mock.Anything).
Return("generated-access-token", nil)
mockTokenSigner.On("GenerateRefreshToken", mock.Anything, "user-123", mock.Anything).
Return("generated-refresh-token", nil)
mockEventPublisher.On("Publish", mock.Anything, "auth.user_login", mock.Anything).
Return(nil)
useCase := NewLoginUseCase(
mockCognitoAuth,
mockUserRepo,
mockSessionRepo,
mockDeviceRepo,
mockPermissionClient,
mockTokenSigner,
mockEventPublisher,
)
input := &LoginInput{
Email: "user@example.com",
Password: "password",
DeviceID: "device-456",
Fingerprint: "fingerprint-xyz",
}
// Act
output, err := useCase.Execute(context.Background(), input)
// Assert
assert.NoError(t, err)
assert.Equal(t, "user-123", output.UserID)
assert.Equal(t, "generated-access-token", output.AccessToken)
assert.Equal(t, "generated-refresh-token", output.RefreshToken)
assert.Equal(t, "Bearer", output.TokenType)
assert.Equal(t, 3600, output.ExpiresIn)
mockCognitoAuth.AssertCalled(t, "InitiateAuth", mock.Anything, "user@example.com", "password")
mockPermissionClient.AssertCalled(t, "CheckUserSuspended", mock.Anything, "user-123")
mockEventPublisher.AssertCalled(t, "Publish", mock.Anything, "auth.user_login", mock.Anything)
}
// Test Case 2: Login - User suspended
func TestLogin_UserSuspended(t *testing.T) {
mockCognitoAuth := new(MockCognitoAuthProvider)
mockUserRepo := new(MockUserRepository)
mockPermissionClient := new(MockPermissionServiceClient)
mockCognitoAuth.On("InitiateAuth", mock.Anything, "user@example.com", "password").
Return(&domain.CognitoAuthResponse{IDToken: "id", AccessToken: "at", RefreshToken: "rt", ExpiresIn: 3600}, nil)
mockCognitoAuth.On("GetUserAttributes", mock.Anything, "at").
Return(&domain.CognitoUserAttributes{Sub: "sub-123", Email: "user@example.com"}, nil)
mockUserRepo.On("FindByCognitoSub", mock.Anything, "sub-123").
Return(&domain.User{UserID: "user-123", CognitoSub: "sub-123"}, nil)
mockPermissionClient.On("CheckUserSuspended", mock.Anything, "user-123").
Return(true, nil) // User is suspended
useCase := NewLoginUseCase(
mockCognitoAuth,
mockUserRepo,
nil,
nil,
mockPermissionClient,
nil,
nil,
)
input := &LoginInput{Email: "user@example.com", Password: "password"}
// Act
output, err := useCase.Execute(context.Background(), input)
// Assert
assert.Error(t, err)
assert.Nil(t, output)
assert.Contains(t, err.Error(), "suspended")
}
// Test Case 3: RefreshToken - Generate new access token
func TestRefreshToken_Success(t *testing.T) {
mockTokenSigner := new(MockTokenSigningProvider)
mockGrantRepo := new(MockRefreshTokenGrantRepository)
mockSessionRepo := new(MockUserSessionRepository)
claims := &domain.TokenClaims{
Sub: "cognito-sub-123",
Exp: time.Now().Add(30 * 24 * time.Hour).Unix(),
Iat: time.Now().Unix(),
}
mockTokenSigner.On("VerifyRefreshToken", mock.Anything, "refresh-token").
Return(claims, nil)
grant := &domain.RefreshTokenGrant{
GrantID: "grant-1",
UserID: "user-123",
JTI: "jti-refresh",
ExpiresAt: time.Now().Add(30 * 24 * time.Hour),
RevokedAt: nil,
}
mockGrantRepo.On("FindByJTI", mock.Anything, "jti-refresh").Return(grant, nil)
mockTokenSigner.On("GenerateAccessToken", mock.Anything, mock.MatchedBy(func(c domain.TokenClaims) bool {
return c.Sub == "cognito-sub-123"
})).Return("new-access-token", nil)
useCase := NewRefreshTokenUseCase(mockTokenSigner, mockGrantRepo, mockSessionRepo)
input := &RefreshTokenInput{RefreshToken: "refresh-token"}
// Act
output, err := useCase.Execute(context.Background(), input)
// Assert
assert.NoError(t, err)
assert.Equal(t, "new-access-token", output.AccessToken)
assert.Equal(t, 3600, output.ExpiresIn)
assert.Equal(t, "Bearer", output.TokenType)
mockTokenSigner.AssertCalled(t, "VerifyRefreshToken", mock.Anything, "refresh-token")
mockGrantRepo.AssertCalled(t, "FindByJTI", mock.Anything, "jti-refresh")
}
// Integration Test: MySQL UserRepository
func TestMySQLUserRepository_Integration(t *testing.T) {
// Setup testcontainers
ctx := context.Background()
container, dbURL, err := setupMySQLContainer(ctx)
assert.NoError(t, err)
defer container.Terminate(ctx)
db, err := sql.Open("MySQL", dbURL)
assert.NoError(t, err)
defer db.Close()
repo := NewMySQLUserRepository(db)
// Create user
user := &domain.User{
UserID: "user-123",
CognitoSub: "cognito-sub-456",
Email: "test@example.com",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Act: Save
err = repo.Save(ctx, user)
assert.NoError(t, err)
// Act: FindByCognitoSub
retrieved, err := repo.FindByCognitoSub(ctx, "cognito-sub-456")
assert.NoError(t, err)
assert.NotNil(t, retrieved)
assert.Equal(t, "test@example.com", retrieved.Email)
}
10. エラーハンドリング¶
10.1 ドメインエラー¶
- InvalidCredentials: Cognito 認証失敗
- UserNotFound: User が存在しない
- UserSuspended: User が SUSPENDED 状態
- TokenExpired: アクセストークンまたはリフレッシュトークン期限切れ
- InvalidRefreshToken: リフレッシュトークンが無効(署名・クレーム不正)
- TokenRevoked: トークンがブロックリストに含まれている
10.2 アプリケーションエラー¶
- CognitoServiceUnavailable: AWS Cognito 接続失敗
- DatabaseError: MySQL クエリ失敗
- PermissionServiceError: Permission Service gRPC 失敗
- DeviceRegistrationError: デバイス登録失敗
- SessionCreationError: セッション作成失敗
10.3 エラー変換¶
| ドメイン/アプリエラー | HTTPステータス | レスポンスBody |
|---|---|---|
| InvalidCredentials | 401 Unauthorized | {"error": "INVALID_CREDENTIALS", "message": "..."} |
| UserNotFound | 404 Not Found | {"error": "USER_NOT_FOUND", "message": "..."} |
| UserSuspended | 403 Forbidden | {"error": "USER_SUSPENDED", "message": "..."} |
| TokenExpired | 401 Unauthorized | {"error": "TOKEN_EXPIRED", "message": "..."} |
| InvalidRefreshToken | 401 Unauthorized | {"error": "INVALID_REFRESH_TOKEN", "message": "..."} |
| CognitoServiceUnavailable | 503 Service Unavailable | {"error": "SERVICE_UNAVAILABLE"} |
| DatabaseError | 500 Internal Server Error | {"error": "INTERNAL_SERVER_ERROR"} |
11. 横断的関心事¶
11.1 ロギング¶
構造化ログ (JSON、OpenTelemetry):
{
"timestamp": "2026-04-13T10:30:45.123Z",
"level": "INFO",
"service": "auth-svc",
"user_id": "user-123",
"action": "LOGIN",
"result": "SUCCESS",
"ip_address": "203.0.113.42",
"device_id": "dev-456",
"duration_ms": 234,
"trace_id": "trace-uuid"
}
監査ログ: すべての認証関連アクション(LOGIN, LOGOUT, TOKEN_REFRESH, DEVICE_REGISTER)を audit_log テーブルに記録。
11.2 認証・認可¶
- 認証: AWS Cognito (メール/パスワード)
- 認可: Permission Service gRPC でユーザー状態チェック (SUSPENDED)
- トークン管理: JWT (RS256), AccessToken (1h), RefreshToken (30d)
11.3 バリデーション¶
- HTTP リクエスト: Content-Type (application/json), Body JSON 形式
- Email バリデーション: RFC 5322 準拠
- Password バリデーション: Cognito ポリシー適用
- JWT クレーム: exp, iat, sub, aud 検証
11.4 キャッシング¶
- User: MySQL primary; Redis L1 キャッシュなし (一貫性重視)
- Blocked Tokens: Redis TTL (トークン exp まで)
- JWKS: Redis キャッシュ (更新頻度低)
12. マイグレーション計画¶
12.1 現状¶
既存の単一アプリケーションで認証をローカル実装。
12.2 目標状態¶
Authentication Service として独立、AWS Cognito 統合、トークン管理一元化。
12.3 マイグレーション手順¶
| フェーズ | 期間 | 実施内容 | リスク |
|---|---|---|---|
| Phase 1: 準備 | Week 1-2 | Auth Service 開発完了、AWS Cognito テナント設定 | なし |
| Phase 2: ステージング | Week 3 | ステージング環境デプロイ、統合テスト (10% ユーザー) | パフォーマンス |
| Phase 3: 本番導入 | Week 4-5 | 本番環境デプロイ、段階的ユーザー移行 (10% → 50% → 100%) | 認証失敗 |
| Phase 4: 監視・調整 | Week 6+ | メトリクス監視、レイテンシー最適化 | なし |
ロールバック計画: API Gateway の設定変更で旧認証エンドポイントへ即座に戻す (< 1分)
13. 未決事項と決定事項¶
| # | 項目 | 現在の決定 | 代替案 | 理由 |
|---|---|---|---|---|
| 1 | トークン署名アルゴリズム | RS256 (RSA-SHA256) | HS256, EdDSA | 公開鍵検証可能、セキュリティ標準 |
| 2 | AccessToken TTL | 3600秒 (1時間) | 1800, 7200秒 | セキュリティと UX のバランス |
| 3 | RefreshToken TTL | 2592000秒 (30日) | 7日, 90日 | 既存 Cognito デフォルト |
| 4 | ユーザー同期 | Cognito → LocalDB (キャッシュ) | Cognito オンリー | レイテンシー削減、キャッシュ効率 |
| 5 | デバイスフィンガープリント | User-Agent + IP hash | TLS fingerprint | 実装簡易、ブラウザ互換性 |
| 6 | トークン無効化方式 | Redis Blocklist (高速) + DB 監査ログ | DB のみ | O(1) チェック速度 |
14. 参考資料¶
- Robert C. Martin, "Clean Architecture: A Craftsman's Guide to Software Structure and Design", 2017
- Mark Richards & Neal Ford, "Fundamentals of Software Architecture", 2020
- OAuth 2.0 Authorization Framework (RFC 6749): https://tools.ietf.org/html/rfc6749
- JWT (RFC 7519): https://tools.ietf.org/html/rfc7519
- AWS Cognito Documentation: https://docs.aws.amazon.com/cognito/
- MySQL 8.x Documentation: https://dev.mysql.com/doc/
- MariaDB Documentation: https://mariadb.com/kb/en/documentation/
- OCI MySQL HeatWave: https://docs.oracle.com/en-us/iaas/mysql-database/
- OCI Queue Service: https://docs.oracle.com/en-us/iaas/Content/queue/
- asynq (Redis queue): https://github.com/hibiken/asynq
- Redis Documentation: https://redis.io/documentation
- OpenTelemetry: https://opentelemetry.io/
最終更新: 2026-04-19 ポリシー適用