인증 관리자로 사용자 로그인 처리

인증 관리자는 단일 API에서 사용자 이름과 비밀번호, 패스키, 제휴 로그인 솔루션(예: Google 계정으로 로그인)과 같은 멀티 로그인 방식을 지원하여 개발자를 위해 통합을 간소화하는 Jetpack API입니다.

또한 인증 관리자는 인증 방식 전반에서 로그인 인터페이스를 통합하므로 사용자는 자신이 선택한 방식과 상관없이 더 명확하고 쉽게 앱에 로그인할 수 있습니다.

이 페이지에서는 패스키의 개념과 Credential Manager API를 사용하여 패스키를 포함한 인증 솔루션의 클라이언트 측 지원을 구현하는 단계를 설명합니다. 보다 자세하고 구체적인 질문에 대한 답변을 제공하는 별도의 FAQ 페이지도 마련되어 있습니다.

여러분의 의견은 Credential Manager API를 개선하는 데 중요한 역할을 합니다. 발견한 문제나 API 개선을 위한 아이디어를 다음 링크를 통해 공유해 주세요.

의견 보내기

패스키 정보

패스키는 비밀번호를 대체하는 더 안전하고 쉬운 방식입니다. 패스키를 사용하면 생체 인식 센서(예: 지문 또는 얼굴 인식)나 PIN, 패턴으로 사용자가 앱과 웹사이트에 로그인할 수 있습니다. 이를 통해 로그인 환경이 원활해지므로 사용자 이름이나 비밀번호를 사용자가 기억하지 않아도 됩니다.

패스키는 FIDO Alliance와 W3C(World Wide Web Consortium)에서 공동 개발한 표준인 WebAuthn(웹 인증)을 사용합니다. WebAuthn은 공개 키 암호화를 사용하여 사용자를 인증합니다. 사용자가 로그인하는 웹사이트나 앱에서는 공개 키를 확인하고 저장할 수 있지만 비공개 키는 확인하고 저장할 수 없습니다. 비공개 키는 보안 비밀로 안전하게 보호됩니다. 또한 키는 고유하고 웹사이트나 앱에 연결되어 있으므로 패스키는 피싱할 수 없어 보안을 강화할 수 있습니다.

인증 관리자를 통해 사용자는 패스키를 만들어 Google 비밀번호 관리자에 저장할 수 있습니다.

기본 요건

인증 관리자를 사용하려면 이 섹션의 단계를 완료하세요.

최신 플랫폼 버전 사용

인증 관리자는 Android 4.4(API 수준 19) 이상에서 지원됩니다.

앱에 종속 항목 추가

앱 모듈의 빌드 스크립트에 다음 종속 항목을 추가합니다.

Kotlin

dependencies {
    implementation("androidx.credentials:credentials:1.3.0-alpha01")

    // optional - needed for credentials support from play services, for devices running
    // Android 13 and below.
    implementation("androidx.credentials:credentials-play-services-auth:1.3.0-alpha01")
}

Groovy

dependencies {
    implementation "androidx.credentials:credentials:1.3.0-alpha01"

    // optional - needed for credentials support from play services, for devices running
    // Android 13 and below.
    implementation "androidx.credentials:credentials-play-services-auth:1.3.0-alpha01"
}

ProGuard 파일의 클래스 보존

모듈의 proguard-rules.pro 파일에 다음 지시어를 추가합니다.

-if class androidx.credentials.CredentialManager
-keep class androidx.credentials.playservices.** {
  *;
}

앱을 축소, 난독화, 최적화하는 방법을 자세히 알아보세요.

디지털 애셋 링크 지원 추가

