Koee DocsKoee 文档

Sign in with Koee

Identity-only sign-in for websites and native applications. The protocol returns a short-lived, signed ID token. It does not issue access tokens, refresh tokens, client secrets for public apps, or permissions to read messages and contacts. It is not a full OAuth/OIDC provider; use this guide and the supplied adapters instead of assuming compatibility with an arbitrary OIDC discovery client.

For independent business clients that provide their own registration and login page, read Koee Open API. That administrator-provisioned integration is separate from this identity-only third-party sign-in protocol.

Availability

Deployment, signing keys and the service feature gate must be enabled before integrations can run. Creating a draft does not grant public access. Only its owner can test a draft; public sign-in requires administrative approval.

Accounts, clients and keys belong to the selected product. Never exchange a Koee code with another product's service.

Register an application

Provide an application name (60 characters), description (500), HTTPS icon URL, HTTPS website URL, HTTPS privacy-policy URL and contact email. Contact email is visible to reviewers, not to users or other applications.

An account can create 5 applications. Each application supports one client for each of Web, iOS, Android and macOS. Platform clients have different client_id values but share the application's stable account identity. Different applications receive different account identifiers.

PlatformRequired configuration
WebUp to 5 exact HTTPS Origins; optional exact full-page redirect URLs
iOS / macOSBundle ID, Apple Team ID; generated callback Scheme and optional registered callbacks
AndroidPackage name, signing-certificate SHA-256 fingerprint; generated callback Scheme and optional registered callbacks

Each client supports up to 5 exact callback URLs. No wildcards, userinfo, fixed query parameters or fragments. Web redirects must belong to registered Origins. An Origin contains scheme, host and optional port, for example https://example.com; it does not contain a path. Localhost and IP hosts are not accepted. Use a controlled HTTPS development domain for testing.

For each configured Web / HTTPS callback host, add the TXT record displayed in the console, then click Verify DNS. Example:

Name: _koee-signin.example.com
Type: TXT
Value: copy the complete generated value from the console

Proofs are valid for 30 days when testing drafts or submitting for review. An approved snapshot remains published until replaced or suspended; proof expiry alone does not silently disable a reviewed production integration.

Saving changes updates the draft only. Submit the draft for review to publish changes to an approved application. Pending/rejected edits do not change the currently approved configuration. Sensitive edits and submission require a real sign-in within 10 minutes; ordinary SSO and QR login do not reset that timer.

Web: popup sign-in

Register your website Origin. No callback backend or redirect URL is needed for popup mode. Copy the SDK into your own build (recommended), or serve it from the published SDK URL with the appropriate CORS policy. The source package is packages/identity-signin-web; it has no runtime dependencies.

import { createSignIn } from './sign-in-v1.mjs';

const identity = createSignIn({
  product: 'koee',
  clientId: 'COPY_WEB_CLIENT_ID',
});

// Call directly in a click handler; do not await anything before opening it.
document.querySelector('#sign-in').addEventListener('click', async () => {
  try {
    const { id_token, claims } = await identity.signInWithPopup();
    // Signature, issuer, audience, nonce and expiry have already been checked.
    showSignedInUser({ issuer: claims.iss, subject: claims.sub });
  } catch (error) {
    if (error.code !== 'cancelled' && error.code !== 'access_denied') {
      showSignInError(error.code);
    }
  }
});

The SDK retains PKCE/state/nonce in memory and checks message Origin, source window and state. The authorization window returns only code / error and state in a type: "identity.signin.result" message to the exact registered Origin. A blocked popup reports popup_blocked; do not automatically open a second authorization channel after a user refuses or cancels.

Do not apply a COOP policy that severs the popup's opener while expecting popup messages. Test deployed headers, Safari and popup blockers. If your site needs strong opener isolation, offer full-page redirect mode instead.

Web: optional full-page redirect

Register the complete callback URL, then:

// Initiating page:
await identity.startRedirect({ redirectUri: 'https://example.com/sign-in/return' });

// Callback page, before rendering any third-party analytics or remote content:
const { id_token, claims } = await identity.completeRedirect();

