Cedarling Sidecar Tutorial#
Introduction#
The goal of this tutorial is to show how to use the Cedarling Sidecar. To demonstrate this, we'll build a very simple Python Flask API Gateway that calls the Cedarling sidecar Authzen REST endpoint to authorize access based on the content of a JWT access token.
Sequence diagram#
sequenceDiagram
title API Gateway
actor User
participant Gateway
participant Sidecar
User->Gateway: GET /protected\nAuthorization: Bearer ...
Gateway->Sidecar: POST /cedarling/evaluation\n{"subject": {"type": "JWT", "id": "cedarling", "properties": {"access_token": "..."} }}
Sidecar->Sidecar: cedarling.authorize(access_token)
alt ALLOW
Sidecar->Gateway: {"decision": true}
Gateway->User: 200 {"result": "protected resource"}
else DENY
Sidecar->Gateway: {"decision": false, "context": {...}}
Gateway->User: 403 Forbidden
end
Sample Authzen request#
{
"subject": {
"type": "token_bundle",
"id": "some_id",
"properties": {
"access_token": ""
}
},
"resource": {
"type": "Jans::HTTP_Request",
"id": "some_id",
"properties": {
"header": {},
"url": {
"protocol": "http",
"host": "www.acme.tld",
"path": "/protected"
}
}
},
"action": {
"name": "Jans::Action::\"GET\""
},
"context": {}
}
Prerequisites#
Before you begin, make sure you have the following:
Docker
installed on your machinePython 3.10
or higherpipx
andPoetry
installed if you're not using Docker
Policy Store Setup (Agama Lab)#
To begin using Cedarling, you need to set up a policy store. We’ll use this Agama Lab for this purpose.
- Sign in to Agama Lab using your GitHub account and click on Policy Designer.
- Choose a repository to store your Cedarling policies and schemas. Ensure that the repository has at least one commit on the default branch.
- After initialization, create a policy store named
gatewayDemo
. - Open the policy store and navigate to Policies.
- Click
Add Policy
, selectText Editor
. - Paste the following Cedar policy:
@id("allow_one") permit( principal is Jans::Workload, action == Jans::Action::"GET", resource is Jans::HTTP_Request ) when { principal has access_token.scope && principal.access_token.scope.contains("openid") };
- Click
Save
. Agama Lab will validate your policy. -
Next, click on
Trusted Issuers
and add the following issuer:- Name:
Gluu
- Description:
Gluu
- OpenID Configuration Endpoint:
https://account.gluu.org/.well-known/openid-configuration
- Copy the URL for your policy store; you'll need it for the sidecar setup.
- Name:
Deploy Cedarling Sidecar#
Docker Instructions#
Create a file named bootstrap.json
. You may use the sample file.
- Set
CEDARLING_POLICY_STORE_URI
to the URL you copied from Agama Lab. - Set
CEDARLING_USER_AUTHZ
to "disabled" - Set
CEDARLING_TOKEN_CONFIGS
to the following value:
{
"access_token": {
"entity_type_name": "Jans::Access_token",
"iss": "disabled",
"aud": "disabled",
"sub": "disabled",
"nbf": "disabled",
"exp": "disabled",
"jti": "disabled"
},
"id_token": {
"entity_type_name": "Jans::id_token",
"iss": "disabled",
"aud": "disabled",
"sub": "disabled",
"nbf": "disabled",
"exp": "disabled",
"jti": "disabled"
},
"userinfo_token": {
"entity_type_name": "Jans::Userinfo_token",
"iss": "disabled",
"aud": "disabled",
"sub": "disabled",
"nbf": "disabled",
"exp": "disabled",
"jti": "disabled"
}
}
-
Set
CEDARLING_MAPPING_WORKLOAD
toJans::Workload
-
Set
CEDARLING_ID_TOKEN_TRUST_MODE
to "none"
Pull the Docker image:
docker pull ghcr.io/janssenproject/jans/cedarling-flask-sidecar:0.0.0-nightly
Run the Docker image, replacing </absolute/path/to/bootstrap.json>
with the absolute path to your bootstrap file:
docker run -e APP_MODE='development' -e CEDARLING_BOOTSTRAP_CONFIG_FILE=/bootstrap.json -e SIDECAR_DEBUG_RESPONSE=True --mount type=bind,src=</absolute/path/to/bootstrap.json>,dst=/bootstrap.json -p 5000:5000 -d ghcr.io/janssenproject/jans/cedarling-flask-sidecar:0.0.0-nightly
The sidecar is now running on http://127.0.0.1:5000. Keep track of the output of the previous command, it is your Docker container ID.
Python instructions (if Docker is unavailable)#
- Clone the Janssen repository and navigate to
jans/jans-cedarling/flask-sidecar
. - Run
poetry install
to install dependencies. - Download and install the latest Cedarling nightly wheel:
wget https://github.com/JanssenProject/jans/releases/download/nightly/cedarling_python-0.0.0-cp310-cp310-manylinux_2_31_x86_64.whl
- Install the nightly wheel:
poetry run pip install cedarling_python-0.0.0-cp310-cp310-manylinux_2_31_x86_64.whl
-
Modify
secrets/bootstrap.json
to your specifications. -
Navigate to
jans/jans-cedarling/flask-sidecar/main
-
Create a file called
.env
and paste in the following content. Alternatively, set the following environment variables:APP_MODE=development CEDARLING_BOOTSTRAP_CONFIG_FILE=../secrets/bootstrap.json SIDECAR_DEBUG_RESPONSE=False
-
Run the sidecar:
poetry run flask run
-
The sidecar is now running on http://127.0.0.1:5000
Setup Test Gateway#
We will use Flask to create a simple API Gateway.
- Create a folder called
demo
and navigate to it. - Create a virtual environment and activate it:
python -m venv venv
source venv/bin/activate
- Install Flask and requests.
pip install flask requests
- Create a file called
gateway.py
with the following content:
from flask import Flask, abort, request
import requests
app = Flask(__name__)
@app.route("/protected")
def protected():
token = request.headers.get("Authorization")
if token is None:
abort(403)
token_jwt = token.split(" ")[1]
payload = {
"subject": {
"type": "token_bundle",
"id": "some_id",
"properties": {
"access_token": token_jwt
}
},
"resource": {
"type": "Jans::HTTP_Request",
"id": "some_id",
"properties": {
"header": {},
"url": {
"protocol": "http",
"host": "www.acme.tld",
"path": "/protected"
}
}
},
"action": {
"name": "Jans::Action::\"GET\""
},
"context": {}
}
response = requests.post("http://127.0.0.1:5000/cedarling/evaluation", json=payload)
if response.ok and response.json()["decision"] == True:
return {"protected_content": "secret"}
abort(403)
if __name__ == '__main__':
app.run(port=5001)
- Run the gateway:
flask --app gateway run --port 5001
Test the Setup#
-
Access the protected endpoint via browser: http://127.0.0.1:5001/protected. You should get a 403 Forbidden response.
-
Access the protected endpoint via curl with the provided JWT:
curl http://127.0.0.1:5001/protected -H "Authorization: Bearer eyJraWQiOiJjb25uZWN0X2Y5YTAwN2EyLTZkMGItNDkyYS05MGNkLWYwYzliMWMyYjVkYl9zaWdfcnMyNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJxenhuMVNjcmI5bFd0R3hWZWRNQ2t5LVFsX0lMc3BaYVFBNmZ5dVlrdHcwIiwiY29kZSI6IjNlMmEyMDEyLTA5OWMtNDY0Zi04OTBiLTQ0ODE2MGMyYWIyNSIsImlzcyI6Imh0dHBzOi8vYWNjb3VudC5nbHV1Lm9yZyIsInRva2VuX3R5cGUiOiJCZWFyZXIiLCJjbGllbnRfaWQiOiJkN2Y3MWJlYS1jMzhkLTRjYWYtYTFiYS1lNDNjNzRhMTFhNjIiLCJhdWQiOiJkN2Y3MWJlYS1jMzhkLTRjYWYtYTFiYS1lNDNjNzRhMTFhNjIiLCJhY3IiOiJzaW1wbGVfcGFzc3dvcmRfYXV0aCIsIng1dCNTMjU2IjoiIiwibmJmIjoxNzMxOTUzMDMwLCJzY29wZSI6WyJyb2xlIiwib3BlbmlkIiwicHJvZmlsZSIsImVtYWlsIl0sImF1dGhfdGltZSI6MTczMTk1MzAyNywiZXhwIjoxNzMyMTIxNDYwLCJpYXQiOjE3MzE5NTMwMzAsImp0aSI6InVaVWgxaERVUW82UEZrQlBud3BHemciLCJ1c2VybmFtZSI6IkRlZmF1bHQgQWRtaW4gVXNlciIsInN0YXR1cyI6eyJzdGF0dXNfbGlzdCI6eyJpZHgiOjMwNiwidXJpIjoiaHR0cHM6Ly9qYW5zLnRlc3QvamFucy1hdXRoL3Jlc3R2MS9zdGF0dXNfbGlzdCJ9fX0.Pt-Y7F-hfde_WP7ZYwyvvSS11rKYQWGZXTzjH_aJKC5VPxzOjAXqI3Igr6gJLsP1aOd9WJvOPchflZYArctopXMWClbX_TxpmADqyCMsz78r4P450TaMKj-WKEa9cL5KtgnFa0fmhZ1ZWolkDTQ_M00Xr4EIvv4zf-92Wu5fOrdjmsIGFot0jt-12WxQlJFfs5qVZ9P-cDjxvQSrO1wbyKfHQ_txkl1GDATXsw5SIpC5wct92vjAVm5CJNuv_PE8dHAY-KfPTxOuDYBuWI5uA2Yjd1WUFyicbJgcmYzUSVt03xZ0kQX9dxKExwU2YnpDorfwebaAPO7G114Bkw208g"
- If the JWT is valid and the policy allows access, you should receive the following response:
{
"protected_content": "secret"
}
Check Logs#
The cedarling decision log will be outputted by the Docker container or directly by the API to stdout. In the case of Docker, this can be retrieved like so:
$ docker logs <container ID>
{
"request_id": "0194cdbc-b8c7-798d-8cc8-fb483448e6fa",
"timestamp": "2025-02-03T21:34:44.935Z",
"log_kind": "Decision",
"pdp_id": "e122cc37-14f7-4033-a547-6791f339218b",
"policystore_id": "c92b24bb010b772e7702811ae0725986bf7d3b39656e",
"policystore_version": "1.6.15",
"principal": "Workload",
"Workload": {"client_id": "slkacm3cv5-vat1u~elog"},
"diagnostics": {
"reason": ["840da5d85403f35ea76519ed1a18a33989f855bf1cf8"],
"errors": []
},
"action": "Jans::Action::\"GET\"",
"resource": "Jans::HTTP_Request::\"some_id\"",
"decision": "ALLOW",
"tokens": {
"access_token": {
"jti": "uZUh1hDUQo6PFkBPnwpGzg"
}
},
"decision_time_ms": 0
}
Customizing the policy#
Let's add one more check in the policy. This time we want to check if the acr
in the access token matches a particular value, for example simple_password_auth
.
This change will essentially involve two changes.
- Update the schema
- Update the policy
Let's update the schema first using steps the below:
- Go to the
Policy Designer
. - Navigate to
Schema
→Entity Types
. - Edit the
access_token
entity type. - Add a new field
acr
with the type set toString
. - Save the schema.
Once the schema has been updated, your new updated Access_token entity type json should be similar to the one below:
{
"shape": {
"type": "Record",
"attributes": {
"aud": {
"type": "EntityOrCommon",
"name": "String"
},
"exp": {
"type": "EntityOrCommon",
"name": "Long"
},
"iat": {
"type": "EntityOrCommon",
"name": "Long"
},
"iss": {
"type": "EntityOrCommon",
"name": "TrustedIssuer"
},
"jti": {
"type": "EntityOrCommon",
"required": false,
"name": "String"
},
"nbf": {
"type": "EntityOrCommon",
"required": false,
"name": "Long"
},
"scope": {
"type": "Set",
"required": false,
"element": {
"type": "EntityOrCommon",
"name": "String"
}
},
"acr": {
"type": "String",
"required": false
}
}
}
}
Let's make the corresponding changes in the policy.
@id("allow_one")
permit(
principal is Jans::Workload,
action == Jans::Action::"GET",
resource is Jans::HTTP_Request
)
when {
principal has access_token.acr &&
principal.acr == "simple_password_auth"
};
The policy now checks whether the acr
value in the access token is present and matches simple_password_auth
.
Created: 2022-07-21