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
.