sourcecode

Apple Java 사용자 확인으로 로그인

codebag 2023. 7. 22. 10:00
반응형

Apple Java 사용자 확인으로 로그인

새로운 애플 기능인 "Apple로 로그인"의 앱 측면을 구현했지만 백엔드에서 authorizationCode로 인증할 수 없습니다.백엔드가 Java로 작성되어 JWT를 생성하고 Apple 서버와 통신할 수 없습니다.

먼저 developer.apple.com -> 인증서, 식별자 및 프로필 -> 키로 이동합니다.Apple 로그인 키를 생성하고 이 키를 다운로드합니다.이 키는 다시 다운로드할 수 없으므로 안전한 장소에 보관하고 다른 사람과 공유하지 마십시오.또한 여기에 표시된 키 ID는 나중에 필요하다는 것을 참조하십시오.팀 ID도 필요합니다.모르시면 페이지 오른쪽 상단에 'YOUR NAME - XX0XX00XXXXX'라고 적혀있습니다.

기본적으로 다음 단계를 수행합니다.

1.키에서 JWT 생성

2. 토큰과 함께 인증 코드 보내기

3. 디코딩 응답

웹과 모바일을 모두 사용하기 위한 업데이트

웹에서 애플 로그인을 사용하려면 몇 가지 단계를 더 수행해야 합니다.

웹에 대한 새 식별자 추가

developer.apple.com -> 인증서, 식별자 및 프로필 -> 식별자로 이동합니다.더하기 버튼을 클릭하여 새 식별자를 등록합니다.서비스 ID를 선택하고 계속합니다.설명 및 식별자를 제공합니다.식별자는 고유해야 하며 번들 ID와 달라야 합니다(예: com.your.bundle.id .web을 사용할 수 있습니다).계속을 클릭하고 등록을 클릭합니다.그런 다음 이 서비스 ID를 구성해야 합니다.아래 나열된 새로 생성한 서비스 ID를 클릭하고 Apple 로그인 확인란을 활성화합니다.그런 다음 도메인을 구성해야 합니다.도메인을 제공하고 URL을 반환합니다.

웹에 대한 일부 중요한 사항은 유효redirect_url을 전달하는 것을 잊거나 동일한 authorization_code를 두 번 이상 사용하려고 하면 invalid_grant 오류가 발생할 수 있습니다.


public class AppleLoginUtil {
    private static String APPLE_AUTH_URL = "https://appleid.apple.com/auth/token";

    private static String KEY_ID = "**********";
    private static String TEAM_ID = "**********";
    private static String CLIENT_ID = "com.your.bundle.id";
    private static String WEB_CLIENT_ID = "com.your.bundle.id.web";
    private static String WEB_REDIRECT_URL = "https://bundle.your.com/";

    private static PrivateKey pKey;

    private static PrivateKey getPrivateKey() throws Exception {
    //read your key
        String path = new ClassPathResource("apple/AuthKey.p8").getFile().getAbsolutePath();

        final PEMParser pemParser = new PEMParser(new FileReader(path));
        final JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
        final PrivateKeyInfo object = (PrivateKeyInfo) pemParser.readObject();
        final PrivateKey pKey = converter.getPrivateKey(object);

        return pKey;
    }

    private static String generateJWT() throws Exception {
        if (pKey == null) {
            pKey = getPrivateKey();
        }

        String token = Jwts.builder()
                .setHeaderParam(JwsHeader.KEY_ID, KEY_ID)
                .setIssuer(TEAM_ID)
                .setAudience("https://appleid.apple.com")
                .setSubject(CLIENT_ID)
                .setExpiration(new Date(System.currentTimeMillis() + (1000 * 60 * 5)))
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .signWith(pKey, SignatureAlgorithm.ES256)
                .compact();

        return token;
    }

    private static String generateWebJWT() throws Exception {
        String token = Jwts.builder()
                .setHeaderParam(JwsHeader.KEY_ID, KEY_ID)
                .setIssuer(TEAM_ID)
                .setAudience("https://appleid.apple.com")
                .setSubject(WEB_CLIENT_ID)
                .setExpiration(new Date(System.currentTimeMillis() + (1000 * 60 * 5)))
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .signWith(getPrivateKey(), SignatureAlgorithm.ES256)
                .compact();

        return token;
    }


    /*
    * Returns unique user id from apple
    * */
    public static String appleAuth(String authorizationCode, boolean forWeb) throws Exception {
        HttpResponse<String> response = Unirest.post(APPLE_AUTH_URL)
                .header("Content-Type", "application/x-www-form-urlencoded")
                .field("client_id", forWeb ? WEB_CLIENT_ID : CLIENT_ID)
                .field("client_secret", forWeb ? generateWebJWT() : generateJWT())
                .field("grant_type", "authorization_code")
                .field("code", authorizationCode)
                .field("redirect_uri", forWeb ? WEB_REDIRECT_URL : null)
                .asString();


        TokenResponse tokenResponse=new Gson().fromJson(response.getBody(),TokenResponse.class);
        String idToken = tokenResponse.getId_token();
        String payload = idToken.split("\\.")[1];//0 is header we ignore it for now
        String decoded = new String(Decoders.BASE64.decode(payload));

        IdTokenPayload idTokenPayload = new Gson().fromJson(decoded,IdTokenPayload.class);

       return idTokenPayload.getSub();
    }

}

