mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-02 16:38:14 +00:00
make jwt handling more errorful
This commit is contained in:
1 parent
d728b678c8
commit
a1a4e7e9df
7 files changed
+152
-34
No files matched your search
File renamed without changes.
@@ -1,2 +1,3 @@
|
||||
pub mod download_certs;
|
||||
pub mod jwk_to_public_key;
|
||||
pub mod key_map;
|
||||
@@ -1,24 +1,27 @@
|
||||
use std::collections::HashSet;
|
||||
use crate::certs::key_map::KeyMap;
|
||||
use crate::claims::claims::Claims;
|
||||
use crate::claims::google_custom_claims::GoogleCustomClaims;
|
||||
use crate::errors::VerifyError;
|
||||
use crate::jwt::decode_jwt_header::decode_jwt_header;
|
||||
use errors::{anyhow, AnyhowResult};
|
||||
use errors::anyhow;
|
||||
use jwt_simple::algorithms::RS256PublicKey;
|
||||
use jwt_simple::algorithms::RSAPublicKeyLike;
|
||||
use jwt_simple::common::VerificationOptions;
|
||||
use jwt_simple::JWTError;
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Decode a Google Sign In JWT.
|
||||
/// Verification options can be supplied to increase clock skew tolerance, etc.
|
||||
pub fn decode_and_verify_token_claims(keys: &KeyMap, token: &str, mut options: Option<VerificationOptions>) -> AnyhowResult<Claims> {
|
||||
let header = decode_jwt_header(token)?;
|
||||
pub fn decode_and_verify_token_claims(keys: &KeyMap, token: &str, mut options: Option<VerificationOptions>) -> Result<Claims, VerifyError> {
|
||||
let header = decode_jwt_header(token)
|
||||
.map_err(|err| VerifyError::JwtDecodeError { source: err })?;
|
||||
|
||||
let key_id = header.kid.as_deref()
|
||||
.or_else(|| keys.keys().next().map(|k|k.as_str()))
|
||||
.ok_or_else(|| anyhow!("No key ID found"))?;
|
||||
|
||||
let key = keys.get(key_id)
|
||||
.ok_or_else(|| anyhow!("Key not found"))?;
|
||||
.ok_or_else(|| VerifyError::JwtKeyMissing { requested_key: key_id.to_string() })?;
|
||||
|
||||
if options.is_none() {
|
||||
options = Some(VerificationOptions {
|
||||
@@ -33,13 +36,26 @@ pub fn decode_and_verify_token_claims(keys: &KeyMap, token: &str, mut options: O
|
||||
decode_and_verify_token_claims_with_key(key, token, options)
|
||||
}
|
||||
|
||||
pub fn decode_and_verify_token_claims_with_key(key: &RS256PublicKey, token: &str, options: Option<VerificationOptions>) -> AnyhowResult<Claims> {
|
||||
let claims = key.verify_token::<GoogleCustomClaims>(token, options)?;
|
||||
pub fn decode_and_verify_token_claims_with_key(key: &RS256PublicKey, token: &str, options: Option<VerificationOptions>) -> Result<Claims, VerifyError> {
|
||||
let result = key.verify_token::<GoogleCustomClaims>(token, options);
|
||||
|
||||
let claims = match result {
|
||||
Ok(claims) => claims,
|
||||
Err(err) => {
|
||||
if let Some(inner_error) = err.downcast_ref::<JWTError>() {
|
||||
match inner_error {
|
||||
JWTError::TokenHasExpired => return Err(VerifyError::JwtExpired),
|
||||
_ => {}, // Fall-through
|
||||
}
|
||||
}
|
||||
return Err(VerifyError::AnyhowError(err));
|
||||
}
|
||||
};
|
||||
|
||||
match claims.issuer.as_deref() {
|
||||
Some("https://accounts.google.com" | "accounts.google.com") => {} // Permitted
|
||||
_ => {
|
||||
return Err(anyhow!("Invalid issuer: {:?}", claims.issuer));
|
||||
return Err(VerifyError::InvalidIssuer { issuer: claims.issuer });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,35 +67,34 @@ pub fn decode_and_verify_token_claims_with_key(key: &RS256PublicKey, token: &str
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::certs::jwk_to_public_key::jwk_to_public_key;
|
||||
use crate::decode_and_verify_token_claims::{decode_and_verify_token_claims, decode_and_verify_token_claims_with_key};
|
||||
use crate::decode_and_verify_token_claims::decode_and_verify_token_claims;
|
||||
use crate::errors::VerifyError;
|
||||
use coarsetime::Duration;
|
||||
use jwt_simple::claims::Audiences;
|
||||
use jwt_simple::prelude::VerificationOptions;
|
||||
use std::fs::read_to_string;
|
||||
use testing::test_file_path::test_file_path;
|
||||
|
||||
/*
|
||||
Example payload:
|
||||
iss https://accounts.google.com
|
||||
azp 788843034237-uqcg8tbgofrcf1to37e1bqphd924jaf6.apps.googleusercontent.com
|
||||
aud 788843034237-uqcg8tbgofrcf1to37e1bqphd924jaf6.apps.googleusercontent.com
|
||||
sub 113101967612396793777
|
||||
email vocodes2020@gmail.com
|
||||
email_verified true
|
||||
nbf 1726786100
|
||||
name Vocodes Vocodes
|
||||
picture https://lh3.googleusercontent.com/a/ACg8ocLz2-2OaAm0MQxR6j8CNr-Po8_Xr-aryATiCn4c0i_TuDmL_g=s96-c
|
||||
given_name Vocodes
|
||||
family_name Vocodes
|
||||
iat 1726786400
|
||||
exp 1726790000
|
||||
jti 4d44eeac06ce79fc0ab2270cfeea30d8acf77613
|
||||
*/
|
||||
Example payload:
|
||||
iss https://accounts.google.com
|
||||
azp 788843034237-uqcg8tbgofrcf1to37e1bqphd924jaf6.apps.googleusercontent.com
|
||||
aud 788843034237-uqcg8tbgofrcf1to37e1bqphd924jaf6.apps.googleusercontent.com
|
||||
sub 113101967612396793777
|
||||
email vocodes2020@gmail.com
|
||||
email_verified true
|
||||
nbf 1726786100
|
||||
name Vocodes Vocodes
|
||||
picture https://lh3.googleusercontent.com/a/ACg8ocLz2-2OaAm0MQxR6j8CNr-Po8_Xr-aryATiCn4c0i_TuDmL_g=s96-c
|
||||
given_name Vocodes
|
||||
family_name Vocodes
|
||||
iat 1726786400
|
||||
exp 1726790000
|
||||
jti 4d44eeac06ce79fc0ab2270cfeea30d8acf77613
|
||||
*/
|
||||
|
||||
#[test]
|
||||
fn test_decode() {
|
||||
let jwk_file = test_file_path("test_data/crypto/google_sign_in.2024-09-20.jwk").unwrap();
|
||||
//let jwk_file = test_file_path("test_data/crypto/google_sign_in.2024-04-01.jwk").unwrap();
|
||||
let jwk_payload = read_to_string(jwk_file).unwrap();
|
||||
let key_map = jwk_to_public_key(&jwk_payload).unwrap();
|
||||
|
||||
@@ -129,4 +144,53 @@ mod tests {
|
||||
Audiences::AsSet(_audiences) => panic!("Expected a single audience"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_with_invalid_keys() {
|
||||
let jwk_file = test_file_path("test_data/crypto/google_sign_in.2024-04-01.jwk").unwrap();
|
||||
let jwk_payload = read_to_string(jwk_file).unwrap();
|
||||
let key_map = jwk_to_public_key(&jwk_payload).unwrap();
|
||||
|
||||
let credential = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImIyNjIwZDVlN2YxMzJiNTJhZmU4ODc1Y2RmMzc3NmMwNjQyNDlkMDQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI3ODg4NDMwMzQyMzctdXFjZzh0YmdvZnJjZjF0bzM3ZTFicXBoZDkyNGphZjYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI3ODg4NDMwMzQyMzctdXFjZzh0YmdvZnJjZjF0bzM3ZTFicXBoZDkyNGphZjYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTMxMDE5Njc2MTIzOTY3OTM3NzciLCJlbWFpbCI6InZvY29kZXMyMDIwQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJuYmYiOjE3MjY3ODYxMDAsIm5hbWUiOiJWb2NvZGVzIFZvY29kZXMiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EvQUNnOG9jTHoyLTJPYUFtME1ReFI2ajhDTnItUG84X1hyLWFyeUFUaUNuNGMwaV9UdURtTF9nPXM5Ni1jIiwiZ2l2ZW5fbmFtZSI6IlZvY29kZXMiLCJmYW1pbHlfbmFtZSI6IlZvY29kZXMiLCJpYXQiOjE3MjY3ODY0MDAsImV4cCI6MTcyNjc5MDAwMCwianRpIjoiNGQ0NGVlYWMwNmNlNzlmYzBhYjIyNzBjZmVlYTMwZDhhY2Y3NzYxMyJ9.EYg71yIkvhxFGc8ZVCXeTOAmPAtLYDphHnkdf1sh8b_Jz4Y7S1DpmiTqQ1ytxu7J1xNixvdwhuIDzSlCvlxaFl8475GvAlyPTNtZtmWbFD5SRM_XHLOynijOp8WQ4nej-CHvT1KjjqMfkZ1EeQMoWk1H72PxPg_RiUgzsklkUs1wOkLAySk7R3EIAl7bIzpoY_WH2pxv9ccFpBtKDHaDqHkxAWBUQX0-G7ZXZBPVz07V28ZfdbzFDapjZaUFbumazh_-J2-9AA6JkcteF4h_gpbBcLYAuxt5bWI5FECWbYe42khwb93WJ5SK12Tt0EPoyzIObJs14NWGAajtHTg3wA";
|
||||
|
||||
let options = VerificationOptions {
|
||||
time_tolerance: Some(Duration::from_days(365 * 30)),
|
||||
reject_before: None,
|
||||
accept_future: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
match decode_and_verify_token_claims(&key_map, credential, Some(options)) {
|
||||
Err(VerifyError::JwtKeyMissing { requested_key}) => {
|
||||
assert_eq!(requested_key, "b2620d5e7f132b52afe8875cdf3776c064249d04");
|
||||
}
|
||||
Err(err) => {
|
||||
panic!("Unexpected error: {:?}", err);
|
||||
}
|
||||
Ok(value) => {
|
||||
panic!("Unexpected success: {:?}", value.claims);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_with_expired_token() {
|
||||
let jwk_file = test_file_path("test_data/crypto/google_sign_in.2024-09-20.jwk").unwrap();
|
||||
let jwk_payload = read_to_string(jwk_file).unwrap();
|
||||
let key_map = jwk_to_public_key(&jwk_payload).unwrap();
|
||||
|
||||
let credential = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImIyNjIwZDVlN2YxMzJiNTJhZmU4ODc1Y2RmMzc3NmMwNjQyNDlkMDQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI3ODg4NDMwMzQyMzctdXFjZzh0YmdvZnJjZjF0bzM3ZTFicXBoZDkyNGphZjYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI3ODg4NDMwMzQyMzctdXFjZzh0YmdvZnJjZjF0bzM3ZTFicXBoZDkyNGphZjYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTMxMDE5Njc2MTIzOTY3OTM3NzciLCJlbWFpbCI6InZvY29kZXMyMDIwQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJuYmYiOjE3MjY3ODYxMDAsIm5hbWUiOiJWb2NvZGVzIFZvY29kZXMiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EvQUNnOG9jTHoyLTJPYUFtME1ReFI2ajhDTnItUG84X1hyLWFyeUFUaUNuNGMwaV9UdURtTF9nPXM5Ni1jIiwiZ2l2ZW5fbmFtZSI6IlZvY29kZXMiLCJmYW1pbHlfbmFtZSI6IlZvY29kZXMiLCJpYXQiOjE3MjY3ODY0MDAsImV4cCI6MTcyNjc5MDAwMCwianRpIjoiNGQ0NGVlYWMwNmNlNzlmYzBhYjIyNzBjZmVlYTMwZDhhY2Y3NzYxMyJ9.EYg71yIkvhxFGc8ZVCXeTOAmPAtLYDphHnkdf1sh8b_Jz4Y7S1DpmiTqQ1ytxu7J1xNixvdwhuIDzSlCvlxaFl8475GvAlyPTNtZtmWbFD5SRM_XHLOynijOp8WQ4nej-CHvT1KjjqMfkZ1EeQMoWk1H72PxPg_RiUgzsklkUs1wOkLAySk7R3EIAl7bIzpoY_WH2pxv9ccFpBtKDHaDqHkxAWBUQX0-G7ZXZBPVz07V28ZfdbzFDapjZaUFbumazh_-J2-9AA6JkcteF4h_gpbBcLYAuxt5bWI5FECWbYe42khwb93WJ5SK12Tt0EPoyzIObJs14NWGAajtHTg3wA";
|
||||
|
||||
match decode_and_verify_token_claims(&key_map, credential, None) {
|
||||
Err(VerifyError::JwtExpired) => {
|
||||
// Expected error case
|
||||
}
|
||||
Err(err) => {
|
||||
panic!("Unexpected error: {:?}", err);
|
||||
}
|
||||
Ok(value) => {
|
||||
panic!("Unexpected success: {:?}", value.claims);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use errors::AnyhowError;
|
||||
use std::error::Error;
|
||||
use std::fmt::Display;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum VerifyError {
|
||||
/// Could not decode a JWT token.
|
||||
JwtDecodeError { source: AnyhowError },
|
||||
|
||||
/// The JWT has expired (was issued before the current
|
||||
/// clock + deadline + skew tolerance)
|
||||
JwtExpired,
|
||||
|
||||
/// The key requested by the JWT was not available.
|
||||
/// This likely means we need to refresh our JWK key set.
|
||||
JwtKeyMissing { requested_key: String },
|
||||
|
||||
/// The issuer of the claims was unexpected
|
||||
InvalidIssuer { issuer: Option<String> },
|
||||
|
||||
/// Any other error type we haven't wrapped.
|
||||
AnyhowError(AnyhowError),
|
||||
}
|
||||
|
||||
impl Error for VerifyError {}
|
||||
|
||||
impl Display for VerifyError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::JwtDecodeError { source } => {
|
||||
write!(f, "JWT decode error: {}", source)
|
||||
}
|
||||
Self::JwtExpired => {
|
||||
write!(f, "JWT expired")
|
||||
}
|
||||
Self::JwtKeyMissing { requested_key } => {
|
||||
write!(f, "JWT key absent: {}", requested_key)
|
||||
}
|
||||
Self::InvalidIssuer { issuer } => {
|
||||
write!(f, "Invalid issuer: {:?}", issuer)
|
||||
}
|
||||
Self::AnyhowError(err) => {
|
||||
write!(f, "AnyhowError: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AnyhowError> for VerifyError {
|
||||
fn from(value: AnyhowError) -> Self {
|
||||
Self::AnyhowError(value)
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
pub mod certs;
|
||||
pub mod claims;
|
||||
pub mod decode_and_verify_token_claims;
|
||||
pub mod download_certs;
|
||||
pub mod errors;
|
||||
|
||||
pub(crate) mod jwt;
|
||||
|
||||
|
||||
+5
-5
@@ -7,25 +7,25 @@ use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
use actix_helpers::extractors::get_request_origin_uri::get_request_origin_uri;
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use log::{info, warn};
|
||||
use sqlx::MySqlPool;
|
||||
use utoipa::ToSchema;
|
||||
use actix_helpers::extractors::get_request_origin_uri::get_request_origin_uri;
|
||||
use errors::AnyhowResult;
|
||||
use google_sign_in::certs::download_certs::download_cert_key_set;
|
||||
use google_sign_in::decode_and_verify_token_claims::decode_and_verify_token_claims;
|
||||
use google_sign_in::download_certs::download_cert_key_set;
|
||||
use google_sign_in::VerificationOptions;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
|
||||
use log::{info, warn};
|
||||
use mysql_queries::mediators::firehose_publisher::FirehosePublisher;
|
||||
use mysql_queries::queries::users::user::create_account::{create_account, CreateAccountArgs, CreateAccountError};
|
||||
use mysql_queries::queries::users::user_sessions::create_user_session::create_user_session;
|
||||
use password::bcrypt_hash_password::bcrypt_hash_password;
|
||||
use sqlx::MySqlPool;
|
||||
use tokens::tokens::user_sessions::UserSessionToken;
|
||||
use user_input_common::check_for_slurs::contains_slurs;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::http_server::session::http::http_user_session_manager::HttpUserSessionManager;
|
||||
use crate::http_server::validations::is_reserved_username::is_reserved_username;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::http_server::endpoints::users::create_account_from_google_sign_in_handler::GoogleCreateAccountErrorResponse;
|
||||
use errors::AnyhowResult;
|
||||
use google_sign_in::certs::download_certs::download_cert_key_set;
|
||||
use google_sign_in::certs::key_map::KeyMap;
|
||||
use google_sign_in::download_certs::download_cert_key_set;
|
||||
use log::{error, info, warn};
|
||||
use std::sync::{Arc, LockResult, RwLock};
|
||||
use anyhow::anyhow;
|
||||
|
||||
Reference in new issue
Block a user