Skip to content

add custom oidc connector#2708

Merged
p-hoffmann merged 11 commits into
developfrom
p-hoffmann/physionet
Jun 19, 2026
Merged

add custom oidc connector#2708
p-hoffmann merged 11 commits into
developfrom
p-hoffmann/physionet

Conversation

@p-hoffmann

Copy link
Copy Markdown
Member

Merge Checklist

Please cross check this list if additions / modifications needs to be done on top of your core changes and tick them off. Reviewer can as well glance through and help the developer if something is missed out.

  • Automated Tests (Jasmine integration tests, Unit tests, and/or Performance tests)
  • Updated Manual tests / Demo Config
  • Documentation (Application guide, Admin guide, Markdown, Readme and/or Wiki)
  • Verified that local development environment is working with latest changes (integrated with latest develop branch)
  • following best practices in code review doc

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds PhysioNet OIDC federation support by introducing a custom Logto connector that surfaces upstream access/refresh tokens into Logto-issued JWT claims, and by adding backend auto-provisioning + entitlements-based role reconciliation to reduce first-login races and enable PhysioNet-driven authorization.

Changes:

  • Add a new @d2e/connector-physionet-oidc package (fork of Logto OIDC connector) plus a patched OIDC connector build artifact, both exposing upstream tokens via globalThis.*tokenMap.
  • Add alp-usermgmt auto-provisioning and PhysioNet entitlements sync (with new env vars), plus retry/backoff logic to reduce first-login race failures.
  • Update Logto post-init and Portal UI routing/login flows to support social sign-in targets and avoid UX delays/retry failures.

Reviewed changes

Copilot reviewed 31 out of 36 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
services/alp-logto/post-init/src/main.ts Adjust Logto sign-in experience + connector target selection and add env override for social sign-in targets.
services/alp-logto/connector-physionet-oidc/tsup.config.ts New tsup build config for the PhysioNet connector package.
services/alp-logto/connector-physionet-oidc/tsconfig.json New TS config for the PhysioNet connector package.
services/alp-logto/connector-physionet-oidc/src/utils.ts Token endpoint exchange helper for the connector.
services/alp-logto/connector-physionet-oidc/src/types.ts Zod guards + config types and scope post-processing (openid).
services/alp-logto/connector-physionet-oidc/src/types.test.ts Unit tests for scope post-processing.
services/alp-logto/connector-physionet-oidc/src/mock.ts Test mock config for connector tests.
services/alp-logto/connector-physionet-oidc/src/index.ts Connector implementation; exposes upstream tokens via globalThis maps.
services/alp-logto/connector-physionet-oidc/src/index.test.ts Tests for authorization URI creation and userinfo flow.
services/alp-logto/connector-physionet-oidc/src/constant.ts Connector metadata (id/target/form items/readme).
services/alp-logto/connector-physionet-oidc/README.md Rationale and usage notes for the fork + token surfacing.
services/alp-logto/connector-physionet-oidc/package.json New workspace package definition and scripts.
services/alp-logto/connector-physionet-oidc/logo.svg Connector logo asset.
services/alp-logto/connector-physionet-oidc/lib/index.js Checked-in build artifact for runtime mounting (currently contains a critical syntax issue).
services/alp-logto/connector-physionet-oidc/lib/index.js.map Source map for the checked-in build artifact.
services/alp-logto/connector-physionet-oidc/CHANGELOG.md Imported upstream changelog for the fork base.
services/alp-logto/connector-physionet-oidc/.gitignore Ignore rules; explicitly keeps lib/ tracked.
services/alp-logto/connector-oidc-patched/lib/index.js Patched OIDC build artifact used to override bundled connector at runtime.
services/alp-logto/connector-oidc-patched/lib/index.js.map Source map for patched OIDC artifact.
plugins/ui/apps/portal/src/containers/shared/NoAccess/NoAccess.tsx Add one-time delayed reload to mitigate first-login provisioning race.
plugins/ui/apps/portal/src/containers/public/PublicRoot.tsx Skip public overview and route directly to login when no OIDC session exists.
plugins/ui/apps/portal/src/containers/auth/oidc/OidcLoginSilent.tsx Add retry/backoff for first-login group fetch + role sync.
plugins/functions/package.json Pass through new alp-usermgmt auto-provision + entitlements env vars (missing one key).
plugins/functions/alp-usermgmt/src/services/index.ts Export new services (AutoProvision + EntitlementsSync).
plugins/functions/alp-usermgmt/src/services/AutoProvisionService.ts New auto-provisioning service, role hook integration, entitlements sync trigger.
plugins/functions/alp-usermgmt/src/services/EntitlementsSyncService.ts New PhysioNet entitlements-driven role reconciliation service.
plugins/functions/alp-usermgmt/src/middlewares/add-user-object-to-req.ts Add M2M token handling and minimal req.user population (needs safer tagging).
plugins/functions/alp-usermgmt/src/middlewares/permitted-user-check.ts Add bypass behavior for “service tokens” (currently unsafe as implemented).
plugins/functions/alp-usermgmt/src/middlewares/permitted-tenant-check.ts Add bypass behavior for “service tokens” (currently unsafe as implemented).
plugins/functions/alp-usermgmt/src/middlewares/grant-roles-by-scopes.ts Integrate auto-provisioning, retry behavior, and entitlements sync; skip M2M provisioning.
plugins/functions/alp-usermgmt/src/env.ts Add new env vars + parsing helper for auto-provision connector allowlist.
plugins/functions/alp-usermgmt/src/api/LogtoAPI.ts Add helpers to fetch user social + SSO identities for connector gating.
plugins/functions/alp-usermgmt/deno.json Add import map entries for new services.
env-vars.md Document new USERMGMT/LOGTO env vars (missing dataset mapping key).
docker-compose.yml Wire new env vars + Logto post-init settings and expose PhysioNet token claims in custom JWT.
docker-compose-local.yml Local dev wiring: physionet host mapping, warmup job, connector override, and sample entitlements env config.
Files not reviewed (2)
  • services/alp-logto/connector-oidc-patched/lib/index.js: Generated file
  • services/alp-logto/connector-physionet-oidc/lib/index.js: Generated file

