summaryrefslogtreecommitdiffstats
path: root/src/auth.rs
blob: 09494fb2119dfaf31800c6bf9bb775048b551a3c (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
use argon2::{Argon2, PasswordHash, PasswordVerifier};
use axum::extract::State;
use axum_extra::{
    headers::{authorization::Basic, Authorization},
    TypedHeader,
};
use uuid::Uuid;

use crate::state::AppState;

use self::{error::Error, jwt::JWT};

pub use self::claims::{AccessClaims, RefreshClaims};

pub mod claims;
pub mod credentials;
pub mod error;
pub mod jwt;

pub fn router() -> axum::Router<AppState> {
    use axum::routing::get;

    axum::Router::new()
        .route("/issue", get(issue))
        .route("/refresh", get(refresh))
        .merge(credentials::router())
}

pub async fn issue(
    State(state): State<AppState>,
    TypedHeader(Authorization(basic)): TypedHeader<Authorization<Basic>>,
) -> Result<(AccessClaims, RefreshClaims), Error> {
    let uuid = Uuid::try_parse(basic.username())?;

    let p: String = sqlx::query_scalar!("SELECT password_hash FROM credential WHERE id = $1", uuid)
        .fetch_optional(&state.pool)
        .await?
        .ok_or(Error::LoginInvalid)?;

    Argon2::default().verify_password(basic.password().as_bytes(), &PasswordHash::new(&p)?)?;

    let refresh = RefreshClaims::issue(uuid);
    let access = refresh.refresh();
    Ok((access, refresh))
}

pub async fn refresh(claims: RefreshClaims) -> AccessClaims {
    claims.refresh()
}

#[cfg(test)]
mod tests {
    use super::*;

    use axum::{
        body::Body,
        http::{header::AUTHORIZATION, Request, StatusCode},
        Router,
    };
    use axum_extra::headers::authorization::Credentials;
    use sqlx::PgPool;
    use tower::ServiceExt;

    use crate::tests::{setup_test_env, TestResult};

    #[sqlx::test(fixtures(path = "../fixtures", scripts("users")))]
    async fn test_issue_ok(pool: PgPool) -> TestResult {
        setup_test_env();

        let router = Router::new().merge(router()).with_state(AppState { pool });

        let auth = Authorization::basic(
            "4c14f795-86f0-4361-a02f-0edb966fb145",
            "solongandthanksforallthefish",
        );

        let request = Request::builder()
            .uri("/issue")
            .method("GET")
            .header(AUTHORIZATION, auth.0.encode())
            .body(Body::empty())?;

        let response = router.oneshot(request).await?;

        assert_eq!(StatusCode::OK, response.status());

        Ok(())
    }

    #[sqlx::test(fixtures(path = "../fixtures", scripts("users")))]
    async fn test_issue_unauthorized(pool: PgPool) -> TestResult {
        setup_test_env();

        let router = Router::new().merge(router()).with_state(AppState { pool });

        let auth = Authorization::basic("4c14f795-86f0-4361-a02f-0edb966fb145", "hunter2");

        let request = Request::builder()
            .uri("/issue")
            .method("GET")
            .header(AUTHORIZATION, auth.0.encode())
            .body(Body::empty())?;

        let response = router.oneshot(request).await?;

        assert_eq!(StatusCode::UNAUTHORIZED, response.status());

        Ok(())
    }
}