Role Based Access Management

JWT Token

AppKernel uses JWT tokens (RS256) for authentication and authorisation. To issue tokens, add the IdentityMixin to a model that has an id and a roles field:

from typing import Annotated
from appkernel import Model, MongoRepository, IdentityMixin, Required, Generator, create_uuid_generator

class User(Model, MongoRepository, IdentityMixin):
    id: Annotated[str | None, Required(), Generator(create_uuid_generator('U'))] = None
    roles: list[str] | None = None

With this, each User instance exposes an auth_token property:

print(f'token: {user.auth_token}')

The token is digitally signed with RS256 and includes an aud claim set to the app_id passed to AppKernelEngine. Tokens issued by one service are rejected by all other services, preventing cross-service token replay.

Setup

JWT tokens require an RSA key pair. Generate one using OpenSSL:

# Generate private key
openssl genpkey -out appkernel.pem -algorithm rsa -pkeyopt rsa_keygen_bits:2048
# Derive the public key
openssl rsa -in appkernel.pem -out appkernel.pub -pubout

By default AppKernel looks for these files at {config-folder}/keys/appkernel.pem and {config-folder}/keys/appkernel.pub. See Key Path Configuration to override this location.

Key Path Configuration

The private and public key paths are resolved in the following priority order:

1. Environment variables (highest priority)

Set APPKERNEL_PRIVATE_KEY_PATH and APPKERNEL_PUBLIC_KEY_PATH to absolute file paths. This is the recommended approach for production and container deployments:

export APPKERNEL_PRIVATE_KEY_PATH=/run/secrets/appkernel.pem
export APPKERNEL_PUBLIC_KEY_PATH=/run/secrets/appkernel.pub

With Docker or Kubernetes, mount the key files as secrets and point the env vars at the mount path.

2. Configuration file (cfg.yml)

Add private_key_path and public_key_path under appkernel.security:

appkernel:
  security:
    private_key_path: /etc/myapp/keys/private.pem
    public_key_path: /etc/myapp/keys/public.pub

3. Default path (fallback)

If neither env vars nor cfg.yml entries are set, AppKernel loads keys from:

{config-folder}/keys/appkernel.pem
{config-folder}/keys/appkernel.pub

where {config-folder} is the cfg_dir argument passed to AppKernelEngine.

Role based authorisation

Configure access control after registering a model:

user_service = kernel.register(User, methods=['GET', 'PUT', 'POST', 'PATCH', 'DELETE'])
user_service.deny_all() \
    .require(Role('user'), methods='GET') \
    .require(Role('admin'), methods=['PUT', 'POST', 'PATCH', 'DELETE'])

Once secured, requests must include a valid JWT token in the Authorization header:

Authorization: Bearer eyJhbGciOiJSUzI1...Mjc0MzEzNDd9.

For custom action endpoints, pass a require list to the @action decorator:

@action(method='POST', require=[CurrentSubject(), Role('admin')])
def change_password(self, current_password, new_password):
    ...

Available permission types:

  • Role — grants access to any user holding the specified role;

  • Anonymous — grants access to unauthenticated users;

  • Denied — explicitly denies access; assign to resources that must never be reachable;

  • CurrentSubject — grants access when the JWT token subject matches the model’s id (useful for users modifying their own data);

CORS

Cross-Origin Resource Sharing (CORS) controls whether browser-based JavaScript on one origin (e.g. https://app.example.com) may call an API on a different origin (e.g. https://api.example.com). Without CORS headers the browser blocks the response, even though the server processed the request.

AppKernel adds no CORS headers by default. Call enable_cors() to opt in:

from appkernel import AppKernelEngine, CorsConfig

kernel = AppKernelEngine('my-app', cfg_dir='./config')
kernel.enable_security()
kernel.enable_rate_limiting()
# enable_cors() must be called LAST so it executes first and handles
# OPTIONS preflight before security / rate-limiting checks run.
kernel.enable_cors(CorsConfig(
    allow_origins=['https://app.example.com'],
))

Middleware ordering

FastAPI / Starlette executes middleware in reverse registration order — the last middleware added runs first. enable_cors() must therefore be called after enable_security() and enable_rate_limiting() so that:

  1. CORS middleware handles preflight OPTIONS requests (and returns 200) before the security middleware rejects them as unauthenticated.

  2. CORS headers are injected on every response, including 4xx and 5xx errors.

CorsConfig reference

Parameter

Default

Description

allow_origins

[] (same-origin only)

Permitted origin list. Use ['*'] with allow_credentials=False only.

allow_methods

GET POST PUT PATCH DELETE OPTIONS

Permitted HTTP methods.

allow_headers

Authorization Content-Type Accept-Language

Permitted request headers.

allow_credentials

False

Set True to send Access-Control-Allow- Credentials: true. Requires explicit origins.

expose_headers

[]

Response headers JS may read.

max_age

600

Preflight cache lifetime in seconds.

Security notes

  • Never use allow_origins=['*'] in production — it allows any website to read API responses from a logged-in user’s browser session.

  • allow_origins=['*'] combined with allow_credentials=True is rejected at startup with ValueError (browsers refuse this combination anyway).

  • Restrict allow_headers to the minimum your frontend actually sends. Broad header allowlists increase the attack surface for header injection.

CSRF

Cross-Site Request Forgery (CSRF) tricks a victim’s browser into sending an authenticated request to your API from a malicious page. The attack works because browsers automatically attach cookies to cross-origin requests.

AppKernel’s current auth model (JWT in the Authorization header) is not vulnerable to CSRF. Browsers do not auto-attach custom headers to cross-origin requests, so an attacker’s page cannot issue an authenticated request without first stealing the token (which would be XSS, a different attack). No CSRF protection is required for header-based JWT APIs.

CSRF protection is required only if you introduce cookie-based authentication. If you add cookie sessions or store JWTs in cookies, adopt one of the following mitigations before the feature ships:

Option A — SameSite cookie attribute (recommended for most cases)

Set SameSite=Strict (or SameSite=Lax) on the auth cookie:

# Example when setting a cookie in a FastAPI response
response.set_cookie(
    key='session',
    value=token,
    httponly=True,
    samesite='strict',   # browser refuses to send on cross-origin requests
    secure=True,         # HTTPS only
)

SameSite=Strict blocks the cookie on all cross-origin requests, including OAuth redirect flows. Use SameSite=Lax if you need OAuth to work; it still blocks cross-origin POST/PUT/DELETE but permits top-level navigations.

Option B — Double-submit cookie pattern

Alongside the HttpOnly session cookie, issue a second readable CSRF token cookie. The client echoes this token in a custom request header (e.g. X-CSRF-Token). The server rejects any state-changing request that lacks the matching header. This is compatible with OAuth flows but adds a round-trip.

Option C — Origin / Referer validation

In a middleware, reject state-changing requests (POST, PUT, PATCH, DELETE) whose Origin or Referer header does not match the known API hostname. Lightweight and stateless but not universally reliable (some proxies strip Referer).

Decision matrix

Auth mechanism

CSRF risk

Recommended mitigation

JWT in Authorization header

None

Nothing required

JWT in HttpOnly cookie

High

SameSite=Strict + Secure on cookie

Session cookie (OAuth, etc.)

High

SameSite=Lax + double-submit if needed