Comment on lines +29 to +38
name: {
en: "PhysioNet (OIDC)",
"zh-CN": "OIDC"
},
logo: "./logo.svg",
logoDark: null,
description: {
en: OpenID Connect 1.0 federation to PhysioNet, with upstream access/refresh tokens exposed via globalThis.tokenMap so d2e can call PhysioNet APIs on behalf of the user. Originally a fork of the standard Logto OIDC connector. (en) is a simple identity layer on top of the OAuth 2.0 protocol.",
"zh-CN": "OpenID Connect 1.0 \u662F\u57FA\u4E8E OAuth 2.0 \u534F\u8BAE\u7684\u4E00\u4E2A\u7B80\u5355\u8EAB\u4EFD\u5C42\u3002"
},
Comment on lines +177 to +187
private async runEntitlementsSync(userId: string, idpUserId: string, bearerToken: string): Promise<void> {
try {
const entitlementsSync = Container.get(EntitlementsSyncService)
const token = jwt.decode(bearerToken) as jwt.JwtPayload
if (token) {
await entitlementsSync.sync(userId, idpUserId, token)
}
} catch (err) {
this.logger.warn(`[AutoProvision] entitlements sync failed for ${idpUserId}: ${err}; continuing`)
}
}
Comment on lines +32 to +37
// M2M tokens have sub === client_id; skip user lookup but still
// set a minimal req.user so downstream middleware doesn't crash.
if (sub === token.client_id) {
req.user = { userId: '', idpUserId: sub } as ITokenUser
return next()
}
Comment on lines +35 to +40
// Service tokens (e.g. WebAPI internal calls) have empty ctxUserId.
// Permission checks require a resolved user to enforce; without one
// the caller is an already-authenticated service, so pass through.
if (!ctxUserId) {
return next()
}
Comment on lines +30 to +32
if (!ctxUserId) {
return next()
}
Comment thread env-vars.md
Comment on lines +30 to +40
| `USERMGMT__AUTO_PROVISION_ENABLED` | bool | Auto-create a usermgmt.user row on first federated OIDC login (default `false`). |
| `USERMGMT__AUTO_PROVISION_CONNECTORS` | csv | Logto social-connector targets allowed to auto-provision (e.g. `physionet,oidc`). |
| `USERMGMT__AUTO_PROVISION_DEFAULT_TENANT_ID` | uuid | Tenant for the default TENANT_VIEWER group; falls back to `APP__TENANT_ID`. |
| `USERMGMT__AUTO_PROVISION_ROLE_HOOK_URL` | url | Optional. POSTs `{idpUserId,email,connectorId,accessToken}` and merges `{roles:[]}`.|
| `USERMGMT__AUTO_PROVISION_ROLE_HOOK_SECRET` | password | Optional bearer token sent to the role hook. |
| `USERMGMT__AUTO_PROVISION_ROLE_HOOK_TIMEOUT_MS` | number | Role hook abort timeout in ms (default `5000`). |
| `USERMGMT__ENTITLEMENTS_SYNC_ENABLED` | bool | Reconcile STUDY_RESEARCHER groups against the upstream IdP's entitlements view on every login (default `false`). |
| `USERMGMT__ENTITLEMENTS_PHYSIONET_BASE_URL` | url | PhysioNet base URL the entitlements sync calls (e.g. `https://physionet.org`). |
| `USERMGMT__ENTITLEMENTS_TIMEOUT_MS` | number | Entitlements fetch abort timeout in ms (default `10000`). |
| `USERMGMT__ENTITLEMENTS_TOKEN_CLAIM` | string | JWT claim name carrying the upstream access token (default `physionet_access_token`). |
| `LOGTO__SOCIAL_SIGNIN_TARGETS` | csv | Logto social-connector targets to enable on the sign-in screen. Defaults to the target of `LOGTO__CONNECTOR_CONFIG`. |
Comment thread docker-compose.yml
Comment on lines +387 to 397
"USERMGMT__AUTO_PROVISION_ENABLED": "${USERMGMT__AUTO_PROVISION_ENABLED:-false}",
"USERMGMT__AUTO_PROVISION_CONNECTORS": "${USERMGMT__AUTO_PROVISION_CONNECTORS:-}",
"USERMGMT__AUTO_PROVISION_DEFAULT_TENANT_ID": "${USERMGMT__AUTO_PROVISION_DEFAULT_TENANT_ID:-}",
"USERMGMT__AUTO_PROVISION_ROLE_HOOK_URL": "${USERMGMT__AUTO_PROVISION_ROLE_HOOK_URL:-}",
"USERMGMT__AUTO_PROVISION_ROLE_HOOK_SECRET": "${USERMGMT__AUTO_PROVISION_ROLE_HOOK_SECRET:-}",
"USERMGMT__AUTO_PROVISION_ROLE_HOOK_TIMEOUT_MS": "${USERMGMT__AUTO_PROVISION_ROLE_HOOK_TIMEOUT_MS:-5000}",
"USERMGMT__ENTITLEMENTS_SYNC_ENABLED": "${USERMGMT__ENTITLEMENTS_SYNC_ENABLED:-false}",
"USERMGMT__ENTITLEMENTS_PHYSIONET_BASE_URL": "${USERMGMT__ENTITLEMENTS_PHYSIONET_BASE_URL:-}",
"USERMGMT__ENTITLEMENTS_TIMEOUT_MS": "${USERMGMT__ENTITLEMENTS_TIMEOUT_MS:-10000}",
"USERMGMT__ENTITLEMENTS_TOKEN_CLAIM": "${USERMGMT__ENTITLEMENTS_TOKEN_CLAIM:-physionet_access_token}",
"NIFI_MGMT__BASE_URL": "http://${PROJECT_NAME:-d2e}-minerva-nifi-mgmt-svc-1:4444/alp-nifi-api/",
Comment on lines +505 to +506
signInMode: "SignInAndRegister",
socialSignIn: { automaticAccountLinking: true },

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Would this enable the "Register" button for all?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It did. Registration is now opt-in behind a new LOGTO__ENABLE_REGISTRATION flag (default false), applied to both the base and social-connector sign-in experiences — so SignInAndRegister only kicks in when a deployment explicitly enables it. Fixed in d0848c0.

