Skip to content

Consume external API using MaskinportenΒΆ

This how-to guides you through the steps required to consume an API secured with Maskinporten:

Configure your applicationΒΆ

Declare all the scopes that you want to consume in your application's NAIS manifest so that your application is granted access to them:

nais.yaml
spec:
  maskinporten:
    enabled: true
    scopes:
      consumes:
        - name: "skatt:some.scope"
        - name: "nav:some/other/scope"
Maskinporten configuration changes are eventually consistent

Changes to the Maskinporten configuration may take up to 15 minutes to propagate.

If you're experiencing issues with access to these scopes, wait a few minutes before trying again.

The scopes themselves are defined and owned by the external API provider. The exact scope values must be exchanged out-of-band.

Ensure that organization has access to scopes

Make sure that the provider has granted NAV (organization number 889640782) access to any scopes that you wish to consume.

Provisioning of client will fail otherwise.

Use webproxy for outbound network connectivity from on-premises environments

If you're on-premises, you must also enable and use webproxy to access Maskinporten.

Finally, configure appropriate outbound access policies to access the external API endpoints.

Acquire tokenΒΆ

To acquire a token, you can either:

Acquire tokens with TexasΒΆ

Texas is not enabled by default

See the Texas documentation for more information.

Send a HTTP POST request to the endpoint found in the NAIS_TOKEN_ENDPOINT environment variable. The request must have a Content-Type header set to either:

  • application/json or
  • application/x-www-form-urlencoded

The body of the request should contain the following parameters:

Parameter Example Value Description
identity_provider maskinporten Always maskinporten.
target example:some.scope Whitespace-separated list of scopes that you want in the issued token from Maskinporten.

If the API provider requires the use of an audience-restricted token, you must also include the following parameter in the request:

Parameter Example Value Description
resource https://some-provider.example/api Optional. Target audience for the token returned by Maskinporten. The exact value is defined by the API provider and exchanged out-of-band.
Token request
POST ${NAIS_TOKEN_ENDPOINT} HTTP/1.1
Content-Type: application/json

{
    "identity_provider": "maskinporten",
    "target": "example:some.scope"
}
Token request
POST ${NAIS_TOKEN_ENDPOINT} HTTP/1.1
Content-Type: application/x-www-form-urlencoded

identity_provider=maskinporten&
target=example:some.scope
Successful response
{
    "access_token": "eyJra...",
    "expires_in": 3599,
    "token_type": "Bearer"
}

Your application does not need to validate this token.

Tokens are cached by default with regards to the expires_in field. To forcibly fetch a new token, set the skip_cache=true parameter in the request.

Acquire tokens manuallyΒΆ

To acquire a token from Maskinporten, you will need to create a client assertion.

Create client assertionΒΆ

The client assertion is a JWT that consists of a header, a payload and a signature.

The header should consist of the following parameters:

Parameter Example Value Description
kid <kid-from-JWK> The key identifier of the private JWK used to sign the assertion. The private key is found in the MASKINPORTEN_CLIENT_JWK environment variable.
typ JWT Represents the type of this JWT. Set this to JWT.
alg RS256 Represents the cryptographic algorithm used to secure the JWT. Set this to RS256.

The payload should have the following claims:

Claim Example Value Description
aud https://test.maskinporten.no/ The audience of the token. Set to the MASKINPORTEN_ISSUER environment variable.
iss 60dea49a-255b-48b5-b0c0-0974ac1c0b53 The issuer of the token. Set to the MASKINPORTEN_CLIENT_ID environment variable.
scope nav:test/api scope is a whitespace-separated list of scopes that you want in the issued token from Maskinporten.
iat 1698435010 iat stands for issued at. Set to now.
exp 1698435070 exp is the expiration time. Between 1 and 120 seconds after now. Typically 30 seconds is fine
jti 2d1a343c-6e7d-4ace-ae47-4e77bcb52db9 The JWT ID of the token. Used to uniquely identify a token. Set this to a unique value such as an UUID.

If the API provider requires the use of an audience-restricted token, you must also include the following claim:

