FIDO2에서 인증 관리자로 이전

패스키, 제휴 로그인, 서드 파티 인증 제공업체를 지원하는 인증 관리자는 Android 인증을 위해 권장되는 API로, 사용자가 사용자 인증 정보를 동기화하고 관리할 수 있는 안전하고 편리한 환경을 제공합니다. 로컬 FIDO2 사용자 인증 정보를 사용하는 개발자의 경우 Credential Manager API와 통합하여 패스키 인증을 지원하도록 앱을 업데이트해야 합니다. 이 문서에서는 FIDO2에서 인증 관리자로 프로젝트를 이전하는 방법을 설명합니다.

FIDO2에서 인증 관리자로 이전해야 하는 이유

대부분의 경우 Android 앱의 인증 제공자를 인증 관리자로 이전해야 합니다. 인증 관리자로 이전하는 이유는 다음과 같습니다.

  • 패스키 지원: 인증 관리자는 비밀번호보다 더 안전하고 사용하기 쉬운 비밀번호가 없는 새로운 인증 메커니즘인 패스키를 지원합니다.
  • 멀티 로그인 방법: 인증 관리자는 비밀번호, 패스키, 제휴 로그인 방법 등의 멀티 로그인 방법을 지원합니다. 이렇게 하면 사용자가 선호하는 인증 방법과 관계없이 더 쉽게 앱에 인증할 수 있습니다.
  • 서드 파티 사용자 인증 정보 제공업체 지원: Android 14 이상에서 인증 관리자는 여러 서드 파티 사용자 인증 정보 제공업체를 지원합니다. 즉, 사용자가 다른 제공업체의 기존 사용자 인증 정보를 사용하여 앱에 로그인할 수 있습니다.
  • 일관된 사용자 환경: 인증 관리자는 여러 앱 및 로그인 메커니즘에서 인증에 더 일관된 사용자 환경을 제공합니다. 이렇게 하면 사용자가 앱의 인증 흐름을 더 쉽게 이해하고 사용할 수 있습니다.

FIDO2에서 인증 관리자로 이전을 시작하려면 아래 단계를 따르세요.

종속 항목 업데이트

  1. 프로젝트의 build.gradle에서 Kotlin 플러그인을 버전 1.8.10 이상으로 업데이트합니다.

    plugins {
      //…
        id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
      //…
    }
    
  2. 프로젝트의 build.gradle에서 인증 관리자 및 Play 서비스 인증을 사용하도록 종속 항목을 업데이트합니다.

    dependencies {
      // ...
      // Credential Manager:
      implementation 'androidx.credentials:credentials:<latest-version>'
    
      // Play Services Authentication:
      // Optional - needed for credentials support from play services, for devices running
      // Android 13 and below:
      implementation 'androidx.credentials:credentials-play-services-auth:<latest-version>'
      // ...
    }
    
  3. FIDO 초기화를 인증 관리자 초기화로 대체합니다. 패스키 생성 및 로그인 방법에 사용하는 클래스에서 이 선언을 추가합니다.

    val credMan = CredentialManager.create(context)
    

패스키 만들기

사용자가 패스키로 로그인할 수 있도록 하려면 먼저 새 패스키를 만들고 이를 사용자 계정과 연결한 후 패스키의 공개 키를 서버에 저장해야 합니다. 등록 함수 호출을 업데이트하여 이 기능으로 앱을 설정합니다.

