Skip to main content

rustauth_plugins/jwt/
mod.rs

1//! Server-side JWT and JWKS plugin.
2
3mod adapter;
4mod claims;
5mod crypto;
6mod endpoints;
7mod keys;
8mod options;
9mod schema;
10mod sign;
11mod verify;
12
13pub use claims::{to_exp_jwt, JwtClaims, TimeInput};
14pub use keys::{Jwk, JwkAlgorithm, Jwks};
15pub use options::{
16    JwtAdapterOptions, JwtCreateJwkHandler, JwtDefinePayloadHandler, JwtGetJwksHandler,
17    JwtGetSubjectHandler, JwtJwksOptions, JwtOptions, JwtOptionsBuilder, JwtSessionContext,
18    JwtSignHandler, JwtSigningOptions,
19};
20pub use schema::JwtSchemaOptions;
21pub use sign::sign_jwt;
22pub use verify::{verify_jwt, verify_jwt_with_options};
23
24use std::sync::Arc;
25
26use http::header;
27use rustauth_core::context::request_state::{current_session, current_session_user};
28use rustauth_core::error::RustAuthError;
29use rustauth_core::plugin::{AuthPlugin, PluginAfterHookAction};
30
31pub const UPSTREAM_PLUGIN_ID: &str = "jwt";
32
33pub fn jwt(options: JwtOptions) -> Result<AuthPlugin, RustAuthError> {
34    options.validate()?;
35    let options = Arc::new(options);
36    let mut plugin = AuthPlugin::new(UPSTREAM_PLUGIN_ID)
37        .with_version(crate::VERSION)
38        .with_state(Arc::clone(&options))
39        .with_schema(schema::jwks_schema(&options.schema))
40        .with_endpoint(endpoints::jwks_endpoint(Arc::clone(&options)))
41        .with_endpoint(endpoints::token_endpoint(Arc::clone(&options)))
42        .with_endpoint(endpoints::sign_jwt_endpoint(Arc::clone(&options)))
43        .with_endpoint(endpoints::verify_jwt_endpoint(Arc::clone(&options)));
44
45    if !options.disable_setting_jwt_header {
46        let options_for_hook = Arc::clone(&options);
47        plugin =
48            plugin.with_async_after_hook("/get-session", move |context, _request, response| {
49                let options = Arc::clone(&options_for_hook);
50                Box::pin(async move {
51                    let token = if let Some(current) = current_session()? {
52                        endpoints::sign_session_token(
53                            context,
54                            &options,
55                            current.session,
56                            current.user,
57                        )
58                        .await?
59                    } else {
60                        let Some(user) = current_session_user()? else {
61                            return Ok(PluginAfterHookAction::Continue(response));
62                        };
63                        let Some(user_id) = user
64                            .get("id")
65                            .and_then(|value| value.as_str())
66                            .map(str::to_owned)
67                        else {
68                            return Ok(PluginAfterHookAction::Continue(response));
69                        };
70                        let mut claims = match user {
71                            serde_json::Value::Object(map) => map,
72                            _ => JwtClaims::new(),
73                        };
74                        claims.insert("sub".to_owned(), serde_json::Value::String(user_id));
75                        sign::sign_jwt_with_options(context, claims, &options).await?
76                    };
77                    let mut response = response;
78                    let token = header::HeaderValue::from_str(&token)
79                        .map_err(|error| RustAuthError::Api(error.to_string()))?;
80                    response.headers_mut().insert("set-auth-jwt", token);
81                    expose_auth_jwt_header(response.headers_mut())?;
82                    Ok(PluginAfterHookAction::Continue(response))
83                })
84            });
85    }
86
87    Ok(plugin)
88}
89
90pub fn jwt_options_from_context(
91    context: &rustauth_core::context::AuthContext,
92) -> Option<Arc<JwtOptions>> {
93    context
94        .plugin(UPSTREAM_PLUGIN_ID)
95        .and_then(|plugin| plugin.state::<JwtOptions>())
96}
97
98fn expose_auth_jwt_header(headers: &mut header::HeaderMap) -> Result<(), RustAuthError> {
99    let current = headers
100        .get(header::ACCESS_CONTROL_EXPOSE_HEADERS)
101        .and_then(|value| value.to_str().ok())
102        .unwrap_or_default();
103    let mut values = current
104        .split(',')
105        .map(str::trim)
106        .filter(|value| !value.is_empty())
107        .map(str::to_owned)
108        .collect::<Vec<_>>();
109    if !values
110        .iter()
111        .any(|value| value.eq_ignore_ascii_case("set-auth-jwt"))
112    {
113        values.push("set-auth-jwt".to_owned());
114    }
115    let value = header::HeaderValue::from_str(&values.join(", "))
116        .map_err(|error| RustAuthError::Api(error.to_string()))?;
117    headers.insert(header::ACCESS_CONTROL_EXPOSE_HEADERS, value);
118    Ok(())
119}