Claim Example Value Description
resource https://api.some-provider.no/ Target audience for the token returned by Maskinporten. The exact value is defined by the API provider and exchanged out-of-band.

Finally, create a signature for the client assertion.

Example Code for Creating a Client Assertion

The sample code below shows how to create and sign a client assertion in a few different languages:

Minimal example code for creating a client assertion in Kotlin, using Nimbus JOSE + JWT.

import com.nimbusds.jose.*
import com.nimbusds.jose.crypto.*
import com.nimbusds.jose.jwk.*
import com.nimbusds.jwt.*
import java.time.Instant
import java.util.Date
import java.util.UUID

val clientId: String = System.getenv("MASKINPORTEN_CLIENT_ID")
val clientJwk: String = System.getenv("MASKINPORTEN_CLIENT_JWK")
val issuer: String = System.getenv("MASKINPORTEN_ISSUER")
val scope: String = "nav:test/api"
val rsaKey: RSAKey = RSAKey.parse(clientJwk)
val signer: RSASSASigner = RSASSASigner(rsaKey.toPrivateKey())

val header: JWSHeader = JWSHeader.Builder(JWSAlgorithm.RS256)
    .keyID(rsaKey.keyID)
    .type(JOSEObjectType.JWT)
    .build()

val now: Date = Date.from(Instant.now())
val expiration: Date = Date.from(Instant.now().plusSeconds(60))
val claims: JWTClaimsSet = JWTClaimsSet.Builder()
    .issuer(clientId)
    .audience(issuer)
    .issueTime(now)
    .claim("scope", scope)
    .expirationTime(expiration)
    .jwtID(UUID.randomUUID().toString())
    .build()

val jwtAssertion: String = SignedJWT(header, claims)
    .apply { sign(signer) }
    .serialize()

Minimal example code for creating a client assertion in Python, using PyJWT.

import json, jwt, os, uuid
from datetime import datetime, timezone, timedelta
from jwt.algorithms import RSAAlgorithm

issuer = os.getenv('MASKINPORTEN_ISSUER')
jwk = os.getenv('MASKINPORTEN_CLIENT_JWK')
client_id = os.getenv('MASKINPORTEN_CLIENT_ID')

header = {
    "kid": json.loads(jwk)['kid']
}

payload = {
    "aud": issuer,
    "iss": client_id,
    "scope": "nav:test/api",
    "iat": datetime.now(tz=timezone.utc),
    "exp": datetime.now(tz=timezone.utc)+timedelta(minutes=1),
    "jti": str(uuid.uuid4())
}

private_key = RSAAlgorithm.from_jwk(jwk)
jwtAssertion = jwt.encode(payload, private_key, "RS256", header)

Request token from MaskinportenΒΆ

Request

The token request is an HTTP POST request. It must have the Content-Type header set to application/x-www-form-urlencoded.

The body of the request should contain the following parameters:

Parameter Value Description
grant_type urn:ietf:params:oauth:grant-type:jwt-bearer Type of grant the client is sending. Always urn:ietf:params:oauth:grant-type:jwt-bearer.
assertion eyJraWQ... The client assertion itself. It should be unique and only used once.

Send the request to the token_endpoint, i.e. the URL found in the MASKINPORTEN_TOKEN_ENDPOINT environment variable:

POST ${MASKINPORTEN_TOKEN_ENDPOINT} HTTP/1.1
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&
assertion=eY...

Response

Maskinporten will respond with a JSON object that contains the access token. Your application does not need to validate this token.

{
    "access_token": "eyJraWQ...",
    "expires_in": 3599,
    ...
}
Cache your tokens

The expires_in field denotes the lifetime of the token in seconds.

Cache and reuse the token until it expires to minimize network latency impact.

A safe cache key for this flow is key = $scope.

See the Maskinporten token documentation for more details.

Consume APIΒΆ

Once you have acquired a new token, you can finally consume the external API by using the token as a Bearer token:

GET /resource HTTP/1.1

Host: api.example.com
Authorization: Bearer eyJraWQ...