그림 1. 이 그림은 인증 관리자를 사용하여 패스키를 만들 때 앱과 서버 간에 데이터가 교환되는 방식을 보여줍니다.
  1. 패스키 생성 중에 createCredential() 메서드로 전송되는 필수 매개변수를 가져오려면 WebAuthn 사양에 설명된 대로 name("residentKey").value("required")registerRequest() 서버 호출에 추가합니다.

    suspend fun registerRequest(sessionId: String ... {
        // ...
        .method("POST", jsonRequestBody {
            name("attestation").value("none")
            name("authenticatorSelection").objectValue {
                name("residentKey").value("required")
            }
        }).build()
        // ...
    }
    
  2. registerRequest() 및 모든 하위 함수의 return 유형을 JSONObject로 설정합니다.

    suspend fun registerRequest(sessionId: String): ApiResult<JSONObject> {
        val call = client.newCall(
            Request.Builder()
                .url("$BASE_URL/<your api url>")
                .addHeader("Cookie", formatCookie(sessionId))
                .method("POST", jsonRequestBody {
                    name("attestation").value("none")
                    name("authenticatorSelection").objectValue {
                        name("authenticatorAttachment").value("platform")
                        name("userVerification").value("required")
                        name("residentKey").value("required")
                    }
                }).build()
        )
        val response = call.await()
        return response.result("Error calling the api") {
            parsePublicKeyCredentialCreationOptions(
                body ?: throw ApiException("Empty response from the api call")
            )
        }
    }
    
  3. 인텐트 런처 및 활동 결과 호출을 처리하는 메서드를 뷰에서 안전하게 삭제합니다.

  4. 이제 registerRequest()JSONObject를 반환하므로 PendingIntent를 만들지 않아도 됩니다. 반환된 인텐트를 JSONObject로 바꿉니다. Credential Manager API에서 createCredential()을 호출하도록 인텐트 런처 호출을 업데이트합니다. createCredential() API 메서드를 호출합니다.

    suspend fun createPasskey(
        activity: Activity,
        requestResult: JSONObject
        ): CreatePublicKeyCredentialResponse? {
            val request = CreatePublicKeyCredentialRequest(requestResult.toString())
            var response: CreatePublicKeyCredentialResponse? = null
            try {
                response = credMan.createCredential(
                    request as CreateCredentialRequest,
                    activity
                ) as CreatePublicKeyCredentialResponse
            } catch (e: CreateCredentialException) {
    
                showErrorAlert(activity, e)
    
                return null
            }
            return response
        }
    
  5. 호출이 성공하면 응답을 서버로 다시 전송합니다. 이 호출의 요청 및 응답은 FIDO2 구현과 유사하므로 변경할 필요가 없습니다.

패스키로 인증

패스키 생성을 설정한 후에는 사용자가 패스키를 사용하여 로그인하고 인증할 수 있도록 앱을 설정할 수 있습니다. 이렇게 하려면 인증 관리자 결과를 처리하도록 인증 코드를 업데이트하고 패스키를 통해 인증하는 함수를 구현합니다.

그림 2. 인증 관리자의 패스키 인증 흐름
  1. getCredential() 요청으로 전송하는 데 필요한 정보를 가져오기 위한 서버에 대한 로그인 요청 호출은 FIDO2 구현과 동일합니다. 변경하지 않아도 됩니다.
  2. 등록 요청 호출과 마찬가지로 반환된 응답은 JSONObject 형식입니다.

    /**
     * @param sessionId The session ID to be used for the sign-in.
     * @param credentialId The credential ID of this device.
     * @return a JSON object.
     */
    suspend fun signinRequest(): ApiResult<JSONObject> {
        val call = client.newCall(Builder().url(buildString {
            append("$BASE_URL/signinRequest")
        }).method("POST", jsonRequestBody {})
            .build()
        )
        val response = call.await()
        return response.result("Error calling /signinRequest") {
            parsePublicKeyCredentialRequestOptions(
                body ?: throw ApiException("Empty response from /signinRequest")
            )
        }
    }
    
    /**
     * @param sessionId The session ID to be used for the sign-in.
     * @param response The JSONObject for signInResponse.
     * @param credentialId id/rawId.
     * @return A list of all the credentials registered on the server,
     * including the newly-registered one.
     */
    suspend fun signinResponse(
        sessionId: String, response: JSONObject, credentialId: String
        ): ApiResult<Unit> {
    
            val call = client.newCall(
                Builder().url("$BASE_URL/signinResponse")
                    .addHeader("Cookie",formatCookie(sessionId))
                    .method("POST", jsonRequestBody {
                        name("id").value(credentialId)
                        name("type").value(PUBLIC_KEY.toString())
                        name("rawId").value(credentialId)
                        name("response").objectValue {
                            name("clientDataJSON").value(
                                response.getString("clientDataJSON")
                            )
                            name("authenticatorData").value(
                                response.getString("authenticatorData")
                            )
                            name("signature").value(
                                response.getString("signature")
                            )
                            name("userHandle").value(
                                response.getString("userHandle")
                            )
                        }
                    }).build()
            )
            val apiResponse = call.await()
            return apiResponse.result("Error calling /signingResponse") {
            }
        }
    
  3. 인텐트 런처 및 활동 결과 호출을 처리하는 메서드를 뷰에서 안전하게 삭제합니다.

  4. 이제 signInRequest()JSONObject를 반환하므로 PendingIntent를 만들지 않아도 됩니다. 반환된 인텐트를 JSONObject로 바꾸고 API 메서드에서 getCredential()을 호출합니다.

    suspend fun getPasskey(
        activity: Activity,
        creationResult: JSONObject
        ): GetCredentialResponse? {
            Toast.makeText(
                activity,
                "Fetching previously stored credentials",
                Toast.LENGTH_SHORT)
                .show()
            var result: GetCredentialResponse? = null
            try {
                val request= GetCredentialRequest(
                    listOf(
                        GetPublicKeyCredentialOption(
                            creationResult.toString(),
                            null
                        ),
                        GetPasswordOption()
                    )
                )
                result = credMan.getCredential(activity, request)
                if (result.credential is PublicKeyCredential) {
                    val publicKeycredential = result.credential as PublicKeyCredential
                    Log.i("TAG", "Passkey ${publicKeycredential.authenticationResponseJson}")
                    return result
                }
            } catch (e: Exception) {
                showErrorAlert(activity, e)
            }
            return result
        }
    
  5. 호출이 성공하면 서버로 응답을 다시 전송하여 사용자를 검증하고 인증합니다. 이 API 호출의 요청 및 응답 매개변수는 FIDO2 구현과 유사하므로 변경하지 않아도 됩니다.

추가 리소스