Android 앱에 패스키 지원을 사용 설정하려면 앱을 앱이 소유한 웹사이트와 연결합니다. 다음 단계를 완료하여 이 연결을 선언할 수 있습니다.

  1. 디지털 애셋 링크인 JSON 파일을 만듭니다. 예를 들어 웹사이트 https://signin.example.com과 패키지 이름이 com.example인 Android 앱이 로그인 사용자 인증 정보를 공유할 수 있다고 선언하려면 다음 콘텐츠를 포함하는 assetlinks.json이라는 파일을 만듭니다.

    [
      {
        "relation" : [
          "delegate_permission/common.handle_all_urls",
          "delegate_permission/common.get_login_creds"
        ],
        "target" : {
          "namespace" : "android_app",
          "package_name" : "com.example.android",
          "sha256_cert_fingerprints" : [
            SHA_HEX_VALUE
          ]
        }
      }
    ]
    

    relation 필드는 선언되는 관계를 설명하는 하나 이상의 문자열 배열입니다. 앱과 사이트에서 로그인 사용자 인증 정보를 공유한다고 선언하려면 delegate_permission/handle_all_urlsdelegate_permission/common.get_login_creds 관계를 지정합니다.

    target 필드는 선언이 적용되는 애셋을 지정하는 객체입니다. 다음은 웹사이트를 식별하기 위한 필드입니다.

    namespace web
    site

    https://domain[:optional_port] 형식의 웹사이트 URL(예: https://www.example.com).

    domain은 정규화되어야 하며, HTTPS에 포트 443을 사용할 때는 optional_port를 생략해야 합니다.

    site 타겟은 루트 도메인만 될 수 있으며 앱 연결을 특정 하위 디렉터리로 제한할 수 없습니다. URL에 후행 슬래시와 같은 경로를 포함하면 안 됩니다.

    하위 도메인은 일치하는 것으로 간주하지 않습니다. 즉, domainwww.example.com으로 지정하면 도메인 www.counter.example.com은 앱과 연결되지 않습니다.

    다음은 Android 앱을 식별하기 위한 필드입니다.

    namespace android_app
    package_name 앱의 매니페스트에 선언된 패키지 이름입니다. 예: com.example.android
    sha256_cert_fingerprints 서명 인증서의 SHA256 지문입니다.
  2. 디지털 애셋 링크 JSON 파일을 로그인 도메인의 다음 위치에 호스팅합니다.

    https://domain[:optional_port]/.well-known/assetlinks.json
    

    예를 들어 로그인 도메인이 signin.example.com이면 https://signin.example.com/.well-known/assetlinks.json에서 JSON 파일을 호스팅합니다.

    디지털 애셋 링크 파일의 MIME 유형은 JSON이어야 합니다. 서버가 응답에 Content-Type: application/json 헤더를 전송하는지 확인합니다.

  3. Google이 디지털 애셋 링크 파일을 가져오도록 호스트에서 허용하는지 확인해야 합니다. robots.txt 파일이 있으면 이 파일을 통해 Googlebot 에이전트가 /.well-known/assetlinks.json을 가져올 수 있어야 합니다. 대부분의 사이트는 다른 서비스가 /.well-known/ 경로에 있는 파일의 메타데이터에 액세스할 수 있도록 자동화된 에이전트가 이러한 파일을 가져오는 것을 허용할 수 있습니다.

    User-agent: *
    Allow: /.well-known/
    
  4. <application> 아래 매니페스트 파일에 다음 줄을 추가합니다.

    <meta-data android:name="asset_statements" android:resource="@string/asset_statements" />
    
  5. 인증 관리자를 통한 비밀번호 로그인을 사용하는 경우 다음 단계에 따라 매니페스트에서 디지털 애셋 링크를 구성합니다. 패스키만 사용하는 경우에는 이 단계가 필요하지 않습니다.

    Android 앱에서 연결을 선언합니다. 로드할 assetlinks.json 파일을 지정하는 객체를 추가합니다. 문자열에서 사용하는 아포스트로피와 따옴표는 이스케이프 처리해야 합니다. 예:

    <string name="asset_statements" translatable="false">
    [{
      \"include\": \"https://signin.example.com/.well-known/assetlinks.json\"
    }]
    </string>
    
    > GET /.well-known/assetlinks.json HTTP/1.1
    > User-Agent: curl/7.35.0
    > Host: signin.example.com
    
    < HTTP/1.1 200 OK
    < Content-Type: application/json
    

인증 관리자 구성

CredentialManager 객체를 구성하고 초기화하려면 다음과 유사한 로직을 추가하세요.

Kotlin

// Use your app or activity context to instantiate a client instance of
// CredentialManager.
val credentialManager = CredentialManager.create(context)

Java

// Use your app or activity context to instantiate a client instance of
// CredentialManager.
CredentialManager credentialManager = CredentialManager.create(context)

사용자 인증 정보 필드 표시

Android 14 및 이후 버전에서는 사용자 이름 또는 비밀번호 필드와 같은 사용자 인증 정보 필드를 표시하는 데 isCredential 속성을 사용할 수 있습니다. 이 속성은 이 뷰가 인증 관리자 및 서드 파티 사용자 인증 정보 제공업체와 호환되는 사용자 인증 정보 필드임을 나타내며, 자동 완성 서비스에서 더 나은 자동 완성 추천을 제공하도록 지원합니다. 앱이 Credential Manager API를 사용하는 경우 사용 가능한 사용자 인증 정보가 있는 인증 관리자 하단 시트가 표시되며, 사용자 이름이나 비밀번호를 위한 자동 완성 채우기 대화상자를 표시할 필요가 없습니다.

