Xác thực với các dịch vụ OAuth2

Sơ đồ logic của mã thông báo xác thực
Hình 1. Quy trình lấy mã thông báo xác thực hợp lệ từ Trình quản lý tài khoản Android.

Để truy cập an toàn vào một dịch vụ trực tuyến, người dùng cần xác thực với dịch vụ đó – họ cần cung cấp bằng chứng về danh tính của mình. Đối với một ứng dụng truy cập vào dịch vụ của bên thứ ba, vấn đề bảo mật thậm chí còn phức tạp hơn. Người dùng không chỉ cần được xác thực để truy cập vào dịch vụ mà còn cần được uỷ quyền để hành động thay mặt người dùng.

Phương thức tiêu chuẩn trong ngành để xử lý việc xác thực các dịch vụ của bên thứ ba là giao thức OAuth2. OAuth2 cung cấp một giá trị duy nhất, được gọi là mã xác thực, đại diện cho cả danh tính của người dùng và quyền của ứng dụng để hành động thay mặt người dùng. Bài học này minh hoạ cách kết nối với một máy chủ Google có hỗ trợ OAuth2. Mặc dù bạn sử dụng các dịch vụ của Google làm ví dụ, nhưng các kỹ thuật được minh hoạ sẽ hoạt động trên mọi dịch vụ hỗ trợ chính xác giao thức OAuth2.

Việc sử dụng OAuth2 giúp ích cho:

  • Nhận sự cho phép của người dùng để truy cập vào một dịch vụ trực tuyến bằng tài khoản của họ.
  • Xác thực một dịch vụ trực tuyến thay mặt cho người dùng.
  • Xử lý lỗi xác thực.

Thu thập thông tin

Để bắt đầu sử dụng OAuth2, bạn cần biết một số điều dành riêng cho API về dịch vụ bạn đang cố truy cập:

  • URL của dịch vụ bạn muốn truy cập.
  • Phạm vi xác thực là một chuỗi xác định kiểu truy cập cụ thể mà ứng dụng của bạn đang yêu cầu. Ví dụ: phạm vi xác thực đối với quyền truy cập chỉ đọc vào Google Tasks là View your tasks, trong khi phạm vi xác thực đối với quyền đọc/ghi vào Google Tasks là Manage your tasks.
  • Mã ứng dụng khách và mật khẩu ứng dụng khách là các chuỗi giúp nhận dạng ứng dụng của bạn với dịch vụ. Bạn cần lấy các chuỗi này trực tiếp từ chủ sở hữu dịch vụ. Google có một hệ thống tự phục vụ để lấy mã ứng dụng khách và khoá bí mật.

Yêu cầu cấp quyền truy cập Internet

Đối với ứng dụng nhắm đến Android 6.0 (API cấp 23) trở lên, chính phương thức getAuthToken() không yêu cầu quyền nào. Tuy nhiên, để thực hiện các thao tác trên mã thông báo, bạn cần thêm quyền INTERNET vào tệp kê khai, như minh hoạ trong đoạn mã sau:

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

Yêu cầu mã thông báo xác thực

Để nhận mã thông báo, hãy gọi AccountManager.getAuthToken().

Thận trọng: Vì một số hoạt động tài khoản có thể liên quan đến hoạt động giao tiếp mạng, nên hầu hết các phương thức AccountManager đều không đồng bộ. Điều này có nghĩa là thay vì thực hiện mọi công việc xác thực trong một hàm, bạn cần triển khai chức năng này dưới dạng một loạt lệnh gọi lại.

Đoạn mã sau đây cho biết cách xử lý một loạt các lệnh gọi lại để lấy mã thông báo:

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
)

Java

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

Trong ví dụ này, OnTokenAcquired là một lớp triển khai AccountManagerCallback. AccountManager gọi run() trên OnTokenAcquired bằng AccountManagerFuture chứa Bundle. Nếu lệnh gọi thành công, mã thông báo sẽ nằm trong Bundle.

Dưới đây là cách bạn có thể nhận mã thông báo từ 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)
    }
}

Java

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);
        ...
    }
}

Nếu mọi việc suôn sẻ, Bundle sẽ chứa mã thông báo hợp lệ trong khoá KEY_AUTHTOKEN và bạn đã hoàn tất.

Yêu cầu đầu tiên của bạn về mã thông báo xác thực có thể không thành công vì một số lý do:

  • Một lỗi thiết bị hoặc mạng đã khiến AccountManager không hoạt động được.
  • Người dùng quyết định không cấp cho ứng dụng của bạn quyền truy cập vào tài khoản này.
  • Thông tin xác thực tài khoản đã lưu trữ không đủ để có quyền truy cập vào tài khoản này.
  • Mã thông báo xác thực được lưu vào bộ nhớ đệm đã hết hạn.

