Apple with Expo를 사용한 로그인
전체 인증 과정이 가능한 한 빈틈이 없도록 하는 것은 모든 잠재적 사용자가 지루한 계정 만들기 폼을 작성하는 데 불쾌감을 느끼지 않도록 하는 데 매우 중요하다.
애플은 이미 2019년 WWDC에서 2FA가 자신의 애플 Id를 강제로 사용해 인증할 수 있도록 하는 로그인 애플을 도입했다.사용자가 응용 프로그램에 들어갈 수 있도록 하는 틈새 없는 해결 방안
오늘 저는 Nest를 사용하여 당신이 관리하는 엑스포 프로젝트에 통합할 것입니다.js 백엔드에서 이 사용자를 검증하고 사용자 계정을 처리합니다.
시작하기 전에 나는 독자가 익숙하다고 가정할 것이다.
우리 뭐가 필요해?
박람회
저희가 관리하는 엑스포 프로젝트에 다음과 같은 추가가 필요합니다.
"expo-apple-authentication"
실행: expo install expo-apple-authentication
SDK 버전과 호환되는 버전이 설치됩니다.이 자습서는 SDK41에 대해 작성되었습니다.There are extra configuration steps you need to follow that can be found here: https://docs.expo.io/versions/latest/sdk/apple-authentication/#configuration
백엔드
우리 둥지를 위해서.추가해야 할 사항은 다음과 같습니다.
https://github.com/auth0/node-jsonwebtoken (
npm install jsonwebtoken
)https://github.com/auth0/node-jwks-rsa (
npm install jwks-rsa
)https://github.com/auth0/jwt-decode (
npm install jwt-decode
)auth 흐름의 간략한 개술
엑스포 패키지 개술
Expo's
expo-apple-authenticator
는 사실상 원스톱 상점으로 응용에 필요한 모든 것을 제공한다.The Sign-in with Apple button will only appear on iOS devices, so make sure to consider this when building the UI.
이 패키지는 디스플레이 모드에 사용되는 기능을 포함하고 있으며, 단추 자체와 묶여 있습니다. 그러나 사용자 정의 단추를 사용하려면 필요하지 않습니다.
<AppleAuthentication.AppleAuthenticationButton
buttonType={AppleAuthentication.AppleAuthenticationButtonType.CONTINUE}
buttonStyle={AppleAuthentication.AppleAuthenticationButtonStyle.BLACK}
cornerRadius={BUTTON_BORDER_RADIUS}
style={{
width: BUTTON_WIDTH,
height: BUTTON_HEIGHT,
}}
onPress={async () => {
//
}}
/>
스타일 옵션도 많기 때문에 이 단추는 프로그램의 다른 단추와 일치합니다.BUTTON_BORDER_RADIUS
, BUTTON_WIDTH
및 BUTTON_HEIGHT
는 응용 프로그램의 모든 단추에 대한 사용자 정의 값입니다.사용자가 Apple 로그인 사용 버튼을 누르면 요청한 범위에 따라 애플리케이션 상단에 있는 네이티브 모드가 표시됩니다.
두 역할 도메인에 액세스할 수 있습니다.
const credential = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
AppleAuthentication.AppleAuthenticationScope.EMAIL,
],
});
사용자가 모드를 취소하지 않으면 다음과 같은 응답이 나타납니다.
export type AppleAuthenticationCredential = {
user: string;
state: string | null;
fullName: AppleAuthenticationFullName | null;
email: string | null;
realUserStatus: AppleAuthenticationUserDetectionStatus;
identityToken: string | null;
authorizationCode: string | null;
};
우리는 되돌아오는 네 개의 필드만 관심 있습니다.user
- 애플리케이션 버전/디바이스 변경 사항이 지속적으로 유지되는 고유한 식별자입니다.fullName
- 사용자 이름 대상, 성, 이름, 닉네임 등 포함email
- 백엔드에서 사용자 계정을 생성하는 사용자의 이메일identityToken
- 시청자, 발행인, 사용자에 대한 정보를 저장하는 JWT 토큰입니다.액세스 이름 및 전자 메일
조심하다
email
와 fullName
는 한 번만 채워집니다.처음 버튼을 눌렀을 때, 장치를 바꾸거나 프로그램을 업데이트했더라도 적용됩니다.사용자의 전자 우편은 JWT 영패에서 여전히 사용할 수 있지만, fullName
처음 떼어내면 더 이상 요청할 수 없습니다. 다음 요청은null로 되돌아옵니다.사용자가 계정을 만들거나 로그인하고 있습니까?
이 단추는 계정 생성 단추와 로그인 단추를 동시에 충당할 것입니다. 사용자가 실행하고 있는 동작을 어떻게 확인합니까?
위에서 말한 바와 같이, 우리는 그들이 처음으로 단추를 눌렀을 때, 전자메일과 이름 값이 채워질 것이라는 것을 안다.이름과 전자 우편 값이 정의되지 않으면 사용자가 프로그램을 처음 사용하고 계정을 만들려고 시도하는 것으로 공평하게 가정할 수 있습니다.
What is the problem with this? If the user signs in through the button, pulls down the name and email values but, say, the device crashes/the subsequent API call fails or they kill the app; they can't create an account through the Sign up with Apple button.
완화 방법은 휴대전화가 채워지면 명칭값을 휴대전화 메모리에 캐시하는 것이다. 따라서 어떤 변두리 상황이 발생해 장치의 이름을 잃어버리면 캐시로 돌아갈 수 있다.
키 값
credential.user
을 사용하여name 값을 캐시에 저장합니다. 이것은 안정적인 유일한 식별자임을 알고 있기 때문입니다.// Expo Apple button
onPress={async () => {
try {
const credential: AppleAuthenticationCredential = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
AppleAuthentication.AppleAuthenticationScope.EMAIL,
],
});
const cachedName: string = await Cache.getAppleLoginName(credential.user);
const detailsArePopulated: boolean = (!!credential.fullName.givenName && !!credential.email);
if (!detailsArePopulated && !cachedName) {
await login(credential.identityToken);
} else if (!detailsArePopulated && cachedName) {
await createAccount(cachedName, credential.user, credential.identityToken);
} else {
await createAccount(
credential.fullName.givenName, credential.user, credential.identityToken,
);
}
} catch (error) {
if (error.code === 'ERR_CANCELED') {
onError('Continue was cancelled.');
} else {
onError(error.message);
}
}
}}
위의 코드 세그먼트에서 확인할 수 있는 내용은 다음과 같습니다.This is not bulletproof! If the user requests the details but the cache gets cleared/they change devices before creating an account, they will not be able to create an account using this flow. We hedged a bet that this was edge case enough to warrant the risk.
JWT 토큰 디코딩 및 검증
사용자가 계정을 만들거나 로그인하는 경우, Nest에 보내는 JWT 영패를 디코딩하고 검증해야 합니다.js API.
Nest를 사용합니다.js 계정 생성과 로그인을 처리하기 위해 두 개의 새로운 노드를 만들어야 합니다.나는 이 두 단점에 각각 보호기를 추가할 것이다.바로 이 방어에서 우리는 애플 auth 정책 코드라고 부른다.
경험에 의하면, 나는 기존의 로그인과 계정 종결점을 사용할 수 없다. 왜냐하면, 이것은 우리가 장치에서 요청을 한 것이 아니라, 영패를 디코딩하고 검증한 후에 받은 정보를 필요로 하기 때문이다.
로그인 보호:
public async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
const token: string = <string>request.body.identityToken;
const jwt: JwtTokenSchema = await this.apple.ValidateTokenAndDecode(token);
try {
const [sessionId, user]: [string, IUser] = await this.login.AttemptLogin(jwt.email);
if (sessionId && user) {
request.user = user;
response.set('Auth-Token', sessionId);
response.set('Access-Control-Expose-Headers', 'Auth-Token');
return true;
}
} catch (error) {
throw new HttpException(`Validation failed for login. ${error.message ?? ''}`, HttpStatus.UNAUTHORIZED);
}
return false;
}
가장 중요한 것은 다음과 같습니다.const jwt: JwtTokenSchema = await this.apple.ValidateTokenAndDecode(token);
우리가 말한 바와 같이 애플의 전략은 가장 논리에 부합된다.논리는 다음과 같다.애플의 공개 키 가져오기
JWT 영패를 사용하기 전에 애플의 개인 키에 서명되었는지 확인해야 합니다.이를 위해, 우리는 애플의 공개 키로 서명을 검증해야 한다.
우선 클라이언트가 보낸 JWT 토큰을 디코딩하고 토큰 헤더의
kid
값을 추출해야 합니다.import jwtDecode, { JwtHeader } from 'jwt-decode';
// rest of file
const tokenDecodedHeader: JwtHeader & { kid: string } = jwtDecode<JwtHeader & { kid: string }>(token, {
header: true,
});
The "kid" (key ID) Header Parameter is a hint indicating which key was used to secure the JWS. This parameter allows originators to explicitly signal a change of key to recipients. The structure of the "kid" value is unspecified. Its value MUST be a case-sensitive string. Use of this Header Parameter is OPTIONAL.
When used with a JWK, the "kid" value is used to match a JWK "kid" parameter value.
그런 다음 키 배열이 포함된 객체를 반환하기 위해 Apple
auth/keys
의 공통 엔드포인트에 HTTP 요청을 실행해야 합니다.자세한 내용here.const applePublicKey: { keys: Array<{ [key: string]: string }> } = await this.api.Get(
'https://appleid.apple.com/auth/keys',
);
this.api.Get
Nest를 위한 HTTP 클라이언트입니다. 자세한 내용을 보십시오.)이 끝점은 다음과 같이 JSON 웹 키 세트를 반환합니다.
{
"keys": [
{
"kty": "RSA",
"kid": "AIDOPK1",
"use": "sig",
"alg": "RS256",
"n": "lxrwmuYSAsTfn....",
"e": "AQAB"
},
{
//
}
]
}
키스 그룹은 보통 여러 개의 요소가 있기 때문에, 우리는 keys
그룹을 JWT 카드 디코딩 헤더에 일치하는 kid
값을 가진 그룹으로 필터해야 한다.const kid: string = tokenDecodedHeader.kid;
const sharedKid: string = applePublicKey.keys.filter(x => x['kid'] === kid)[0]?.['kid'];
현재 우리는 kid
값을 사용하여 애플의 공개 키를 가져와야 한다.이를 위해 위 패키지를 사용합니다: jwks-rsa
.const client: jwksClient.JwksClient = jwksClient({
jwksUri: 'https://appleid.apple.com/auth/keys',
});
const key: jwksClient.CertSigningKey | jwksClient.RsaSigningKey = await client.getSigningKey(sharedKid);
const signingKey: string = key.getPublicKey();
만일 모든 것이 순조롭다면, 우리는 지금 애플의 공개 키가 있어야 한다. 우리는 그것으로 우리의 JWT 영패가 같은 키로 서명되었음을 검증할 수 있다.감사jsonwebtoken
:try {
const res: JwtTokenSchema = <JwtTokenSchema>jwt.verify(token, signingKey);
} catch (error) {
// token is invalid
}
나는 유형 안전을 위해 내 자신을 썼다JwtTokenSchema
:export type JwtTokenSchema = {
iss: string;
aud: string;
exp: number;
iat: number;
sub: string;
nonce: string;
c_hash: string;
email: string;
email_verified: string;
is_private_email: string;
auth_time: number;
};
이제 디코딩된 JWT 영패가 생겼습니다. 나머지 신분 검증 과정을 계속하기 전에 기본적인 검증을 실행하기만 하면 됩니다.private ValidateToken(token: JwtTokenSchema): void {
if (token.iss !== 'https://appleid.apple.com') {
throw { message: 'Issuers do not match!' };
}
if (token.aud !== this.audience) {
throw { message: 'Audiences do not match!' };
}
}
this.audience
는 당신의 앱의 묶음 ID입니다. 이것은 당신이 엑스포에서인지 제작 중인지에 따라 달라집니다.나는 클래스의 구조 함수에서 삼원 설정을 한다.this.audience = this.isInProd ? 'co.repetitio.repetitio' : 'host.exp.Exponent' // this will be your bundle id, found in app.json
현재, 우리는 디코딩된 영패가 있어야 한다. 우리는 애플의 공개 키에 따라 그것을 검증했고, 사용자가 그들이 말한 사용자라고 안전하게 가정할 수 있으며, 정확한 프로그램에 로그인하려고 시도하고 있다.전체 클래스는 밑에 링크됩니다.여기에서 당신은 자유롭게 로그인 과정을 계속하거나 계정을 만들 수 있습니다.
I have uploaded the code to a public repo so you can take a deeper look here.
읽어주셔서 감사합니다!
Reference
이 문제에 관하여(Apple with Expo를 사용한 로그인), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/bendix/implementing-sign-in-with-apple-with-a-managed-expo-workflow-and-a-nest-js-backend-8ja텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)