获取 API 访问令牌

本指南说明如何通过 API 调用获取 ACP 平台访问令牌。适用于需要以编程方式访问平台 API 的场景。

概述

ACP 使用基于 Dex 的 OIDC 身份验证系统。登录流程遵循 OAuth 2.0 Authorization Code Flow,由五次 API 调用组成,且必须在同一个 HTTP 会话内完成(共享 cookie)

Step 1: Retrieve login metadata
    GET /console-platform/api/v1/token/login

Step 2: Obtain the Dex request ID (req_id)
    GET /dex/api/v1/authorize  ← uses query parameters from the auth_url returned in Step 1

Step 3: Retrieve the RSA public key
    GET /dex/pubkey

Step 4: Submit encrypted credentials and obtain the authorization code
    POST /dex/api/v1/authorize/{idp}?req={req_id}

Step 5: Exchange the authorization code for a token
    GET /console-platform/api/v1/token/callback
WARNING

整个流程必须使用同一个 HTTP 会话(共享 cookie jar)。第 2 步会设置 cpaas_oidc_auth_flow 会话 cookie,第 5 步需要该 cookie 才能签发令牌。没有此 cookie 时,第 5 步会返回 invalid authentication session

前提条件

ParameterDescriptionValue
PLATFORM_URL平台 HTTPS 基础 URL(不包含结尾路径)https://<platform>
CLIENT_IDOAuth client ID(固定)alauda-auth
SCOPE请求的权限 scope(固定)openid profile offline_access email groups ext
REDIRECT_URI回调 URL(固定){PLATFORM_URL}/dex/callback
IDPIdentity provider IDlocal(本地账户默认值)、ldapoidc 等。

步骤

步骤 1:获取登录元数据

请求:

GET /console-platform/api/v1/token/login

查询参数:

ParameterValue
client_idalauda-auth
redirect_urihttps://{platform}/dex/callback
response_typecode
scopeopenid profile offline_access email groups ext

响应(200 OK):

{
  "auth_url": "https://{platform}/console-dex/auth?access_type=offline&client_id=alauda-auth&code_challenge=...&state=...&...",
  "state": "VpweqrTxqHxpXZzjNOUe1Zbqe7fPIYFN3_HoEYAxWnA",
  "logout_url": "https://{platform}/dex/logout?post_logout_redirect_uri=...&state=..."
}
NOTE

auth_url 中的 code_challenge 及相关参数由服务端生成,用于 server-to-Dex PKCE 交换,并存储在 cpaas_oidc_auth_flow cookie 中。你无需解析这些参数。在第 2 步中,请原样传递 auth_url 中的完整查询字符串


步骤 2:获取 Dex 请求 ID

使用第 1 步返回的 auth_url 中的查询参数。

此步骤会设置 cpaas_oidc_auth_flow 会话 cookie。后续所有请求都必须携带该 cookie。

请求:

GET /dex/api/v1/authorize?{query_from_auth_url}

其中 {query_from_auth_url} 是从第 1 步 auth_url 中提取出的完整查询字符串(原样传递):

access_type=offline&client_id=alauda-auth&code_challenge=...&code_challenge_method=S256
&nonce=...&redirect_uri=...&response_type=code&scope=...&state=...

响应(200 OK):

{
  "req": "kyrqxpv6tvnj5a3efhkou3seocnehfxout42rb3p75q3g3oue6u"
}

步骤 3:获取 RSA 公钥

请求:

GET /dex/pubkey

响应(200 OK):

{
  "ts": "1779940350",
  "pubkey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...\n-----END PUBLIC KEY-----\n",
  "pubkey_encode": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0K..."
}
FieldDescription
ts时间戳。必须包含在加密载荷中。每次登录尝试都需要获取新的值。
pubkeyPEM 格式的 RSA 公钥,用于加密密码。
pubkey_encodepubkey 的 Base64 编码形式,与 pubkey 等价。

步骤 4:提交加密凭据并获取授权码

密码加密:

  1. 构造 JSON 载荷:{"ts": "<第 3 步中的 ts>", "password": "<明文密码>"}
  2. 使用 RSA 公钥和 PKCS#1 v1.5 填充方式加密 JSON 字节
  3. 对加密结果进行 Base64 编码(标准编码,不是 URL-safe)

请求:

POST /dex/api/v1/authorize/{idp}?req={req_id}
Content-Type: application/json
Cookie: cpaas_oidc_auth_flow=...

路径参数:

ParameterDescription
{idp}Identity provider ID:local(本地账户)、ldap 等。

查询参数:

ParameterValue
req第 2 步返回的 req ID

请求体:

{
  "account": "admin",
  "password": "<RSA-encrypted Base64 string>"
}

响应(200 OK):

{
  "session_state": "",
  "redirect_url": "https://{platform}/dex/callback?code=qdul75ionlvlsn3ggg2m...&state=VpweqrTxqH..."
}

redirect_url 中提取 codestate 参数,供第 5 步使用。


步骤 5:使用授权码换取令牌

请求:

GET /console-platform/api/v1/token/callback
Cookie: cpaas_oidc_auth_flow=...

查询参数:

ParameterSourceRequired
code第 4 步中从 redirect_url 提取Yes
state第 4 步中从 redirect_url 提取Yes

响应(200 OK):

{
  "token_type": "bearer",
  "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
  "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
  "refresh_token": "CjNuY3V3bmRvcW90NjRwaHl4NXB5bndiY2Zuc2MzcXhkdGhud2...",
  "expire_at": "2025-01-02T12:00:00Z",
  "issued_at": "2025-01-01T12:00:00Z",
  "token_storage": "local"
}
FieldDescription
access_token用于 API 访问的 JWT。在请求头中以 Authorization: Bearer <token> 形式传递。
id_token包含用户身份声明(email、username 等)的 JWT。
refresh_token用于刷新 access_token 的不透明字符串。
expire_ataccess_token 的过期时间(UTC)。
issued_ataccess_token 的签发时间(UTC)。
token_storage令牌存储位置。固定值:local

完整示例

Shell Script(curl)

curl 通过 -c(写入 cookie)和 -b(读取 cookie)共享 cookie jar 文件来维护会话。

依赖项: curljq(JSON 解析)、openssl(RSA 加密)

用法:

# Option 1: edit the configuration section in the script, then run it
bash acp-login.sh

# Option 2: pass credentials via environment variables (recommended — avoids plaintext passwords in the script)
ACP_PLATFORM=https://<platform> ACP_USERNAME=admin ACP_PASSWORD=xxx bash acp-login.sh

# Extract the access_token
bash acp-login.sh | jq -r '.access_token'
#!/usr/bin/env bash
# ACP platform API login script — obtains an access token via the OAuth2 Authorization Code Flow
# Dependencies: curl jq openssl

set -euo pipefail

# ============================================================
# Configuration (can be overridden by environment variables of the same name)
# ============================================================
PLATFORM="${ACP_PLATFORM:-https://your.platform.com}"   # Platform base URL, no trailing path
USERNAME="${ACP_USERNAME:-admin}"                       # Username
PASSWORD="${ACP_PASSWORD:-}"                            # Password (recommended: pass via ACP_PASSWORD env var)
IDP="${ACP_IDP:-local}"                                # Identity provider: local | ldap | oidc
# ============================================================

CLIENT_ID="alauda-auth"
SCOPE="openid profile offline_access email groups ext"
REDIRECT_URI="${PLATFORM}/dex/callback"

log() { echo "$*" >&2; }
die() { echo "ERROR: $*" >&2; exit 1; }

