OAuth2 서비스 인증

인증 토큰 로직 다이어그램
그림 1. Android 계정 관리자에서 유효한 인증 토큰을 가져오는 절차

온라인 서비스에 안전하게 액세스하려면 사용자는 신원을 증명하여 서비스에 인증해야 합니다. 타사 서비스에 액세스하는 애플리케이션의 경우 보안 문제는 훨씬 더 복잡합니다. 사용자가 서비스에 액세스하도록 인증받아야 하며 애플리케이션도 사용자를 대신할 수 있는 권한을 부여받아야 합니다.

타사 서비스 인증을 다루는 업계 표준 방식은 OAuth2 프로토콜입니다. OAuth2는 인증 토큰이라는 단일 값을 제공하며 이 값은 사용자의 신원뿐 아니라 사용자를 대신할 수 있는 애플리케이션의 권한도 나타냅니다. 이 과정에서는 OAuth2를 지원하는 Google 서버에 연결하는 방법을 보여줍니다. Google 서비스를 예로 사용했지만, 여기서 보여주는 기법은 OAuth2 프로토콜을 제대로 지원하는 모든 서비스에 적용됩니다.

OAuth2를 사용하면 다음에 유용합니다.

  • 사용자로부터 사용자 계정을 이용해 온라인 서비스에 액세스할 권한을 얻습니다.
  • 사용자를 대신해 온라인 서비스에 인증합니다.
  • 인증 오류를 처리합니다.

정보 수집하기

OAuth2 사용을 시작하려면 액세스하려는 API에 관해 다음과 같은 사항을 알아야 합니다.

  • 액세스할 서비스의 URL
  • 인증 범위: 앱이 요청하는 특정 액세스 유형을 정의하는 문자열입니다. 예를 들어 Google Tasks 읽기 전용 액세스의 인증 범위는 View your tasks이고 Google Tasks 읽기/쓰기 액세스의 인증 범위는 Manage your tasks입니다.
  • 클라이언트 ID클라이언트 비밀번호: 서비스에 앱 ID를 알리는 문자열입니다. 서비스 소유자로부터 직접 이 문자열을 가져와야 합니다. Google에는 클라이언트 ID와 클라이언트 비밀번호를 가져오는 셀프서비스 시스템이 있습니다. 이 시스템을 사용하여 Google Tasks API에 사용할 값을 가져오는 방법은 REST API 승인 및 사용하기 문서에서 설명합니다.

인터넷 권한 요청하기

Android 6.0(API 수준 23) 이상을 타겟팅하는 앱의 경우 getAuthToken() 메서드 자체에 권한이 필요하지는 않습니다. 하지만 토큰 작업을 실행하려면 다음 코드 스니펫과 같이 매니페스트 파일에 INTERNET 권한을 추가해야 합니다.

    <manifest ... >
        <uses-permission android:name="android.permission.INTERNET" />
        ...
    </manifest>
    

인증 토큰 요청하기

토큰을 가져오려면 AccountManager.getAuthToken()을 호출합니다.

주의: 일부 계정 작업에는 네트워크 통신이 포함될 수 있으므로 대부분의 AccountManager 메서드는 비동기식입니다. 따라서 모든 인증 작업을 단일 함수로 처리하는 대신 일련의 콜백으로 구현해야 합니다.

다음 스니펫은 일련의 콜백을 사용하여 토큰을 가져오는 방법을 보여줍니다.

Kotlin

    val am: AccountManager = AccountManager.get(this)
    val options = Bundle()

    am.getAuthToken(
            myAccount_,                     // Account retrieved using getAccountsByType()
            "Manage your tasks",            // Auth scope
            options,                        // Authenticator-specific options
            this,                           // Your activity
            OnTokenAcquired(),              // Callback called when a token is successfully acquired
            Handler(OnError())              // Callback called if an error occurs
    )
    

자바

    AccountManager am = AccountManager.get(this);
    Bundle options = new Bundle();

    am.getAuthToken(
        myAccount_,                     // Account retrieved using getAccountsByType()
        "Manage your tasks",            // Auth scope
        options,                        // Authenticator-specific options
        this,                           // Your activity
        new OnTokenAcquired(),          // Callback called when a token is successfully acquired
        new Handler(new OnError()));    // Callback called if an error occurs
    

