05
11

๊ตฌ๊ธ€ One-Tap ๋กœ๊ทธ์ธ์„ ํŒŒ์ด์–ด๋ฒ ์ด์Šค๋กœ ๊ฒฝ์œ ํ•ด์„œ ๊ตฌํ˜„ํ–ˆ๋‹ค.

 

๊ธฐ์กด ๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ ๊ตฌํ˜„ํ•œ ์ฝ”๋“œ๋ฅผ ๋จผ์ € ๋ณด์ž. ํŒŒ์ด์–ด๋ฒ ์ด์Šค ๊ณต์‹ ๋ฌธ์„œ์— ์žˆ๋Š” ์Šค๋‹ˆํŽซ์€ ์ž˜ ์ž‘๋™ํ•˜๊ธด ํ•˜์ง€๋งŒ ๋„ˆ๋ฌด ์˜ค๋ž˜๋ผ์„œ ๋‚ด ๋งˆ์Œ๋Œ€๋กœ ๊ฐœ์กฐํ–ˆ๋‹ค.

 

ํ˜น์‹œ ๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ์„ ์˜ˆ์‹œ์ฝ”๋“œ๋Œ€๋กœ ๋”ฐ๋ผํ–ˆ์„ ๋•Œ

developer console is not set up correctly.
service: oauth2:openid amgv: error when calling server using gmsnetworkstack.

๋กœ๊ทธ์— ์ด๋Ÿฐ ๋ฌธ์žฅ์ด ๋ณด์ธ๋‹ค๋ฉด, firebase ํ”„๋กœ์ ํŠธ ์„ค์ •์— SHA-1 ๋“ฑ๋ก์ด ์•ˆ๋ผ์žˆ๋Š” ์ƒํƒœ์ด๋ฏ€๋กœ SHA-1๋“ฑ๋ก์„ ํ•ด์ฃผ๋ฉด ํ•ด๊ฒฐ๋  ๊ฒƒ์ด๋‹ค.

https://github.com/firebase/snippets-android/blob/b8f65e9150fe927a5f0473e15e16fa5803189b60/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/GoogleSignInActivity.kt#L49-L54

 

snippets-android/auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/GoogleSignInActivity.kt at b8f65e9150fe927a5f

Android snippets for firebase.google.com. Contribute to firebase/snippets-android development by creating an account on GitHub.

github.com

 

ํ•˜๋‚˜ํ•˜๋‚˜ ๋œฏ์–ด๋ณด์ž. ํ˜„์žฌ ํ”„๋กœ์ ํŠธ์—์„œ SAA ๊ตฌ์กฐ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์–ด์„œ Fragment๊ธฐ์ค€์œผ๋กœ ์ž‘์„ฑ๋˜์—ˆ๋‹ค.

์˜ˆ์ œ ์ฝ”๋“œ์—์„œ onActivityResult๋กœ ์ž‘์„ฑ๋˜์–ด์žˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ๋‹ค. ๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ ์•กํ‹ฐ๋น„ํ‹ฐ๋ฅผ ๋„์šฐ๊ณ , ๊ทธ๊ฒƒ์— ๋Œ€ํ•œ ์‘๋‹ต๊ฐ’์„ ๋ฐ›์•„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ถ€๋ถ„์ธ๋ฐ ์ด ์ฝ”๋“œ๋Š” deprecated๋œ์ง€ ์˜ค๋ž˜๋‹ค.

๊ทธ๋ž˜์„œ ActivityResultLauncher๋ฅผ ์‚ฌ์šฉํ•ด ์ฝ”๋“œ ๊ตฌ์กฐ๋ฅผ ์ข€ ๋ฐ”๊ฟ”๋ดค๋‹ค. ์ดˆ๊ธฐํ™” ์‹œ์ ์ด๋ž‘ ์‚ฌ์šฉ์‹œ์ ์ด ๋‹ค๋ฅด๋ฏ€๋กœ lateinit์œผ๋กœ ์„ ์–ธํ•ด์คฌ๋‹ค.

private lateinit var googleSignInLauncher: ActivityResultLauncher<Intent>

๊ทธ๋ฆฌ๊ณ  ์ด ๋Ÿฐ์ฒ˜๋ฅผ ์ดˆ๊ธฐํ™”ํ•ด์ค˜์•ผํ•˜๋Š”๋ฐ fragment์—์„œ๋Š” ์–ด๋””์„œ ์ดˆ๊ธฐํ™” ํ•ด์ค˜์•ผํ• ๊นŒ?

View๊ฐ€ ์ƒ์„ฑ๋œ ๋’ค์— ์ดˆ๊ธฐํ™”ํ•˜๋ฉด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. ๊ทธ๋ž˜์„œ onCreate์—์„œ View๊ฐ€ ์ƒ์„ฑ๋˜๊ธฐ ์ „์— ์ดˆ๊ธฐํ™”๋ฅผ ํ•ด์ค˜์•ผํ•œ๋‹ค.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    googleSignInLauncher =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
            googleSignIn(result.data)
        }
}

result์˜ dataํ”„๋กœํผํ‹ฐ์— ๋‹ด๊ธด ๊ฒŒ Intent๋‹ค. ์ด๊ฑธ ๋ฐ›์•„์„œ ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๋ฉด ๋œ๋‹ค.

์ด๋ ‡๊ฒŒ ๋งŒ๋“  launcher๋Š” ํ˜ธ์ถœํ•  ๋•Œ ์ธ์ž๋กœ singInIntent๋ฅผ ๋ฐ›๋Š”๋‹ค.

val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
            .requestIdToken(activity.getString(R.string.default_web_client_id))
            .requestEmail()
            .build()

val googleSignInClient = GoogleSignIn.getClient(activity, gso)
googleSignInClient.signInIntent // <- ์ด๊ฑธ ์‚ฌ์šฉํ•œ๋‹ค

์ด ์ฝ”๋“œ๋ฅผ ๊ทธ๋Œ€๋กœ ๊ฐ–๋‹ค์“ฐ๋ฉด ์•ˆ๋˜๊ณ  ๊ทธ๋ƒฅ ํ๋ฆ„๋งŒ ๋ณด๋ฉด ๋œ๋‹ค. 

requestIdToken์— ์‚ฌ์šฉ๋˜๋Š” default_web_client_id๋Š” ํŒŒ์ด์–ด๋ฒ ์ด์Šค auth๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋‹ค๋ฉด ๋นŒ๋“œํ•  ๋•Œ ์ž๋™์œผ๋กœ ์ƒ๊ธฐ๋Š”๋ฐ, google-services 4.4.1๋ฒ„์ „์˜ ๊ฒฝ์šฐ ํŒŒ์ผ์ด ๋ฉ€์ฉกํ•˜๊ฒŒ ์žˆ์–ด๋„ ์—†๋‹ค๊ณ  ๋‚˜์˜ค๋Š” IDE๋ฒ„๊ทธ๊ฐ€ ์žˆ๋‹ค.(๋Ÿฐํƒ€์ž„ ๋•Œ์˜ ๋ฌธ์ œ๋Š” ์—†๋‹ค.) 4.3.8๋กœ ๋ฒ„์ „์„ ๋‚ด๋ฆฌ๋‹ˆ๊นŒ ์‚ฌ๋ผ์กŒ๋Š”๋ฐ, ์›Œ๋‚™ ํŒŒ์ด์–ด๋ฒ ์ด์Šค๊ฐ€ ๋ฌผ๋ ค์žˆ๋Š”๊ฒŒ ๋งŽ๋‹ค๋ณด๋‹ˆ ๋ฒ„์ „ ์ˆ˜์ •์ด ์ข€ ๋‘๋ ค์› ๋‹ค.

private fun googleSignIn(data: Intent?) {
    runCatching {
        val task = GoogleSignIn.getSignedInAccountFromIntent(data)
        task.getResult(ApiException::class.java)
    }.onSuccess { account ->
        CoroutineScope(Dispatchers.Main).launch {
            val user = loginRepoInstance.firebaseAuthWithGoogle(account.idToken!!)
            updateUI(user)
        }
    }.onFailure { e ->
        Log.w(TAG, "Google sign in failed $e")
    }
}

