[구글 로그인] "error_description": "Malformed auth code." 문제 해결

 

문제 상황

400 Bad Request: "{<EOL>  "error": "invalid_grant",
<EOL>  "error_description": "Malformed auth code."<EOL>}"

 

구글 로그인 구현 중에 위와 같은 에러가 발생했습니다.

구글링을 열심히 해봤지만 해당 문구와 관련된 자료가 적어서 해결하는데 고생했습니다. 😭

 

인증 코드가 잘못되었다는 것을 알려주고 있는데, 구글측으로 전달하는 id 토큰 값을 확인해봤습니다.

일반적으로 구글 로그인 로직은 아래와 같습니다.

 

1. 구글에 사용자 로그인

2. 구글에서 id token 값을 발급

3. id token 값을 서버로 보내서 서버에서 access token을 반환

 

위 경우 id 토큰 값을 백엔드 서버 개발자에게 전달해주면 서버에서 access 토큰을 반환했습니다.

하지만 현재 개발하고 있는 서버에서는 위 과정을 거치지 않고 access token 값을 원했습니다.

 

그 경우 로직은 아래와 같은데,

 

1. 구글에 사용자 로그인

2. 구글에서 auth code 값을 발급

3. auth code 값과 중요 정보로 access token 반환

4. 서버에 access token을 활용한 로그인 

 

오류가 난 이유는 바뀐 로직에 따른 처리를 해야했는데, 기존에 사용한 템플릿을 그대로 가져와서 발생한 것 같습니다.

따라서 access token을 받기 위해서 auth code를 받는 방법이 필요했습니다.

문제 해결

    @POST("token")
    suspend fun fetchGoogleAuthInfo(
        @Body request: LoginGoogleRequest
    ): Response<LoginGoogleResponse>?

 

구글 측에서 access token을 받기 위해서 사용자 인증 정보를 Body에 담아서 전달해야 합니다.

https://www.googleapis.com/oauth2/v4

private fun getGoogleRetrofit(): Retrofit{
    val logInterceptor = HttpLoggingInterceptor()
    logInterceptor.level = HttpLoggingInterceptor.Level.BODY
    return Retrofit.Builder()
        .baseUrl("$GOOGLE_BASE_URL/")
        .addConverterFactory(GsonConverterFactory.create())
        .client(OkHttpClient.Builder().addInterceptor(logInterceptor).build())
        .build()
}

 

baseurl을 구글측에 요청할 주소로 지정하고, auth 정보를 담은 LoginGoogleRequest를 통해 LoginGoogleResponse를 받습니다.

data class LoginGoogleRequest(
    @SerializedName("grant_type")
    val grant_type: String,
    @SerializedName("client_id")
    val client_id: String,
    @SerializedName("client_secret")
    val client_secret: String,
    @SerializedName("redirect_uri")
    val redirect_uri: String,
    @SerializedName("code")
    val code: String
)

data class LoginGoogleResponse(
    var access_token: String = "",
    var expires_in: Int = 0,
    var scope: String = "",
    var token_type: String = "",
    var refresh_token: String = "",
)

 

구글측에 grant_type, client_id, client_secret, redirect_uri, code를 전달합니다.

 

grant_type은 "authorization_code"로 전달하면 되고,

client_id와 client_secret은 구글 클라이언트에 등록한 "web client"에서 확인해야 합니다.

 

정확한 값을 입력해야 하므로 json 다운로드를 통해서 복사하는 것을 추천합니다..!

 

redirect_uri는 정확히 무슨 기능을 하는지 모르겠으나 우선 공백으로 입력했습니다.

 

다음으로는 code인데요.

Malformed auth code 에러가 발생한 근본적인 원인이 여기에 있었습니다.

auth_code를 인증받아서 code를 전달해야 하는데, 저는 이전 프로젝트에서 받은 id token 값을 전달하고 있었습니다.

 

기존에는 구글 로그인 런처를 생성할 때 requestIdToken을 통해서 id Token 값을 받아왔습니다.

마찬가지로 requestServerAuthCode를 활용해서 auth_code 값을 받아오면 됩니다.

    fun login(
        launcher: ActivityResultLauncher<Intent>
    ) {
        CoroutineScope(Dispatchers.IO).launch {
            val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
                .requestIdToken(NetworkManager.GOOGLE_ID)
                .requestServerAuthCode(NetworkManager.GOOGLE_ID)
                .requestEmail()
                .build()
            val googleSignInClient = GoogleSignIn.getClient(context, gso)
            val signInIntent: Intent = googleSignInClient.signInIntent
            launcher.launch(signInIntent)
        }
    }

 

여기서 중요한 점은 idToken 값과 마찬가지로 Web Client의 클라이언트 ID 값을 전달해야 합니다.

    fun setLauncher(
        result: ActivityResult,
        firebaseAuth: FirebaseAuth,
        loginCallback: (String?) -> Unit,
        saveEmail: (String) -> Unit
    ) {
        var tokenId: String?
        var email: String
        if (result.resultCode == AppCompatActivity.RESULT_OK) {
            val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
            try {
                task.getResult(ApiException::class.java)?.let { account ->
                    tokenId = account.serverAuthCode
                    if (tokenId != null && tokenId != "") {
                        val credential: AuthCredential =
                            GoogleAuthProvider.getCredential(account.serverAuthCode, null)

                        firebaseAuth.signInWithCredential(credential).addOnCompleteListener {
                            if (firebaseAuth.currentUser != null) {
                                val user: FirebaseUser = firebaseAuth.currentUser!!
                                email = user.email.toString()
                                Log.e(ContentValues.TAG, "email : $email")
                                val googleSignInToken = account.serverAuthCode ?: ""
                                if (googleSignInToken != "") {
                                    loginCallback(googleSignInToken)
                                    saveEmail(email)
                                } else {
                                    loginCallback(null)
                                }
                            } else {
                                loginCallback(null)
                            }
                        }
                    }
                } ?: throw Exception()
            } catch (e: Exception) {
                e.printStackTrace()
                loginCallback(null)
            }
        } else {
            loginCallback(null)
        }
    }

 

구글 로그인 런처를 생성할 때 tokenId 값으로 account의 serverAuthCode를 받고, 

    suspend fun loginRequest(idToken: String): LoginResult<LoginGoogleResponse> {
        networkManager
            .getGoogleLoginApiService()
            .fetchGoogleAuthInfo(
                networkManager.googleTokenRequest(idToken)
            )
            ?.run {
                return LoginResult.Success(this.body() ?: LoginGoogleResponse())
            } ?: return LoginResult.Error(Exception("Exception"))
    }

 

위와 같이 기존에 제작한 api로 코드를 전송해주면 됩니다.

파라미터가 idToken으로 명시 되어 있는데 실제로는 auth_token을 전달하고 있습니다.

 

구글 측에 전달해야하는 인증 값을 idToken만을 생각하고 있었는데,

auth_token이라는 별도의 토큰으로 access token을 발급받아야 했습니다.

 

정상적으로 요청과 응답을 받을 수 있었습니다.

access_token으로 백엔드 서버로 로그인 요청을 보낼 수 있습니다.

구글 refresh token의 경우 유효기간이 6개월 정도 되는 것으로 알고 있는데,

리플래쉬 토큰을 활용하는 경우 어떻게 처리할지 고민해봐야 할 것 같습니다.