이 예에서 OnTokenAcquiredAccountManagerCallback을 구현하는 클래스입니다. AccountManagerBundle이 포함된 AccountManagerFutureOnTokenAcquired에서 run()을 호출합니다. 호출이 성공하면 토큰은 Bundle 내에 있습니다.

다음은 Bundle에서 토큰을 가져오는 방법입니다.

Kotlin

    private class OnTokenAcquired : AccountManagerCallback<Bundle> {

        override fun run(result: AccountManagerFuture<Bundle>) {
            // Get the result of the operation from the AccountManagerFuture.
            val bundle: Bundle = result.getResult()

            // The token is a named value in the bundle. The name of the value
            // is stored in the constant AccountManager.KEY_AUTHTOKEN.
            val token: String = bundle.getString(AccountManager.KEY_AUTHTOKEN)
        }
    }
    

자바

    private class OnTokenAcquired implements AccountManagerCallback<Bundle> {
        @Override
        public void run(AccountManagerFuture<Bundle> result) {
            // Get the result of the operation from the AccountManagerFuture.
            Bundle bundle = result.getResult();

            // The token is a named value in the bundle. The name of the value
            // is stored in the constant AccountManager.KEY_AUTHTOKEN.
            String token = bundle.getString(AccountManager.KEY_AUTHTOKEN);
            ...
        }
    }
    

모든 것이 제대로 진행되면 BundleKEY_AUTHTOKEN 키에 포함되어 있는 유효한 토큰을 사용할 수 있습니다. 하지만 순조롭게 진행되지 않을 때도 있습니다.

인증 토큰 다시 요청하기

첫 번째 인증 토큰 요청이 다음과 같은 이유로 인해 실패할 수도 있습니다.

  • 기기나 네트워크의 오류로 인해 AccountManager가 실패했습니다.
  • 사용자가 앱에 계정 액세스 권한을 부여하지 않기로 했습니다.
  • 저장된 계정 사용자 인증 정보가 충분하지 않아 계정 액세스 권한을 얻지 못했습니다.
  • 캐시된 인증 토큰이 만료되었습니다.

위의 처음 두 사례의 경우 애플리케이션은 일반적으로 사용자에게 오류 메시지를 표시하기만 하는 방식으로 사소하게 처리할 수 있습니다. 네트워크가 다운되었거나 사용자가 액세스 권한을 부여하지 않기로 결정했을 때는 애플리케이션이 할 수 있는 일이 별로 없습니다. 마지막 두 사례는 좀 더 복잡합니다. 제대로 작동하는 애플리케이션은 이러한 실패를 자동으로 처리해야 하기 때문입니다.

세 번째 실패 사례, 즉 불충분한 사용자 인증 정보는 AccountManagerCallback(이전 예에서 OnTokenAcquired)에서 수신한 Bundle을 통해 전달됩니다. BundleKEY_INTENT 키에 Intent가 포함되어 있다면 인증자가 사용자와 직접 상호작용해야 유효한 토큰을 획득할 수 있다고 개발자에게 알려주는 것입니다.

인증자에서 Intent가 반환되는 데는 많은 이유가 있을 수 있습니다. 사용자가 이 계정에 처음으로 로그인했기 때문일 수도 있고 사용자의 계정이 만료되어 사용자가 다시 로그인해야 하거나 저장된 사용자 인증 정보가 잘못되었기 때문일 수도 있습니다. 계정에 2단계 인증이 요구되거나 망막 스캔을 위해 카메라를 활성화해야 하기 때문일 수도 있습니다. 그 이유가 무엇인지는 중요하지 않습니다. 유효한 토큰을 원한다면 Intent를 실행하여 가져와야 합니다.

Kotlin

    private inner class OnTokenAcquired : AccountManagerCallback<Bundle> {

        override fun run(result: AccountManagerFuture<Bundle>) {
            val launch: Intent? = result.getResult().get(AccountManager.KEY_INTENT) as? Intent
            if (launch != null) {
                startActivityForResult(launch, 0)
            }
        }
    }
    

