1mod models;
2
3use crate::{Method, QueryArgs, QueryResult, Sdk, SdkResult, NO_BODY};
4use anyhow::{format_err, Result};
5use jsonwebtoken::{
6 DecodingKey, TokenData, Validation,
7};
8pub use models::*;
9pub use oauth2::{basic::{BasicTokenIntrospectionResponse, BasicTokenType}, TokenIntrospectionResponse, TokenResponse};
10use oauth2::{url, AccessToken, AuthUrl, AuthorizationCode, ClientId, ClientSecret, IntrospectionUrl, RedirectUrl, RefreshToken, TokenUrl};
11use openssl::pkey::Id;
12use openssl::{
13 base64,
14 pkey::{PKey, Public},
15 sha::sha256,
16};
17use rand::Rng;
18use std::{fmt::Write, iter};
19use uuid::Uuid;
20
21impl Sdk {
22 pub fn authn(&self) -> AuthSdk {
23 AuthSdk { sdk: self.clone() }
24 }
25}
26
27#[derive(Debug, Clone)]
28pub struct AuthSdk {
29 sdk: Sdk,
30}
31
32impl AuthSdk {
33 fn client_id(&self) -> ClientId {
34 ClientId::new(self.sdk.client_id.to_string())
35 }
36
37 fn client_secret(&self) -> ClientSecret {
38 ClientSecret::new(self.sdk.client_secret.to_string())
39 }
40
41 fn auth_url(&self, url_path: &str) -> Result<AuthUrl, url::ParseError> {
42 let mut url = String::new();
43
44 url.write_str(&self.sdk.endpoint).unwrap();
45 url.write_str(url_path).unwrap();
46
47 AuthUrl::new(url)
48 }
49
50 fn token_url(&self, url_path: &str) -> Result<TokenUrl, url::ParseError> {
51 let mut url = String::new();
52
53 url.write_str(&self.sdk.endpoint).unwrap();
54 url.write_str(url_path).unwrap();
55
56 Ok(TokenUrl::new(url)?)
57 }
58
59 fn introspect_url(&self, url_path: &str) -> Result<IntrospectionUrl> {
60 let mut url = String::new();
61
62 url.write_str(&self.sdk.endpoint)?;
63 url.write_str(url_path)?;
64
65 Ok(IntrospectionUrl::new(url)?)
66 }
67
68 fn logout_url(&self, path: String) -> String {
69 let mut logout_url = String::new();
70
71 logout_url.write_str(&self.sdk.endpoint).unwrap();
72 logout_url.write_str(&path).unwrap();
73
74 logout_url
75 }
76
77 pub async fn get_oauth_token(&self, code: String) -> SdkResult<CasdoorTokenResponse> {
79 let casdoor_client = OAuth2Client::new(self.client_id(), self.client_secret(), self.auth_url("/api/login/oauth/authorize")?)
80 .await
81 .unwrap();
82
83 let token_res: CasdoorTokenResponse = casdoor_client
84 .get_oauth_token(
85 AuthorizationCode::new(code),
86 RedirectUrl::new(self.sdk.endpoint.to_string())?,
87 self.token_url("/api/login/oauth/access_token")?,
88 )
89 .await
90 .unwrap();
91
92 Ok(token_res)
93 }
94
95 pub async fn refresh_oauth_token(&self, refresh_token: String) -> SdkResult<CasdoorTokenResponse> {
97 let casdoor_client = OAuth2Client::new(self.client_id(), self.client_secret(), self.auth_url("/api/login/oauth/authorize")?)
98 .await
99 .unwrap();
100
101 let token_res = casdoor_client
102 .refresh_token(RefreshToken::new(refresh_token), self.token_url("/api/login/oauth/refresh_token")?)
103 .await
104 .unwrap();
105
106 Ok(token_res)
107 }
108
109 pub async fn introspect_access_token(&self, token: String) -> SdkResult<BasicTokenIntrospectionResponse> {
110 let client = OAuth2Client::new(self.client_id(), self.client_secret(), self.auth_url("/api/login/oauth/authorize")?)
111 .await
112 .unwrap();
113
114 let tk: AccessToken = AccessToken::new(token);
115
116 let intro_res = client
117 .get_introspect_access_token(self.introspect_url("/api/login/oauth/introspect").unwrap(), &tk)
118 .await
119 .unwrap();
120
121 Ok(intro_res)
122 }
123
124 pub fn parse_jwt_token(&self, token: &str) -> SdkResult<ClaimsStandard> {
125 let header = jsonwebtoken::decode_header(token)?;
126
127 let mut validation = Validation::new(header.alg);
128 validation.set_audience(&[&self.sdk.client_id]);
129 validation.validate_aud = true;
130 validation.validate_exp = true;
131 validation.validate_nbf = true;
132
133 let pb_key = self.sdk.replace_cert_to_pub_key().unwrap();
134
135 let td = get_tk(pb_key, validation, token).unwrap();
136
137 Ok(td.claims)
138 }
139
140 pub fn get_signing_url(&self, redirect_url: String) -> String {
141 let scope = "read";
142 let state = self.sdk.app_name.clone().unwrap_or_default();
143 let base = format!("{}/login/oauth/authorize", self.sdk.endpoint);
144 let nonce = Uuid::new_v4();
145
146 let signing_url = url::Url::parse_with_params(
147 base.as_str(),
148 &[
149 ("client_id", self.client_id().as_str()),
150 ("redirect_uri", redirect_url.as_str()),
151 ("scope", scope),
152 ("response_type", "code"),
153 ("state", state.as_str()),
154 ("code_challenge_method", "S256"),
155 ("nonce", nonce.to_string().as_str()),
156 ("code_challenge", generate_code_challange(generate_random_string(43)).as_str()),
157 ],
158 )
159 .unwrap();
160
161 signing_url.to_string()
162 }
163
164 pub async fn logout(&self, id_token: &str, post_logout_redirect_uri: &str, state: &str) -> SdkResult<String> {
165 let logout_url = url::Url::parse_with_params(
166 self.logout_url("/api/logout".to_string()).as_str(),
167 &[
168 ("id_token_hint", id_token),
169 ("post_logout_redirect_uri", post_logout_redirect_uri),
170 ("state", state),
171 ],
172 )?;
173
174 let client = reqwest::Client::new();
175
176 let response = client.post(logout_url).send().await?.text().await?;
177
178 Ok(response)
179 }
180
181 pub fn get_signup_url(&self, redirect_url: String) -> String {
182 redirect_url.replace("/login/oauth/authorize", "/signup/oauth/authorize")
183 }
184
185 pub fn get_signup_url_enable_password(&self) -> String {
186 format!("{}/signup/{}", self.sdk.endpoint, self.sdk.app_name.clone().unwrap_or_default())
187 }
188
189 pub fn get_user_profile_url(&self, uname: String, token: Option<String>) -> String {
190 let param = match token {
191 Some(token) if !token.is_empty() => format!("?access_token={}", token),
192 _ => "".to_string(),
193 };
194 format!("{}/users/{}/{uname}{param}", self.sdk.endpoint, self.sdk.org_name)
195 }
196
197 pub fn get_my_profile_url(&self, token: Option<String>) -> String {
198 let param = match token {
199 Some(token) if !token.is_empty() => format!("?access_token={}", token),
200 _ => "".to_string(),
201 };
202 format!("{}/account{}", self.sdk.endpoint, param)
203 }
204
205 pub async fn get_sessions(&self, query_args: QueryArgs) -> SdkResult<QueryResult<Session>> {
206 self.sdk.get_models(None, query_args).await
207 }
208
209 pub async fn get_session(&self, session_pk_id: &str) -> SdkResult<Session> {
210 self.sdk
211 .request_data(
212 Method::GET,
213 self.sdk.get_url_path("get-session", true, [("sessionPkId", session_pk_id)])?,
214 NO_BODY,
215 )
216 .await?
217 .into_data_default()
218 }
219
220 pub async fn is_session_duplicated(&self, session_pk_id: &str, session_id: &str) -> SdkResult<bool> {
221 self.sdk
222 .request_data(
223 Method::GET,
224 self.sdk
225 .get_url_path("is-session-duplicated", true, [("sessionPkId", session_pk_id), ("sessionId", session_id)])?,
226 NO_BODY,
227 )
228 .await?
229 .into_data_default()
230 }
231}
232
233fn generate_random_string(length: usize) -> String {
234 const CHARSET: &[u8] = b"AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890";
235 let mut rng = rand::thread_rng();
236 let one_char = || CHARSET[rng.gen_range(0..CHARSET.len())] as char;
237 iter::repeat_with(one_char).take(length).collect()
238}
239
240fn generate_code_challange(verifier: String) -> String {
241 let bb = verifier.as_bytes();
242 let digest = sha256(bb);
243 base64::encode_block(&digest).replace("=", "-")
244}
245
246fn get_tk(pb_key: PKey<Public>, validation: Validation, token: &str) -> Result<TokenData<ClaimsStandard>> {
247 match pb_key.id() {
248 Id::RSA => {
249 let rsa_pb_key = pb_key.rsa()?.public_key_to_pem()?;
250 let decode_key = &DecodingKey::from_rsa_pem(&rsa_pb_key)?;
251 let token_data: TokenData<ClaimsStandard> = jsonwebtoken::decode(token, decode_key, &validation)?;
252
253 Ok(token_data)
254 },
255 Id::EC => {
256 let ec_pb_key = pb_key.ec_key()?.public_key_to_pem()?;
257 let decode_key = &DecodingKey::from_ec_pem(&ec_pb_key)?;
258 let token_data: TokenData<ClaimsStandard> = jsonwebtoken::decode(token, decode_key, &validation)?;
259
260 Ok(token_data)
261 },
262 Id::RSA_PSS => {
263 let ec_pb_key = pb_key.rsa()?.public_key_to_pem()?;
264 let decode_key = &DecodingKey::from_rsa_pem(&ec_pb_key)?;
265 let token_data: TokenData<ClaimsStandard> = jsonwebtoken::decode(token, decode_key, &validation)?;
266
267 Ok(token_data)
268 },
269 _ => {
270 Err(format_err!("not supported"))
271 },
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use std::fs;
278
279 use crate::Config;
280
281 #[test]
282 fn successfully_rs256_cert_ps256() {
283 let token = fs::read_to_string("./src/authn/testdata/tok_rs256_ps.txt").unwrap();
284 let cert = fs::read_to_string("./src/authn/testdata/cert_ps256.txt").unwrap();
285 let cfg = Config::new(
286 "http://localhost:8000".to_string(),
287 "2707072ef8e8048ce2df".to_string(),
288 "7d315de093a1b8268d0c7eb192bbe02f35a8877d".to_string(),
289 cert,
290 "built-in".to_string(),
291 Some("app-built-in".to_owned())
292 )
293 .into_sdk();
294
295 let authnx = cfg.authn();
296
297 let tk = authnx.parse_jwt_token(&token).unwrap();
298 assert_eq!(true, tk.reg_claims.audience.contains(&cfg.client_id));
299 }
300
301 #[test]
302 fn successfully_es256_jwt_custom() {
303 let token = fs::read_to_string("./src/authn/testdata/tok_rs256_custom.txt").unwrap();
304 let cert = fs::read_to_string("./src/authn/testdata/cert_rs256_standart.txt").unwrap();
305 let cfg = Config::new(
306 "http://localhost:8000".to_string(),
307 "1c1e0a611af6f09cb383".to_string(),
308 "secret".to_string(),
309 cert,
310 "Kubernetes".to_string(),
311 Some("Cluster".to_owned())
312 )
313 .into_sdk();
314
315 let authnx = cfg.authn();
316
317 let tk = authnx.parse_jwt_token(&token).unwrap();
318 assert_eq!(true, tk.reg_claims.audience.contains(&cfg.client_id));
319 }
320 #[test]
321 fn successfully_es256_jwt_standart() {
322 let token = fs::read_to_string("./src/authn/testdata/tok_rs256_standart.txt").unwrap();
323 let cert = fs::read_to_string("./src/authn/testdata/cert_rs256_standart.txt").unwrap();
324 let cfg = Config::new(
325 "http://localhost:8000".to_string(),
326 "1c1e0a611af6f09cb383".to_string(),
327 "secret".to_string(),
328 cert,
329 "Kubernetes".to_string(),
330 Some("Cluster".to_owned())
331 )
332 .into_sdk();
333
334 let authnx = cfg.authn();
335
336 let tk = authnx.parse_jwt_token(&token).unwrap();
337 assert_eq!("user", tk.user.display_name);
338 assert_eq!(true, tk.reg_claims.audience.contains(&cfg.client_id));
339 }
340
341 #[test]
342 fn successfully_es256_jwt() {
343 let token = fs::read_to_string("./src/authn/testdata/tok_es256.txt").unwrap();
344 let cert = fs::read_to_string("./src/authn/testdata/cert_es256.txt").unwrap();
345 let cfg = Config::new(
346 "http://localhost:8000".to_string(),
347 "7883231e5f0792b5acdf".to_string(),
348 "secret".to_string(),
349 cert,
350 "org_name".to_string(),
351 Some("app_name".to_owned())
352 )
353 .into_sdk();
354
355 let authnx = cfg.authn();
356
357 let tk = authnx.parse_jwt_token(&token).unwrap();
358 assert_eq!("user1", tk.user.display_name);
359 assert_eq!(true, tk.reg_claims.audience.contains(&cfg.client_id));
360 }
361
362 #[test]
363 fn successfully_es384_jwt() {
364 let token = fs::read_to_string("./src/authn/testdata/tok_es384.txt").unwrap();
365 let cert = fs::read_to_string("./src/authn/testdata/cert_es384.txt").unwrap();
366 let cfg = Config::new(
367 "http://localhost:8000".to_string(),
368 "7883231e5f0792b5acdf".to_string(),
369 "secret".to_string(),
370 cert,
371 "org_name".to_string(),
372 Some("app_name".to_owned())
373 )
374 .into_sdk();
375
376 let authnx = cfg.authn();
377
378 let tk = authnx.parse_jwt_token(&token).unwrap();
379 assert_eq!("user1", tk.user.display_name);
380 assert_eq!(true, tk.reg_claims.audience.contains(&cfg.client_id));
381 }
382
383 #[test]
384 fn successfully_rs512_jwt() {
385 let token = fs::read_to_string("./src/authn/testdata/tok_rs512.txt").unwrap();
386 let cert = fs::read_to_string("./src/authn/testdata/cert_rs256.txt").unwrap();
387 let cfg = Config::new(
388 "http://localhost:8000".to_string(),
389 "7883231e5f0792b5acdf".to_string(),
390 "secret".to_string(),
391 cert,
392 "org_name".to_string(),
393 Some("app_name".to_owned())
394 )
395 .into_sdk();
396
397 let authnx = cfg.authn();
398
399 let tk = authnx.parse_jwt_token(&token).unwrap();
400 assert_eq!("user1", tk.user.display_name);
401 assert_eq!(true, tk.reg_claims.audience.contains(&cfg.client_id));
402 }
403
404 #[test]
405 #[should_panic]
406 fn bad_algo_rs_tk256_cert512() {
407 let token = fs::read_to_string("./src/authn/testdata/tok_rs256.txt").unwrap();
408 let cert = fs::read_to_string("./src/authn/testdata/cert_rs512.txt").unwrap();
409 let cfg = Config::new(
410 "http://localhost:8000".to_string(),
411 "e953686f04e7055b698b".to_string(),
412 "secret".to_string(),
413 cert,
414 "org_name".to_string(),
415 Some("app_name".to_owned())
416 )
417 .into_sdk();
418
419 let authnx = cfg.authn();
420
421 let _tk = authnx.parse_jwt_token(&token).unwrap();
422 }
423
424 #[test]
425 #[should_panic]
426 fn bad_algo_es_tk256_cert512() {
427 let token = fs::read_to_string("./src/authn/testdata/tok_es256.txt").unwrap();
428 let cert = fs::read_to_string("./src/authn/testdata/cert_es384.txt").unwrap();
429 let cfg = Config::new(
430 "http://localhost:8000".to_string(),
431 "e953686f04e7055b698b".to_string(),
432 "secret".to_string(),
433 cert,
434 "org_name".to_string(),
435 Some("app_name".to_owned())
436 )
437 .into_sdk();
438
439 let authnx = cfg.authn();
440
441 let _tk = authnx.parse_jwt_token(&token).unwrap();
442 }
443}