Koble til backend
Be om tilgang først
Svarene kommer ikke fram før Team eSyfo har lagt appen deres inn som inbound hos mottakeren i Lumi. Det er det eneste steget der dere må vente på andre. Send en PR eller en tilgangsforespørsel før dere gjør resten.
Svaret går fra widgeten via appens backend til Lumi API:
- Widgeten sender
submission.transportPayloadtil et endepunkt i appens backend. - Backenden bytter brukerens token mot et nytt token for Lumi API med TokenX eller Entra ID. Dette kalles tokenveksling.
- Backenden sender payloaden videre til Lumi API med det nye tokenet.
Hvorfor tokenveksling?
Widgeten kjører i brukerens nettleser og kan ikke selv hente et token til Lumi API. Tokenet backenden får fra innloggingen, gjelder bare for appen deres. Etter tokenvekslingen har backenden et token med Lumi API som mottaker (audience). Fra det leser Lumi API hvilket team og hvilken app som sendte svaret.
1. Velg riktig endepunkt
Endepunktet avhenger av hvem som bruker appen:
| App | Tokenveksling | Endepunkt |
|---|---|---|
| For sluttbrukere (nav.no, arbeidsgiver, privatperson) | TokenX | POST /api/tokenx/v1/feedback |
| Intern (Modia, veiledersystemer, fagsystemer) | Entra ID (OBO) | POST /api/azure/v1/feedback |
Interne apper og Modia
Interne apper veksler token med Entra ID. Der heter tokenvekslingen on-behalf-of (OBO). I prod kaller alle interne apper lumi-api direkte. I dev avhenger det av hvilken Entra ID-tenant appen bruker:
nav.no: appen kallerlumi-apidirekte, som i prodtrygdeetaten.no, slik Modia-apper gjør: appen sender svarene tillumi-submission-proxy
Lumi API ligger i tenanten nav.no, og et OBO-token gjelder bare innenfor én tenant. Proxyen ligger i trygdeetaten.no, sjekker tokenet og sender svaret videre til Lumi API. Den finnes bare i dev-gcp.
Tenanten står i spec.azure.application.tenant i Nais-manifestet. Hvis feltet mangler, ser dere på brukeren dere logger inn med i dev: en bruker på @trygdeetaten.no betyr tenanten trygdeetaten.no, og den vanlige Nav-brukeren betyr nav.no. Er dere fortsatt usikre, skriv det i tilgangsforespørselen.
Fra tokenet lagrer Lumi API bare team og app sammen med svaret. Nav-identen som kan ligge i OBO-tokenet, blir verken brukt eller lagret. For å begrense antall innsendinger per bruker holder Lumi API en hash av brukerens ID i minnet. Legg heller ikke Nav-ident eller andre personopplysninger i context.
2. Sett miljøvariabler i Nais
Velg blokken som passer tokenvekslingen, miljøet og tenanten. Alle tre verdiene må komme fra samme blokk.
TokenX (apper for sluttbrukere)
Produksjon:
spec:
env:
- name: LUMI_API_HOST
value: http://lumi-api.team-esyfo
- name: LUMI_AUDIENCE
value: "prod-gcp:team-esyfo:lumi-api"
- name: LUMI_FEEDBACK_PATH
value: /api/tokenx/v1/feedbackDev:
spec:
env:
- name: LUMI_API_HOST
value: http://lumi-api.team-esyfo
- name: LUMI_AUDIENCE
value: "dev-gcp:team-esyfo:lumi-api"
- name: LUMI_FEEDBACK_PATH
value: /api/tokenx/v1/feedbackEntra ID (interne apper)
Hvilken dev-blokk dere skal bruke, står i Interne apper og Modia.
Produksjon:
spec:
env:
- name: LUMI_API_HOST
value: http://lumi-api.team-esyfo
- name: LUMI_AUDIENCE
value: "api://prod-gcp.team-esyfo.lumi-api/.default"
- name: LUMI_FEEDBACK_PATH
value: /api/azure/v1/feedbackDev med trygdeetaten.no-tenant:
spec:
env:
- name: LUMI_API_HOST
value: http://lumi-submission-proxy.team-esyfo
- name: LUMI_AUDIENCE
value: "api://dev-gcp.team-esyfo.lumi-submission-proxy/.default"
- name: LUMI_FEEDBACK_PATH
value: /api/azure/v1/feedbackDev med nav.no-tenant:
spec:
env:
- name: LUMI_API_HOST
value: http://lumi-api.team-esyfo
- name: LUMI_AUDIENCE
value: "api://dev-gcp.team-esyfo.lumi-api/.default"
- name: LUMI_FEEDBACK_PATH
value: /api/azure/v1/feedback3. Send inn svar til Lumi API
Backenden henter, validerer og veksler token med @navikt/oasis:
pnpm add @navikt/oasis@navikt/oasis ligger i GitHub Package Registry og ikke på npmjs. Appen trenger derfor disse linjene i .npmrc, der NPM_AUTH_TOKEN er et GitHub-token med read:packages. Tokenet må også være tilgjengelig når appen bygges i CI.
@navikt:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NPM_AUTH_TOKEN}Next.js App Router med TokenX
Lag ruten app/api/lumi/feedback/route.ts. Den tar imot transportPayload fra widgeten, sjekker brukerens token, veksler det og sender svaret videre:
import {
getToken,
requestTokenxOboToken,
validateIdportenToken,
} from "@navikt/oasis";
export async function POST(request: Request) {
const host = process.env.LUMI_API_HOST;
const audience = process.env.LUMI_AUDIENCE;
const feedbackPath = process.env.LUMI_FEEDBACK_PATH;
if (!host || !audience || !feedbackPath) {
console.error("Lumi-konfigurasjon mangler");
return new Response(null, { status: 500 });
}
const token = getToken(request);
if (!token) {
return new Response(null, { status: 401 });
}
const validation = await validateIdportenToken(token);
if (!validation.ok) {
return new Response(null, { status: 401 });
}
const obo = await requestTokenxOboToken(token, audience);
if (!obo.ok) {
console.error("Tokenveksling feilet:", obo.error.message);
return new Response(null, { status: 500 });
}
// transportPayload fra widgeten, sendt videre uendret
const body = await request.text();
let response: Response;
let errorText = "";
try {
response = await fetch(`${host}${feedbackPath}`, {
method: "POST",
headers: {
Authorization: `Bearer ${obo.token}`,
"Content-Type": "application/json",
},
body,
// Gi opp før widgeten, som venter i ti sekunder
signal: AbortSignal.timeout(8_000),
});
if (!response.ok) {
errorText = await response.text();
}
} catch (error) {
const timedOut = error instanceof Error && error.name === "TimeoutError";
console.error("Lumi API svarte ikke:", error);
return new Response(null, { status: timedOut ? 504 : 502 });
}
if (!response.ok) {
console.error(`Lumi API svarte med ${response.status}: ${errorText}`);
}
const headers = new Headers();
const retryAfter = response.headers.get("Retry-After");
if (retryAfter) {
headers.set("Retry-After", retryAfter);
}
return new Response(null, { status: response.status, headers });
}Slik virker ruten:
- Innloggingen i Nais legger brukerens token i
Authorization-headeren på alle kall til backenden. Med ID-porten slår dere den på medidporten.enabled: trueogidporten.sidecar.enabled: true.getToken(request)henter tokenet fra headeren. validateIdportenTokensjekker at tokenet er gyldig og laget for appen deres.requestTokenxOboTokenveksler tokenet til et token for Lumi API. Appen må hatokenx.enabled: truei Nais-manifestet.- Ruten sender statuskoden fra Lumi API videre til nettleseren, så dere ser den i nettverksfanen. Feilmeldingen fra Lumi API står i loggen.
Retry-Aftersendes videre, så widgeten kan vente så lenge Lumi API ber om før den prøver igjen. Se Tidsgrense og nytt forsøk.- Svarer ikke Lumi API innen åtte sekunder, gir ruten opp, logger
Lumi API svarte ikkeog svarer 504. Widgeten venter i ti sekunder på hele kallet til backenden. Bruker tokenvekslingen mer enn to sekunder, gir widgeten opp først.
Har appen basePath i next.config, ligger ruten under den stien. Se Base-sti for hvordan endpoint får den med.
requestOboToken med både TokenX og Entra ID
@navikt/oasis har også requestOboToken, som velger TokenX eller Entra ID ut fra miljøvariablene i Nais. Har appen begge slått på, returnerer den alltid en feil. Bruk derfor requestTokenxOboToken eller requestAzureOboToken. validateToken har samme begrensning når appen har både ID-porten og Entra ID.
Express med TokenX
I en Express-backend er ruten den samme, men getToken får req fra Express. Sett grensen i express.json() til 1 MB, som er grensen i Lumi API. Standarden i Express er 100 kB.
import express from "express";
import {
getToken,
requestTokenxOboToken,
validateIdportenToken,
} from "@navikt/oasis";
const app = express();
app.post(
"/api/lumi/feedback",
express.json({ limit: "1mb" }),
async (req, res) => {
try {
const token = getToken(req);
if (!token) {
res.sendStatus(401);
return;
}
const validation = await validateIdportenToken(token);
if (!validation.ok) {
res.sendStatus(401);
return;
}
const obo = await requestTokenxOboToken(
token,
process.env.LUMI_AUDIENCE ?? "",
);
if (!obo.ok) {
console.error("Tokenveksling feilet:", obo.error.message);
res.sendStatus(500);
return;
}
const response = await fetch(
`${process.env.LUMI_API_HOST}${process.env.LUMI_FEEDBACK_PATH}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${obo.token}`,
"Content-Type": "application/json",
},
// transportPayload fra widgeten, sendt videre uendret
body: JSON.stringify(req.body),
// Gi opp før widgeten, som venter i ti sekunder
signal: AbortSignal.timeout(8_000),
},
);
if (!response.ok) {
console.error(
`Lumi API svarte med ${response.status}: ${await response.text()}`,
);
}
const retryAfter = response.headers.get("Retry-After");
if (retryAfter) {
res.set("Retry-After", retryAfter);
}
res.sendStatus(response.status);
} catch (error) {
const timedOut = error instanceof Error && error.name === "TimeoutError";
console.error(
timedOut ? "Lumi API svarte ikke:" : "Innsending til Lumi feilet:",
error,
);
res.sendStatus(timedOut ? 504 : 500);
}
},
);try/catch trengs fordi Express 4 ikke fanger feil i asynkrone ruter. validateIdportenToken kaster en feil når Nais-variablene for ID-porten mangler, for eksempel lokalt.
Interne apper med Entra ID
Ruten er den samme for en intern app. Dette er annerledes:
| TokenX | Entra ID | |
|---|---|---|
| Nais-manifestet | idporten.enabled: true, idporten.sidecar.enabled: true og tokenx.enabled: true | azure.application.enabled: true og azure.sidecar.enabled: true |
| Validering | validateIdportenToken(token) | validateAzureToken(token) |
| Tokenveksling | requestTokenxOboToken(token, audience) | requestAzureOboToken(token, audience) |
LUMI_FEEDBACK_PATH | /api/tokenx/v1/feedback | /api/azure/v1/feedback |
Host, audience og sti kommer fra en av Entra ID-blokkene i steg 2. Bruker backenden Express, ser ruten ut som i Express med TokenX, med validateAzureToken og requestAzureOboToken i stedet.
4. Konfigurer access policies
Både appen deres og mottakeren hos Lumi må ha en access policy som slipper trafikken gjennom. Outbound-regelen, hosten og audience i tokenet må peke på samme mottaker:
| Tokenveksling | Miljø | Tenant i appen | Mottaker |
|---|---|---|---|
| TokenX | dev-gcp | – | lumi-api |
| TokenX | prod-gcp | – | lumi-api |
| Entra ID | dev-gcp | nav.no | lumi-api |
| Entra ID | dev-gcp | trygdeetaten.no | lumi-submission-proxy |
| Entra ID | prod-gcp | nav.no | lumi-api |
lumi-submission-proxy finnes bare i dev-gcp. Se Interne apper og Modia hvis dere er usikre på tenanten.
Appen deres (outbound)
Kaller appen lumi-api direkte, legger dere til:
spec:
accessPolicy:
outbound:
rules:
- application: lumi-api
namespace: team-esyfoBruker appen Entra ID med tenanten trygdeetaten.no i dev, legger dere til denne regelen i stedet:
spec:
accessPolicy:
outbound:
rules:
- application: lumi-submission-proxy
namespace: team-esyfoMottakeren (inbound)
Appen må også legges til som inbound i mottakeren fra tabellen: lumi-api for direkte kall, eller lumi-submission-proxy for trygdeetaten.no i dev. Uten inbound-regelen får ikke appen veksle token til mottakeren. Det kan dere ordne på to måter:
- Vet dere hvilken mottaker dere skal ha, kan dere sende en PR selv. Det går raskest.
- Er dere usikre, kan dere opprette en issue med app-navn, namespace og auth-type, så ordner Team eSyfo resten. Tilgangen gjelder da både dev og prod.
Regelen ser slik ut:
spec:
accessPolicy:
inbound:
rules:
- application: din-app
namespace: ditt-teamSend PR selv
Legg regelen til under spec.accessPolicy.inbound.rules i filen for mottakeren og hvert miljø dere trenger:
| Mottaker | Miljø | Fil |
|---|---|---|
lumi-api | dev-gcp | apps/lumi-api/nais/app/dev.yaml |
lumi-api | prod-gcp | apps/lumi-api/nais/app/prod.yaml |
lumi-submission-proxy | dev-gcp | apps/lumi-submission-proxy/nais/dev.yaml |
- Ta alltid med
namespace. Uten den gjelder regelen en app iteam-esyfo. - Skriv auth-type (TokenX eller Entra ID) i PR-beskrivelsen. Team eSyfo sjekker tenanten i appens Nais-manifest og at mottakeren stemmer.
- Inbound-listen er selve tilgangskontrollen. Team eSyfo går derfor gjennom alle endringer, og tilgangen gjelder først når PR-en er merget og mottakeren er deployet.
Går appen via proxyen i dev, trenger den ikke inbound i lumi-api i dev. Proxyen har allerede sin egen tilgang til lumi-api. I prod trenger appen inbound i lumi-api.
5. Velg lagring
Widgeten husker at brukeren har lukket surveyen bare når den får lagre det. Standardstrategien consent krever Nav-dekoratøren og at brukeren har samtykket. Interne apper uten dekoratøren må sette storageStrategy: "localStorage". Se Lagring for hva som skjer uten samtykke, og hvordan dere velger strategi.
Komplett sjekkliste
Før dere setter appen i produksjon, sjekk at dere har:
- [ ] bedt om inbound-tilgang hos mottakeren (send PR selv eller opprett issue), helst først
- [ ] installert
@navikt/lumi-surveyog lagt den delte surveyen i appen - [ ] importert widgeten fra
@navikt/lumi-survey/client, eller lagt"use client"øverst i filen med widgeten, hvis appen bruker Next.js App Router - [ ] importert
@navikt/ds-cssog@navikt/lumi-survey/styles.css - [ ] satt
transporttil{ endpoint: "/api/lumi/feedback" }, eller laget en egentransport.submitsom sendersubmission.transportPayloadog kaster en feil når svaret ikke erok - [ ] installert
@navikt/oasisog laget endepunktet i backenden med validering og tokenveksling (TokenX eller Entra ID) - [ ] riktig endepunkt (
/api/tokenx/v1/feedbackeller/api/azure/v1/feedback) - [ ]
LUMI_API_HOST,LUMI_AUDIENCEogLUMI_FEEDBACK_PATHsatt i Nais-manifestet - [ ] outbound access policy mot riktig mottaker (
lumi-apiellerlumi-submission-proxy) iteam-esyfo - [ ] riktig
storageStrategy(consent,localStorageellernone) - [ ] testet i dev at et innsendt svar vises i dashboardet
Test i dev
Deploy appen til dev-gcp og sjekk at hele kjeden virker:
- Åpne appen i dev og send inn et svar på surveyen.
- Sjekk at svaret vises i dashboardet i dev.
Feiler innsendingen, finner dere vanlige feil og løsninger i Feilsøking.
Videre lesing
- Velg hva dere vil måle: hvilket oppsett som passer til det dere vil finne ut
- Spørsmålstyper: vurdering, fritekst, ett valg og flere valg
- Vis bare relevante spørsmål: vis spørsmål ut fra tidligere svar
- Context og tags: legg til metadata dere kan filtrere på i dashboardet
