Hands-On Labs
10 labs covering every integration concern. Each tells the business story up front โ expand Technical Details for config, API calls, and implementation guidance.
OIDC Client Setup
E2, E6Business Story: Elevate registers as an OIDC Relying Party with IDHub. This is the "handshake" โ after it's done, Elevate users can sign in with any IDHub identity provider, and Elevate gets back a JWT with organization context, roles, and entitlements.
โ๏ธ Technical Details
OIDC Client Configuration
client_id | elevate-simulator |
grant_type | authorization_code with PKCE (S256) |
redirect_uri | http://localhost:3003/auth/callback |
scopes | openid profile email organizations entitlements |
authorize_endpoint | /api/oidc/{realm}/authorize |
token_endpoint | /api/oidc/{realm}/token |
MGM App Resource
In the MGM App, this OIDC client is managed as a client-application resource in the b2b realm. It defines the scopes available and which organization-level permissions can be requested.
{
"type": "client-application",
"id": "elevate-simulator",
"data": {
"name": "Elevate Simulator",
"grantTypes": ["authorization_code"],
"scopes": ["openid", "profile", "email", "organizations", "entitlements"],
"redirectUris": ["http://localhost:3003/auth/callback"]
}
}Domain-Based Org Resolution
E2, E3, E8
Business Story: When a user enters their email, IDHub matches the domain
to an organization. @berlin-med.de โ Berlin Medical Center. This determines
which login method they see, which org they join, and which roles become available.
Behind the Scenes
Domain resolution runs server-side inside LoginUI via a Next.js Server Action โ no public API endpoint is exposed.
// LoginUI server action (runs on server only)
"use server";
await resolveOrganizationsByDomain(domain, realm);
// โ { matched, orgId, orgName, identityProviderType, autoJoin }โ๏ธ Technical Details
Server Action (replaces former public endpoint)
// Next.js Server Action โ never exposed as HTTP route
import { resolveDomainAction } from "@/app/actions/resolve-domain";
const result = await resolveDomainAction({ domain: "berlin-med.de", realm: "b2b" });
// โ { matched: true, orgId: "berlin-medical-center",
// orgName: "Berlin Medical Center",
// identityProviderType: "OIDC",
// autoJoin: true, autoJoinRole: "member" }org-domain Resource (MGM App)
Each domain mapping is an org-domain resource under the organization:
{
"type": "org-domain",
"id": "berlin-med.de",
"path": "/orgs/berlin-medical-center/domains",
"data": {
"domain": "berlin-med.de",
"verified": true
}
}Resolution Priority
- Exact domain match โ single org โ auto-join
- Multi-org match โ user picks from selector
- No match โ continue without org (or search, if E8 is enabled)
Organization IDP Federation
E2Business Story: Berlin Medical Center has its own Azure AD. Instead of creating separate passwords, their staff click "Sign In" and are seamlessly redirected to their corporate login. After authenticating there, they return to Elevate with full org context โ no extra steps.
Try It โ Federated Login
Sign in as dr.mueller@berlin-med.de (password: test1234). You'll be redirected through Keycloak, then back here with organization context.
โ๏ธ Technical Details
identity-provider Resource
Federation is configured via an identity-provider resource with type federation-oidc:
{
"type": "identity-provider",
"id": "berlin-med-keycloak",
"data": {
"name": "Berlin Medical Center SSO",
"type": "federation-oidc",
"issuer": "http://localhost:8080/realms/berlin-med",
"clientId": "idhub-federation",
"clientSecret": "...",
"scopes": ["openid", "profile", "email"],
"autoCreateAccount": true,
"claimMapping": {
"email": "email",
"given_name": "given_name",
"family_name": "family_name"
}
}
}Flow Sequence
- User enters
dr.mueller@berlin-med.de - LoginUI calls
resolve-domainโ finds Berlin Medical Center + IDP - LoginUI redirects to Keycloak's
/authorizeendpoint - User authenticates with Keycloak
- Keycloak redirects back to LoginUI with auth code
- LoginUI exchanges code, creates/updates IDHub account
- LoginUI issues IDHub token with org context โ redirect to Elevate
Role Templates & Assignment
E3, E4, E5Business Story: Bayer defines roles once โ "Admin", "HCP", "Wholesaler" โ and every organization inherits them. When the first user registers at an org, they become Admin automatically. Subsequent users get a default role. This scales to 8,000+ orgs without per-org configuration.
| Role | Self-Registrable? | Default? | Permissions |
|---|---|---|---|
| admin | No (auto-promoted) | No | All modules + member management |
| hcp | Yes | Yes โ | Portal + Club Bayer |
| wholesaler | Yes | No | Portal + Club Bayer + My-Orders |
| guest | Yes | No | Portal only (read-only dashboard) |
โ๏ธ Technical Details
realm-role-template Resource
{
"type": "realm-role-template",
"id": "hcp",
"data": {
"name": "Healthcare Professional",
"isDefault": true,
"isSelfRegistrable": true,
"clientPermissions": {
"elevate-simulator": ["dashboard:read", "training:read"]
}
}
}Auto-Promote First Member (E3)
Configured in realm-org-settings:
{
"type": "realm-org-settings",
"data": {
"autoPromoteFirstMember": true,
"adminRoleId": "admin"
}
}How Roles Flow into the Token
- User joins org โ assigned default role (or admin if first)
- Role's
clientPermissionsdetermine per-module access - Token includes
organizations[].roleandclient_permissions[]
Attribute Collection & Schemas
E7, E9, E12Business Story: Bayer needs extra information from members โ billing address for wholesalers, specialty for HCPs. Attribute schemas define what data to collect, at what level (realm-wide vs. per-org vs. per-role), and whether it's mandatory. This drives both UX (what forms show) and access control (My-Orders needs an address).
Applies to ALL members
e.g., email, phone
Org-specific overrides
e.g., billingAddress mandatory
Role-specific fields
e.g., wholesaler: shippingAddress
โ๏ธ Technical Details
3-Tier Attribute Schema Inheritance
// realm-org-settings โ memberAttributeSchema (realm-wide)
{
"memberAttributeSchema": {
"fields": {
"phone": { "type": "string", "required": false, "label": "Phone Number" },
"specialty": { "type": "enum", "required": false, "options": ["cardiology", "oncology", "general"] }
}
}
}
// org-level override (e.g., for Munich Pharma)
{
"memberAttributeOverrides": {
"billingAddress": { "type": "address", "required": true }
}
}
// role-level (set in realm-role-template)
{
"roleAttributes": {
"shippingAddress": { "type": "address", "required": true }
}
}Attribute Conditions for Entitlements (E7)
Module access can depend on attributes, not just role:
// In org-client-permission for My-Orders:
{
"attributeConditions": [
{ "field": "billingAddress", "operator": "exists" },
{ "field": "shippingAddress", "operator": "exists" }
]
}Module Entitlements & Permission Gating
E6, E7Business Story: Users sign in once and get access to multiple modules โ Portal, Club Bayer, My-Orders โ based on their role and attributes. A wholesaler with a billing address sees all three. An HCP without an address sees only Portal and Club Bayer. Elevate reads permissions from the JWT โ no extra API calls needed.
Dashboard access for all authenticated members
Training content, requires training:read
Order management, requires orders:create + billing address
โ๏ธ Technical Details
org-client-permission Resource
Defines what permissions each client app can request:
{
"type": "org-client-permission",
"id": "elevate-simulator",
"data": {
"clientApplicationId": "elevate-simulator",
"permissions": ["dashboard:read", "training:read", "orders:create", "orders:read"],
"attributeConditions": {
"orders:create": [
{ "field": "billingAddress", "operator": "exists" }
]
}
}
}JWT Token Structure
After login, the JWT contains computed permissions:
{
"sub": "abc123",
"email": "user@berlin-med.de",
"organizations": [{
"organizationId": "berlin-medical-center",
"organizationName": "Berlin Medical Center",
"role": "wholesaler",
"basePermissions": ["member:read"]
}],
"client_permissions": ["dashboard:read", "training:read", "orders:create"],
"entitlements": {
"portal": { "eligible": true },
"club-bayer": { "eligible": true },
"my-orders": { "eligible": true }
}
}Client-Side Gating Pattern
// In Elevate's frontend:
const token = decodeJWT(accessToken);
const perms = token.client_permissions || [];
// Gate a module
if (perms.includes('orders:create')) {
showMyOrdersModule();
} else {
showUpgradePrompt('Complete your billing address to access My-Orders');
}Member Rules Engine
E11Business Story: Instead of manually assigning roles, Bayer defines rules. "If the user's business type is wholesaler, assign the wholesaler role." These rules run automatically when a member is added or attributes change โ keeping 8,000+ organizations perfectly consistent without admin intervention.
| Rule | Condition | Action |
|---|---|---|
| wholesaler-auto-assign | businessType == "wholesaler" | Assign role: wholesaler |
| hcp-auto-assign | businessType == "hcp" | Assign role: hcp |
| guest-fallback | No other rule matches | Assign role: guest |
โ๏ธ Technical Details
member-rule Resource
{
"type": "member-rule",
"id": "wholesaler-auto-assign",
"data": {
"name": "Auto-assign wholesaler role",
"priority": 100,
"condition": {
"field": "memberAttributes.businessType",
"operator": "eq",
"value": "wholesaler"
},
"action": {
"type": "assign-role",
"roleId": "wholesaler"
}
}
}Rule Evaluation Flow
- Member joins org or attributes change โ trigger evaluation
- Rules sorted by
priority(highest first) - First matching rule wins โ action applied
- If no rule matches, default role used (if configured)
- Role change โ permissions recalculated โ next token reflects change
Where to Configure
Rules are realm-level resources. Manage in: MGM App โ b2b realm โ Member Rules.
Token Structure, Claims & Custom Mappers
E6, E14Business Story: The IDHub JWT is the "passport" that Elevate reads. It contains who the user is, which organization they belong to, what role they have, and what modules they can access โ all in one standard token. No extra API calls needed for basic authorization.
Custom Token Mappers (E14): Beyond the standard OIDC claims, IDHub injects
per-client custom claims configured through the MGM App. For the Elevate Simulator,
the access token includes org_department, b2b_tier, and roles
โ and the aud claim is extended with https://api.elevate.bayer.com.
These are evaluated server-side from account data at every token issuance.
Sign in to see your live JWT and decoded claims.
โ๏ธ Technical Details
Standard OIDC Claims
| Claim | Type | Description |
|---|---|---|
sub | string | IDHub account ID (unique per user) |
email | string | User's email address |
name | string | Full name |
given_name | string | First name |
family_name | string | Last name |
realm | string | IDHub realm (e.g., "b2b") |
org_id | string | Active organization ID |
organizations | array | All memberships with role + permissions |
client_permissions | array | Effective permissions for this client app |
entitlements | object | Module eligibility map |
๐ง Custom Claims from Token Mappers E14
These claims are injected per-client by Token Mappers configured on the client-application resource.
They are evaluated server-side by Profile API's resolve-token-claims task at token issuance time โ
the client never controls what claims appear. Mappers are configured in MGM App โ Client Applications โ Token Mappers tab.
| Claim | Mapper Type | Target | Description |
|---|---|---|---|
org_department | account-attribute | access_token | Reads mixed.department from the user's mixed account |
b2b_tier | hardcoded | access_token | Static value "enterprise" โ identifies this as an enterprise B2B client |
roles | account-tags | both | Account tags filtered by role:* โ emits role tags as an array |
display_name | expression | id_token | Computed: firstName + lastName or falls back to email |
aud (extended) | audience | access_token | Appends https://api.elevate.bayer.com to the audience array |
Supported Mapper Types
| Type | Description |
|---|---|
account-attribute | Reads a dot-path field from the mixed account (e.g., mixed.department) |
hardcoded | Emits a static value (string, number, boolean) for every token |
account-tags | Emits the account's tags (optionally filtered by a glob pattern) |
expression | Evaluates a safe expression โ dot-paths, ternary, comparisons, string methods |
profile-attribute | Reads a field from a specific identity provider profile (e.g., SAP CDC) |
audience | Appends additional audience URIs to the aud claim |
Token Lifecycle
- Access tokens are short-lived (configurable, typically 5โ15 min)
- Refresh tokens enable silent renewal
- Role/permission changes take effect at next token refresh
- Custom claims re-evaluated on every token issuance (always up-to-date)
- Token is signed with RS256 โ verify with JWKS endpoint
- Token size monitored: warning at 4 KB, hard limit at 8 KB
Profile API Integration
E9, E12Business Story: Sometimes Elevate needs more than what's in the JWT โ full profile data, organization details, or to update member attributes. The Profile API (GraphQL) provides this. It's the "deep data layer" behind the token.
Sign in to query the Profile API for your live account data.
โ๏ธ Technical Details
GraphQL Endpoint
Profile API runs on port 4000 (local) or via the proxy at /api/profile.
Example Queries
# Get entitlements for current user
query {
getAccountEntitlements(clientApplicationId: "elevate-simulator") {
accountId
organizations {
organizationId
organizationName
role
basePermissions
clientEntitlements {
clientApplicationId
permissions
features
}
}
}
}
# Get account by profile
query {
getAccountByProfile(provider: "SAP_CDC", profileId: "user123") {
accountId
profiles { provider profileId data }
mixedAccount { email firstName lastName }
}
}Authentication
Profile API requires a valid IDHub access token in the Authorization: Bearer {token} header. The simulator proxies this through /api/profile.
MyAccount & MGM App
E4, E9, E10Business Story: Two companion apps complete the Elevate integration. MyAccount is the user-facing self-service portal โ users update their profile, manage org attributes, and review consent. MGM App is the admin tool โ Bayer admins configure realms, roles, rules, schemas, and organizations.
โ๏ธ Technical Details
MyAccount Context Configuration
MyAccount uses context-based routing. For B2B realm:
// contextMapping.json
{
"b2b": {
"realm": "b2b",
"name": "B2B Portal",
"profileApiUrl": "http://localhost:4000/graphql",
"features": {
"organizations": true,
"memberAttributes": true,
"roleManagement": true
}
}
}
// Access via: http://localhost:3000?context=b2bMGM App โ Key B2B Admin Flows
- Realm โ Organizations: Browse/search the 8K+ org list
- Realm โ Role Templates: Define roles + client permissions
- Realm โ Member Rules: Configure rule-based role assignment
- Realm โ Org Settings: Set realm-wide defaults + overrides
- Realm โ Client Applications: Manage OIDC clients like Elevate