updateUI๋Š” ๋กœ๊ทธ์ธ์— ์„ฑ๊ณตํ–ˆ์„ ๋•Œ ui ์—…๋ฐ์ดํŠธ๋ฅผ ํ•ด์ฃผ๋Š” ๋ถ€๋ถ„์ด๋ผ์„œ ์ด๊ฑด ๊ตฌํ˜„์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง€๋‹ˆ ๋„˜์–ด๊ฐ€๊ณ , runCatching์œผ๋กœ try-catch๋ฅผ ๋Œ€์‹ ํ•ด ๊ฐ€๋…์„ฑ์„ ๋†’์—ฌ๋ดค๋‹ค. 
๋ฌธ์ œ๋Š” ์œ„์—์„œ client๋ฐ›์•„์˜ฌ ๋•Œ๋„ ์‚ฌ์šฉ๋˜์—ˆ๋˜ GoogleSignIn์ด deprecated๋˜์–ด์„œ Credential Manager๋ฅผ ์‚ฌ์šฉํ•ด์•ผ๋œ๋‹ค๋Š” ์ ์ด๋‹ค. ๊ทธ๋ž˜์•ผ ์•„๋ž˜ ์‚ฌ์ง„์ฒ˜๋Ÿผ ์ค„ ๊ทธ์–ด์ง„ ๋ชจ์Šต์„ ์น˜์›Œ๋ฒ„๋ฆด ์ˆ˜๊ฐ€ ์žˆ๋‹ค.

๋ฏธ๊ด€ ์ƒ ํ•ด๋กญ๋‹ค

 

# CredentialManager ๋กœ Migrationํ•˜๊ธฐ

๊ณต์‹๋ฌธ์„œ์— ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฐ€์ด๋“œ๊ฐ€ ์žˆ๋Š”๋ฐ ์†”์งํžˆ ์—…๋ฐ์ดํŠธ๋„ ๋Š๋ฆฌ๊ณ  ์ข€ ๋ˆˆ์— ์ž˜ ์•ˆ๋“ค์–ด์˜จ๋‹ค.

implementation("androidx.credentials:credentials:<latest version>")
implementation("androidx.credentials:credentials-play-services-auth:<latest version>")
implementation("com.google.android.libraries.identity.googleid:googleid:<latest version>")

credential๋ฒ„์ „์€ 1.3.0-alpha03, googleid๋Š” 1.1.0์ด 2024-05-11๊ธฐ์ค€ ์ตœ์‹ ๋ฒ„์ „์ด๋‹ค.

 

Credential๋กœ ๋ฐ”๊พธ๊ฒŒ ๋˜๋ฉด ๋‹ฌ๋ผ์ง€๋Š” ์ ์ด ๋ช‡๊ฐ€์ง€ ์ƒ๊ธด๋‹ค.

  1. Activity/Fragment์—์„œ ์ฒ˜๋ฆฌํ•˜๋˜ ์ฝ”๋“œ๋ฅผ ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.
  2. ๋กœ๊ทธ์ธ ์š”์ฒญํ•˜๋Š” ๊ณผ์ •์ด suspend fun์„ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ฝ”๋ฃจํ‹ด์„ ๋ฌด์กฐ๊ฑด ์‚ฌ์šฉํ•ด์•ผ๋œ๋‹ค.
  3. ๋กœ๊ทธ์ธ UI๊ฐ€ ์˜ˆ์˜๊ฒŒ ๋ฐ”๋€๋‹ค.

๊ทธ๋ƒฅ ๋กœ๊ทธ์ธ ํŒ์—…์ด์—ˆ๋˜๊ฒŒ ๋ฐ”ํ…€์‹œํŠธ๋กœ ๋ฐ”๋€๋‹ค. ์• ๋‹ˆ๋ฉ”์ด์…˜๋„ ๋”ํ•ด์ ธ์„œ ๋„ˆ๋ฌด ๋ณด๊ธฐ ์ข‹๋‹ค.

๋กœ๊ทธ์ธ์„ ์ˆ˜ํ–‰ํ• ๋ ค๋ฉด credential์„ ํ†ตํ•ด idToken์„ ๊ฐ€์ ธ์™€์•ผํ•˜๋Š”๋ฐ ๊ทธ ๊ณผ์ •์ด suspend fun์œผ๋กœ ์ž‘์„ฑ๋˜์–ด์žˆ๋‹ค. 

suspend fun getCredential(
        context: Context,
        request: GetCredentialRequest,
    ): GetCredentialResponse = suspendCancellableCoroutine { continuation ->
    // Any Android API that supports cancellation should be configured to propagate
    // coroutine cancellation as follows:
    val canceller = CancellationSignal()
    continuation.invokeOnCancellation { canceller.cancel() }

    val callback = object : CredentialManagerCallback<GetCredentialResponse,
        GetCredentialException> {
        override fun onResult(result: GetCredentialResponse) {
            if (continuation.isActive) {
                continuation.resume(result)
            }
        }

        override fun onError(e: GetCredentialException) {
            if (continuation.isActive) {
                continuation.resumeWithException(e)
            }
        }
    }

    getCredentialAsync(
        context,
        request,
        canceller,
        // Use a direct executor to avoid extra dispatch. Resuming the continuation will
        // handle getting to the right thread or pool via the ContinuationInterceptor.
        Runnable::run,
        callback)
}

