티스토리 뷰
구글 로그인을 진행하는데 Nonce 데이터를 추가하려고 한다.
<구글 로그인 참고>
[SwiftUI] 구글 로그인 구현
구글 로그인 구현 방법을 구글에서 정리해준 걸 참고하여 작성하였다. iOS 및 macOS용 Google 로그인 시작하기 | Authentication | Google for Developers중요: 2024년 5월 1일부터 Apple에서는 GoogleSignIn-iOS
steelbeartaeng2.tistory.com
이전에 만들었던 [카카오에 nonce 추가]를 보면 알 수 있듯이 직접 nonce를 만들어서 kakao에서 제공하는 로그인 요청의 nonce 파라미터에 넣어 보냈다.
[SwiftUI] - Kakao ID 토큰에 nonce 추가
프로젝트 진행 중 카카오 로그인 보안 강화를 위해 nonce를 추가하기로 결정하였다. Kakao Developers카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친
steelbeartaeng2.tistory.com
그래서 구글도 똑같이 클라이언트에서 nonce를 만들어 nonce 파라미터에 넣어서 보내려고 했는데 이럴수가~~~!!!!
구글은 보안을 위해 구글에서 직접 nonce를 만들어 idToken에 포함해 버린다는 것 ㅎㅎㅎㅎㅎ
🧐 nonce를 왜 추출해야 하는가???
구글은 로그인 요청을 처리할 때 자동으로 nonce 값을 추가하여 ID 토큰에 포함시킨다고 했다. 즉, 사용자가 로그인 요청을 보낼 때 구글 서버는 자동으로 고유한 nonce 값을 포함한 ID 토큰을 발급한다.
그럼 왜 클라이언트가 굳이 nonce를 추출해야 할까??
구글에서 발급한 ID 토큰은 이미 구글의 서버에서 유효성이 검증되었지만, 우리의 서버에서는 구글이 자동으로 설정한 nonce 값을 직접 검증할 수 없기 때문이다.
로직이 다음과 같다.
서버에서 클라이언트가 보낸 토큰에서 nonce 값을 추출하고, 이를 요청에 포함된 nonce와 비교함으로써, 토큰이 유효하고 안전한지 확인할 수 있다.
그래서,,, idToken을 직접 까보고 nonce를 추출해야 한다,, ㅎㅎ
그럼 이 idToken이 어떻게 이루어져 있는지 파악해 보자!
1. idToken
✏️ idToken
idToken은 JWT 타입이다. 그래서 받은 idToken을 JWT 사이트에 넣어보면 포맷을 확인할 수 있다.
idToken을 넣은 건 보안상 못 보여주고,,,
JWT 사이트에서 아래와 같이 기본적으로 데이터를 보여주고 있어 이 데이터를 사용해서 구조를 파악해 보자.
JWT는 클라이언트와 서버 간에 JSON 객체를 안전하게 전송하기 위한 표준이다. 일반적으로 세 부분으로 구성된 문자열로, 각 부분은 마침표(.)로 구분한다.
기본적으로 주어진 데이터를 확인해 보면 마침표(.)를 기준으로 구분하고 있는 걸 확인할 수 있다.
- Header (헤더): 토큰의 타입과 해싱 알고리즘 정보를 포함
- Payload (페이로드): 토큰에 담길 실제 데이터를 포함
- Signature (서명): 헤더와 페이로드를 합친 후 비밀 키로 서명한 값으로, 토큰의 무결성을 검증하는 데 사용
마침표(.)를 기준으로 구분한 세 부분은 순서대로 헤더, 페이로드, 서명으로 구분된다.
구글의 idToken 페이로드를 확인해보면 아래와 같다.
여기서 nonce의 값을 뽑아내려고 하면 idToken의 마침표(.)로 구분한 세 가지 부분 중 두 번째인 페이로드 부분에서 nonce 데이터를 찾으면 된다.
이제 좀 idToken의 구조와 nonce값이 있는 위치를 이해하였으니 nonce를 추출해 보자.
2. nonce 추출
Google Sign-In SDK를 사용하는 iOS 앱에서 현재 로그인된 Google 사용자를 확인하는 로직을 넣어주고 user의 idToken을 가져와보자.
if GIDSignIn.sharedInstance.currentUser != nil {
let user = GIDSignIn.sharedInstance.currentUser
guard let user = user else {
return
}
let idToken = user.idToken
}
1️⃣ idToken 마침표(.)로 구분
마침표(.)으로 구분하여 헤더, 페이로드, 서명을 나누어준다.
let jwtParts = idToken?.tokenString.components(separatedBy: ".")
2️⃣ JWT 형식 확인 및 Base64 디코딩
코드에서 조건을 정리하면 아래와 같다.
- jwtParts?.count == 3 으로 JWT가 세 부분(Header, Payload, Signature)으로 구성되었는지 확인하다.
- let payloadData = Data(base64Encoded: jwtParts?[1] ?? "", options: .ignoreUnknownCharacters) 는 페이로드(jwtParts?[1])를 Base64로 디코딩하여 Data 객체로 변환한다.
- 여기서 options: .ignoreUnknownCharacters는 Base64 디코딩 중 알 수 없는 문자를 무시하도록 설정하는 옵션이다.
최종적으로 guard 구문은 위 조건들이 모두 만족하지 않으면(즉, JWT 형식이 올바르지 않거나 Base64 디코딩에 실패한 경우), 오류 메시지를 출력하고 함수 실행을 종료한다.
guard jwtParts?.count == 3, let payloadData = Data(base64Encoded: jwtParts?[1] ?? "", options: .ignoreUnknownCharacters) else {
print("Invalid JWT format")
return
}
3️⃣ JSON 디코딩 및 nonce 값 추출
JWT의 페이로드 부분을 JSON 형식으로 디코딩하고, 그 JSON 데이터에서 nonce 값을 추출하는 작업을 수행한다.
만약 JSON 디코딩 과정에서 오류가 발생하면, 오류를 출력하고 성공하면 추출한 nonce 데이터를 저장한다.
do {
let payloadJSON = try JSONSerialization.jsonObject(with: payloadData, options: []) as? [String: Any]
let nonce = payloadJSON?["nonce"] as? String ?? ""
} catch {
print("Error decoding JSON: \(error)")
}
자 여기까지 구현하고 실행해보면 nonce값을 잘 받아오고 실행하는데 아무 문제가 없다~~~ 라고 생각한 내가 오산이었다.
팀원들 중 몇명은 성공하고 몇명은 2️⃣번 과정에서 오류가 나서 nonce를 추출하는 3️⃣번까지 진행조차 하지 못하는 오류를 발견하였다....
그래서 원인을 파악하다가 두가지 이유를 알 수 있었다.
- 패딩 문제: Base64 디코딩 시 문자열의 길이가 4의 배수가 아닐 경우 문제가 발생할 수 있어, = 으로 패딩 값을 채워줘야 한다.
- URL-safe Base64 인코딩: JWT는 일반적으로 URL-safe Base64 인코딩을 사용하므로, -를 +로, _를 /로 대체해야 한다.
문자열 길이가 4의 배수가 아닌 경우 문제가 발생할 수 있고, 특정 기호를 다른 기호로 대체해야한다는 것도 처음 알았다,,
이때까지 성공했던 몇명은 그냥 위의 두 가지 이유를 잘 피해갔던 사례인 것 같다,, 그래서 두 문제를 해결하기 위해 2️⃣번 과정을 아래와 같이 수정했다.
2️⃣ Base64URL 인코딩을 Base64 인코딩으로 변환
JWT의 페이로드 부분(jwtParts?[1])은 Base64URL 형식으로 인코딩되어 있다.
Base64URL 인코딩에서는 + 대신 -, / 대신 _를 사용한다. 이 코드는 -를 +로, _를 /로 대체하여 일반적인 Base64 형식으로 변환하도록 한다.
let payloadBase64 = jwtParts?[1].replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
Base64 인코딩 문자열의 길이는 4의 배수여야 한다. 이 코드는 문자열 길이를 4의 배수로 맞추기 위해 필요한 경우 = 문자를 추가하여 패딩을 적용힌다.
let payloadPadded = payloadBase64!.padding(toLength: ((payloadBase64!.count + 3) / 4) * 4, withPad: "=", startingAt: 0)
🤔 왜 4의 배수, = 문자를 사용하는가?
Base64 인코딩에서 패딩(=)은 데이터를 3바이트(24비트) 단위로 맞추기 위해 사용되는데 Base64는 6비트씩 데이터를 분할하여 ASCII 문자로 변환하는 방식이므로, 인코딩된 문자열의 길이가 4의 배수여야 한다.
인코딩할 데이터의 길이가 3바이트(24비트)의 배수가 아닐 경우, Base64 인코딩에서 6비트 단위로 정확히 나누어지지 않게 되어서 패딩을 사용해서라도 문자열의 길이를 맞춰줘야 한다.
'='은 bit 수를 맞춰주기 위해 0으로 채워주는 역할을 하는 하나의 약속이라고 생각하면 된다고 한다~
🤔 Base64URL과 Base64의 차이
🟡 Base64
Base64와 Base64URL의 가장 큰 차이점은 62번, 63번, pad 부분이다.
URL에서는 +, /, = 는 특수하게 예약된 제어문자이다. + 는 띄어쓰기(공백문자, 스페이스)를 의미하며, / 는 URL 디렉토리 간의 경로 구분자이며, = 는 파라미터에서 name 과 value 사이에 쓰이는 기호이다.
Base64의 기호를 URL에서 사용하면 문제가 있으니 그래서 나온 것이 Base64URL 이라고 한다.
🟡 Base64URL
62번, 63번, pad 부분을 보면 Base64에 +와 / 이었던 것이 -와 _ 가 되었다.
하지만 = 는 그대로인데 그 이유는 URL 에서 어차피 padding은 제일 마지막 부분에 위치하기 때문에 브라우저에서 자동으로 그것을 특수한 제어문자로 인식하지 않고 URL 인코딩해서 %3D가 된다고 한다.
- 문자 차이: Base64는 +, /를 사용하지만, Base64URL은 -, _로 대체한다.
- 패딩 차이: Base64는 = 패딩을 사용하지만, Base64URL은 생략하거나 제어문자로 인식하지 않도록 한다.
이렇게 해서 구글 로그인에서 nonce값 추출하기는 이렇게 성공적으로 끝낼 수 있었고, 추출한 nonce로 백엔드에 요청할 때 같이 넣어서 보낼 수 있었다!!
참고
https://domdom.tistory.com/155
https://developers.google.com/identity/openid-connect/openid-connect?hl=ko
'스위프트 > SwiftUI' 카테고리의 다른 글
[SwiftUI] - TabView 렌더링과 LazyView 사용법 (0) | 2024.09.29 |
---|---|
[SwiftUI] - Clean Architecture 도입 전 개념 정리 (0) | 2024.09.21 |
[SwiftUI] - Object Storage presigned url 발급 (0) | 2024.08.08 |
[SwiftUI] - 리스트 항목의 터치 영역 설정 (0) | 2024.07.08 |
[SwiftUI] - 디바이스 모델명, OS, 앱 버전 출력 (0) | 2024.06.02 |