2022-03월에 Android 앱을 1차 출시하고,
3월~6월 한 학기 동안 교내에 홍보를 하며 테스트를 해 보았다.
사실 20대가 대부분인 대학교 특성 상 아이폰 유저가 대부분일것이다..
iOS에 대한 요청이 꽤 있었고
감사하게도 2022-07월부터 앱센터 iOS 두 분이 함께해주시기로 했다!!
기존에 개발해 놓았던 서버에 크게 변동되는 것은 없었지만
한 2달동안 내 발목을 붙잡았던 애.플.로.그.인
ㅋㅋㅋ 아우
iOS님들 죄송합니다...ㅎ 제가 미루고 부족한 탓에 출시가 늦어졌습니다ㅠㅠ
공식문서 : https://developer.apple.com/documentation/sign_in_with_apple
어떤 로그인 방식을 사용할까에 대한 논의 후, 안드로이드 유저에게는 아주 편한 구글로그인으로 선택했다.
그치만 앱스토어에서는 적어도 하나의 소셜 로그인을 사용한다면,
반드시 Apple 로그인은 필수로 넣어야 한다는 것이다.
에라이... 애플은 사용자에겐 편한데 개발자에겐 힘든것같움 ㅠ
구글에서는 엑세스 토큰을 발급해주는 곳이 있어서 클라이언트 기기 없이도 서버측 테스트를 편하게 할 수 있었음.
근데 이 애플 자슥은 그런 웹 페이지같은 것도 없고...
실제 우리 App 단에서 요청을 보내야 한다고 들었고..
Client 단이 뒷받침 되어야 서버 테스트가 가능하고,
발급된 authorization_code가 유효기간이 10분인가 이기 때문에 실시간으로 소통이 필요하다.
Apple Development Account에서 애플로그인을 위해 정보들을 체크하고 key 파일을 받아와야 한다.
테스트 용으로 내가 해보려고 Apple 기기 하나 없지만 Apple 계정을 만들었것만
참내 macOS에서만 발급이 가능한가보네...
아무튼 이 과정에서
BUNDLE_ID
, SERVICE_ID
, TEAM_ID
, KEY_IDENTIFIER
의 정보가 생성되는 것 같고 얘가 나중에 우리서버에서 애플서버로 인증을 요청할 때 쓰인다.
여기서 생성한 key파일 (우리는 p8파일)
도 서버에서 PRIVATE_KEY
로 쓰인다.
- 얘네 값은 환경변수에 담아둔다.
우리 앱에서 로그인을 요청하고,
애플 서버에서 그 사용자에 대한 사용자 정보(id token)
와
이 사용자가 맞는지 검증할 토큰(authorization_code)
를 내려준다.
id token은 기기와 사용자 정보 고유값(email, sub,..) 인 것 같고,
authorization_code 얘는 유효시간 10분인듯
그리고 여기서 authorization_code
를 아래만들 POST 요청에 body에 담아서 보내준다.
src/server/routes/login/appleOauthLogin.ts
export default defineRoute('post', '/login/oauth/apple', schema, async (req, res) => {
const {accessToken} = req.body;
const {user, jwt, rememberMeToken} = await LoginService.appleOAuthLogin(accessToken);
return res
.header('token', jwt)
.cookie(config.server.jwt.cookieName, jwt, config.server.jwt.cookieOptions)
.json({
jwt,
userId: user.id,
rememberMeToken,
});
});
body에 담겨온 authorization_code
를 편의상 코드에서는 accessToken
이라 칭했다.
LoginService.appleOAuthLogin
에 이 accessToken
를 넘겨주고
로그인이 성공되어 돌아온 user 정보를 받아온다.
아래에서 다시 설명하겠지만, rememberMeToken
은 자동로그인을 하기 위함이다.
/** 엑세스 토큰으로 로그인 (사실 authorization_code)
* @param accessToken
*/
async appleOAuthLogin(accessToken: string): Promise<LoginResult> {
const {email, oauthId} = await this.resolveUserInfoFromApple(accessToken);
const user = await this.getOrCreateUser(email, 'apple', oauthId);
return this.onSuccess(user);
}
private async resolveUserInfoFromApple(accessToken: string) {
try {
return await getAppleOAuthInfo(accessToken); //token -> info
} catch (e: any) {
printError(e);
throw WrongAuth();
}
}
private async getOrCreateUser(email: string, oauthProvider: string, oauthId: string): Promise<User> {
const found = await User.findOne({where: {oauthProvider, oauthId}});
if (found != null) {
return found;
}
return await User.create({
email: email,
nickname: `uni-${new Date().getTime()}`,
oauthProvider: oauthProvider,
oauthId: oauthId,
rememberMeToken: generateUUID(),
}).save();
}
accessToken
를 보내 인증을 요청하는 함수 getAppleOAuthInfo
를 호출하게 된다.여기서는 로그인 성공 후 받아온 정보로 데이터베이스의 user 테이블에 생성하는 함수 getOrCreateUser
를 호출하게 된다.
rememberMeToken
에는 임의로 uuid를 생성하는 함수 generateUUID()를 호출한다./**
* 애플 로그인 (Token -> User Info)
*/
export async function getAppleOAuthInfo(accessToken: string): Promise<OAuthInfo> {
const clientID = config.external.appleSignIn.bundleID; /*일단 지금은 Apple iOS 기기용으로 기대중*/
const info = await appleSignin.getAuthorizationToken(accessToken, {
clientID,
clientSecret: appleSignin.getClientSecret({
clientID,
...config.external.appleSignIn
}),
...config.external.appleSignIn
});
log(info)
const {id_token, refresh_token} = info; /* info에서 refresh token은 버리나봄...*/
const idTokenDecoded = jwt.decode(id_token) as { email: string, sub: string };
const {email, sub} = idTokenDecoded;
if (email == null) {
throw NoEmail();
}
if (sub == null) {
throw NoSubject();
}
return {
email: email,
oauthId: sub,
};
}
환경변수에 담아두었던 key 값들 (bundleID
, serviceID
, teamID
, keyIdentifier
, privateKey
,redirectUri
) 이 여기서
config.external.appleSignIn
에 담겨있다고 보면 된다.
accessToken
과 key값 정보를 애플 서버로 넘겨주고,
그 리턴 타입은 아래와 같다. (이 코드에서 info
에 담겨짐)
(appleSignin 정보)
export interface AppleAuthorizationTokenResponseType {
/** A token used to access allowed data. */
access_token: string;
/** It will always be Bearer. */
token_type: 'Bearer';
/** The amount of time, in seconds, before the access token expires. */
expires_in: number;
/** used to regenerate (new) access tokens. */
refresh_token: string;
/** A JSON Web Token that contains the user’s identity information. */
id_token: string;
}
사용자의 id_token을 decode 하여 email과 sub(=oauth id) 를 받아오고
두 값을 리턴한다.
코드 시나리오.
모바일(App-client)로부터 로그인 요청을 받고,
애플 서버가 auth code를 발급해 준다.
그 code를 우리 서버(App-server)에 제시하고
이 토큰을 들고 애플서버로 가서 사용자가 진쨔 맞는지 검증
맞으면 사용자에 대한 정보 id_token을 내려줌
얘는 jwt 형태이기 때문에 decode해서 email, subject,.. 정보 알아냄
그 사용자가 신규 회원이면 DB에 저장.
이제 유저 정보(userId)와 자동로그인을 위한 rememberMeToken를 응답 내려줌.
요청
응답
애플 로그인 해 보려 검색해 보는데 알고 있는 것과 달라서 뭐가 맞는지 확인차 여쭤봅니다.
앱에서 로그인하면 id_token과 authorization code를 주는데
id_token은 사용자 정보가 들어 있고 (이메일, 이름 등)
authorization code는 만료 기간 갱신을 위한 토큰이 아닌가요?
굳이 다른 소셜 로그인과 비교하자면 id_token은 access_token, authorization code는 refresh token으로 이해했는데 본문글을 읽고 좀 헷갈려서요.
로그인할 때 이미 사용자 정보가 담긴 id_token을 주지만 이걸 열어보기 위해, 검증을 위해
애플 서버의 퍼블릭키를 발급받아 같은 알고리즘 타입의 키를 이용해 id_token을 복호화해
사용자 정보를 이용하는 것이 아닌지 궁금합니다.