1use {
2 base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD},
3 reqwest::{Client, Url},
4 serde::{Deserialize, Serialize},
5 sha2::{Digest, Sha256},
6 smbcloud_model::error_codes::{ErrorCode, ErrorResponse},
7 smbcloud_network::{environment::Environment, network},
8 uuid::Uuid,
9};
10
11const AUTHORIZE_PATH: &str = "oauth/authorize";
12const TOKEN_PATH: &str = "oauth/token";
13const USERINFO_PATH: &str = "oauth/userinfo";
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct AuthorizationRequest {
17 pub authorize_url: String,
18 pub redirect_uri: String,
19 pub state: String,
20 pub code_verifier: String,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct CallbackPayload {
25 pub code: String,
26 pub state: String,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct TokenResponse {
31 pub access_token: String,
32 pub token_type: String,
33 pub expires_in: Option<i32>,
34 pub refresh_token: Option<String>,
35 pub scope: Option<String>,
36 pub id_token: Option<String>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct UserInfo {
41 pub sub: String,
42 pub email: Option<String>,
43 pub email_verified: Option<bool>,
44 pub tenant_id: Option<u64>,
45 pub tenant_slug: Option<String>,
46}
47
48pub fn build_authorization_request(
49 env: Environment,
50 oidc_client_id: &str,
51 redirect_uri: String,
52) -> Result<AuthorizationRequest, ErrorResponse> {
53 let code_verifier = format!("{}{}", Uuid::new_v4().simple(), Uuid::new_v4().simple());
54 let code_challenge = URL_SAFE_NO_PAD.encode(Sha256::digest(code_verifier.as_bytes()));
55 let state = Uuid::new_v4().to_string();
56
57 let mut url =
58 issuer_base_url(env)
59 .join(AUTHORIZE_PATH)
60 .map_err(|err| ErrorResponse::Error {
61 error_code: ErrorCode::ParseError,
62 message: err.to_string(),
63 })?;
64
65 url.query_pairs_mut()
66 .append_pair("client_id", oidc_client_id)
67 .append_pair("redirect_uri", &redirect_uri)
68 .append_pair("response_type", "code")
69 .append_pair("scope", "openid profile email")
70 .append_pair("state", &state)
71 .append_pair("code_challenge", &code_challenge)
72 .append_pair("code_challenge_method", "S256");
73
74 Ok(AuthorizationRequest {
75 authorize_url: url.to_string(),
76 redirect_uri,
77 state,
78 code_verifier,
79 })
80}
81
82pub fn parse_callback_url(callback_url: &str) -> Result<CallbackPayload, ErrorResponse> {
83 let url = Url::parse(callback_url).map_err(|err| ErrorResponse::Error {
84 error_code: ErrorCode::ParseError,
85 message: err.to_string(),
86 })?;
87
88 let mut code = None;
89 let mut state = None;
90
91 for (key, value) in url.query_pairs() {
92 match key.as_ref() {
93 "code" => code = Some(value.into_owned()),
94 "state" => state = Some(value.into_owned()),
95 _ => {}
96 }
97 }
98
99 match (code, state) {
100 (Some(code), Some(state)) => Ok(CallbackPayload { code, state }),
101 _ => Err(ErrorResponse::Error {
102 error_code: ErrorCode::InvalidParams,
103 message: "Missing authorization code or state.".to_string(),
104 }),
105 }
106}
107
108pub async fn exchange_code(
109 env: Environment,
110 oidc_client_id: &str,
111 redirect_uri: &str,
112 code: &str,
113 code_verifier: &str,
114) -> Result<TokenResponse, ErrorResponse> {
115 let url = issuer_base_url(env)
116 .join(TOKEN_PATH)
117 .map_err(|err| ErrorResponse::Error {
118 error_code: ErrorCode::ParseError,
119 message: err.to_string(),
120 })?;
121
122 let builder = Client::new()
123 .post(url)
124 .form(&[
125 ("grant_type", "authorization_code"),
126 ("client_id", oidc_client_id),
127 ("code", code),
128 ("redirect_uri", redirect_uri),
129 ("code_verifier", code_verifier),
130 ])
131 .header("Accept", "application/json");
132
133 network::request(builder).await
134}
135
136pub async fn get_userinfo(
137 env: Environment,
138 access_token: &str,
139 tenant_id: Option<&str>,
140) -> Result<UserInfo, ErrorResponse> {
141 let url = issuer_base_url(env)
142 .join(USERINFO_PATH)
143 .map_err(|err| ErrorResponse::Error {
144 error_code: ErrorCode::ParseError,
145 message: err.to_string(),
146 })?;
147
148 let mut builder = Client::new()
149 .get(url)
150 .bearer_auth(access_token)
151 .header("Accept", "application/json");
152
153 if let Some(tenant_id) = tenant_id {
154 builder = builder.header("X-Smbcloud-Tenant-Id", tenant_id);
155 }
156
157 network::request(builder).await
158}
159
160fn issuer_base_url(env: Environment) -> Url {
161 Url::parse(&format!("{}://{}/", env.api_protocol(), env.api_host()))
162 .expect("valid smbcloud api base url")
163}