The SDK stores only short-lived transaction data in same-tab sessionStorage, consumes it once, removes code/state from the address bar, exchanges the code and verifies the ID token. It does not store an official messenger session or share cookies across domains. Callback pages should use Referrer-Policy: no-referrer.

Native applications

The Flutter adapter is in packages/identity-signin-flutter. It supports iOS and Android official-App authorization, with system-browser fallback after a confirmed launch failure. macOS uses the system authentication browser. The adapter's Apple browser path currently uses the generated custom Scheme; platform-native integrations can also use registered verified HTTPS callbacks.

final signIn = NiximSignIn(
  product: IdentityProduct.koee,
  clientId: 'COPY_PLATFORM_CLIENT_ID',
  callbackUri: Uri.parse('COPY_GENERATED_CALLBACK_URL'),
);
final result = await signIn.signIn();
// result.claims has been verified; result.subject is app-specific.
// Explicit browser retry, if the user chooses it:
// await signIn.signIn(preferOfficialApp: false);

Register the generated Scheme in the receiving app. For iOS use CFBundleURLTypes / CFBundleURLSchemes in Info.plist. For Android register a VIEW + DEFAULT + BROWSABLE intent filter on your singleTop activity, restricted to your exact scheme and callback path; enable app_links handling and disable Flutter's competing automatic deep-link handler. For macOS register the Scheme in Info.plist and enable outgoing network access. No Apple Developer / Google Cloud entry is required merely to declare a custom Scheme. Verified HTTPS links need the platform's domain association files and entitlements instead.

The SDK directly launches the fixed official Android package or an iOS Universal Link with universalLinksOnly. It performs no installed-package query and adds no <queries>, QUERY_ALL_PACKAGES or LSApplicationQueriesSchemes declarations. Android browser fallback uses Custom Tabs; Apple uses ASWebAuthenticationSession. Use a cancel button wired to signIn.cancel() while waiting. Android browser closure does not reliably report cancellation; explicit cancellation or the bounded timeout ends that attempt. Process death discards the in-memory verifier; start again rather than accepting an unbound callback.

Custom Schemes are not exclusive ownership proofs. PKCE prevents a different app that intercepts the code from redeeming it without the verifier. Platform metadata and the public client ID are not App Attest / Play Integrity evidence.

Native protocol

All secrets below are independent random 32-byte values, encoded as unpadded base64url (43 characters). Generate them with a cryptographically secure source. code_challenge is base64url(SHA-256(verifier)); only S256 is supported.

POST https://auth.koee.app/identity/native/requests
Content-Type: application/json

{
  "client_id": "COPY_PLATFORM_CLIENT_ID",
  "response_type": "code",
  "scope": "openid",
  "response_mode": "query",
  "destination": "COPY_REGISTERED_CALLBACK_URL",
  "state": "RANDOM_43_CHARACTER_VALUE",
  "nonce": "ANOTHER_RANDOM_43_CHARACTER_VALUE",
  "code_challenge": "SHA256_VERIFIER_BASE64URL",
  "code_challenge_method": "S256"
}

Response: {id, launch_ticket, requester_secret, expires_at}. Launch:

https://auth.koee.app/identity/open-app#request_id=ID&launch_ticket=TICKET

Keep requester_secret and verifier inside the requesting app. Never put them in links, analytics, logs or QR codes. The native launch ticket is valid for 120 seconds. Only the signed-in official phone can read the application details and approve or refuse this request. After approval the code gets a full 60-second redemption window. Status polling never returns the code or user identity.

POST /identity/native/requests/{id}/status and /cancel accept JSON {requester_secret}. Do not poll faster than 2 seconds. These endpoints do not accept a browser Origin. A browser authorization must be created through the authorization center, not by obtaining native approval secrets in JavaScript.

If the official App cannot be launched, cancel the native request first and create a new browser authorization with fresh state, nonce and verifier. Never fall back after refusal, cancellation or timeout. The native fallback landing page does not approve the native ticket in a browser.

Browser authorization parameters

Open https://auth.koee.app/identity/authorize with these query parameters:

ParameterValue
client_idRegistered platform client ID
response_typecode
scopeopenid or omitted
response_modeweb_message for Web popup; query for registered redirect
destinationExact registered Origin (popup) or full callback (redirect)
stateRandom per-attempt 43-character base64url value
nonceIndependent random per-attempt 43-character base64url value
code_challengeS256 challenge
code_challenge_methodS256

Browser requests last 10 minutes. Approval codes last at most 60 seconds, bounded by the request deadline. The user confirms every third-party authorization even when already signed in. Login/signup share the same entry; the service determines whether the account already exists. QR authentication first signs into the official center; it does not skip third-party confirmation.

Exchange the code

POST https://auth.koee.app/identity/token?client_id=COPY_PLATFORM_CLIENT_ID
Content-Type: application/json

{
  "grant_type": "authorization_code",
  "client_id": "COPY_PLATFORM_CLIENT_ID",
  "response_mode": "query",
  "destination": "COPY_REGISTERED_CALLBACK_URL",
  "code": "RECEIVED_CODE",
  "code_verifier": "ORIGINAL_VERIFIER"
}

Popup exchanges use response_mode: "web_message" and the original Origin as destination. The body and query client IDs must agree. Browser requests use JSON and exact-Origin CORS preflight; omit credentials. Native HTTP exchanges do not send a browser Origin. The code is atomically consumed once; concurrent redemption has one winner. If an exchange result is lost, start a new sign-in.

{ "id_token": "SIGNED_RS256_JWT", "id_token_expires_in": 300 }

Do not expect access_token, refresh_token or token_type.

Verify identity

Use the fixed product issuer's /identity/jwks.json; never follow a token's jku, x5u or arbitrary key URL. Accept only RS256 and the expected kid from these public keys. Verify the actual signature and all of:

  • iss exactly equals https://auth.koee.app.
  • aud exactly equals the initiating platform client ID.
  • nonce equals the initiating transaction's nonce.
  • sub is a nonempty, app-specific opaque identifier.
  • iat and exp are integer seconds; lifetime is at most 300 seconds.
  • Current time is within the valid interval, with at most 60 seconds clock skew.

Store (iss, sub) as the account key. Do not use a nickname, handle, email or phone number as the unique identity. The same application gets the same subject across its platform clients; different applications get different subjects. There is no userinfo endpoint in this version.

An application may have no backend or existing account system: it can use the verified identity directly for local personalization. If it has protected server data, its server must verify the ID token and bind the nonce to its own login attempt before creating its own session; client-side verification does not protect a server from forged requests.

Errors and revocation

access_denied / cancelled mean the user stopped the attempt. invalid_grant means the code/request is expired, consumed, changed or no longer authorized. rate_limited requires backoff. identity_unavailable means the service gate or signing configuration is unavailable. Do not silently retry one-use exchanges.

Users can revoke an application's grant in Settings → Privacy and security → Authorized applications. Revocation invalidates unconsumed codes and requires a new approval. It cannot instantly erase already-issued offline-verifiable ID tokens or terminate a third party's own sessions. Apps and administrators can disable an integration; new approvals and exchanges then fail.

Implementation checklist

  1. Create the application and configure the correct platform client.
  2. Verify domains; test only with the owning account until approved.
  3. Register the exact callback or Origin and keep verifier/state/nonce per attempt.
  4. Validate full callback, state and JWT; do not trust decoded JSON alone.
  5. Test refusal, app absence, popup blockers, duplicate callbacks, expiry, account switching, revoked grants and network loss.
  6. Submit for review. Changes to published configuration require a new review.

使用 Koee 登录

面向网站和原生应用的身份验证登录。协议返回短期有效、带签名的 ID token,不签发 access token、refresh token、公共应用的客户端密钥,也不授予读取消息或联系人的权限。它不是完整的 OAuth/OIDC 提供方;请使用本指南和配套适配器,不要假设可直接使用任意 OIDC discovery 客户端。

为独立业务客户端提供自己的注册和登录页面,请阅读 Koee Open API。该接入由管理员配置,与本章仅用于身份验证的第三方登录协议独立。

可用条件

