Skip to content

CIBA 后端通道认证 ​

CIBA(Client Initiated Backchannel Authentication)用于“发起方拿不到用户浏览器”的场景:例如柜台系统要为客户办理业务、呼叫中心要确认来电者身份。客户端在后端调用 wicket,wicket 记录一条待确认请求,宿主应用负责通知用户并记录用户的确认结果,客户端轮询令牌端点拿到令牌。

本页所有请求与响应均为 v0.1.4 上实际运行的结果,Go 片段均已编译并在实验程序中跑通。

支持范围 ​

项目v0.1.4 状态
端点 POST /connect/ciba已注册
令牌投递模式仅 poll(发现文档 backchannel_token_delivery_modes_supported: ["poll"])
ping / push不支持(ping 有通知代码但不在允许列表中且未经验证;push 未实现)
用户确认没有内置的交互服务,由宿主直接更新请求存储(见下文)
id_token不签发:即使请求含 openid,令牌响应中 id_token 也为空字符串
刷新令牌客户端 AllowOfflineAccess 且请求含 offline_access 时签发

1. 配置客户端 ​

go
ciba := models.NewClient()
ciba.ClientId = "ciba-client"
secret := models.NewSecret()
secret.Value = utils.Sha256("ciba-secret")
ciba.ClientSecrets = []*models.Secret{secret}
ciba.AllowedScopes = []string{"openid", "orders.read"}
ciba.AllowOfflineAccess = true // 可选
if err := ciba.SetAllowedGrantTypes(wicket.GrantTypesBackchannelAuthentication); err != nil {
	log.Fatal(err)
}
// 可选:覆盖全局默认值
// interval := 10; ciba.PollingInterval = &interval
// lifetime := 120; ciba.BackchannelAuthenticationLifetime = &lifetime

BackchannelTokenDeliveryMode 留空即为 poll。全局默认值在 WicketOptions.BackchannelAuthentication 中:

选项默认值说明
AllowedDeliveryModes["poll"]允许的投递模式
DefaultRequestLifetime300 秒请求有效期;客户端的 requested_expiry 只能缩短它
DefaultPollingInterval5 秒返回给客户端的 interval
SlowDownIntervalIncrement5 秒每次触发 slow_down 后该请求的轮询间隔增加量

BackchannelAuthentication.Enabled 在 v0.1.4 中没有任何代码读取,不能用它关闭 CIBA。

2. 替换默认的用户校验器 ​

默认校验器不能用于生产

未注入时 wicket 使用的默认 BackchannelAuthenticationUserValidator 仅供开发联调:它不会到宿主用户库中解析或核验用户提示。生产环境请务必注入自己的 BackchannelAuthenticationUserValidator,只接受能解析为真实用户的提示;id_token_hint、login_hint_token 须先验签再使用。

注入自己的实现,把提示解析为宿主用户库中的真实用户:

go
// userValidator 把 login_hint 解析为宿主用户库中的真实用户。
type userValidator struct {
	lookup func(ctx context.Context, hint string) (subjectID string, ok bool)
}

func (v *userValidator) Validate(ctx context.Context, c *validators_models.BackchannelAuthenticationUserValidationContext) error {
	if c.LoginHint == "" {
		// 本例不接受 id_token_hint / login_hint_token;要支持须先验签再取 sub
		return errors.New("login_hint required")
	}
	sub, ok := v.lookup(ctx, c.LoginHint)
	if !ok {
		return errors.New("unknown user")
	}
	p, err := wicketmodels.NewLocalUser(wicketmodels.WithLocalUserSubjectId(sub)).CreatePrincipal()
	if err != nil {
		return err
	}
	c.Subject = p
	return nil
}

cfg.SetBackchannelAuthenticationUserValidator(&userValidator{lookup: findUserByHint})

Validate 返回任意错误时,端点统一回 invalid_request(实测对未知用户返回 {"error":"invalid_request","error_description":""})。

以 Subject 为准

v0.1.4 中入库记录的 SubjectId 字段不一定等于用户的 sub,请不要依赖它。Subject 才是校验器解析出的用户主体,令牌中的 sub 取自 Subject;宿主通知用户时请从 Subject 读取 sub。

