#[cfg(feature = "verify")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "verify")]
use std::collections::HashMap;
#[cfg(feature = "verify")]
use crate::Client;
#[cfg(feature = "verify")]
use crate::IdToken;
#[cfg(feature = "verify")]
use crate::ProjectId;
#[cfg(feature = "verify")]
pub type VerificationResult = std::result::Result<
crate::verification::IdTokenPayloadClaims,
VerificationError,
>;
#[cfg(feature = "verify")]
#[derive(Debug, thiserror::Error)]
pub enum VerificationError {
#[error("Decode ID token header failed: {0:?}")]
DecodeTokenHeaderFailed(jsonwebtoken::errors::Error),
#[error("Invalid type in ID token header: {0:?}")]
InvalidTokenType(Option<String>),
#[error("Invalid algorithm in ID token header: {0:?}")]
InvalidAlgorithm(jsonwebtoken::Algorithm),
#[error("No kid in the ID token header")]
KidNotFound,
#[error("HTTP request error to get public key from https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com: {0:?}")]
HttpRequestError(reqwest::Error),
#[error("Invalid response status code to get public key from https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com: {0:?}")]
InvalidResponseStatusCode(reqwest::StatusCode),
#[error("Deserialize response JSON to hash map failed to get public key from https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com")]
DeserializeResponseJsonFailed(reqwest::Error),
#[error("Target public key specified by kid not found in key map: {0:?}")]
PublicKeyNotFound(String),
#[error("Get decoding key failed: {0:?}")]
GetDecodingKeyFailed(jsonwebtoken::errors::Error),
#[error("Decode ID token failed: {0:?}")]
DecodeTokenFailed(jsonwebtoken::errors::Error),
#[error("The ID token is expired at {0:?}")]
TokenExpired(u64),
#[error("The ID token is issued in the future at {0:?}")]
TokenIssuedInTheFuture(u64),
}
#[cfg(feature = "verify")]
pub struct VerificationConfig {
client: Client,
project_id: ProjectId,
}
#[cfg(feature = "verify")]
impl VerificationConfig {
pub fn new(project_id: ProjectId) -> Self {
Self {
client: Client::new(),
project_id,
}
}
#[cfg(feature = "custom_client")]
pub fn custom(
client: Client,
project_id: ProjectId,
) -> Self {
Self {
client,
project_id,
}
}
pub async fn verify_id_token(
&self,
id_token: &IdToken,
) -> VerificationResult {
verify_id_token(
&self.client,
&id_token,
&self.project_id,
)
.await
}
}
#[cfg(feature = "verify")]
#[derive(Debug, Deserialize, Serialize)]
pub struct IdTokenPayloadClaims {
pub exp: u64,
pub iat: u64,
pub aud: String,
pub iss: String,
pub sub: String,
pub auth_time: u64,
}
#[cfg(feature = "verify")]
async fn verify_id_token(
client: &Client,
id_token: &IdToken,
project_id: &ProjectId,
) -> VerificationResult {
let header = jsonwebtoken::decode_header(&id_token.inner())
.map_err(VerificationError::DecodeTokenHeaderFailed)?;
if header.typ != Some("JWT".to_string()) {
return Err(VerificationError::InvalidTokenType(
header.typ,
));
}
if header.alg != jsonwebtoken::Algorithm::RS256 {
return Err(VerificationError::InvalidAlgorithm(
header.alg,
));
}
let kid = header
.kid
.ok_or(VerificationError::KidNotFound)?;
let response = client
.inner()
.get("https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com")
.send()
.await
.map_err(VerificationError::HttpRequestError)?;
if response.status() != reqwest::StatusCode::OK {
return Err(
VerificationError::InvalidResponseStatusCode(response.status()),
);
}
let key_map = response
.json::<HashMap<String, String>>()
.await
.map_err(|error| {
VerificationError::DeserializeResponseJsonFailed(error)
})?;
let key = key_map
.get(&kid)
.ok_or(VerificationError::PublicKeyNotFound(
kid,
))?;
let decoding_key = jsonwebtoken::DecodingKey::from_rsa_pem(key.as_bytes())
.map_err(VerificationError::GetDecodingKeyFailed)?;
let mut validation =
jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::RS256);
validation.set_audience(&[project_id.inner()]);
validation.set_issuer(&[format!(
"https://securetoken.google.com/{}",
project_id.inner()
)]);
validation.set_required_spec_claims(&[
"exp",
"iat",
"aud",
"iss",
"sub",
"auth_time",
]);
let decoded = jsonwebtoken::decode::<IdTokenPayloadClaims>(
&id_token.inner(),
&decoding_key,
&validation,
)
.map_err(VerificationError::DecodeTokenFailed)?;
let time_stamp = jsonwebtoken::get_current_timestamp();
if decoded.claims.exp < time_stamp {
return Err(VerificationError::TokenExpired(
decoded.claims.exp,
));
}
if decoded.claims.iat > time_stamp {
return Err(
VerificationError::TokenIssuedInTheFuture(decoded.claims.iat),
);
}
Ok(decoded.claims)
}