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

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

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

패스키 정보

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

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

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

기본 요건

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

최신 플랫폼 버전 사용

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

앱에 종속 항목 추가

앱 모듈의 build.gradle 파일에 다음 종속 항목을 추가합니다.

Groovy

dependencies {
    implementation "androidx.credentials:credentials:1.0.0-alpha02"

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

Kotlin

dependencies {
    implementation("androidx.credentials:credentials:1.0.0-alpha02")

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

디지털 애셋 링크 지원 추가

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

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

    [{
      "relation": ["delegate_permission/common.get_login_creds"],
      "target": {
        "namespace": "web",
        "site": "https://signin.example.com"
      }
     },
     {
      "relation": ["delegate_permission/common.get_login_creds"],
      "target": {
        "namespace": "android_app",
        "package_name": "com.example",
        "sha256_cert_fingerprints": [
          SHA_HEX_VALUE
        ]
      }
     }]
    

    relation 필드는 선언되는 관계를 설명하는 하나 이상의 문자열 배열입니다. 앱과 사이트에서 로그인 사용자 인증 정보를 공유한다고 선언하려면 delegate_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. Android 앱에서 다음과 같이 연결을 선언합니다.

    1. asset_statements 문자열 리소스를 strings.xml 파일에 추가합니다. asset_statements 문자열은 로드할 assetlinks.json 파일을 지정하는 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)

사용자 로그인

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

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

    Kotlin

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

    Java

    // Retrieves the user's saved password for your app from their
    // password provider.
    GetPasswordOption getPasswordOption = new GetPasswordOption();
    
    // Get passkeys from the user's public key credential provider.
    GetPublicKeyCredentialOption getPublicKeyCredentialOption =
            new GetPublicKeyCredentialOption(requestJson, preferImmediatelyAvailableCredentials);
  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(
                request = getCredRequest,
                activity = activity,
            )
            handleSignIn(result)
        } catch (e : GetCredentialException) {
            handleFailure(e)
        }
    }
    
    fun handleSignIn(result: GetCredentialResponse) {
        // Handle the successfully returned credential.
        val credential = result.credential
    
        when (credential) {
            is PublicKeyCredential -> {
                responseJson = credential.authenticationResponseJson
                fidoAuthenticateWithServer(responseJson)
            }
            is PasswordCredential -> {
                val username = credential.id
                val password = credential.password
                passwordAuthenticateWithServer(username, password)
            }
            else -> {
                // Catch any unrecognized credential type here.
                Log.e(TAG, "Unexpected type of credential")
            }
        }
    }

    Java

    credentialManager.getCredentialAsync(
        getCredRequest,
        activity,
        cancellationSignal,
        requireContext().getMainExecutor(),
        new CredentialManagerCallback<GetCredentialResponse, GetCredentialException>() {
            @Override
            public void onResult(GetCredentialResponse result) {
                // Handle the successfully returned credential.
                Credential credential = result.getCredential();
                if (credential instanceof PublicKeyCredential) {
                    String responseJson = ((PublicKeyCredential) credential)
                            .getAuthenticationResponseJson();
                    fidoAuthenticateToServer(responseJson);
                } else if (credential instanceof PasswordCredential) {
                    Log.d(TAG, "Got PasswordCredential");
                    String id = ((PasswordCredential) credential).getId();
                    String password = ((PasswordCredential) credential)
                            .getPassword();
                    firebaseSignInWithPassword(id, password);
                } else {
                  Log.e(
                      TAG,
                      "Unexpected type of credential: " +
                      credential.getClass().getName());
                }
            }
    
            @Override
            public void onError(GetCredentialException e) {
                Log.e(TAG, "Sign in failed with exception", e);
            }
        }
    );

다음 스니펫은 패스키를 가져올 때 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"
  }
}

등록 흐름

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

패스키 만들기

사용자가 패스키를 등록하고 재인증에 사용할 수 있도록 하려면 다음과 같이 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(
                request = createPublicKeyCredentialRequest,
                activity = activity,
            )
            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 CreateCustomCredentialException -> {
                // 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(
        createPublicKeyCredentialRequest,
        requireActivity(),
        cancellationSignal,
        requireContext().getMainExecutor(),
        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 CreateCustomCredentialException) {
                    // 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": "nhkQXfE59Jb97VyyNJkvDiXucMEvltduvcrDmGrODHY",
  "rp": {
    "name": "CredMan App Test",
    "id": "credential-manager-app-test.glitch.me"
  },
  "user": {
    "id": "2HzoHm_hY0CjuEESY9tY6-3SdjmNHOoNqaPDcZGzsr0",
    "name": "helloandroid@gmail.com",
    "displayName": "helloandroid@gmail.com"
  },
  "pubKeyCredParams": [
    {
      "type": "public-key",
      "alg": -7
    },
    {
      "type": "public-key",
      "alg": -257
    }
  ],
  "timeout": 1800000,
  "attestation": "none",
  "excludeCredentials": [],
  "authenticatorSelection": {
    "authenticatorAttachment": "platform",
    "requireResidentKey": true,
    "residentKey": "required",
    "userVerification": "required"
  }
}

JSON 응답 처리

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

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

사용자 비밀번호 저장

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

Kotlin

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

    // Create credentials and handle result.
    coroutineScope.launch {
        try {
            val result =
                    credentialManager.createCredential(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(
        createPasswordRequest,
        requireActivity(),
        cancellationSignal,
        requireContext().getMainExecutor(),
        new CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException> {
            @Override
            public void onResult(CreateCredentialResponse result) {
                handleResult(result);
            }

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

일반적인 오류 해결

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

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

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

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

On Begin Sign In Failure: 8: 알 수 없는 내부 오류입니다.
  1. 기기가 Google 계정으로 올바르게 설정되지 않았습니다.
  2. 패스키 JSON이 잘못 생성되고 있습니다.
CreatePublicKeyCredentialDomException: 수신 요청을 검증할 수 없습니다. 앱의 패키지 ID가 서버에 등록되어 있지 않습니다. 서버 측 통합에서 이를 검증하세요.