Skip to content

[Fix/native/#219] 재설치 시 세션 초기화#220

Open
sterdsterd wants to merge 3 commits intodevelopfrom
fix/native/reinstall-session-#219
Open

[Fix/native/#219] 재설치 시 세션 초기화#220
sterdsterd wants to merge 3 commits intodevelopfrom
fix/native/reinstall-session-#219

Conversation

@sterdsterd
Copy link
Collaborator

📌 Related Issue Number


✅ Key Changes

이번 PR에서 작업한 내용을 간략히 설명해주세요

  1. Keychain 하드닝 + 재설치 감지

src/utils/auth.ts

변경 내용
keychainService pointer-native-auth로 keychain 항목 격리
keychainAccessible AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY → iCloud 동기화/백업 복원 차단
재설치 감지 AsyncStorage에 installMarker 저장. 마커 없으면 stale Keychain 데이터 전부 삭제
마이그레이션 기존 유저의 old keychain → new keychain 자동 마이그레이션
  1. State Machine 확장 + 서버 검증

src/stores/authStore.ts

변경 내용
SessionStatus 'unknown' | 'hydrating' | 'checking' | 'authenticated' | 'unauthenticated'
verifySession() access token → /api/student/me 검증 → 실패 시 refresh → 실패 시 clear + unauthenticated
Refresh token rotation 서버 응답의 새 access/refresh token 즉시 교체

src/hooks/useLoadAssets.ts

  • hydrateAuthState() → hydrateFromStorage() → verifySession() 순서로 파이프라인화

src/navigation/RootNavigator.tsx

  • hydrating, checking 상태에서도 스플래시 화면 표시 (서버 검증 완료 전 메인 화면 진입 차단)
  1. bareClient 추가
파일 Before After
bareClient.ts (신규) middleware 없는 openapi-fetch 인스턴스
postRefreshToken.ts raw fetch() + 수동 URL/body 조립 bareClient.POST('/api/student/auth/refresh', ...)
authMiddleware.ts raw fetch() + env.apiBaseUrl 참조 bareClient.GET('/api/student/me', ...) + bareClient.GET('/api/teacher/me', ...)
authStore.ts raw fetch() + env import bareClient.GET() + bareClient.POST()

…nd reinstall detection

- Add keychainService and AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY to prevent iCloud sync/backup token restoration
- Detect app reinstall via AsyncStorage marker and clear stale Keychain tokens
- Migrate existing keychain items from old (unscoped) to new (namespaced) keychain
…side token validation

- Expand SessionStatus with 'hydrating' and 'checking' intermediate states
- Add verifySession() that validates tokens via /api/student/me and refresh flow
- Update useLoadAssets to run full hydrate → verify pipeline
- Update RootNavigator to show splash during hydrating/checking states
…lows

- Add middleware-free openapi-fetch client (bareClient) for bootstrap/auth operations
- Migrate postRefreshToken, authMiddleware, and verifyStudentSession from raw fetch to bareClient
- Gain type safety from OpenAPI schema while avoiding authMiddleware circular dependency
@sterdsterd sterdsterd requested a review from Copilot February 11, 2026 20:06
@sterdsterd sterdsterd self-assigned this Feb 11, 2026
@sterdsterd sterdsterd added 🐞 Fix 버그 수정 🔨 Refactor 코드 리팩토링 labels Feb 11, 2026
@vercel
Copy link

vercel bot commented Feb 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pointer-admin Ready Ready Preview, Comment Feb 11, 2026 8:06pm

@sterdsterd sterdsterd linked an issue Feb 11, 2026 that may be closed by this pull request
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to ensure sessions are properly reset on app reinstall (especially iOS Keychain persistence) while strengthening credential storage and adding a bootstrap-time server session verification flow.

Changes:

  • Harden SecureStore usage by namespacing Keychain items (keychainService) and disabling iCloud sync/backup restoration (AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY), plus add reinstall detection.
  • Expand auth state machine to include hydrating/checking and add a verifySession() step that validates /api/student/me and refreshes tokens if needed.
  • Introduce a middleware-free bareClient (openapi-fetch) and refactor refresh/profile calls to use it.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
apps/native/src/utils/auth.ts Adds SecureStore options, keychain migration helper, and reinstall detection hook in hydration.
apps/native/src/stores/authStore.ts Adds new session statuses and server-side session verification with refresh-token rotation.
apps/native/src/hooks/useLoadAssets.ts Sequences auth hydration → store hydration → server verification during app bootstrap.
apps/native/src/navigation/RootNavigator.tsx Keeps splash visible during hydrating/checking to prevent entering the app before verification completes.
apps/native/src/apis/bareClient.ts Adds a middleware-free OpenAPI client for bootstrap/auth flows.
apps/native/src/apis/controller/student/auth/postRefreshToken.ts Switches refresh-token call to bareClient.
apps/native/src/apis/authMiddleware.ts Uses bareClient for /me calls (instead of raw fetch/env).
apps/native/src/apis/index.ts Exports bareClient from the APIs barrel.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +115 to +136
export const handleReinstallDetection = async () => {
if (!useSecureStore) return;

try {
const marker = await AsyncStorage.getItem(INSTALL_MARKER_KEY);

if (marker === null) {
const allKeys = Object.keys(memory) as (keyof AuthMemory)[];
await Promise.all(
allKeys.map(async (key) => {
const storageKey = buildKey(key);
try {
await SecureStore.deleteItemAsync(storageKey, secureStoreOptions);
await SecureStore.deleteItemAsync(storageKey);
} catch {
// noop
}
})
);

await AsyncStorage.setItem(INSTALL_MARKER_KEY, Date.now().toString());
}
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleReinstallDetection treats a missing AsyncStorage marker as a reinstall and deletes all SecureStore items. On the first app run after introducing this feature (i.e., an app update), existing users will also have no marker, so this will forcibly clear their tokens and also prevents the intended old→new keychain migration from ever running. Consider using a second marker persisted in SecureStore (e.g., an installId stored both in SecureStore and AsyncStorage) to distinguish “upgrade with no marker yet” from a true reinstall, and only clear keychain when SecureStore indicates a prior install but AsyncStorage is missing.

Copilot uses AI. Check for mistakes.
Comment on lines +62 to +63
name?: string;
grade?: string;
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

verifyStudentSession types name/grade as string, but StudentProfile elsewhere allows string | null and /me responses often model nullable fields. Returning null while typing as string forces downstream truthy checks and can lead to stale profile values. Update the return type to name?: string | null / grade?: string | null (and propagate that through the store) so nullability is handled explicitly.

Suggested change
name?: string;
grade?: string;
name?: string | null;
grade?: string | null;

Copilot uses AI. Check for mistakes.
Comment on lines +147 to +150
if (result.name !== undefined || result.grade !== undefined) {
if (result.name) await setName(result.name);
if (result.grade) await setGrade(result.grade);
}
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In verifySession, if (result.name) ... / if (result.grade) ... will skip persisting valid falsy values (e.g., empty string) and also won’t clear previously stored values when the server returns null. Prefer checking against undefined (and allowing null to be written) so local storage matches the server state.

Suggested change
if (result.name !== undefined || result.grade !== undefined) {
if (result.name) await setName(result.name);
if (result.grade) await setGrade(result.grade);
}
if (result.name !== undefined) {
await setName(result.name);
}
if (result.grade !== undefined) {
await setGrade(result.grade);
}

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +8
const { data, error } = await bareClient.POST('/api/student/auth/refresh', {
body: {
refreshToken: getRefreshToken() ?? '',
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When no refresh token exists, this sends refreshToken: '' to the refresh endpoint. That creates an avoidable network call and can make server-side logs/metrics noisy. Consider returning { isSuccess: false } early (or throwing) when getRefreshToken() is null/empty, and keep the request body strictly valid when a token is present.

Suggested change
const { data, error } = await bareClient.POST('/api/student/auth/refresh', {
body: {
refreshToken: getRefreshToken() ?? '',
const refreshToken = getRefreshToken();
if (!refreshToken) {
return { isSuccess: false, error: new Error('No refresh token') };
}
const { data, error } = await bareClient.POST('/api/student/auth/refresh', {
body: {
refreshToken,

Copilot uses AI. Check for mistakes.
Comment on lines 104 to 107
if (!isTeacher && !getName() && !getGrade()) {
const result = await fetch(`${env.apiBaseUrl}/api/student/me`, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
const { data } = await bareClient.GET('/api/student/me', {
headers: { Authorization: `Bearer ${accessToken}` },
});
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bareClient.GET('/api/student/me', ...) is executed even when accessToken is null (e.g., refresh failed and reissueStudentToken() returned null), resulting in an Authorization: Bearer null request. Add a guard like if (accessToken && ...) (or return early when token refresh fails) to avoid the extra call and confusing 401s.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐞 Fix 버그 수정 🔨 Refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[fix] 재설치 시 세션 초기화

1 participant