3. 发起请求 ​

bash
curl -s -u ciba-client:ciba-secret \
  -d "scope=openid orders.read" \
  -d login_hint=alice@example.com \
  -d "binding_message=Pay 42" \
  http://localhost:5000/connect/ciba
json
{"auth_req_id":"zUzdxlNeR9ql20ix3rJQ8W29kEw","expires_in":300,"interval":5}
参数说明
scope必需;缺失时返回 invalid_scope
login_hint / login_hint_token / id_token_hint至少一个;全部缺失返回 invalid_request
binding_message展示给用户、用于核对的短文本
user_code用户预先设置的口令,原样保存,是否校验由宿主决定
requested_expiry秒;只能比默认值短
resource资源指示器,可多个

客户端认证与令牌端点相同(client_secret_basic / client_secret_post)。未被允许 CIBA 授权类型的客户端返回 unauthorized_client。注意此端点的错误体没有令牌端点那样的 custom 字段。

4. 通知用户:包装请求存储 ​

wicket 在请求校验通过后调用 BackchannelAuthenticationRequestStore.StoreBackchannelAuthenticationRequest 入库,此外不会通知任何人。最直接的接入点是包装这个存储,在入库成功后推送给用户:

go
type notifyingRequests struct {
	storage.BackchannelAuthenticationRequestStore
	notify func(req *models.BackchannelAuthenticationRequest)
}

func (s *notifyingRequests) StoreBackchannelAuthenticationRequest(ctx context.Context, id string, req *models.BackchannelAuthenticationRequest) error {
	if err := s.BackchannelAuthenticationRequestStore.StoreBackchannelAuthenticationRequest(ctx, id, req); err != nil {
		return err
	}
	s.notify(req) // req.RequestId、req.Subject、req.BindingMessage、req.RequestedScopes 可用
	return nil
}

requests := &notifyingRequests{
	BackchannelAuthenticationRequestStore: myStore, // 开发时可用 memory.NewMemoryBackchannelAuthenticationRequestStore()
	notify:                                pushToUserDevice,
}
cfg.SetBackchannelAuthenticationRequestStore(requests)

notify 在请求处理路径上同步执行,耗时操作请投递到宿主自己的队列。

5. 记录用户的确认结果 ​

用户在 App 或站内页面上点击“同意 / 拒绝”后,宿主直接修改请求状态:

go
func decide(ctx context.Context, store storage.BackchannelAuthenticationRequestStore, requestID string, approve bool) error {
	req, err := store.FindBackchannelAuthenticationRequest(ctx, requestID)
	if err != nil {
		return err
	}
	if req.Status != models.BackchannelAuthenticationRequestStatusPending {
		return errors.New("request is no longer pending")
	}
	if approve {
		req.Status = models.BackchannelAuthenticationRequestStatusApproved
	} else {
		req.Status = models.BackchannelAuthenticationRequestStatusDenied
	}
	return store.UpdateBackchannelAuthenticationRequest(ctx, requestID, req)
}

确认页必须先验证“当前登录的用户就是 req.Subject 对应的用户”,再调用 decide。状态取值为 pending、approved、denied、consumed;令牌签发后 wicket 会把状态改为 consumed。

6. 客户端轮询 ​

bash
curl -s -u ciba-client:ciba-secret \
  -d grant_type=urn:openid:params:grant-type:ciba \
  -d auth_req_id=$AUTH_REQ_ID \
  http://localhost:5000/connect/token

各阶段的响应(“属于其他客户端”一项依据源码,其余均为实测):

时机响应
用户尚未确认{"custom":{},"error":"authorization_pending","error_description":""}
距上次轮询不足 interval 秒slow_down(该请求的间隔随之增加 SlowDownIntervalIncrement)
用户已确认200,含 access_token(sub 为确认的用户),id_token 为空字符串
用户已拒绝access_denied
超过有效期expired_token
已兑换过 / auth_req_id 属于其他客户端 / 不存在invalid_grant

客户端应遵守返回的 interval,收到 slow_down 后把间隔加大 5 秒再试。

相关阅读 ​

以 Apache License 2.0 发布 · 文档内容 © 2026 Wicket 作者