torrust_index_backend/web/api/v1/auth.rs
1//! API authentication.
2//!
3//! The API uses a [bearer token authentication scheme](https://datatracker.ietf.org/doc/html/rfc6750).
4//!
5//! API clients must have an account on the website to be able to use the API.
6//!
7//! # Authentication flow
8//!
9//! - [Registration](#registration)
10//! - [Login](#login)
11//! - [Using the token](#using-the-token)
12//!
13//! ## Registration
14//!
15//! ```bash
16//! curl \
17//! --header "Content-Type: application/json" \
18//! --request POST \
19//! --data '{"username":"indexadmin","email":"indexadmin@torrust.com","password":"BenoitMandelbrot1924","confirm_password":"BenoitMandelbrot1924"}' \
20//! http://127.0.0.1:3001/v1/user/register
21//! ```
22//!
23//! **NOTICE**: The first user is automatically an administrator. Currently,
24//! there is no way to change this. There is one administrator per instance.
25//! And you cannot delete the administrator account or make another user an
26//! administrator. For testing purposes, you can create a new administrator
27//! account by creating a new user and then manually changing the `administrator`
28//! field in the `torrust_users` table to `1`.
29//!
30//! ## Login
31//!
32//! ```bash
33//! curl \
34//! --header "Content-Type: application/json" \
35//! --request POST \
36//! --data '{"login":"indexadmin","password":"BenoitMandelbrot1924"}' \
37//! http://127.0.0.1:3001/v1/user/login
38//! ```
39//!
40//! **Response**
41//!
42//! ```json
43//! {
44//! "data":{
45//! "token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjp7InVzZXJfaWQiOjEsInVzZXJuYW1lIjoiaW5kZXhhZG1pbiIsImFkbWluaXN0cmF0b3IiOnRydWV9LCJleHAiOjE2ODYyMTU3ODh9.4k8ty27DiWwOk4WVcYEhIrAndhpXMRWnLZ3i_HlJnvI",
46//! "username":"indexadmin",
47//! "admin":true
48//! }
49//! }
50//! ```
51//!
52//! **NOTICE**: The token is valid for 2 weeks (`1_209_600` seconds). After that,
53//! you will have to renew the token.
54//!
55//! **NOTICE**: The token is associated with the user role. If you change the
56//! user's role, you will have to log in again to get a new token with the new
57//! role.
58//!
59//! ## Using the token
60//!
61//! Some endpoints require authentication. To use the token, you must add the
62//! `Authorization` header to your request. For example, if you want to add a
63//! new category, you must do the following:
64//!
65//! ```bash
66//! curl \
67//! --header "Content-Type: application/json" \
68//! --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjp7InVzZXJfaWQiOjEsInVzZXJuYW1lIjoiaW5kZXhhZG1pbiIsImFkbWluaXN0cmF0b3IiOnRydWV9LCJleHAiOjE2ODYyMTU3ODh9.4k8ty27DiWwOk4WVcYEhIrAndhpXMRWnLZ3i_HlJnvI" \
69//! --request POST \
70//! --data '{"name":"new category","icon":null}' \
71//! http://127.0.0.1:3001/v1/category
72//! ```
73//!
74//! **Response**
75//!
76//! ```json
77//! {
78//! "data": "new category"
79//! }
80//! ```
81use std::sync::Arc;
82
83use hyper::http::HeaderValue;
84
85use crate::common::AppData;
86use crate::errors::ServiceError;
87use crate::models::user::{UserClaims, UserCompact, UserId};
88use crate::services::authentication::JsonWebToken;
89use crate::web::api::v1::extractors::bearer_token::BearerToken;
90
91pub struct Authentication {
92 json_web_token: Arc<JsonWebToken>,
93}
94
95impl Authentication {
96 #[must_use]
97 pub fn new(json_web_token: Arc<JsonWebToken>) -> Self {
98 Self { json_web_token }
99 }
100
101 /// Create Json Web Token
102 pub async fn sign_jwt(&self, user: UserCompact) -> String {
103 self.json_web_token.sign(user).await
104 }
105
106 /// Verify Json Web Token
107 ///
108 /// # Errors
109 ///
110 /// This function will return an error if the JWT is not good or expired.
111 pub async fn verify_jwt(&self, token: &str) -> Result<UserClaims, ServiceError> {
112 self.json_web_token.verify(token).await
113 }
114
115 /// Get logged-in user ID from bearer token
116 ///
117 /// # Errors
118 ///
119 /// This function will return an error if it can get claims from the request
120 pub async fn get_user_id_from_bearer_token(&self, maybe_token: &Option<BearerToken>) -> Result<UserId, ServiceError> {
121 let claims = self.get_claims_from_bearer_token(maybe_token).await?;
122 Ok(claims.user.user_id)
123 }
124
125 /// Get Claims from bearer token
126 ///
127 /// # Errors
128 ///
129 /// This function will:
130 ///
131 /// - Return an `ServiceError::TokenNotFound` if `HeaderValue` is `None`.
132 /// - Pass through the `ServiceError::TokenInvalid` if unable to verify the JWT.
133 async fn get_claims_from_bearer_token(&self, maybe_token: &Option<BearerToken>) -> Result<UserClaims, ServiceError> {
134 match maybe_token {
135 Some(token) => match self.verify_jwt(&token.value()).await {
136 Ok(claims) => Ok(claims),
137 Err(e) => Err(e),
138 },
139 None => Err(ServiceError::TokenNotFound),
140 }
141 }
142}
143
144/// Parses the token from the `Authorization` header.
145///
146/// # Panics
147///
148/// This function will panic if the `Authorization` header is not a valid `String`.
149pub fn parse_token(authorization: &HeaderValue) -> String {
150 let split: Vec<&str> = authorization
151 .to_str()
152 .expect("variable `auth` contains data that is not visible ASCII chars.")
153 .split("Bearer")
154 .collect();
155 let token = split[1].trim();
156 token.to_string()
157}
158
159/// If the user is logged in, returns the user's ID. Otherwise, returns `None`.
160///
161/// # Errors
162///
163/// It returns an error if we cannot get the user from the bearer token.
164pub async fn get_optional_logged_in_user(
165 maybe_bearer_token: Option<BearerToken>,
166 app_data: Arc<AppData>,
167) -> Result<Option<UserId>, ServiceError> {
168 match maybe_bearer_token {
169 Some(bearer_token) => match app_data.auth.get_user_id_from_bearer_token(&Some(bearer_token)).await {
170 Ok(user_id) => Ok(Some(user_id)),
171 Err(error) => Err(error),
172 },
173 None => Ok(None),
174 }
175}