Thiết kế: Authentication Feature
Linear Issue: N/A
Status: Production
1. Tổng quan kiến trúc
1.1 Cấu trúc hệ thống
flowchart TB
subgraph Client["Mobile/Web Client"]
App["Mobile App / Web"]
end
subgraph Auth["authentication module (port 8085)"]
AC["AuthController\n/api/auth/*"]
LF["LoginHandlerFactory"]
W3["Web3AuthLoginHandler\n(WEB3_AUTH, DM2, SOLANA)"]
IM["ImmutableLoginHandler\n(IMMUTABLE)"]
AS["AuthService\n(core logic)"]
SEC["SecurityJwtService\n(JWT)"]
ASYNC["AuthAsyncService\n(@Async ops)"]
end
subgraph Infra["Shared Infrastructure"]
Redis[("Redis\nSession + Lock")]
MySQL[("MySQL\nUser/Wallet")]
FCM["Firebase\nPush Notification"]
Node["NodeServerService\nBlockchain"]
Email["EmailService"]
end
App -->|"POST /api/auth/generate-code"| AC
App -->|"POST /api/auth/sign-in\n(x-api-key)"| AC
App -->|"DELETE /api/auth/logout/{token}"| AC
AC --> LF
LF --> W3 & IM
W3 & IM --> AS
AS --> SEC
AS --> ASYNC
SEC -->|"access_token + refresh_token"| AC
AS --> Redis
AS --> MySQL
ASYNC --> FCM
ASYNC --> Node
ASYNC --> Email
1.2 Các thay đổi chính khi mở rộng provider
| Mục |
Hiện tại |
Khi thêm provider mới |
| Handler |
Web3AuthLoginHandler, ImmutableLoginHandler |
Tạo XxxLoginHandler implements LoginHandler |
| Factory |
Map key = type.getHandlerKey() + "LoginHandler" |
Đăng ký bean mới với naming convention |
| Wallet type |
BICONOMY/DM2/SOLANA/IMMUTABLE |
Thêm enum WalletType + branch trong getWalletType() |
| LoginEnum |
LoginType enum |
Thêm value mới |
2. Domain model
2.1 UserModel (các field liên quan auth)
// Key fields in UserModel
String id;
String email;
UserEnum.Status status; // ACTIVE | NO_ACTIVE | DELETED
String authCode; // 6-char random code
Instant expireAuthCode; // code TTL = 5 phút
Integer loginSession; // 0=new, 1=first, ≥2=subsequent
Instant firstLoginAt;
LoginEnum.LoginType latestLoginType;
String avatarUrl;
String referralCode;
String ipAddress;
String languageCode;
2.2 UserWalletModel
String userId;
LoginEnum.LoginType loginMethod; // source provider
UserWalletEnum.WalletType walletType; // BICONOMY | DM2 | SOLANA | IMMUTABLE
String publicAddress;
String verifier;
String verifierId;
String aggregateVerifier;
String appPubKey;
2.3 CodeLoginResponse
String id; // userId
String code; // 6-char auth code
String verifierId;
String loginType;
Instant createTime;
Instant expiredAt;
boolean isFirstLogin;
String accessToken;
String refreshToken;
String message; // optional
3. Sequence diagrams
3.1 generate-code (new user)
sequenceDiagram
participant C as Client
participant AC as AuthController
participant LF as LoginHandlerFactory
participant H as Web3AuthLoginHandler
participant AS as AuthService
participant AA as AuthAsyncService
participant DB as MySQL
participant R as Redis
C->>AC: POST /api/auth/generate-code\n{login_type, id_token, device_type, firebase_token}
AC->>LF: getHandler("WEB3_AUTH")
LF-->>AC: Web3AuthLoginHandler
AC->>H: generateCode(baseMap, request)
H->>H: validate/decode id_token (Jose/JWT)
H->>AS: getUserModelByEmail(email)
AS->>DB: query UserWallet by email
DB-->>AS: empty → new user
AS-->>H: null
H->>AS: setLoginCodeResponse(...)
AS->>R: lock(USER_EMAIL_LOCKER:{verifierId})
AS->>DB: INSERT UserModel
AS->>AA: createUserWallet(loginType)
AA->>DB: INSERT UserWalletModel
AS->>AA: initializeUserSystem(userId)
AS->>AA: sendWelcomeEmail()
AS->>AA: assignNftOwner()
AS->>AS: generateAndSaveAuthCode (TTL 5m)
AS->>AS: SecurityJwtService.createUserAuthResponse()
AS->>R: createOrUpdateSession(userId, deviceType, firebaseToken)
AS-->>H: CodeLoginResponse
H-->>AC: response
AC-->>C: Result.OK(CodeLoginResponse)
3.2 generate-code (existing user)
sequenceDiagram
participant C as Client
participant H as Web3AuthLoginHandler
participant AS as AuthService
participant DB as MySQL
participant R as Redis
C->>H: generateCode (email đã tồn tại)
H->>AS: getUserModelByEmail(email)
AS->>DB: query → found UserModel
AS-->>H: UserModel
H->>AS: getLoginCodeResponse(userModel, ...)
AS->>AS: handleAvatarUrlAndUserName(userModel)
AS->>AS: handleUserWallet (update/create)
AS->>AS: handleSecondaryWallet (async)
AS->>AS: generateAndSaveLoginCodeAndLoginType()
AS->>AS: SecurityJwtService.createUserAuthResponse()
AS->>R: createOrUpdateSession()
AS-->>H: CodeLoginResponse
H-->>C: Result.OK(CodeLoginResponse)
3.3 logout
sequenceDiagram
participant C as Client
participant AC as AuthController
participant UDTV as UserDeviceTokenService
participant AS as AuthService
participant R as Redis
C->>AC: DELETE /api/auth/logout/{firebaseToken}\n(Authorization: Bearer <jwt>)
AC->>AC: getLoginUserId() from JWT
AC->>UDTV: findBy(userId, firebaseToken)
UDTV-->>AC: UserDeviceTokenModel
AC->>UDTV: deleteById(id)
AC->>AS: invalidateUserSession(userId)
AS->>R: sessionRepository.invalidateSession(userId)
AC-->>C: Result.OK()
4. Security design
4.1 JWT Filter flow (mọi request)
flowchart LR
Req[HTTP Request] --> WL{Whitelist?}
WL -->|Yes| Pass[Pass through]
WL -->|No| JWT[Extract JWT\nfrom Authorization]
JWT --> Valid{Valid token?}
Valid -->|No| 401[401 Unauthorized]
Valid -->|Yes| Banned{User banned?}
Banned -->|YES: NO_ACTIVE| 403[403 Forbidden]
Banned -->|No| Session{Session valid?\n(Redis check)}
Session -->|No| 401
Session -->|Yes| Continue[Continue to Controller]
4.2 Concurrent login guard
// AuthService.signIn()
lockerService.getInLockWithException(
LockerService.LockKeyPreFix.USER_EMAIL_LOCKER,
verifierId, // lock key = verifierId (unique per user)
() -> { /* signIn logic */ },
BannedException.class
);
4.3 Single-device policy
- Chỉ áp dụng khi
x-site: APP header.
UserSessionRepository.createOrUpdateSession() lưu 1 session per userId trong Redis.
- Login từ thiết bị mới → ghi đè session cũ → thiết bị cũ nhận
NEW_DEVICE_CONNECTED notification qua broadcastPubSubService.
5. State machine - User status
stateDiagram-v2
[*] --> ACTIVE: User mới tạo
ACTIVE --> NO_ACTIVE: Admin ban user
NO_ACTIVE --> ACTIVE: Admin unban
ACTIVE --> DELETED: User xóa tài khoản
DELETED --> [*]
note right of NO_ACTIVE: Login bị chặn\nBannedException
note right of DELETED: Login bị chặn\nUnprocessableEntityException
6. Error handling
| Tình huống |
Exception |
HTTP |
Message |
Sai login_type |
BadRequestException |
400 |
INVALID_LOGIN_TYPE |
| User bị ban |
BannedException |
403 |
YOUR_ACCOUNT_HAS_BEEN_BANNED |
| Sai API key |
Return Result.error |
200 |
SECRET_KEY_IS_INVALID |
Email không tồn tại (check-email) |
BadRequestException |
400 |
EMAIL_NOT_FOUND |
| Wallet không tồn tại |
BadRequestException |
400 |
USER_WALLET_NOT_FOUND |
| Code hết hạn |
BadRequestException |
400 |
LOGIN_CODE_IS_EXPIRED |
7. Async operations (AuthAsyncService)
| Method |
Mô tả |
Blocking? |
createUserWallet() |
Tạo wallet chính |
Blocking (cần trả về wallet) |
createSecondaryWallet() |
Tạo wallet phụ (WEB3↔DM2) |
Non-blocking async |
initializeUserSystem() |
Init coin + equipment |
Non-blocking async |
sendWelcomeEmail() |
Email chào mừng |
Non-blocking async |
assignNftOwner() |
Gán NFT ownership |
Non-blocking async |
handleDeviceToken() |
Save/update firebase token |
Non-blocking async |
8. Configuration
# authentication/src/main/resources/application.yaml
server:
port: 8085
tomcat:
max-connections: 1000
threads:
max: 500
min-spare: 50
application:
authentication:
secret-key: ${AUTH_SECRET_KEY} # x-api-key cho sign-in + check-email
security:
whitelist:
- /api/auth/generate-code
- /api/auth/sign-in
- /api/auth/check-email
- /api/health-check/**
9. Điểm cần chú ý & rủi ro
| # |
Vấn đề |
Ảnh hưởng |
Biện pháp |
| 1 |
Rate limit generate-code đang bị comment out |
Có thể bị brute-force |
Re-enable @RateLimit hoặc thêm captcha |
| 2 |
validateCode() bị comment out trong signIn() |
Auth code không được validate |
Cần review xem đây là intentional hay không |
| 3 |
create-account-fake endpoint không có auth guard |
Tạo fake account tùy ý |
Thêm internal-only guard hoặc xóa |
| 4 |
Secondary wallet creation là async, không biết thành công |
DM2/WEB3_AUTH user có thể thiếu wallet phụ |
Monitor async errors, thêm retry |