isCredential 속성을 사용하려면 관련 뷰에 추가합니다.

<TextView
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:isCredential="true"
...
 />

사용자 로그인

사용자 계정과 연결된 패스키 및 비밀번호 옵션을 모두 가져오려면 다음 단계를 따르세요.

  1. 비밀번호 및 패스키 인증 옵션을 초기화합니다.

    Kotlin

    // Retrieves the user's saved password for your app from their
    // password provider.
    val getPasswordOption = GetPasswordOption()
    
    // Get passkey from the user's public key credential provider.
    val getPublicKeyCredentialOption = GetPublicKeyCredentialOption(
        requestJson = requestJson
    )

    Java

    // Retrieves the user's saved password for your app from their
    // password provider.
    GetPasswordOption getPasswordOption = new GetPasswordOption();
    
    // Get passkey from the user's public key credential provider.
    GetPublicKeyCredentialOption getPublicKeyCredentialOption =
            new GetPublicKeyCredentialOption(requestJson);
  2. 이전 단계에서 가져온 옵션을 사용하여 로그인 요청을 빌드합니다.

    Kotlin

    val getCredRequest = GetCredentialRequest(
        listOf(getPasswordOption, getPublicKeyCredentialOption)
    )

    Java

    GetCredentialRequest getCredRequest = new GetCredentialRequest.Builder()
        .addCredentialOption(getPasswordOption)
        .addCredentialOption(getPublicKeyCredentialOption)
        .build();
  3. 로그인 흐름을 시작합니다.

    Kotlin

    coroutineScope.launch {
        try {
            val result = credentialManager.getCredential(
                // Use an activity-based context to avoid undefined system UI
                // launching behavior.
                context = activityContext,
                request = getCredRequest
            )
            handleSignIn(result)
        } catch (e : GetCredentialException) {
            handleFailure(e)
        }
    }
    
    fun handleSignIn(result: GetCredentialResponse) {
        // Handle the successfully returned credential.
        val credential = result.credential
    
        when (credential) {
            is PublicKeyCredential -> {
                val responseJson = credential.authenticationResponseJson
                // Share responseJson i.e. a GetCredentialResponse on your server to
                // validate and  authenticate
            }
            is PasswordCredential -> {
                val username = credential.id
                val password = credential.password
                // Use id and password to send to your server to validate
                // and authenticate
            }
          is CustomCredential -> {
              // If you are also using any external sign-in libraries, parse them
              // here with the utility functions provided.
              if (credential.type == ExampleCustomCredential.TYPE)  {
              try {
                  val ExampleCustomCredential = ExampleCustomCredential.createFrom(credential.data)
                  // Extract the required credentials and complete the authentication as per
                  // the federated sign in or any external sign in library flow
                  } catch (e: ExampleCustomCredential.ExampleCustomCredentialParsingException) {
                      // Unlikely to happen. If it does, you likely need to update the dependency
                      // version of your external sign-in library.
                      Log.e(TAG, "Failed to parse an ExampleCustomCredential", e)
                  }
              } else {
                // Catch any unrecognized custom credential type here.
                Log.e(TAG, "Unexpected type of credential")
              }
            } else -> {
                // Catch any unrecognized credential type here.
                Log.e(TAG, "Unexpected type of credential")
            }
        }
    }

    Java

    credentialManager.getCredentialAsync(
        // Use activity based context to avoid undefined
        // system UI launching behavior
        activity,
        getCredRequest,
        cancellationSignal,
        <executor>,
        new CredentialManagerCallback<GetCredentialResponse, GetCredentialException>() {
            @Override
            public void onResult(GetCredentialResponse result) {
                handleSignIn(result);
            }
    
            @Override
            public void onError(GetCredentialException e) {
                handleFailure(e);
            }
        }
    );
    
    public void handleSignIn(GetCredentialResponse result) {
        // Handle the successfully returned credential.
        Credential credential = result.getCredential();
        if (credential instanceof PublicKeyCredential) {
            String responseJson = ((PublicKeyCredential) credential).getAuthenticationResponseJson();
            // Share responseJson i.e. a GetCredentialResponse on your server to validate and authenticate
        } else if (credential instanceof PasswordCredential) {
            String username = ((PasswordCredential) credential).getId();
            String password = ((PasswordCredential) credential).getPassword();
            // Use id and password to send to your server to validate and authenticate
        } else if (credential instanceof CustomCredential) {
            if (ExampleCustomCredential.TYPE.equals(credential.getType())) {
                try {
                    ExampleCustomCredential customCred = ExampleCustomCredential.createFrom(customCredential.getData());
                    // Extract the required credentials and complete the
                    // authentication as per the federated sign in or any external
                    // sign in library flow
                } catch (ExampleCustomCredential.ExampleCustomCredentialParsingException e) {
                    // Unlikely to happen. If it does, you likely need to update the
                    // dependency version of your external sign-in library.
                    Log.e(TAG, "Failed to parse an ExampleCustomCredential", e);
                }
            } else {
                // Catch any unrecognized custom credential type here.
                Log.e(TAG, "Unexpected type of credential");
            }
        } else {
            // Catch any unrecognized credential type here.
            Log.e(TAG, "Unexpected type of credential");
        }
    }

