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
) →401
with reasonInvalid token
.Unknown, revoked or expired-in-store refresh token →
403
with 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
/aud
claims 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_KEY
andACCESS_PUBLIC_KEY
(and theirREFRESH_*
equivalents) with PEM strings. For compatibility, a singleACCESS_SECRET_KEY
/REFRESH_SECRET_KEY
may 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/refresh
with 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_at
and areplaced_by
pointer 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/login
Accepts JSON
{"username": "<name>", "password": "<password>"}
and returns an access/refresh token pair and the user’s primary key.POST /auth/refresh
Accepts 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/logout
Stateless logout that clears the user context on the server.
GET /auth/me
Returns 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 validAuthorization
header. 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/auth
routes. 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 usingcustom
authentication, ensureAPI_USER_MODEL
is configured so the response can be serialised.API_EXPOSE_ME
(bool, defaultTrue
): whenFalse
the 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
roles
attribute on the user model, e.g.User.roles = ["admin"]
.require_roles
reads roles fromcurrent_user
after 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_roles
pulls roles fromcurrent_user
.
Custom authentication¶
Resolve the user from your custom credentials.
Call
set_current_user
with an object exposingroles
.require_roles
authorises 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
.ALL
or*
: 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. |