이슈 #256
진행중8-4. 사용자 인증 API (JWT)
Chris Park이(가) 약 7시간 전에 추가함.
설명
로그인. 토큰 발급
CP Chris Park이(가) 7달 전에 변경 Actions #1
- 프로젝트을(를) 14에서 Alphea App(으)로 변경되었습니다.
- 목표버전을(를) 6월 중간 버전(으)로 지정되었습니다.
JL James Lee이(가) 7달 전에 변경 Actions #3
Step A — PostgreSQL 인프라 추가¶
사용자 인증(지갑 + 디바이스) 포팅의 사전 단계로, Rust 프로젝트에 PostgreSQL 연결 및 초기 스키마를 도입했습니다.
-
Repository:
alphea-ai-edge -
Commit: commit:server|2451853 —
feat: add PostgreSQL infrastructure with initial schema
구현 내용¶
-
sqlx의존성 추가 (postgres,chrono,migratefeatures) -
docker-compose.yml작성 —postgres:17-alpine+ healthcheck, 포트 5432 노출 -
.env.example추가 — 로컬 개발용DATABASE_URL기본값 - 초기 마이그레이션
migrations/0001_initial_schema.sql-
accounts— 지갑 주소, 이메일, 유저네임, 비밀번호 해시, 추천 코드 -
wallets— 계정별 AGX 잔액 및 누적 수익 -
nodes— 엣지/모바일/가상 노드 (device_id, device_meta JSONB 포함)
-
-
src/store/mod.rs신설 —Database래퍼 (connect,migrate,ping) -
src/config.rs에DATABASE_URL환경변수 추가 -
AppState에 DB pool 주입,main.rs에서 기동 시 자동 마이그레이션 -
SystemService.HealthRPC 가 실제 DB 연결 상태를 반영하도록 개선
검증 결과¶
| 항목 | 결과 |
|---|---|
cargo build |
성공 |
| PostgreSQL 컨테이너 | healthy |
| DB 연결 | connected to PostgreSQL |
| 마이그레이션 적용 | database migrations applied |
| gRPC 서버 기동 | 0.0.0.0:3000 |
| 테이블 생성 확인 |
accounts, wallets, nodes, _sqlx_migrations
|
다음 단계¶
- Step B — 인증 도메인 로직: secp256k1 서명 검증, HS256 JWT, in-memory challenge store
-
Step C — AuthService 핸들러 (
Challenge,Verify,GenerateKeypair) 구현
관련 DB 스키마 상세는 #254 참조
JL James Lee이(가) 7달 전에 변경 Actions #4
Step B — 인증 도메인 로직 (secp256k1 / JWT / Challenge Store)¶
사용자 인증 포팅의 두 번째 단계로, DB나 gRPC 핸들러에 의존하지 않는 순수 도메인 로직을 구현했습니다. 레거시 Go/TypeScript 구현과 byte-for-byte 호환을 목표로 했습니다.
-
Repository:
alphea-ai-edge -
Commit: commit:server|430589e —
feat: add auth domain logic (secp256k1, JWT, challenge store)
구현 내용¶
1. src/domain/auth/crypto.rs
-
secp256k1랜덤 keypair 생성 -
derive_keypair_from_credentials(email, password, salt)— 이메일/비밀번호 복구용 결정론적 keypair (SHA256(email:password:salt)→ secp256k1 secret) -
Ethereum 스타일 지갑 주소:
0x+ hex(keccak256(uncompressed_pubkey[1..])[12..]) -
verify_signature— 현재 레거시 Go의 TODO 동작을 그대로 재현 (형식 검증 +demo-sig-bypass). 실제 ECDSA recovery 는 Step C 에서 구현 예정 -
hash_password—SHA256(password + salt)→ lowercase hex -
verify_password—subtle::ConstantTimeEq로 타이밍 공격 방어 -
keccak256()— Keccak-256 legacy (Ethereum 호환, SHA3-256 아님) -
generate_challenge()—"ALPHEA Edge Network. Sign this message... Nonce: {16-byte hex}"고정 포맷
2. src/domain/auth/jwt.rs
-
레거시 비표준 HS256 정확히 재현:
SHA256(headerB64 + "." + payloadB64 + secret)(HMAC 아님, TypeScript Bun 서버와 호환 목적) -
JwtPayload필드:sub/typ/scope/iat/exp(레거시와 동일한 네이밍/순서) APP_SCOPES = ["wallet:read", "jobs:submit", "jobs:read", "stats:read"]NODE_SCOPES = ["node:heartbeat", "jobs:claim", "jobs:report"]- 토큰 수명 3600초 (1시간)
- 헤더는
{"alg":"HS256","typ":"JWT"}리터럴 고정 — Gojson.Marshal사전순 정렬 출력과 매칭
3. src/domain/auth/challenge.rs
-
DashMap<String, StoredChallenge>기반 in-memory 저장소 - 5분 TTL, 일회용 (verify 는 결과 여부와 무관하게 엔트리 소비 → replay 방지)
- 만료된 엔트리는
verify시 거부 -
spawn_cleanup_task()— 1분 주기 백그라운드 cleanup 태스크 (Step C 에서 서버 기동 시 호출 예정)
4. 설정 확장
-
Cargo.toml:secp256k1,sha2,sha3,hex,base64,rand,subtle,serde,serde_json,dashmap추가 -
src/config.rs:JWT_SECRET,AUTH_SALT,PASSWORD_SALT환경변수 추가 -
.env.example및 CLAUDE.md 반영
검증 결과¶
| 항목 | 결과 |
|---|---|
cargo build |
성공 (dead_code 경고만 — Step C 에서 해소) |
cargo test |
23개 테스트 통과, 0 실패 |
테스트 커버리지
-
crypto.rs(9개): 랜덤/결정론 keypair, 지갑 주소 포맷, 패스워드 해싱/검증, 챌린지 nonce 고유성,demo-sig-bypass, hex 오류 거부, 길이 검증,keccak256("")known vector -
jwt.rs(7개): app/node 토큰 라운드트립, 빈 타입 기본값, 커스텀 scope 오버라이드, 잘못된 secret/포맷/만료 거부, 서명 알고리즘이SHA256(sig_input + secret)임을 직접 검증 -
challenge.rs(6개): 발급/검증 라운드트립, 일회용, 알 수 없는 키, 잘못된 챌린지, 만료 엔트리, cleanup, 덮어쓰기
레거시 호환성 검증 포인트¶
- JWT 서명 스키마: 일반적인 HMAC-SHA256 이 아닌
SHA256(concat)방식 (레거시 주석에 "must match TypeScript" 명시) - JWT 필드명:
typ/scope(일반적인type/scopes가 아님) - Keccak-256:
sha3::Keccak256(NIST SHA3-256 아님, Ethereum 호환) -
HEADER_JSON은 Gojson.Marshal(map[string]string)의 알파벳 정렬 출력과 매칭되는 리터럴로 고정
다음 단계¶
-
Step C — AuthService gRPC 핸들러 (
Challenge,Verify,GenerateKeypair) 구현,auth.proto도입,AppState에ChallengeStore주입, 실제 ECDSA recovery 구현
이번 단계는 DB 스키마 변경이 없어 #254 에는 참조 링크만 남겨둡니다.
JL James Lee이(가) 7달 전에 변경 Actions #5
Step C — AuthService (지갑 + 디바이스 인증) 구현¶
사용자 인증 포팅의 세 번째 단계로, 실제 gRPC AuthService 의 핵심 RPC 3개와 엔드-투-엔드 검증을 완료했습니다.
-
Repository:
alphea-ai-edge -
Commit: commit:server|9b7ffb3 —
feat: add AuthService with wallet and device authentication
구현된 RPC¶
-
Challenge— 지갑 주소 입력 →ChallengeStore에서 5분 TTL 일회용 challenge 발급 -
Verify— challenge 검증 → 서명 검증 → accounts/wallets upsert → 디바이스가 있으면 mobile node 자동 등록 → JWT 발급 → 마지막 접속 시간 fire-and-forget 업데이트 -
GenerateKeypair— 랜덤 secp256k1 keypair 생성
Stub 처리 (후속 단계)
-
SetPassword,Login,EmailRegister,EmailRecover→Status::unimplemented
ECDSA 서명 검증 (레거시 TODO 해결)¶
레거시 Go 의 verify_signature 는 포맷만 검증하고 서명 자체는 검증하지 않는 TODO 상태였습니다. Rust 포팅에서는 실제 ECDSA recovery 를 구현했습니다:
- 65-byte
R||S||V또는 64-byteR||S모두 지원 - V 값은
0/1(raw) 및27/28(Ethereum personal_sign) 모두 허용 - 메시지를
keccak256으로 해시한 뒤 pubkey 복구 → Ethereum 주소 도출 → 대소문자 무시 비교 -
demo-sig-bypass 는 simulator 호환을 위해 유지
토큰 타입 승격 로직¶
레거시와 동일하게:
-
device_id+device_meta없음 →typ: "app", 기본 app scopes - 디바이스 컨텍스트 있음 →
typ: "node",APP_SCOPES ∪ NODE_SCOPES병합
프로젝트 구조 개선¶
-
src/lib.rs신설 — 프로젝트를 라이브러리 + 바이너리 혼합 crate 로 변경.examples/와 integration test 가 proto / domain 모듈을 재사용할 수 있게 됨 -
src/store/submodule 구조로 재편성 -
build.rs에file_descriptor_set_path설정 → gRPC reflection 지원 -
AppState가Config를 보유하도록 확장,ChallengeStorecleanup 태스크 기동 -
AuthServiceServer및tonic-reflection서버 등록
검증 결과¶
| 항목 | 결과 |
|---|---|
cargo build |
성공 |
cargo test |
30개 통과, 0 실패 (기존 23 + ECDSA recovery 3 + store::nodes 4) |
| gRPC reflection |
grpcurl -plaintext localhost:3000 list → alphea.v1.AuthService, alphea.v1.SystemService 확인 |
SystemService/Health |
status: ok, db_connected: true
|
AuthService/GenerateKeypair |
65-byte 언컴프레스드 pubkey + 0x + 40 hex 주소 정상 반환 |
| End-to-End |
examples/auth_roundtrip.rs 로 Challenge → Ethereum 스타일 서명 → Verify 전체 플로우 검증 성공 |
End-to-End 실행 로그¶
→ generated wallet: 0x7a72e61aaadd48917e5845d41f0f7575831c56af
→ received challenge
ALPHEA Edge Network. Sign this message to verify your wallet ownership.
This does not cost any gas. Nonce: 33c07a659b6e53d6c44193280b45b445
→ signature: 0xcf98732861bca944... (65 bytes, v=0x1c)
=== Verify succeeded ===
access_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.<payload>.<sig>
token_type: Bearer
expires_in: 3600
node_id: rn_7a72e61a_demo-device-42 ← 레거시 포맷 정확 일치
profile: wallet=0x7a72e61a... balance=0 total_earned=0
DB 에도 accounts / wallets / nodes 행이 정확히 영속화되었고, nodes.device_meta JSONB 에 DeviceMeta 5개 필드가 그대로 저장되었습니다. 자세한 DB 관련 내용은 #254 참조.
새로 추가된 주요 파일¶
-
proto/alphea/v1/auth.proto(레거시에서 복사,go_package제거) -
src/handler/auth.rs— AuthHandler -
src/store/accounts.rs/wallets.rs/nodes.rs -
src/lib.rs— 라이브러리 루트 -
examples/auth_roundtrip.rs— E2E 수동 검증 도구
의존성 추가¶
-
tonic-reflection(0.13),uuid(v4),sqlxmacrosfeature,secp256k1recoveryfeature
알려진 한계 (후속 작업 예정)¶
- 4개 stub RPC (
SetPassword,Login,EmailRegister,EmailRecover) 미구현 - JWT auth 인터셉터 미구현 — 보호된 RPC 호출 시 토큰 검증이 아직 연결되지 않음
- rate limit, 로그인 실패 카운터 등 프로덕션 보안 기능 없음
수동 검증 도구 사용법
docker-compose up -dcargo run(서버 기동, 3000 포트)- 별도 터미널에서
cargo run --example auth_roundtrip
JL James Lee이(가) 7달 전에 변경 Actions #6
사용자 인증 포팅 마무리 — 범위 확정¶
사용자 인증 (지갑 + 디바이스) 포팅을 현 시점에서 종료합니다. 레거시에 있던 일부 RPC 와 JWT 인터셉터는 의도적으로 이번 범위에서 제외하고, 아래와 같이 처리합니다.
포팅 완료 (유효 범위)¶
AuthService/Challenge-
AuthService/Verify— ECDSA recovery + 디바이스 자동 등록 + 토큰 타입 승격 AuthService/GenerateKeypair- secp256k1 + JWT + ChallengeStore 도메인 로직 (Step B)
- PostgreSQL 연결 +
accounts/wallets/nodes스키마 (Step A, 상세는 #254) - gRPC reflection + end-to-end 수동 검증 도구 (
examples/auth_roundtrip.rs)
Deprecated — 포팅하지 않음¶
아래 4개 RPC 는 레거시 전용 기능으로 판단되어 Rust 포팅 대상에서 제외합니다. 현재 handler/auth.rs 에서 Status::unimplemented 스텁으로 남아 있으며, 추후에도 구현 예정 없음:
| RPC | 레거시 용도 | 제외 사유 |
|---|---|---|
AuthService/SetPassword |
인증된 지갑에 비밀번호 설정 (SHA256 + salt) | 비밀번호 인증 흐름 자체를 더 이상 사용하지 않음 |
AuthService/Login |
지갑 주소 + 비밀번호 로그인 | 상동 |
AuthService/EmailRegister |
SHA256(email:password:salt) 결정론적 keypair 파생 후 계정 생성 |
이메일/패스워드 복구 흐름 deprecated |
AuthService/EmailRecover |
동일한 방식으로 기존 지갑 복구 | 상동 |
후속 작업에서 스텁을 제거하거나 proto 에서 해당 RPC 정의 자체를 삭제할지는 별도 정리 단계에서 결정합니다. 현재는 proto 호환을 유지하고 런타임에서만 차단된 상태입니다.
추후 작업 — JWT 인터셉터¶
레거시의 JWT 인터셉터 (전역 AuthInterceptor) 는 allow-list 기반으로 보호된 RPC 를 가로막는 역할입니다. 현재 Rust 쪽에 포팅된 RPC 는 전부 레거시 allow-list 의 public 목록에 포함되어 있어, 인터셉터를 지금 구현해도 실제로 막을 대상이 없습니다.
| 현재 포팅된 RPC | 레거시 allow-list 분류 |
|---|---|
SystemService/Health, SystemService/Info
|
public |
AuthService/Challenge, Verify, GenerateKeypair
|
public |
따라서 JWT 인터셉터 포팅은 보호된 RPC 가 필요한 서비스가 Rust 쪽으로 넘어올 때 해당 포팅과 한 세트로 묶어 진행합니다. 트리거가 될 만한 서비스:
-
WalletService— 전 RPC 가requireAuthenticatedWallet()사용 -
FriendsService—requireWallet()사용 -
VfsService— 전 파일 작업 인증 필요 -
NodeService/ConnectNode— bidi streaming,typ="node"토큰 요구 (인터셉터 밖에서 별도 검증 — 레거시와 동일)
이 중 어느 하나라도 포팅 착수 시 함께 다룰 예정입니다. 도메인 단 JWT 인코딩/디코딩 로직은 이미 src/domain/auth/jwt.rs 에 구현되어 있으므로 (30개 단위 테스트 통과 상태), 인터셉터 구현 시에는 미들웨어 배선과 allow-list 정의만 추가하면 됩니다.
다음 포팅 영역¶
사용자 인증 이후 어떤 서비스/도메인을 먼저 포팅할지는 별도 이슈에서 결정합니다. 이 이슈 (#256) 는 상태를 완료 로 전환 가능합니다.
JL James Lee이(가) 7달 전에 변경 Actions #8
8-4 작업 완료 — 해결 전환¶
사용자 인증 포팅 Step A/B/C 완료:
- Step A — PostgreSQL 인프라 + 초기 스키마 (commit:server|2451853)
- Step B — secp256k1 + JWT + ChallengeStore 도메인 로직 (commit:server|430589e)
-
Step C —
AuthService.Challenge/Verify/GenerateKeypair+ 실제 ECDSA recovery + 디바이스 자동 등록 (commit:server|9b7ffb3)
포팅 범위 확정 (앞서 마무리 댓글 참조):
-
SetPassword/Login/EmailRegister/EmailRecover— deprecated, 포팅 대상 제외 - JWT 인터셉터 전역화 — 보호 RPC 가 많아지면 별도 이슈로 분리
JWT 검증 인프라는 Section 17 Step 3 에서 domain::auth::require_auth 헬퍼로 부분 도입되어 NodeService.DeleteNode 에서 실사용 중 (commit:server|d9668e6, #313 참조).
JL James Lee이(가) 6달 전에 변경 Actions #11
- 상태을(를) 진행에서 해결(으)로 변경되었습니다.
GoTrue 기반 사용자 인증 구현 완료 (commit:server|8f52241, commit:server|442ebd1)¶
이 이슈에서 요구하는 "로그인 / 토큰 발급" 기능을 Supabase GoTrue(자체 호스팅) 기반으로 구현했습니다.
구현 방식¶
기존 wallet 기반 secp256k1 challenge/verify 인증 대신 GoTrue(supabase/gotrue:v2.158.1) 를 표준 인증 서버로 채택:
- 로그인/회원가입은 GoTrue REST API 위임 (
POST /token?grant_type=password,POST /signup) - GoTrue가 발행한 JWT(HS256)를 alphea-ai-edge가 직접 검증 —
JWT_SECRET환경변수 공유 -
AuthContext { user_id: GoTrue UUID, email, token_type }가 모든 보호 RPC에 주입됨
토큰 발급 엔드포인트 (GoTrue, port 9011)¶
POST :9011/token?grant_type=password
{"email": "user@example.com", "password": "…"}
→ {"access_token": "eyJ…", "expires_in": 3600, "user": {"id": "uuid", "email": "…"}}
POST :9011/signup
{"email": "user@example.com", "password": "…"}
→ {"access_token": "eyJ…", "user": {"id": "uuid", "email": "…"}}
alphea-ai-edge JWT 검증¶
src/domain/auth/jwt.rs — 표준 jsonwebtoken crate(HS256):
- GoTrue 토큰:
typclaim 없음 →token_type = "user",sub= GoTrue UUID - 노드 토큰:
typ: "node"→token_type = "node",sub= node_id (별도 유지)
docker-compose 설정¶
gotrue:
image: supabase/gotrue:v2.158.1
ports: ["9011:9011"]
environment:
GOTRUE_JWT_SECRET: ${JWT_SECRET}
GOTRUE_MAILER_AUTOCONFIRM: "true"
관련 이슈¶
JL James Lee이(가) 6달 전에 변경 Actions #13
Go 백포트 Phase 1 — JWT 표준화¶
commit:server|03c6c64 (이슈 #434)
본 이슈에서 설계된 JWT 인증 API 를 GoTrue 호환 표준으로 제자리:
-
internal/domain/auth/jwt.go가 기존 비표준SHA256(sigInput + secret)방식을 버리고 golang-jwt/jwt/v4 기반 표준 HS256 키 서명으로 교체 - 서버 발급 토큰
GenerateNodeToken(nodeID, secret, ttl)(typ="node",sub=node_id), GoTrue 발급 사용자 토큰(sub=UUID, notyp,email)을 동일 검증 경로로 검증 -
JWT_SECRET이GOTRUE_JWT_SECRET과 동일값이면 alphea-rpc 가 GoTrue 로 서명된 토큰을 수용
기존 AuthService 의 사용자 로그인(가 Challenge / Verify / Login / EmailRegister / EmailRecover 등)은 GoTrue 가 대체하고 proto 에서 제거.