Apple with Expo를 사용한 로그인

This post originally appeared on Forward Digital's blog


전체 인증 과정이 가능한 한 빈틈이 없도록 하는 것은 모든 잠재적 사용자가 지루한 계정 만들기 폼을 작성하는 데 불쾌감을 느끼지 않도록 하는 데 매우 중요하다.
애플은 이미 2019년 WWDC에서 2FA가 자신의 애플 Id를 강제로 사용해 인증할 수 있도록 하는 로그인 애플을 도입했다.사용자가 응용 프로그램에 들어갈 수 있도록 하는 틈새 없는 해결 방안
오늘 저는 Nest를 사용하여 당신이 관리하는 엑스포 프로젝트에 통합할 것입니다.js 백엔드에서 이 사용자를 검증하고 사용자 계정을 처리합니다.
시작하기 전에 나는 독자가 익숙하다고 가정할 것이다.
  • 엑스포
  • 새둥지.js
  • JavaScript
  • 타자 원고
  • 백엔드 코드는 Nest를 사용하지만, 많은 코드는 사용할 수 있는 다른 노드의 백엔드 프레임워크로 전송됩니다.

    우리 뭐가 필요해?


    박람회


    저희가 관리하는 엑스포 프로젝트에 다음과 같은 추가가 필요합니다.
  • "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 )
  • 우리는 그것으로 JWT 영패가 애플의 공개 키
  • 에 서명되었음을 검증했다

  • https://github.com/auth0/node-jwks-rsa ( npm install jwks-rsa )
  • Apple의 키스 API 포트에서 서명 키를 검색하고 Apple의 공개 키를 검색합니다

  • https://github.com/auth0/jwt-decode ( npm install jwt-decode )
  • Apple 버튼
  • 로그인을 통해 생성된 JWT를 디코딩하기 위해 사용합니다.

    auth 흐름의 간략한 개술

  • 사용자가 Apple 로그인 버튼을 누르고 네이티브 Apple 모드
  • 로 로그인
  • 디바이스의 2FA 승인을 통과하면 빈 JWT 토큰을 반환합니다.
  • 사용자가 로그인하거나 계정을 만들려고 하는지 확인
  • 사용자 이름이 있는 JWT 토큰을 백엔드에 보냅니다(계정을 만들고 있는 경우)
  • 그리고 우리는 JWT 영패를 디코딩하여 애플의 공개 키를 얻고 JWT 영패가 애플이 서명했는지 검증한다
  • Dell 번들 id
  • 에 따른 인증 토큰
  • 이 점에서 우리는 그들이 애플의 신분 검증을 통과했다고 안전하게 가정할 수 있다. 우리는 우리의 백엔드 신분 검증을 통해 그들을 로그인하거나 우리가 두 번째 단계에서 얻은 값을 사용하여 우리의 데이터베이스에 계정을 만들 수 있다.
  • 엑스포 패키지 개술


    Expo'sexpo-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_WIDTHBUTTON_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 토큰입니다.
  • 액세스 이름 및 전자 메일


    조심하다emailfullName는 한 번만 채워집니다.처음 버튼을 눌렀을 때, 장치를 바꾸거나 프로그램을 업데이트했더라도 적용됩니다.사용자의 전자 우편은 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);
            }
        }
    }}
    
    위의 코드 세그먼트에서 확인할 수 있는 내용은 다음과 같습니다.
  • 사용자의 이메일 및 이름 요청
  • 캐시에 저장된 이름 값 검색
  • 캐시 및 자격 증명 값이 정의되지 않은 경우 버튼을 사용하여 계정을 성공적으로 생성한 것으로 가정하고 로그인 흐름을 시작합니다
  • .
  • 캐시 이름이 명시되었지만 명시된 자격 증명이 없는 경우 Apple 요청 값과 서버에서 계정을 만드는 사이에 문제가 발생했다고 가정하여 캐시 이름으로 계정을 만듭니다
  • .
  • 그렇지 않으면 채워진 요청 값으로 계정을 만듭니다.
  • 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.
    읽어주셔서 감사합니다!

    좋은 웹페이지 즐겨찾기