Skip to content

패스키 관리 목록이 자리표시자 1건 고정 — 등록된 패스키 미반영 #555

Open

Description

@OnlyReadOneBook

📜 Overview (Required)

설정 → 패스키 관리 화면의 목록이 실제 등록된 패스키와 무관하게 자리표시자 1건으로 고정된다 (2026-07-28 코드 QA).

실측

  • PassKeyListScreen.kt:61-63 — 목록 자리에 PasskeyListItem() 을 인자 없이 1회 렌더한다. 조회 결과를 받는 경로가 없다.
  • PasskeyListItem.kt:31-32 — 항목 내용이 리터럴 "이름", "생성일시" 이고, 아이콘도 ic_apple_login 고정이다.
  • 화면은 SettingNavGraph.kt:131 에 연결돼 실제 진입 가능하다.

즉 패스키를 하나도 등록하지 않아도 1건이 있는 것처럼 보이고, 여러 개 등록해도 1건만 보인다.

수정 방향: 등록된 패스키 목록을 조회해 항목별로 렌더하고, 0건일 때는 빈 상태 안내를 표시한다.

시안의 빈 상태:

Image

📌 Child Issue(Optional)

No response

📍 Note (Optional)

  • 정적 실측만으로 확인한 항목이다. 화면 진입 후 실제 표시는 서버 재개(12시 이후) + 로그인 상태에서 재확인이 필요하다.
  • 미구현 단계라면 이 이슈를 그 작업의 체크 항목으로 써도 무방하다.
  • QA 근거: 노션 2차 QA(2026-07-28) "확인된 개선" 문단 — 기기 지문을 등록해 패스키 목록 화면 도달에 성공했고, 자리표시자 표시를 별건으로 등록한 것이 이 이슈다.

