원클릭으로
nav-auth
Azure AD, TokenX, ID-porten, Maskinporten og JWT-validering for Nav-applikasjoner
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Azure AD, TokenX, ID-porten, Maskinporten og JWT-validering for Nav-applikasjoner
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Generer conventional commit-meldinger med Nav-relevante scopes og breaking change-format
Expert builder for the Aksel design system (Nav / @navikt) React components, design tokens, layout primitives, theming (light/dark), icons, CSS, the Tailwind preset, version migrations, and Figma-to-code. Trigger on any frontend UI task that mentions Aksel, Nav/Navikt, "designsystemet", or @navikt/ds-* / @navikt/aksel-* packages — or that asks to add, create, build, or refactor a component (button, input, modal, table, alert, card, form) or layout, or to implement a design from Figma (a pasted figma.com/design/...?node-id link, "implement this design", "build this from Figma", design-to-code). Strong signals "using/with aksel", "@navikt/ds-react", "design system", a pasted figma.com link. If the work is frontend UI and there is any Aksel signal, invoke this skill unless the user explicitly opts out.
Integrer og konfigurer Nav Dekoratøren – felles header og footer for nav.no-applikasjoner. Bruk når et team skal ta i bruk Dekoratøren, oppdatere konfigurasjon, legge til breadcrumbs/språkvelger/analytics, håndtere samtykke (ekomloven), CSP eller feilsøke integrasjon mot dekoratøren.
Lag responsive layouts med Aksel Design System (v8+) - spacing tokens, layout primitives (Box, HStack, VStack, HGrid, Page, Bleed) og ResponsiveProp
Generer og kjør Playwright E2E-tester for webapplikasjoner med page objects, auth fixtures og tilgjengelighetstester
Kompakt output-stil som kutter fyllord og beholder teknisk substans — spar output-tokens uten å miste nøyaktighet.
| name | nav-auth |
| description | Azure AD, TokenX, ID-porten, Maskinporten og JWT-validering for Nav-applikasjoner |
| license | MIT |
| compatibility | Application on Nais with authentication needs |
| metadata | {"domain":"auth","tags":"azure-ad tokenx id-porten maskinporten jwt auth oasis"} |
Patterns for authentication and authorization in Nav applications. Covers Azure AD, TokenX, ID-porten, Maskinporten, and JWT validation.
# Decode JWT token payload (without verification — note: uses tr for base64url)
echo "<token>" | cut -d'.' -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq .
# Fetch Azure AD OpenID config
curl -s "https://login.microsoftonline.com/nav.no/.well-known/openid-configuration" | jq .
# Check auth env var names in pod (values hidden)
kubectl exec -it <pod> -n <namespace> -- env | grep -oE '^(AZURE|TOKEN_X|IDPORTEN)[^=]*'
# Test if JWKS endpoint is reachable
curl -s "$AZURE_OPENID_CONFIG_JWKS_URI" | jq '.keys | length'
Use when: Internal Nav employees need to access the application.
Nais Configuration:
azure:
application:
enabled: true
tenant: nav.no
Environment Variables (auto-injected): AZURE_APP_CLIENT_ID, AZURE_APP_CLIENT_SECRET, AZURE_APP_WELL_KNOWN_URL, AZURE_OPENID_CONFIG_ISSUER, AZURE_OPENID_CONFIG_JWKS_URI
Kotlin/Ktor:
install(Authentication) {
jwt("azureAd") {
verifier(azureAdConfiguration.jwksUri)
validate { credential ->
val audience = credential.payload.audience
if (audience.contains(expectedAudience)) {
JWTPrincipal(credential.payload)
} else null
}
}
}
routing {
authenticate("azureAd") {
get("/api/internal") {
val principal = call.principal<JWTPrincipal>()
val userId = principal?.payload?.subject
call.respond(data)
}
}
}
TypeScript/Next.js with @navikt/oasis:
import { validateAzureToken } from "@navikt/oasis";
export async function GET(request: Request) {
const token = getToken(request);
if (!token) return new Response("Unauthorized", { status: 401 });
const validation = await validateAzureToken(token);
if (!validation.ok) return new Response("Forbidden", { status: 403 });
const userId = validation.payload.sub;
return Response.json({ userId });
}
Use when: One Nav service calls another on behalf of a user.
Nais Configuration:
tokenx:
enabled: true
accessPolicy:
inbound:
rules:
- application: calling-service
namespace: team-calling
outbound:
rules:
- application: downstream-service
namespace: team-downstream
Environment Variables: TOKEN_X_WELL_KNOWN_URL, TOKEN_X_CLIENT_ID, TOKEN_X_PRIVATE_JWK
TypeScript with @navikt/oasis:
import { requestOboToken, getToken } from "@navikt/oasis";
export async function GET(request: Request) {
const token = getToken(request);
if (!token) return new Response("Unauthorized", { status: 401 });
// TokenX audience: "cluster:namespace:app-name"
const obo = await requestOboToken(token, "dev-gcp:team-namespace:downstream-service");
if (!obo.ok) return new Response("Token exchange failed", { status: 403 });
// Service-to-service calls within the cluster use HTTP (internal traffic never leaves the mesh)
const response = await fetch("http://downstream-service/api/data", {
headers: { Authorization: `Bearer ${obo.token}` },
});
return Response.json(await response.json());
}
Note:
@navikt/oasisauto-caches OBO tokens. Azure AD audience uses different format:"api://dev-gcp.namespace.app-name/.default"
Kotlin Token Exchange:
suspend fun exchangeToken(token: String, targetApp: String): String {
val response = httpClient.submitForm(
url = System.getenv("TOKEN_X_TOKEN_ENDPOINT"),
formParameters = Parameters.build {
append("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange")
append("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
append("client_assertion", createClientAssertion())
append("subject_token_type", "urn:ietf:params:oauth:token-type:jwt")
append("subject_token", token)
append("audience", "dev-gcp:team-namespace:$targetApp")
}
)
return response.body<TokenResponse>().access_token
}
Use when: Norwegian citizens authenticate with BankID/MinID.
idporten:
enabled: true
sidecar:
enabled: true
level: Level4
ID-porten sidecar handles authentication. Application receives validated JWT with fødselsnummer in claims.
Use when: External organizations need machine-to-machine access.
maskinporten:
enabled: true
scopes:
consumes:
- name: "nav:example/scope"
When accepting Azure AD M2M tokens (sub == oid), always validate azp against AZURE_APP_PRE_AUTHORIZED_APPS:
// ✅ Correct — validate azp against pre-authorized apps
validate { credentials ->
if (!erMaskinTilMaskin(credentials)) return@validate null
val azpClaim = credentials.payload.getClaim("azp").asString()
val preAuthorizedApp = preAuthorizedApps
.firstOrNull { it.clientId == azpClaim }
?: return@validate null // reject unknown callers
JWTPrincipal(credentials.payload)
}
// ❌ Wrong — accepts ANY app in the Azure AD tenant
validate { credentials ->
if (!erMaskinTilMaskin(credentials)) return@validate null
JWTPrincipal(credentials.payload) // no azp check!
}
Cross-check auth code against .nais/nais.yaml accessPolicy.inbound.rules — every app allowed at network level should also be validated at token level.
class AuthenticationTest {
private val mockOAuth2Server = MockOAuth2Server()
@BeforeEach fun setup() { mockOAuth2Server.start() }
@AfterEach fun tearDown() { mockOAuth2Server.shutdown() }
@Test
fun `should authenticate with valid token`() {
val token = mockOAuth2Server.issueToken(
issuerId = "azuread",
subject = "test-user",
claims = mapOf("preferred_username" to "test@nav.no", "roles" to listOf("user"))
)
val response = client.get("/api/protected") { bearerAuth(token.serialize()) }
response.status shouldBe HttpStatusCode.OK
}
}
import { vi, describe, it, expect } from "vitest";
import { validateAzureToken } from "@navikt/oasis";
vi.mock("@navikt/oasis", () => ({
validateAzureToken: vi.fn(),
requestOboToken: vi.fn(),
getToken: vi.fn(),
}));
describe("auth middleware", () => {
it("should accept valid Azure token", async () => {
vi.mocked(validateAzureToken).mockResolvedValue({
ok: true,
payload: { sub: "user-123", aud: "client-id" },
});
const response = await GET(mockRequest("valid-token"));
expect(response.status).toBe(200);
});
});
| Problem | Solution |
|---|---|
| "Invalid audience" | Verify AZURE_APP_CLIENT_ID matches expected audience |
| "Token expired" | Implement token refresh; check system time sync |
| TokenX exchange fails | Check access policies and that target has TokenX enabled |
| JWKS retrieval fails | Cache JWKS with TTL; handle refresh on validation failure |
accessPolicy and auth validation must match — drift means dead code or missing rules@navikt/oasis auto-caches OBO tokens — don't add your own cache layersub == oid — detect this to apply azp validationcluster:ns:app vs api://.../.default)azp against pre-authorized apps for M2M tokens.nais/ accessPolicy inbound rules