๋งค๊ฐœ๋ณ€์ˆ˜๋กœ๋Š” context(Intent ๋‚ ๋ฆฌ๋ ค๋ฉด ํ•„์š”ํ•˜๋‹ˆ๊นŒ), GetCredentialRequest ์ด๋ ‡๊ฒŒ ๋‘๊ฐœ๊ฐ€ ํ•„์š”ํ•˜๋‹ค. 

์ค€๋น„ ๊ณผ์ •์ด ์žˆ๋‹ค. credential manager ์ธ์Šคํ„ด์Šค๊ฐ€ ํ•„์š”ํ•˜๊ณ , ๊ตฌ๊ธ€๋กœ๊ทธ์ธ์— ์‚ฌ์šฉํ•  google id option ์ธ์Šคํ„ด์Šค, ๊ทธ๋ฆฌ๊ณ  idToken์„ ์–ป์„ ๋•Œ ํ•„์š”ํ•œ get credential request ์ธ์Šคํ„ด์Šค ์ด๋ ‡๊ฒŒ ์„ธ๊ฐœ๋ฅผ ์ดˆ๊ธฐํ™” ํ•ด์ค˜์•ผํ•œ๋‹ค.

val credentialManager = CredentialManager.create(context)
val googleIdOption = GetGoogleIdOption.Builder()
    .setFilterByAuthorizedAccounts(false)
    .setAutoSelectEnabled(true)
    .setServerClientId(context.getString(R.string.default_web_client_id))
    .build()

val credentialRequest: GetCredentialRequest = GetCredentialRequest.Builder()
    .addCredentialOption(googleIdOption)
    .build()

googleIdOption์—์„œ setAutoSelectEnabled๋ฅผ ํ™œ์„ฑํ™”ํ•˜๋ฉด, ๋ฐ”ํ…€์‹œํŠธ๊ฐ€ ์˜ฌ๋ผ๊ฐ€๋ฉด์„œ ๊ณ„์ •์ด ์ž๋™์„ ํƒ๋œ๋‹ค. ๋‚˜๋Š” ํŒŒ์ด์–ด๋ฒ ์ด์Šค auth๋ฅผ ์‚ฌ์šฉํ•˜๋‹ˆ๊นŒ ๋”ฐ๋กœ web client id๋ฅผ ๊ด€๋ฆฌํ•˜์ง€๋Š” ์•Š์•˜๋‹ค.

setFilterByAuthorizedAccounts(false)๋Š” ์ด์ „์— ๋กœ๊ทธ์ธ ํ•œ ๊ณ„์ •์„ ํ™•์ธํ•  ์ง€ ์ •ํ•˜๋Š” ์˜ต์…˜์ด๋‹ค.

 

googleIdOption๊ณผ credentialRequest๋ฅผ buildํ•˜๋Š” ๊ณผ์ •์ด ์•ฝ๊ฐ„ ์ง€์—ฐ์‹œ๊ฐ„์ด ์žˆ๋‹ค๊ณ  ๋Š๊ปด์„œ ์ด๊ฑด Splash Activity์—์„œ ์ฒ˜๋ฆฌํ•˜๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค.

 

์ด์ œ ๊ตฌํ˜„์„ ๋ณด์ž. ๋‚ด๊ฐ€ ์ž‘์„ฑํ•œ ํ•จ์ˆ˜๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

suspend fun requestGoogleLogin(
        context: Context,
        onSuccessListener: (FirebaseUser?) -> Unit,
    ) {
    runCatching {
        credentialManager.getCredential(
            request = credentialRequest,
            context = context,
        )
    }.onSuccess {
        when (val credential = it.credential) {
            is CustomCredential -> {
                if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
                    runCatching {
                        GoogleIdTokenCredential.createFrom(credential.data)
                    }.onSuccess { googleIdTokenCredential ->
                        onSuccessListener(
                            firebaseAuthWithGoogle(googleIdTokenCredential.idToken)
                        )
                    }.onFailure {
                        Log.e(TAG, "Received an invalid google id token response", it)
                    }
                }
            }
        }
    }.onFailure {
        Log.d(TAG, "requestGoogleLogin: ${it.localizedMessage ?: "unknown error"}")
    }
}

ui์—…๋ฐ์ดํŠธ ํ•  ๋ถ€๋ถ„์€ fragment์—์„œ ์ฃผ์ž…๋ฐ›๋„๋ก ํ•˜๊ณ , ํŒŒ์ด์–ด๋ฒ ์ด์Šค๋กœ ๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ์„ ํ• ๊ฒƒ์ด๋ฏ€๋กœ CustomCredential๋กœ ์บ์ŠคํŒ… ํ•ด์ค€๋‹ค. ์ด๋ ‡๊ฒŒ ์–ป์€ googleIdToken์„ ์ด์ „์— ์‚ฌ์šฉํ•˜๋˜ firebaseAuthWithGoogle๋กœ ์ „๋‹ฌํ•˜๋ฉด ๋๋‚œ๋‹ค.

 

๋‹ค๋ฅธ ์•ฑ๋“ค์—์„œ ๋ณผ ์ˆ˜ ์žˆ๋Š” Google ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธ์œผ๋กœ ๋กœ๊ทธ์ธํ•˜๊ณ ์‹ถ๋‹ค๋ฉด ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ getCredential์˜ request์— ๋„ฃ์–ด์ฃผ๋ฉด ๋œ๋‹ค๊ณ  ํ•œ๋‹ค. ์‚ฌ์‹ค ์ด ๋ถ€๋ถ„์€ ์ฐจ์ด์ ์„ ์ž˜ ๋ชจ๋ฅด๊ฒ ๋‹ค...

val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption.Builder()
  .setServerClientId(WEB_CLIENT_ID)
  .setNonce(<nonce string to use when generating a Google ID token>)
  .build()

Credential Manager๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•œ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ฉด์˜ค ํžˆ๋ ค ์ „๋ณด๋‹ค ํ›จ์”ฌ ๊ฐ„๋‹จํ•ด์ง„ ๊ฒƒ ๊ฐ™๋‹ค. client๋ฅผ ์ง์ ‘ ๊ด€๋ฆฌํ•  ์ˆ˜ ์—†๋‹ค ๋ฟ์ด์ง€ ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ์— ์ถฉ์‹คํ•˜๊ณ  ํŽธ๋ฆฌํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค. 

 

๋กœ๊ทธ ์•„์›ƒ๋„ ๊ฐ„๋‹จํ•˜๋‹ค.

suspend fun logout() {
    credentialManager.clearCredentialState(request = ClearCredentialStateRequest())
    auth.signOut()
}

๋กœ๊ทธ์•„์›ƒ์— ์‚ฌ์šฉ๋˜๋Š” clearCredentialState์—ญ์‹œ suspend fun์œผ๋กœ ์ž‘์„ฑ๋˜์–ด์žˆ๋‹ค. ๋‚˜๋Š” firebase auth๋„ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋กœ๊ทธ์•„์›ƒ์„ ํ•˜๋ ค๋ฉด credential, firebase auth ๋‘˜ ๋‹ค ํ•ด์ค˜์•ผํ•ด์„œ ์ด๋ ‡๊ฒŒ ๋งŒ๋“ค์—ˆ๋‹ค.

 

์ธ์ž๋กœ๋Š” ClearCredentialStateRequest ์ธ์Šคํ„ด์Šค๋ฅผ ๊ทธ๋ƒฅ ๋„ฃ์–ด์ฃผ๋ฉด ๋œ๋‹ค. ๋“ค์–ด๊ฐ€๋ณด๋ฉด ์•„๋ฌด ๋‚ด์šฉ๋„ ์—†๋Š”๋ฐ, ๊ทธ๋ƒฅ flag์šฉ๋„๋กœ ์‚ฌ์šฉํ•˜๋Š” ์ธ์Šคํ„ด์Šค๋˜๊ฐ€, ๊ณต๊ฐœ๋œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์•„๋‹๊นŒ ์ถ”์ธกํ•ด๋ณธ๋‹ค.

 

๋„์›€์ด ๋๋‹ค๋ฉด ๋Œ“๊ธ€์ด๋‚˜ ๊ณต๊ฐ ๋ฒ„ํŠผ ํ•œ ๋ฒˆ์”ฉ ๋ˆ„๋ฅด๊ณ  ๊ฐ€์ฃผ์„ธ์š”!

 

๋ฐ˜์‘ํ˜•
COMMENT