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