diff --git a/services/api.ts b/services/api.ts index 5a886e2..b470690 100644 --- a/services/api.ts +++ b/services/api.ts @@ -6,7 +6,7 @@ import { ApiErrorResponse } from '@/types/club.types'; import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; import * as Application from 'expo-application'; import Constants from 'expo-constants'; -import { getOrCreateAuthSubject, getStoredAccessToken, saveAccessToken } from './auth-token-storage'; +import { getStoredAccessToken, resolveAuthSubject, saveAccessToken } from './auth-token-storage'; const APP_VERSION_HEADER_KEY = 'APP_VERSION'; @@ -160,7 +160,7 @@ function attachInterceptors(client: AxiosInstance, options: { withAuth: boolean if (!refreshTokenPromise) { refreshTokenPromise = (async () => { try { - const sub = await getOrCreateAuthSubject(); + const sub = await resolveAuthSubject(); const iat = Math.floor(Date.now() / 1000); const response = await publicApiClient.post('/auth/student', { sub, iat }); const nextToken = extractAccessToken(response.data); diff --git a/services/auth-token-storage.ts b/services/auth-token-storage.ts index 0d9220a..b01d929 100644 --- a/services/auth-token-storage.ts +++ b/services/auth-token-storage.ts @@ -6,13 +6,20 @@ const AUTH_SUBJECT_KEY = '@auth_subject'; let cachedAccessToken: string | null | undefined; let cachedAuthSubject: string | null | undefined; +// sub는 서버가 발급하는 신원(studentId)의 근거가 되므로 추측 가능한 값이면 안 된다. +// crypto.getRandomValues는 app/_layout.tsx의 react-native-get-random-values로 폴리필된다. function generateUuidV4(): string { - const template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'; - return template.replace(/[xy]/g, (char) => { - const random = Math.floor(Math.random() * 16); - const value = char === 'x' ? random : (random & 0x3) | 0x8; - return value.toString(16); - }); + if (typeof globalThis.crypto?.getRandomValues !== 'function') { + throw new Error('crypto.getRandomValues를 사용할 수 없어 auth subject를 생성할 수 없습니다.'); + } + + const bytes = new Uint8Array(16); + globalThis.crypto.getRandomValues(bytes); + bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4 + bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 10 + + const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join(''); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; } export async function getStoredAccessToken(): Promise { @@ -87,3 +94,18 @@ export function getJwtSubject(accessToken: string): string | null { return null; } } + +/** + * /auth/student 요청 본문에 실을 sub. + * + * 서버가 요청 sub를 무시하고 자체 UUID로 신원을 만들던 시절이 있어서, 기존 설치는 + * 저장된 @auth_subject와 토큰 안의 신원이 서로 다른 값이다. 실제 편지함이 달린 신원은 + * 토큰 쪽이므로, 저장된 토큰이 있으면 그 payload.sub를 그대로 다시 보낸다. + * 토큰이 없는 신규 설치에서만 @auth_subject를 쓴다. + */ +export async function resolveAuthSubject(): Promise { + const accessToken = await getStoredAccessToken(); + const tokenSubject = accessToken ? getJwtSubject(accessToken) : null; + + return tokenSubject ?? getOrCreateAuthSubject(); +} diff --git a/services/auth-token.service.ts b/services/auth-token.service.ts index 922f69c..1f05fcd 100644 --- a/services/auth-token.service.ts +++ b/services/auth-token.service.ts @@ -1,5 +1,5 @@ import { publicApi } from './api'; -import { getOrCreateAuthSubject, getStoredAccessToken, saveAccessToken } from './auth-token-storage'; +import { getStoredAccessToken, resolveAuthSubject, saveAccessToken } from './auth-token-storage'; type IssueAccessTokenResponse = | { @@ -46,7 +46,7 @@ function extractAccessToken(response: IssueAccessTokenResponse): string | null { export async function issueAccessToken(): Promise { const payload: IssueAccessTokenPayload = { - sub: await getOrCreateAuthSubject(), + sub: await resolveAuthSubject(), iat: Math.floor(Date.now() / 1000), };