跳转至

通过自定义界面实现单点登录延续

当终端用户的浏览器中已经存在有效的 Authany 会话时,您的自定义界面可以显示“以 user@example.com 身份继续”选项,而不必要求用户重新输入凭据。这一能力由 select_account 提供,它是一种可添加到 login 和 signup_login 流程的 identify 步骤中的身份识别选项。

select_account 通过您的自定义界面已经在调用的 Authentication Flow API 端点工作,没有新增端点,也没有新增令牌。当浏览器存在符合条件的会话时,identify 步骤的响应会包含一个描述该账号的额外选项;如果没有符合条件的会话,响应保持不变,因此即使自定义界面不识别该选项,也能像之前一样正常工作。

如果您是初次构建自定义界面,请先阅读 Authentication Flow API 概述。

使用场景

同一项目内多个应用间的单点登录。 应用 A 和应用 B 是同一个 Authany 项目下的两个 OAuth 客户端,并共用同一个自定义界面。用户登录应用 A 后,首次在同一浏览器中打开应用 B。由于 Authany 会话 Cookie 是共享的,自定义界面可以提供以同一账号继续的选项。用户只需点击一次,而无需再次输入凭据。

单一的“继续”入口。 您的自定义界面使用一个合并页面(signup_login 流程),而不是分开的登录页和注册页。当会话已经存在时,该页面会在常规的邮箱和社交登录选项旁显示“以 user@example.com 身份继续”。如果用户想拒绝,只需选择其他选项,甚至可以借此注册第二个账号。

前置条件

  • 您的自定义界面必须与 Authany 端点同站(same-site)。 只有当您的自定义界面与 Authany 端点共享同一个可注册域名(即 eTLD+1,例如 example.com,比如 auth.example.com 和 ui.example.com)时,浏览器才会将 Authany 会话 Cookie 发送给 Authentication Flow API。实际操作中,这意味着需要为您的项目设置自定义域名。托管在无关域名上的自定义界面永远不会看到 select_account 选项。
  • 已配置自定义界面 URI。 您的 OAuth 客户端必须在 Authany 管理台中设置自定义界面 URI(应用 > 您的应用 > 自定义界面)。请注意,同站(same-site)不等同于同源(same-origin):ui.example.com 和 auth.example.com 仍然是跨源的,因此浏览器会对这些 API 调用应用 CORS。Authany 允许来自已注册为自定义界面 URI 的源发起带凭据的跨源请求,因此您无需额外配置 CORS。
  • 从浏览器发送会话 Cookie。 使用 fetch() 调用 Authentication Flow API 时必须设置 credentials: 'include',否则不会发送会话 Cookie,该选项也永远不会出现。
flowchart TD
    appa["App A"] --> ui
    appb["App B"] --> ui
    subgraph samesite ["example.com — same registrable domain"]
        ui["Shared Custom UI<br/>ui.example.com"]
        authgear["Authgear<br/>auth.example.com"]
    end
    other["Custom UI on an unrelated domain<br/>ui.other-domain.io"]

    ui -- "session cookie sent,<br/>select_account offered" --> authgear
    other -. "cookie never sent,<br/>option never appears" .-> authgear

说明

如果您的自定义界面有各自独立的登录和注册页面,其首次创建流程的调用类型必须是 login 或 signup_login,绝不能直接是 signup。select_account 只存在于这两种流程类型中,因此如果一开始就使用 signup,您将永远无法得知是否存在会话。只有当用户没有符合条件的会话(或拒绝了该会话)并且想要注册新账号时,才需要之后再单独创建 signup 流程。

工作原理

select-account 的交互过程内嵌在每个自定义界面已经遵循的同一套 OAuth 流程之中。唯一新增的部分是 identify 响应中的额外选项,以及用于完成该选项的一键式输入:

