Migrering fra basic auth til token-autentisering
Dagens løsning for maskinbrukere (M2M) benytter basic auth, der brukernavn og passord sendes med i hver enkelt forespørsel. Det langsiktige målet er å flytte alle integrasjoner over til Maskinporten som nasjonal fellesløsning for virksomhetsautentisering.
Som et første steg tilbyr Matrikkelen nå et eget token-endepunkt, slik at maskinbrukere kan autentisere seg én gang og deretter bruke kortlevde tokens (JWT) i stedet for å sende brukernavn og passord i hver forespørsel.
Fase 1 – Token fra Matrikkelen
Matrikkelen tilbyr token-endepunkt i henhold til OpenID Connect-standarden. Token-endepunktet finnes via well-known URL
for det aktuelle miljøet, og tokenet brukes som Bearer-token i alle videre kall mot Matrikkelen.
Klientinformasjon
Klient-ID er den samme på tvers av alle miljøer:
| Parameter | Verdi |
|---|---|
client_id | matrikkel-token-exchange |
Well-known URL-er per miljø:
| Miljø | URL |
|---|---|
| dev | https://kc-test.matrikkel.no/auth/realms/matrikkelen-test/.well-known/openid-configuration |
| betatest | https://kc-betatest.matrikkel.no/auth/realms/matrikkelen-test/.well-known/openid-configuration |
| prodtest | https://auth.matrikkel.no/auth/realms/matrikkelen-test/.well-known/openid-configuration |
| prod | https://auth.matrikkel.no/auth/realms/matrikkelen-prod/.well-known/openid-configuration |
Hente token
Token-endepunktet (token_endpoint) bør hentes dynamisk fra well-known URL-en for det aktuelle miljøet, fremfor å
hardkodes. Dette sikrer at integrasjonen din automatisk plukker opp eventuelle endringer i endepunktadressen.
Send en POST-forespørsel mot token_endpoint med brukernavn og passord for å få utstedt et token:
POST {{token_endpoint}}
Content-Type: application/x-www-form-urlencoded
grant_type=password&
client_id=matrikkel-token-exchange&
username={{username}}&
password={{password}}
Responsen vil inneholde et access_token og et refresh_token:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIi[...]wia2lYkisFKY3jZPBwTSPwSMcHyuMt22B0BOwZ4Z0CQ",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "eyJhbGciOiJIUzUxMiIsInR5cCIg[...]pA9J5wO9tYbiMHZV_E6biD4bItcg_yO1iV6eF9cZAEUTvApKDT30srcSw",
"token_type": "Bearer",
"not-before-policy": 1619516791,
"session_state": "PE4[...]AxLUH4b",
"scope": "openid microprofile-jwt"
}
Bruk access_token som Authorization: Bearer <access_token> i alle kall til Matrikkelen.
Fornye token
Når access tokenet er utløpt, kan det fornyes med refresh_token – uten å sende brukernavn og passord på nytt:
POST {{token_endpoint}}
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&
refresh_token=eyJhbGciOiJIUzUxMiIsInR5cCIg[...]pA9J5wO9tYbiMHZV_E6biD4bItcg_yO1iV6eF9cZAEUTvApKDT30srcSw
Eksempel – Kotlin (OkHttp)
Eksempelet bruker Nimbus OAuth 2.0 SDK for OIDC-discovery og token-håndtering, samt Nimbus JOSE+JWT for å lese utløpstidspunktet fra JWT-en.
// build.gradle
implementation("com.nimbusds:oauth2-oidc-sdk:11.x")
implementation("com.nimbusds:nimbus-jose-jwt:9.x")
import com.nimbusds.jose.crypto.factories.DefaultJWSVerifierFactory
import com.nimbusds.jwt.SignedJWT
import com.nimbusds.oauth2.sdk.*
import com.nimbusds.oauth2.sdk.auth.Secret
import com.nimbusds.oauth2.sdk.id.ClientID
import com.nimbusds.oauth2.sdk.token.RefreshToken
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata
import java.net.URL
import java.util.Date
/**
* Fase 0 - Basic auth (eksisterende løsning)
*/
fun basicAuth(username: String, password: String): Interceptor {
return HeadersInterceptor {
mapOf("Authorization" to "Basic ${base64("${username}:${password}")}")
}
}
/**
* Fase 1 - Token fra Matrikkelen
*
* Skaffer et gyldig token ved å:
* 1. Bruke eksisterende access_token hvis det fortsatt er gyldig
* 2. Fornye med refresh_token hvis access_token er utløpt
* 3. Gjøre et nytt password grant hvis begge er utløpt
*/
const val MATRIKKEL_CLIENT_ID = "matrikkel-token-exchange"
fun matrikkelToken(username: String, password: String, wellKnownUrl: String): Interceptor {
val tokenClient = MatrikkelTokenClient(username, password, wellKnownUrl)
return HeadersInterceptor {
mapOf("Authorization" to "Bearer ${tokenClient.getToken()}")
}
}
private class MatrikkelTokenClient(
private val username: String,
private val password: String,
private val wellKnownUrl: String,
) {
private val clientID = ClientID(MATRIKKEL_CLIENT_ID)
private val providerMetadata: OIDCProviderMetadata by lazy {
OIDCProviderMetadata.parse(URL(wellKnownUrl).readText())
}
private var accessToken: String? = null
private var refreshToken: RefreshToken? = null
fun getToken(): String {
val current = accessToken
return when {
current.isNotExpired() -> current
refreshToken != null -> sendTokenRequest(RefreshTokenGrant(refreshToken))
else -> sendTokenRequest(ResourceOwnerPasswordCredentialsGrant(username, Secret(password)))
}
}
private fun sendTokenRequest(grant: AuthorizationGrant): String {
val request = TokenRequest(providerMetadata.tokenEndpointURI, clientID, grant)
val tokens = TokenResponse
.parse(request.toHTTPRequest().send())
.toSuccessResponse()
.tokens
accessToken = tokens.accessToken.value
refreshToken = tokens.refreshToken
return tokens.accessToken.value
}
private fun String?.isNotExpired(): Boolean {
return SignedJWT.parse(this)?.jwtClaimsSet?.expirationTime?.after(Date()) ?: false
}
}
fun createMatrikkelKlient() {
OkHttpClient.Builder()
// Fase 0 – bytt ut med fase 1 under
.addInterceptor(basicAuth(username, password))
// Fase 1
// .addInterceptor(matrikkelToken(username, password, wellKnownUrl))
// Fase 2
// .addInterceptor(matrikkelMaskinportenToken(username, maskinportenTokenClient))
.build()
}
Fase 2 – Maskinporten (Anbefalt)
Generell dokumentasjon av maskinporten og bruk av maskinporten finnes på digdir sine sider; her.
Tilganger kan tildeles av kartverkets kundesenter om din organisasjon selv trenger tilgang, eller bli delegert videre fra en organisasjon som allerede har tilgang om dere skal opptre på vegne av dem.
For at tidligere roller og tilganger i matrikkelen fortsatt skal kunne brukes kreves det at det er etablert en kobling mellom brukeren og organisasjonen. Dette gjøres av kartverkets kundesenter, og må søkes om av eieren av brukeren.
Token til matrikkelen
Tilgang til matrikkelen benytter seg av scopet kartverk:matrikkel:brukernavn,
samtidig kreves det at audience er satt i henhold til tabellen nedenfor (Audience begrenset tokens) *.
| Miljø | Resource/Audience |
|---|---|
| Kurs | https://kurs.matrikkel.no |
| Test (betatest, prodtest etc) | https://test.matrikkel.no |
| Produksjon | https://matrikkel.no |
* Dette oppnåes ved å inkludere resource claimet når man henter tokenet sitt fra maskinporten.
Videre må brukernavnet til en eksisterende bruker i matrikkelen (med orgnr tilknytning) inkluderes i headeren X-Matrikkel-Brukernavn.
Eksempelvis:
fun matrikkelMaskinportenToken(
username: String,
maskinportenTokenClient: MaskinportenTokenClient
): Interceptor {
val token = maskinportenTokenClient.getToken("kartverk:matrikkel:brukernavn")
return HeadersInterceptor {
mapOf(
"Authorization" to "Bearer $token",
"X-Matrikkel-Brukernavn" to username,
)
}
}
Det finnes etablerte bibliotek som kan brukes for henting av tokens fra Maskinporten. F.eks https://github.com/ks-no/fiks-maskinporten