Comment thread docker-compose-local.yml Outdated
Comment on lines +38 to +43
USERMGMT__AUTO_PROVISION_ENABLED: "true"
USERMGMT__AUTO_PROVISION_CONNECTORS: "physionet"
USERMGMT__AUTO_PROVISION_DEFAULT_TENANT_ID: "e0348e4d-2e17-43f2-a3c6-efd752d17c23"
USERMGMT__ENTITLEMENTS_SYNC_ENABLED: "true"
USERMGMT__ENTITLEMENTS_PHYSIONET_BASE_URL: "http://physionet-host:8000"
USERMGMT__ENTITLEMENTS_DATASET_MAPPING: '{"demo":"demoeicu/1.0"}'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

  1. Is this for your local or all developer will have to enable this by default?
  2. These ENV does not seems to be all. Missing HOOK_URL, HOOK_SECRET?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

  1. Made it opt-in — both master switches (USERMGMT__AUTO_PROVISION_ENABLED, USERMGMT__ENTITLEMENTS_SYNC_ENABLED) now default to false, so the PhysioNet flow is off for local dev unless a dev sets them to true (the rest stays pre-wired as env-overridable defaults). Fixed in f7ae567.
  2. Intentional — the role hook is optional (callRoleHook returns early when the URL is unset), and local dev assigns roles via the entitlements sync, not the hook. The HOOK_URL / HOOK_SECRET vars are documented in env-vars.md for deployments that use a hook.