자바

    private class OnTokenAcquired implements AccountManagerCallback<Bundle> {
        @Override
        public void run(AccountManagerFuture<Bundle> result) {
            ...
            Intent launch = (Intent) result.getResult().get(AccountManager.KEY_INTENT);
            if (launch != null) {
                startActivityForResult(launch, 0);
                return;
            }
        }
    }
    

이 예에서는 startActivityForResult()를 사용하므로 자체 활동에서 onActivityResult()를 구현하여 Intent의 결과를 캡처할 수 있습니다. 이 점은 중요합니다. 인증자의 응답 Intent에서 결과를 캡처하지 않으면 사용자가 인증에 성공했는지 알 수 없습니다. 결과가 RESULT_OK이면 인증자는 개발자가 요청한 액세스 수준에 충분하도록 저장된 사용자 인증 정보를 업데이트한 것이며 AccountManager.getAuthToken()을 다시 호출하여 새 인증 토큰을 요청해야 합니다.

마지막 사례인 토큰 만료는 실제로 AccountManager 실패가 아닙니다. 토큰의 만료 여부를 알아내려면 서버에 연결하는 것이 유일한 방법이지만 AccountManager가 지속적으로 온라인에서 모든 토큰의 상태를 확인하면 많은 비용을 낭비하게 됩니다. 토큰 만료는 개발자의 애플리케이션이 인증 토큰을 사용하여 온라인 서비스에 액세스하려고 할 때에만 감지될 수 있는 실패입니다.

온라인 서비스에 연결하기

아래의 예는 Google 서버에 연결하는 방법을 보여줍니다. Google은 업계 표준 OAuth2 프로토콜을 사용하여 요청을 인증하므로 여기서 설명하는 기법은 광범위하게 적용할 수 있습니다. 하지만 모든 서버가 다르다는 것을 염두에 두세요. 구체적인 상황을 고려하여 이 안내의 내용을 일부 수정해야 할 수도 있습니다.

Google API를 사용할 때는 각 요청별로 값 4개(API 키, 클라이언트 ID, 클라이언트 비밀번호, 인증 키)를 제공해야 합니다. 처음 세 값은 Google API 콘솔 웹사이트에서 가져오고 마지막은 AccountManager.getAuthToken()을 호출하여 가져온 문자열 값입니다. 이 값을 HTTP 요청의 일부로 Google 서버에 전달합니다.

Kotlin

    val url = URL("https://www.googleapis.com/tasks/v1/users/@me/lists?key=$your_api_key")
    val conn = url.openConnection() as HttpURLConnection
    conn.apply {
        addRequestProperty("client_id", your client id)
        addRequestProperty("client_secret", your client secret)
        setRequestProperty("Authorization", "OAuth $token")
    }
    

자바

    URL url = new URL("https://www.googleapis.com/tasks/v1/users/@me/lists?key=" + your_api_key);
    URLConnection conn = (HttpURLConnection) url.openConnection();
    conn.addRequestProperty("client_id", your client id);
    conn.addRequestProperty("client_secret", your client secret);
    conn.setRequestProperty("Authorization", "OAuth " + token);
    

요청에서 HTTP 오류 코드 401이 반환된다면 토큰이 거부된 것입니다. 마지막 섹션에서 설명한 대로 거부되는 가장 일반적인 이유는 토큰 만료입니다. 해결 방법은 간단합니다. AccountManager.invalidateAuthToken()을 호출하고 토큰 획득 절차를 한 번 더 반복하면 됩니다.

토큰 만료가 상당히 흔한 상황이고 이를 해결하는 방법도 쉽기 때문에 많은 애플리케이션이 토큰을 요청하기도 전에 토큰이 만료되었을 것으로 가정합니다. 토큰 갱신이 서버에서 비용이 별로 들지 않는 작업이라면 AccountManager.getAuthToken()을 먼저 호출하기 전에 AccountManager.invalidateAuthToken()을 호출하여 인증 토큰을 두 번 요청할 필요성을 없애는 것이 좋습니다.