다음 예는 패스키를 가져올 때 JSON 요청의 형식을 지정하는 방법을 보여줍니다.

{
  "challenge": "T1xCsnxM2DNL2KdK5CLa6fMhD7OBqho6syzInk_n-Uo",
  "allowCredentials": [],
  "timeout": 1800000,
  "userVerification": "required",
  "rpId": "credential-manager-app-test.glitch.me"
}

다음 예에서는 공개 키 사용자 인증 정보를 가져온 후 JSON 응답이 어떻게 표시되는지 보여줍니다.

{
  "id": "KEDetxZcUfinhVi6Za5nZQ",
  "type": "public-key",
  "rawId": "KEDetxZcUfinhVi6Za5nZQ",
  "response": {
    "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiVDF4Q3NueE0yRE5MMktkSzVDTGE2Zk1oRDdPQnFobzZzeXpJbmtfbi1VbyIsIm9yaWdpbiI6ImFuZHJvaWQ6YXBrLWtleS1oYXNoOk1MTHpEdll4UTRFS1R3QzZVNlpWVnJGUXRIOEdjVi0xZDQ0NEZLOUh2YUkiLCJhbmRyb2lkUGFja2FnZU5hbWUiOiJjb20uZ29vZ2xlLmNyZWRlbnRpYWxtYW5hZ2VyLnNhbXBsZSJ9",
    "authenticatorData": "j5r_fLFhV-qdmGEwiukwD5E_5ama9g0hzXgN8thcFGQdAAAAAA",
    "signature": "MEUCIQCO1Cm4SA2xiG5FdKDHCJorueiS04wCsqHhiRDbbgITYAIgMKMFirgC2SSFmxrh7z9PzUqr0bK1HZ6Zn8vZVhETnyQ",
    "userHandle": "2HzoHm_hY0CjuEESY9tY6-3SdjmNHOoNqaPDcZGzsr0"
  }
}

사용자 인증 정보가 없는 경우 예외 처리

간혹 사용자에게 사용 가능한 사용자 인증 정보가 없거나 사용자가 사용 가능한 사용자 인증 정보를 사용하는 데 동의하지 않을 수 있습니다. getCredential()이 호출되었으나 사용자 인증 정보를 찾을 수 없으면 NoCredentialException이 반환됩니다. 이 경우 코드에서 NoCredentialException 인스턴스를 처리해야 합니다.

Kotlin

try {
  val credential = credentialManager.getCredential(credentialRequest)
} catch (e: NoCredentialException) {
  Log.e("CredentialManager", "No credential available", e)
}

Java

try {
  Credential credential = credentialManager.getCredential(credentialRequest);
} catch (NoCredentialException e) {
  Log.e("CredentialManager", "No credential available", e);
}

Android 14 이상에서는 getCredential()을 호출하기 전에 prepareGetCredential() 메서드를 사용하여 계정 선택기를 표시할 때 지연 시간을 줄일 수 있습니다.

Kotlin

val response = credentialManager.prepareGetCredential(
  GetCredentialRequest(
    listOf(
      <getPublicKeyCredentialOption>,
      <getPasswordOption>
    )
  )
}

Java

GetCredentialResponse response = credentialManager.prepareGetCredential(
  new GetCredentialRequest(
    Arrays.asList(
      new PublicKeyCredentialOption(),
      new PasswordOption()
    )
  )
);

prepareGetCredential() 메서드는 UI 요소를 호출하지 않습니다. 이는 나중에 getCredential() API를 통해 나머지 사용자 인증 정보 가져오기 작업(UI와 관련됨)을 시작할 수 있도록 준비 작업을 실행하는 데에만 도움이 됩니다.