Activity

  1. OnlyReadOneBook commented on Jul 28, 2026

    @OnlyReadOneBook
    CollaboratorAuthor

    2026-07-28 12:28 런타임 확인 시도 — 미확인으로 남깁니다.

    QA 계정에 등록된 패스키가 0건이라 설정 → 패스키 관리 진입 시 PassKeyScreen(등록 안내 + "패스키 등록" 버튼)이 뜨고, 이슈에서 지적한 목록 화면(PassKeyListScreen)에는 도달하지 못했습니다. 패스키를 1건 이상 등록해야 확인 가능한데, 계정에 실제 인증 수단을 추가하는 작업이라 QA 범위에서 제외했습니다. 정적 실측(자리표시자 1건 고정) 자체는 그대로 유효합니다.

  2. OnlyReadOneBook commented on Jul 28, 2026

    @OnlyReadOneBook
    CollaboratorAuthor

    2026-07-28 15:44 런타임 추가 확인 — 여전히 미확인입니다.

    QA 계정(dnfjddk2+qa0726@gmail.com, 패스키 0건)으로 패스키 관리에 들어가 "패스키 등록" 까지 진행했으나, 에뮬레이터에 지문이 등록돼 있지 않아 "지문 인증하기" 가 비활성이고 대체 경로("비밀번호로 인증하기")는 요구값이 불명확해(#574) 등록을 완료하지 못했습니다. 따라서 이슈가 지적한 PassKeyListScreen(패스키 1건 이상일 때의 목록)에는 이번에도 도달하지 못했습니다.

    정적 실측(자리표시자 1건 고정)은 그대로 유효하며, 확인하려면 기기에 지문을 등록한 에뮬이 필요합니다.

  3. OnlyReadOneBook commented on Jul 28, 2026

    @OnlyReadOneBook
    CollaboratorAuthor

    2026-07-28 17:40 런타임 재현 확인 — 지문을 등록해 패스키 생성까지 성공시켰습니다(에뮬 지문은 adb emu finger touch <id> 로 등록까지 직접 해야 인증이 통과합니다. GUI 로 등록한 지문은 id 가 달라 매칭되지 않았습니다).

    패스키를 1건 생성한 뒤 목록 화면입니다.

    패스키 목록
    [애플 로고]   이름
                  생성일시                    ✕
    
    • 항목 내용이 이름 · 생성일시 자리표시자 그대로입니다 — 실제 패스키 정보가 아닙니다
    • 아이콘이 애플 로고(ic_apple_login)입니다. 안드로이드에서 지문으로 만든 패스키인데 애플 아이콘이 붙습니다

    추가로 확인된 것 — 패스키가 서버에 등록되지 않습니다.
    "패스키 등록 → 지문 인증 → 패스키 생성이 완료되었습니다" 전 과정에서 네트워크 요청이 0건이었습니다. 로컬에서만 처리되는 것으로 보이며, 이 상태면 다른 기기에서는 이 패스키로 로그인할 수 없습니다. 목록이 자리표시자인 것도 서버에서 받아올 데이터가 없기 때문일 수 있습니다.

    정리하면 고칠 지점이 셋입니다 — ① 생성한 패스키를 서버에 등록, ② 목록을 실데이터로 렌더, ③ 제공자에 맞는 아이콘.

  4. OnlyReadOneBook commented on Jul 29, 2026

    @OnlyReadOneBook
    CollaboratorAuthor

    시안 대조 결과 (2026-07-29 Figma 실측).

    패스키 목록은 시안에 정의돼 있고, 현재 코드의 자리표시자와 형식이 다릅니다.

    정리하면 시안 기준값은:

    1. 항목 라벨은 리터럴 이름/생성일시 가 아니라 제공자명 + 생성일시 YYYY.MM.DD. HH:MM
    2. 아이콘도 ic_apple_login 고정이 아니라 제공자에 따라 달라지는 자리입니다
    3. 0건 빈 상태 = 목록 섹션 미표시 — 별도 빈 상태 문구는 시안에 없습니다

    본문에 적은 "0건일 때는 빈 상태 안내를 표시한다"는 시안과 다릅니다. 안내 문구를 넣을지 섹션을 감출지는 시안대로라면 감추는 쪽입니다.

  5. changed the issue type fromtoon Jul 29, 2026
  6. added
    enhancementNew feature or request
    and removed
    bugSomething isn't working
    on Jul 29, 2026
  7. OnlyReadOneBook commented on Aug 25, 2026

    @OnlyReadOneBook
    CollaboratorAuthor

    재현 확인 (develop 4e0a648eb, 2026-08-25 17:06 KST)

    설정 → 패스키 관리 → 패스키 등록 → 인증 통과 후 나타나는 "패스키 목록" 에 등록된 패스키가 하나도 없는데 항목 1건이 표시되고, 그 내용이 실제 데이터가 아니라 필드 라벨 그대로입니다.

    [애플 아이콘]   이름
                    생성일시              [X]
    

    부수 관찰 — 안드로이드 앱인데 애플 아이콘이 쓰이고 있습니다. 자리표시자를 실데이터로 바꿀 때 플랫폼 아이콘도 함께 정리하면 좋겠습니다.

    참고로 인증 전 화면(패스키 등록 유도)은 정상입니다. 자리표시자는 인증을 통과해야 보이므로 진입 경로가 길어 놓치기 쉽습니다.

  8. changed the title [-]fix(setting): 패스키 관리 목록이 자리표시자 1건 고정 — 등록된 패스키 미반영[/-] [+]패스키 관리 목록이 자리표시자 1건 고정 — 등록된 패스키 미반영[/+] on Aug 28, 2026
  9. added
    bugSomething isn't working
    and removed
    enhancementNew feature or request
    on Aug 28, 2026
  10. added
    pr-open이 이슈를 구현하는 PR 이 열려 있다 — base 가 기본 브랜치가 아니어도 드러낸다
    on Sep 6, 2026
  11. OnlyReadOneBook commented on Sep 7, 2026

    @OnlyReadOneBook
    CollaboratorAuthor

    PR #1927의 리뷰 요청에 따라 이 이슈를 열린 상태로 유지합니다. PR의 자동 종료 연결을 Closes #555에서 Refs #555로 바꿨습니다.

    현재 구현은 실제 목록/options/register API를 호출하고 실패 시 완료를 차단하지만, 실제 서버 등록 성공은 아직 검증하지 못했습니다. 직접 관측한 최종 응답은 HTTP 400 / code 2700이고 서버 목록은 전후 0건입니다.

    남은 완료 조건:

    • 최초 등록 응답과 배포된 RP/origin·challenge 검증 설정을 대조해 거절 원인을 확정한다. 공통 인증 재시도·origin 불일치는 아직 후보일 뿐이다.
    • 승인된 QA 환경에서 새 options로 실제 Credential Manager 자격 증명을 생성하고 register 성공을 확인한다.
    • 서버 목록에 생성된 항목과 앱의 성공 직후 갱신·재진입 유지를 확인한다.
    • 물리 기기 생체인증 QA와 실제 증거를 남긴다.

    기존 증거는 PR 본문의 d213a92 기록에 연결돼 있습니다. 이번 작업에서는 새 서버 성공을 주장하지 않으며, CI의 제어된 fixture 결과로 이 완료 조건을 대체하지 않습니다.

  12. removed
    pr-open이 이슈를 구현하는 PR 이 열려 있다 — base 가 기본 브랜치가 아니어도 드러낸다
    on Sep 7, 2026
  13. OnlyReadOneBook commented on Sep 13, 2026

    @OnlyReadOneBook
    CollaboratorAuthor

    실측 기록 (2026-09-13, FE 7d87aa3f4 / BE main 3593aa3)

    이번엔 코드로 낼 것이 없어 기록만 남깁니다. 남은 완료 조건 4건은 전부 실서버·실기기 QA이고, 구현 범위는 #1944 로 분리돼 PR #1927 이 물고 있습니다. 다만 서버도 기기도 없이 FE·BE 실코드만으로 첫 완료 조건(400/code 2700 거절 원인 확정)의 후보를 크게 줄일 수 있었습니다. 아래 네 가지가 그 결과이고, 마지막에 본문·완료 조건 정정을 따로 적었습니다.

    1. register 에서 2700 이 나올 수 있는 자리는 세 곳뿐입니다

    BE PasskeyService.register 는 challenge 를 먼저 소비하고 그다음에 검증합니다.

    • PasskeyService.java:114-117 clientData 나 challenge 가 없을 때 2700
    • PasskeyService.java:119-122 passkeyChallengeService.consume(challenge, TYPE_REGISTER) 가 던질 때 2700
    • PasskeyService.java:123-125 challenge 에 묶인 userId 가 요청 userId 와 다를 때 2700
    • PasskeyService.java:127-140 여기서 비로소 webAuthnManager.verify 를 부르고, origin·RP·서명 검증 실패는 전부 RuntimeException 으로 잡혀 PASSKEY_VERIFICATION_FAILED 로 나갑니다

    ErrorCode.java:257-258 이 2700 = 400, 2701 = 401 입니다. 즉 origin 이나 RP 가 어긋나면 401/2701 이 나오지 400/2700 은 나오지 않습니다.

    PasskeyChallengeService.consume 은 :42-63 이고, :47-48 에서 Redis 값을 읽자마자 지웁니다. 키가 없거나 비었으면 :49-51 에서 2700 입니다. 발급은 :27-36 에서 challenge-ttl-seconds TTL 로 넣습니다(application.yml:118, 기본 300).

    QA 기록(docs/qa/evidence/d213a9267860363c814fc6dbdcbc753743122550.json)이 마지막 clientDataJSON 을 디코딩해 challenge 가 그 직전 options 값과 정확히 같음을 확인했으므로 첫 번째 경우는 빠집니다. 타입도 register options 라 어긋날 수 없고, 세션이 같으니 userId 도 어긋날 이유가 없습니다. 남는 설명은 하나입니다. register 가 서버에 닿았을 때 그 Redis 키가 이미 없었다는 것이고, 그 이유는 둘뿐입니다. 앞선 시도가 이미 소비했거나, 배포 환경의 PASSKEY_CHALLENGE_TTL_SECONDS 가 실제로 56초보다 짧거나입니다. 기록된 options 응답과 credential POST 사이는 56.063초이고 기본값 300초보다 짧습니다.

    참고로 그 QA 기록의 diagnosis_limits 는 BE main fcca1ffd 기준이라 좌표가 밀려 있습니다. 당시 PasskeyService.java:117 / ErrorCode.java:258-259 로 적힌 것이 지금은 :119 / :257-258 입니다.

    2. 재전송 가설은 여전히 가설이지만, 성립하면 반드시 2700 이 됩니다

    FE TokenAuthenticator.kt:22-69 는 Bearer 401 을 받으면 응답 본문의 code 를 보지 않고 TokenReissuer.reissue 로 토큰을 굴린 뒤 같은 요청을 그대로 재전송합니다. 2701 은 HTTP 401 이므로 첫 시도가 2701 로 떨어지면 이 경로를 탑니다. 그런데 그 첫 시도가 :119 의 consume 을 이미 통과한 뒤에 :127-140 에서 떨어진 것이라면 challenge 는 그 시점에 지워져 있습니다. 따라서 재전송은 구조적으로 2700 이 됩니다. 관측된 "/auth/reissue 200 직후 register 400/2700" 시퀀스가 이 형태와 맞습니다.

    다만 이걸 확정으로 적지는 않습니다. AuthInterceptor.kt:64-74 에 만료 임박 시 선제 reissue 가 따로 있어서, reissue 200 하나만으로는 중간에 401 이 있었는지 가릴 수 없습니다. 이 한계는 QA 기록에도 이미 적혀 있습니다.

    3. "최초 등록 응답이 로그에 없었다" 는 반증이 아닙니다

    이게 이번에 새로 확인한 것 중 다음 QA 에 가장 직접 걸리는 부분입니다.

    NetworkModule.kt:151-173 이 MainClient 를 조립하는데, :168 에서 HttpLoggingInterceptor 를 addInterceptor 로 달고 :169 에서 .authenticator(tokenAuthenticator) 를 겁니다. OkHttp 에서 Authenticator 재시도는 RetryAndFollowUpInterceptor 아래에서 일어나고, 애플리케이션 인터셉터는 그보다 위라 호출당 한 번만 돕니다. 즉 애플리케이션 계층 로거는 마지막 응답 하나만 보고, 중간 401 은 구조적으로 남지 않습니다. 같은 파일 :170-171 의 주석("응답이 앱 쪽으로 넘어가기 전에 낚아채 … 다른 요청처럼 인터셉터를 거침")도 애플리케이션 인터셉터 기준으로는 사실과 다릅니다.

    그래서 다음 QA 에서 계측을 바꾸지 않고 실서버 재시도만 하면 같은 2700 을 다시 보게 됩니다. 결정적 관측이 되려면 둘 중 하나가 필요합니다.

    1. 디버그 빌드에서 network 계층 캡처(addNetworkInterceptor 또는 프록시)로 register 시도를 전부 뜨기
    2. 그 호출만 Authenticator 재시도에서 빼고 첫 응답을 그대로 보기

    어느 쪽이든 첫 응답의 code 가 2701 인지 2700 인지가 갈리면 1번의 두 갈래(선행 소비 / TTL) 중 하나가 바로 닫힙니다.

    4. 아직 어디에도 없는 것: 등록에 성공해도 목록 이름이 시안과 다릅니다

    BE 는 register 바디 최상위의 displayName 이 있을 때만 그걸 쓰고, 없으면 "패스키" 로 저장합니다(PasskeyService.java:344-352, 상수는 :54, PasskeyResponse.java:11 의 @Schema example 도 "패스키"). PR #1927 의 PasskeyRepositoryImpl.kt:15-16 은 Credential Manager 응답 JSON 을 그대로 POST 하므로 최상위 displayName 이 없습니다. BE 는 {credential, displayName} 래퍼 형태도 이미 받습니다(PasskeyService.java:332-342 의 credentialJson).

    시안(설정 - 패스키 관리, 목록 있음)의 항목 라벨은 제공자명(icloud keychain)입니다. 지금 형태로는 서버 등록이 성공해도 모든 행이 "패스키" 로 뜹니다. 무엇을 보낼지는 열어 둡니다. Credential Manager 의 create 응답에서 제공자명을 얻을 공개 경로가 있는지가 갈림길이고, 이건 #1944 에서 그 파일을 손댈 때 같이 답할 부분입니다.

    본문·완료 조건 정정

    본문은 고치지 않았습니다. 아래 네 가지는 이미 코멘트로 정정돼 있거나 PR 범위 안에서 결론이 나 있어서, 여기에 모아만 둡니다.

    본문 실측 두 줄은 origin/develop 에서 그대로 유효합니다. PassKeyListScreen.kt:61-63 의 인자 없는 PasskeyListItem(), PasskeyListItem.kt:31-32 의 리터럴 "이름"·"생성일시", :28 의 ic_apple_login 고정 전부 그대로입니다.

    여기서 코드를 떼지 않는 이유

    목록 렌더, 0건 위임, 로딩, 실패, 재시도가 전부 PR #1927(APPROVED, MERGEABLE, base refactor/1502-setting-state-mvi → PR #1967 → PR #1962 → develop) 안에 있습니다. 4번의 displayName 도 그 PR 이 쥔 파일이라 따로 내면 충돌합니다. 삭제(X 버튼) 범위는 #765 에서 PM 확인 대기 중입니다. 2번의 재전송 경로 자체를 바꾸는 것은 core:network 의 인증 재시도 계약이라 #555 의 범위 밖이고, 3번에 적은 계측은 그 계약을 건드리지 않는 디버그 빌드 한정 조치입니다.

  14. OnlyReadOneBook commented on Oct 7, 2026

    @OnlyReadOneBook
    CollaboratorAuthor

    실서버·실기기 QA (2026-10-07)

    검증 리비전: PR #2288 HEAD 6ddb57011f2102253bd8241501998ec04024a99c. 설치 앱 1.0+6ddb570, Galaxy S25(SM_S931N), Android 16. 실제 지문 인증은 사용자가 기기에서 수행했습니다.

    별도 QA 계정으로 서버 패스키 0건과 앱 등록 안내를 확인한 뒤 Credential Manager로 새 자격 증명을 생성했습니다.

    • 16:02:57.431 KST: register/options 요청. HTTP 200.
    • 16:03:06.093 KST: register 요청. HTTP 200, code 200. 앱 완료 다이얼로그 확인.
    • clientDataJSON의 type은 webauthn.create, challenge는 직전 options와 일치했습니다. Android 앱 origin은 설치 APK의 공유 debug 인증서 해시와 일치했습니다.
    • 서버 목록은 0건에서 1건으로 증가했습니다. 신규 id 2, createdAt 2026-10-07T16:03:06.29475.
    • 완료 확인 직후 앱 목록에 "패스키", "2026.10.07 16:03"이 표시됐고, 설정으로 나갔다가 재진입한 뒤에도 유지됐습니다.
    • 이번 등록 구간에서 register POST는 1회였고, 로그에 토큰 재발급 요청은 없었습니다.

    새 options의 실제 등록 성공, 서버 목록 반영, 앱 갱신·재진입 유지, 물리 기기 생체 인증을 확인했습니다. 과거 최초 응답은 이번 성공 로그로 직접 재현한 것이 아니므로, 과거 2700의 최초 거절 사유까지 이번 QA로 확정했다고 주장하지 않습니다. 토큰 재발급 예외 수정은 #2287 / PR #2288에서 추적하며 아직 머지 전입니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area:setting설정 기능 영역bugSomething isn't working

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions