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