summaryrefslogtreecommitdiffstats
path: root/crates/auth/src/server/routes/authorised.rs
blob: 42bbde23f13412be6ea8bf69e4717a48a2e7db34 (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
use std::{str::FromStr, time::Duration};

use anyhow::Context;
use axum::{
    extract::{Query, State},
    http::HeaderMap,
    response::{IntoResponse, Redirect},
};
use axum_extra::{TypedHeader, headers};
use oauth2::{AuthorizationCode, TokenResponse};
use reqwest::header::SET_COOKIE;
use serde::{Deserialize, Serialize};
use sqlx::types::uuid;
use tower_sessions::{
    SessionStore,
    session::{Id, Record},
};

use crate::{
    error::AppError,
    server::{
        OAUTH_CSRF_COOKIE, csrf_token_validation::csrf_token_validation_workflow, routes::Provider,
    },
    state::AppHandle,
};

#[derive(Debug, Deserialize)]
pub struct AuthRequest {
    provider: Provider,
    code: String,
    pub state: String,
}

#[derive(Debug, Deserialize, Serialize)]
struct User {
    id: String,
    avatar: Option<String>,
    username: String,
    discriminator: String,
}

/// The cookie to store the session id for user information.
const SESSION_COOKIE: &str = "info";
const SESSION_DATA_KEY: &str = "data";

async fn login_authorized(
    Query(query): Query<AuthRequest>,
    State(state): State<AppHandle>,
    TypedHeader(cookies): TypedHeader<headers::Cookie>,
) -> Result<impl IntoResponse, AppError> {
    let oauth_session_id = Id::from_str(
        cookies
            .get(OAUTH_CSRF_COOKIE)
            .context("missing session cookie")?,
    )
    .unwrap();
    csrf_token_validation_workflow(&query, &state.session_store, oauth_session_id).await?;

    let client = state.http_client.clone();
    let store = state.session_store.clone();

    // Get an auth token
    let token = state
        .discord_client
        .exchange_code(AuthorizationCode::new(query.code.clone()))
        .request_async(&client)
        .await
        .context("failed in sending request request to authorization server")?;

    let user_data: User = client
        // https://discord.com/developers/docs/resources/user#get-current-user
        .get("https://discordapp.com/api/users/@me")
        .bearer_auth(token.access_token().secret())
        .send()
        .await
        .context("failed in sending request to target Url")?
        .json::<User>()
        .await
        .context("failed to deserialize response as JSON")?;

    // Create a new session filled with user data
    let session_id = Id(i128::from_le_bytes(uuid::Uuid::new_v4().to_bytes_le()));
    store
        .create(&mut Record {
            id: session_id,
            data: [(
                SESSION_DATA_KEY.to_string(),
                serde_json::to_value(user_data).unwrap(),
            )]
            .into(),
            expiry_date: time::OffsetDateTime::now_utc()
                + Duration::from_secs(state.local_config.oauth.session_lifespan),
        })
        .await
        .context("failed in inserting serialized value into session")?;

    // Store session and get corresponding cookie.
    let cookie = format!("{SESSION_COOKIE}={session_id}; SameSite=Lax; HttpOnly; Secure; Path=/");

    // Set cookie
    let mut headers = HeaderMap::new();
    headers.insert(
        SET_COOKIE,
        cookie.parse().context("failed to parse cookie")?,
    );

    Ok((headers, Redirect::to("/")))
}