Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 67 additions & 2 deletions docs/specs/sdk-settings-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

---

Expand Down Expand Up @@ -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
Expand All @@ -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;
}
```
Expand All @@ -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"
}
Copy link
Copy Markdown
Contributor Author

@carmenlau carmenlau May 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it’s okay for different APIs to have different shapes as long as they are well documented and easy to understand, but we should keep the terminology consistent. (We could discuss)

Initially, I was wondering whether we should use the term login_id, since it feels more like an internal implementation detail to me. But after checking across different components, login_id has already been used in several places, so I think it’s fine to use it here as well.

For reference, how do the other APIs handle the login_id identity:

Auth flow api
https://docs.authgear.com/customization/custom-ui/authentication-flow-api

User import api
https://docs.authgear.com/reference/apis/user-import-api#initiate-import

User search
https://docs.authgear.com/reference/apis/admin-api/api-examples/search-for-users#search-users-by-email

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems very different...

Can we map a data model behind that we can make it more consistent?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me draft something about it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've drafted a spec at #5758 as a guideline for future API consistency work.

One thing we didn't cover in the meeting, webhook events have the same issue as Admin GraphQL: identity data is exposed as a raw namespaced claims map rather than typed fields. We'll keep it as-is for now, same as Admin GraphQL.

],
"https://authgear.com/claims/user/recovery_code_enabled": true
Expand All @@ -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.
16 changes: 16 additions & 0 deletions docs/specs/user-profile/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Comment thread
tung2744 marked this conversation as resolved.
- `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).
Expand Down
Loading