Skip to content

OIDC-oppsett

Denne guiden er for ops-ingeniøren som skal koble Lumi til organisasjonens OIDC-provider i en BYOC-installasjon. Etter at stegene er gjennomført kan dine interne brukere logge inn i dashboardet med samme identitet som de bruker i andre verktøy.

Provider-dekning

Keycloak er referanse-implementasjonen og er testet ende-til-ende mot repoets kode.

Auth0 og Okta-seksjonene under er kildeverifisert mot provider-dokumentasjon, men ikke E2E-verifisert mot en ekte Lumi-installasjon ennå. Behandle dem som provider-spesifikke konfigurasjonsnotater til de er validert med en testet tenant/org, et vellykket login → dashboard → API-kall, og sanerte token-samples fra access-tokenet.

Hvordan Lumi bruker OIDC

Dashboardet gjør en OAuth 2.0 Authorization Code-flyt med PKCE mot providern og lagrer token-paret i en kryptert server-side-sesjon (PostgreSQL). Per request videresender dashboardet access-tokenet (ikke id-tokenet) som Authorization: Bearer ... til Lumi-API-et. API-et validerer signatur, issuer og audience, og leser brukerens grupper fra tokenet.

Groups må være på access-tokenet, ikke bare id-tokenet

Dashboardet forward-er access-tokenet. Provideren må derfor legge gruppeverdiene på access-tokenet. For Keycloak styres dette via group-membership-mapperens access.token.claim: true-flagg; Auth0 og Okta har egne steg lenger ned.

Sentrale token-claims som Lumi leser:

ClaimFormålFeilmodus hvis feil
issMå matche OIDC_ISSUER_URL401, API-loggen skriver OIDC token verification failed
audMå matche OIDC_AUDIENCE401, samme logg-linje som over
azp, client_id eller cidMå matche LUMI_DASHBOARD_CLIENT_ID på endepunkter som bruker ClientAuthorizationPlugin403 Caller is not authorized for this endpoint
subBrukerens unike ID (persisteres)
email (fallback: preferred_username)Vist i dashboardetTom e-postkolonne, ingen hard feil
groups (konfigurerbar via OIDC_GROUPS_CLAIM)Team-mapping og org-adminBruker får teams: [] og isOrgAdmin: false

OidcAuthProvider i Kotlin-koden logger kun generelle verifikasjonsfeil (ikke hvilken claim som mangler) — for å finne rotårsak må du dekode tokenet lokalt og sammenligne claims mot env-varene.

Miljøvariabler

Både dashboard og API trenger sine egne sett. Sett dem før oppstart — API-et feiler startup dersom produksjon-påkrevde variabler mangler.

Dashboard

OIDC_ISSUER_URL, OIDC_CLIENT_ID, OIDC_REDIRECT_URI og LUMI_SESSION_SECRET er påkrevd når AUTH_PROVIDER=oidc. OIDC_CLIENT_SECRET er kun påkrevd for confidential clients; public clients med PKCE lar den stå tom. OIDC_SCOPES er valgfri.

sh
AUTH_PROVIDER=oidc
OIDC_ISSUER_URL=https://keycloak.din-bedrift.no/realms/lumi
OIDC_BROWSER_ISSUER_URL=https://keycloak.din-bedrift.no/realms/lumi  # valgfri; se split-networking nedenfor
OIDC_CLIENT_ID=lumi-dashboard
OIDC_CLIENT_SECRET=<fra Keycloaks Credentials-fane>  # kun confidential clients
OIDC_REDIRECT_URI=https://lumi.din-bedrift.no/auth/callback
OIDC_SCOPES=openid,profile,email  # valgfri; se Entra/custom API-scope nedenfor
LUMI_SESSION_SECRET=<base64-kodet 32+ bytes>

# Session-store: samme PostgreSQL som API-et
DB_HOST=postgres.din-bedrift.no
DB_PORT=5432
DB_DATABASE=lumi
DB_USERNAME=lumi
DB_PASSWORD=<fra secret>

Dashboardet lagrer OIDC-sesjonsparet (access/refresh/id-tokens) kryptert i en sessions-tabell i samme PostgreSQL som API-et. Uten DB_-variablene kan dashboardet ikke persistere sesjoner og login feiler før redirecten kommer tilbake.

