Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
1ae32af
[OIDC] Specify https://authgear.com/claims/user/identities claim
carmenlau May 27, 2026
9da5fbf
[OIDC] Add login_id_type to identities claim for login_id identity
carmenlau May 28, 2026
98d3e26
[OIDC] Add created_at and updated_at to authenticators claim spec
carmenlau May 28, 2026
c17ac69
[OIDC] Add provider_type to identities userinfo claim spec
carmenlau May 29, 2026
45e975f
[OIDC] Rename provider_type/provider_alias to oauth_provider_type/oau…
carmenlau Jun 3, 2026
b22cccd
Add implementation plan for identities userinfo claim
carmenlau May 27, 2026
fa180cc
Add ClaimIdentities constant and UserInfoIdentity model type
carmenlau May 27, 2026
d2a4586
Add UserInfoIdentityService and populate identities in UserInfoService
carmenlau May 27, 2026
f84c872
Expose https://authgear.com/claims/user/identities in OIDC userinfo
carmenlau May 27, 2026
e85e58b
Wire UserInfoIdentityService and regenerate mocks
carmenlau May 27, 2026
25fe040
Test identities claim in OIDC GetUserInfo
carmenlau May 27, 2026
5c7ed27
e2e: Test identities and authenticators claims in userinfo endpoint
carmenlau May 27, 2026
6260c25
Fix TestGetUserInfoBearer to mock IdentityService
carmenlau May 27, 2026
4355774
Add created_at, updated_at, and login_id_key to identities userinfo c…
carmenlau May 28, 2026
fb53330
Add login_id_type to identities userinfo claim
carmenlau May 28, 2026
6dcf02e
Add provider_type to identities userinfo claim
carmenlau May 29, 2026
b4ec9a0
Rename provider_type/provider_alias to oauth_provider_type/oauth_prov…
carmenlau Jun 3, 2026
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
1 change: 1 addition & 0 deletions cmd/authgear/background/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

317 changes: 317 additions & 0 deletions docs/plans/oidc/userinfo-identities-claim-js.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
# Implementation Plan: `https://authgear.com/claims/user/identities` — JS SDK

## 1. Goal / Scope

Add the `identities` field to the `UserInfo` type in the JS SDK so that callers of `fetchUserInfo()` can read which identities are linked to the user, for login ID identities which key was used, and for OAuth identities which provider alias they used.

This mirrors how `authenticators` was added. All changes are in `packages/authgear-core/src/types.ts` and its test file. The three other packages (`authgear-web`, `authgear-react-native`, `authgear-capacitor`) import `UserInfo` from core and require no changes.

Spec: `docs/specs/sdk-settings-actions.md` (Full UserInfo Design, Display Linked OAuth Providers).

---

## 2. Type Changes

### File: `packages/authgear-core/src/types.ts`

#### Add `IdentityType` enum

Place after the existing `AuthenticatorKind` enum (before `Authenticator` interface):

```typescript
/**
* @public
*/
export enum IdentityType {
LoginID = "login_id",
OAuth = "oauth",
Anonymous = "anonymous",
Biometric = "biometric",
Passkey = "passkey",
SIWE = "siwe",
LDAP = "ldap",
Unknown = "unknown",
}
```

`Unknown` follows the same defensive pattern used by `AuthenticatorType` — unknown values from the server decode gracefully.

#### Add `LoginIDType` enum

Place after `IdentityType`:

```typescript
/**
* @public
*/
export enum LoginIDType {
Email = "email",
Phone = "phone",
Username = "username",
Unknown = "unknown",
}
```

`Unknown` follows the same pattern — if the server introduces a new login ID type, the SDK decodes it gracefully rather than silently dropping or corrupting the value.

#### Add `Identity` interface

Place after the `LoginIDType` enum:

```typescript
/**
* @public
*/
export interface Identity {
type: IdentityType;
createdAt: Date;
updatedAt: Date;
loginIDKey?: string;
loginIDType?: LoginIDType;
oauthProviderType?: string;
oauthProviderAlias?: string;
}
```

`createdAt` and `updatedAt` are always present, decoded from the `"created_at"` and `"updated_at"` RFC 3339 strings in the JSON response — matching the pattern used by `Authenticator`.

`loginIDKey` is present only when `type` is `IdentityType.LoginID` (e.g. `"email"`, `"phone"`, `"username"`).

`loginIDType` is present only when `type` is `IdentityType.LoginID`. It is one of `LoginIDType.Email`, `LoginIDType.Phone`, `LoginIDType.Username`, or `LoginIDType.Unknown`.

`oauthProviderType` is present only when `type` is `IdentityType.OAuth` (e.g. `"google"`, `"facebook"`).

`oauthProviderAlias` is present only when `type` is `IdentityType.OAuth`.

#### Extend `UserInfo` interface

Add `identities` after `authenticators`:

```typescript
authenticators?: Authenticator[];
identities?: Identity[];
```

#### Add `parseIdentityType` and `parseLoginIDType` functions

Place after the existing `parseAuthenticatorKind` function:

