diff --git a/docs/specs/sdk-settings-actions.md b/docs/specs/sdk-settings-actions.md index 1c696d2b815..22247f7a06c 100644 --- a/docs/specs/sdk-settings-actions.md +++ b/docs/specs/sdk-settings-actions.md @@ -13,6 +13,7 @@ This document specifies the API design of settings actions. - [Setup / Change / Remove MFA Password](#setup--change--remove-MFA-password) - [Setup / Manage MFA TOTP](#setup--manage-mfa-totp) - [Setup / View Recovery Code](#setup--view-recovery-code) +- [Display Linked OAuth Providers](#display-linked-oauth-providers) --- @@ -353,6 +354,32 @@ await authgear.setupRecoveryCode({ redirectURI: "com.example://complete" }); await authgear.viewRecoveryCode({ redirectURI: "com.example://complete" }); ``` +## Display Linked OAuth Providers + +### Intention + +App developers might want to know which OAuth providers the user has linked to their account, for example to show a "Connected accounts" screen or gate features behind a specific provider being linked. + +### SDK Design + +- Display Linked OAuth Providers + +```typescript +const userInfo = await authgear.fetchUserInfo(); +const linkedOAuthProviders = userInfo.identities + .filter((i) => i.type === "oauth") + .map((i) => i.oauthProviderAlias); +``` + +- Check whether a specific provider is linked + +```typescript +const userInfo = await authgear.fetchUserInfo(); +const isGoogleLinked = userInfo.identities.some( + (i) => i.type === "oauth" && i.oauthProviderAlias === "google" +); +``` + ## Full UserInfo Design - SDK Object @@ -361,16 +388,29 @@ await authgear.viewRecoveryCode({ redirectURI: "com.example://complete" }); interface Authenticator { kind: "primary" | "secondary"; type: "password" | "passkey" | "totp" | "oob_otp_email" | "oob_otp_sms"; + createdAt: Date; + updatedAt: Date; email?: string; phoneNumber?: string; } +interface Identity { + type: "login_id" | "oauth" | "anonymous" | "biometric" | "passkey" | "siwe" | "ldap"; + createdAt: Date; + updatedAt: Date; + loginIDKey?: string; // Present when type is "login_id", e.g. "email", "phone", "username" + loginIDType?: "email" | "phone" | "username"; // Present when type is "login_id" + oauthProviderType?: string; // Present when type is "oauth", e.g. "google", "facebook" + oauthProviderAlias?: string; // Present when type is "oauth" +} + interface UserInfo { sub: string; email: string; phoneNumber: string; preferredUsername: string; authenticators: []Authenticator; + identities: []Identity; recoveryCodeEnabled: boolean; } ``` @@ -386,21 +426,45 @@ interface UserInfo { "https://authgear.com/claims/user/authenticators": [ { "kind": "primary", - "type": "password" + "type": "password", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" }, { "kind": "secondary", "type": "oob_otp_email", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z", "email": "oob_otp_email@example.com" }, { "kind": "secondary", "type": "oob_otp_sms", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z", "phone_number": "+85212345678" }, { "kind": "secondary", - "type": "totp" + "type": "totp", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + } + ], + "https://authgear.com/claims/user/identities": [ + { + "type": "oauth", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z", + "oauth_provider_type": "google", + "oauth_provider_alias": "google" + }, + { + "type": "login_id", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z", + "login_id_key": "email", + "login_id_type": "email" } ], "https://authgear.com/claims/user/recovery_code_enabled": true @@ -410,4 +474,5 @@ interface UserInfo { ## Security Considerations - We will expose user's password status together with MFA emails and phone numbers in userinfo endpoint, therefore client apps will be able to know them. If the client app is malicious, they may use the information to attack an authgear user. +- We expose which identity types and OAuth provider aliases are linked via `https://authgear.com/claims/user/identities`. A malicious client app could use this to infer which providers a user has accounts with. - We can hide these fields in Third-Party Clients (by checking scope `https://authgear.com/scopes/full-access`) to mitigate the risk. diff --git a/docs/specs/user-profile/design.md b/docs/specs/user-profile/design.md index df158ec36bf..9aa477631b9 100644 --- a/docs/specs/user-profile/design.md +++ b/docs/specs/user-profile/design.md @@ -491,6 +491,22 @@ A boolean. True if the user can perform reauthentication. A list of authenticators owned by the user. See [this doc](../sdk-settings-actions.md). +#### https://authgear.com/claims/user/identities + +A list of identities linked to the user. See [this doc](../sdk-settings-actions.md). + +Each element in the array has the following fields: + +- `type` (string): The identity type. One of `login_id`, `oauth`, `anonymous`, `biometric`, `passkey`, `siwe`, `ldap`. +- `created_at` (string): RFC 3339 timestamp of when the identity was created. +- `updated_at` (string): RFC 3339 timestamp of when the identity was last updated. +- `login_id_key` (string, optional): The configured key name of the login ID (e.g. `"email"`, `"phone"`, `"username"`). Present only when `type` is `login_id`. +- `login_id_type` (string, optional): The type of login ID. One of `"email"`, `"phone"`, `"username"`. Present only when `type` is `login_id`. +- `oauth_provider_type` (string, optional): The type of the OAuth provider (e.g. `"google"`, `"facebook"`). Present only when `type` is `oauth`. +- `oauth_provider_alias` (string, optional): The configured alias of the OAuth provider. Present only when `type` is `oauth`. + +> **Design note:** Fields inside each identity element use plain keys (e.g. `provider_alias`) rather than the namespaced form (`https://authgear.com/claims/oauth/provider_alias`). Namespaced keys at the top level of the userinfo response are an OIDC convention to avoid collisions with standard claims; they are not used inside nested objects — the OIDC standard itself follows this pattern (e.g. the `address` claim uses plain keys like `street_address` and `country`). The Admin API GraphQL exposes identity claims as a raw `IdentityClaims` scalar that includes the namespaced keys, but that is a different surface reflecting the full internal claims map rather than a curated view. + #### https://authgear.com/claims/user/recovery_code_enabled A boolean. See [this doc](../sdk-settings-actions.md).