캐시된 데이터는 PrepareGetCredentialResponse 객체에 반환됩니다. 기존 사용자 인증 정보가 있으면 결과가 캐시되며 나중에 나머지 getCredential() API를 실행하여 캐시된 데이터로 계정 선택기를 불러올 수 있습니다.

등록 흐름

패스키비밀번호를 사용하여 인증을 위해 사용자를 등록할 수 있습니다.

패스키 만들기

사용자가 패스키를 등록하고 재인증에 사용할 수 있도록 하려면 CreatePublicKeyCredentialRequest 객체를 사용하여 사용자 인증 정보를 등록하세요.

Kotlin

fun createPasskey(requestJson: String, preferImmediatelyAvailableCredentials: Boolean) {
    val createPublicKeyCredentialRequest = CreatePublicKeyCredentialRequest(
        // Contains the request in JSON format. Uses the standard WebAuthn
        // web JSON spec.
        requestJson = requestJson,
        // Defines whether you prefer to use only immediately available
        // credentials, not hybrid credentials, to fulfill this request.
        // This value is false by default.
        preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials,
    )

    // Execute CreateCredentialRequest asynchronously to register credentials
    // for a user account. Handle success and failure cases with the result and
    // exceptions, respectively.
    coroutineScope.launch {
        try {
            val result = credentialManager.createCredential(
                // Use an activity-based context to avoid undefined system
                // UI launching behavior
                context = activityContext,
                request = createPublicKeyCredentialRequest,
            )
            handlePasskeyRegistrationResult(result)
        } catch (e : CreateCredentialException){
            handleFailure(e)
        }
    }
}

fun handleFailure(e: CreateCredentialException) {
    when (e) {
        is CreatePublicKeyCredentialDomException -> {
            // Handle the passkey DOM errors thrown according to the
            // WebAuthn spec.
            handlePasskeyError(e.domError)
        }
        is CreateCredentialCancellationException -> {
            // The user intentionally canceled the operation and chose not
            // to register the credential.
        }
        is CreateCredentialInterruptedException -> {
            // Retry-able error. Consider retrying the call.
        }
        is CreateCredentialProviderConfigurationException -> {
            // Your app is missing the provider configuration dependency.
            // Most likely, you're missing the
            // "credentials-play-services-auth" module.
        }
        is CreateCredentialUnknownException -> ...
        is CreateCredentialCustomException -> {
            // You have encountered an error from a 3rd-party SDK. If you
            // make the API call with a request object that's a subclass of
            // CreateCustomCredentialRequest using a 3rd-party SDK, then you
            // should check for any custom exception type constants within
            // that SDK to match with e.type. Otherwise, drop or log the
            // exception.
        }
        else -> Log.w(TAG, "Unexpected exception type ${e::class.java.name}")
    }
}

Java

public void createPasskey(String requestJson, boolean preferImmediatelyAvailableCredentials) {
    CreatePublicKeyCredentialRequest createPublicKeyCredentialRequest =
            // `requestJson` contains the request in JSON format. Uses the standard
            // WebAuthn web JSON spec.
            // `preferImmediatelyAvailableCredentials` defines whether you prefer
            // to only use immediately available credentials, not  hybrid credentials,
            // to fulfill this request. This value is false by default.
            new CreatePublicKeyCredentialRequest(
                requestJson, preferImmediatelyAvailableCredentials);

    // Execute CreateCredentialRequest asynchronously to register credentials
    // for a user account. Handle success and failure cases with the result and
    // exceptions, respectively.
    credentialManager.createCredentialAsync(
        // Use an activity-based context to avoid undefined system
        // UI launching behavior
        requireActivity(),
        createPublicKeyCredentialRequest,
        cancellationSignal,
        executor,
        new CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException>() {
            @Override
            public void onResult(CreateCredentialResponse result) {
                handleSuccessfulCreatePasskeyResult(result);
            }

            @Override
            public void onError(CreateCredentialException e) {
                if (e instanceof CreatePublicKeyCredentialDomException) {
                    // Handle the passkey DOM errors thrown according to the
                    // WebAuthn spec.
                    handlePasskeyError(((CreatePublicKeyCredentialDomException)e).getDomError());
                } else if (e instanceof CreateCredentialCancellationException) {
                    // The user intentionally canceled the operation and chose not
                    // to register the credential.
                } else if (e instanceof CreateCredentialInterruptedException) {
                    // Retry-able error. Consider retrying the call.
                } else if (e instanceof CreateCredentialProviderConfigurationException) {
                    // Your app is missing the provider configuration dependency.
                    // Most likely, you're missing the
                    // "credentials-play-services-auth" module.
                } else if (e instanceof CreateCredentialUnknownException) {
                } else if (e instanceof CreateCredentialCustomException) {
                    // You have encountered an error from a 3rd-party SDK. If
                    // you make the API call with a request object that's a
                    // subclass of
                    // CreateCustomCredentialRequest using a 3rd-party SDK,
                    // then you should check for any custom exception type
                    // constants within that SDK to match with e.type.
                    // Otherwise, drop or log the exception.
                } else {
                  Log.w(TAG, "Unexpected exception type "
                          + e.getClass().getName());
                }
            }
        }
    );
}

