Maskinporten Production Setup
Configure the Maskinporten live integration, interpret MASKINPORTEN_AUTH_FAILED, and run the readiness probe before cutover.
[Cite this as: Apier.no Docs v0.1.0 — last updated 2026-05-18]
Overview
This guide is for operators flipping the Apier Maskinporten integration from mock mode to live. It covers the prerequisites Digdir requires before the live environment will issue tokens, the seven environment variables that gate the live adapter, the /_internal/maskinporten/readiness probe that returns a structured go/no-go verdict, the MASKINPORTEN_AUTH_FAILED error code that surfaces when a live token exchange is rejected, and the Sentry breadcrumb categories an on-call engineer reads when triaging a Maskinporten alert. It does not duplicate the OAuth2 background in the Maskinporten Developer Guide — read that first if you have not. End-user (API consumer) framing for the same error code lives in Error handling and the Compliance Explainer; this guide is the operator-facing companion.
Prerequisites
Three independent pieces of infrastructure must exist before Maskinporten will issue live tokens for Apier:
- Maskinporten production client. Registered in Samarbeidsportalen self-service (sjolvbetjening.samarbeid.digdir.no) against the production environment, not the test (
tt02) environment. The client identifies the integrating organisation and is bound to your virksomhetssertifikat. - Altinn System Register live entry. Required for any Altinn-backed scope (
altinn:serviceowner/*,altinn:authentication/*). The merged scope-pinning work insrc/lib/altinn/scopes.tsenumerates the four wire scopes the current codebase consumes. - Virksomhetssertifikat + kid. Your hardware-issued digital certificate (from Buypass or Commfides) is the signing key for the JWS assertion the live adapter sends to Maskinporten. The
kid(key identifier) is the JWK-set entry the public half is uploaded under in Samarbeidsportalen — write that exact string intoMASKINPORTEN_KID(see below). A rotated key in the portal without a synchronisedMASKINPORTEN_KIDenv-var update produces aMASKINPORTEN_AUTH_FAILEDon every exchange.
Related reading: the Maskinporten Developer Guide covers the OAuth2 client-credentials flow and the JWS assertion structure. The Altinn System Users guide covers the three-leg delegation setup that downstream Altinn scopes depend on.
Required environment variables (live mode)
| Variable | Required in live | Purpose |
|---|---|---|
MASKINPORTEN_MODE | yes | "live" flips the adapter factory from MockMaskinportenClient to the live getToken path. Any other value keeps the mock adapter active. |
MASKINPORTEN_CLIENT_ID | yes | The Maskinporten client identifier registered for your organisation in Samarbeidsportalen. Appears as iss in the JWS assertion. |
MASKINPORTEN_KID | yes | The kid of the JWK whose public half is currently uploaded to Maskinporten for this client. Written into the JWS header so Maskinporten can select the right key to verify against (Apier PR #166). |
MASKINPORTEN_PRIVATE_KEY | yes | PEM-encoded private key paired with the kid above. Parsed via crypto.createPrivateKey inside the env-validation superRefine at boot — a malformed PEM fails the deploy, not the first request (Apier PR #166). |
MASKINPORTEN_TOKEN_ENDPOINT | yes | Maskinporten token-exchange URL. https:// is enforced. Production and test environments use different endpoints — confirm against docs.digdir.no/docs/idporten. |
MASKINPORTEN_ISSUER | yes | The aud value the JWS assertion is built against. https:// is enforced; no fallback to MASKINPORTEN_TOKEN_ENDPOINT (Apier PR #166 closed that silent-coupling). Production and test environments use different issuer values. |
INTERNAL_API_SECRET | yes | Bearer secret required by the /_internal/maskinporten/readiness route. >= 32 characters enforced at boot in live mode (Apier PR #168). Generate via openssl rand -hex 32. |
Boot-time validation rejects any deploy with a missing or malformed value — see src/lib/env.ts for the live-mode superRefine that runs these checks. Misconfiguration cannot reach a request handler.
Readiness probe (cutover go/no-go)
The /_internal/maskinporten/readiness route is the recommended pre-flight check before flipping MASKINPORTEN_MODE to live. It is pure compute — it parses the env, parses the PEM via the same code path the live adapter uses to sign a JWS, and returns a structured verdict. It never makes a network call to Maskinporten, so it is safe to run from a deploy pipeline at every release.
curl -H "Authorization: Bearer $INTERNAL_API_SECRET" \
https://apier.no/api/v1/_internal/maskinporten/readinessThe probe returns one of three response shapes.
200 — ready
{ "status": "ready" }All required env vars are present, the PEM parses cleanly, a dummy JWS assertion signs without error. It is safe to flip MASKINPORTEN_MODE to live on the same deployment.
200 — mock
{ "status": "mock" }MASKINPORTEN_MODE !== "live". The probe is informational — no Maskinporten env vars are validated in this branch. No operator action is required; this is the default state for preview and development deployments.
503 — not ready
{
"success": false,
"error_code": "SERVER_ERROR",
"explanation": {
"summary": "Maskinporten live-mode prerequisites are not satisfied"
},
"failures": [
{ "field": "MASKINPORTEN_PRIVATE_KEY", "reason": "invalid or unusable for JWT signing" }
]
}The response uses Apier's standard ApiErrorResponse envelope (success: false, error_code, explanation, plus the standard _meta trust-metadata block — omitted above for brevity) extended with a failures array that names each invalid field and a short structured reason. Do not flip MASKINPORTEN_MODE to live until every entry is resolved. The 503 is also returned when VERCEL_ENV=production and MASKINPORTEN_MODE is still mock — defence in depth against an accidental mock-in-prod deploy (Apier PR #168).
MASKINPORTEN_AUTH_FAILED — what it means
MASKINPORTEN_AUTH_FAILED is the Apier-canonical error code surfaced when Maskinporten rejects a live token exchange. The reliability layer at src/lib/reliability/error-mappings.ts:39-41 normalises every Maskinporten 401 — whether the upstream response body carries TOKEN_EXPIRED, INVALID_TOKEN, or no body code at all — into the single MASKINPORTEN_AUTH_FAILED code so callers handle one code instead of three.
The verbatim explainer payload an API consumer sees (Norwegian Bokmål — Apier serves end-user error text in the consumer's regulatory locale):
{
"success": false,
"error_code": "MASKINPORTEN_AUTH_FAILED",
"explanation": {
"summary": "Maskinporten avslo Apier sin tokenforespørsel.",
"why": "Maskinporten returnerte 401 — Apier sin token er utløpt, sertifikatet er ugyldig, eller en scope-tildeling er trukket tilbake. Server-side feil; API-klienten kan ikke reparere selv.",
"fix_steps": [
"Vent og prøv igjen om noen minutter — Apier henter nytt token automatisk når det forrige utløper.",
"Hvis problemet vedvarer i mer enn et kvarter: kontakt Apier support med X-Correlation-ID fra svarhodet.",
"Bruk `?dry_run=true` i mellomtiden for å validere innholdet uten å sende inn."
],
"relevant_link": "https://apier.no/status"
}
}The consumer-facing text frames MASKINPORTEN_AUTH_FAILED as a transient server-side problem the API client cannot fix. From the operator's side that framing is approximately true — the client really cannot fix it — but a credential-or-config drift in our deployment is the cause that needs operator attention.
Differentiation from other 503 / upstream-failure codes
MASKINPORTEN_AUTH_FAILED is one of several codes the reliability layer emits when an upstream call fails. The triage table below lets an on-call engineer pick the right next step from the error code alone:
| Code | Trigger | Action |
|---|---|---|
MASKINPORTEN_AUTH_FAILED | Maskinporten returned 401 and rejected the JWS assertion. | Check kid, host clock, endpoints, and key revocation (see remediation below). |
TRANSIENT_UPSTREAM_FAILURE | The retry budget (4 attempts: 1 initial + 3 backoffs) was exhausted on 5xx or 429 responses. | Check the Maskinporten status page. The credential is probably fine. |
timeout outcome in Sentry | An AbortSignal fired before Maskinporten responded. | Check network connectivity from Vercel arn1 (Stockholm) to Maskinporten. |
A MASKINPORTEN_AUTH_FAILED is always a credential or configuration drift — never a Maskinporten outage. If Maskinporten is genuinely down, you will see TRANSIENT_UPSTREAM_FAILURE after the retry budget expires, or the timeout outcome in Sentry.
MASKINPORTEN_AUTH_FAILED — remediation
Work the list top-to-bottom. Stop at the first step that resolves the alert; each subsequent step is more invasive than the last.
- Run the readiness probe. If it returns
503, thefailuresarray names each missing or malformed env var. Fix and redeploy — the live adapter cannot succeed until every readiness failure is resolved. - Check the host clock. Maskinporten rejects JWS assertions with
iatskew of more than a few seconds. Vercel'sarn1runners are NTP-synchronised by default, but a Cloudflare-fronted preview or a self-hosted runner can drift. IfMASKINPORTEN_AUTH_FAILEDstarted after a network or kernel change, suspect the clock first. - Verify
MASKINPORTEN_KIDmatches the kid registered at Samarbeidsportalen. If you rotated the key in the portal without updating the env var, the JWS header carries a kid Maskinporten cannot resolve to a public key, and every exchange fails withINVALID_TOKEN. The portal's Nøkler page is the source of truth. - Confirm
MASKINPORTEN_ISSUERandMASKINPORTEN_TOKEN_ENDPOINTmatch the active environment. Production andtt02(test) have different values. A copy-paste from test config into production env produces a 401 with no useful body content because the assertion'sauddoes not match the production token endpoint's expected issuer. - If steps 1–4 pass and the error persists, the client may have been revoked at Digdir. A revocation can land for several reasons: certificate expiry, a Samarbeidsportalen administrator removing the client, a compliance review at Digdir. Open a support ticket with Digdir and include the
X-Correlation-IDfrom a failing response — Apier audit logs that ID against the upstream attempt.
Observability signals
PR #170 wired structured Sentry breadcrumbs into every Maskinporten token exchange. The breadcrumb categories let an on-call engineer reconstruct a single token-exchange attempt without reading the raw retry-loop code:
token_request_start— info,attempt=0. One per token exchange. Emits at the first call into the live adapter.token_request_retry— warning,attemptin1..3. Emits before each backoff. Carries a boundedreason_codeenum so Sentry can group retries by cause.token_request_success— info. Emits on a successful exchange withduration_msfor latency histograms.token_request_exhausted— error. Emits when the retry budget is exhausted; carries anoutcometag (see below).
The outcome tag on token_request_exhausted is the primary signal an on-call engineer uses to triage:
client_error—4xxresponse, non-retryable. This is a credential or configuration issue; this guide's remediation list is the right starting point.exhausted— four transient retries failed (5xx,429, or network errors). Maskinporten is probably degraded; check Digdir's status surface.timeout— anAbortSignalfired before any response. Network path fromarn1to Maskinporten is the right place to look.
A Sentry alert without the breadcrumb chain is missing context; if a MASKINPORTEN_AUTH_FAILED arrives without a token_request_exhausted breadcrumb, the alert was forwarded from somewhere other than the live token exchange — investigate the calling route directly.
Key rotation
Pre-cutover, the same kid must live in both the env (MASKINPORTEN_KID) and the Samarbeidsportalen Nøkler page; the public half of the keypair is uploaded under that kid, and the JWS assertion the live adapter signs carries the same string in its header. Production rotation includes multiple operational steps, with the key cutover itself centred on uploading the new public key to Samarbeidsportalen and then updating MASKINPORTEN_KID + MASKINPORTEN_PRIVATE_KEY in Vercel before redeploy. See Maskinporten Key Rotation for the full procedure (verification probe, validity-overlap window, rollback path, and old-key cleanup). Treat production rotation as a maintenance-window event and coordinate with on-call.
Related reading
- Maskinporten Developer Guide — OAuth2 client-credentials background and the JWS assertion structure.
- How Altinn 3 System Users Work — three-leg System User setup, the prerequisite for the Altinn-backed scopes the live adapter consumes.
- Error handling and the Compliance Explainer — end-user (API consumer) framing for
MASKINPORTEN_AUTH_FAILEDand the rest of the structured-error envelope. - API reference — full
ApierErrorCodeenum, including the position-15 entry this guide elaborates on.
How Altinn 3 System Users Work
The two delegation models in Altinn 3, why System Users exist, and the three-leg setup an agent needs before it can act on behalf of a Norwegian company.
Maskinporten Key Rotation
Operator runbook for rotating the production Maskinporten signing keypair (MASKINPORTEN_KID + MASKINPORTEN_PRIVATE_KEY).