프로젝트

일반

사용자정보

Actions

이슈 #256

진행중
CP JL

8-4. 사용자 인증 API (JWT)

이슈 #256: 8-4. 사용자 인증 API (JWT)

Chris Park이(가) 약 7시간 전에 추가함.

상태:
해결
우선순위:
높음
담당자:
목표버전:
시작일:
2026/04/28
완료일:
2026/04/30 (약 6달 지연)
진척도:

0%

추정시간:

설명

로그인. 토큰 발급


연결된 일감 2 (2 건 진행 중 — 모두 미완료)

다음 일감과 관련됨: 이슈 #375: JWT 전역 인터셉터 구현 (alphea-ai-edge)해결James Lee2026/04/15

Actions
다음 일감과 관련됨: 이슈 #434: [Go backport] Supabase GoTrue 인증 전환 — Phase 1: 인프라 + 인증 코어해결Henry Park2026/05/11

Actions

CP Chris Park이(가) 7달 전에 변경 Actions #1

  • 프로젝트을(를) 14에서 Alphea App(으)로 변경되었습니다.
  • 목표버전을(를) 6월 중간 버전(으)로 지정되었습니다.

JL James Lee이(가) 7달 전에 변경 Actions #2

  • 상태을(를) 신규에서 진행(으)로 변경되었습니다.

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, migrate features)
  • 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.Health RPC 가 실제 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"} 리터럴 고정 — Go json.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 은 Go json.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-byte R||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 를 보유하도록 확장, ChallengeStore cleanup 태스크 기동
  • 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), sqlx macros feature, secp256k1 recovery feature

알려진 한계 (후속 작업 예정)

  • 4개 stub RPC (SetPassword, Login, EmailRegister, EmailRecover) 미구현
  • JWT auth 인터셉터 미구현 — 보호된 RPC 호출 시 토큰 검증이 아직 연결되지 않음
  • rate limit, 로그인 실패 카운터 등 프로덕션 보안 기능 없음

수동 검증 도구 사용법

  1. docker-compose up -d
  2. cargo run (서버 기동, 3000 포트)
  3. 별도 터미널에서 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 #7

  • 상태을(를) 진행에서 해결(으)로 변경되었습니다.

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이(가) 7달 전에 변경 Actions #9

  • 다음 일감과 관련됨:에 이슈 #375: JWT 전역 인터셉터 구현 (alphea-ai-edge)이(가) 추가되었습니다.

RL Ryan Lee이(가) 6달 전에 변경 Actions #10

  • 상태을(를) 해결에서 진행(으)로 변경되었습니다.

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 토큰: typ claim 없음 → 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"

관련 이슈

  • #411 — GoTrue 전환 전체 범위 (상세 구현 내역)
  • #254 — accounts/wallets 스키마 변경

JL James Lee이(가) 6달 전에 변경 Actions #12

  • 다음 일감과 관련됨:에 이슈 #434: [Go backport] Supabase GoTrue 인증 전환 — Phase 1: 인프라 + 인증 코어이(가) 추가되었습니다.

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, no typ, email)을 동일 검증 경로로 검증
  • JWT_SECRET 이 GOTRUE_JWT_SECRET 과 동일값이면 alphea-rpc 가 GoTrue 로 서명된 토큰을 수용

기존 AuthService 의 사용자 로그인(가 Challenge / Verify / Login / EmailRegister / EmailRecover 등)은 GoTrue 가 대체하고 proto 에서 제거.

Actions

내보내기 PDF Atom