Sometimes it's necessary to call an aID-endpoint without having a valid OAuth access tokens, other times it's necessary to call APIs that require more access than is provided by the access tokens. Traditionally aID has used signed requests for this. This is a home made solution. aID is now transitioning to using JWT tokens. This mechanism has become a defacto standard for authorization between servers, and is a part of OAuth. The caller, called a client will have to request an m2m-token called a client_credential before calling APIs that need a certain permission.  until it's no longer valid and then reused in later calls.
The implementation implements this specification:
The M2M client credentials are in the format called urn:ietf:params:oauth:grant-type:jwt-bearer. When calling APIs this credential should be provided to the API as a bearer token in the Authorization header, like this:
Client credentials introduce some new concepts:
The subject (sub) is who the token was created for. In this context it's the client that will later call aID APIs. It's the Client ID of the OAuth client in aID.
An audience (aud) in this context is who this token was intended for, "The intended recipient of the token (the resource server).", in other words the API the client wants to call to get some data or do something.
When configuring which APIs a client is allowed to call in aID, audience is one of the parameters to limit this.
Some example audiences:
- public-aid-user-api(GET /api/aid/users/self etc.)
- public-aid-access-api(POST /api/aid/users|groups/:uuid/access etc.)
- public-aid-groups-api(GET /api/aid/users/self/groups etc.)
Scopes are often linked to attribute names. So a client can have uuid, name, phone as scopes. In m2m tokens we are more specific, so we add an operation to the attributes. For example name:read means that the token only gives access to reading name, not writing to name. If access to update something is required, the scope would be name:write for example.
The client credential should be provided as a bearer token in the Authorization header, like this:
Note that the content of the header is not the token, the word Bearer is also a part of the header!
The API is responsible for verifying the token, but the request will fail if it's not valid anymore, so the client should also verify that it isn't expired before trying to use it. The API will verify that the JWT is valid, that it's signed by aID, that the API is included in the audience and that the scope is sufficient.
Before using the token, it needs to be fetched from aID. This can be done like this:
$ curl -X POST https://www.aid.no/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=c4feb4b3" \
  -d "client_secret=1c3b00d4" \
  -d "scope=uuid:read%20name:read"
