Skip to content

Maskinporten Client

The NAIS platform provides support for declarative provisioning of Maskinporten clients.

A client allows your application to integrate with Maskinporten to acquire access tokens. These tokens authenticate your application when consuming external APIs whom require Maskinporten tokens.

Spec

nais.yaml
spec:
  maskinporten:
    enabled: true 
    scopes:
      consumes:
        - name: "skatt:some.scope"
        - name: "nav:some/other/scope"

  # required for on-premises only
  webproxy: true

See the NAIS manifest reference for the complete specification.

Network Connectivity

Maskinporten is an external service. The platform automatically configures outbound access to the Maskinporten hosts.

You do not have to explicitly configure outbound access to Maskinporten yourselves in GCP.

If you're on-premises however, you must enable and use webproxy.

Runtime Variables & Credentials

Your application will automatically be injected with both environment variables and files at runtime. You can use whichever is most convenient for your application.

The files are available at the following path: /var/run/secrets/nais.io/maskinporten/

Name Description
MASKINPORTEN_CLIENT_ID Client ID that uniquely identifies the client in Maskinporten.
MASKINPORTEN_CLIENT_JWK Private JWK (RSA) for the client.
MASKINPORTEN_SCOPES Whitespace-separated string of scopes registered for the client.
MASKINPORTEN_WELL_KNOWN_URL The well-known URL for the metadata discovery document
MASKINPORTEN_ISSUER issuer from the metadata discovery document.
MASKINPORTEN_TOKEN_ENDPOINT token_endpoint from the metadata discovery document.

These variables are used when acquiring tokens.

Getting Started

To consume external APIs, you will need to do three things:

  1. Declare the scopes that you want to consume
  2. Acquire tokens from Maskinporten
  3. Consume the API using the token

1. Declare Consumer Scopes

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

nais.yaml
spec:
  maskinporten:
    enabled: true
    scopes:
      consumes:
        - name: "skatt:some.scope"
        - name: "nav:some/other/scope"

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

Make sure that the provider has granted NAV (organization number 889640782) consumer access to any scopes that you wish to consume. Provisioning of client will fail otherwise.

2. Acquire Token

To acquire a token from Maskinporten, you will need to create a JWT grant.

A JWT grant is a JWT that is used to authenticate your client with Maskinporten. The token is signed using a private key belonging to your client.

2.1. Create JWT Grant

The JWT consists of a header, a payload and a signature.

The header should consist of the following parameters:

Parameter 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 this to the Maskinporten issuer, i.e. MASKINPORTEN_ISSUER.
iss 60dea49a-255b-48b5-b0c0-0974ac1c0b53 The issuer of the token. Set this to your client_id, i.e. MASKINPORTEN_CLIENT_ID.
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. Timestamp (seconds after Epoch) for when the JWT was issued or created.
exp 1698435070 exp is the expiration time. Timestamp (seconds after Epoch) for when the JWT is no longer valid. This must be less than 120 seconds after iat. That is, the maximum lifetime of the token must be no greater than 120 seconds.
jti 2d1a343c-6e7d-4ace-ae47-4e77bcb52db9 The JWT ID of the token. Used to uniquely identify a token. Set this to a UUID or similar.

The JWT grant should be unique and only used once. That is, every token request to Maskinporten should have a unique JWT grant:

  • Set the JWT ID (jti) claim to a unique value, such as an UUID.
  • Set the JWT expiry (exp) claim so that the lifetime of the token is reasonably low:
    • The maximum lifetime allowed is 120 seconds.
    • A lifetime between 10-30 seconds should be fine for most situations.

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, a signature is created by hashing the header and payload, and then signing the hash using your client's private key.

Example Code for Creating a JWT Grant

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

Minimal example code for creating a JWT grant 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 jwtGrant: String = SignedJWT(header, claims)
    .apply { sign(signer) }
    .serialize()

Minimal example code for creating a JWT grant 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)
grant = jwt.encode(payload, private_key, "RS256", header)

2.2. Request Token from Maskinporten

Request

The token request is an HTTP POST request. It should have the Content-Type 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 JWT grant itself.

Send the request to the token_endpoint, i.e. MASKINPORTEN_TOKEN_ENDPOINT:

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.

{
    "access_token": "eyJraWQ...",
    "token_type": "Bearer",
    "expires_in": 3599,
    "scope": "nav:test/api"
}
Parameter Description
access_token The access token that you may use to consume an external API.
token_type The token type. Should always be Bearer.
expires_in The lifetime of the token in seconds. Cache and reuse the token until it expires to minimize network latency impact.
scope A list of scopes issued in the access token.

See the Maskinporten token documentation for more details.

3. Consume API

Once you have acquired the token, you can finally consume the external API.

Use the token in the Authorization header as a Bearer token:

GET /resource HTTP/1.1

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

Success!