JSON 요청 형식 지정

패스키를 만든 후에는 사용자 계정에 연결하고 패스키의 공개 키를 서버에 저장해야 합니다. 다음 코드 예는 패스키를 만들 때 JSON 요청의 형식을 지정하는 방법을 보여줍니다.

앱에서 원활한 인증 지원하기에 관한 블로그 게시물에서는 패스키를 만들 때와 패스키를 사용하여 인증할 때 JSON 요청의 형식을 지정하는 방법을 설명합니다. 이에 더해 비밀번호가 효과적인 인증 솔루션이 아닌 이유, 기존 생체 인식 사용자 인증 정보를 활용하는 방법, 앱을 내가 소유한 웹사이트와 연결하는 방법, 패스키를 만드는 방법, 패스키를 사용하여 인증하는 방법을 설명합니다.

{
  "challenge": "abc123",
  "rp": {
    "name": "Credential Manager example",
    "id": "credential-manager-test.example.com"
  },
  "user": {
    "id": "def456",
    "name": "helloandroid@gmail.com",
    "displayName": "helloandroid@gmail.com"
  },
  "pubKeyCredParams": [
    {
      "type": "public-key",
      "alg": -7
    },
    {
      "type": "public-key",
      "alg": -257
    }
  ],
  "timeout": 1800000,
  "attestation": "none",
  "excludeCredentials": [
    {"id": "ghi789", "type": "public-key"},
    {"id": "jkl012", "type": "public-key"}
  ],
  "authenticatorSelection": {
    "authenticatorAttachment": "platform",
    "requireResidentKey": true,
    "residentKey": "required",
    "userVerification": "required"
  }
}

authenticatorAttachment 값 설정

authenticatorAttachment 매개변수는 사용자 인증 정보 생성 시에만 설정할 수 있습니다. platform, cross-platform을 지정하거나 값을 지정하지 않을 수 있습니다. 대부분의 경우 값을 지정하지 않는 것이 권장됩니다.

  • platform: 사용자의 현재 기기를 등록하거나 비밀번호 사용자에게 로그인 후 패스키로 업그레이드하라는 메시지를 표시하려면 authenticatorAttachmentplatform으로 설정합니다.
  • cross-platform: 이 값은 일반적으로 다단계 사용자 인증 정보를 등록할 때 사용되며 패스키 컨텍스트에서는 사용되지 않습니다.
  • 값 없음: 사용자가 선호하는 기기에서 패스키를 생성할 수 있는 유연성을 제공하려면(예: 계정 설정) 사용자가 패스키를 추가하도록 선택할 때 authenticatorAttachment 매개변수를 지정하면 안 됩니다. 대부분의 경우 매개변수를 지정하지 않는 것이 가장 좋습니다.

중복 패스키 생성 방지

선택사항인 excludeCredentials 배열에 사용자 인증 정보 ID를 나열하여 동일한 패스키 제공업체에 이미 패스키가 있는 경우 새 패스키가 생성되지 않도록 합니다.

JSON 응답 처리

다음 코드 스니펫은 공개 키 사용자 인증 정보를 만드는 JSON 응답의 예를 보여줍니다. 반환된 공개 키 사용자 인증 정보를 처리하는 방법을 자세히 알아보세요.