```typescript
/**
* @internal
*/
export function parseIdentityType(value: string): IdentityType {
switch (value) {
case "login_id":
return IdentityType.LoginID;
case "oauth":
return IdentityType.OAuth;
case "anonymous":
return IdentityType.Anonymous;
case "biometric":
return IdentityType.Biometric;
case "passkey":
return IdentityType.Passkey;
case "siwe":
return IdentityType.SIWE;
case "ldap":
return IdentityType.LDAP;
default:
return IdentityType.Unknown;
}
}

/**
* @internal
*/
export function parseLoginIDType(value: string): LoginIDType {
switch (value) {
case "email":
return LoginIDType.Email;
case "phone":
return LoginIDType.Phone;
case "username":
return LoginIDType.Username;
default:
return LoginIDType.Unknown;
}
}
```

#### Add `_decodeIdentities` function

Place after `_decodeAuthenticators`:

```typescript
/**
* @internal
*/
export function _decodeIdentities(r: any): Identity[] | undefined {
if (!Array.isArray(r)) {
return undefined;
}
return r.map((i) => {
const identity: Identity = {
type: parseIdentityType(i["type"]),
createdAt: new Date(i["created_at"]),
updatedAt: new Date(i["updated_at"]),
};
if (identity.type === IdentityType.LoginID) {
identity.loginIDKey = i["login_id_key"];
identity.loginIDType =
i["login_id_type"] != null
? parseLoginIDType(i["login_id_type"])
: undefined;
}
if (identity.type === IdentityType.OAuth) {
identity.oauthProviderType = i["oauth_provider_type"];
identity.oauthProviderAlias = i["oauth_provider_alias"];
}
return identity;
});
}
```

#### Extend `_decodeUserInfo`

Add after the `authenticators` line:

```typescript
authenticators: _decodeAuthenticators(
r["https://authgear.com/claims/user/authenticators"]
),
identities: _decodeIdentities(
r["https://authgear.com/claims/user/identities"]
),
```

---

## 3. Test Changes

### File: `packages/authgear-core/src/types.test.ts`

#### Update import

Add `IdentityType` to the import:

```typescript
import {
_decodeUserInfo,
AuthenticatorType,
AuthenticatorKind,
IdentityType,
LoginIDType,
} from "./types";
```

#### Extend `USER_INFO` fixture

Add `"https://authgear.com/claims/user/identities"` after the authenticators array in the JSON string:

```json
"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"
},
{
"type": "unknown_future_type",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
],
```

#### Extend `expected` object in the test

Add `identities` after `authenticators`:

```typescript
identities: [
{
type: IdentityType.OAuth,
createdAt: new Date("2024-01-01T00:00:00Z"),
updatedAt: new Date("2024-01-01T00:00:00Z"),
oauthProviderType: "google",
oauthProviderAlias: "google",
},
{
type: IdentityType.LoginID,
createdAt: new Date("2024-01-01T00:00:00Z"),
updatedAt: new Date("2024-01-01T00:00:00Z"),
loginIDKey: "email",
loginIDType: LoginIDType.Email,
},
{
type: IdentityType.Unknown,
createdAt: new Date("2024-01-01T00:00:00Z"),
updatedAt: new Date("2024-01-01T00:00:00Z"),
},
],
```

The third case (`"unknown_future_type"`) verifies the `Unknown` fallback.

#### Extend `raw` in the `expected` object

Add the raw key to match the fixture (the `raw` field in `UserInfo` is the unmodified input):

```typescript
"https://authgear.com/claims/user/identities": [
{ type: "oauth", oauth_provider_type: "google", oauth_provider_alias: "google" },
{ type: "login_id", login_id_key: "email", login_id_type: "email" },
{ type: "unknown_future_type" },
],
```

---

## 4. File-Level Change Summary

| File | Change |
|---|---|
| `packages/authgear-core/src/types.ts` | Add `IdentityType` enum, `LoginIDType` enum, `Identity` interface, `parseIdentityType`, `parseLoginIDType`, `_decodeIdentities`; extend `UserInfo` and `_decodeUserInfo` |
| `packages/authgear-core/src/types.test.ts` | Extend import, fixture JSON, and expected object |

No changes to `authgear-web`, `authgear-react-native`, or `authgear-capacitor` — they re-export `UserInfo` from core unchanged.

---

## 5. Verification

```
cd packages/authgear-core
npx jest src/types.test.ts
```

Also run the TypeScript check across the repo:

```
cd /path/to/authgear-sdk-js
npm run typecheck
```

---

## 6. Atomic Commit Plan

### Commit 1 — Types and decoder
**File:** `packages/authgear-core/src/types.ts`

Add `IdentityType` enum, `Identity` interface, `parseIdentityType`, `_decodeIdentities`, extend `UserInfo.identities`, extend `_decodeUserInfo`.

### Commit 2 — Tests
**File:** `packages/authgear-core/src/types.test.ts`

Extend import, fixture, and expected object. Tests must pass.
Loading
Loading