1use super::JwtAudience;
2use super::JwtClaimsConfig;
3use super::JwtPlugin;
4use super::JwtSignOptions;
5use super::RemoteJwtPayload;
6use super::ResolvedJwtSigningKey;
7use super::claims::normalize_signing_claims;
8use super::claims::validate_critical_header;
9use super::claims::validate_numeric_date;
10use super::crypto;
11use alibi_core::AuthContext;
12use alibi_core::AuthError;
13use alibi_core::AuthRequest;
14use alibi_core::AuthResult;
15use alibi_core::AuthSchema;
16use base64::Engine;
17use base64::engine::general_purpose::URL_SAFE_NO_PAD;
18use serde_json::Map;
19use serde_json::Value;
20use serde_json::json;
21impl JwtPlugin {
22 pub async fn sign_jwt(
24 &self,
25 payload: Map<String, Value>,
26 options: &JwtSignOptions,
27 request: Option<&AuthRequest>,
28 ctx: &AuthContext<impl AuthSchema>,
29 ) -> AuthResult<String> {
30 if self.config.remote_signer.is_some() {
31 return self
32 .sign_remote_jwt(Value::Object(payload).into(), options, ctx)
33 .await;
34 }
35 self.sign_jwt_checked(payload, options, request, ctx, Ok(()))
36 .await
37 }
38
39 pub async fn sign_jwt_json(
47 &self,
48 payload: &alibi_core::utils::json::JsValue,
49 options: &JwtSignOptions,
50 request: Option<&AuthRequest>,
51 ctx: &AuthContext<impl AuthSchema>,
52 ) -> AuthResult<String> {
53 let object = payload
54 .as_object()
55 .ok_or_else(|| AuthError::bad_request("JWT payload must be an object"))?;
56 if self.config.remote_signer.is_some() {
57 return self.sign_remote_jwt(payload.clone(), options, ctx).await;
58 }
59 let validation = (|| {
60 for field in ["exp", "iat", "nbf"] {
61 validate_numeric_date(
62 field,
63 object
64 .get(field)
65 .and_then(alibi_core::utils::json::JsValue::as_f64),
66 )?;
67 }
68 for field in ["iss", "sub", "jti"] {
69 if object
70 .get(field)
71 .and_then(alibi_core::utils::json::JsValue::as_f64)
72 .is_some_and(|number| {
73 !number.is_finite() && (field == "iss" || !number.is_nan())
74 })
75 {
76 return Err(AuthError::internal(format!(
77 "\"{field}\" claim must be a string"
78 )));
79 }
80 }
81 Ok(())
82 })();
83 let payload = payload
84 .to_json_value()?
85 .as_object()
86 .cloned()
87 .ok_or_else(|| AuthError::bad_request("JWT payload must be an object"))?;
88 self.sign_jwt_checked(payload, options, request, ctx, validation)
89 .await
90 }
91
92 pub(in crate::jwt) async fn sign_jwt_checked(
93 &self,
94 payload: Map<String, Value>,
95 options: &JwtSignOptions,
96 request: Option<&AuthRequest>,
97 ctx: &AuthContext<impl AuthSchema>,
98 validation: AuthResult<()>,
99 ) -> AuthResult<String> {
100 let payload = self.default_claims(payload, options.claims.as_ref(), ctx)?;
101 let resolved;
102 let key = if let Some(key) = options.resolved_key.as_deref() {
103 key
104 } else {
105 resolved = self
106 .resolve_signing_key(options, request, ctx)
107 .await?
108 .ok_or_else(|| AuthError::internal("No local JWT signing key"))?;
109 &resolved
110 };
111 validation?;
113 Self::sign_resolved(payload, options, key)
114 }
115
116 pub(in crate::jwt) async fn sign_remote_jwt(
117 &self,
118 mut raw_claims: alibi_core::utils::json::JsValue,
119 options: &JwtSignOptions,
120 ctx: &AuthContext<impl AuthSchema>,
121 ) -> AuthResult<String> {
122 use alibi_core::utils::json::JsValue;
123 let JsValue::Object(payload) = &mut raw_claims else {
124 return Err(AuthError::bad_request("JWT payload must be an object"));
125 };
126 let config = options.claims.as_ref().unwrap_or(&self.config.claims);
127 let mut own_keys: Vec<String> = payload.keys().cloned().collect();
128 own_keys.sort_by_key(|key| {
130 key.parse::<u32>()
131 .ok()
132 .filter(|index| *index != u32::MAX && index.to_string() == *key)
133 .map_or(u64::MAX, u64::from)
134 });
135 let mut undefined_claims = Vec::new();
136 for name in ["iat", "exp", "nbf", "iss", "aud"] {
139 if !payload.contains_key(name) {
140 own_keys.push(name.to_owned());
141 if name == "iat" || name == "nbf" {
142 undefined_claims.push(name.to_owned());
143 }
144 }
145 if payload.get(name).is_none_or(JsValue::is_null) {
146 let value = match name {
147 "exp" => config.expiration.timestamp_raw(payload.get("iat")),
148 "iss" => JsValue::String(
149 config
150 .issuer
151 .clone()
152 .unwrap_or_else(|| ctx.config.base_url.clone()),
153 ),
154 "aud" => serde_json::to_value(
155 config
156 .audience
157 .clone()
158 .unwrap_or_else(|| JwtAudience::One(ctx.config.base_url.clone())),
159 )?
160 .into(),
161 _ => continue,
162 };
163 _ = payload.insert(name.to_owned(), value);
164 }
165 }
166 let payload = RemoteJwtPayload {
167 raw_claims,
168 own_keys,
169 undefined_claims,
170 };
171 let remote = self
172 .config
173 .remote_signer
174 .as_ref()
175 .ok_or_else(|| AuthError::internal("No remote JWT signer"))?;
176 remote
177 .sign(&payload, options)
178 .await
179 .map_err(|error| match error {
180 AuthError::Internal(_) => AuthError::CallbackFailure(Box::new(error)),
181 error => error,
182 })
183 }
184
185 pub(in crate::jwt) fn default_claims(
186 &self,
187 mut payload: Map<String, Value>,
188 override_claims: Option<&JwtClaimsConfig>,
189 ctx: &AuthContext<impl AuthSchema>,
190 ) -> AuthResult<Map<String, Value>> {
191 let config = override_claims.unwrap_or(&self.config.claims);
192 if payload.get("exp").is_none_or(Value::is_null) {
193 let expiration = config.expiration.timestamp(payload.get("iat"));
194 _ = payload.insert("exp".to_owned(), expiration);
195 }
196 if payload.get("iss").is_none_or(Value::is_null) {
197 _ = payload.insert(
198 "iss".to_owned(),
199 json!(config.issuer.as_deref().unwrap_or(&ctx.config.base_url)),
200 );
201 }
202 if payload.get("aud").is_none_or(Value::is_null) {
203 _ = payload.insert(
204 "aud".to_owned(),
205 serde_json::to_value(
206 config
207 .audience
208 .clone()
209 .unwrap_or_else(|| JwtAudience::One(ctx.config.base_url.clone())),
210 )?,
211 );
212 }
213 Ok(payload)
214 }
215
216 pub(in crate::jwt) fn sign_resolved(
217 mut payload: Map<String, Value>,
218 options: &JwtSignOptions,
219 key: &ResolvedJwtSigningKey,
220 ) -> AuthResult<String> {
221 let mut header = options.header.clone().unwrap_or_default();
222 _ = header.insert("alg".to_owned(), json!(key.algorithm.as_str()));
223 _ = header.insert("kid".to_owned(), json!(key.key_id));
224 validate_critical_header(&header, true)?;
225 normalize_signing_claims(&mut payload)?;
226 let input = format!(
227 "{}.{}",
228 URL_SAFE_NO_PAD.encode(alibi_core::utils::json::to_vec(&header)?),
229 URL_SAFE_NO_PAD.encode(alibi_core::utils::json::to_vec(&payload)?)
230 );
231 let signature = crypto::sign(key.algorithm, &key.private_key, input.as_bytes())?;
232 Ok(format!("{input}.{}", URL_SAFE_NO_PAD.encode(signature)))
233 }
234}