casdoor_sdk_rust/authn/
mod.rs

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    /// Gets the pivotal and necessary secret to interact with the Casdoor server
78    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    /// Refreshes the OAuth token
96    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}