feat(auth): 애플 로그인 nonce를 iOS와 동일하게 수정
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
- [x] 환경변수 설정 추가 가이드: VUE_APP_APPLE_CLIENT_ID, VUE_APP_APPLE_REDIRECT_URI (.env.*)
|
||||
- [x] 서버 엔드포인트 요청 형식 정합화: POST /member/login/apple 본문에 container/identityToken/nonce 전달
|
||||
- [x] 트러블슈팅 가이드 추가: 403 에러 및 200 응답 후 중단 현상 원인 분석
|
||||
- [x] nonce 정합화 보완: iOS와 동일한 raw nonce 생성 규칙 + SHA-256(Base64URL) 적용
|
||||
- [ ] QA/수동검증: 실제 Apple 계정으로 팝업 로그인 플로우 확인 및 토큰 교환 성공 확인(테스트 서버 배포 후)
|
||||
|
||||
## 범위 변경 사항
|
||||
@@ -45,10 +46,10 @@ Content-Type: application/json
|
||||
```
|
||||
|
||||
- container 값은 web로 고정합니다.
|
||||
- nonce는 프론트에서 매 로그인 시점에 보안 난수로 생성합니다(raw). Apple에 전달하는 nonce는 SHA-256 해시(hex)로 보냅니다. 서버에서는 raw nonce를 받아 동일한 방식으로 해시하여 id_token 내 nonce와 일치하는지 검증합니다.
|
||||
- nonce는 프론트에서 매 로그인 시점에 보안 난수로 생성합니다(raw). Apple에 전달하는 nonce는 SHA-256 해시(Base64URL)로 보냅니다. 서버에서는 raw nonce를 받아 동일한 방식으로 해시하여 id_token 내 nonce와 일치하는지 검증합니다.
|
||||
|
||||
코드 반영 사항:
|
||||
- src/views/Login/Login.vue: 로그인 직전 raw nonce 생성 → SHA-256 해시(hex)를 AppleID.auth.init의 nonce로 설정 → signIn 후 id_token과 raw nonce를 서버로 전송.
|
||||
- src/views/Login/Login.vue: 로그인 직전 raw nonce 생성(iOS와 동일한 문자셋/샘플링 규칙) → SHA-256 해시(Base64URL)를 AppleID.auth.init의 nonce로 설정 → signIn 후 id_token과 raw nonce를 서버로 전송.
|
||||
- src/api/member.js: Authorization 헤더 제거. 요청 본문으로 { container: 'web', identityToken, nonce, ... } 전송.
|
||||
|
||||
## 환경변수 설정(redirect URI, apple_client_id)
|
||||
@@ -185,5 +186,17 @@ Content-Type: application/json
|
||||
- 조치: Apple Return URLs에 등록된 도메인으로 앱을 실제 배포하여 동일 오리진 환경에서 검증할 것을 권장함
|
||||
- 결과: 테스트 서버 배포 시 오리진 불일치 문제가 해결되어 정상적인 Promise resolve 및 서버 전송이 가능해짐
|
||||
|
||||
### 4차 수정(nonce 불일치 이슈 정합화)
|
||||
- 무엇을: Web의 nonce 생성/해시 로직을 iOS 앱에서 사용하는 규칙과 동일하게 수정
|
||||
- 왜: Apple 로그인 시 서버 검증 단계에서 `invalid nonce`가 발생하는 문제를 해소하기 위해
|
||||
- 어떻게:
|
||||
- 코드: `src/views/Login/Login.vue`
|
||||
- `generateNonce`: iOS와 동일한 문자셋(`0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._`) + 16바이트 샘플링 방식으로 raw nonce 생성
|
||||
- `sha256Hex`: SHA-256 결과를 hex가 아닌 Base64URL(`+`→`-`, `/`→`_`, `=` 제거)로 인코딩하도록 변경
|
||||
- 해시 기능 미지원 브라우저에서는 예외를 발생시켜 잘못된 nonce 전송을 방지
|
||||
- 검증 명령:
|
||||
- `npm run lint` → 성공
|
||||
- 결과: Apple SDK에 전달되는 nonce 포맷이 iOS와 동일해져 서버 nonce 검증 정합성 개선
|
||||
|
||||
## 정정/메모
|
||||
- 초기 계획 문서는 구현 직후 정리되었습니다. 향후 환경변수/서버 설정 완료 시 체크박스 및 검증 기록을 추가 업데이트하세요.
|
||||
|
||||
@@ -104,30 +104,61 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 보안을 위한 랜덤 nonce 생성(Base64URL)
|
||||
// iOS randomNonceString과 동일한 규칙으로 raw nonce 생성
|
||||
generateNonce(length = 32) {
|
||||
try {
|
||||
const bytes = new Uint8Array(length);
|
||||
(window.crypto || window.msCrypto).getRandomValues(bytes);
|
||||
let binary = '';
|
||||
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
||||
// base64url 인코딩
|
||||
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
} catch (e) {
|
||||
// crypto 사용 불가 시 폴백(난수품질 낮음)
|
||||
return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
|
||||
if (length <= 0) {
|
||||
throw new Error('Nonce length must be greater than zero.');
|
||||
}
|
||||
|
||||
const crypto = window.crypto || window.msCrypto;
|
||||
const charset = '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._';
|
||||
let result = '';
|
||||
let remainingLength = length;
|
||||
|
||||
while (remainingLength > 0) {
|
||||
const randoms = new Uint8Array(16);
|
||||
|
||||
try {
|
||||
crypto.getRandomValues(randoms);
|
||||
} catch (e) {
|
||||
// iOS 코드와 동일하게 난수 생성 실패 시 UUID 폴백
|
||||
if (crypto && typeof crypto.randomUUID === 'function') {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11)
|
||||
.replace(/[018]/g, c => (
|
||||
c ^ ((Math.random() * 16) >> (c / 4))
|
||||
).toString(16));
|
||||
}
|
||||
|
||||
for (let i = 0; i < randoms.length && remainingLength > 0; i++) {
|
||||
if (randoms[i] < charset.length) {
|
||||
result += charset[randoms[i]];
|
||||
remainingLength -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
async sha256Hex(message) {
|
||||
if (window.crypto && window.crypto.subtle && typeof TextEncoder !== 'undefined') {
|
||||
if (!(window.crypto && window.crypto.subtle && typeof TextEncoder !== 'undefined')) {
|
||||
throw new Error('SHA-256 해시를 지원하지 않는 환경입니다.');
|
||||
}
|
||||
|
||||
const enc = new TextEncoder().encode(message);
|
||||
const buf = await window.crypto.subtle.digest('SHA-256', enc);
|
||||
const arr = Array.from(new Uint8Array(buf));
|
||||
return arr.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
const arr = new Uint8Array(buf);
|
||||
let binary = '';
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
binary += String.fromCharCode(arr[i]);
|
||||
}
|
||||
// 폴백: 해시 불가 시 원본 반환(해시 미적용)
|
||||
return message;
|
||||
|
||||
return btoa(binary)
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/, '');
|
||||
},
|
||||
|
||||
initAppleLogin() {
|
||||
|
||||
Reference in New Issue
Block a user