summaryrefslogtreecommitdiffstats
path: root/src/routes/jwt.rs
blob: 6a229a30da3eeac64685c7e3d6d8c25f46804b9c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
use std::sync::Arc;

use axum::{
    extract::{Request, State},
    response::IntoResponse,
};
use axum_extra::{
    extract::{cookie::Cookie, CookieJar},
    headers::{authorization::Bearer, Authorization},
    routing::TypedPath,
    TypedHeader,
};
use jsonwebtoken::{DecodingKey, Validation};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use uuid::Uuid;

use crate::{error::AuthError, state::AppState, Error};

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct Claims {
    pub sub: Uuid,
    pub iat: i64,
    pub exp: i64,
    pub jti: Uuid,
}

impl Claims {
    const MAX_AGE: i64 = 3600;

    pub fn new(sub: Uuid) -> Self {
        let iat = OffsetDateTime::now_utc().unix_timestamp();
        let exp = iat + Self::MAX_AGE;
        let jti = uuid::Uuid::new_v4();
        Self { sub, iat, exp, jti }
    }

    pub fn encode(&self, secret: &[u8]) -> Result<String, jsonwebtoken::errors::Error> {
        jsonwebtoken::encode(
            &jsonwebtoken::Header::default(),
            self,
            &jsonwebtoken::EncodingKey::from_secret(secret),
        )
    }
}

impl From<Uuid> for Claims {
    fn from(value: Uuid) -> Self {
        Self::new(value)
    }
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
struct Session {
    jti: Uuid,
    uuid: Uuid,
}

#[derive(Debug, Deserialize, TypedPath)]
#[typed_path("/api/auth/refresh")]
pub struct Refresh;

impl Refresh {
    #[tracing::instrument]
    pub async fn post(
        self,
        State(state): State<Arc<AppState>>,
        TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
        cookie_jar: CookieJar,
    ) -> Result<impl IntoResponse, Error> {
        let Claims { sub, .. } = jsonwebtoken::decode::<Claims>(
            bearer.token(),
            &DecodingKey::from_secret(state.jwt_secret.as_ref()),
            &Validation::default(),
        )?
        .claims;

        let claims = Claims::from(sub);

        let token = jsonwebtoken::encode(
            &jsonwebtoken::Header::default(),
            &claims,
            &jsonwebtoken::EncodingKey::from_secret(state.jwt_secret.as_ref()),
        )?;

        let cookie = Cookie::build(("token", token))
            .expires(OffsetDateTime::from_unix_timestamp(claims.exp)?)
            .secure(true)
            .http_only(true);

        Ok(cookie_jar.add(cookie))
    }
}

pub async fn authenticate(
    State(state): State<Arc<AppState>>,
    cookie_jar: CookieJar,
    mut req: Request,
) -> Result<Request, AuthError> {
    let token = cookie_jar
        .get("token")
        .ok_or(AuthError::JwtNotFound)?
        .to_string();

    let claims = jsonwebtoken::decode::<Claims>(
        &token,
        &DecodingKey::from_secret(state.jwt_secret.as_ref()),
        &Validation::default(),
    )?
    .claims;

    req.extensions_mut().insert(claims);
    Ok(req)
}