接入前必须完成部署、配置签名密钥并开启服务功能开关。创建草稿不会获得公开访问权限。草稿仅供所有者测试;公开登录须经管理员批准。

  • 管理应用
  • 创建应用
  • 签发者/认证中心:https://auth.koee.app
  • 公钥:https://auth.koee.app/identity/jwks.json
  • Web SDK:https://koee.app/sdk/sign-in-v1.mjs

账号、客户端和密钥属于所选产品。不得将 Koee 的授权码提交给另一产品的服务兑换。

登记应用

提供应用名称(60 字符)、描述(500 字符)、HTTPS 图标地址、HTTPS 网站地址、HTTPS 隐私政策地址和联系邮箱。联系邮箱仅对审核人员可见,不向用户或其他应用公开。

一个账号可创建 5 个应用。每个应用可分别配置一个 Web、iOS、Android 和 macOS 客户端。各平台的 client_id 不同,但共享该应用内稳定的账号身份。不同应用获得不同的账号标识符。

平台必填配置
Web最多 5 个精确 HTTPS Origin;可选的精确整页跳转回调 URL
iOS / macOSBundle ID、Apple Team ID;生成的回调 Scheme 及可选的已登记回调
Android包名、签名证书 SHA-256 指纹;生成的回调 Scheme 及可选的已登记回调

每个客户端最多支持 5 个精确回调 URL。不允许通配符、用户信息、固定查询参数或片段。Web 跳转必须属于已登记的 Origin。Origin 包含协议、主机和可选端口,例如 https://example.com,不含路径。不接受 localhost 和 IP 主机。测试应使用由你控制的 HTTPS 开发域名。

对每个配置的 Web/HTTPS 回调主机,添加控制台显示的 TXT 记录,然后点击 Verify DNS(验证 DNS)。示例:

Name: _koee-signin.example.com
Type: TXT
Value: copy the complete generated value from the console

测试草稿或提交审核时,域名证明有效期为 30 天。已批准的配置快照会持续发布,直到被替换或暂停;证明过期本身不会悄悄停用已审核的线上接入。

保存修改只更新草稿。要发布已批准应用的变更,须提交草稿审核。待审或被拒绝的修改不影响当前已批准配置。敏感修改及提交要求最近 10 分钟内完成真实登录;普通 SSO 和二维码登录不会重置此计时。

Web:弹窗登录

登记网站 Origin 即可。弹窗模式不需要回调后端或跳转 URL。建议将 SDK 复制进自己的构建;也可在设置适当 CORS 策略后使用公开 SDK 地址。源码包为 packages/identity-signin-web,无运行时依赖。

import { createSignIn } from './sign-in-v1.mjs';

const identity = createSignIn({
  product: 'koee',
  clientId: 'COPY_WEB_CLIENT_ID',
});

// Call directly in a click handler; do not await anything before opening it.
document.querySelector('#sign-in').addEventListener('click', async () => {
  try {
    const { id_token, claims } = await identity.signInWithPopup();
    // Signature, issuer, audience, nonce and expiry have already been checked.
    showSignedInUser({ issuer: claims.iss, subject: claims.sub });
  } catch (error) {
    if (error.code !== 'cancelled' && error.code !== 'access_denied') {
      showSignInError(error.code);
    }
  }
});

SDK 将 PKCE/state/nonce 保存在内存中,并校验消息 Origin、来源窗口和 state。授权窗口仅向精确登记的 Origin 发送 type: "identity.signin.result" 消息,内容为 code/error 和 state。弹窗被拦截时返回 popup_blocked;用户拒绝或取消后,不要自动打开另一条授权通道。

需要接收弹窗消息时,不要使用会断开弹窗 opener 的 COOP 策略。验证部署后的响应头、Safari 和弹窗拦截行为。若网站需要严格隔离 opener,应提供整页跳转模式。

Web:可选整页跳转

登记完整回调 URL,然后:

// Initiating page:
await identity.startRedirect({ redirectUri: 'https://example.com/sign-in/return' });

// Callback page, before rendering any third-party analytics or remote content:
const { id_token, claims } = await identity.completeRedirect();

SDK 仅在同一标签页的 sessionStorage 中存储短期事务数据,消费一次后删除;它会从地址栏移除 code/state,兑换授权码并验证 ID token。SDK 不存储官方即时通信会话,也不跨域共享 Cookie。回调页应使用 Referrer-Policy: no-referrer。

原生应用

Flutter 适配器位于 packages/identity-signin-flutter。支持 iOS 和 Android 官方 App 授权,仅在确认启动失败后回退至系统浏览器;macOS 使用系统认证浏览器。适配器的 Apple 浏览器流程目前使用生成的自定义 Scheme;平台原生接入也可使用已登记、经过验证的 HTTPS 回调。

final signIn = NiximSignIn(
  product: IdentityProduct.koee,
  clientId: 'COPY_PLATFORM_CLIENT_ID',
  callbackUri: Uri.parse('COPY_GENERATED_CALLBACK_URL'),
);
final result = await signIn.signIn();
// result.claims has been verified; result.subject is app-specific.
// Explicit browser retry, if the user chooses it:
// await signIn.signIn(preferOfficialApp: false);

在接收回调的应用中登记生成的 Scheme。iOS 在 Info.plist 中配置 CFBundleURLTypes/CFBundleURLSchemes。Android 在 singleTop Activity 上登记 VIEW + DEFAULT + BROWSABLE intent filter,严格限定 scheme 和回调路径;启用 app_links 处理,并关闭 Flutter 与其冲突的自动深链处理。macOS 在 Info.plist 中登记 Scheme,并允许出站网络访问。仅声明自定义 Scheme 不需要在 Apple Developer/Google Cloud 创建条目;经过验证的 HTTPS 链接则需要平台域名关联文件和 entitlement。

SDK 直接启动固定的官方 Android 包,或使用 universalLinksOnly 打开 iOS Universal Link。它不查询已安装应用,不添加 <queries>、QUERY_ALL_PACKAGES 或 LSApplicationQueriesSchemes 声明。Android 浏览器回退使用 Custom Tabs,Apple 使用 ASWebAuthenticationSession。等待时提供调用 signIn.cancel() 的取消按钮。Android 浏览器关闭不能可靠地报告取消;显式取消或有限超时会结束该次尝试。进程退出会丢失内存中的 verifier,此时应重新开始,不得接受未绑定事务的回调。

自定义 Scheme 不能证明专属所有权。PKCE 可阻止拦截授权码的其他应用在没有 verifier 的情况下兑换。平台资料和公开 client ID 不属于 App Attest/Play Integrity 证明。

原生协议

以下各秘密值均为独立的 32 字节随机值,编码为不带填充的 base64url(43 字符)。使用密码学安全的随机源生成。code_challenge 为 base64url(SHA-256(verifier)),仅支持 S256。

POST https://auth.koee.app/identity/native/requests
Content-Type: application/json

{
  "client_id": "COPY_PLATFORM_CLIENT_ID",
  "response_type": "code",
  "scope": "openid",
  "response_mode": "query",
  "destination": "COPY_REGISTERED_CALLBACK_URL",
  "state": "RANDOM_43_CHARACTER_VALUE",
  "nonce": "ANOTHER_RANDOM_43_CHARACTER_VALUE",
  "code_challenge": "SHA256_VERIFIER_BASE64URL",
  "code_challenge_method": "S256"
}

响应:{id, launch_ticket, requester_secret, expires_at}。启动地址:

https://auth.koee.app/identity/open-app#request_id=ID&launch_ticket=TICKET

将 requester_secret 和 verifier 留在请求应用内部,禁止放入链接、分析数据、日志或二维码。原生启动票据有效期为 120 秒。只有已登录的官方手机 App 可查看应用详情并批准或拒绝请求。批准后,授权码获得完整的 60 秒兑换窗口。状态轮询不会返回授权码或用户身份。

POST /identity/native/requests/{id}/status 和 /cancel 接收 JSON {requester_secret}。轮询间隔不得短于 2 秒。这些接口不接受浏览器 Origin。浏览器授权必须通过认证中心创建,不得在 JavaScript 中获取原生批准秘密值。