sequenceDiagram
    participant App as Your App
    participant CustomUI as Custom UI (ui.example.com)
    participant Authgear as Authgear (auth.example.com)

    Note over App,Authgear: The browser already holds an Authgear session cookie
    App->>Authgear: GET /oauth2/authorize?client_id=...
    Authgear-->>CustomUI: 302 to Custom UI URI (client_id, x_ref, ...)
    CustomUI->>Authgear: POST /api/v1/authentication_flows<br/>fetch with credentials: 'include' — cookie sent (same-site)
    Authgear-->>CustomUI: identify options include select_account<br/>(display_name, user_id)
    Note over CustomUI: Render "Continue as user@example.com"
    CustomUI->>Authgear: POST /api/v1/authentication_flows/states/input<br/>{ "identification": "select_account", "index": 0 }
    Authgear-->>CustomUI: action.type: finished (finish_redirect_uri)
    CustomUI->>Authgear: Top-level navigation to finish_redirect_uri
    Authgear-->>App: 302 to redirect_uri with authorization code
    App->>Authgear: POST /oauth2/token (exchange code for tokens)

用户全程无需输入任何凭据,唯一的操作就是点击“以 user@example.com 身份继续”。如果没有符合条件的会话,创建流程的响应中不会包含 select_account 条目,您的界面会按照普通登录流程继续。

步骤 1:将 select_account 添加到您的流程配置中

在 Authany 管理台中,进入高级 > 编辑配置,在 authentication_flow 部分下,将 select_account 添加到登录流程的 identify 步骤中:

authentication_flow:
  login_flows:
  - name: default
    steps:
    - type: identify
      one_of:
      - identification: select_account
      - identification: email
        steps:
        - type: authenticate
          one_of:
          - authentication: primary_password

上面的 select_account 条目没有嵌套的 steps,因此选择它会立即完成登录。如果想先要求提供额外信息(例如 2FA 验证码),可以为该条目添加一个嵌套的 authenticate 步骤。参见下文的在延续登录时要求 2FA。

步骤 2:在自定义界面中检测该选项

像往常一样创建流程,并通过 url_query 转发 Authany 传递给您自定义界面 URI 的查询参数(尤其是 client_id 和 x_ref):

const response = await fetch("https://auth.example.com/api/v1/authentication_flows", {
  method: "POST",
  credentials: "include", // required: sends the Authgear session cookie
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    type: "login",
    name: "default",
    url_query: window.location.search.substring(1),
  }),
});

当浏览器存在符合条件的会话时,identify 步骤的 options 中会包含一个 select_account 条目,其中带有该账号的显示名称和用户 ID:

{
    "result": {
        "state_token": "authflowstate_VGHZ8SBCKGZK2KW84TCAKWGM8QZH0B69",
        "type": "login",
        "name": "default",
        "action": {
            "type": "identify",
            "data": {
                "type": "identification_data",
                "options": [
                    {
                        "identification": "select_account",
                        "display_name": "user@example.com",
                        "user_id": "user_01J8ZC0M2N3P4Q5R6S7T8V9W0X"
                    },
                    {
                        "identification": "email"
                    }
                ]
            }
        }
    }
}

使用 display_name 来渲染“以 user@example.com 身份继续”按钮。与本 API 其他地方的 masked_display_name 不同,该字段不会被脱敏返回,因为它标识的是已经绑定在调用方自身会话 Cookie 上的账号。

当没有符合条件的会话时,select_account 条目不会出现,响应的其余部分保持不变。

步骤 3:提交所选项

当用户点击“以…身份继续”时,通过该选项的 index(即它在 options 数组中的位置)提交该选项:

{
    "state_token": "authflowstate_VGHZ8SBCKGZK2KW84TCAKWGM8QZH0B69",
    "input": {
        "identification": "select_account",
        "index": 0
    }
}

使用步骤 1 中的最小配置,该流程会立即完成:

{
    "result": {
        "state_token": "authflowstate_ABCJVB0IJKLQ2S1K2G34RX56R1C1E789",
        "type": "login",
        "name": "default",
        "action": {
            "type": "finished",
            "data": {
                "finish_redirect_uri": "https://auth.example.com/oauth2/consent?..."
            }
        }
    }
}

通过顶层导航(而不是 fetch() 调用)将浏览器重定向到 finish_redirect_uri,方式与其他已完成的流程完全相同。

说明

user_id 仅用于展示,是只读的。输入内容始终只携带 index;服务器会在提交时根据自身的会话 Cookie 重新解析账号,因此伪造或过期的 user_id 永远无法用来选择其他账号。

步骤 4:处理用户拒绝的情况

