Gå til hovedinnhold

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:

ParameterVerdi
client_idmatrikkel-token-exchange

Well-known URL-er per miljø:

MiljøURL
devhttps://kc-test.matrikkel.no/auth/realms/matrikkelen-test/.well-known/openid-configuration
betatesthttps://kc-betatest.matrikkel.no/auth/realms/matrikkelen-test/.well-known/openid-configuration
prodtesthttps://auth.matrikkel.no/auth/realms/matrikkelen-test/.well-known/openid-configuration
prodhttps://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 != null && !isExpired(current) -> current
refreshToken != null -> fetchWithRefreshToken()
else -> fetchWithPassword()
}
}

private fun fetchWithPassword(): String =
sendTokenRequest(ResourceOwnerPasswordCredentialsGrant(username, Secret(password)))

private fun fetchWithRefreshToken(): String {
val token = refreshToken ?: return fetchWithPassword()
return runCatching { sendTokenRequest(RefreshTokenGrant(token)) }
.getOrElse { fetchWithPassword() }
}

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 isExpired(jwt: String): Boolean =
SignedJWT.parse(jwt).jwtClaimsSet.expirationTime?.before(Date()) ?: true
}

fun createMatrikkelKlient() {
OkHttpClient.Builder()
// Fase 0 – bytt ut med fase 1 under
.addInterceptor(basicAuth(username, password))
// Fase 1
// .addInterceptor(matrikkelToken(username, password, wellKnownUrl))
.build()
}

Fase 2 – Maskinporten (kommer)

Det er en strategisk målsetning at Matrikkelen skal ta i bruk nasjonale fellesløsninger fra Digitaliseringsdirektoratet, herunder Maskinporten for virksomhetsautentisering. Detaljer om denne migreringen vil bli publisert når løsningen er klar.