Authentication¶
flarchitect provides several helpers to secure your API quickly. Enable one or
more strategies via API_AUTHENTICATE_METHOD.
Available methods are jwt, basic, api_key and custom.
Each example below uses the common setup defined in
demo/authentication/app_base.py. Runnable snippets demonstrating each
strategy live in the project repository: jwt_auth.py, basic_auth.py,
api_key_auth.py, and custom_auth.py. You can also protect routes based on
user roles using the require_roles decorator.
Method |
Required config keys |
Demo |
|---|---|---|
|
|
|
|
API_USER_MODEL, API_USER_LOOKUP_FIELD, API_CREDENTIAL_CHECK_METHOD |
|
|
API_KEY_AUTH_AND_RETURN_METHOD (or API_CREDENTIAL_HASH_FIELD + API_CREDENTIAL_CHECK_METHOD) |
|
|
Error responses¶
Authentication failures are serialised with create_response(), so each
payload includes standard metadata like the API version, timestamp and response
time.
Missing or invalid credentials return a 401:
{
"api_version": "0.1.0",
"datetime": "2024-01-01T00:00:00+00:00",
"status_code": 401,
"errors": {"error": "Unauthorized", "reason": "Authorization header missing"},
"response_ms": 5.0,
"total_count": 1,
"next_url": null,
"previous_url": null,
"value": null
}
Expired tokens also yield a 401:
{
"api_version": "0.1.0",
"datetime": "2024-01-01T00:00:00+00:00",
"status_code": 401,
"errors": {"error": "Unauthorized", "reason": "Token has expired"},
"response_ms": 5.0,
"total_count": 1,
"next_url": null,
"previous_url": null,
"value": null
}
Refresh failures fall into two categories:
Invalid refresh JWT (bad format, wrong signature, wrong
iss/aud) →401with reasonInvalid token.Unknown, revoked or expired-in-store refresh token →
403with reasonInvalid or expired refresh token.
Example 403 response:
{
"api_version": "0.1.0",
"datetime": "2024-01-01T00:00:00+00:00",
"status_code": 403,
"errors": {"error": "Forbidden", "reason": "Invalid or expired refresh token"},
"response_ms": 5.0,
"total_count": 1,
"next_url": null,
"previous_url": null,
"value": null
}
JWT authentication¶
JSON Web Tokens (JWT) allow a client to prove their identity by including a
signed token with every request. The token typically contains the user’s ID and
an expiry timestamp. Clients obtain an access/refresh pair from a login endpoint
and then send the access token in the Authorization header:
Authorization: Bearer <access-token>
To enable JWT support you must provide ACCESS_SECRET_KEY and
REFRESH_SECRET_KEY values along with a user model. A minimal configuration
looks like:
class Config(BaseConfig):
API_AUTHENTICATE_METHOD = ["jwt"]
ACCESS_SECRET_KEY = "access-secret"
REFRESH_SECRET_KEY = "refresh-secret"
API_USER_MODEL = User
API_USER_LOOKUP_FIELD = "username"
API_CREDENTIAL_CHECK_METHOD = "check_password"
Token lifetimes default to 360 minutes for access tokens and 2880
minutes (two days) for refresh tokens. Override these durations with
API_JWT_EXPIRY_TIME and API_JWT_REFRESH_EXPIRY_TIME respectively. The
default algorithm is HS256 (override via
API_JWT_ALGORITHM). When decoding a
token, flarchitect.authentication.jwt.get_user_from_token() resolves the
secret key in this order: explicit argument → ACCESS_SECRET_KEY environment
variable → Flask config.
Hardening options¶
JWT validation can be tightened with the following settings:
API_JWT_ALLOWED_ALGORITHMS: Restrict verification to a specific set of algorithms (list or comma-separated string). Defaults to the configured algorithm.API_JWT_ISSUER/API_JWT_AUDIENCE: Include and enforceiss/audclaims during encode/decode.API_JWT_LEEWAY: Allow small clock skew (in seconds) when validatingexp/iat.API_JWT_ALGORITHM="RS256": Use RSA key pairs. SetACCESS_PRIVATE_KEYandACCESS_PUBLIC_KEY(and theirREFRESH_*equivalents) with PEM strings. For compatibility, a singleACCESS_SECRET_KEY/REFRESH_SECRET_KEYmay be used to verify if public keys are not set, but key pairs are recommended.
Token rotation and revocation¶
Refresh tokens are single‑use. When clients call
POST /auth/refreshwith a valid refresh token, the server revokes the token and issues a new access/refresh pair.Deny‑list and auditing: The refresh token store persists
created_at,last_used_at,revoked/revoked_atand areplaced_bypointer to the next token. This provides a clear trail for incident response.Programmatic revocation: Administrators can revoke a specific token at any time with
revoke_refresh_token(token)fromflarchitect.authentication.token_store.
Built‑in endpoints¶
When JWT is enabled, flarchitect registers the following routes:
POST /auth/loginAccepts JSON
{"username": "<name>", "password": "<password>"}and returns an access/refresh token pair and the user’s primary key.POST /auth/refreshAccepts JSON
{"refresh_token": "<token>"}and returns a new access token. For robustness, a value prefixed with"Bearer "is accepted and normalised (e.g.,"Bearer <token>"). Invalid refresh JWTs yield401; revoked or expired-in-store tokens return403.POST /auth/logoutStateless logout that clears the user context on the server.
GET /auth/meReturns the current authenticated user as JSON. This endpoint is available when a user model is configured and any supported authentication method is enabled (
jwt,basic,api_key, orcustom). The response uses the model’s output schema, so field visibility follows your schema settings. Requires a validAuthorizationheader. The path is configurable viaAPI_AUTH_ME_ROUTE(default"/auth/me"). You can disable exposing this endpoint entirely withAPI_EXPOSE_ME=False.
Clients include the access token with each request using the standard header:
Authorization: Bearer <access-token>
Auth routes configuration¶
The built‑in auth routes register automatically when JWT is enabled. You can adjust this behaviour via configuration:
API_AUTO_AUTH_ROUTES(bool, defaultTrue): whenFalse, flarchitect does not register the default/authroutes. This is useful if you want to provide your own endpoints.API_AUTH_REFRESH_ROUTE(str, default"/auth/refresh"): path for the refresh endpoint. The endpoint accepts{"refresh_token": "..."}and returns a new access token using the standard response wrapper.API_AUTH_ME_ROUTE(str, default"/auth/me"): path for the current-user endpoint. When usingcustomauthentication, ensureAPI_USER_MODELis configured so the response can be serialised.API_EXPOSE_ME(bool, defaultTrue): whenFalsethe current-user endpoint is not registered even if a user model is configured.
Protecting manual routes¶
Endpoints generated by flarchitect are automatically secured when
API_AUTHENTICATE_METHOD includes "jwt". If you add your own Flask routes
outside the generated API, decorate them with jwt_authentication to enforce
the same protection:
from flarchitect.core.architect import jwt_authentication
@app.get("/profile")
@jwt_authentication
def profile() -> dict[str, str]:
return {"status": "ok"}
This decorator reads the Authorization header, validates the token and sets
current_user. Automatically created endpoints do not need it because global
settings already apply authentication.
Refresh token storage¶
By default, flarchitect persists JWT refresh tokens in an SQL table named
refresh_tokens. The table contains four columns:
token– the encoded refresh token (primary key)user_pk– the user’s primary key as a stringuser_lookup– the configured user lookup valueexpires_at– the token’s expiry timestamp
The table is created automatically when a refresh token is stored. You can
manage tokens directly using helpers from
flarchitect.authentication.token_store:
from datetime import datetime, timedelta, timezone
from flarchitect.authentication.token_store import (
delete_refresh_token,
get_refresh_token,
store_refresh_token,
)
expires = datetime.now(timezone.utc) + timedelta(days=1)
store_refresh_token(
"encoded-token", user_pk="1", user_lookup="alice", expires_at=expires
)
stored = get_refresh_token("encoded-token")
if stored:
print(stored.user_pk, stored.expires_at)
delete_refresh_token("encoded-token")
Basic authentication¶
HTTP Basic Auth is the most straightforward option. The client includes a
username and password in the Authorization header on every request. The
credentials are base64 encoded but otherwise sent in plain text, so HTTPS is
strongly recommended.
Provide a lookup field and password check method on your user model:
class Config(BaseConfig):
API_AUTHENTICATE_METHOD = ["basic"]
API_USER_MODEL = User
API_USER_LOOKUP_FIELD = "username"
API_CREDENTIAL_CHECK_METHOD = "check_password"
flarchitect also provides a simple login route for this strategy. POST to
/auth/login with a Basic Authorization header to verify
credentials and receive basic user information:
curl -X POST -u username:password http://localhost:5000/auth/login
You can then access endpoints with tools such as curl:
curl -u username:password http://localhost:5000/api/books
See demo/authentication/basic_auth.py for a runnable snippet.
API key authentication¶
API key auth associates a user with a single token. Clients send the token in
each request via an Authorization header using the Api-Key scheme. The
framework passes the token to a function you provide (or validates a stored
hash) and uses the returned user for the request.
If you store hashed tokens on the model, set API_CREDENTIAL_HASH_FIELD to the attribute holding the hash so flarchitect can validate keys.
Attach a function that accepts an API key and returns a user. The function can
also call set_current_user:
def lookup_user_by_token(token: str) -> User | None:
user = User.query.filter_by(api_key=token).first()
if user:
set_current_user(user)
return user
class Config(BaseConfig):
API_AUTHENTICATE_METHOD = ["api_key"]
API_KEY_AUTH_AND_RETURN_METHOD = staticmethod(lookup_user_by_token)
When this method is enabled flarchitect exposes a companion login route. POST
an Api-Key Authorization header to /auth/login to validate the key
and retrieve basic user details:
curl -X POST -H "Authorization: Api-Key <token>" http://localhost:5000/auth/login
Clients include the API key with each request using:
curl -H "Authorization: Api-Key <token>" http://localhost:5000/api/books
See demo/authentication/api_key_auth.py for more detail.
Custom authentication¶
For complete control supply your own callable. This method lets you support any
authentication strategy you like: session cookies, HMAC signatures or
third-party OAuth flows. Your callable should return True on success and may
call set_current_user to attach the authenticated user to the request.
def custom_auth() -> bool:
token = request.headers.get("X-Token", "")
user = User.query.filter_by(api_key=token).first()
if user:
set_current_user(user)
return True
return False
class Config(BaseConfig):
API_AUTHENTICATE_METHOD = ["custom"]
API_CUSTOM_AUTH = staticmethod(custom_auth)
Clients can then call your API with whatever headers your function expects:
curl -H "X-Token: <token>" http://localhost:5000/api/books
See demo/authentication/custom_auth.py for this approach in context.
Role-based access¶
Use the require_roles decorator to restrict access based on user roles. The
decorator reads current_user.roles which is populated by the active
authentication method.
from flarchitect.authentication import require_roles
@app.get("/admin")
@require_roles("admin")
def admin_dashboard():
return {"status": "ok"}
Pass multiple roles to require all of them. To allow access when a user has
any of the listed roles, set any_of=True:
@require_roles("admin", "editor", any_of=True)
def update_post():
...
Defining roles¶
Roles can be attached to the user model or embedded in authentication tokens so
require_roles can evaluate permissions.
JWT¶
Persist a
rolesattribute on the user model, e.g.User.roles = ["admin"].require_rolesreads roles fromcurrent_userafter the token is validated and the user is loaded.
API keys¶
Store roles on the user model.
In the lookup function, return a user object with those roles:
def lookup_user_by_token(token: str) -> User | None: user = User.query.filter_by(api_key=token).first() if user: set_current_user(user) return user
require_rolespulls roles fromcurrent_user.
Custom authentication¶
Resolve the user from your custom credentials.
Call
set_current_userwith an object exposingroles.require_rolesauthorises the request using those roles.
Common roles¶
Role |
Responsibility |
|---|---|
|
Full access to manage resources and users. |
|
Create and modify resources but cannot manage users. |
|
Read-only access to resources. |
If the authenticated user lacks any of the required roles—or if no user is
authenticated—a 403 response is raised.
Config-driven roles¶
You can assign roles to endpoints without decorating functions by setting a
single map in configuration or on a model’s Meta. This is the most
maintainable way to protect all generated CRUD routes consistently.
Use API_ROLE_MAP with method names as keys. Values may be a list of roles
that must all be present, a string for a single role, or a dictionary with an
any_of flag for “any of these roles” semantics.
Global example (applies to all models):
app.config.update(
API_AUTHENTICATE_METHOD=["jwt"], # ensure authentication is enabled
API_ROLE_MAP={
"GET": ["viewer"], # both list & string forms are accepted
"POST": {"roles": ["editor", "admin"], "any_of": True},
"PATCH": ["editor", "admin"], # require all listed roles
"DELETE": ["admin"],
"ALL": True, # optional: means "auth-only" for any unspecified methods
},
)
Model-specific example (overrides global for this model only):
class Book(Base):
__tablename__ = "books"
class Meta:
api_role_map = {
"GET_MANY": ["viewer"],
"GET_ONE": ["viewer"],
"POST": ["editor"],
"PATCH": {"roles": ["editor", "admin"], "any_of": True},
"DELETE": ["admin"],
}
Recognised keys¶
GET,POST,PATCH,DELETE: Protects the corresponding CRUD endpoints.GET_MANY/GET_ONE: Optional split for collection vs single-item GET.RELATION_GET: Protects relation endpoints like/parents/{id}/children.ALLor*: Fallback applied when a method key is not present.
Fallbacks¶
If you prefer very simple policies, instead of API_ROLE_MAP you can set one
of the following (globally or on a model’s Meta):
API_ROLES_REQUIRED: list of roles, all must be present.API_ROLES_ACCEPTED: list of roles where any grants access.
These apply to all endpoints for that model and are overridden by
API_ROLE_MAP when both are present.
Troubleshooting¶
Problem |
Solution |
|---|---|
Missing Authorization header |
Include the appropriate |
Token has expired |
Use the refresh token to obtain a new access token. |
Invalid or expired refresh token |
Log in again to receive a new access/refresh token pair. |