myc_http_tools/providers/google/
endpoints.rs

1use super::{
2    functions::{get_google_user, request_token},
3    models::{QueryCode, TokenClaims},
4};
5use crate::models::auth_config::AuthConfig;
6
7use actix_web::{
8    cookie::{time::Duration as ActixWebDuration, Cookie},
9    get, web, HttpResponse, Responder,
10};
11use chrono::{prelude::*, Duration};
12use jsonwebtoken::{encode, EncodingKey, Header};
13use myc_config::optional_config::OptionalConfig;
14use reqwest::header::LOCATION;
15
16pub fn configure(conf: &mut web::ServiceConfig) {
17    conf.service(google_callback_url);
18}
19
20/// Callback URL for Google Oauth2
21///
22/// This endpoint is called by Google after the user authorizes the application.
23///
24#[utoipa::path(
25    get,
26    responses(
27        (
28            status = 200,
29            description = "Redirect user to auth url",
30        )
31    ),
32    security(())
33)]
34#[get("/callback")]
35pub async fn google_callback_url(
36    query: web::Query<QueryCode>,
37    data: web::Data<AuthConfig>,
38) -> impl Responder {
39    let code = &query.code;
40    let state = &query.state;
41
42    if code.is_empty() {
43        return HttpResponse::Unauthorized().json(serde_json::json!({
44            "status": "fail",
45            "message": "Authorization code not provided!"
46        }));
47    }
48
49    let token_response = match request_token(code.as_str(), &data).await {
50        Err(err) => {
51            return HttpResponse::BadGateway().json(serde_json::json!({
52                "status": "fail",
53                "message": err.to_string()
54            }));
55        }
56        Ok(res) => res,
57    };
58
59    let google_user = match get_google_user(
60        &token_response.access_token,
61        &token_response.id_token,
62    )
63    .await
64    {
65        Err(err) => {
66            return HttpResponse::BadGateway().json(serde_json::json!({
67                "status": "fail",
68                "message": err.to_string(),
69            }));
70        }
71        Ok(res) => res,
72    };
73
74    let config = match data.as_ref().google.to_owned() {
75        OptionalConfig::Disabled => {
76            return HttpResponse::BadGateway().json(serde_json::json!({
77                "status": "fail",
78                "message": "Google Oauth2 is disabled!"
79            }));
80        }
81        OptionalConfig::Enabled(config) => config,
82    };
83
84    let jwt_max_age = match config.jwt_max_age.async_get_or_error().await {
85        Ok(age) => age,
86        Err(err) => {
87            return HttpResponse::BadGateway().json(serde_json::json!({
88                "status": "fail",
89                "message": err.to_string()
90            }));
91        }
92    };
93
94    let jwt_secret = match config.jwt_secret.async_get_or_error().await {
95        Ok(secret) => secret,
96        Err(err) => {
97            return HttpResponse::BadGateway().json(serde_json::json!({
98                "status": "fail",
99                "message": err.to_string()
100            }));
101        }
102    };
103
104    let client_origin = match config.client_origin.async_get_or_error().await {
105        Ok(origin) => origin,
106        Err(err) => {
107            return HttpResponse::BadGateway().json(serde_json::json!({
108                "status": "fail",
109                "message": err.to_string()
110            }));
111        }
112    };
113
114    let now = Utc::now();
115    let iat = now.timestamp() as usize;
116    let exp = (now + Duration::minutes(jwt_max_age)).timestamp() as usize;
117
118    let claims = TokenClaims {
119        sub: google_user.id.to_owned(),
120        exp,
121        iat,
122        iss: "https://accounts.google.com".to_string(),
123        id: google_user.id.to_owned(),
124        email: google_user.email.to_owned(),
125        verified_email: google_user.verified_email.to_owned(),
126        name: google_user.name.to_owned(),
127        given_name: google_user.given_name.to_owned(),
128        family_name: google_user.family_name.to_owned(),
129        picture: google_user.picture.to_owned(),
130        locale: google_user.locale.to_owned(),
131    };
132
133    let token = match encode(
134        &Header::default(),
135        &claims,
136        &EncodingKey::from_secret(jwt_secret.as_ref()),
137    ) {
138        Ok(token) => token,
139        Err(err) => {
140            tracing::debug!("Error encoding token: {:?}", err);
141            return HttpResponse::BadGateway().json(serde_json::json!({
142                "status": "fail",
143                "message": "Error encoding token"
144            }));
145        }
146    };
147
148    HttpResponse::Ok()
149        .append_header((
150            LOCATION,
151            format!("{}{}", client_origin.to_owned(), state),
152        ))
153        .cookie(
154            Cookie::build("token", token.to_owned())
155                .path("/")
156                .max_age(ActixWebDuration::new(60 * jwt_max_age, 0))
157                .http_only(true)
158                .finish(),
159        )
160        .json(serde_json::json!({
161            "status": "success",
162            "token": token
163        }))
164}