Skip to main content

casdoor_rust_sdk/
jwt.rs

1// Copyright 2026 The Casdoor Authors. All Rights Reserved.
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 base64::engine::general_purpose::STANDARD;
16use base64::Engine as _;
17use jsonwebtoken::{Algorithm, DecodingKey, Validation};
18use serde::{Deserialize, Serialize};
19use serde_json::Value;
20use x509_parser::prelude::{FromDer, X509Certificate};
21
22use crate::client::Client;
23use crate::error::{CasdoorError, Result};
24use crate::user::User;
25
26/// Claims is the payload of the JWT token issued by Casdoor, it embeds the [`User`] the
27/// token was issued to.
28#[derive(Debug, Clone, Default, Serialize, Deserialize)]
29#[serde(rename_all = "camelCase")]
30pub struct Claims {
31    #[serde(flatten)]
32    pub user: User,
33
34    #[serde(default, alias = "TokenType")]
35    pub token_type: String,
36    #[serde(default)]
37    pub nonce: String,
38    #[serde(default)]
39    pub tag: String,
40    #[serde(default)]
41    pub scope: String,
42    /// The `azp` (Authorized Party) claim. Optional.
43    /// See <https://openid.net/specs/openid-connect-core-1_0.html#IDToken>.
44    #[serde(default)]
45    pub azp: String,
46    #[serde(default)]
47    pub provider: String,
48    #[serde(default)]
49    pub signin_method: String,
50
51    // The registered claims of RFC 7519.
52    #[serde(default)]
53    pub iss: String,
54    #[serde(default)]
55    pub sub: String,
56    /// The audience, it is a string or an array of strings depending on the token format.
57    #[serde(default)]
58    pub aud: Value,
59    #[serde(default)]
60    pub exp: Option<i64>,
61    #[serde(default)]
62    pub nbf: Option<i64>,
63    #[serde(default)]
64    pub iat: Option<i64>,
65    #[serde(default)]
66    pub jti: String,
67}
68
69impl Claims {
70    /// Return true if the token is a refresh token.
71    ///
72    /// Casdoor emits the claim as `tokenType` for the JWT, JWT-Empty and JWT-Standard
73    /// formats, and as `TokenType` for JWT-Custom, both land in `token_type`.
74    pub fn is_refresh_token(&self) -> bool {
75        self.token_type == "refresh-token"
76    }
77}
78
79impl Client {
80    /// Parse and verify a JWT token issued by Casdoor with the application's certificate,
81    /// and return its claims. The RSA (`RS256`, `RS384`, `RS512`, `PS256`, `PS384`,
82    /// `PS512`) and the ECDSA (`ES256`, `ES384`) signing methods are supported.
83    pub fn parse_jwt_token(&self, token: &str) -> Result<Claims> {
84        let header = jsonwebtoken::decode_header(token)?;
85        let key = self.decoding_key(header.alg)?;
86
87        let mut validation = Validation::new(header.alg);
88        validation.validate_aud = false;
89
90        Ok(jsonwebtoken::decode::<Claims>(token, &key, &validation)?.claims)
91    }
92
93    fn decoding_key(&self, alg: Algorithm) -> Result<DecodingKey> {
94        let pem = public_key_pem(&self.config.certificate)?;
95
96        let key = match alg {
97            Algorithm::RS256
98            | Algorithm::RS384
99            | Algorithm::RS512
100            | Algorithm::PS256
101            | Algorithm::PS384
102            | Algorithm::PS512 => DecodingKey::from_rsa_pem(pem.as_bytes())?,
103            Algorithm::ES256 | Algorithm::ES384 => DecodingKey::from_ec_pem(pem.as_bytes())?,
104            Algorithm::EdDSA => DecodingKey::from_ed_pem(pem.as_bytes())?,
105            _ => {
106                return Err(CasdoorError::Certificate(format!(
107                    "unsupported signing method: {alg:?}"
108                )))
109            }
110        };
111
112        Ok(key)
113    }
114}
115
116/// Turn the certificate of the Casdoor application into a public key in the PEM format,
117/// which is what the JWT libraries expect. Both an x509 certificate and a bare public key
118/// are accepted, so that a certificate copied from Casdoor's "Certs" page just works.
119fn public_key_pem(certificate: &str) -> Result<String> {
120    let der = decode_pem_body(certificate)?;
121
122    // A certificate wraps the public key, a "PUBLIC KEY" PEM already is the key itself.
123    let public_key_der = match X509Certificate::from_der(&der) {
124        Ok((_, cert)) => cert.tbs_certificate.subject_pki.raw.to_vec(),
125        Err(_) => der,
126    };
127
128    let mut pem = String::from("-----BEGIN PUBLIC KEY-----\n");
129    for line in STANDARD.encode(&public_key_der).as_bytes().chunks(64) {
130        pem.push_str(&String::from_utf8_lossy(line));
131        pem.push('\n');
132    }
133    pem.push_str("-----END PUBLIC KEY-----\n");
134
135    Ok(pem)
136}
137
138/// Decode the base64 body of the first PEM block of `content`.
139fn decode_pem_body(content: &str) -> Result<Vec<u8>> {
140    let body: String = content
141        .lines()
142        .map(str::trim)
143        .skip_while(|line| !line.starts_with("-----BEGIN"))
144        .skip(1)
145        .take_while(|line| !line.starts_with("-----END"))
146        .collect();
147
148    if body.is_empty() {
149        return Err(CasdoorError::Certificate(
150            "the certificate is empty or is not in the PEM format".to_string(),
151        ));
152    }
153
154    STANDARD
155        .decode(body)
156        .map_err(|e| CasdoorError::Certificate(e.to_string()))
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    /// A self-signed test certificate and a token signed by its private key, the token
164    /// expires in the year 2100.
165    const CERTIFICATE: &str = include_str!("../tests/testdata/cert.pem");
166    const TOKEN: &str = include_str!("../tests/testdata/token.txt");
167
168    fn client() -> Client {
169        Client::new(
170            "http://localhost:8000",
171            "client-id",
172            "client-secret",
173            CERTIFICATE,
174            "built-in",
175            "app-built-in",
176        )
177    }
178
179    #[test]
180    fn parse_jwt_token_reads_the_user_of_the_token() {
181        let claims = client().parse_jwt_token(TOKEN.trim()).unwrap();
182
183        assert_eq!(claims.user.owner, "built-in");
184        assert_eq!(claims.user.name, "admin");
185        assert_eq!(claims.user.display_name, "Admin User");
186        assert_eq!(claims.user.email, "admin@example.com");
187        assert!(claims.user.is_admin);
188
189        // The Casdoor server sends a null for the empty slices of its Go structs.
190        assert!(claims.user.groups.is_empty());
191        assert!(claims.user.address.is_empty());
192
193        assert_eq!(claims.token_type, "access-token");
194        assert!(!claims.is_refresh_token());
195        assert_eq!(claims.tag, "staff");
196        assert_eq!(claims.iss, "http://localhost:8000");
197        assert_eq!(claims.exp, Some(4102444800));
198    }
199
200    #[test]
201    fn parse_jwt_token_rejects_a_tampered_token() {
202        let token = TOKEN.trim();
203        let tampered = format!("{}a", &token[..token.len() - 1]);
204
205        assert!(client().parse_jwt_token(&tampered).is_err());
206    }
207
208    #[test]
209    fn parse_jwt_token_rejects_a_token_when_the_certificate_is_not_set() {
210        let client = Client::new("http://localhost:8000", "", "", "", "built-in", "");
211
212        let error = client.parse_jwt_token(TOKEN.trim()).unwrap_err();
213        assert!(matches!(error, CasdoorError::Certificate(_)));
214    }
215
216    #[test]
217    fn public_key_pem_accepts_a_public_key_as_well_as_a_certificate() {
218        let from_certificate = public_key_pem(CERTIFICATE).unwrap();
219        let from_public_key = public_key_pem(&from_certificate).unwrap();
220
221        assert!(from_certificate.starts_with("-----BEGIN PUBLIC KEY-----\n"));
222        assert_eq!(from_certificate, from_public_key);
223    }
224}