OIDC_BROWSER_ISSUER_URL brukes kun når serveren når provideren på en annen URL enn nettleseren (klassisk docker-compose-situasjon der containeren når http://keycloak:8080 men brukeren må sendes til http://localhost:8180). Sett begge like i et vanlig BYOC-oppsett hvor Keycloak er tilgjengelig fra både server og nettleser på samme URL.

OIDC_SCOPES er en komma-separert liste over scopes dashboardet ber om ved login og refresh. La den stå tom for Keycloak-defaulten openid,profile,email. Providers som krever et eksplisitt API-scope i access-tokenet, typisk Entra ID eller scope-gated Okta-claims, bør settes til f.eks. openid,profile,email,api://<api-client-id>/access_as_user eller openid,profile,email,groups. openid må være med for at login fortsatt er en OIDC-flyt; API-scopet må korrespondere med OIDC_AUDIENCE som API-et validerer.

LUMI_SESSION_SECRET krypterer sesjonscookien (AES-256-GCM). Generer én gang per miljø og lagre trygt:

sh
openssl rand -base64 32

Velg public eller confidential client bevisst

Keycloak-klienter finnes i to varianter: public (PKCE, ingen secret) og confidential (PKCE + secret). Lumi-dashboardet støtter begge. For produksjon anbefales vanligvis confidential client hvis provideren og driftsmodellen din støtter det; lokalt referanseoppsett i keycloak/lumi-realm.json bruker public client.

API

sh
LUMI_ENV=production
AUTH_PROVIDER=oidc
OIDC_ISSUER_URL=https://keycloak.din-bedrift.no/realms/lumi
OIDC_AUDIENCE=lumi-dashboard
OIDC_GROUPS_CLAIM=groups                  # valgfri, default "groups"
OIDC_JWKS_URI=                            # valgfri; se split-networking nedenfor
LUMI_DASHBOARD_CLIENT_ID=lumi-dashboard   # brukes av API-et for client-identifikasjon
LUMI_ADMIN_GROUP=lumi-admins              # påkrevd i produksjon (API feiler startup uten)

AUTH_PROVIDER=oidc + LUMI_ENV=production er obligatorisk

Auth-modus styres av AUTH_PROVIDER — default local (statisk dev-prinsipal uten OIDC-validering). Sett AUTH_PROVIDER=oidc i produksjon, ellers slipper ingen OIDC-token gjennom validering og dashboardet snakker med et API i lokal dev-modus.

LUMI_ENV=production styrer separat strengere startup-validering (krever f.eks. LUMI_DASHBOARD_CLIENT_ID, LUMI_ADMIN_GROUP). Kjører du AUTH_PROVIDER=oidc uten LUMI_ENV=production, går API-et teknisk sett opp, men du mister guards som ville fanget manglende/feil-konfigurerte variabler før startup.

Vanlig BYOC-fallgruve — dobbeltsjekk begge variabler før go-live.

LUMI_ADMIN_GROUP er påkrevd i produksjon

Uten LUMI_ADMIN_GROUP blir enhver autentisert OIDC-bruker org-admin. API-et feiler startup dersom variabelen er tom når AUTH_PROVIDER=oidc. Se apps/lumi-api/src/main/kotlin/no/lumi/config/ServerEnv.kt.


Keycloak-oppsett

Lumi-repoet inneholder en realm-definisjon på keycloak/lumi-realm.json som er ment som utviklings- og referanse-oppsett, ikke for direkte prod-import. Den har:

  • Realm lumi med public client lumi-dashboard
  • Group-membership-mapper med full.path: false og access.token.claim: truegroups sendes som lumi-admins, team-alpha etc. på både id-token og access-token
  • Audience-mapper på client-en slik at access-tokenets aud = lumi-dashboard
  • Demo-grupper: lumi-admins, team-alpha, team-beta
  • Testbrukere med hardkodede passord (admin@lumi.test, user@lumi.test, user2@lumi.test) — derfor kan realm-filen ikke importeres direkte i prod
  • Public client-type som matcher lokal .env.oidc. For prod anbefales normalt confidential client hvis provideren og driftsmodellen din støtter det.

Anbefalt: sett opp prod-realm manuelt med referanse-filen som mal

For prod skal du opprette et tomt realm og gjenbruke navnekonvensjoner og mapper-config fra lumi-realm.json, men ikke importere det direkte.

  1. Master realm → Create realm → navn lumi (eller noe annet; husk å oppdatere OIDC_ISSUER_URL)
  2. Clients → Create client:
    • Client ID: lumi-dashboard
    • Client type: OpenID Connect
    • Anbefalt for prod: skru på Client authentication (= confidential client → gir deg en client secret). For public client lar du den stå av og utelater OIDC_CLIENT_SECRET.
    • Authentication flow: kryss av Standard flow, la Direct access grants være av i prod
    • Valid redirect URIs: https://lumi.din-bedrift.no/auth/callback
    • Valid post logout redirect URIs: https://lumi.din-bedrift.no/*
    • Web origins: https://lumi.din-bedrift.no
  3. Hvis client-en er confidential: Clients → lumi-dashboard → Credentials → kopier Client secret → sett som OIDC_CLIENT_SECRET på dashboardet
  4. Clients → lumi-dashboard → Advanced → Proof Key for Code Exchange Code Challenge MethodS256
  5. Client scopes → Create scope → navn groups, type Default
    • Legg til Mappers → By configuration → Group Membership
    • Name: groups, Token claim name: groups
    • Slå av Full group path (gir lumi-admins, ikke /lumi-admins)
    • Slå på Add to access token, Add to ID token, Add to userinfo
  6. Clients → lumi-dashboard → Client scopes → lumi-dashboard-dedicated (client-ens innebygde dedicated scope) → Add mapper → By configuration → Audience
    • Name: aud-lumi-dashboard
    • Included Client Audience: velg lumi-dashboard
    • Add to access token: på
    • Dette gir access-tokens med aud: ["lumi-dashboard"] som matcher OIDC_AUDIENCE på API-et. Ved å legge mapperen på dedicated scope slipper du å koble et eksternt scope til client-en.
  7. Clients → lumi-dashboard → Client scopes → Add client scope → legg til groups-scopet fra steg 5 som Default. Uten dette steget får tokenet ikke med seg groups-claimen.
  8. Groups → Create grouplumi-admins og eventuelt team-grupper (f.eks. team-alpha, team-beta)
  9. Users: legg til brukerne dine (eller koble på en identity provider som Entra ID / Google / LDAP), og assign brukere til grupper via Users → [user] → Groups → Join Group

Mappe Keycloak-grupper til Lumi-team

Keycloak-grupper kommer ikke automatisk inn som Lumi-team. Hver Keycloak-gruppe må eksplisitt kobles til et Lumi-team.

Første-gangs-oppsett oppretter admin-mapping

Når LUMI_ADMIN_GROUP er satt, oppretter /setup-wizarden automatisk en mapping fra admin-gruppen til det første default-teamet. Det gjør bootstrap-kallet og Innstillinger → Organisasjon → Team tilgjengelig umiddelbart etter setup.

Merk at mappingen også gir admin-gruppen tilgang til data i default-teamet. Hvis dere vil skille admin-tilgang fra data-tilgang, opprett et dedikert admin-team etter setup, map LUMI_ADMIN_GROUP dit og fjern mappingen fra default når ordinære team-grupper er lagt inn.

  1. Logg inn i dashboardet som admin. Hvis organisasjonen ikke er satt opp enda, havner du i /setup-wizarden — fullfør den først (oppretter org + første team).
  2. Gå til Innstillinger → Organisasjon → Team.
  3. For hvert team: legg til OIDC-gruppemapping hvor Group ID er det samme navnet Keycloak sender i groups-claimen (f.eks. team-alpha, uten slash).
  4. Lagre.

Endringer i team-mappings trer i kraft på neste request — Lumi slår opp groups → team per forespørsel mot databasen. Endringer på IdP-siden (bruker får ny gruppe i Keycloak) krever at brukeren logger ut og inn igjen for å få et nytt token med den nye group-claimen.

Alternativ: importer referanse-realmet for dev

Direkteimport er greit for lokal utvikling. Bruk docker-compose.yml i repo-roten (som allerede pinner Keycloak til versjon 26.2, bruker port 8180 for å unngå konflikt med API-ets 8080, og auto-importerer realmet):

sh
docker compose up -d keycloak

Split-networking (server-URL ≠ nettleser-URL) er avansert

Anbefalt prod-oppsett: la OIDC-provideren være tilgjengelig på én URL (typisk via ingress) for både dashboard-server, API og nettleser. Sett samme OIDC_ISSUER_URL overalt. Dette unngår all split-kompleksiteten.

Hvis split-nettverk er nødvendig (lokal compose, eller k8s uten felles ingress), brukes tre separate env-vars for ulike roller:

Env-varBrukt avRolle
OIDC_ISSUER_URLAPIMå matche token-ens iss-claim (satt av IdP, typisk KC_HOSTNAME_URL i Keycloak)
OIDC_ISSUER_URLDashboard (server)URL dashboard-serveren bruker for server-side token-exchange — må være reachable fra dashboard-container-en
OIDC_BROWSER_ISSUER_URLDashboard (browser)URL nettleseren redirects til for login — må være reachable fra brukernes nettlesere
OIDC_SCOPESDashboardKomma-separerte scopes for login/refresh. Default er openid,profile,email; legg til API-scope for Entra/custom providers
OIDC_JWKS_URIAPI (valgfri)Hvis JWKS-discovery via OIDC_ISSUER_URL feiler fra container-nettverket, pek direkte på container-reachable JWKS-endepunkt

Dashboardets OIDC_ISSUER_URL og API-ets OIDC_ISSUER_URL trenger ikke være like hvis dashboard-container og API-container ser provideren på ulike URL-er, men verdien API-et validerer mot matche det IdP-en stempler som iss.

Helm-charten eksponerer split-networking-verdiene direkte:

yaml
api:
  env:
    oidcIssuerUrl: https://keycloak.din-bedrift.no/realms/lumi
    oidcJwksUri: http://keycloak.lumi.svc.cluster.local:8080/realms/lumi/protocol/openid-connect/certs
dashboard:
  env:
    oidcIssuerUrl: http://keycloak.lumi.svc.cluster.local:8080/realms/lumi
    oidcBrowserIssuerUrl: https://keycloak.din-bedrift.no/realms/lumi

Bruk api.extraEnv eller dashboard.extraEnv for avanserte env-vars som chartet ikke modellerer eksplisitt, inkludert valueFrom-referanser til egne Secrets eller ConfigMaps.

docker-compose.yml i repo-roten viser Keycloak + Postgres-oppsettet som kjører i lokaltest, men lar OIDC-env-varene stå tomme — du må fylle dem inn manuelt per providern din. Det fungerer som et referansepunkt for nettverkstopologien (Keycloak på host-port 8180 med KC_HOSTNAME_URL=http://localhost:8180 mens API/dashboard kjører i samme docker-nettverk), ikke som en ferdig utfylt konfigurasjon.

For standalone-setup kan du kjøre:

sh
docker run -d --name lumi-keycloak -p 8180:8080 \
  -e KEYCLOAK_ADMIN=admin \
  -e KEYCLOAK_ADMIN_PASSWORD=admin \
  -v "$(pwd)/keycloak:/opt/keycloak/data/import:ro" \
  quay.io/keycloak/keycloak:26.2 \
  start-dev --import-realm

Port 8180 er host-porten som nettleseren treffer (KC_HOSTNAME_URL=http://localhost:8180 i compose). Containere på samme docker-nettverk når derimot Keycloak via service-navn på intern-porten (http://keycloak:8080) — localhost:8180 fra innsiden av en container er container-en selv, ikke Keycloak.

Dette er en nettverks-reachability-forskjell, ikke en issuer-identitet-forskjell: iss-claim i tokenet må fortsatt matche Keycloak sin annonserte issuer, og OIDC_ISSUER_URL på API-et (som validerer mot iss) må matche det. Splitter du issuer-URL-en mellom komponenter får du lett issuer-mismatch-feil. Lumi håndterer nettverks-forskjellen via en egen variabel for nettleser-redirects (OIDC_BROWSER_ISSUER_URL) — se "Split-networking (server-URL ≠ nettleser-URL)"-seksjonen lenger ned for den eksakte variabel-matrisen. Flagget --import-realm leser alle *.json i /opt/keycloak/data/import/ ved oppstart.

Lokal public client uten secret

Realm-filen har lumi-dashboard som public client (ingen secret), og .env.oidc i repo-roten er i samme modus. Dashboardet sender da clientSecret = null til OIDC-providerklienten og krever ikke OIDC_CLIENT_SECRET ved startup. Hvis du endrer client-en til confidential i Keycloak admin-UI, må du også sette OIDC_CLIENT_SECRET i .env.oidc.

Første-gangs-admin

Sett LUMI_ADMIN_GROUP til gruppen du er medlem av før du logger inn første gang. Uten en riktig satt admin-gruppe vil du ikke kunne administrere andre team senere. For lokalt dev-oppsett finnes LUMI_LOCAL_ADMIN=true som gjør dev-brukeren til admin — se CLAUDE.md-reglene. Dette virker kun når AUTH_PROVIDER=local.


Auth0 (kildeverifisert, ikke E2E-verifisert)

Auth0 skiller tydelig mellom OIDC-login og access-tokens til en konkret API. Lumi-API-et trenger et JWT access-token hvor aud matcher Auth0 API Identifier og hvor gruppeclaimen ligger på access-tokenet. Auth0s egne docs sier at access-token for egen API krever mål-audience/API Identifier, og at private custom claims som groups ikke bør brukes uten namespace.

Kildegrunnlag:

Nåværende Lumi-kode sender ikke Auth0 audience i authorize-URL-en

Dashboardet har per i dag OIDC_SCOPES, men ingen egen env-var for provider-spesifikke authorize-parametre som Auth0s audience. For en dedikert Auth0-tenant kan du bruke Auth0 Tenant Settings → API Authorization Settings → Default Audience og sette den til Lumi API Identifier. Auth0 beskriver dette som ekvivalent med å legge audience på alle authorization requests i tenant-en, og advarer om at det kan endre oppførsel for andre apper.

Ikke bruk tenant-wide Default Audience i en delt Auth0-tenant uten å vurdere konsekvensene. Da bør Lumi først få eksplisitt støtte for å sende audience kun på Lumi-login.

Auth0-steg

  1. Applications → APIs → Create API
    • Name: Lumi API
    • Identifier: en URI du kontrollerer, f.eks. https://lumi.din-bedrift.no/api
    • Signing Algorithm: RS256
    • For lengre dashboard-sesjoner: enable Allow Offline Access på API-et slik at Auth0 kan utstede refresh tokens når offline_access scopes inn.
    • Sett API-ets Identifier som OIDC_AUDIENCE på Lumi-API-et.
  2. Applications → Applications → Create Application
    • Type: Regular Web Application
    • Allowed Callback URLs: https://lumi.din-bedrift.no/auth/callback
    • Allowed Logout URLs: https://lumi.din-bedrift.no/
    • Allowed Web Origins: https://lumi.din-bedrift.no
    • Sett Client ID som OIDC_CLIENT_ID på dashboardet og LUMI_DASHBOARD_CLIENT_ID på API-et.
    • Sett Client Secret som OIDC_CLIENT_SECRET på dashboardet hvis applikasjonen er confidential.
  3. Tenant Settings → API Authorization Settings
    • For en dedikert Lumi-tenant: sett Default Audience til API Identifier fra steg 1.
    • For en delt tenant: ikke gjør dette uten egen risikovurdering; se warningen over.
  4. Actions → Library → Build Custom → Login / Post Login
    • Legg grupper på access-tokenet med en namespaced claim.
    • Claim-navnet groups er restricted i Auth0. Bruk f.eks. https://lumi.din-bedrift.no/groups, og sett samme verdi i OIDC_GROUPS_CLAIM.
  5. Actions → Flows → Login
    • Dra Actionen inn i Login-flowen og deploy.
  6. Logg inn på nytt i Lumi slik at dashboardet får et nytt access-token med riktig audience og groups-claim.

Hvis offline_access ikke er aktivert og med i OIDC_SCOPES, returnerer ikke Auth0 refresh token til dashboardet. Da må brukeren logge inn på nytt når access-tokenet utløper.

Eksempel på Auth0 Action. Bruk en gruppe-/rolle-kilde som faktisk finnes i tenant-en deres; app_metadata.lumi_groups er et eksplisitt eksempel som unngår å blande personprofil med autorisasjonsdata:

js
exports.onExecutePostLogin = async (event, api) => {
  const namespace = "https://lumi.din-bedrift.no";
  const groups = event.user.app_metadata?.lumi_groups ?? [];

  if (Array.isArray(groups)) {
    api.accessToken.setCustomClaim(
      `${namespace}/groups`,
      groups.map(String),
    );
  }
};

Auth0-env:

sh
# Dashboard
AUTH_PROVIDER=oidc
OIDC_ISSUER_URL=https://<tenant>.<region>.auth0.com/
OIDC_CLIENT_ID=<auth0-application-client-id>
OIDC_CLIENT_SECRET=<auth0-application-client-secret>
OIDC_REDIRECT_URI=https://lumi.din-bedrift.no/auth/callback
OIDC_SCOPES=openid,profile,email,offline_access

# API
AUTH_PROVIDER=oidc
OIDC_ISSUER_URL=https://<tenant>.<region>.auth0.com/
OIDC_AUDIENCE=https://lumi.din-bedrift.no/api
OIDC_GROUPS_CLAIM=https://lumi.din-bedrift.no/groups
LUMI_DASHBOARD_CLIENT_ID=<auth0-application-client-id>
LUMI_ADMIN_GROUP=lumi-admins

Dekod access-tokenet lokalt etter login og verifiser formen, uten å lime tokenet i nettbaserte dekodere:

json
{
  "iss": "https://<tenant>.<region>.auth0.com/",
  "aud": [
    "https://lumi.din-bedrift.no/api",
    "https://<tenant>.<region>.auth0.com/userinfo"
  ],
  "azp": "<auth0-application-client-id>",
  "sub": "auth0|...",
  "https://lumi.din-bedrift.no/groups": ["lumi-admins", "team-alpha"]
}

Okta (kildeverifisert, ikke E2E-verifisert)

Okta kan legge grupper på access-tokenet via en custom authorization server. Bruk ikke org authorization server for Lumi-grupper: Okta-dokumentasjonen sier at org authorization server bare kan gi groups-claim på ID-tokenet, mens Lumi trenger access-tokenet.

Kildegrunnlag:

Okta-steg

  1. Security → API → Authorization Servers
    • Bruk default custom authorization server eller opprett en ny authorization server.
    • Okta API Access Management kreves for custom authorization servers i produksjonsorgs.
    • Noter issuer: https://<okta-domain>/oauth2/<authorizationServerId>.
    • Noter Audience. For default er dette ofte api://default; for egen server velger du en URI som representerer Lumi-API-et.
    • Sett Audience som OIDC_AUDIENCE på Lumi-API-et.
  2. Security → API → Authorization Servers → [server] → Access Policies
    • Verifiser at authorization serveren har en policy som gjelder Lumi OIDC-appen.
    • Legg til en rule som tillater Authorization Code flow for Lumi-brukerne.
    • Tillat scopes openid, profile, email og groups. Hvis dashboardet skal kunne fornye sesjonen uten ny login, tillat også offline_access og Refresh Token grant i samme policy/rule.
    • Hvis ingen policy/rule matcher requesten, feiler Okta authorization-requesten før Lumi får tokens.
  3. Security → API → Authorization Servers → [server] → Claims → Add Claim
    • Name: groups
    • Include in token type: Access Token
    • Value type: Groups
    • Filter: bruk en allowlist/regex som bare tar med Lumi-relevante grupper, f.eks. ^(lumi-admins|team-.+)$
    • Include in: Any scope eller et dedikert groups scope hvis dere vil scope-gate claimen.
  4. Applications → Applications → [Lumi OIDC app]
    • Sign-in redirect URI: https://lumi.din-bedrift.no/auth/callback
    • Client ID settes som OIDC_CLIENT_ID på dashboardet og LUMI_DASHBOARD_CLIENT_ID på API-et.
    • Client Secret settes som OIDC_CLIENT_SECRET på dashboardet hvis appen er confidential.
  5. Hvis claimen er scope-gated: sett dashboardets OIDC_SCOPES=openid,profile,email,groups,offline_access. Dropp offline_access hvis dere bevisst vil ha korte dashboard-sesjoner uten refresh.
  6. Logg inn på nytt i Lumi slik at dashboardet får et nytt access-token fra custom authorization serveren.

Okta-env:

sh
# Dashboard
AUTH_PROVIDER=oidc
OIDC_ISSUER_URL=https://<okta-domain>/oauth2/default
OIDC_CLIENT_ID=<okta-application-client-id>
OIDC_CLIENT_SECRET=<okta-application-client-secret>
OIDC_REDIRECT_URI=https://lumi.din-bedrift.no/auth/callback
OIDC_SCOPES=openid,profile,email,groups,offline_access

# API
AUTH_PROVIDER=oidc
OIDC_ISSUER_URL=https://<okta-domain>/oauth2/default
OIDC_AUDIENCE=api://default
OIDC_GROUPS_CLAIM=groups
LUMI_DASHBOARD_CLIENT_ID=<okta-application-client-id>
LUMI_ADMIN_GROUP=lumi-admins

Dekod access-tokenet lokalt etter login og verifiser formen:

json
{
  "iss": "https://<okta-domain>/oauth2/default",
  "aud": "api://default",
  "cid": "<okta-application-client-id>",
  "sub": "00u...",
  "groups": ["lumi-admins", "team-alpha"]
}

E2E-verifisering før Auth0/Okta markeres som støttet

For hver provider må vi gjøre denne sjekklisten før #213 kan lukkes:

  1. Logg inn i dashboardet med en bruker som har både admin-gruppe og minst én team-gruppe.
  2. Bekreft at dashboardet kaller Lumi-API-et med access-tokenet fra provideren og får 200 fra bootstrap-/team-endepunkter.
  3. Dekod access-tokenet lokalt og lag en sanert token-sample uten ekte verdier for sub, e-post, tenant-ID-er eller gruppe-/brukeridentifikatorer.
  4. Bekreft at iss, aud, azp/client_id/cid, og groups-claim matcher env-varene.
  5. Bekreft at en bruker uten team-gruppe får 403 fra API-et, og dokumenter den faktiske responsen fra testmiljøet.
  6. Dokumenter faktiske feilmeldinger fra provider/API under troubleshooting.

Troubleshooting

"403 Forbidden: No teams authorized for user"

API-et returnerer 403 når brukerens groups-claim ikke matcher noen team-mapping i team_group_mappings-tabellen. Vanlige årsaker:

  1. groups mangler på access-tokenet — bekreft at lumi-dashboard-client-en har group-membership-mapperen aktivert med Add to access token. Dekode tokenet lokalt og verifiser groups-feltet.
  2. Ingen team-mappings er opprettet ennå — admin må inn i dashboardets Innstillinger → Organisasjon → Team og koble OIDC-gruppe-ID til et team.
  3. Group-navn/ID matcher ikke — Keycloak sender lumi-admins (uten slash) som standard; sjekk at mapping-raden bruker samme formatet.
  4. Auth0-claimen er lagt feil stedapi.idToken.setCustomClaim(...) hjelper ikke Lumi; bruk api.accessToken.setCustomClaim(...) og et namespaced claim-navn som matcher OIDC_GROUPS_CLAIM.
  5. Okta bruker org authorization server — den kan gi groups på ID-token, men ikke på access-tokenet. Bruk custom authorization server og claim med Include in token type: Access Token.
  6. Gruppe-overflow (primært Entra-spesifikt): API-loggen skriver OIDC group overage detected. Filtrer gruppene i providern.

Ikke lim inn access-tokens i eksterne verktøy

Tokens gir tilgang til Lumi-API-et så lenge de er gyldige (typisk 1 time). Bruk et lokalt verktøy for å dekode. Merk: unngå å lime tokenet direkte inn i shell-kommandoer — det ender i shell-historikken. Lagre heller i en midlertidig fil først, og slett etter bruk, eller bruk HISTFILE=/dev/null i en dedikert shell.

JWT bruker base64url (ikke standard base64) så konverter først:

sh
# Lagre tokenet i en temp-fil (ikke lim inn i kommandolinjen)
token_file=$(mktemp)
# ...skriv tokenet til $token_file via editor eller pbpaste...

# Bytt _/- til /+ og legg til padding iht. RFC 4648 (base64url har payload-lengde
# ≡ 0, 2, eller 3 (mod 4); 1 er ugyldig). Dekoder med -d (Linux/GNU, nyere macOS)
# og faller tilbake til -D (BSD/eldre macOS).
payload=$(cut -d. -f2 "$token_file" | tr '_-' '/+')
case $(( ${#payload} % 4 )) in
  0) ;;                              # allerede riktig lengde
  2) payload="${payload}==" ;;        # mangler 2 padding-chars
  3) payload="${payload}=" ;;         # mangler 1 padding-char
  1) echo "Ugyldig base64url-lengde i payload" >&2; exit 1 ;;
esac
(echo "$payload" | base64 -d 2>/dev/null || echo "$payload" | base64 -D) | jq .

# Rydd opp
rm "$token_file"

Eller bruk jwt CLI (brew install mike-engel/jwt-cli/jwt-cli på macOS, cargo install jwt-cli cross-platform) — men pipe tokenet inn stdin for å unngå shell-history:

sh
jwt decode - < "$token_file"

Aldri lim inn i nettleser-baserte dekodere eller pastebins.

"OIDC token verification failed"

API-et logger denne meldingen på DEBUG-nivå. Standardnivået er INFO, så du vil ikke se den i produksjonsloggen uten å heve nivået. Sett LUMI_API_LOG_LEVEL=DEBUG midlertidig på API-et, eller api.env.logLevel=DEBUG når du deployer via Helm.

Mulige årsaker når du ser meldingen:

  • aud matcher ikke OIDC_AUDIENCE — dekod tokenet og sammenlign.
  • Dashboardet ber ikke om API-scopet providern krever — for Entra/custom API-er, sett OIDC_SCOPES=openid,profile,email,api://<api-client-id>/access_as_user og logg inn på nytt.
  • Auth0 access-tokenet er for /userinfo, ikke Lumi-API-et — sett Auth0 Default Audience i en dedikert tenant, eller legg til kode-støtte for Auth0 audience før delt-tenant-oppsett.
  • Okta issuer er org authorization server (https://<domain>) i stedet for custom authorization server (https://<domain>/oauth2/<id>) — bruk custom issuer og sett OIDC_AUDIENCE til authorization serverens Audience.
  • iss matcher ikke OIDC_ISSUER_URL — typisk trailing slash eller feil realm-navn.
  • JWKS URL-discovery fungerer (gjøres ved API-startup mot OIDC_ISSUER_URL/.well-known/openid-configuration), men selve nøkkel-hentingen skjer ved første token-validering. Hvis nettverket til providern er blokkert fra API-pod-en vil valideringen feile. Sjekk egress-regler.
  • Token utløpt — refresh-logikken i dashboardet skal dekke dette, men hvis klokken på server/klient driver mye kan tokens feile prematurt.

"isOrgAdmin = false selv om jeg er i admin-gruppen"

  1. Bekreft at LUMI_ADMIN_GROUP matcher eksakt formatet providern sender. For vårt realm-oppsett: lumi-admins uten ledende slash (full.path: false).
  2. Restart API-en — LUMI_ADMIN_GROUP leses ved oppstart.
  3. Logg ut + inn igjen i dashboardet for å få nytt token.

"Login fungerer lokalt men ikke i prod"

Redirect URI må matche det som er registrert i providern eksakt, inkludert protokoll, port og trailing slash. Åpne provider-siden og sjekk registrerte redirect URIs mot hva dashboardet sender (se Location-headeren i devtools når du klikker "Logg inn").

"API feiler startup: LUMI_ADMIN_GROUP is required"

Forventet når AUTH_PROVIDER=oidc og LUMI_ADMIN_GROUP er tom. Dette er håndhevet for å unngå utilsiktet full tilgang til alle autentiserte brukere. Sett env-varen før restart.

"LUMI_SESSION_SECRET is required"

Dashboardet starter ikke. Generer en med openssl rand -base64 32 og sett verdien. For rotering kan flere komma-separerte verdier settes samtidig — eldre sesjoner fortsetter å fungere til de utløper.


Se også

Lumi Analytics — bygget på navikt/lumi (MIT-lisens)