Skip to content

Commit 1d483b6

Browse files
StrehkclaudeCodeRabbitcoderabbitai[bot]
authored
feat: Replace Zitadel with Logto as OIDC identity provider (#415)
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> Co-authored-by: CodeRabbit <[email protected]> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Tade Strehk <[email protected]>
1 parent f2587d1 commit 1d483b6

File tree

28 files changed

+1138
-286
lines changed

28 files changed

+1138
-286
lines changed

.env.example

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,40 @@ DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres
3232
# ───────────────────────────────────────────────────────────────────────────────
3333
# AUTHENTICATION (OIDC)
3434
# ───────────────────────────────────────────────────────────────────────────────
35-
# Supports any OpenID Connect provider (tested with ZITADEL)
35+
# Supports any OpenID Connect provider (recommended: Logto — https://logto.io/)
36+
#
37+
# ┌─── Logto Setup Guide ───────────────────────────────────────────────────────┐
38+
# │ │
39+
# │ 1. Create a "Traditional Web" application in Logto Console │
40+
# │ 2. Set the redirect URI to: <your-domain>/auth/login-callback │
41+
# │ 3. Copy the App ID → PUBLIC_OIDC_CLIENT_ID │
42+
# │ 4. Copy the App Secret → OIDC_CLIENT_SECRET │
43+
# │ │
44+
# │ Custom JWT Claims (required): │
45+
# │ Go to Logto Console → JWT Customizer → User access token and add: │
46+
# │ │
47+
# │ const getCustomJwtClaims = async ({ │
48+
# │ token, context, environmentVariables, api │
49+
# │ }) => { │
50+
# │ return { │
51+
# │ roles: context.user.roles, │
52+
# │ mfa: context.user.mfaVerificationFactors, │
53+
# │ password: context.user.hasPassword, │
54+
# │ sso_identities: context.user.ssoIdentities, │
55+
# │ social_identities: │
56+
# │ Object.keys(context.user.identities) ?? [] │
57+
# │ }; │
58+
# │ } │
59+
# │ │
60+
# │ This exposes user roles, MFA status, and identity provider info │
61+
# │ in the access token so the application can use them for │
62+
# │ authorization and UI display. │
63+
# │ │
64+
# └─────────────────────────────────────────────────────────────────────────────┘
3665

3766
# [REQUIRED] OIDC Discovery URL (usually ends with /.well-known/openid-configuration)
3867
# Local mock: http://localhost:8080/default/.well-known/openid-configuration
39-
# Production: https://your-oidc-provider.com
68+
# Logto: https://<your-tenant>.logto.app/oidc/.well-known/openid-configuration
4069
PUBLIC_OIDC_AUTHORITY=http://localhost:8080/default/.well-known/openid-configuration
4170

4271
# [REQUIRED] OAuth2 Client ID from your OIDC provider
@@ -46,12 +75,34 @@ PUBLIC_OIDC_CLIENT_ID=default
4675
# OIDC_CLIENT_SECRET=your-client-secret
4776

4877
# [REQUIRED] OAuth2 scopes to request
49-
# Must include "openid" and "offline_access" at minimum
50-
OIDC_SCOPES="openid profile offline_access address email family_name gender given_name locale name phone preferred_username urn:zitadel:iam:org:projects:roles urn:zitadel:iam:user:metadata urn:zitadel:iam:org:project:id:zitadel:aud"
51-
52-
# [OPTIONAL] JWT claim path for user roles (ZITADEL-specific format shown)
78+
# These scopes control ID token and userinfo claims, NOT the access token.
79+
# Access token claims are set via the JWT Customizer script above.
80+
#
81+
# openid — required by OIDC spec
82+
# profile — user name, picture, etc.
83+
# offline_access — enables refresh tokens
84+
# email — user email address
85+
# phone — user phone number
86+
# identity — Logto scope: linked social & SSO identities
87+
# role — Logto scope: user roles for authorization
88+
# custom_data — Logto scope: custom user data
89+
OIDC_SCOPES="openid profile offline_access email phone identity role custom_data"
90+
91+
# [OPTIONAL] JWT claim path for user roles
5392
# Used to determine team member permissions
54-
OIDC_ROLE_CLAIM=urn:zitadel:iam:org:project:275671427955294244:roles
93+
# With the custom JWT claims above, roles are available at the "roles" claim
94+
OIDC_ROLE_CLAIM=roles
95+
96+
# [OPTIONAL] Machine-to-machine app credentials for Logto Management API
97+
# Required for user impersonation feature (token exchange via subject tokens)
98+
# Create an M2M app in Logto Console with access to the Management API resource
99+
# OIDC_M2M_CLIENT_ID=your-m2m-app-id
100+
# OIDC_M2M_CLIENT_SECRET=your-m2m-app-secret
101+
# [OPTIONAL] Logto Management API resource indicator
102+
# For Logto Cloud: https://<tenant-id>.logto.app/api
103+
# For self-hosted: check your Logto API Resources settings (e.g. https://default.logto.app/api)
104+
# If not set, derived from PUBLIC_OIDC_AUTHORITY by replacing /oidc with /api
105+
# OIDC_M2M_RESOURCE=https://default.logto.app/api
55106

56107
# ───────────────────────────────────────────────────────────────────────────────
57108
# APPLICATION FEATURES
@@ -64,6 +115,10 @@ OIDC_ROLE_CLAIM=urn:zitadel:iam:org:project:275671427955294244:roles
64115
# [OPTIONAL] Enable global user notes visible to team members (default: false)
65116
PUBLIC_GLOBAL_USER_NOTES_ACTIVE=true
66117

118+
# [OPTIONAL] Show OIDC migration notice before login (default: false)
119+
# TEMPORARY: Enable during identity provider migration to warn users
120+
# PUBLIC_OIDC_MIGRATION_NOTICE=false
121+
67122
# [OPTIONAL] Maximum character length for application motivation text (default: 1200)
68123
PUBLIC_MAX_APPLICATION_TEXT_LENGTH=1200
69124

@@ -195,4 +250,4 @@ SMTP_FROM_NAME=MUNIFY Delegator
195250
# PUBLIC_VERSION=1.0.0
196251

197252
# [AUTO] Git commit SHA
198-
# PUBLIC_SHA=abc123
253+
# PUBLIC_SHA=abc123

.github/actions/setup-bun/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ runs:
66
steps:
77
- uses: oven-sh/setup-bun@v2
88

9-
- uses: actions/cache@v4
9+
- uses: actions/cache@v5
1010
with:
1111
path: |
1212
~/.bun/install/cache

.github/workflows/ci.yml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,38 +18,38 @@ jobs:
1818
format:
1919
runs-on: ubuntu-latest
2020
steps:
21-
- uses: actions/checkout@v4
21+
- uses: actions/checkout@v5
2222
- uses: ./.github/actions/setup-bun
2323
- run: bun run format:check
2424

2525
lint:
2626
runs-on: ubuntu-latest
2727
steps:
28-
- uses: actions/checkout@v4
28+
- uses: actions/checkout@v5
2929
- uses: ./.github/actions/setup-bun
3030
- run: bunx svelte-kit sync
3131
- run: bun run lint
3232

3333
test:
3434
runs-on: ubuntu-latest
3535
steps:
36-
- uses: actions/checkout@v4
36+
- uses: actions/checkout@v5
3737
- uses: ./.github/actions/setup-bun
3838
- run: bunx svelte-kit sync
3939
- run: bun run test
4040

4141
i18n:
4242
runs-on: ubuntu-latest
4343
steps:
44-
- uses: actions/checkout@v4
44+
- uses: actions/checkout@v5
4545
- uses: ./.github/actions/setup-bun
4646
- run: bun run i18n:check
4747
- run: bun run i18n:validate
4848

4949
security:
5050
runs-on: ubuntu-latest
5151
steps:
52-
- uses: actions/checkout@v4
52+
- uses: actions/checkout@v5
5353
- uses: ./.github/actions/setup-bun
5454
- uses: aquasecurity/[email protected]
5555
with:
@@ -67,7 +67,7 @@ jobs:
6767
typecheck:
6868
runs-on: ubuntu-latest
6969
steps:
70-
- uses: actions/checkout@v4
70+
- uses: actions/checkout@v5
7171
- uses: ./.github/actions/setup-bun
7272
- run: bunx svelte-kit sync
7373
- run: bunx prisma generate
@@ -81,7 +81,7 @@ jobs:
8181
env:
8282
NODE_OPTIONS: '--max-old-space-size=8192'
8383
steps:
84-
- uses: actions/checkout@v4
84+
- uses: actions/checkout@v5
8585
- uses: ./.github/actions/setup-bun
8686
- run: bunx svelte-kit sync
8787
- run: bunx prisma generate
@@ -101,7 +101,7 @@ jobs:
101101
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
102102
SENTRY_URL: ${{ secrets.SENTRY_URL }}
103103
steps:
104-
- uses: actions/checkout@v4
104+
- uses: actions/checkout@v5
105105

106106
- uses: docker/setup-buildx-action@v3
107107

@@ -198,7 +198,7 @@ jobs:
198198
needs: [format, lint, test, i18n, security, typecheck]
199199
runs-on: ubuntu-latest
200200
steps:
201-
- uses: actions/checkout@v4
201+
- uses: actions/checkout@v5
202202
with:
203203
fetch-depth: 2
204204

@@ -285,7 +285,7 @@ jobs:
285285
permissions:
286286
contents: write
287287
steps:
288-
- uses: actions/checkout@v4
288+
- uses: actions/checkout@v5
289289

290290
- name: Extract version
291291
id: version

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ MUNify DELEGATOR is a SvelteKit-based application for managing Model United Nati
1212
- **Backend**: Node.js with SvelteKit server routes
1313
- **Database**: PostgreSQL via Prisma ORM
1414
- **GraphQL**: Pothos schema builder with graphql-yoga server, Houdini client
15-
- **Auth**: OpenID Connect (OIDC) - tested with ZITADEL
15+
- **Auth**: OpenID Connect (OIDC) - recommended provider: Logto
1616
- **i18n**: Paraglide-JS for internationalization (default locale: German)
1717
- **Runtime**: Bun (package manager and development runtime)
1818
- **Observability**: OpenTelemetry tracing support
@@ -281,7 +281,7 @@ Required variables (see `.env.example`):
281281
- `PUBLIC_OIDC_AUTHORITY` - OIDC provider URL
282282
- `PUBLIC_OIDC_CLIENT_ID` - OAuth client ID
283283
- `OIDC_SCOPES` - OAuth scopes (must include `openid`)
284-
- `OIDC_ROLE_CLAIM` - JWT claim for roles (ZITADEL format)
284+
- `OIDC_ROLE_CLAIM` - JWT claim for roles
285285
- `CERTIFICATE_SECRET` - Secret for signing participation certificates
286286
- OpenTelemetry vars (optional): `OTEL_ENDPOINT_URL`, `OTEL_SERVICE_NAME`
287287

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ bunx lefthook install
4848

4949
## Deployment
5050

51-
The easiest way to deploy delegator on your own hardware is to use our provided [docker images](https://hub.docker.com/r/deutschemodelunitednations/delegator). You can find an example docker compose file in the [example](./example/) directoy. Please note that delegator relies on an [OIDC](https://auth0.com/intro-to-iam/what-is-openid-connect-oidc) issuer to be connected and properly configured. We recommend [ZITADEL](https://zitadel.com/) but any issuer of your choice will work. There are some additional instructions on this topic to be found in the example compose file.
51+
The easiest way to deploy delegator on your own hardware is to use our provided [docker images](https://hub.docker.com/r/deutschemodelunitednations/delegator). You can find an example Docker Compose file in the [example](./example/) directory. Please note that delegator relies on an [OIDC](https://auth0.com/intro-to-iam/what-is-openid-connect-oidc) issuer to be connected and properly configured. We recommend [Logto](https://logto.io/) but any issuer of your choice will work. There are some additional instructions on this topic to be found in the example compose file.
5252

5353
## FAQ
5454

WARP.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ bun run machine-translate
131131

132132
#### Authentication & Authorization
133133

134-
- **OIDC Integration**: Uses OpenID Connect (designed for ZITADEL but supports any OIDC provider)
134+
- **OIDC Integration**: Uses OpenID Connect (recommended: Logto, but supports any OIDC provider)
135135
- **Context Building**: `src/api/context/context.ts` constructs request context with OIDC data
136136
- **Permission System**: CASL ability-based authorization
137137
- Definitions in `src/api/abilities/entities/`
@@ -221,5 +221,5 @@ Example: `feat(delegation): add nation preference selection`
221221
Use provided Docker images: `deutschemodelunitednations/delegator`
222222

223223
- Example compose file in `example/` directory
224-
- Requires external OIDC provider (ZITADEL recommended)
224+
- Requires external OIDC provider (Logto recommended)
225225
- Environment variables must be configured (see `.env.example`)

dev.docker-compose.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ services:
2929
# "given_name": "Delegator Jr.",
3030
# "preferred_username": "delegatoruser_123",
3131
# "locale": "de",
32-
# "urn:zitadel:iam:org:project:275671427955294244:roles": {"admin": {}}
32+
# "roles": ["admin"]
3333
# }
3434
mockoidc:
3535
image: ghcr.io/navikt/mock-oauth2-server:2.1.10
@@ -143,7 +143,7 @@ services:
143143
- '1025:1025' # SMTP server
144144
- '8025:8025' # Web UI
145145
environment:
146-
MP_SMTP_AUTH_ACCEPT_ANY: 1
146+
MP_SMTP_AUTH: 'mailpit:mailpit'
147147
MP_SMTP_AUTH_ALLOW_INSECURE: 1
148148

149149
# Bugsink - Self-hosted error tracking (Sentry-compatible)

messages/de.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616
"accessFlowSaved": "Zugangsausweis zugewiesen und Anwesenheit erfasst.",
1717
"accountExists": "Konto vorhanden",
1818
"accountHolder": "Kontoinhaber*in",
19+
"accountUpdateSuccessBackupCodes": "Deine Backup-Codes wurden erfolgreich aktualisiert.",
20+
"accountUpdateSuccessEmail": "Deine E-Mail-Adresse wurde erfolgreich aktualisiert.",
21+
"accountUpdateSuccessMfa": "Deine Authenticator-App-Einstellungen wurden erfolgreich aktualisiert.",
22+
"accountUpdateSuccessPasskey": "Deine Passkey-Einstellungen wurden erfolgreich aktualisiert.",
23+
"accountUpdateSuccessPassword": "Dein Passwort wurde erfolgreich geändert.",
24+
"accountUpdateSuccessUsername": "Dein Benutzername wurde erfolgreich aktualisiert.",
1925
"actions": "Aktionen",
2026
"activeConferences": "Aktive Konferenzen",
2127
"activeMembers": "Aktive Mitglieder",
@@ -193,6 +199,7 @@
193199
"authErrorTechnicalTitle": "Anmeldung fehlgeschlagen",
194200
"authErrorTokenExchangeDescription": "Der Anmeldevorgang konnte nicht abgeschlossen werden. Das kann passieren, wenn die Anmeldung zu lange gedauert hat oder ein Netzwerkproblem aufgetreten ist.",
195201
"authErrorTryAgain": "Erneut versuchen",
202+
"authenticatorApp": "Authenticator-App",
196203
"author": "Autor*in",
197204
"authorization": "Berechtigung",
198205
"averageAgeOnlyApplied": "Konferenz-Altersdurchschnitt (nur angemeldete)",
@@ -201,6 +208,7 @@
201208
"backToConferencePapers": "Zurück zu Konferenz-Papieren",
202209
"backToDashboard": "Zurück zum Dashboard",
203210
"backToHome": "Zurück zur Homepage",
211+
"backupCodes": "Backup-Codes",
204212
"badgeDataDescription": "CSV-Exporte für den Druck von Namensschildern. Enthält Teilnehmernamen, Länder und Einwilligungsstatus.",
205213
"badgeDataTitle": "Namensschild-Daten",
206214
"bankName": "Name der Bank",
@@ -319,6 +327,9 @@
319327
"certificateTemplate": "Basis-PDF für das Zertifikat",
320328
"changeAnswer": "Antwort ändern",
321329
"changeDelegationPreferences": "Wünsche anpassen",
330+
"changeEmail": "Ändern",
331+
"changePassword": "Ändern",
332+
"changeUsername": "Ändern",
322333
"changesSuccessful": "Die Änderungen wurden erfolgreich gespeichert.",
323334
"chaseSeedData": "CHASE Seed-Daten",
324335
"checkEmails": "E-Mails prüfen",
@@ -824,6 +835,7 @@
824835
"male": "Männlich",
825836
"malformedPlaceholders": "Textbaustein enthält fehlerhafte Platzhalter. Stelle sicher, dass alle \\{\\{ ein passendes \\}\\} haben.",
826837
"manageConference": "Konferenzeinstellungen und Teilnehmende verwalten",
838+
"managePasskeys": "Verwalten",
827839
"managePreferences": "Einstellungen verwalten",
828840
"manageSnippets": "Textbausteine verwalten",
829841
"markAsProblem": "Als Problem markieren",
@@ -834,6 +846,19 @@
834846
"mediaConsentStatus": "Fotostatus",
835847
"members": "Mitglieder",
836848
"membersPerDelegation": "Plätze pro Delegation",
849+
"migrationNoticeContinue": "Weiter zum Login",
850+
"migrationNoticeFaqEmailPasswordBody": "Melde dich einfach mit deiner bisherigen E-Mail-Adresse und deinem Passwort an. Alles sollte wie gewohnt funktionieren.",
851+
"migrationNoticeFaqEmailPasswordTitle": "Ich habe mich mit E-Mail und Passwort angemeldet \u2014 was muss ich tun?",
852+
"migrationNoticeFaqHelpBody": "Bei Problemen kontaktiere uns bitte unter <a href=\"mailto:{email}\" class=\"link\">{email}</a>.",
853+
"migrationNoticeFaqHelpTitle": "Ich brauche Hilfe \u2014 wen kann ich kontaktieren?",
854+
"migrationNoticeFaqIdentityProviderBody": "Wir haben unseren Identit\u00e4tsanbieter \u2014 das System, das deine Anmeldedaten verwaltet \u2014 von <a href=\"https://zitadel.com\" target=\"_blank\" class=\"link\">Zitadel</a> auf <a href=\"https://logto.io\" target=\"_blank\" class=\"link\">Logto</a> umgestellt. Das verbessert die Sicherheit und Zuverl\u00e4ssigkeit. Deine Kontodaten wurden \u00fcbertragen.",
855+
"migrationNoticeFaqIdentityProviderTitle": "Was hat sich ge\u00e4ndert und warum?",
856+
"migrationNoticeFaqNewUserBody": "Wenn du dich zum ersten Mal registrierst, betrifft dich diese \u00c4nderung nicht. Erstelle einfach auf der n\u00e4chsten Seite ein neues Konto.",
857+
"migrationNoticeFaqNewUserTitle": "Ich bin neu und m\u00f6chte mich registrieren \u2014 was muss ich tun?",
858+
"migrationNoticeFaqSocialBody": "Social-Login-Konten (z. B. Google) konnten nicht automatisch migriert werden. Bitte melde dich stattdessen mit deiner E-Mail-Adresse an. Falls du noch kein Passwort festgelegt hast, nutze die Option \u201ePasswort vergessen\u201c auf der Login-Seite, um eines zu erstellen.",
859+
"migrationNoticeFaqSocialTitle": "Ich habe Google oder einen anderen Social-Login verwendet \u2014 was muss ich tun?",
860+
"migrationNoticeSubtitle": "Wir haben unser Login-System aktualisiert. Bitte lies die folgenden Informationen, bevor du fortf\u00e4hrst.",
861+
"migrationNoticeTitle": "Wichtig: Login-System aktualisiert",
837862
"missingInformation": "Fehlende Informationen",
838863
"motivation": "Motivation",
839864
"myAccount": "Mein Konto",
@@ -1036,6 +1061,7 @@
10361061
"participation": "Teilnahme",
10371062
"participationCount": "Teilnahmen",
10381063
"participationType": "Teilnahmeart",
1064+
"passkeys": "Passkeys",
10391065
"password": "Passwort",
10401066
"pastConferences": "Vergangene Konferenzen",
10411067
"pathDoesNotExist": "Der Pfad {path} konnte nicht gefunden werden ({error})",
@@ -1382,6 +1408,7 @@
13821408
"snippets": "Textbausteine",
13831409
"specialRole": "Spezielle Rolle",
13841410
"specialWishes": "Spezielle Wünsche",
1411+
"ssoIdentities": "Verknüpfte Konten",
13851412
"start": "Start",
13861413
"startAssignment": "Assistent starten",
13871414
"startCamera": "Kamera starten",

0 commit comments

Comments
 (0)