make jwt handling more errorful

This commit is contained in:
Brandon Thomas committed 2024-09-22 16:31:14 -04:00
1 parent d728b678c8
commit a1a4e7e9df
7 files changed
+152 -34

No files matched your search

@@ -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;
@@ -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;