Skip to main content

renox_core/auth/
tokens.rs

1use serde::Serialize;
2
3use super::User;
4use crate::Result;
5use crate::crypto::{constant_time_eq, random_token};
6use crate::db::{DateTime, Db, Model, now};
7
8/// An API token. Only a SHA-256 hash of the secret is stored.
9#[derive(Debug, Clone, Serialize)]
10#[non_exhaustive]
11pub struct AccessToken {
12    /// The `personal_access_tokens` row id, also the part before `|` in the plain token.
13    pub id: i64,
14    /// The user the token acts as.
15    pub user_id: i64,
16    /// A label chosen at creation, e.g. the device or integration.
17    pub name: String,
18    /// What the token may do (`AuthUser::token_can`); `None` for everything.
19    pub abilities: Option<Vec<String>>,
20    /// When a request last authenticated with it; `None` if never used.
21    pub last_used_at: Option<DateTime>,
22    /// When it stops working; `None` for never.
23    pub expires_at: Option<DateTime>,
24    /// When it was created.
25    pub created_at: Option<DateTime>,
26}
27
28/// A freshly created token. `plain` is shown once; send it as
29/// `Authorization: Bearer <plain>`.
30#[derive(Debug, Clone, Serialize)]
31#[non_exhaustive]
32pub struct NewToken {
33    /// The stored token.
34    pub token: AccessToken,
35    /// The secret as `<id>|<random>`; not stored, so it can't be shown again.
36    pub plain: String,
37}
38
39pub(crate) fn sha256_hex(value: &str) -> String {
40    use sha2::{Digest, Sha256};
41    Sha256::digest(value.as_bytes())
42        .iter()
43        .map(|b| format!("{b:02x}"))
44        .collect()
45}
46
47fn from_row(row: &crate::db::Row) -> std::result::Result<AccessToken, crate::db::DbError> {
48    Ok(AccessToken {
49        id: row.try_get("id")?,
50        user_id: row.try_get("user_id")?,
51        name: row.try_get("name")?,
52        abilities: parse_abilities(row.try_get("abilities")?),
53        last_used_at: row.try_get("last_used_at")?,
54        expires_at: row.try_get("expires_at")?,
55        created_at: row.try_get("created_at")?,
56    })
57}
58
59const COLUMNS: &str = "id, user_id, name, abilities, last_used_at, expires_at, created_at";
60
61fn parse_abilities(json: Option<String>) -> Option<Vec<String>> {
62    json.and_then(|json| serde_json::from_str(&json).ok())
63}
64
65impl User {
66    /// Creates an API token that may do everything the user may, optionally
67    /// expiring at `expires_at`.
68    pub async fn create_token(
69        &self,
70        db: &Db,
71        name: &str,
72        expires_at: Option<DateTime>,
73    ) -> Result<NewToken> {
74        self.insert_token(db, name, None, expires_at).await
75    }
76
77    /// Creates an API token limited to `abilities` (checked with
78    /// `AuthUser::token_can` or `Routes::require_ability`), e.g. a read-only
79    /// token: `create_token_with(&db, "reports", &["orders:read"], None)`.
80    /// `"*"` allows everything.
81    pub async fn create_token_with(
82        &self,
83        db: &Db,
84        name: &str,
85        abilities: &[&str],
86        expires_at: Option<DateTime>,
87    ) -> Result<NewToken> {
88        self.insert_token(db, name, Some(abilities), expires_at)
89            .await
90    }
91
92    async fn insert_token(
93        &self,
94        db: &Db,
95        name: &str,
96        abilities: Option<&[&str]>,
97        expires_at: Option<DateTime>,
98    ) -> Result<NewToken> {
99        let secret = random_token();
100        let created = now();
101        let abilities = abilities.map(|list| serde_json::json!(list).to_string());
102        let row = crate::db::sql(format!(
103            "INSERT INTO personal_access_tokens (user_id, name, abilities, token, expires_at, created_at, updated_at) \
104             VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING {COLUMNS}"
105        ))
106        .bind(self.id)
107        .bind(name)
108        .bind(abilities)
109        .bind(sha256_hex(&secret))
110        .bind(expires_at)
111        .bind(created)
112        .bind(created)
113        .fetch_one(db)
114        .await?;
115        let token = from_row(&row)?;
116        Ok(NewToken {
117            plain: format!("{}|{secret}", token.id),
118            token,
119        })
120    }
121
122    /// The user's API tokens, newest first.
123    pub async fn tokens(&self, db: &Db) -> Result<Vec<AccessToken>> {
124        let rows = crate::db::sql(format!(
125            "SELECT {COLUMNS} FROM personal_access_tokens WHERE user_id = ? ORDER BY id DESC"
126        ))
127        .bind(self.id)
128        .fetch_all(db)
129        .await?;
130        Ok(rows
131            .iter()
132            .map(from_row)
133            .collect::<std::result::Result<_, _>>()?)
134    }
135
136    /// Revokes one of the user's tokens; returns whether it existed.
137    pub async fn revoke_token(&self, db: &Db, token_id: i64) -> Result<bool> {
138        let done =
139            crate::db::sql("DELETE FROM personal_access_tokens WHERE id = ? AND user_id = ?")
140                .bind(token_id)
141                .bind(self.id)
142                .execute(db)
143                .await?;
144        Ok(done > 0)
145    }
146
147    /// Revokes all of the user's tokens.
148    pub async fn revoke_tokens(&self, db: &Db) -> Result<u64> {
149        let done = crate::db::sql("DELETE FROM personal_access_tokens WHERE user_id = ?")
150            .bind(self.id)
151            .execute(db)
152            .await?;
153        Ok(done)
154    }
155}
156
157/// Deletes tokens that expired more than `grace` ago; returns how many.
158/// `rnx tokens:prune` (from the `Auth` module) runs it with a day's grace.
159pub async fn prune_expired_tokens(db: &Db, grace: std::time::Duration) -> Result<u64> {
160    let before = now() - chrono::Duration::from_std(grace).unwrap_or_default();
161    Ok(
162        crate::db::sql("DELETE FROM personal_access_tokens WHERE expires_at < ?")
163            .bind(before)
164            .execute(db)
165            .await?,
166    )
167}
168
169/// The token's id and abilities (`None` for every ability).
170pub(crate) type TokenGrant = (i64, Option<Vec<String>>);
171
172/// The user behind `Authorization: Bearer <id|secret>` and the token, if
173/// the token is valid.
174pub(crate) async fn authenticate(db: &Db, bearer: &str) -> Result<Option<(User, TokenGrant)>> {
175    let Some((id, secret)) = bearer.split_once('|') else {
176        return Ok(None);
177    };
178    let Ok(id) = id.parse::<i64>() else {
179        return Ok(None);
180    };
181    let Some(row) = crate::db::sql(
182        "SELECT user_id, token, abilities, expires_at FROM personal_access_tokens WHERE id = ?",
183    )
184    .bind(id)
185    .fetch_optional(db)
186    .await?
187    else {
188        return Ok(None);
189    };
190    let hash: String = row.try_get("token")?;
191    let expires_at: Option<DateTime> = row.try_get("expires_at")?;
192    if !constant_time_eq(&hash, &sha256_hex(secret)) || expires_at.is_some_and(|at| at <= now()) {
193        return Ok(None);
194    }
195    crate::db::sql("UPDATE personal_access_tokens SET last_used_at = ? WHERE id = ?")
196        .bind(now())
197        .bind(id)
198        .execute(db)
199        .await?;
200    let abilities = parse_abilities(row.try_get("abilities")?);
201    Ok(User::find(db, row.try_get("user_id")?)
202        .await?
203        .map(|user| (user, (id, abilities))))
204}