API Console
test
Sign in with BES

接入手机 App

做完后,App 通过自己的登录客户端(client)用 BES 账号登录并选租户(tenant),令牌绑定设备上的密钥;App 只连登录地址,不带证书。

环境登录地址带证书调用的接口地址
localhttps://id.local.globalbes.sghttps://id.local.globalbes.sg:19443
testhttps://id.test.globalbes.sghttps://id.test.globalbes.sg:9443
staging待定待定
prod待定待定

Sign in to send requests to see which of these steps are done. Sign in

1. 登记
1.1

登记 App 的登录客户端

在哪里做:测试台

登记 App 的登录客户端 bim-app,得到它的 client_id。App 打开登录页和换令牌时带上。

POSTClient Register

在测试台点本步的 Client Register 标签,按下面的值填表(id 用前面步骤返回的),点 Send request。

填表的值:

{
  "request_ref": "register-bim-app",
  "client_id": "bim-app",
  "product_id": "b41d9a70-6c2e-4a0f-8e7d-52a9c3f0d614",
  "deployment_version": "v1",
  "display_name": "BIM app",
  "kind": "public_native",
  "auth_method": "none",
  "redirect_uris": [
    "com.example.bim://callback"
  ],
  "scopes": [
    "openid",
    "profile"
  ],
  "dpop_required": true,
  "token_profile": "native"
}

返回:

{
  "client_id": "bim-app",
  "display_name": "BIM app",
  "kind": "public_native",
  "auth_method": "none",
  "redirect_uris": [
    "com.example.bim://callback"
  ],
  "post_logout_redirect_uris": [],
  "backchannel_logout_uri": null,
  "scopes": [
    "openid",
    "profile"
  ],
  "dpop_required": true,
  "token_profile": "native",
  "service_account_id": null,
  "status": "pending",
  "version": 0,
  "product_id": "b41d9a70-6c2e-4a0f-8e7d-52a9c3f0d614",
  "public_key_jwk": null,
  "deployment_version": "v1",
  "client_secret": null
}
  • 和产品(product)的网页登录客户端分开登记。
  • kind: public_native、auth_method: none、dpop_required: true、token_profile: native,没有密钥。
  • redirect_uris 写 App 的回调,例如自定义 scheme com.example.bim://callback。

错误:

HTTP 错误码 处理
409 CLIENT_ID_CONFLICT 这个 client_id 已被占用,换一个。
1.2

启用 App 的登录客户端

在哪里做:测试台

把 bim-app 改成 active,得到可用的登录客户端。App 从此能登录。

POSTClient Set Status

在测试台点本步的 Client Set Status 标签,按下面的值填表(id 用前面步骤返回的),点 Send request。

填表的值:

{
  "client_id": "bim-app",
  "request_ref": "activate-bim-app",
  "product_id": "b41d9a70-6c2e-4a0f-8e7d-52a9c3f0d614",
  "status": "active"
}

返回:

{
  "client_id": "bim-app",
  "display_name": "BIM app",
  "kind": "public_native",
  "auth_method": "none",
  "redirect_uris": [
    "com.example.bim://callback"
  ],
  "post_logout_redirect_uris": [],
  "backchannel_logout_uri": null,
  "scopes": [
    "openid",
    "profile"
  ],
  "dpop_required": true,
  "token_profile": "native",
  "service_account_id": null,
  "status": "active",
  "version": 1,
  "product_id": "b41d9a70-6c2e-4a0f-8e7d-52a9c3f0d614",
  "public_key_jwk": null,
  "deployment_version": "v1"
}
  • App 会话最长 30 天,7 天不用就结束。

做完这一部分,手里有:App 的 client_id(bim-app),状态 active。App 登录时用它,没有密钥。

2. 登录
2.1

生成设备密钥

在哪里做:App 代码

App 首次启动时在设备上生成一对密钥,得到 dpop_jkt。登录时带上,之后每个请求都用这把密钥签名。

  • 首次启动时在设备的安全硬件(Android Keystore / iOS Secure Enclave)生成 EC P-256 密钥对,整个安装期间保留。
  • 公钥的 JWK 指纹(RFC 7638,base64url,43 个字符)就是 dpop_jkt。
2.2

打开登录页

在哪里做:App 代码

App 用系统浏览器打开 BES 登录页。用户登录完,App 的回调收到 code,下一步用它换令牌。

GETAuthorize (sign-in page)

用系统浏览器(Custom Tabs / ASWebAuthenticationSession)打开:

GET https://id.test.globalbes.sg/authorize?response_type=code&client_id=bim-app&redirect_uri=com.example.bim%3A%2F%2Fcallback&scope=openid%20profile&state=<state>&nonce=<nonce>&code_challenge=<code_challenge>&code_challenge_method=S256&dpop_jkt=<dpop_jkt>
  • state、nonce、code_challenge(只支持 S256)和 dpop_jkt 都必填。
  • code_challenge = BASE64URL(SHA256(code_verifier))。
2.3

用 code 换令牌

在哪里做:App 代码

带 DPoP 证明用 code 换令牌,得到绑定设备密钥的 access_token 和 refresh_token。存在设备上,选租户和调接口时用。

POSTToken

不带登录客户端认证,带 DPoP 证明:

POST https://id.test.globalbes.sg/oauth/token
Content-Type: application/x-www-form-urlencoded
DPoP: <dpop_proof>

grant_type=authorization_code&client_id=bim-app&code=<code>&code_verifier=<code_verifier>&redirect_uri=com.example.bim%3A%2F%2Fcallback

