diff --git a/cmd/authgear/background/wire_gen.go b/cmd/authgear/background/wire_gen.go index 454a7b6314b..89f06cac45d 100644 --- a/cmd/authgear/background/wire_gen.go +++ b/cmd/authgear/background/wire_gen.go @@ -580,6 +580,7 @@ func newUserService(p *deps.BackgroundProvider, appID string, appContext *config RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, diff --git a/docs/plans/oidc/userinfo-identities-claim-js.md b/docs/plans/oidc/userinfo-identities-claim-js.md new file mode 100644 index 00000000000..f1037080741 --- /dev/null +++ b/docs/plans/oidc/userinfo-identities-claim-js.md @@ -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. diff --git a/docs/plans/oidc/userinfo-identities-claim-server.md b/docs/plans/oidc/userinfo-identities-claim-server.md new file mode 100644 index 00000000000..a03da4819c1 --- /dev/null +++ b/docs/plans/oidc/userinfo-identities-claim-server.md @@ -0,0 +1,255 @@ +# Implementation Plan: `https://authgear.com/claims/user/identities` + +## 1. Goal / Scope + +Add `https://authgear.com/claims/user/identities` to the OIDC userinfo endpoint. +Each element exposes `type` (string), for login ID identities `login_id_key` (string) and `login_id_type` (string), and for OAuth identities `oauth_provider_type` (string) and `oauth_provider_alias` (string). + +This follows the same pattern as `https://authgear.com/claims/user/authenticators`: +- Returned from the userinfo endpoint only (not embedded in the ID token). +- Gated by `isClaimAllowed` in `GetUserInfo`, so it is only included when the client requests the claim in scope (first-party clients get it via `full-access`). +- Cached inside the existing `UserInfo` Redis cache; no new cache key. + +Spec: `docs/specs/user-profile/design.md` (Special Claims) and `docs/specs/sdk-settings-actions.md` (Full UserInfo Design). + +--- + +## 2. Model Changes + +### `pkg/api/model/claims.go` + +Add one constant after `ClaimAuthenticators`: + +```go +ClaimIdentities ClaimName = "https://authgear.com/claims/user/identities" +``` + +### `pkg/api/model/userinfo.go` + +Add a new struct after `UserInfoAuthenticator`: + +```go +type UserInfoIdentity struct { + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Type model.IdentityType `json:"type"` + LoginIDKey string `json:"login_id_key,omitempty"` + LoginIDType model.LoginIDKeyType `json:"login_id_type,omitempty"` + OAuthProviderType string `json:"oauth_provider_type,omitempty"` + OAuthProviderAlias string `json:"oauth_provider_alias,omitempty"` +} +``` + +Mirrors `UserInfoAuthenticator` which also carries `created_at`/`updated_at`. + +--- + +## 3. UserInfo Service + +### `pkg/lib/userinfo/userinfo.go` + +**New interface** (place alongside the existing `UserInfoAuthenticatorService` interface): + +```go +type UserInfoIdentityService interface { + ListByUser(ctx context.Context, userID string) ([]*identity.Info, error) +} +``` + +This is satisfied by `*identity/service.Service`, which already has: +```go +// pkg/lib/authn/identity/service/service.go +func (s *Service) ListByUser(ctx context.Context, userID string) ([]*identity.Info, error) +``` + +**Add field to `UserInfoService` struct:** + +```go +IdentityService UserInfoIdentityService +``` + +**Add field to `UserInfo` struct:** + +```go +Identities []model.UserInfoIdentity `json:"identities"` +``` + +**Extend `getUserInfoFromDatabase()`** — add after the authenticators block (before the `recoveryCodes` block): + +```go +identityInfos, err := s.IdentityService.ListByUser(ctx, userID) +if err != nil { + return nil, err +} + +userinfoIdentities := []model.UserInfoIdentity{} +for _, info := range identityInfos { + uiIdentity := model.UserInfoIdentity{ + CreatedAt: info.CreatedAt, + UpdatedAt: info.UpdatedAt, + Type: info.Type, + } + if info.Type == model.IdentityTypeLoginID && info.LoginID != nil { + uiIdentity.LoginIDKey = info.LoginID.LoginIDKey + uiIdentity.LoginIDType = info.LoginID.LoginIDType + } + if info.Type == model.IdentityTypeOAuth && info.OAuth != nil { + uiIdentity.OAuthProviderType = info.OAuth.ProviderID.Type + uiIdentity.OAuthProviderAlias = info.OAuth.ProviderAlias + } + userinfoIdentities = append(userinfoIdentities, uiIdentity) +} +``` + +`info.CreatedAt` and `info.UpdatedAt` are on `identity.Info` (line 9–10 of `pkg/lib/authn/identity/info.go`). + +`info.LoginID.LoginIDKey` is the `LoginIDKey string` field on `pkg/lib/authn/identity/loginid_identity.go` (the configured key name, e.g. `"email"`, `"phone"`, `"username"`). + +`info.LoginID.LoginIDType` is the `LoginIDType model.LoginIDKeyType` field on the same struct — one of `"email"`, `"phone"`, `"username"`. + +`info.OAuth.ProviderAlias` is the `ProviderAlias string` field defined at `pkg/lib/authn/identity/oauth_identity.go`. + +**Update the return value:** + +```go +return &UserInfo{ + User: u, + AccountAccountStaleFrom: u.AccountStatusStaleFrom, + EffectiveRoleKeys: roleKeys, + Authenticators: userinfoAuthens, + Identities: userinfoIdentities, + RecoveryCodeEnabled: len(recoveryCodes) > 0, +}, nil +``` + +--- + +## 4. OIDC Token Issuer + +### `pkg/lib/oauth/oidc/id_token.go` + +In `GetUserInfo()`, add after the `ClaimAuthenticators` block: + +```go +if isClaimAllowed(string(model.ClaimIdentities)) { + out[string(model.ClaimIdentities)] = userInfo.Identities +} +``` + +No changes to `PopulateUserClaimsInIDToken` — the identities claim is userinfo-only, matching the authenticators claim. + +--- + +## 5. Wire / DI + +`pkg/lib/userinfo/deps.go` uses `wire.Struct(new(UserInfoService), "*")`, which injects all exported fields by type automatically. No change is needed to `deps.go` itself — wire will pick up `IdentityService UserInfoIdentityService` as long as the concrete type is bound. + +The concrete type `*identity/service.Service` is already present in all wire scopes that construct `UserInfoService` (auth, admin, resolver, redisqueue). The existing wire graphs already provide `*identity/service.Service` for other dependencies in those same scopes. + +After adding the field, run: + +``` +make generate +``` + +This regenerates all `wire_gen.go` files (`pkg/auth/wire_gen.go`, `pkg/admin/wire_gen.go`, `pkg/resolver/wire_gen.go`, `pkg/redisqueue/wire_gen.go`). Each `userInfoService := &userinfo.UserInfoService{...}` block gains `IdentityService: identityService`. + +--- + +## 6. Cache / Deployment Compatibility + +- **Cache key**: unchanged — `app:{appID}:userinfo:{userID}:{role}`. +- **Cached shape change**: the `UserInfo` JSON gains a new `"identities"` field. Old cached entries lack it; Go's `json.Unmarshal` leaves `Identities` as `nil`. +- **Serving stale cache**: during the deploy window, a cached entry without `"identities"` will cause the claim to be absent from the response. This is transient — `duration.Short` (5 minutes) is the cache TTL, so all stale entries expire quickly. +- **Identity change invalidation**: `pkg/lib/userinfo/sink.go` calls `PurgeUserInfo` for every userID returned by `RequireReindexUserIDs()` and `DeletedUserIDs()` on each non-blocking event. Identity add/remove events already trigger reindex, so caches are purged on identity changes post-deploy. +- **No cache key version bump required.** + +--- + +## 7. Mock Regeneration + +`pkg/lib/userinfo/userinfo.go` has a `//go:generate` directive that produces `userinfo_mock_test.go`. After adding `UserInfoIdentityService`, run: + +``` +make generate +``` + +This regenerates the mock for the new interface so tests can mock `IdentityService`. + +--- + +## 8. Test Plan + +### `pkg/lib/oauth/oidc/id_token_test.go` + +Style: Convey BDD (existing file imports `goconvey`). + +**Extend `TestGetUserInfo`** (the `TestIDTokenIssuer_GetUserInfo` Convey block): + +1. Add `Identities: []model.UserInfoIdentity{...}` to the mock `userinfo.UserInfo` return value in `mockUserInfoService.EXPECT()`. +2. Add `string(model.ClaimIdentities)` to the `scopes` slice passed to `oauth.ClientClientLike`. +3. Assert the output JSON contains `"https://authgear.com/claims/user/identities"` with the expected array. + +Concrete cases to cover: + +| Scenario | `Identities` in mock | Expected JSON key | +|---|---|---| +| OAuth identity | `[{CreatedAt: t, UpdatedAt: t, Type: "oauth", OAuthProviderType: "google", OAuthProviderAlias: "google"}]` | `[{"created_at":"...","updated_at":"...","type":"oauth","oauth_provider_type":"google","oauth_provider_alias":"google"}]` | +| Login ID identity | `[{CreatedAt: t, UpdatedAt: t, Type: "login_id", LoginIDKey: "email", LoginIDType: "email"}]` | `[{"created_at":"...","updated_at":"...","type":"login_id","login_id_key":"email","login_id_type":"email"}]` | +| Mixed | both of the above | both elements | +| Empty | `[]` | `[]` | + +**Extend `TestGetUserInfo`** (the `TestGetUserInfo` Convey block, which uses map assertions): + +Add: +```go +So(userInfo[string(model.ClaimIdentities)], ShouldResemble, []model.UserInfoIdentity{ + {Type: model.IdentityTypeOAuth, OAuthProviderType: "google", OAuthProviderAlias: "google"}, +}) +``` + +--- + +## 9. File-Level Change Summary + +| File | Change | +|---|---| +| `pkg/api/model/claims.go` | Add `ClaimIdentities` constant | +| `pkg/api/model/userinfo.go` | Add `UserInfoIdentity` struct | +| `pkg/lib/userinfo/userinfo.go` | Add interface, field on service, field on UserInfo, populate in `getUserInfoFromDatabase` | +| `pkg/lib/oauth/oidc/id_token.go` | Add `ClaimIdentities` to `GetUserInfo` output | +| `pkg/auth/wire_gen.go` | Regenerated — `IdentityService` field added to `UserInfoService` construction | +| `pkg/admin/wire_gen.go` | Regenerated | +| `pkg/resolver/wire_gen.go` | Regenerated | +| `pkg/redisqueue/wire_gen.go` | Regenerated | +| `pkg/lib/userinfo/userinfo_mock_test.go` | Regenerated (new mock for `UserInfoIdentityService`) | +| `pkg/lib/oauth/oidc/id_token_test.go` | Extend existing Convey tests | + +--- + +## 10. Atomic Commit Plan + +### Commit 1 — Model types +**Files:** `pkg/api/model/claims.go`, `pkg/api/model/userinfo.go` + +Add `ClaimIdentities` constant and `UserInfoIdentity` struct. No behavior change. + +### Commit 2 — UserInfo service: populate identities +**Files:** `pkg/lib/userinfo/userinfo.go` + +Add `UserInfoIdentityService` interface, `IdentityService` field on `UserInfoService`, `Identities` field on `UserInfo`, and the population logic in `getUserInfoFromDatabase`. + +### Commit 3 — Expose claim in OIDC userinfo +**Files:** `pkg/lib/oauth/oidc/id_token.go` + +Add `ClaimIdentities` output block in `GetUserInfo`. + +### Commit 4 — Wire regeneration +**Files:** `pkg/auth/wire_gen.go`, `pkg/admin/wire_gen.go`, `pkg/resolver/wire_gen.go`, `pkg/redisqueue/wire_gen.go`, `pkg/lib/userinfo/userinfo_mock_test.go` + +Run `make generate`. Commit all regenerated files together. Build must pass (`go build ./...`). + +### Commit 5 — Tests +**Files:** `pkg/lib/oauth/oidc/id_token_test.go` + +Extend Convey tests to cover the identities claim in all cases listed in the test plan. Run `go test ./pkg/lib/oauth/oidc/...` to verify. 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). diff --git a/e2e/cmd/e2e/pkg/wire_gen.go b/e2e/cmd/e2e/pkg/wire_gen.go index 49104f36d3c..f1b2083a77b 100644 --- a/e2e/cmd/e2e/pkg/wire_gen.go +++ b/e2e/cmd/e2e/pkg/wire_gen.go @@ -523,6 +523,7 @@ func newUserImport(p *deps.AppProvider) *userimport.UserImportService { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, diff --git a/e2e/pkg/e2eclient/client.go b/e2e/pkg/e2eclient/client.go index eee8c09486d..e4ca6330ca9 100644 --- a/e2e/pkg/e2eclient/client.go +++ b/e2e/pkg/e2eclient/client.go @@ -279,7 +279,8 @@ type OAuthExchangeCodeOptions struct { } type OAuthExchangeCodeResult struct { - IDToken map[string]any `json:"id_token"` + IDToken map[string]any `json:"id_token"` + AccessToken string `json:"access_token"` } func (c *Client) OAuthExchangeCode(opts OAuthExchangeCodeOptions) (result *OAuthExchangeCodeResult, err error) { @@ -363,8 +364,11 @@ func (c *Client) OAuthExchangeCode(opts OAuthExchangeCodeOptions) (result *OAuth return } + accessToken, _ := tokenRespBody["access_token"].(string) + result = &OAuthExchangeCodeResult{ - IDToken: idTokenMap, + IDToken: idTokenMap, + AccessToken: accessToken, } return } diff --git a/e2e/tests/oidc/oauth_identity.sql b/e2e/tests/oidc/oauth_identity.sql new file mode 100644 index 00000000000..5edebcd6bcc --- /dev/null +++ b/e2e/tests/oidc/oauth_identity.sql @@ -0,0 +1,29 @@ +{{ $oauth_identity_id := uuidv4 }} + +INSERT INTO _auth_identity ( + id, app_id, type, user_id, created_at, updated_at +) +SELECT + '{{ $oauth_identity_id }}', + '{{ .AppID }}', + 'oauth', + i.user_id, + NOW(), + NOW() +FROM _auth_identity i +JOIN _auth_identity_login_id l ON i.id = l.id AND l.app_id = i.app_id +WHERE i.app_id = '{{ .AppID }}' +AND l.login_id = 'e2e_userinfo_user@example.com'; + +INSERT INTO _auth_identity_oauth ( + id, app_id, provider_type, provider_keys, provider_user_id, claims, profile +) +VALUES ( + '{{ $oauth_identity_id }}', + '{{ .AppID }}', + 'google', + '{}', + 'google-subject-123', + '{}', + '{}' +); diff --git a/e2e/tests/oidc/userinfo.test.yaml b/e2e/tests/oidc/userinfo.test.yaml new file mode 100644 index 00000000000..05e53bf3da2 --- /dev/null +++ b/e2e/tests/oidc/userinfo.test.yaml @@ -0,0 +1,134 @@ +name: Userinfo endpoint returns identities and authenticators claims +authgear.yaml: + override: | + authentication: + identities: + - login_id + - oauth + primary_authenticators: + - password + identity: + login_id: + keys: + - type: username + - type: email + oauth: + providers: + - alias: google + client_id: "google" + type: google + verification: + claims: + email: + enabled: true + required: false +before: + - type: user_import + user_import: users.json + - type: custom_sql + custom_sql: + path: oauth_identity.sql +steps: + - name: oauth_setup + action: oauth_setup + + # Log in with username + password + - name: create_flow + action: create + input: | + { + "type": "login", + "name": "default", + "url_query": "{{ .steps.oauth_setup.result.query }}" + } + output: + result: | + { + "action": { + "type": "identify" + } + } + + - name: identify + action: input + input: | + { + "identification": "username", + "login_id": "e2e_userinfo_user" + } + output: + result: | + { + "action": { + "type": "authenticate" + } + } + + - name: authenticate + action: input + input: | + { + "authentication": "primary_password", + "password": "password" + } + output: + result: | + { + "action": { + "type": "finished", + "data": { + "finish_redirect_uri": "[[string]]" + } + } + } + + # Exchange the authorization code for tokens + - name: exchange_code + action: oauth_exchange_code + oauth_exchange_code_code_verifier: "{{ .steps.oauth_setup.result.code_verifier }}" + oauth_exchange_code_redirect_uri: "{{ .steps.authenticate.result.action.data.finish_redirect_uri }}" + + # Call the userinfo endpoint with the access token + - name: userinfo + action: http_request + http_request_method: GET + http_request_url: http://{{.AppID}}.authgeare2e.localhost:4000/oauth2/userinfo + http_request_headers: + Authorization: "Bearer {{ .steps.exchange_code.result.access_token }}" + http_output: + http_status: 200 + json_body: | + { + "sub": "[[string]]", + "https://authgear.com/claims/user/identities": [ + { + "type": "login_id", + "created_at": "[[string]]", + "updated_at": "[[string]]", + "login_id_key": "email", + "login_id_type": "email" + }, + { + "type": "login_id", + "created_at": "[[string]]", + "updated_at": "[[string]]", + "login_id_key": "username", + "login_id_type": "username" + }, + { + "type": "oauth", + "created_at": "[[string]]", + "updated_at": "[[string]]", + "oauth_provider_type": "google", + "oauth_provider_alias": "google" + } + ], + "https://authgear.com/claims/user/authenticators": [ + { + "type": "password", + "kind": "primary", + "created_at": "[[string]]", + "updated_at": "[[string]]" + } + ] + } diff --git a/e2e/tests/oidc/users.json b/e2e/tests/oidc/users.json new file mode 100644 index 00000000000..c75572c8233 --- /dev/null +++ b/e2e/tests/oidc/users.json @@ -0,0 +1,14 @@ +{ + "identifier": "preferred_username", + "records": [ + { + "preferred_username": "e2e_userinfo_user", + "email": "e2e_userinfo_user@example.com", + "name": "UserInfo User", + "password": { + "type": "bcrypt", + "password_hash": "$2y$10$/wLavnCmYGP/zzpw/mR1iOK5y5hGyrEFJtmaIbvFf9VA6l2O4NMKO" + } + } + ] +} diff --git a/pkg/admin/wire_gen.go b/pkg/admin/wire_gen.go index d3c8d08f512..a2189cd372a 100644 --- a/pkg/admin/wire_gen.go +++ b/pkg/admin/wire_gen.go @@ -617,6 +617,7 @@ func newGraphQLHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -1819,6 +1820,7 @@ func newUserImportCreateHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -2276,6 +2278,7 @@ func newUserImportGetHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -2734,6 +2737,7 @@ func newUserExportCreateHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, diff --git a/pkg/api/model/claims.go b/pkg/api/model/claims.go index be35bbe5909..e3987a689e6 100644 --- a/pkg/api/model/claims.go +++ b/pkg/api/model/claims.go @@ -16,6 +16,7 @@ const ( ClaimUserIsVerified ClaimName = "https://authgear.com/claims/user/is_verified" ClaimUserCanReauthenticate ClaimName = "https://authgear.com/claims/user/can_reauthenticate" ClaimAuthenticators ClaimName = "https://authgear.com/claims/user/authenticators" + ClaimIdentities ClaimName = "https://authgear.com/claims/user/identities" ClaimRecoveryCodeEnabled ClaimName = "https://authgear.com/claims/user/recovery_code_enabled" ClaimOAuthAsserted ClaimName = "https://authgear.com/claims/oauth/asserted" ) diff --git a/pkg/api/model/userinfo.go b/pkg/api/model/userinfo.go index e4d4bbf6e85..7bc169bb0db 100644 --- a/pkg/api/model/userinfo.go +++ b/pkg/api/model/userinfo.go @@ -8,3 +8,13 @@ type UserInfoAuthenticator struct { Type AuthenticatorType `json:"type"` Kind AuthenticatorKind `json:"kind"` } + +type UserInfoIdentity struct { + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Type IdentityType `json:"type"` + LoginIDKey string `json:"login_id_key,omitempty"` + LoginIDType LoginIDKeyType `json:"login_id_type,omitempty"` + OAuthProviderType string `json:"oauth_provider_type,omitempty"` + OAuthProviderAlias string `json:"oauth_provider_alias,omitempty"` +} diff --git a/pkg/auth/wire_gen.go b/pkg/auth/wire_gen.go index 0644f96cd3e..b863fd5426a 100644 --- a/pkg/auth/wire_gen.go +++ b/pkg/auth/wire_gen.go @@ -612,6 +612,7 @@ func newOAuthAuthorizeHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } remoteIP := deps.ProvideRemoteIP(request, trustProxy) userAgentString := deps.ProvideUserAgentString(request) @@ -1595,6 +1596,7 @@ func newOAuthConsentHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } remoteIP := deps.ProvideRemoteIP(request, trustProxy) userAgentString := deps.ProvideUserAgentString(request) @@ -2697,6 +2699,7 @@ func newOAuthTokenHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } httpRequestURL := httputil.GetRequestURL(request, httpProto, httpHost) sqlBuilder := appdb.NewSQLBuilder(databaseCredentials) @@ -3908,6 +3911,7 @@ func newOAuthRevokeHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -4308,6 +4312,7 @@ func newOAuthJWKSHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } remoteIP := deps.ProvideRemoteIP(request, trustProxy) userAgentString := deps.ProvideUserAgentString(request) @@ -5160,6 +5165,7 @@ func newOAuthUserInfoHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } remoteIP := deps.ProvideRemoteIP(request, trustProxy) userAgentString := deps.ProvideUserAgentString(request) @@ -6190,6 +6196,7 @@ func newOAuthEndSessionHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -6656,6 +6663,7 @@ func newOAuthAppSessionTokenHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } httpRequestURL := httputil.GetRequestURL(request, httpProto, httpHost) sqlBuilder := appdb.NewSQLBuilder(databaseCredentials) @@ -7833,6 +7841,7 @@ func newAPIAnonymousUserSignupHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -8896,6 +8905,7 @@ func newAPIAnonymousUserPromotionCodeHandler(p *deps.RequestProvider) http.Handl RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -9937,6 +9947,7 @@ func newAPIPresignImagesUploadHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -10463,6 +10474,7 @@ func newWebAppAuthflowV2VerifyBotProtectionHandler(p *deps.RequestProvider) http RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -11661,6 +11673,7 @@ func newWebAppAuthflowV2SelectAccountHandler(p *deps.RequestProvider) http.Handl RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -12734,6 +12747,7 @@ func newWebAppAuthflowV2SSOCallbackHandler(p *deps.RequestProvider) http.Handler RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -13956,6 +13970,7 @@ func newWechatCallbackHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -15002,6 +15017,7 @@ func newWebAppAuthflowV2VerifyLoginLinkOTPHandler(p *deps.RequestProvider) http. RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -16124,6 +16140,7 @@ func newWebAppAuthflowV2SettingsHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -17229,6 +17246,7 @@ func newWebAppAuthflowV2SettingsProfileEditHandler(p *deps.RequestProvider) http RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -18330,6 +18348,7 @@ func newWebAppAuthflowV2SettingsBiometricHandler(p *deps.RequestProvider) http.H RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -19446,6 +19465,7 @@ func newWebAppAuthflowV2SettingsMFAHandler(p *deps.RequestProvider) http.Handler RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -20534,6 +20554,7 @@ func newWebAppAuthflowV2SettingsMFAViewRecoveryCodeHandler(p *deps.RequestProvid RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -21649,6 +21670,7 @@ func newWebAppAuthflowV2SettingsMFACreatePasswordHandler(p *deps.RequestProvider RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -22763,6 +22785,7 @@ func newWebAppAuthflowV2SettingsMFAPasswordHandler(p *deps.RequestProvider) http RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -23877,6 +23900,7 @@ func newWebAppAuthflowV2SettingsMFAChangePasswordHandler(p *deps.RequestProvider RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -24980,6 +25004,7 @@ func newWebAppAuthflowV2SettingsTOTPHandler(p *deps.RequestProvider) http.Handle RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -26103,6 +26128,7 @@ func newWebAppAuthflowV2SettingsMFACreateTOTPHandler(p *deps.RequestProvider) ht RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -27217,6 +27243,7 @@ func newWebAppAuthflowV2SettingsMFAEnterTOTPHandler(p *deps.RequestProvider) htt RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -28332,6 +28359,7 @@ func newWebAppAuthflowV2SettingsOOBOTPHandler(p *deps.RequestProvider) http.Hand RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -29456,6 +29484,7 @@ func newWebAppAuthflowV2SettingsMFACreateOOBOTPHandler(p *deps.RequestProvider) RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -30570,6 +30599,7 @@ func newWebAppAuthflowV2SettingsMFAEnterOOBOTPHandler(p *deps.RequestProvider) h RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -31676,6 +31706,7 @@ func newWebAppAuthflowV2SettingsChangePasskeyHandler(p *deps.RequestProvider) ht RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -32812,6 +32843,7 @@ func newWebAppAuthflowV2SettingsSessionsHandler(p *deps.RequestProvider) http.Ha RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -33927,6 +33959,7 @@ func newWebAppAuthflowV2SettingsChangePasswordHandler(p *deps.RequestProvider) h RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -35030,6 +35063,7 @@ func newWebAppAuthflowV2SettingsDeleteAccountHandler(p *deps.RequestProvider) ht RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -36129,6 +36163,7 @@ func newWebAppAuthflowV2SettingsDeleteAccountSuccessHandler(p *deps.RequestProvi RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -37208,6 +37243,7 @@ func newWebAppAuthflowV2SettingsAdvancedSettingsHandler(p *deps.RequestProvider) RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -38283,6 +38319,7 @@ func newWebAppLogoutHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -39455,6 +39492,7 @@ func newWebAppReturnHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -40512,6 +40550,7 @@ func newWebAppAuthflowV2ErrorHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -41692,6 +41731,7 @@ func newWebAppCSRFErrorInstructionHandler(p *deps.RequestProvider) http.Handler RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -42890,6 +42930,7 @@ func newWebAppAuthflowV2NotFoundHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -43981,6 +44022,7 @@ func newWebAppPasskeyCreationOptionsHandler(p *deps.RequestProvider) http.Handle RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -44997,6 +45039,7 @@ func newWebAppPasskeyRequestOptionsHandler(p *deps.RequestProvider) http.Handler RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -46012,6 +46055,7 @@ func newWebAppFeatureDisabledHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -47087,6 +47131,7 @@ func newWebAppTesterHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -48290,6 +48335,7 @@ func newAPIWorkflowNewHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -49313,6 +49359,7 @@ func newAPIWorkflowGetHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -50292,6 +50339,7 @@ func newAPIWorkflowInputHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -51305,6 +51353,7 @@ func newAPIWorkflowV2Handler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -52330,6 +52379,7 @@ func newAPIAuthenticationFlowV1CreateHandler(p *deps.RequestProvider) http.Handl RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -53431,6 +53481,7 @@ func newAPIAuthenticationFlowV1InputHandler(p *deps.RequestProvider) http.Handle RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -54500,6 +54551,7 @@ func newAPIAuthenticationFlowV1GetHandler(p *deps.RequestProvider) http.Handler RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -55601,6 +55653,7 @@ func newAPIAccountManagementV1IdentificationHandler(p *deps.RequestProvider) htt RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -56507,6 +56560,7 @@ func newAPIAccountManagementV1IdentificationOAuthHandler(p *deps.RequestProvider RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -57439,6 +57493,7 @@ func newWebAppAuthflowV2LoginHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -58643,6 +58698,7 @@ func newWebAppAuthflowV2SignupHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -59840,6 +59896,7 @@ func newWebAppAuthflowV2PromoteHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -61027,6 +61084,7 @@ func newWebAppAuthflowV2EnterPasswordHandler(p *deps.RequestProvider) http.Handl RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -62211,6 +62269,7 @@ func newWebAppAuthflowV2EnterOOBOTPHandler(p *deps.RequestProvider) http.Handler RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -63399,6 +63458,7 @@ func newWebAppAuthflowV2CreatePasswordHandler(p *deps.RequestProvider) http.Hand RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -64585,6 +64645,7 @@ func newWebAppAuthflowV2EnterTOTPHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -65769,6 +65830,7 @@ func newWebAppAuthflowV2SetupTOTPHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -66949,6 +67011,7 @@ func newWebAppAuthflowV2ViewRecoveryCodeHandler(p *deps.RequestProvider) http.Ha RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -68129,6 +68192,7 @@ func newWebAppAuthflowV2OOBOTPLinkHandler(p *deps.RequestProvider) http.Handler RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -69314,6 +69378,7 @@ func newWebAppAuthflowV2ChangePasswordHandler(p *deps.RequestProvider) http.Hand RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -70500,6 +70565,7 @@ func newWebAppAuthflowV2ChangePasswordSuccessHandler(p *deps.RequestProvider) ht RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -71680,6 +71746,7 @@ func newWebAppAuthflowV2UsePasskeyHandler(p *deps.RequestProvider) http.Handler RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -72864,6 +72931,7 @@ func newWebAppAuthflowV2PromptCreatePasskeyHandler(p *deps.RequestProvider) http RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -74044,6 +74112,7 @@ func newWebAppAuthflowV2EnterRecoveryCodeHandler(p *deps.RequestProvider) http.H RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -75224,6 +75293,7 @@ func newWebAppAuthflowV2SetupOOBOTPHandler(p *deps.RequestProvider) http.Handler RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -76404,6 +76474,7 @@ func newWebAppAuthflowV2TerminateOtherSessionsHandler(p *deps.RequestProvider) h RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -77584,6 +77655,7 @@ func newWebAppAuthflowV2ForgotPasswordHandler(p *deps.RequestProvider) http.Hand RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -78771,6 +78843,7 @@ func newWebAppAuthflowV2ForgotPasswordOTPHandler(p *deps.RequestProvider) http.H RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -79953,6 +80026,7 @@ func newWebAppAuthflowV2ForgotPasswordLinkSentHandler(p *deps.RequestProvider) h RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -81134,6 +81208,7 @@ func newWebAppAuthflowV2ReauthHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -82331,6 +82406,7 @@ func newWebAppAuthflowV2ResetPasswordHandler(p *deps.RequestProvider) http.Handl RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -83587,6 +83663,7 @@ func newWebAppAuthflowV2ResetPasswordSuccessHandler(p *deps.RequestProvider) htt RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -85121,6 +85198,7 @@ func newWebAppAuthflowV2OAuthProviderDemoCredentialHandler(p *deps.RequestProvid RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -86301,6 +86379,7 @@ func newWebAppAuthflowV2FinishFlowHandler(p *deps.RequestProvider) http.Handler RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -87481,6 +87560,7 @@ func newWebAppAuthflowV2AccountLinkingHandler(p *deps.RequestProvider) http.Hand RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -88779,6 +88859,7 @@ func newWebAppAuthflowV2WechatHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -89961,6 +90042,7 @@ func newWebAppAuthflowV2LDAPLoginHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -91029,6 +91111,7 @@ func newSAMLMetadataHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } remoteIP := deps.ProvideRemoteIP(request, trustProxy) userAgentString := deps.ProvideUserAgentString(request) @@ -91910,6 +91993,7 @@ func newSAMLLoginHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } remoteIP := deps.ProvideRemoteIP(request, trustProxy) userAgentString := deps.ProvideUserAgentString(request) @@ -92821,6 +92905,7 @@ func newSAMLLoginFinishHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } remoteIP := deps.ProvideRemoteIP(request, trustProxy) userAgentString := deps.ProvideUserAgentString(request) @@ -93725,6 +93810,7 @@ func newSAMLLogoutHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } remoteIP := deps.ProvideRemoteIP(request, trustProxy) userAgentString := deps.ProvideUserAgentString(request) @@ -94763,6 +94849,7 @@ func newWebAppAuthflowV2SettingsProfile(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -95850,6 +95937,7 @@ func newWebAppAuthflowV2SettingsIdentityAddEmailHandler(p *deps.RequestProvider) RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -96952,6 +97040,7 @@ func newWebAppAuthflowV2SettingsIdentityEditEmailHandler(p *deps.RequestProvider RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -98056,6 +98145,7 @@ func newWebAppAuthflowV2SettingsIdentityListEmailHandler(p *deps.RequestProvider RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -99146,6 +99236,7 @@ func newWebAppAuthflowV2SettingsIdentityVerifyEmailHandler(p *deps.RequestProvid RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -100253,6 +100344,7 @@ func newWebAppAuthflowV2SettingsIdentityViewEmailHandler(p *deps.RequestProvider RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -101366,6 +101458,7 @@ func newWebAppAuthflowV2SettingsIdentityChangePrimaryEmailHandler(p *deps.Reques RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -102461,6 +102554,7 @@ func newWebAppAuthflowV2SettingsIdentityAddPhoneHandler(p *deps.RequestProvider) RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -103564,6 +103658,7 @@ func newWebAppAuthflowV2SettingsIdentityEditPhoneHandler(p *deps.RequestProvider RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -104669,6 +104764,7 @@ func newWebAppAuthflowV2SettingsIdentityListPhoneHandler(p *deps.RequestProvider RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -105759,6 +105855,7 @@ func newWebAppAuthflowV2SettingsIdentityViewPhoneHandler(p *deps.RequestProvider RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -106867,6 +106964,7 @@ func newWebAppAuthflowV2SettingsIdentityChangePrimaryPhoneHandler(p *deps.Reques RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -107962,6 +108060,7 @@ func newWebAppAuthflowV2SettingsIdentityVerifyPhoneHandler(p *deps.RequestProvid RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -109069,6 +109168,7 @@ func newWebAppAuthflowV2SettingsIdentityListUsernameHandler(p *deps.RequestProvi RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -110103,6 +110203,7 @@ func newWebAppAuthflowV2SettingsIdentityNewUsernameHandler(p *deps.RequestProvid RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -111206,6 +111307,7 @@ func newWebAppAuthflowV2SettingsIdentityViewUsernameHandler(p *deps.RequestProvi RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -112312,6 +112414,7 @@ func newWebAppAuthflowV2SettingsIdentityEditUsernameHandler(p *deps.RequestProvi RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -113460,6 +113563,7 @@ func newWebAppAuthflowV2SettingsIdentityListOAuthHandler(p *deps.RequestProvider RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -115076,6 +115180,7 @@ func newSessionMiddleware(p *deps.RequestProvider) httproute.Middleware { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } httpRequestURL := httputil.GetRequestURL(request, httpProto, httpHost) sqlBuilder := appdb.NewSQLBuilder(databaseCredentials) @@ -116050,6 +116155,7 @@ func newWebAppSessionMiddleware(p *deps.RequestProvider) httproute.Middleware { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -116998,6 +117104,7 @@ func newWebAppUIParamMiddleware(p *deps.RequestProvider) httproute.Middleware { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } remoteIP := deps.ProvideRemoteIP(request, trustProxy) userAgentString := deps.ProvideUserAgentString(request) @@ -118032,6 +118139,7 @@ func newSettingsSubRoutesMiddleware(p *deps.RequestProvider) httproute.Middlewar RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -118962,6 +119070,7 @@ func newAuthenticationFlowRateLimitMiddleware(p *deps.RequestProvider) httproute RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, @@ -119395,6 +119504,7 @@ func newAccountManagementRateLimitMiddleware(p *deps.RequestProvider) httproute. RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, diff --git a/pkg/lib/deps/deps_common.go b/pkg/lib/deps/deps_common.go index 5a9abc12cd9..08fe479085d 100644 --- a/pkg/lib/deps/deps_common.go +++ b/pkg/lib/deps/deps_common.go @@ -339,6 +339,7 @@ var CommonDependencySet = wire.NewSet( wire.Bind(new(featurestdattrs.IdentityService), new(*identityservice.Service)), wire.Bind(new(featurepasskey.IdentityService), new(*identityservice.Service)), wire.Bind(new(forgotpassword.IdentityService), new(*identityservice.Service)), + wire.Bind(new(userinfo.UserInfoIdentityService), new(*identityservice.Service)), wire.Bind(new(oauthhandler.PromotionCodeStore), new(*identityanonymous.StoreRedis)), wire.Bind(new(oauthhandler.AnonymousIdentityProvider), new(*identityanonymous.Provider)), diff --git a/pkg/lib/oauth/oidc/id_token.go b/pkg/lib/oauth/oidc/id_token.go index d2d234943cb..d35ee1a0ad4 100644 --- a/pkg/lib/oauth/oidc/id_token.go +++ b/pkg/lib/oauth/oidc/id_token.go @@ -296,6 +296,9 @@ func (ti *IDTokenIssuer) GetUserInfo(ctx context.Context, userID string, clientL if isClaimAllowed(string(model.ClaimAuthenticators)) { out[string(model.ClaimAuthenticators)] = userInfo.Authenticators } + if isClaimAllowed(string(model.ClaimIdentities)) { + out[string(model.ClaimIdentities)] = userInfo.Identities + } if isClaimAllowed(string(model.ClaimRecoveryCodeEnabled)) { out[string(model.ClaimRecoveryCodeEnabled)] = userInfo.RecoveryCodeEnabled } diff --git a/pkg/lib/oauth/oidc/id_token_test.go b/pkg/lib/oauth/oidc/id_token_test.go index d5a8e55dc1f..8d7bc2e4fc4 100644 --- a/pkg/lib/oauth/oidc/id_token_test.go +++ b/pkg/lib/oauth/oidc/id_token_test.go @@ -253,6 +253,10 @@ func TestIDTokenIssuer_GetUserInfo(t *testing.T) { Kind: model.AuthenticatorKindPrimary, }, }, + Identities: []model.UserInfoIdentity{ + {CreatedAt: createdAt, UpdatedAt: updatedAt, Type: model.IdentityTypeOAuth, OAuthProviderType: "google", OAuthProviderAlias: "google"}, + {CreatedAt: createdAt, UpdatedAt: updatedAt, Type: model.IdentityTypeLoginID, LoginIDKey: "email", LoginIDType: model.LoginIDKeyTypeEmail}, + }, }, nil, ) @@ -265,7 +269,7 @@ func TestIDTokenIssuer_GetUserInfo(t *testing.T) { ClientID: "client-id", ApplicationType: config.OAuthClientApplicationTypeSPA, } - client := oauth.ClientClientLike(clientConfig, []string{"openid", "email", oauth.FullUserInfoScope, string(model.ClaimAuthenticators), string(model.ClaimPhoneNumber), string(model.ClaimEmail)}) + client := oauth.ClientClientLike(clientConfig, []string{"openid", "email", oauth.FullUserInfoScope, string(model.ClaimAuthenticators), string(model.ClaimIdentities), string(model.ClaimPhoneNumber), string(model.ClaimEmail)}) userInfo, err := issuer.GetUserInfo(context.Background(), "user-id", client) So(err, ShouldBeNil) @@ -298,6 +302,22 @@ func TestIDTokenIssuer_GetUserInfo(t *testing.T) { } ], "https://authgear.com/claims/user/can_reauthenticate": true, + "https://authgear.com/claims/user/identities": [ + { + "created_at": "2019-12-31T23:00:00Z", + "updated_at": "2019-12-31T23:30:00Z", + "type": "oauth", + "oauth_provider_type": "google", + "oauth_provider_alias": "google" + }, + { + "created_at": "2019-12-31T23:00:00Z", + "updated_at": "2019-12-31T23:30:00Z", + "type": "login_id", + "login_id_key": "email", + "login_id_type": "email" + } + ], "https://authgear.com/claims/user/is_anonymous": false, "https://authgear.com/claims/user/is_verified": true, "https://authgear.com/claims/user/recovery_code_enabled": true, @@ -340,6 +360,9 @@ func TestGetUserInfo(t *testing.T) { Kind: model.AuthenticatorKindPrimary, }, }, + Identities: []model.UserInfoIdentity{ + {CreatedAt: now, UpdatedAt: now, Type: model.IdentityTypeOAuth, OAuthProviderType: "google", OAuthProviderAlias: "google"}, + }, RecoveryCodeEnabled: true, }, nil, @@ -353,7 +376,7 @@ func TestGetUserInfo(t *testing.T) { client := &config.OAuthClientConfig{ ClientID: "client-id", } - scopes := []string{"openid", "email", "https://authgear.com/scopes/full-userinfo"} + scopes := []string{"openid", "email", "https://authgear.com/scopes/full-userinfo", string(model.ClaimIdentities)} clientLike := oauth.ClientClientLike(client, scopes) clientLike.PIIAllowedInIDToken = true @@ -374,5 +397,8 @@ func TestGetUserInfo(t *testing.T) { Kind: model.AuthenticatorKindPrimary, }, }) + So(userInfo[string(model.ClaimIdentities)], ShouldResemble, []model.UserInfoIdentity{ + {CreatedAt: now, UpdatedAt: now, Type: model.IdentityTypeOAuth, OAuthProviderType: "google", OAuthProviderAlias: "google"}, + }) }) } diff --git a/pkg/lib/userinfo/userinfo.go b/pkg/lib/userinfo/userinfo.go index 78f50fa1a5e..4103fca80f8 100644 --- a/pkg/lib/userinfo/userinfo.go +++ b/pkg/lib/userinfo/userinfo.go @@ -12,6 +12,7 @@ import ( "github.com/authgear/authgear-server/pkg/api/model" "github.com/authgear/authgear-server/pkg/lib/authn/authenticator" + "github.com/authgear/authgear-server/pkg/lib/authn/identity" "github.com/authgear/authgear-server/pkg/lib/authn/mfa" "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/lib/infra/redis" @@ -35,6 +36,7 @@ type UserInfo struct { AccountAccountStaleFrom *time.Time `json:"account_status_stale_from,omitempty"` EffectiveRoleKeys []string `json:"effective_role_keys"` Authenticators []model.UserInfoAuthenticator `json:"authenticators"` + Identities []model.UserInfoIdentity `json:"identities"` RecoveryCodeEnabled bool `json:"recovery_code_enabled"` } @@ -50,6 +52,10 @@ type UserInfoMFAService interface { ListRecoveryCodes(ctx context.Context, userID string) ([]*mfa.RecoveryCode, error) } +type UserInfoIdentityService interface { + ListByUser(ctx context.Context, userID string) ([]*identity.Info, error) +} + type UserQueries interface { Get(ctx context.Context, id string, role accesscontrol.Role) (*model.User, error) } @@ -63,6 +69,7 @@ type UserInfoService struct { RolesAndGroupsQueries RolesAndGroupsQueries AuthenticatorService UserInfoAuthenticatorService MFAService UserInfoMFAService + IdentityService UserInfoIdentityService } func (s *UserInfoService) GetUserInfoGreatest(ctx context.Context, userID string) (*UserInfo, error) { @@ -138,6 +145,29 @@ func (s *UserInfoService) getUserInfoFromDatabase(ctx context.Context, userID st userinfoAuthens = append(userinfoAuthens, userinfoAuthen) } + identityInfos, err := s.IdentityService.ListByUser(ctx, userID) + if err != nil { + return nil, err + } + + userinfoIdentities := []model.UserInfoIdentity{} + for _, info := range identityInfos { + uiIdentity := model.UserInfoIdentity{ + CreatedAt: info.CreatedAt, + UpdatedAt: info.UpdatedAt, + Type: info.Type, + } + if info.Type == model.IdentityTypeLoginID && info.LoginID != nil { + uiIdentity.LoginIDKey = info.LoginID.LoginIDKey + uiIdentity.LoginIDType = info.LoginID.LoginIDType + } + if info.Type == model.IdentityTypeOAuth && info.OAuth != nil { + uiIdentity.OAuthProviderType = info.OAuth.ProviderID.Type + uiIdentity.OAuthProviderAlias = info.OAuth.ProviderAlias + } + userinfoIdentities = append(userinfoIdentities, uiIdentity) + } + recoveryCodes, err := s.MFAService.ListRecoveryCodes(ctx, userID) if err != nil { return nil, err @@ -148,6 +178,7 @@ func (s *UserInfoService) getUserInfoFromDatabase(ctx context.Context, userID st AccountAccountStaleFrom: u.AccountStatusStaleFrom, EffectiveRoleKeys: roleKeys, Authenticators: userinfoAuthens, + Identities: userinfoIdentities, RecoveryCodeEnabled: len(recoveryCodes) > 0, }, nil } diff --git a/pkg/lib/userinfo/userinfo_mock_test.go b/pkg/lib/userinfo/userinfo_mock_test.go index 5892ee39799..32e0993010b 100644 --- a/pkg/lib/userinfo/userinfo_mock_test.go +++ b/pkg/lib/userinfo/userinfo_mock_test.go @@ -10,6 +10,7 @@ import ( model "github.com/authgear/authgear-server/pkg/api/model" authenticator "github.com/authgear/authgear-server/pkg/lib/authn/authenticator" + identity "github.com/authgear/authgear-server/pkg/lib/authn/identity" mfa "github.com/authgear/authgear-server/pkg/lib/authn/mfa" accesscontrol "github.com/authgear/authgear-server/pkg/util/accesscontrol" gomock "github.com/golang/mock/gomock" @@ -134,6 +135,44 @@ func (mr *MockUserInfoMFAServiceMockRecorder) ListRecoveryCodes(ctx, userID inte return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRecoveryCodes", reflect.TypeOf((*MockUserInfoMFAService)(nil).ListRecoveryCodes), ctx, userID) } +// MockUserInfoIdentityService is a mock of UserInfoIdentityService interface. +type MockUserInfoIdentityService struct { + ctrl *gomock.Controller + recorder *MockUserInfoIdentityServiceMockRecorder +} + +// MockUserInfoIdentityServiceMockRecorder is the mock recorder for MockUserInfoIdentityService. +type MockUserInfoIdentityServiceMockRecorder struct { + mock *MockUserInfoIdentityService +} + +// NewMockUserInfoIdentityService creates a new mock instance. +func NewMockUserInfoIdentityService(ctrl *gomock.Controller) *MockUserInfoIdentityService { + mock := &MockUserInfoIdentityService{ctrl: ctrl} + mock.recorder = &MockUserInfoIdentityServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockUserInfoIdentityService) EXPECT() *MockUserInfoIdentityServiceMockRecorder { + return m.recorder +} + +// ListByUser mocks base method. +func (m *MockUserInfoIdentityService) ListByUser(ctx context.Context, userID string) ([]*identity.Info, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListByUser", ctx, userID) + ret0, _ := ret[0].([]*identity.Info) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListByUser indicates an expected call of ListByUser. +func (mr *MockUserInfoIdentityServiceMockRecorder) ListByUser(ctx, userID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByUser", reflect.TypeOf((*MockUserInfoIdentityService)(nil).ListByUser), ctx, userID) +} + // MockUserQueries is a mock of UserQueries interface. type MockUserQueries struct { ctrl *gomock.Controller diff --git a/pkg/lib/userinfo/userinfo_test.go b/pkg/lib/userinfo/userinfo_test.go index 818c0586fc0..f7faaf1209a 100644 --- a/pkg/lib/userinfo/userinfo_test.go +++ b/pkg/lib/userinfo/userinfo_test.go @@ -13,6 +13,7 @@ import ( "github.com/authgear/authgear-server/pkg/api/model" "github.com/authgear/authgear-server/pkg/lib/authn/authenticator" + "github.com/authgear/authgear-server/pkg/lib/authn/identity" "github.com/authgear/authgear-server/pkg/lib/authn/mfa" "github.com/authgear/authgear-server/pkg/lib/config" @@ -89,6 +90,7 @@ func TestGetUserInfoBearer(t *testing.T) { rolesAndGroupsQueries := NewMockRolesAndGroupsQueries(ctrl) authenticatorService := NewMockUserInfoAuthenticatorService(ctrl) mfaService := NewMockUserInfoMFAService(ctrl) + identityService := NewMockUserInfoIdentityService(ctrl) now := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) createdAt := now.Add(-1 * time.Hour) @@ -133,9 +135,30 @@ func TestGetUserInfoBearer(t *testing.T) { }, } + idens := []*identity.Info{ + { + CreatedAt: createdAt, + UpdatedAt: updatedAt, + Type: model.IdentityTypeLoginID, + LoginID: &identity.LoginID{ + LoginIDKey: "email", + LoginIDType: model.LoginIDKeyTypeEmail, + }, + }, + { + CreatedAt: createdAt, + UpdatedAt: updatedAt, + Type: model.IdentityTypeOAuth, + OAuth: &identity.OAuth{ + ProviderAlias: "google", + }, + }, + } + userQueries.EXPECT().Get(gomock.Any(), "user-id", config.RoleBearer).Return(user, nil) rolesAndGroupsQueries.EXPECT().ListEffectiveRolesByUserID(gomock.Any(), "user-id").Return(roles, nil) authenticatorService.EXPECT().List(gomock.Any(), "user-id", gomock.Any()).Return(authns, nil) + identityService.EXPECT().ListByUser(gomock.Any(), "user-id").Return(idens, nil) mfaService.EXPECT().ListRecoveryCodes(gomock.Any(), "user-id").Return([]*mfa.RecoveryCode{ { Code: "some-code", @@ -150,6 +173,7 @@ func TestGetUserInfoBearer(t *testing.T) { RolesAndGroupsQueries: rolesAndGroupsQueries, AuthenticatorService: authenticatorService, MFAService: mfaService, + IdentityService: identityService, AuthenticationConfig: &config.AuthenticationConfig{ PrimaryAuthenticators: &[]model.AuthenticatorType{ model.AuthenticatorTypePassword, @@ -193,6 +217,10 @@ func TestGetUserInfoBearer(t *testing.T) { Kind: model.AuthenticatorKindSecondary, }, }, + Identities: []model.UserInfoIdentity{ + {CreatedAt: createdAt, UpdatedAt: updatedAt, Type: model.IdentityTypeLoginID, LoginIDKey: "email", LoginIDType: model.LoginIDKeyTypeEmail}, + {CreatedAt: createdAt, UpdatedAt: updatedAt, Type: model.IdentityTypeOAuth, OAuthProviderAlias: "google"}, + }, RecoveryCodeEnabled: true, }) }) diff --git a/pkg/redisqueue/wire_gen.go b/pkg/redisqueue/wire_gen.go index 8a25eac6663..c4d8befedb1 100644 --- a/pkg/redisqueue/wire_gen.go +++ b/pkg/redisqueue/wire_gen.go @@ -485,6 +485,7 @@ func newUserImportService(ctx context.Context, p *deps.AppProvider) *userimport. RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } userinfoSink := &userinfo.Sink{ UserInfoService: userInfoService, diff --git a/pkg/resolver/wire_gen.go b/pkg/resolver/wire_gen.go index 36e7a98e546..e5338a2edc9 100644 --- a/pkg/resolver/wire_gen.go +++ b/pkg/resolver/wire_gen.go @@ -516,6 +516,7 @@ func newSessionMiddleware(p *deps.RequestProvider) httproute.Middleware { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } httpRequestURL := httputil.GetRequestURL(request, httpProto, httpHost) sqlBuilder := appdb.NewSQLBuilder(databaseCredentials) @@ -1344,6 +1345,7 @@ func newSessionResolveHandler(p *deps.RequestProvider) http.Handler { RolesAndGroupsQueries: queries, AuthenticatorService: readOnlyService, MFAService: mfaReadOnlyService, + IdentityService: serviceService, } resolveHandler := &handler.ResolveHandler{ Database: handle,