check_deps() {
    local missing=()
    for cmd in curl jq openssl; do
        command -v "$cmd" &>/dev/null || missing+=("$cmd")
    done
    [[ ${#missing[@]} -eq 0 ]] || die "Missing required tools: ${missing[*]}"
}

check_vars() {
    [[ -n "${PLATFORM}" ]] || die "PLATFORM must not be empty"
    [[ -n "${USERNAME}" ]] || die "USERNAME must not be empty"
    [[ -n "${PASSWORD}" ]] || die "PASSWORD must not be empty (set the ACP_PASSWORD environment variable or fill it in the script)"
}

urlencode() { jq -rn --arg v "$1" '$v|@uri'; }

# Extract a field from a JSON response; print the response and exit if the field is missing
get_field() {
    local json="$1" field="$2" label="$3"
    local val
    val=$(printf '%s' "$json" | jq -r --arg f "$field" '.[$f] // empty' 2>/dev/null) \
      || die "${label}: jq parse failed"
    [[ -n "$val" ]] || die "${label}: field '${field}' missing from response
$(printf '%s' "$json" | jq . 2>/dev/null || printf '%s' "$json")"
    printf '%s' "$val"
}

# Extract a query parameter value from a URL
query_param() {
    printf '%s' "$1" | grep -o "${2}=[^&]*" | cut -d= -f2-
}

main() {
    check_deps
    check_vars

    local COOKIE_JAR PUBKEY_RESP PUBKEY_PEM PAYLOAD
    COOKIE_JAR=$(mktemp /tmp/acp-login-XXXXXX.jar)
    PUBKEY_RESP=$(mktemp /tmp/acp-pubkey-XXXXXX.json)
    PUBKEY_PEM=$(mktemp /tmp/acp-key-XXXXXX.pem)
    PAYLOAD=$(mktemp /tmp/acp-payload-XXXXXX.json)
    trap "rm -f '$COOKIE_JAR' '$PUBKEY_RESP' '$PUBKEY_PEM' '$PAYLOAD'" EXIT

    # Step 1: Retrieve login metadata
    log "[1/5] Retrieving login metadata..."
    local STEP1 AUTH_URL
    STEP1=$(curl -sk -c "$COOKIE_JAR" \
      "${PLATFORM}/console-platform/api/v1/token/login?client_id=${CLIENT_ID}&redirect_uri=$(urlencode "${REDIRECT_URI}")&response_type=code&scope=$(urlencode "${SCOPE}")") \
      || die "Step 1: curl request failed"
    AUTH_URL=$(get_field "$STEP1" "auth_url" "Step 1")

    # Step 2: Obtain the Dex request ID (also writes the cpaas_oidc_auth_flow cookie)
    log "[2/5] Obtaining Dex request ID..."
    local AUTH_QUERY STEP2 REQ_ID
    AUTH_QUERY="${AUTH_URL#*\?}"
    STEP2=$(curl -sk -c "$COOKIE_JAR" -b "$COOKIE_JAR" \
      "${PLATFORM}/dex/api/v1/authorize?${AUTH_QUERY}") \
      || die "Step 2: curl request failed"
    REQ_ID=$(get_field "$STEP2" "req" "Step 2")

    # Step 3: Retrieve the RSA public key (save response to a file to avoid newline truncation in bash variables)
    log "[3/5] Retrieving RSA public key..."
    curl -sk -b "$COOKIE_JAR" "${PLATFORM}/dex/pubkey" -o "$PUBKEY_RESP" \
      || die "Step 3: curl request failed"
    local TS
    TS=$(jq -r '.ts // empty' "$PUBKEY_RESP")
    [[ -n "$TS" ]] || die "Step 3: 'ts' field missing from response"
    jq -r '.pubkey_encode' "$PUBKEY_RESP" | base64 -d > "$PUBKEY_PEM"
    [[ -s "$PUBKEY_PEM" ]] || die "Step 3: public key decode failed"

    # Step 4: Encrypt credentials, submit them, and obtain the authorization code
    log "[4/5] Submitting credentials and obtaining authorization code..."
    jq -cn --arg ts "$TS" --arg pwd "$PASSWORD" '{"ts":$ts,"password":$pwd}' > "$PAYLOAD"
    local ENCRYPTED STEP4 REDIRECT_URL CODE STATE
    ENCRYPTED=$(openssl pkeyutl -encrypt -pubin -inkey "$PUBKEY_PEM" \
      -pkeyopt rsa_padding_mode:pkcs1 -in "$PAYLOAD" 2>/dev/null | base64 | tr -d '\n') \
      || die "Step 4: password encryption failed"
    STEP4=$(curl -sk -b "$COOKIE_JAR" -c "$COOKIE_JAR" \
      -X POST "${PLATFORM}/dex/api/v1/authorize/${IDP}?req=${REQ_ID}" \
      -H "Content-Type: application/json" \
      -d "$(jq -cn --arg account "$USERNAME" --arg password "$ENCRYPTED" '{"account":$account,"password":$password}')") \
      || die "Step 4: curl request failed"
    REDIRECT_URL=$(get_field "$STEP4" "redirect_url" "Step 4 (check username/password and IDP parameter)")
    CODE=$(query_param "$REDIRECT_URL" "code")
    STATE=$(query_param "$REDIRECT_URL" "state")
    [[ -n "$CODE"  ]] || die "Step 4: 'code' parameter missing from redirect_url"
    [[ -n "$STATE" ]] || die "Step 4: 'state' parameter missing from redirect_url"

    # Step 5: Exchange the authorization code for an access token
    log "[5/5] Exchanging authorization code for access token..."
    local TOKENS
    TOKENS=$(curl -sk -b "$COOKIE_JAR" \
      "${PLATFORM}/console-platform/api/v1/token/callback?code=${CODE}&state=$(urlencode "${STATE}")") \
      || die "Step 5: curl request failed"
    get_field "$TOKENS" "access_token" "Step 5" > /dev/null

    local EXPIRE_AT
    EXPIRE_AT=$(printf '%s' "$TOKENS" | jq -r '.expire_at // "unknown"')
    log ""
    log "Login successful | User: ${USERNAME} | Token expires at: ${EXPIRE_AT}"
    log ""
    printf '%s\n' "$TOKENS" | jq .
}

main "$@"

使用令牌

在后续所有 API 请求的 Authorization 请求头中包含 access_token

curl -sk https://your.platform.com/kubernetes/{cluster}/api/v1/namespaces \
  -H "Authorization: Bearer <access_token>"

重要说明

  1. 必须共享会话 cookie:整个登录流程必须使用同一个 HTTP 会话(共享 cookie jar),以便 cpaas_oidc_auth_flow cookie 能够在每一步自动传递。没有此 cookie 时,第 5 步会返回 invalid authentication session (400)

  2. ts 时间戳不能重复使用:第 3 步返回的 ts 对每次请求都是唯一的。加密前必须将其与密码组合成 JSON 载荷。重复使用之前的 ts 值会导致第 4 步认证失败。

  3. 选择 IDP 参数

    • local:ACP 本地账户(默认)
    • ldap:LDAP/AD 域账户;具体 ID 取决于你的平台配置
  4. TLS 证书验证:示例脚本中的 -k 参数会跳过证书验证。仅适用于使用自签名证书的测试环境。在生产环境中,请移除 -k,并配置有效的 CA 证书,或将平台 CA 添加到系统信任存储中。