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("/")))
}
|