Skip to main content

alibi_plugins/jwt/
endpoint.rs

1use super::{
2    JwtAudience, JwtClaimsConfig, JwtExpiration, JwtKeyPairConfig, JwtPlugin, JwtSession,
3    JwtSignOptions,
4};
5use crate::authentication_helpers::{JsonField, JsonFieldKind};
6use crate::endpoint::{definition, validate_fields, validation};
7use alibi_core::endpoint::{
8    EndpointCall, EndpointDefinition, EndpointInput, EndpointResponse, ServerEndpoint,
9};
10use alibi_core::utils::json::JsValue;
11use alibi_core::{AuthContext, AuthError, AuthResult, AuthSchema, HttpMethod};
12use chrono::Duration;
13use serde::Deserialize;
14use serde_json::{Map, Value};
15use std::str::FromStr;
16
17#[derive(Debug, Deserialize)]
18pub struct JwtTokenOutput {
19    pub token: String,
20}
21
22#[derive(Debug, Deserialize)]
23pub struct JwtVerifyOutput {
24    pub payload: Option<Map<String, Value>>,
25}
26
27pub(super) fn definitions(jwks_path: &str) -> Vec<EndpointDefinition> {
28    vec![
29        definition(
30            "getToken",
31            "getJSONWebToken",
32            Some("/token"),
33            HttpMethod::Get,
34        ),
35        definition(
36            "getJwks",
37            "getJSONWebKeySet",
38            Some(jwks_path),
39            HttpMethod::Get,
40        ),
41        definition("signJWT", "signJWT", None, HttpMethod::Post),
42        definition("verifyJWT", "verifyJWT", None, HttpMethod::Post),
43    ]
44}
45
46pub(super) fn validate(call: &EndpointCall) -> AuthResult<EndpointInput> {
47    let body = match call.operation_id() {
48        "signJWT" => Some(validate_fields(
49            call.body(),
50            "body",
51            &[
52                JsonField {
53                    name: "payload",
54                    kind: JsonFieldKind::Record,
55                    required: true,
56                },
57                JsonField {
58                    name: "overrideOptions",
59                    kind: JsonFieldKind::Record,
60                    required: false,
61                },
62            ],
63        )?),
64        "verifyJWT" => Some(validate_fields(
65            call.body(),
66            "body",
67            &[
68                JsonField::string("token", true),
69                JsonField::string("issuer", false),
70            ],
71        )?),
72        "getJSONWebToken" if call.headers().is_none() => {
73            return Err(validation("Headers is required"));
74        }
75        _ => call.body().cloned(),
76    };
77    Ok(EndpointInput {
78        body,
79        query: call.query().cloned(),
80    })
81}
82
83impl JwtPlugin {
84    #[must_use]
85    pub const fn token_endpoint() -> ServerEndpoint<JwtTokenOutput> {
86        ServerEndpoint::new("jwt", "getToken")
87    }
88
89    #[must_use]
90    pub const fn jwks_endpoint() -> ServerEndpoint<Value> {
91        ServerEndpoint::new("jwt", "getJwks")
92    }
93
94    /// Sign JSON through registered middleware; plain `sign_jwt` remains directly callable.
95    #[must_use]
96    pub fn sign_endpoint(payload: JsValue) -> ServerEndpoint<JwtTokenOutput> {
97        ServerEndpoint::new("jwt", "signJWT").with_body_value(JsValue::Object(
98            [("payload".into(), payload)].into_iter().collect(),
99        ))
100    }
101
102    #[must_use]
103    pub fn verify_endpoint(
104        token: impl Into<String>,
105        issuer: Option<String>,
106    ) -> ServerEndpoint<JwtVerifyOutput> {
107        let mut body = indexmap::IndexMap::new();
108        _ = body.insert("token".into(), JsValue::String(token.into()));
109        if let Some(issuer) = issuer {
110            _ = body.insert("issuer".into(), JsValue::String(issuer));
111        }
112        ServerEndpoint::new("jwt", "verifyJWT").with_body_value(JsValue::Object(body))
113    }
114
115    pub(super) async fn call_endpoint(
116        &self,
117        call: &EndpointCall,
118        ctx: &AuthContext<impl AuthSchema>,
119    ) -> AuthResult<EndpointResponse> {
120        match call.operation_id() {
121            "getJSONWebToken" => {
122                let read =
123                    alibi_core::session::cookie_cache::runtime::authenticated(ctx, call, false)
124                        .await
125                        .map_err(|_| super::unauthorized())?
126                        .ok_or_else(super::unauthorized)?;
127                let user = match &read.user {
128                    alibi_core::AuthenticatedUser::Stored(user) => ctx.user_view(user),
129                    alibi_core::AuthenticatedUser::Cached(user) => (**user).clone(),
130                };
131                let session = JwtSession {
132                    user,
133                    session: call.virtual_session().unwrap_or(read.session),
134                    needs_refresh: read.needs_refresh,
135                    updated_at: None,
136                    version: None,
137                };
138                call.record_authenticated_session(session.user.clone(), session.session.clone());
139                EndpointResponse::json(
140                    &serde_json::json!({"token":self.sign_session_token(call.request(),ctx,&session).await?}),
141                )
142            }
143            "getJSONWebKeySet" => {
144                EndpointResponse::json(&self.jwks_value(call.request(), ctx).await?)
145            }
146            "signJWT" => {
147                let payload = call
148                    .body()
149                    .and_then(|body| body.get("payload"))
150                    .ok_or_else(|| validation("[body.payload] Invalid input"))?;
151                let plugin = self.with_json_overrides(
152                    call.body().and_then(|body| body.get("overrideOptions")),
153                )?;
154                EndpointResponse::json(
155                    &serde_json::json!({"token":plugin.sign_jwt_json(payload,&JwtSignOptions::default(),call.request(),ctx).await?}),
156                )
157            }
158            "verifyJWT" => {
159                #[derive(Deserialize)]
160                struct Body {
161                    token: String,
162                    issuer: Option<String>,
163                }
164                let body: Body = call.body_as()?;
165                EndpointResponse::json(
166                    &serde_json::json!({"payload":self.verify_jwt(&body.token,body.issuer.as_deref(),call.request(),ctx).await?}),
167                )
168            }
169            _ => Err(AuthError::not_found("Unregistered JWT operation")),
170        }
171    }
172
173    fn with_json_overrides(&self, overrides: Option<&JsValue>) -> AuthResult<Self> {
174        let mut plugin = self.clone();
175        let Some(overrides) = overrides else {
176            return Ok(plugin);
177        };
178        if let Some(jwt) = overrides.get("jwt") {
179            plugin.config.claims = JwtClaimsConfig::default();
180            plugin.config.remote_signer = None;
181            if let Some(issuer) = jwt.get("issuer").filter(|value| !value.is_null()) {
182                plugin.config.claims.issuer = Some(
183                    issuer
184                        .as_str()
185                        .ok_or_else(|| AuthError::internal("JWT issuer must be a string"))?
186                        .into(),
187                );
188            }
189            if let Some(audience) = jwt.get("audience").filter(|value| !value.is_null()) {
190                plugin.config.claims.audience = Some(alibi_core::utils::json::from_value::<
191                    JwtAudience,
192                >(audience.clone())?);
193            }
194            if let Some(expiration) = jwt.get("expirationTime").filter(|value| !value.is_null()) {
195                plugin.config.claims.expiration = if let Some(value) = expiration.as_f64() {
196                    JwtExpiration::Numeric(value)
197                } else if let Some(value) = expiration.as_str() {
198                    JwtExpiration::AfterSeconds(time_seconds(value)?)
199                } else {
200                    return Err(AuthError::internal("Invalid JWT expiration time"));
201                };
202            }
203        }
204        if let Some(jwks) = overrides.get("jwks") {
205            plugin.config.key_pair = JwtKeyPairConfig::default();
206            plugin.config.additional_key_pairs.clear();
207            plugin.config.remote_url = jwks
208                .get("remoteUrl")
209                .and_then(JsValue::as_str)
210                .map(str::to_owned);
211            plugin.config.disable_private_key_encryption = jwks
212                .get("disablePrivateKeyEncryption")
213                .and_then(JsValue::as_bool)
214                .unwrap_or(false);
215            plugin.config.rotation_interval = jwks
216                .get("rotationInterval")
217                .and_then(JsValue::as_f64)
218                .filter(|value| *value != 0.0)
219                .map(duration)
220                .transpose()?;
221            plugin.config.grace_period = duration(
222                jwks.get("gracePeriod")
223                    .and_then(JsValue::as_f64)
224                    .unwrap_or(2_592_000.0),
225            )?;
226            if let Some(config) = jwks.get("keyPairConfig") {
227                plugin.config.key_pair = key_config(config)?;
228            }
229            if let Some(configs) = jwks.get("keyPairConfigs").and_then(JsValue::as_array) {
230                plugin.config.additional_key_pairs =
231                    configs.iter().map(key_config).collect::<AuthResult<_>>()?;
232            }
233        }
234        if overrides.get("adapter").is_some() {
235            plugin.config.keyring = None;
236        }
237        Ok(plugin)
238    }
239}
240
241fn key_config(value: &JsValue) -> AuthResult<JwtKeyPairConfig> {
242    Ok(JwtKeyPairConfig {
243        algorithm: super::JwtAlgorithm::from_str(
244            value
245                .get("alg")
246                .and_then(JsValue::as_str)
247                .unwrap_or("EdDSA"),
248        )?,
249        modulus_length: value
250            .get("modulusLength")
251            .and_then(JsValue::as_f64)
252            .map(|value| {
253                value
254                    .to_string()
255                    .parse::<usize>()
256                    .map_err(|error| AuthError::internal(error.to_string()))
257            })
258            .transpose()?,
259    })
260}
261
262fn duration(value: f64) -> AuthResult<Duration> {
263    let millis = (value * 1000.0)
264        .trunc()
265        .to_string()
266        .parse::<i64>()
267        .map_err(|error| AuthError::internal(error.to_string()))?;
268    Ok(Duration::milliseconds(millis))
269}
270
271fn time_seconds(value: &str) -> AuthResult<f64> {
272    let invalid = || {
273        AuthError::internal(format!(
274            "Invalid time string format: \"{value}\". Use formats like \"7d\", \"30m\", \"1 hour\", etc."
275        ))
276    };
277    let mut text = value;
278    let sign = if let Some(rest) = text.strip_prefix('-') {
279        text = rest;
280        Some(-1.0)
281    } else if let Some(rest) = text.strip_prefix('+') {
282        text = rest;
283        Some(1.0)
284    } else {
285        None
286    };
287    text = text.strip_prefix(' ').unwrap_or(text);
288    let count = text
289        .bytes()
290        .take_while(|byte| byte.is_ascii_digit() || *byte == b'.')
291        .count();
292    let number = text.get(..count).ok_or_else(invalid)?;
293    if number.is_empty()
294        || number.starts_with('.')
295        || number.ends_with('.')
296        || number.bytes().filter(|byte| *byte == b'.').count() > 1
297    {
298        return Err(invalid());
299    }
300    let mut unit = text
301        .get(count..)
302        .ok_or_else(invalid)?
303        .strip_prefix(' ')
304        .unwrap_or_else(|| &text[count..]);
305    let ago = unit.ends_with(" ago");
306    let from_now = unit.ends_with(" from now");
307    if (ago || from_now) && sign.is_some() {
308        return Err(invalid());
309    }
310    if ago {
311        unit = unit.strip_suffix(" ago").ok_or_else(invalid)?;
312    } else if from_now {
313        unit = unit.strip_suffix(" from now").ok_or_else(invalid)?;
314    }
315    let factor = match unit.to_ascii_lowercase().as_str() {
316        "second" | "seconds" | "sec" | "secs" | "s" => 1.0,
317        "minute" | "minutes" | "min" | "mins" | "m" => 60.0,
318        "hour" | "hours" | "hr" | "hrs" | "h" => 3600.0,
319        "day" | "days" | "d" => 86400.0,
320        "week" | "weeks" | "w" => 604_800.0,
321        "month" | "months" | "mo" => 2_592_000.0,
322        "year" | "years" | "yr" | "yrs" | "y" => 31_557_600.0,
323        _ => return Err(invalid()),
324    };
325    let seconds = number.parse::<f64>().map_err(|_| invalid())?
326        * factor
327        * sign.unwrap_or(if ago { -1.0 } else { 1.0 });
328    Ok((seconds + 0.5).floor())
329}