img[alt="app logo"] { height: 40px; margin-bottom: 20px; }
button[name="submit"]{ background: #000080 !important; }`,
signInMode: "SignIn", //Disable user registration At Login screen
signInMode: process.env.LOGTO__CONNECTOR_CONFIG ? "SignInAndRegister" : "SignIn",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LOGTO__CONNECTOR_CONFIG used by Entra & Entra External ID. These 2 connector does not need "Register" button

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed. Decoupled the Register button from LOGTO__CONNECTOR_CONFIG — it is now gated on the new LOGTO__ENABLE_REGISTRATION flag (default false), so Entra / Entra External ID keep a pure sign-in screen. Documented in env-vars.md and wired into docker-compose.yml. Fixed in d0848c0.

signal: controller.signal,
})
if (!response.ok) {
if (response.status === 404) return false

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

is 404 assumed as "no-access"?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, intentionally. A 404 from /oauth/dataset-access/ means PhysioNet has no access record for this user/dataset (or the slug/version is unknown), so we treat it as "no access" and withhold/revoke the researcher role. Other non-OK statuses throw and are treated as transient, so existing roles are kept rather than revoked on a blip. Added a clarifying comment in d0848c0.

Comment thread plugins/functions/alp-usermgmt/src/middlewares/add-user-object-to-req.ts Outdated
const entitlementsSync = Container.get(EntitlementsSyncService)
await entitlementsSync.sync(userId!, sub, token).catch(err => {
logger.warn(`[Entitlements] sync threw for ${sub}: ${err}; keeping existing roles`)
})

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This would be called on every calls. Is this fine?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Gated and fail-soft: sync() returns immediately when USERMGMT__ENTITLEMENTS_SYNC_ENABLED is off, the base URL is unset, or the token has no PhysioNet claim — a no-op for non-PhysioNet deployments. When enabled it makes one upstream call per request to reconcile dataset access, with errors caught so existing roles are kept. We can add short-lived caching later if the per-request cost matters.

- connector-physionet-oidc/lib: fix invalid build artifact (unquoted
  description.en, mismatched zh-CN name) to match src/constant.ts
- AutoProvisionService: strip "Bearer " prefix before jwt.decode so
  entitlements sync and the role-hook payload get the bare token
- usermgmt authz: tag M2M/service tokens with SERVICE_USER_ID sentinel
  and bypass checks only for it; deny (403) unprovisioned end-users with
  empty userId instead of bypassing (privilege-escalation fix)
- post-init: gate the Register button on new LOGTO__ENABLE_REGISTRATION
  flag so Entra / Entra External ID keep a pure sign-in screen
- add USERMGMT__ENTITLEMENTS_DATASET_MAPPING to env-vars.md,
  docker-compose.yml and plugins/functions/package.json
- EntitlementsSyncService: document 404 = no-access handling
@p-hoffmann p-hoffmann added this pull request to the merge queue Jun 19, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jun 19, 2026
@p-hoffmann p-hoffmann added this pull request to the merge queue Jun 19, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jun 19, 2026
pg@8.7.3 depends on pg-protocol ^1.5.0; the 1.15.0 release (2026-06-19)
ships .d.ts using generic Buffer<...>, which @types/node@^18 does not
define, breaking the postinstall tsc with TS2315 'Type Buffer is not
generic'. This broke the d2e Docker Build (d2e-pg-mgmt-init) across all
PRs in the merge queue. Pin via yarn resolutions to 1.14.0 (plain Buffer).
@p-hoffmann p-hoffmann merged commit ba47e4d into develop Jun 19, 2026
71 checks passed
@p-hoffmann p-hoffmann deleted the p-hoffmann/physionet branch June 19, 2026 05:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants