Developers guide for slices tokens

Introduction

For the exchange of information between the different services of Slices, we take inspiration from microservices architecture design, where information between services can be exchanged via JSON Web Tokens (JWT), which:

  • are versatile, as they can contain arbitrary data

  • are signed, which allows them to be verified without the need to contact the issuing party

Current usage in the Slices portal

The Slices Portal (https://portal.slices-ri.eu) is the authority for users and projects within Slices.

A project groups one or more users together, allowing them to collaborate and access resources together. A project has an expiration date: all resources requested within the project expire at the latest when the project expires.

User tokens

JSON Web Tokens that authenticate an user can be retrieved from the portal via an OAuth2 authentication flow, where they are passed as an OAuth access token to the service.

An example token looks as follows:

eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImVjMSJ9.eyJpc3MiOiJodHRwczovL2FjY291bnQuaWxhYnQuaW1lYy5iZSIsImF1ZCI6Imh0dHBzOi8vb3BlbmlkY29ubmVjdC5uZXQvIiwiaWF0IjoxNzIwNzkzODgyLCJleHAiOjE3MjA3OTc0ODIsImF1dGhfdGltZSI6MTcyMDc4ODIwMCwic3ViIjoidXNlcl9hY2NvdW50LmlsYWJ0LmltZWMuYmVfMG1hNHJrczA2czlreGFoeHJnamhnNnk0MWIiLCJzY29wZSI6Im9wZW5pZCB1c2VyaW5mbyBwcm9qZWN0cyIsInByZWZlcnJlZF91c2VybmFtZSI6InR3bG9jYWwifQ.-95HwFCIC3ucM5zGvxqLN5jkh491SZNKzR1cOIzh8dE2_YMiwrjDm_msmScRwxNSik4nBgO09SsfgeTpHEX1mA

This token is typically passed in the Authorization-header when making requests to the API

You can use jwt.io to decode this token, this will show you the following:

{
  "iss": "https://account.ilabt.imec.be",
  "aud": "https://openidconnect.net/",
  "iat": 1720793882,
  "exp": 1720797482,
  "auth_time": 1720788200,
  "sub": "user_account.ilabt.imec.be_0ma4rks06s9kxahxrgjhg6y41b",
  "scope": "openid userinfo projects",
  "preferred_username": "twlocal"
}

Project tokens

JSON Web Tokens that authenticate the project can be requested via the projects-API of the portal.

Retrieving all the projects the user has access to: GET call to https://portal.slices-ri.eu/apis/proj.slices-ri.eu/v1alpha1/projects/

$ curl --request GET \
  --url https://portal.slices-ri.eu/apis/proj.slices-ri.eu/v1alpha1/projects/ \
  --header 'Authorization: Bearer ey...mA'

This yields:

{
  "projects": [
    {
      "created_at": "2020-01-06T12:00:00Z",
      "description": "iLab.t development experiments and stable deployments",
      "expires_at": "2030-12-15T00:00:00Z",
      "id": "proj_account.ilabt.imec.be_5pzabws7n79ydsagfg3vntg590",
      "name": "ilabt-dev",
      "role": "member"
    }
  ]
}

Retrieving a project token:

GET call to https://portal.slices-ri.eu/apis/proj.slices-ri.eu/v1alpha1/projects/[project_id], with the Accept-header set to application/jwt:

curl --request GET \
  --url https://portal.slices-ri.eu/apis/proj.slices-ri.eu/v1alpha1/projects/proj_account.ilabt.imec.be_5pzabws7n79ydsagfg3vntg590 \
  --header 'Accept: application/jwt' \
  --header 'Authorization: Bearer ey..mA'

This results in the project token:

eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCtzbGljZXMuZXUvcHJvaiIsImtpZCI6ImVjMSJ9.eyJpc3MiOiJodHRwczovL2FjY291bnQuaWxhYnQuaW1lYy5iZSIsImlhdCI6MTcyMDc5Mzg4NCwiZXhwIjoxNzIwNzk3NDg0LCJzdWIiOiJwcm9qX2FjY291bnQuaWxhYnQuaW1lYy5iZV81OXozcWFja3AxOXZlc2h3OHlyOGt3czB5eiIsIm5hbWUiOiJ0d2xvY2FscHJvaiIsInByb2pfZXhwIjpudWxsLCJhY3QiOnsic3ViIjoidXNlcl9hY2NvdW50LmlsYWJ0LmltZWMuYmVfMG1hNHJrczA2czlreGFoeHJnamhnNnk0MWIiLCJyb2xlIjoibGVhZCJ9fQ.6FlXre8UHZ8ZDdtasLbvx04qKTwokGU6CPyteat3tByefe94fDlfJbCEnFRLVILEKesE1YOy_dYog6HUQ6E8ZA

After decoding this JWT, you get the following information:

{
  "iss": "https://account.ilabt.imec.be",
  "iat": 1720793884,
  "exp": 1720797484,
  "sub": "proj_account.ilabt.imec.be_59z3qackp19veshw8yr8kws0yz",
  "name": "twlocalproj",
  "proj_exp": "2030-12-15T00:00:00Z",
  "act": {
    "sub": "user_account.ilabt.imec.be_0ma4rks06s9kxahxrgjhg6y41b",
    "role": "lead"
  }
}

Note that this token also contains information on the user that requested it in the “act”-claim, together with the role of that user within the project. Possible roles are ‘lead’, ‘admin’, ‘member’ and ‘viewer’.

Validating the tokens

First, we need to check if the JWT has a valid signature. For this, we retrieve the JSON Web Key Set that lists the public keys that the portal currently uses from the well-known URL https://portal.slices-ri.eu/.well-known/jwks.json .

JWT parsing libraries typically have utility functions that allow you to easily use these JWKS.

For example, the Python library pyjwt ( which can be installed via the command $ pip3 install pyjwt) can be used as follows:

import jwt
from jwt import PyJWKClient

token = "ey..mA"

url = "https://portal.slices-ri.eu/.well-known/jwks.json"
jwks_client = PyJWKClient(url)
signing_key = jwks_client.get_signing_key_from_jwt(token)
data = jwt.decode(token, signing_key.key, algorithms=["ES256"])

print(data)

Which results in the following output at the time of writing:

{'iss': 'https://account.ilabt.imec.be', 'aud': 'https://openidconnect.net/', 'iat': 1720793882, 'exp': 1720797482, 'auth_time': 1720788200, 'sub': 'user_account.ilabt.imec.be_0ma4rks06s9kxahxrgjhg6y41b', 'scope': 'openid userinfo projects', 'preferred_username': 'twlocal'}

JWT are also validated by checking the “issued at” timestamp (iat) is before the current time and the “expires at” timestamp (exp) is after the current time. If you try to replicate this result after the token has expired (1720797482 is the Unix timestamp for ‘Fri Jul 12 2024 15:18:02 GMT+0000’), this will fail. You can temporarily disable this by configuring your JWT parsing library to relax its checks, for example:

data = jwt.decode(token, signing_key.key, algorithms=["ES256"], options={"verify_exp": False})

Using the Slices CLI

The following section shows how you can use the Slices CLI to get user and project JWT tokens, which can be useful during development.

If you do not have a Slices account yet, please register for one. After that, install the Slices CLI.

Authentication

$ slices auth login
Please go to https://portal.slices-ri.eu/oauth/authorize_device?user_code=VKVK-WXQW. Login and enter code
VKVK-WXQW to continue.
Successfully logged in as 'twalcari'

By going to the requested URL and following the instructions you will perform an OAuth authentication flow. The result is stored in the file ~/.slices/auth.json.

Projects

Listing your projects is done via the slices project list command:

$ slices project list
                                           Projects for twalcari
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓
┃ ID                                Name                    Role    Created At        Expires At       ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩
│ proj_account.ilabt.imec.be_5pza…  ilabt-dev               member  2020-01-06 13:00  2030-12-15 01:00 │
└──────────────────────────────────┴────────────────────────┴────────┴──────────────────┴──────────────────┘

To ease initial development, we have also implemented a command which allows you to retrieve the project token of a project of which you are a member:

$ slices project token ilabt-dev
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCtzbGljZXMuZXUvcHJvaiIsImtpZCI6ImVjMSJ9.eyJpc3MiOiJodHRwczovL2FjY291bnQuaWxhYnQuaW1lYy5iZSIsImlhdCI6MTcyMDc5MjI2MSwiZXhwIjoxNzIwNzk1ODYxLCJzdWIiOiJwcm9qX2FjY291bnQuaWxhYnQuaW1lYy5iZV81cHphYndzN243OXlkc2FnZmczdm50ZzU5MCIsIm5hbWUiOiJpbGFidC1kZXYiLCJwcm9qX2V4cCI6IjIwMzAtMTItMTVUMDA6MDA6MDBaIiwiYWN0Ijp7InN1YiI6InVzZXJfYWNjb3VudC5pbGFidC5pbWVjLmJlXzIyYmVzbnhrbTM5eXhzYW1wcXo5bTdlYmEzIiwicm9sZSI6Im1lbWJlciJ9fQ.q2-6oz7jkDIVF0dlrm6bvCPqzQ93sm-CZ6_ujbR3K2wnwLtO5yEI9HkuIByfAix7qVWZI3Dec5lK3sOkxq683A

Example server

The following code shows an example implementation of a server based on FastAPI that validates and parses the contents of the JWT token passed in the Authorization-header.

from typing import Annotated

import jwt
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.security import HTTPBearer
from jwt import PyJWKClient, PyJWTError

url = "https://portal.slices-ri.eu/.well-known/jwks.json"
jwks_client = PyJWKClient(url)

def decode_jwt(token):

    """Verify and decode token."""
    signing_key = jwks_client.get_signing_key_from_jwt(token)
    return jwt.decode(token, signing_key.key, algorithms=["ES256"])

class JWTBearer(HTTPBearer):
    """Defines that a JWT-token must be passed in the Authorization-header.""" 
    def __init__(self, auto_error: bool = True):
        super().__init__(auto_error=auto_error)

  
    async def __call__(self, request: Request):
        credentials = await super().__call__(request)
        if not credentials:
            raise HTTPException(status_code=403, detail="Invalid authorization code.")
        if not credentials.scheme == "Bearer":
            raise HTTPException(status_code=403, detail="Invalid authentication scheme.")
        if not self.verify_jwt(credentials.credentials):
            raise HTTPException(status_code=403, detail="Invalid token or expired token.")
        return credentials.credentials

    def verify_jwt(self, jwtoken: str) -> bool:
        try:
            decode_jwt(jwtoken)
        except PyJWTError:
            return False
        else:
            return True


app = FastAPI()
jwt_bearer_scheme = JWTBearer()

def get_auth_token(token: Annotated[str, Depends(jwt_bearer_scheme)]):
    """Retrieve the token from the Authorization-header and decodes it."""
    return decode_jwt(token)


@app.get("/hello/")
def hello(auth_token: Annotated[dict, Depends(get_auth_token)]):
    """Example API endpoint which returns the token contents."""
    return {"message": "hello world!", "auth_token": auth_token}

if __name__ == "__main__":
    # start server
    import uvicorn

    uvicorn.run(app)

You’ll need the following dependencies to run it:

$ pip install pyjwt fastapi uvicorn

When running this file with python3 server.py it will start a server on port 8000. The API endpoint can be reached on http://localhost:8000/hello . FastAPI also serves an OpenAPI browser on http://localhost:8000/docs that you can use to try out this code.

Token generation example

To generate a signed JWT token, you need to generate a keypair to sign it with. To generate an ES256-compatible keypair, use:

$ openssl ecparam -name prime256v1 -genkey -noout -out private.ec.key
$ openssl ec -in private.ec.key -pubout -out public.pem

You can then add the following code to the example server to generate a token that is valid for 15 minutes:

from datetime import datetime, UTC
from pathlib import Path
from fastapi import Response

JWT_ISSUER = "http://example.slices-ri.eu"
JWT_LIFETIME = 15 * 60  # seconds

PRIVATE_KEY = Path("private.ec.key")

@app.get("/token")
def token():
    """Return JWT token."""

    with PRIVATE_KEY.open() as f:
        privkey = f.read()

    token = jwt.encode(
        {
            "iss": JWT_ISSUER,
            "iat": datetime.now(UTC).timestamp(),
            "exp": datetime.now(UTC).timestamp() + JWT_LIFETIME,
            "hello": "world",
        },
        privkey,
        headers={"kid": PRIVATE_KEY_ID},
        algorithm="ES256",
    )
    return Response(token, media_type="application/jwt")

To allow clients to validate the signature of your JWT, you need to expose your public key as ‘key1’ in a JSON Web Key Set at /.well-known/jwks.json.