나는 토큰을 생성하기 위해 Bouncy Castle jwt를 사용했습니다.그리고 휴식을 위한 언레스트와 gson.

 <!-- https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk15on -->
    <dependency>
        <groupId>org.bouncycastle</groupId>
        <artifactId>bcpkix-jdk15on</artifactId>
        <version>1.63</version>
    </dependency>

<!--JJWT-->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.10.7</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.10.7</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.10.7</version>
        <scope>runtime</scope>
    </dependency>

<!--UNIREST-->
    <dependency>
        <groupId>com.mashape.unirest</groupId>
        <artifactId>unirest-java</artifactId>
        <version>1.4.9</version>
    </dependency>
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
        <version>4.3.6</version>
    </dependency>
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpasyncclient</artifactId>
        <version>4.0.2</version>
    </dependency>
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpmime</artifactId>
        <version>4.3.6</version>
    </dependency>
    <dependency>
        <groupId>org.json</groupId>
        <artifactId>json</artifactId>
        <version>20140107</version>
    </dependency>

당신이 알고 싶다면 저도 이 수업들에 대한 반응을 분석했습니다.

public class TokenResponse {

    private String access_token;
    private String token_type;
    private Long expires_in;
    private String refresh_token;
    private String id_token;

    ..getters and setters
}


public class IdTokenPayload {

    private String iss;
    private String aud;
    private Long exp;
    private Long iat;
    private String sub;//users unique id
    private String at_hash;
    private Long auth_time;
    private Boolean nonce_supported;
    private Boolean email_verified;
    private String email;

    ..getters and setters
}

나도 오류가 있었지만 몇 번 수정한 후에 작동합니다. 아래에서 내 수정 사항을 찾으십시오. 참고로 코틀린입니다.

   private suspend fun getPrivateKey(): Status {

    return awaitBlocking {
        val authKeyFile = appleConfig.getString("auth_private_key_file", "")

        val authTokenFilePath = getDataDir()!!.resolve(authKeyFile).absolutePath

        val pemParser = PEMParser(FileReader(authTokenFilePath))
        val converter = JcaPEMKeyConverter()
        val obj = pemParser.readObject() as PrivateKeyInfo

        val privateKey = converter.getPrivateKey(obj)

        successStatus(data = privateKey)
    }
}


 /**
 * generateSecretKey
 */
suspend fun generateSecretKey() : Status{

    val getAuthPrivateKey = getPrivateKey()

    if(getAuthPrivateKey.isError()){
        logger.fatal(getAuthPrivateKey.message)
        return errorStatus("system_busy")
    }

    val privateKeyData =  getAuthPrivateKey.getData<PrivateKey>()

    val clientId = "com.company.app"

    //team id found in apple developer portal
    val teamId = appleConfig.getString("team_id","")

    //apple sign in key ID found in app developer portal
    val authKeyId = appleConfig.getString("auth_key_id","")


    val header = mutableMapOf<String,Any>(
            "alg" to "E256",
            "kid" to authKeyId
    )

    val now = Instant.now().epochSecond

    val claims = mutableMapOf<String,Any>(
            "iss"   to teamId,
            "iat"  to now,
            "exp"  to now + 86400*180,
            "aud"  to "https://appleid.apple.com",
            "sub" to clientId
    )

    println("header - $header")
    println("claims - $claims")

   val token = Jwts.builder()
                .setHeader(header)
                .setClaims(claims)
                .signWith(privateKeyData,SignatureAlgorithm.ES256)
                .compact();


    return successStatus(data = token)
} //end fun





    /**
     * fetchApplePublicKeys
     */
private suspend fun fetchAccessToken(authInfo: JsonObject): Status {
    return  try{

        val authCode = authInfo.getString("auth_code")
        val clientIdToken = authInfo.getString("id_token")

        val accessTokenEndpoint = 
appleConfig.getString("access_token_endpoint")

        val secretKeyTokenStatus = generateSecretKey()

        if(secretKeyTokenStatus.isError()){
            logger.fatal(secretKeyTokenStatus.message)
            return errorStatus("system_busy")
        }

        val clientSecret = secretKeyTokenStatus.getData<String>()

        val redirectUrl = ""

        val clientId = appleConfig.getString("client_id")


        val formData = MultiMap.caseInsensitiveMultiMap()

        formData.add("client_secret",clientSecret)
                .add("client_id",clientId)
                .add("redirect_uri",redirectUrl)
                .add("grant_type","authorization_code")
                .add("code",authCode)

        println("accessTokenEndpoint - $accessTokenEndpoint")
        println("formData - $formData")

        val responseData  = httpClient(this::class)
                .postAbs(accessTokenEndpoint)
                .putHeader("Content-Type","application/x-www-form-urlencoded")
                .sendFormAwait(formData)
                .bodyAsJsonObject()

        println("responseData - ${responseData}")

        if(responseData.containsKey("error")){
            logger.fatal(responseData.getString("error"))
            return errorStatus("social_auth_failed")
        }

        //val responseIdToken = responseData.getString("id_token","")

       return successStatus(data = responseData)
    } catch (e: Exception){
        logger.fatal(e.message,e)
        errorStatus("system_busy")
    }
}

언급URL : https://stackoverflow.com/questions/58252089/sign-in-with-apple-java-user-verification

반응형