没有专门的“拒绝”输入项。如果用户想以其他身份登录,可以改为提交其他任意选项的输入,例如:

{
    "state_token": "authflowstate_VGHZ8SBCKGZK2KW84TCAKWGM8QZH0B69",
    "input": {
        "identification": "email",
        "login_id": "another-user@example.com"
    }
}

之后该流程会完全按照普通登录继续进行。

在 signup_login 流程中使用 select_account

在 signup_login 流程中,select_account 只声明一个 login_flow;它只能延续已有的登录,永远不能用于注册。选择它会切换到指定的登录流程,并在其中重放同一个 identify 输入,因此该登录流程自身也必须声明一个 select_account 条目。如果没有,重放的输入将无法匹配目标流程中的任何选项,提交会失败而不是完成登录:

authentication_flow:
  signup_login_flows:
  - name: default
    steps:
    - type: identify
      one_of:
      - identification: select_account
        login_flow: default_login
      - identification: email
        signup_flow: default_signup
        login_flow: default_login

  login_flows:
  - name: default_login
    steps:
    - type: identify
      one_of:
      - identification: select_account
      - identification: email
        steps:
        - type: authenticate
          one_of:
          - authentication: primary_password

目标登录流程中在 identify 之后配置的任何步骤(例如 terminate_other_sessions)仍会执行;通过 select_account 延续登录不会跳过这些步骤。

在延续登录时要求 2FA

如果想在通过已有会话延续登录时要求提供新的第二因素,同时不为普通登录添加该步骤,可以为 select_account 条目添加一个专属的嵌套 authenticate 步骤:

1
2
3
4
5
6
      one_of:
      - identification: select_account
        steps:
        - type: authenticate
          one_of:
          - authentication: secondary_totp

此时提交 select_account 会返回一个 authenticate 操作,而不是直接完成:

{
    "result": {
        "state_token": "authflowstate_XYZ3JVB0IJKLQ2S1K2G34RX56R1C1E78",
        "type": "login",
        "name": "default",
        "action": {
            "type": "authenticate",
            "authentication": "secondary_totp",
            "data": {
                "type": "authentication_data",
                "options": [
                    { "authentication": "secondary_totp" }
                ]
            }
        }
    }
}

像往常一样提交 TOTP 验证码即可完成该流程。

强制使用 login_hint 或 id_token_hint

如果您的应用程序在授权请求中传递了 login_hint 或 id_token_hint,Authany 会在重定向到您自定义界面 URI 时,将这两个参数与 client_id 和 x_ref 一并转发。

Authentication Flow API 特意不会根据这些提示(hint)过滤 select_account 选项;只要存在符合条件的会话,该选项就会被提供。如果您希望仅在匹配提示时才提供延续登录,需要在自定义界面中自行实现该检查:自行解析提示、将其与该选项的 user_id 进行比对,若不匹配则隐藏该选项,并回退到没有符合条件会话时界面的处理方式。对于 id_token_hint,这意味着需要解码 ID 令牌,并将其 sub 声明与该选项的 user_id 进行比对。如果自定义界面不关心这些提示,可以完全忽略 user_id。

错误处理

如果在选项显示之后、输入提交之前会话发生了变化(例如用户在另一个标签页中登出或切换了账号),API 会返回:

{
    "error": {
        "name": "Unauthorized",
        "reason": "SelectAccountSessionChanged",
        "message": "session no longer matches the selected account",
        "code": 401
    }
}

处理方式是重新启动流程:创建一个新的身份验证流程,并渲染最新响应中包含的选项。

该选项何时不会出现

在以下任一情况成立时,select_account 选项都不会出现,响应会与未配置该功能时相同:

  • 不存在有效会话,或未发送会话 Cookie(缺少 credentials: 'include',或自定义界面与 Authany 不同站)。
  • 流程配置的 identify 步骤中未列出 select_account。
  • 授权请求携带了 prompt=login,或其 max_age 已过期(Authany 会将其视为 prompt=login)。
  • 该会话是在禁用 SSO 的情况下建立的——例如 SDK 配置了 isSSOEnabled: false,这会抑制共享会话 Cookie(参见移动应用 / 网站的 SSO)。

注意

授权请求中的 prompt=select_account 与本功能无关,尽管名称相同,但不会产生任何效果。