{
  "id": "KEDetxZcUfinhVi6Za5nZQ",
  "type": "public-key",
  "rawId": "KEDetxZcUfinhVi6Za5nZQ",
  "response": {
    "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoibmhrUVhmRTU5SmI5N1Z5eU5Ka3ZEaVh1Y01Fdmx0ZHV2Y3JEbUdyT0RIWSIsIm9yaWdpbiI6ImFuZHJvaWQ6YXBrLWtleS1oYXNoOk1MTHpEdll4UTRFS1R3QzZVNlpWVnJGUXRIOEdjVi0xZDQ0NEZLOUh2YUkiLCJhbmRyb2lkUGFja2FnZU5hbWUiOiJjb20uZ29vZ2xlLmNyZWRlbnRpYWxtYW5hZ2VyLnNhbXBsZSJ9",
    "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViUj5r_fLFhV-qdmGEwiukwD5E_5ama9g0hzXgN8thcFGRdAAAAAAAAAAAAAAAAAAAAAAAAAAAAEChA3rcWXFH4p4VYumWuZ2WlAQIDJiABIVgg4RqZaJyaC24Pf4tT-8ONIZ5_Elddf3dNotGOx81jj3siWCAWXS6Lz70hvC2g8hwoLllOwlsbYatNkO2uYFO-eJID6A"
  }
}

클라이언트 데이터 JSON에서 출처 확인

origin은 요청이 발생한 애플리케이션 또는 웹사이트를 나타내며 피싱 공격으로부터 보호하기 위해 패스키에서 사용합니다. 앱의 서버는 승인된 앱 및 웹사이트의 허용 목록과 대조하여 클라이언트 데이터 출처를 확인해야 합니다. 서버가 인식할 수 없는 출처의 앱이나 웹사이트로부터 요청을 수신하는 경우 요청은 거부되어야 합니다.

웹 사례에서 origin은 사용자 인증 정보가 로그인된 동일한 사이트 출처를 반영합니다. 예를 들어 URL이 https://www.example.com:8443/store?category=shoes#athletic인 경우 originhttps://www.example.com:8443입니다.

Android 앱의 경우 사용자 에이전트가 자동으로 origin을 호출 앱의 서명으로 설정합니다. 패스키 API 호출자를 검증하려면 이 서명이 서버에서 일치하는 것으로 확인되어야 합니다. Android origin은 다음과 같이 APK 서명 인증서의 SHA-256 해시에서 파생된 URI입니다.

android:apk-key-hash:<sha256_hash-of-apk-signing-cert>

키 저장소의 서명 인증서 SHA-256 해시는 다음 터미널 명령어를 실행하여 찾을 수 있습니다.

keytool -list -keystore <path-to-apk-signing-keystore>

SHA-256 해시는 콜론으로 구분된 16진수 형식(91:F7:CB:F9:D6:81…)이고 Android origin 값은 base64url로 인코딩됩니다. 이 Python 예시는 해시 형식을 콜론으로 구분된 호환되는 16진수 형식으로 변환하는 방법을 보여줍니다.

import binascii
import base64
fingerprint = '91:F7:CB:F9:D6:81:53:1B:C7:A5:8F:B8:33:CC:A1:4D:AB:ED:E5:09:C5'
print("android:apk-key-hash:" + base64.urlsafe_b64encode(binascii.a2b_hex(fingerprint.replace(':', ''))).decode('utf8').replace('=', ''))

fingerprint 값을 자체 값으로 바꿉니다. 다음은 결과를 보여주는 예시입니다.

android:apk-key-hash:kffL-daBUxvHpY-4M8yhTavt5QnFEI2LsexohxrGPYU

그런 다음 이 문자열을 서버에서 허용된 출처로 일치시킬 수 있습니다. 디버깅 및 출시용 인증서와 같은 서명 인증서가 여러 개 있거나 앱이 여러 개 있는 경우 이 프로세스를 반복하고 이러한 모든 출처를 서버에서 유효한 것으로 허용합니다.

사용자 비밀번호 저장

사용자가 앱에서 인증 흐름을 위해 사용자 이름과 비밀번호를 제공하는 경우 개발자는 사용자를 인증하는 데 사용할 수 있는 사용자 인증 정보를 등록할 수 있습니다. 다음과 같이 CreatePasswordRequest 객체를 만들면 됩니다.

Kotlin

fun registerPassword(username: String, password: String) {
    // Initialize a CreatePasswordRequest object.
    val createPasswordRequest =
            CreatePasswordRequest(id = username, password = password)

    // Create credential and handle result.
    coroutineScope.launch {
        try {
            val result =
                credentialManager.createCredential(
                    // Use an activity based context to avoid undefined
                    // system UI launching behavior.
                    activityContext,
                    createPasswordRequest
                  )
            handleRegisterPasswordResult(result)
        } catch (e: CreateCredentialException) {
            handleFailure(e)
        }
    }
}