DPoP 证明是用设备密钥签名的 JWT:

  • header:typ 为 dpop+jwt,alg 为 ES256,jwk 为公钥(kty EC、crv P-256、x、y)。
  • htm:本次请求的 HTTP 方法,例如 POST。
  • htu:本次请求的完整地址,不含查询串,用 https://id.test.globalbes.sg 拼,例如 https://id.test.globalbes.sg/oauth/token。
  • iat:当前时间(秒),接受 60 秒前到 5 秒后。
  • jti:每个请求一个新的随机值,重复的被拒;重试也要新证明。
  • ath:带 access_token 的请求要有,值是这个令牌的 SHA-256,base64url。
  • 之后带令牌的请求用 Authorization: DPoP <access_token> 加新的证明。
2.4

刷新令牌

在哪里做:App 代码

access_token 过期前带 DPoP 证明换一对新令牌。存下新的 refresh_token,下次刷新用。

POSTToken
POST https://id.test.globalbes.sg/oauth/token
Content-Type: application/x-www-form-urlencoded
DPoP: <dpop_proof>

grant_type=refresh_token&client_id=bim-app&refresh_token=<refresh_token>
  • 证明用同一把设备密钥签名。
  • 每个 refresh_token 只能用一次,存下新的那个。

做完这一部分,手里有:设备密钥、绑定它的 access_token 和 refresh_token。之后每个请求都用它们。

3. 选租户
3.1

列出可进的租户

在哪里做:App 代码

带令牌列出此人在本产品能进的租户和项目,得到租户列表。App 用它显示自己的选择页。

GETSession Tenant List

程序里发这个请求(id 用前面步骤返回的):

带:用户的 access_token

请求:

GET https://id.test.globalbes.sg/api/sessions/tenants
Authorization: DPoP <access_token>
DPoP: <dpop_proof>

返回:

HTTP/1.1 200 OK

{
  "tenants": [
    {
      "tenant_id": "7c0e2a5e-3b1f-4d6a-9a53-1f2f6d0c8a11",
      "tenant_code": "topaz",
      "display_name": "Topaz Construction",
      "projects": [
        {
          "project_id": "e2f7a1c4-0d35-4b8e-a6c9-7d18f4b2a903",
          "display_name": "Harbour Tower"
        }
      ],
      "mfa_required": false
    }
  ]
}
  • 每个请求带 Authorization: DPoP <access_token> 和新的 DPoP 证明,证明的 htm、htu 对应本次请求,ath 对应这个令牌。
  • App 登录客户端(public_native)必须提供 DPoP;机密 Web 客户端由服务端持有令牌,不要求 DPoP。
  • 项目级产品每个租户列出可进的项目;租户级产品 projects 为空。
  • mfa_required: true 的租户要比密码更强的登录。
3.2

选定租户

在哪里做:App 代码

用户选好后,App 发选定的租户,得到 context_id。调产品接口时带上,产品转给 session.validate。

POSTSession Tenant Select

程序里发这个请求(id 用前面步骤返回的):

带:用户的 access_token

请求:

POST https://id.test.globalbes.sg/api/sessions/tenants/select
Authorization: DPoP <access_token>
DPoP: <dpop_proof>
Content-Type: application/json

{
  "tenant_id": "7c0e2a5e-3b1f-4d6a-9a53-1f2f6d0c8a11",
  "project_id": "e2f7a1c4-0d35-4b8e-a6c9-7d18f4b2a903"
}

返回:

HTTP/1.1 200 OK

{
  "context_id": "0f6b8d2e-4a17-4c93-9e5b-a1d27c4f3b68",
  "tenant_id": "7c0e2a5e-3b1f-4d6a-9a53-1f2f6d0c8a11",
  "project_id": "e2f7a1c4-0d35-4b8e-a6c9-7d18f4b2a903",
  "expires_at": "2026-01-01T20:00:00+00:00"
}
  • context_id 存在会话里。
  • 同一租户再选一次返回同一个 context_id。
  • expires_at 是上限,每次使用仍会检查。

做完这一部分,手里有:选定租户的 context_id,App 调产品接口时带上。

4. 登出
4.1

结束登录

在哪里做:App 代码

用户退出时,App 带令牌结束在 Control 里的会话,令牌失效。然后删掉设备上的令牌。

POSTSession Logout

程序里发这个请求(id 用前面步骤返回的):

带:用户的 access_token

请求:

POST https://id.test.globalbes.sg/api/sessions/logout
Authorization: DPoP <access_token>
DPoP: <dpop_proof>
Content-Type: application/json

{
  "login_scope": "client"
}

返回:

HTTP/1.1 204 No Content
  • login_scope: client 只结束 App 的会话;product 结束此人在本产品的全部会话。
  • 然后删掉设备上的令牌;设备密钥保留,下次登录还用。
4.2

关闭租户上下文

在哪里做:App 代码

切换租户时关掉旧的 context_id,不登出。之后用新选的 context_id。

DELETESession Context Revoke

程序里发这个请求(id 用前面步骤返回的):

带:用户的 access_token

请求:

DELETE https://id.test.globalbes.sg/api/sessions/contexts/0f6b8d2e-4a17-4c93-9e5b-a1d27c4f3b68
Authorization: DPoP <access_token>
DPoP: <dpop_proof>

返回:

HTTP/1.1 204 No Content

做完这一部分,手里有:App 的会话已结束,设备上的令牌删掉,设备密钥保留。