discovery_connect/api/
auth.rs

1// Copyright 2023 Ikerian AG
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use reqwest::Client;
16use serde::{Deserialize, Serialize};
17use serde_json;
18
19#[derive(Debug, Serialize)]
20pub struct AuthPayload {
21    pub username: String,
22    pub password: String,
23    pub grant_type: &'static str,
24    pub mfa_code: Option<String>,
25}
26
27#[derive(Debug, Deserialize)]
28pub struct AuthResponse {
29    pub challenge: Option<String>,
30    pub access_token: String,
31    pub refresh_token: String,
32}
33
34/// Retrieves a new access token using a refresh token.
35///
36/// Sends a request to refresh an access token using the provided refresh token. This function constructs
37/// a refresh token payload, sends it to the specified OAuth token endpoint, and handles the server's response.
38///
39/// # Arguments
40///
41/// * `client` - A reference to a `Client` object used to send HTTP requests.
42/// * `url` - A `String` reference representing the base URL of the OAuth token endpoint.
43/// * `refresh_token` - A `String` reference representing the refresh token used to obtain a new access token.
44///
45/// # Returns
46///
47/// A `Result` containing either the authentication response data on success (`AuthResponse`),
48/// or an `reqwest::Error` on failure. The `AuthResponse` includes the new access token and other relevant information.
49///
50/// # Errors
51///
52/// Returns an `Err` of type `reqwest::Error` if the access token request fails, if the server response status
53/// is not OK, or if there is an issue with the server's response format.
54///
55/// # Example
56///
57/// ```
58/// use discovery_connect::auth::{access_token, AuthResponse};
59/// use reqwest::Client;
60///
61/// async fn get_access_token_example() {
62///     let client = Client::new();
63///     let base_url = "https://api.example.discovery.retinai.com";
64///     let refresh_token = "your_refresh_token";
65///     let response = access_token(&client, &base_url, &refresh_token).await;
66///     match response {
67///         Ok(auth_response) => println!("Access token: {}", auth_response.access_token),
68///         Err(e) => println!("Error: {}", e),
69///     }
70/// }
71/// ```
72pub async fn access_token(
73    client: &Client,
74    url: &str,
75    refresh_token: &str,
76) -> Result<AuthResponse, reqwest::Error> {
77    let refresh_payload = serde_json::json!({
78        "grant_type": "refresh_token",
79        "refresh_token": refresh_token
80    });
81
82    let url = format!("{}/oauth/token/", url);
83
84    match client.post(&url).json(&refresh_payload).send().await {
85        Ok(response) => {
86            if response.status() != reqwest::StatusCode::OK {
87                let e: reqwest::Error = response.error_for_status().unwrap_err();
88                return Err(e);
89            }
90            let response: AuthResponse = response.json().await.unwrap();
91            Ok(response)
92        }
93        Err(error) => {
94            eprintln!("  error: {:?}", error);
95            Err(error)
96        }
97    }
98}
99
100/// Authenticates a user against a server and obtains an authentication token.
101///
102/// Asynchronously authenticates a user using their credentials and optionally a multi-factor authentication (MFA) code.
103/// This function builds the authentication request, sends it to the server, and processes the response.
104///
105/// # Arguments
106///
107/// * `client` - A reference to a `reqwest::Client` used for making HTTP requests.
108/// * `url` - A string slice representing the base URL of the authentication server.
109/// * `client_id` - A string slice representing the client identifier.
110/// * `client_secret` - A string slice representing the client secret.
111/// * `email` - A string slice representing the user's email address.
112/// * `password` - A string slice representing the user's password.
113/// * `mfa_code` - An optional string slice representing the multi-factor authentication code.
114///
115/// # Returns
116///
117/// A `Result` containing either the authentication response data on success (`AuthResponse`),
118/// or an `reqwest::Error` on failure.
119///
120/// # Errors
121///
122/// Returns an `Err` of type `reqwest::Error` if the authentication request fails or if the server
123/// response status is not OK.
124///
125/// # Example
126///
127/// ```
128/// use discovery_connect::{QueryClient};
129/// use discovery_connect::auth::{login, AuthResponse};
130/// use std::sync::Arc;
131///
132/// let qc = Arc::new(QueryClient::new(
133///         "https://api.example.discovery.retinai.com",
134///         "client_id",
135///         "client_secret",
136///         "user@example",
137///         "password123",
138///         std::time::Duration::from_secs(10)));
139/// async {
140///     let res = login(
141///         &qc.client,
142///         "https://api.example.discovery.retinai.com",
143///         "client_id123",
144///         "client_secret456",
145///         "user@example.com",
146///         "password123",
147///         Some("123456")
148///     ).await;
149///     match res {
150///         Ok(auth_response) => println!("Access Token: {:?}", auth_response.access_token),
151///         Err(e) => println!("Error: {}", e),
152///     }
153/// };
154/// ```
155pub async fn login(
156    client: &Client,
157    url: &str,
158    client_id: &str,
159    client_secret: &str,
160    email: &str,
161    password: &str,
162    mfa_code: Option<&str>,
163) -> Result<AuthResponse, reqwest::Error> {
164    let credentials = format!("{}:{}", client_id, client_secret);
165    // let credentials = general_purpose::STANDARD.encode(credentials);
166    let credentials = base64_url::encode(&credentials);
167
168    let url = format!("{}/oauth/token/", url);
169
170    let payload = AuthPayload {
171        username: email.to_string(),
172        password: password.to_string(),
173        grant_type: "password",
174        mfa_code: mfa_code.map(|s| s.to_string()),
175    };
176
177    let c = client
178        .post(&url)
179        .header("Authorization", format!("Basic {}", credentials))
180        .json(&payload);
181
182    match c.send().await {
183        Ok(response) => {
184            if response.status() != reqwest::StatusCode::OK {
185                return Err(response.error_for_status().unwrap_err());
186            }
187            let response: AuthResponse = response.json().await.unwrap();
188            Ok(response)
189        }
190        Err(error) => Err(error),
191    }
192}