docs-site/content/docs/how-to/api-key-auth.md
+++ title = "Protect a Route with an API Key" description = "Authenticate requests with a per-user API key using the ApiToken<T> extractor and Authenticable::find_by_api_key." date = 2021-05-01T18:10:00+00:00 updated = 2021-05-01T18:10:00+00:00 draft = false weight = 41 sort_by = "weight" template = "docs/page.html"
[extra] lead = "" toc = true top = false +++
Goal: authenticate a request with a long-lived, per-user API key instead of a JWT — useful for machine-to-machine or CLI clients that shouldn't have to re-authenticate for a fresh token.
Loco provides auth::ApiToken<T>, an axum extractor that reads a key from the Authorization header and loads the matching user via your model's Authenticable::find_by_api_key.
with-db feature (on by default) — ApiToken<T> is compiled only under #[cfg(feature = "with-db")].loco_rs::model::Authenticable, in particular find_by_api_key. See the Authenticable contract for the trait shape and an example implementation.api_key), and a way to populate it — loco_rs::hash::random_string is a convenient generator; see Hash and verify passwords.Unlike JWT auth, ApiToken<T> needs no auth.jwt configuration at all — it doesn't call into auth.jwt.secret/expiration/location. It only needs a database and an Authenticable implementation.
find_by_api_keyuse loco_rs::model::{Authenticable, ModelError, ModelResult};
use sea_orm::{DatabaseConnection, EntityTrait, ColumnTrait, QueryFilter};
impl Authenticable for super::_entities::users::Model {
async fn find_by_api_key(db: &DatabaseConnection, api_key: &str) -> ModelResult<Self> {
let user = super::_entities::users::Entity::find()
.filter(super::_entities::users::Column::ApiKey.eq(api_key))
.one(db)
.await?;
user.ok_or(ModelError::EntityNotFound)
}
async fn find_by_claims_key(db: &DatabaseConnection, claims_key: &str) -> ModelResult<Self> {
// Required by the trait even if this app only uses ApiToken.
// See jwt-auth.md if you also support JWTWithUser<T>.
unimplemented!()
}
}
ApiToken<T> to a handleruse loco_rs::prelude::*;
use loco_rs::controller::extractor::auth;
async fn current_by_api_key(
auth: auth::ApiToken<users::Model>,
State(_ctx): State<AppContext>,
) -> Result<Response> {
format::json(&auth.user)
}
pub fn routes() -> Routes {
Routes::new()
.prefix("user")
.add("/current-api", get(current_by_api_key))
}
auth.user is the fully loaded T (your Authenticable model) — there's no separate claims struct to unwrap, unlike the JWT extractors.
ApiToken<T> always reads the key from the Authorization: Bearer <key> header, and only from there. This is a hard-coded read (extract_token_from_header), independent of any auth.jwt.location setting — the location config (Bearer/Query/Cookie, described in Configure where Loco looks for the JWT) applies to the JWT/JWTWithUser extractors only, never to ApiToken.
curl --location '127.0.0.1:5150/api/user/current-api' \
--header 'Authorization: Bearer <API_KEY>'
401 Unauthorized (the model lookup misses, mapped from ModelError::EntityNotFound).500 Internal Server Error, and is logged via tracing::error!.Authenticable contract in full, plus the JWT / JWTWithUser<T> extractors.ApiToken.with-db default and what it gates.