Google OAuth vs Apple OAuth 완벽 가이드
목차
1. 기본 OAuth 흐름 차이
Google OAuth
특징
- 리프레시 토큰: 항상 제공
- 사용자 정보: 매번 조회 가능
- 토큰 갱신: 리프레시 토큰으로 언제든지 재발급
- 유연성: 높음
동작 방식
- 사용자가 Google 계정으로 로그인
- Authorization Code 발급
- Access Token + Refresh Token 발급
- 사용자 정보 조회 (UserInfo API)
- Access Token 만료 시 Refresh Token으로 재발급
- 필요할 때마다 사용자 정보 재조회 가능
Apple OAuth
특징
- 리프레시 토큰: 최초 로그인 시에만 제공
- 사용자 정보: 최초 로그인 시에만 제공
- 토큰 갱신: 리프레시 토큰이 없으면 재로그인 필요
- 보안 중심: 사용자 프라이버시를 더 엄격하게 보호
동작 방식
- 사용자가 Apple 계정으로 로그인
- Authorization Code 발급
- 최초 로그인 시: Access Token + Refresh Token + User Info 제공
- 재로그인 시: Access Token만 제공 (Refresh Token, User Info 없음)
- Refresh Token이 없으면 사용자 재로그인 필요
핵심 차이
| 항목 | Apple | |
|---|---|---|
| 리프레시 토큰 | 매번 제공 | 최초 1회만 |
| 사용자 정보 | 매번 조회 가능 | 최초 1회만 |
| 데이터 관리 | 유연함 | 반드시 최초 저장 필수 |
2. Client Secret 생성 방식 차이
핵심 요약
| 항목 | Apple | |
|---|---|---|
| 생성 방식 | 동적 JWT 생성 | 정적 문자열 |
| 알고리즘 | ES256 (ECDSA) | 없음 (평문) |
| 암호화 방식 | 비대칭 (Public/Private Key) | 대칭 (Shared Secret) |
| 유효기간 | 최대 6개월 (권장 1시간) | 영구 |
| 보안성 | 높음 | 중간 |
Apple OAuth: 동적 Client Secret (JWT)
생성 방식
JWT를 **ES256 알고리즘 (ECDSA with SHA-256)**으로 서명하여 생성합니다.
출처: scottbrady.io (opens in a new tab)
"Apple does not support shared secrets for client authorization, instead using a custom implementation similar to JWT Bearer Token for Client Authentication where developers must generate and sign their own credentials."
JWT 구조
Header.Payload.Signature- Header:
{"alg": "ES256", "kid": "ABC123DEF4"} - Payload:
{"iss": "TEAM_ID", "aud": "https://appleid.apple.com", "sub": "CLIENT_ID", "exp": 1516242622} - Signature: Private Key로 서명한 값
필요한 정보
4가지 필수 파라미터
Team ID: Apple Developer 계정 식별자Client ID: Service Identifier (예: com.example.app)Key ID: Private Key 식별자Private Key: .p8 파일 형태의 ECDSA Private Key
JWT 필수 클레임
출처: bannister.me (opens in a new tab)
"The client secret JWT must include four mandatory claims:
- iss (Issuer): Your Apple Team ID
- aud (Audience): "https://appleid.apple.com (opens in a new tab)"
- sub (Subject): Your Service ID (client_id)
- exp (Expiry): Maximum of 6 months"
코드 예시
public getClientSecret(): string {
return appleSignin.getClientSecret({
clientID: this.clientId, // Service Identifier
keyIdentifier: this.keyId, // Key ID
teamID: this.teamId, // Team ID
privateKey: this.privateKey, // .p8 파일의 Private Key
});
}보안 이점
비대칭 암호화
출처: criipto.com (opens in a new tab)
"Private Key JWT significantly reduces the attack surface for credential theft by using asymmetric cryptography where your application keeps a private key to itself and shares only its corresponding public key with the authorization server. The private key never leaves your application."
재생 공격 방지
출처: criipto.com (opens in a new tab)
"Private Key JWT uses specific JWT claims (like jti for unique IDs and exp for expiration) to prevent replay attacks, and has the security advantage of shorter lifetime of the client_assertion—usually a maximum of an hour."
권장 사항: 매 요청마다 생성
출처: bannister.me (opens in a new tab)
"A better approach is to generate the JWT for your client secret on each request. It is very fast to generate and will not add any noticeable performance decrease to your requests."
Google OAuth: 정적 Client Secret
생성 방식
Google API Console에서 OAuth 클라이언트 생성 시 자동으로 발급됩니다.
출처: Google Cloud Console Help (opens in a new tab)
"Visiting the Google API Console to obtain OAuth 2.0 credentials such as a client ID and client secret that are known to both Google and your application."
코드 예시
// 환경 변수에서 한 번만 로드
private readonly clientSecret: string;
constructor() {
this.clientSecret = process.env.GOOGLE_CLIENT_SECRET; // 정적 값
}
// OAuth 요청 시 그대로 사용
async exchangeCodeForTokens(code: string) {
const response = await httpClient.post('https://oauth2.googleapis.com/token', {
client_id: this.clientId,
client_secret: this.clientSecret, // 평문 문자열
code: code,
grant_type: 'authorization_code',
redirect_uri: this.redirectUri,
});
}보안 고려사항
유출 시 위험
출처: criipto.com (opens in a new tab)
"If the secret is leaked, intercepted, or stolen, anyone can impersonate your application, and client secrets are usually long-lived and sent in plaintext over the wire."
서버 환경에서만 사용
출처: oauth.net (opens in a new tab)
"The client secret must be kept confidential. If a deployed app cannot keep the secret confidential, such as single-page Javascript apps or native apps, then the secret is not used."
3. Authorization Code Flow 상세 설명
전체 흐름 (시퀀스)
[사용자] → [클라이언트 앱] → [Apple/Google 로그인] → [Authorization Code 발급]
→ [백엔드 서버] → [Token 교환 요청] → [Apple/Google 검증] → [Access Token 발급]상세 단계
- 사용자가 "Apple/Google로 로그인" 버튼 클릭
- 클라이언트가 사용자를 Authorization Server로 리다이렉트
- 사용자가 인증 (Face ID, 비밀번호 등)
- Authorization Server가 Authorization Code 생성
- Redirect URI로 사용자를 리다이렉트 (Code 포함)
- 클라이언트가 Code를 백엔드 서버로 전송
- 백엔드가 Code를 Token Endpoint로 전송 (+ Client Secret)
- Authorization Server가 검증 후 Access Token, Refresh Token, ID Token 발급
Authorization Code란?
특성
출처: RFC 6749 - Section 4.1.2 (opens in a new tab)
"The authorization code MUST expire shortly after it is issued to mitigate the risk of leaks. A maximum authorization code lifetime of 10 minutes is RECOMMENDED."
출처: Apple Developer Documentation (opens in a new tab)
"The authorization code has a five (5) minute expiry and is restricted to single-use."
보안 특성
- 유효기간: Apple 5분, OAuth 표준 최대 10분
- Single-use: 한 번만 사용 가능, 재사용 시 모든 토큰 무효화
- Opaque String: 읽을 수 없는 불투명한 문자열
- Client-specific: 발급받은 클라이언트만 사용 가능
Code에 연결된 정보 (서버 내부 저장)
- 사용자 식별자
- client_id
- redirect_uri
- scope
- 발급 시간
- 사용 여부
Token 교환 과정
요청 예시
POST https://appleid.apple.com/auth/token
Content-Type: application/x-www-form-urlencoded
client_id=com.example.app
&client_secret=eyJhbGciOiJFUzI1NiIsImtpZCI6IkFCQzEyMyJ9...
&code=c1234567890abcdef
&grant_type=authorization_code
&redirect_uri=https://example.com/callbackApple/Google이 검증하는 항목
출처: RFC 6749 - Section 4.1.3 (opens in a new tab)
"The authorization server MUST:
- require client authentication for confidential clients
- authenticate the client if client authentication is included
- ensure that the authorization code was issued to the authenticated client
- verify that the authorization code is valid
- ensure that the redirect_uri parameter is present if included in the initial request"
1. client_id 검증
- 등록된 앱인지 확인
2. client_secret 검증
Apple의 경우 (JWT):
- JWT 구조 파싱 (Header, Payload, Signature)
- 알고리즘이 ES256인지 확인
- JWT Header의
kid로 Public Key 조회 - Public Key로 서명 검증
- Claims 검증:
iss: Team ID 확인aud: "https://appleid.apple.com (opens in a new tab)" 확인sub: client_id와 일치 확인exp: 만료되지 않았는지 확인
Google의 경우 (평문):
- 데이터베이스에 저장된 Secret과 일치하는지 확인
3. code 유효성 검증
- 데이터베이스에서 code 조회
- 만료되지 않았는지 확인 (5분 이내)
- 이전에 사용되지 않았는지 확인
- 요청한 client_id와 code 발급 시 client_id 일치 확인
4. redirect_uri 검증
- Code 발급 시 사용된 redirect_uri와 동일한지 확인
Apple의 JWT 서명 검증 상세
Public Key 획득
출처: Apple Developer Documentation (opens in a new tab)
"Apple's public keys are available at https://appleid.apple.com/auth/keys (opens in a new tab)"
Apple은 개발자가 Private Key를 생성할 때 Public Key를 내부 데이터베이스에 저장합니다.
검증 과정
출처: Sarunw (opens in a new tab)
"Use the kid claim from the JWT header to determine which key you need. Your backend has to check the header to get the correct kid to get the correct key from the auth/keys endpoint."
- JWT 파싱:
Header.Payload.Signature로 분리 kid추출: JWT Header에서 Key ID 추출- Public Key 선택: Apple 데이터베이스에서 해당
kid의 Public Key 조회 - 서명 검증:
- Header + Payload를 Base64URL 디코딩
- ECDSA P-256 곡선 + SHA-256 해시 알고리즘 사용
- Public Key로 Signature 검증
- 서명이 유효하면 해당 Private Key로 서명되었음을 증명
응답 (Token)
{
"access_token": "beg3456...67Or9",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "abc789...xyz123",
"id_token": "eyJraWQiOiJlWGF1bm1MIiwiYWxnIjoiUlMyNTYifQ..."
}출처: Medium - Apple Sign-In (opens in a new tab)
"access_token - A token reserved for future use; currently no data set has been defined for access, valid for an hour
id_token - A JSON Web Token that contains the user's identity information
refresh_token - The refresh token used to regenerate new access tokens"
토큰 설명
access_token: 현재 미래 사용 예약, 1시간 유효id_token: 사용자 신원 정보 (이메일, sub, name 등)refresh_token: Access Token 갱신용expires_in: 3600초 (1시간)
왜 Authorization Code를 사용하는가?
1. 브라우저 히스토리 보호
출처: StackOverflow (opens in a new tab)
"The Authorization Code Grant Type keeps sensitive information from the browser history. When the authorization server redirects back to the application, the URL of this redirect ends up in the browser history, so using a short-lived code instead of the actual token prevents the access token from being logged."
2. 중간자 공격 방지
출처: Auth0 (opens in a new tab)
"The client app, (browser or native app), can have the delivered token intercepted. In order to reduce this threat, short-lived authorization codes are passed instead of tokens and exchanged for tokens over a more secure direct connection between client and authorization server."
3. XSS/Malware 위험 감소
출처: OAuth 2.0 Simplified (opens in a new tab)
"The access token is never visible to the user or their browser, so it is the most secure way to pass the token back to the application, reducing the risk of the token leaking to someone else."
왜 Client Secret이 필요한가?
출처: StackOverflow (opens in a new tab)
"Even if the attacker stole the authorization code the attacker would not be able to create an access token because the attacker would need to have access to your client_secret too."
핵심 이유
- 클라이언트 인증: Code만으로는 누가 요청하는지 알 수 없음
- Code 탈취 방지: Code를 가로채도 Secret 없이는 Token 획득 불가
- 2단계 보안: Code + Secret 조합으로 보안 강화
왜 Redirect URI를 검증하는가?
출처: Information Security StackExchange (opens in a new tab)
"Redirect URI validation prevents OAuth providers from being used as phishing vectors, as the redirect_uri is an address used by OAuth providers to deliver the access_token via browser redirect."
핵심 이유
- Code 탈취 방지: 공격자가 자신의 redirect_uri로 Code를 받으려는 시도 차단
- 피싱 방지: 악의적인 앱이 정상 앱으로 위장하는 것 방지
- Open Redirect 공격 방지: 임의의 URL로 리다이렉트하는 것 차단
4. 보안 Best Practices
1. PKCE (Proof Key for Code Exchange) 사용
출처: RFC 9126 (opens in a new tab)
"One of the key recommendations from OAuth 2.0 Security Best Current Practice (RFC 9126) is to use PKCE for all OAuth clients, including confidential clients, to protect against code interception attacks."
PKCE 추가 파라미터
code_challenge: SHA256(code_verifier)의 Base64URL 인코딩code_challenge_method: "S256"code_verifier: 랜덤 생성된 43-128자 문자열
2. State 파라미터로 CSRF 방지
Authorization 요청 시 랜덤 state 값을 생성하고, 콜백에서 동일한 값이 돌아오는지 검증합니다.
3. Nonce 사용 (OpenID Connect)
id_token의 nonce claim을 검증하여 재생 공격을 방지합니다.
4. HTTPS 필수
모든 OAuth 2.0 통신은 반드시 HTTPS를 사용해야 합니다.
5. Token 저장 보안
- Refresh Token: 서버 데이터베이스에 암호화하여 저장
- Access Token: 메모리 또는 암호화된 쿠키에 저장
- 절대 금지: localStorage에 토큰 저장 (XSS 취약)
5. 참고 문서
OAuth 2.0 표준
- RFC 6749 - The OAuth 2.0 Authorization Framework (opens in a new tab)
- OAuth 2.0 Simplified (opens in a new tab)
Apple Sign In
- Apple Developer - Generate and validate tokens (opens in a new tab)
- Apple Developer - TokenResponse (opens in a new tab)
- Apple Developer - Fetch Apple's public key (opens in a new tab)
- bannister.me - Generating Client Secret (opens in a new tab)
- scottbrady.io - Implementing Sign in with Apple (opens in a new tab)
Google OAuth
보안
- ubisecure.com - Private Key JWT vs Client Secret (opens in a new tab)
- criipto.com - Private Key JWT (opens in a new tab)
- Auth0 - Authorization Code Flow (opens in a new tab)
요약 테이블
기본 OAuth 흐름 비교
| 항목 | Apple | |
|---|---|---|
| 리프레시 토큰 | 매번 제공 | 최초 1회만 |
| 사용자 정보 | 매번 조회 가능 | 최초 1회만 |
| 구현 난이도 | 쉬움 | 중간 (최초 저장 필수) |
Client Secret 비교
| 항목 | Apple | |
|---|---|---|
| 타입 | 정적 문자열 | 동적 JWT |
| 알고리즘 | 없음 | ES256 (ECDSA) |
| 암호화 | 대칭 (Shared Secret) | 비대칭 (Public/Private Key) |
| 유효기간 | 영구 | 최대 6개월 (권장 1시간) |
| 보안성 | 중간 | 높음 |
| 구현 복잡도 | 낮음 | 높음 |
Authorization Code 비교
| 항목 | OAuth 표준 | Apple |
|---|---|---|
| 유효기간 | 최대 10분 권장 | 5분 |
| 사용 횟수 | 1회 | 1회 |
| 재사용 시 | 에러 | 모든 토큰 무효화 |