Các ứng dụng có thể xử lý 2 trường hợp đầu tiên theo cách đơn giản, thường là bằng cách hiển thị thông báo lỗi cho người dùng. Nếu mạng ngừng hoạt động hoặc người dùng quyết định không cấp quyền truy cập, thì ứng dụng của bạn sẽ không thể làm gì được. Hai trường hợp cuối phức tạp hơn một chút, vì các ứng dụng hoạt động tốt sẽ tự động xử lý những lỗi này.

Trường hợp lỗi thứ ba, khi không có đủ thông tin xác thực, sẽ được thông báo qua Bundle mà bạn nhận được trong AccountManagerCallback (OnTokenAcquired của ví dụ trước). Nếu Bundle đưa Intent vào khoá KEY_INTENT, thì trình xác thực sẽ cho bạn biết cần tương tác trực tiếp với người dùng thì mới có thể cung cấp cho bạn mã thông báo hợp lệ.

Có thể có nhiều lý do để trình xác thực trả về một Intent. Đây có thể là lần đầu tiên người dùng đăng nhập vào tài khoản này. Có thể tài khoản của người dùng đã hết hạn và họ cần đăng nhập lại hoặc có thể có thể thông tin xác thực mà họ đã lưu trữ không chính xác. Có thể tài khoản yêu cầu xác thực hai yếu tố hoặc cần kích hoạt camera để quét võng mạc. Lý do không thực sự quan trọng. Nếu muốn có mã thông báo hợp lệ, bạn sẽ phải kích hoạt Intent để lấy mã đó.

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)
        }
    }
}

Java

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;
        }
    }
}

Xin lưu ý rằng ví dụ này sử dụng startActivityForResult() để bạn có thể thu thập kết quả của Intent bằng cách triển khai onActivityResult() trong hoạt động của riêng mình. Điều quan trọng là: Nếu không thu được kết quả từ phản hồi Intent của trình xác thực, thì bạn không thể biết liệu người dùng đã xác thực thành công hay chưa.

Nếu kết quả là RESULT_OK, thì trình xác thực đã cập nhật thông tin xác thực được lưu trữ để đủ cho cấp truy cập mà bạn đã yêu cầu và bạn nên gọi lại AccountManager.getAuthToken() để yêu cầu mã thông báo xác thực mới.

Trường hợp cuối cùng (khi mã thông báo đã hết hạn) thực ra không phải là lỗi AccountManager. Cách duy nhất để khám phá xem một mã thông báo đã hết hạn hay chưa là liên hệ với máy chủ, và việc AccountManager liên tục truy cập trực tuyến để kiểm tra trạng thái của tất cả các mã thông báo sẽ lãng phí và tốn kém. Vì vậy, đây là lỗi chỉ có thể phát hiện được khi một ứng dụng như của bạn cố gắng sử dụng mã thông báo xác thực để truy cập vào một dịch vụ trực tuyến.

Kết nối với dịch vụ trực tuyến

Ví dụ bên dưới cho biết cách kết nối với máy chủ Google. Vì Google sử dụng giao thức OAuth2 tiêu chuẩn ngành để xác thực các yêu cầu, nên các kỹ thuật được thảo luận ở đây có thể áp dụng rộng rãi. Tuy nhiên, hãy lưu ý rằng mỗi máy chủ là khác nhau. Bạn có thể thấy mình cần thực hiện những điều chỉnh nhỏ đối với những hướng dẫn này để phù hợp với tình huống cụ thể của mình.

API của Google yêu cầu bạn cung cấp 4 giá trị cho mỗi yêu cầu: khoá API, mã ứng dụng khách, mật khẩu ứng dụng khách và khoá xác thực. Ba URL đầu tiên đến từ trang web Bảng điều khiển API của Google. Giá trị cuối cùng là giá trị chuỗi mà bạn nhận được bằng cách gọi AccountManager.getAuthToken(). Bạn chuyển các giá trị này đến Máy chủ Google như một phần của yêu cầu HTTP.

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")
}

Java

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);

Nếu yêu cầu trả về mã lỗi HTTP 401, thì mã thông báo của bạn đã bị từ chối. Như đã đề cập trong phần trước, lý do phổ biến nhất cho vấn đề này là mã thông báo đã hết hạn. Cách khắc phục rất đơn giản: gọi AccountManager.invalidateAuthToken() và lặp lại quá trình thu nạp mã thông báo một lần nữa.

Việc mã thông báo hết hạn rất phổ biến và việc khắc phục mã cũng rất dễ dàng, nên nhiều ứng dụng sẽ giả định rằng mã thông báo đã hết hạn trước khi yêu cầu. Nếu việc gia hạn mã thông báo là một thao tác rẻ cho máy chủ của bạn, thì bạn nên gọi AccountManager.invalidateAuthToken() trước lệnh gọi đầu tiên đến AccountManager.getAuthToken() và phòng không cần yêu cầu mã thông báo xác thực 2 lần.