Apple 로그인 구현과 검증
Swift 기반 iOS 앱 프로젝트를 진행하다보면 OAuth2 로그인을 구현해야 하는 경우가 있다. 특히, Apple App Store에 OAuth2가 포함된 iOS 앱을 출시하려면 반드시 Apple 로그인을 포함해야 앱 심사가 통과된다. 그래서 iOS 앱에서 OAuth2 로그인을 구현한 앱들은 거의 대부분 Apple 로그인이 구현되어 있다.
Apple 로그인을 통해 사용자가 로그인하게 되면 토큰(Token) 값을 얻을 수 있는데 이 토큰을 바탕으로 인증된 사용자가 로그인을 시도한 것인지 Apple 서버로부터 검증할 수 있다. 이러한 검증 과정이 있어야 부정한 로그인을 방지하고, 인증된 사용자인지 확인하여 신뢰할 수 있는 로그인을 사용자에게 제공할 수 있다. 이러한 Apple 로그인을 구현하는 과정에서 토큰을 얻는 과정과 이러한 토큰을 어떻게 인증해야 하는지 알아보면서 Apple 로그인 구현부터 토큰 검증에 대해 알아보자.
Apple 로그인 구현 전 필요 조건
1. Apple Developers 가입
2. Apple Developer Program 가입 및 결제
Apple 로그인을 위한 Apple Developers 설정
1. Apple Developers에 접속하여 'Certificate, Identifiers & Profiles'로 이동
2. 'Certificate, Identifiers & Profiles' 화면에서 좌측 메뉴에서 'Identifiers' 클릭 후 '+' 버튼 클릭
3. 'App IDs' 옵션 선택 후 'Continue' 클릭
4. 'App' 선택 후 'Continue' 클릭
5. 'Description' 및 'Bundle ID'(iOS 앱 프로젝트의 App Bundle ID, Xcode에서 확인 가능) 작성
6. 스크롤을 내려 'Sign in with Apple' 항목 체크
7. 'Continue' 클릭
8. 'Register' 클릭
(옵션) Firebase 설정 (Firebase를 통한 Apple 로그인 관리 시 필요)
* 필요 선행 작업 *
- Firebase 프로젝트 생성
- Apple 로그인 기능을 탑재하려는 iOS 프로젝트에 'GoogleService.info' 파일 추가
1. Firebase 프로젝트 -> Authentication 메뉴 이동 -> '로그인 방법 설정' 클릭
2. 'Apple' 클릭
3. '사용 설정' 활성화
4. Firebase 프로젝트 도메인 확인 후 복사
5. '저장' 클릭 (서비스 ID는 Android나 Web 환경에서 Apple 로그인 구현 시 작성 필요)
6. Apple Developers의 'Services' 메뉴 화면에서 'Configure' 클릭
7. 'Email Sources'의 '+' 버튼 클릭
8. Firebase에서 복사한 프로젝트 도메인 주소를 'Domains and Subdomains'에 입력
9. Firebase Authentication의 'Templates' 탭의 '발신 주소' 이메일 복사
10. Firebase에서 복사한 '발신 주소' 이메일을 'Email Addresses' 필드에 입력 후 'Next' 클릭하여 등록
Xcode 내 프로젝트 설정
1. Apple 로그인을 구현할 iOS 프로젝트 오픈 후, 'TARGETS'의 프로젝트 이름 클릭 및 'Capability' 클릭
2. 'Sign in with Apple' 검색 후 클릭하여 추가
Apple 로그인 구현 코드 (Swift)
Swift를 통한 Apple 로그인을 구현하는데 잘 정리된 블로그가 있기에 아래 링크를 첨부하여 대체하겠다.
- 참고
https://medium.com/hcleedev/swift-apple-로그인-firebase와-연동해-만들기-swiftui-9e0e3106d4c5
Swift: Apple 로그인 Firebase와 연동해 만들기(SwiftUI)
SwiftUI에서 Apple 로그인을 이용하고, Firebase와 연동하는 법까지 알아보자
medium.com
Apple 로그인 후 얻는 정보
로그인을 통해 얻을 수 있는 정보는 아래와 같다.
- authorization_code : Apple 서버로부터 Apple 로그인 결과를 검증하기 위한 코드
- id_token : 유저 정보를 확인할 수 있는 JWT 형식 Token (참고 : https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple)
- email : 로그인한 유저의 Email
- fullName, givenName, familyName : 로그인한 유저의 이름 정보
Apple로부터 얻은 토큰을 검증하는 방법
앱에서 Apple 로그인을 완료하고나서 얻은 여러 값 중에 로그인한 사용자가 신뢰할 수 있는 Apple 유저인지 확인하는 절차를 진행하기 위한 토큰(Token)인 Authorization_code(혹은 refresh_token)을 얻을 수 있다. 이 토큰을 Apple REST API 서버에 전송하면 승인된 Apple 사용자인지 확인할 수 있다. 이러한 과정을 거치면 부정한 Apple 유저인지 확인하여 비정상적인 서비스 접근을 예방할 수 있다.
Apple의 토큰 검증 REST API
Apple은 토큰 검증 REST API를 통해 인증된 Apple 유저인지 확인할 수 있는 서비스를 제공한다.
Token 검증을 위한 URL 주소와 필드는 위와 같이 구성하여 요청하면 된다. Token 검증 요청에 필요한 필드에 대해서 설명하면 아래와 같다.
1. client_id
Apple Developer의 'Certificates, Identifiers & Profiles'에서 등록한 App ID 혹은 Service ID를 말한다. 대표적으로 App ID는 iOS 같은 Apple 기기에서 Apple 로그인을 제공하기 위해 필요한 식별값이고, Service ID는 Apple 기기가 아닌 디바이스에서 웹 로그인 방식을 통한 Apple 로그인을 제공하기 위해 필요한 식별값이다. Apple 로그인 구현을 위해서는 반드시 App ID 또는 Service ID 생성이 필요하기에 이를 Apple Token 검증에도 사용되고 있다.
2. client_secret
Apple Developers에 등록한 private key를 바탕으로 생성한 JWT 토큰을 말한다. 'Certificates, Identifiers & Profiles'에서 'Keys' 메뉴에서 'Sign in with Apple' 옵션이 활성화된 private key 등록 및 파일을 다운받을 수 있다. 다운받은 private key를 활용하여 Apple Token 검증에 필요한 JWT 토큰을 만들어야 하는데, JWT 토큰에 포함되어야 할 값은 아래와 같다.
- header
(1) alg : JWT Token의 암호화 알고리즘 형식을 의미하는 값이다. "ES256"을 입력하면 된다.
(2) kid : Apple Developers에 등록한 private key의 ID 값이다. private key 발급 후에 확인할 수 있다.
- payload
(1) iss : Apple Developer 계정의 Team ID
(2) iat : client_secret을 등록한 시간
(3) exp : JWT Token 만료 시간
(4) aud : "https://appleid.apple.com" 입력
(5) sub : client_id 생성 간에 등록한 프로젝트의 Bundle ID
3. code
Apple 로그인 후 발급된 Authorization Code 값이다. 'grant_type'이 'authorization_code'일 때 입력하는 parameter이며, Authorization Code 방식의 Apple Token을 검증하기 위해 입력해야 하는 필드이다.
4. grant_type
Apple Token 유형을 의미한다. Apple Token 검증에 필요한 종류는 크게 두 가지로 나뉘는 데, Authorization Code와 Refresh Token으로 나뉜다. Authorization Code 방식의 Apple Token 검증을 원하면 'authorization_code'를 입력하면 되고, Refresh Token 방식 검증을 원하면 'refresh_token'을 입력하면 된다.
5. refresh_token
Apple Token 검증 요청 후 받은 Response 값 중 'refresh_token'을 입력하는 필드이다. 제일 처음에 Authorization Code 방식의 Apple Token 검증 요청한 이후 얻은 refresh_token을 바탕으로 Apple Token 요청이 가능하다. Authorization Code의 노출을 최소화하기 위한 방법으로 Apple Token 검증을 통해 발급되는 refresh_token을 통해 지속적인 Apple Token 검증이 가능하다.
- 참고
https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
Apple Developer Documentation
developer.apple.com
Apple 토큰 검증을 위한 Private Key 발급
1. Apple Developer의 'Certificates, Identifiers & Profiles'에서 좌측 'Keys' 메뉴 클릭 후 생성 버튼 클릭
2. Key name 입력 및 'Sign in with Apple' 체크 후 'Configure' 버튼 클릭
3. 대상 App ID 선택 후 'Save' 버튼 클릭
4. 'Continue' 클릭
5. 'Register' 버튼 클릭
6. 'Download' 클릭해서 private key 파일 다운
- 참고
https://developer.apple.com/help/account/manage-keys/create-a-private-key
Create a private key to access a service - Manage keys - Account - Help - Apple Developer
Feedback Assistant Submit feedback, report bugs, and request enhancements to APIs and developer tools. Send us feedback
developer.apple.com
Apple 토큰 검증하는 방법 (Node.js 기준)
* 필요 라이브러리 *
- axios
- Apple 서버로 Apple Token 검증 요청을 위해 필요
- https://www.npmjs.com/package/axios
axios
Promise based HTTP client for the browser and node.js. Latest version: 1.2.0, last published: 5 days ago. Start using axios in your project by running `npm i axios`. There are 90003 other projects in the npm registry using axios.
www.npmjs.com
- jwt
- Apple Token 검증에 필요한 client_secret을 만드는 데 필요
- https://www.npmjs.com/package/jsonwebtoken
jsonwebtoken
JSON Web Token implementation (symmetric and asymmetric). Latest version: 8.5.1, last published: 4 years ago. Start using jsonwebtoken in your project by running `npm i jsonwebtoken`. There are 21581 other projects in the npm registry using jsonwebtoken.
www.npmjs.com
- verifyAppleToken
- Apple 서버로부터 응답받은 Apple Token 검증 결과 중 id_token을 Decoding하기 위해 필요
- https://www.npmjs.com/package/verify-apple-token
verify-apple-token
Verify the Apple id token on the server side.. Latest version: 2.1.0, last published: 2 years ago. Start using verify-apple-token in your project by running `npm i verify-apple-token`. There is 1 other project in the npm registry using verify-apple-token.
www.npmjs.com
* Apple Token 검증을 위한 샘플 코드(Node.js) *
// Apple Token 검증 함수
appleTokenFunc = async (token, isRefreshToken = false) => {
var response = await axios({
method: 'post',
url: 'https://appleid.apple.com/auth/token',
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
data: qs.stringify({
client_id: 'com.xxxxxxx.xxxx',
client_secret: getNewAppleClientSecret(), // apple token 검증을 위한 jwt 생성
grant_type: isRefreshToken ? 'refresh_token' : 'authorization_code', // Refresh Token 인증이면 'refresh_token' 입력, 아니면 'authorization_code' 입력
code: isRefreshToken ? null : token, // grant_type == 'authorization_code' 일 때, authorization_code 입력
refresh_token: isRefreshToken ? token : null, // grant_type == 'refresh_token' 일 때, refresh_token 입력
}),
});
es.log('@@@ apple response.data =', response.data);
var resultMap = await verifyAppleTokenFunc(response.data?.id_token); // Apple Server로부터 받은 jwt인 id_token을 decode
if (resultMap.get('result')) {
return { id: resultMap.get('id'), refreshToken: response.data.refresh_token };
} else {
return { id: null, error: resultMap.get('errorMessage') };
}
};
// jwt 기반 client_secret을 생성하는 함수
getNewAppleClientSecret = () => {
const applePrivateKey = fs.readFileSync('src/config/AuthKey_XXXXXXXX.p8', 'utf8'); // Apple Developers에 등록된 Apple 간편로그인을 위한 private key
// jwt 라이브러리 사용
return jwt.sign(
{
iss: "XXXXXXXX", // Apple Developer Team ID
iat: Math.floor(Date.now() / 1000), // Token 인증 시작 시간
exp: Math.floor(Date.now() / 1000 + 300), // Token 만료 시간
aud: "https://appleid.apple.com",
sub: "com.xxxxxxx.xxxx", // Bundle ID
},
applePrivateKey,
{
header : {
alg: "ES256",
kid: "XXXXXXXX" // Apple Private Key ID 입력
}
});
};
// Apple Server가 반환한 결과 중 jwt 형식인 id_token을 decode하는 함수
verifyAppleTokenFunc = async (idToken) => {
var resultMap = new Map();
es.log('idToken : ', idToken);
try {
const jwtClaims = await verifyAppleToken({ // verifyAppleToken 라이브러리 사용
idToken: idToken,
clientId: "com.xxxxxxx.xxxx"
}); // id_token verify 진행
es.log("jwtClaims for apple : ", jwtClaims);
// Apple Server가 반환한 id_token의 유효기간이 만료되면 error message를 반환
if (jwtClaims.exp < Math.floor(Date.now() / 1000)) {
resultMap.set('result', false);
resultMap.set('errorMessage', "expired token");
} else {
resultMap.set('result', true);
resultMap.set('id', jwtClaims.sub); // 로그인하는 User의 고유한 Apple 계정 식별 ID를 저장한다.
}
return resultMap;
} catch (error) {
es.log('apple login token error:', error);
resultMap.set('result', false);
resultMap.set('errorMessage', error);
return resultMap;
}
};