若官方 App 无法启动,先取消原生请求,再用全新的 state、nonce 和 verifier 创建新的浏览器授权。拒绝、取消或超时后不得回退。原生回退落地页不会在浏览器中批准原生票据。

浏览器授权参数

打开 https://auth.koee.app/identity/authorize,使用以下查询参数:

参数值
client_id已登记的平台 client ID
response_typecode
scopeopenid 或省略
response_modeWeb 弹窗使用 web_message;已登记跳转使用 query
destination精确登记的 Origin(弹窗)或完整回调地址(跳转)
state每次尝试独立生成的 43 字符随机 base64url 值
nonce每次尝试独立生成的另一 43 字符随机 base64url 值
code_challengeS256 challenge
code_challenge_methodS256

浏览器请求有效期为 10 分钟。批准后的授权码最多有效 60 秒,且不能超过请求截止时间。即使已经登录,用户仍须确认每一次第三方授权。登录与注册共用入口,服务判断账号是否存在。二维码认证先登录官方认证中心,不会跳过第三方确认。

兑换授权码

POST https://auth.koee.app/identity/token?client_id=COPY_PLATFORM_CLIENT_ID
Content-Type: application/json

{
  "grant_type": "authorization_code",
  "client_id": "COPY_PLATFORM_CLIENT_ID",
  "response_mode": "query",
  "destination": "COPY_REGISTERED_CALLBACK_URL",
  "code": "RECEIVED_CODE",
  "code_verifier": "ORIGINAL_VERIFIER"
}

弹窗兑换使用 response_mode: "web_message",destination 为原始 Origin。请求体与查询参数中的 client ID 必须一致。浏览器使用 JSON 和精确 Origin 的 CORS 预检,省略 credentials。原生 HTTP 兑换不发送浏览器 Origin。授权码以原子方式消费一次,并发兑换只有一个成功者。若兑换结果丢失,应重新登录。

{ "id_token": "SIGNED_RS256_JWT", "id_token_expires_in": 300 }

不要期待返回 access_token、refresh_token 或 token_type。

验证身份

使用固定产品签发者的 /identity/jwks.json,不得跟随 token 中的 jku、x5u 或任意密钥 URL。仅接受这些公钥对应的 RS256 和预期 kid。验证真实签名,以及以下所有条件:

  • iss 精确等于 https://auth.koee.app。
  • aud 精确等于发起请求的平台 client ID。
  • nonce 等于发起事务的 nonce。
  • sub 是非空、仅用于该应用的不透明标识符。
  • iat 和 exp 为整数秒;有效时长最多 300 秒。
  • 当前时间处于有效区间,最多允许 60 秒时钟偏差。

将 (iss, sub) 作为账号键。不要使用昵称、handle、邮箱或手机号作为唯一身份。同一应用跨平台获得相同 subject,不同应用获得不同 subject。本版本不提供 userinfo 接口。

应用可以没有后端或既有账号系统,直接使用已验证身份实现本地个性化。若涉及受保护的服务端数据,服务端必须验证 ID token,并在创建本站会话前将 nonce 绑定至自己发起的登录尝试;客户端验证无法保护服务端免受伪造请求。

错误与撤销

access_denied/cancelled 表示用户结束尝试。invalid_grant 表示授权码或请求已过期、消费、变更或不再获准。rate_limited 要求退避。identity_unavailable 表示服务开关或签名配置不可用。不得静默重试一次性兑换。

用户可在设置 → 隐私与安全 → 已授权应用中撤销应用授权。撤销会使未消费授权码失效,并要求重新批准;它无法立即消除已签发、可离线验证的 ID token,也无法结束第三方自己的会话。应用和管理员可停用接入,此后新的批准和兑换会失败。

实施检查清单

  1. 创建应用并配置正确的平台客户端。
  2. 验证域名;获批前仅用所有者账号测试。
  3. 登记精确回调或 Origin,每次尝试独立保存 verifier/state/nonce。
  4. 验证完整回调、state 和 JWT,不得仅信任解码后的 JSON。
  5. 测试拒绝、App 未安装、弹窗拦截、重复回调、过期、切账号、撤销授权及网络中断。
  6. 提交审核。已发布配置的变更需再次审核。