Java

void registerPassword(String username, String password) {
    // Initialize a CreatePasswordRequest object.
    CreatePasswordRequest createPasswordRequest =
        new CreatePasswordRequest(username, password);

    // Register the username and password.
    credentialManager.createCredentialAsync(
        // Use an activity-based context to avoid undefined
        // system UI launching behavior
        requireActivity(),
        createPasswordRequest,
        cancellationSignal,
        executor,
        new CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException>() {
            @Override
            public void onResult(CreateCredentialResponse result) {
                handleResult(result);
            }

            @Override
            public void onError(CreateCredentialException e) {
                handleFailure(e);
            }
        }
    );
}

사용자 인증 정보 복구 지원

사용자가 사용자 인증 정보를 저장한 기기에 더 이상 액세스할 수 없는 경우 안전한 온라인 백업에서 복구해야 할 수도 있습니다. 이 사용자 인증 정보 복구 프로세스를 지원하는 방법에 관한 자세한 내용은 Google 비밀번호 관리자의 패스키 보안 블로그 게시물의 '액세스 복구 또는 새 기기 추가' 섹션을 참고하세요.

패스키 엔드포인트 잘 알려진 URL을 사용하여 비밀번호 관리 도구 지원 추가

비밀번호 및 사용자 인증 정보 관리 도구와의 원활한 통합과 향후 호환성을 위해 패스키 엔드포인트 잘 알려진 URL 지원을 추가하는 것이 좋습니다. 이는 정렬된 당사자가 패스키 지원을 공식적으로 광고하고 패스키 등록 및 관리를 위한 직접 링크를 제공할 수 있는 개방형 프로토콜입니다.

  1. 웹사이트와 Android 및 iOS 앱이 있는 https://example.com의 신뢰 당사자의 경우 잘 알려진 URL은 https://example.com/.well-known/passkey-endpoints입니다.
  2. URL이 쿼리되면 응답은 다음 스키마를 사용해야 합니다.

    {
      "enroll": "https://example.com/account/manage/passkeys/create"
      "manage": "https://example.com/account/manage/passkeys"
    }
    
  3. 이 링크가 웹이 아닌 앱에서 직접 열리도록 하려면 Android App Links를 사용합니다.

  4. 자세한 내용은 GitHub의 패스키 엔드포인트 잘 알려진 URL 설명에서 확인할 수 있습니다.

일반적인 오류 해결

다음 표에는 몇 가지 일반적인 오류 코드, 설명 및 그 원인에 관한 정보가 나와 있습니다.

오류 코드 및 설명 원인
On Begin Sign In Failure: 16: 취소된 로그인 메시지가 너무 많아 호출자가 일시적으로 차단되었습니다.

개발 중에 이러한 24시간 쿨다운 기간이 발생하면 Google Play 서비스의 앱 저장용량을 삭제하여 재설정할 수 있습니다.

또는 테스트 기기나 에뮬레이터에서 이 쿨다운을 전환하려면 다이얼러 앱으로 이동하여 다음 코드를 입력하세요. *#*#66382723#*#* 다이얼러 앱은 모든 입력을 지우고 닫힐 수 있지만 확인 메시지는 표시되지 않습니다.

On Begin Sign In Failure: 8: 알 수 없는 내부 오류입니다.
  1. 기기가 Google 계정으로 올바르게 설정되지 않았습니다.
  2. 패스키 JSON이 잘못 생성되고 있습니다.
CreatePublicKeyCredentialDomException: 수신 요청을 검증할 수 없습니다. 앱의 패키지 ID가 서버에 등록되어 있지 않습니다. 서버 측 통합에서 이를 검증하세요.
CreateCredentialUnknownException: 비밀번호 저장 중 원탭 16으로부터 비밀번호 발견 실패 응답 16: 사용자에게 Android 자동 완성에서 메시지가 표시될 가능성이 크므로 비밀번호 저장 건너뛰기 이 오류는 Android 13 및 이전 버전에서 Google이 자동 완성 제공업체인 경우에만 발생합니다. 이 경우 사용자에게 Google 자동 완성의 저장 메시지가 표시되고 비밀번호가 Google 비밀번호 관리자에 저장됩니다. Google 자동 완성을 사용하여 저장된 사용자 인증 정보는 Credential Manager API에 양방향으로 공유됩니다. 따라서 이 오류는 무시해도 됩니다.

추가 리소스

Credential Manager API 및 패스키에 관한 자세한 내용은 다음 리소스를 참고하세요.