The response will then look like this:
{
  "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImhHdUNHM1Q5aXlDNWQ1dkdYVnc3aF9kTzVLUmh4ZnUzM192d21iNG1XWWsiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsicHVibGljLWFpZC1ncm91cC1hcGkiLCJwdWJsaWMtYWlkLXVzZXItYXBpIl0sImNsaWVudF9pZCI6ImM0ZmViNGIzIiwiZXhwIjoxNzYxMDUyMzA3LCJpYXQiOjE3NjEwNDg3MDcsImlzcyI6Imh0dHBzOi8vd3d3LmFpZC5uby5sb2NhbGhvc3QuYXBpLm5vLyIsImp0aSI6IjY0MGU0ZDM5LTRjMTktNDc3YS1iZmRhLTlmNjZjNDU2ZDA1OSIsImxlZ2FsX2VudGl0eSI6ImFtZWRpYSIsIm5iZiI6MTc2MTA0ODcwNywic2NvcGUiOiJ1dWlkOnJlYWQgbmFtZTpyZWFkIiwic3ViIjoiYzRmZWI0YjMifQ.fIbnIWVfG3TNRt4e8PznxudYFFI0gwTbz2Tzb4rE_Vhx8N0BmwLmjd15aohYj1XszK9ot5oYOiJWobYLiizPnxS676MIkvafcBFOpm6HN49k858Phfnhyk20dvu18FsU1Xvqvx00OWSAHtFCSI4KX1QDRs5xA2esoBHuTZcHCVPAA1IAMyaCT_XvqGQ3dgKFdUlLVb3gWn86ZfAdojRj_Jvg4rIeD-q6RElmDcjqVx5dN4wybONJ_VPHVlUg_BwdoWnoQd4EZZDuD1cCmmF6kBgYNCxbkE0MPHhnRVfRoMwqRruSR4WfWqYexufnA8enYTk-htG3MmgXd0cGOtrTIg",
  "token_type": "Bearer",
  "expires_in": 3599,
  "scope": "uuid:read name:read"
}
This access token can then be inspected at < jwt.io > for example, and it would look like this: {
  "aud": ["public-aid-group-api", "public-aid-user-api"],
  "client_id": "c4feb4b3",
  "exp": 1761052307,
  "iat": 1761048707,
  "iss": "https://www.aid.no/",
  "jti": "640e4d39-4c19-477a-bfda-9f66c456d059",
  "legal_entity": "amedia",
  "nbf": 1761048707,
  "scope": "uuid:read name:read",
  "sub": "c4feb4b3"
}
Here we can see that the token has been created by aID (if you actually inspect the JWT in  jwt.io , you will see that it's created by aID in a dev-evironment, so it' not actually valid in production), it's created for the expected client and contains the audience and scope we want. The entire access_token attribute string should be used as Bearer in calls to APIs, like this: $ curl --insecure -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImhHdUNHM1Q5aXlDNWQ1dkdYVnc3aF9kTzVLUmh4ZnUzM192d21iNG1XWWsiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsicHVibGljLWFpZC1ncm91cC1hcGkiLCJwdWJsaWMtYWlkLXVzZXItYXBpIl0sImNsaWVudF9pZCI6ImM0ZmViNGIzIiwiZXhwIjoxNzYxMDUyMzA3LCJpYXQiOjE3NjEwNDg3MDcsImlzcyI6Imh0dHBzOi8vd3d3LmFpZC5uby5sb2NhbGhvc3QuYXBpLm5vLyIsImp0aSI6IjY0MGU0ZDM5LTRjMTktNDc3YS1iZmRhLTlmNjZjNDU2ZDA1OSIsImxlZ2FsX2VudGl0eSI6ImFtZWRpYSIsIm5iZiI6MTc2MTA0ODcwNywic2NvcGUiOiJ1dWlkOnJlYWQgbmFtZTpyZWFkIiwic3ViIjoiYzRmZWI0YjMifQ.fIbnIWVfG3TNRt4e8PznxudYFFI0gwTbz2Tzb4rE_Vhx8N0BmwLmjd15aohYj1XszK9ot5oYOiJWobYLiizPnxS676MIkvafcBFOpm6HN49k858Phfnhyk20dvu18FsU1Xvqvx00OWSAHtFCSI4KX1QDRs5xA2esoBHuTZcHCVPAA1IAMyaCT_XvqGQ3dgKFdUlLVb3gWn86ZfAdojRj_Jvg4rIeD-q6RElmDcjqVx5dN4wybONJ_VPHVlUg_BwdoWnoQd4EZZDuD1cCmmF6kBgYNCxbkE0MPHhnRVfRoMwqRruSR4WfWqYexufnA8enYTk-htG3MmgXd0cGOtrTIg.eyJpc3MiOiJodHRwczovL3d3dy5haWQubm8ubG9jYWxob3N0LmFwaS5uby8iLCJzdWIiOiJhbWVkaWEtYWJvLUZTMlJMczIwQlFyVndlT2kiLCJhdWQiOlsicHVibGljLWFpZC1ncm91cC1hcGkiLCJwdWJsaWMtYWlkLXVzZXItYXBpIl0sImV4cCI6MTc2MDAwOTE3MywiaWF0IjoxNzYwMDA1NTczLCJuYmYiOjE3NjAwMDU1NzMsImNsaWVudF9pZCI6ImFtZWRpYS1hYm8tRlMyUkxzMjBCUXJWd2VPaSIsImp0aSI6IjRiNWE3NzBjLTA0YjktNGE4Ni1iY2I5LWVlZTg2M2NkOTFlNyJ9.LHAclzDkNc_VmrObmLxnEDJoYP3-BNQ7Za-NwYnXIRWA1U0fwPZc-0EVsHRKFb_DEefgNWC-XJVAdoRMAwshOUd99T4fF9Xc63KKAS8Sv1qLrUqv5LqSKOC6N4h9OFXqt5XamPUk15KmVwTBLj88CqpQBQ1en0jk3M7LEfXIJIYwFNVya1advd_fKIgRZq0xTBPNCoMFGdVxBiVEJnSGhTujPwyg6BY6nldmq_mDZqugTNTreAR970hVG9Fzm2f4IwKKNgrvmCkRhUM_TbE1zdKgkklmG9BjCMOn1b94wIiLH-Xrr8mz1M-aHyPNZIx4VZxX-kFogMPIbfYS-sUoNA' https://www.aid.no.localhost.api.no/aid/api/users/e319371f-ea7a-4ee5-97ce-710eed6819da/groups
To make sure the signature can be verified, you can also have a look at the header in  jwt.io , which should look like this (with a real production token, not the one above): {
  "alg": "RS256",
  "kid": "cxzSmMQFSud_fVId1vVDUpXR1CxrWFeeISmH5ghVQIE",
  "typ": "JWT"
}
The public key that will be used for verifying this signature can be found like this (or by using OIDC):
$ curl https://www.aid.no/.well-known/jwks.json
The response looks like this:
{
  "keys": [
    {
      "e": "AQAB",
      "kid": "aid-jwt",
      "kty": "RSA",
      "n": "z5DUia4JkiryUMGrjSxPyiY1gpSIXxcA9nl2KC4jstGnVcmlxlo0MFqzgLrPWkn9MJc29cyZAI5c4OqAcPbycjNxbz8Ev5zFkzKGSaMhVHuUMtUdsdq687k7ykxhgm2pplXKaPswZqkCqtFrBtmLnbemwXXjCDY1x9mbpY7pnIUaAVPZkDMyp31QZgrvRS_XBU_GMne3mDvvsFd2ZXg4eqQiggy5vg8wg2jYIroiVAEXf5Fhswj0mETDeTGOr4IZObzdGcQcpW6e6EnnPoJaiNYhGqIysHc5M9ewE9wJH5T0_DxjXbuf0gN62OiUXCkpPpd9-PkoEsKDXPS8GFd6Aw",
      "use": "sig"
    },
    {
      "e": "AQAB",
      "kid": "cxzSmMQFSud_fVId1vVDUpXR1CxrWFeeISmH5ghVQIE",
      "kty": "RSA",
      "n": "z5DUia4JkiryUMGrjSxPyiY1gpSIXxcA9nl2KC4jstGnVcmlxlo0MFqzgLrPWkn9MJc29cyZAI5c4OqAcPbycjNxbz8Ev5zFkzKGSaMhVHuUMtUdsdq687k7ykxhgm2pplXKaPswZqkCqtFrBtmLnbemwXXjCDY1x9mbpY7pnIUaAVPZkDMyp31QZgrvRS_XBU_GMne3mDvvsFd2ZXg4eqQiggy5vg8wg2jYIroiVAEXf5Fhswj0mETDeTGOr4IZObzdGcQcpW6e6EnnPoJaiNYhGqIysHc5M9ewE9wJH5T0_DxjXbuf0gN62OiUXCkpPpd9-PkoEsKDXPS8GFd6Aw",
      "use": "sig"
    }
  ]
}
The key with kid cxzSmMQFSud_fVId1vVDUpXR1CxrWFeeISmH5ghVQIE should be used to verify this JWT. The other key here is a duplicate with a different kid for backward compatibility. The kid should be found in the JWT header, not hardcoded. 
Unless the API-calls are only once an hour or less frequent, the token should be cached in memory in the client. The token should then be refreshed just before it expires. The easiest way to implement this is by keeping the expires_in part of the token response and set up some kind of timer before that happens. Also make sure that the token pool never returns an expired token, it's better to block while fetching a new one than to use an expired token, since this will potentially result in a user facing error. 
Here are some libraries that handles client credentials correctly, including caching:
:
:
: