google sso - better error types, README with setup instructions (#1565)
This commit is contained in:
1 parent
d25b206cae
commit
06cc380d74
12 files changed
+150
-105
No files matched your search
Generated
-1
@@ -4865,7 +4865,6 @@ version = "0.0.1"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"coarsetime",
|
||||
"errors",
|
||||
"jwt-simple",
|
||||
"reqwest 0.12.23",
|
||||
"serde",
|
||||
|
||||
@@ -21,7 +21,6 @@ path = "src/lib.rs"
|
||||
workspace-build-acceleration.workspace = true
|
||||
|
||||
# Internal
|
||||
errors.workspace = true
|
||||
|
||||
# External
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Google Sign In
|
||||
|
||||
https://developers.google.com/identity/sign-in/web/sign-in
|
||||
|
||||
This has a link to the management console.
|
||||
|
||||
## Frontend Setup
|
||||
|
||||
```html
|
||||
<!-- Load Library -->
|
||||
<script src="https://apis.google.com/js/platform.js" async defer></script>
|
||||
|
||||
<!-- Specify Client ID-->
|
||||
<meta name="google-signin-client_id" content="YOUR_CLIENT_ID.apps.googleusercontent.com">
|
||||
```
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
use crate::certs::jwk_to_public_key::jwk_to_public_key;
|
||||
use crate::certs::key_map::KeyMap;
|
||||
use errors::{anyhow, AnyhowResult};
|
||||
use crate::error::google_sign_in_error::GoogleSignInError;
|
||||
|
||||
/// https://developers.google.com/identity/gsi/web/guides/verify-google-id-token
|
||||
/// "These keys are regularly rotated; examine the Cache-Control header in
|
||||
/// the response to determine when you should retrieve them again."
|
||||
const GOOGLE_CERTS_URL : &str = "https://www.googleapis.com/oauth2/v3/certs";
|
||||
|
||||
pub async fn download_cert_key_set() -> AnyhowResult<KeyMap> {
|
||||
pub async fn download_cert_key_set() -> Result<KeyMap, GoogleSignInError> {
|
||||
let certs = download_certs().await?;
|
||||
let key_map = jwk_to_public_key(&certs)?;
|
||||
Ok(key_map)
|
||||
}
|
||||
|
||||
pub async fn download_certs() -> AnyhowResult<String> {
|
||||
let response = reqwest::get(GOOGLE_CERTS_URL).await?;
|
||||
pub async fn download_certs() -> Result<String, GoogleSignInError> {
|
||||
let response = reqwest::get(GOOGLE_CERTS_URL)
|
||||
.await
|
||||
.map_err(GoogleSignInError::CertDownloadHttpError)?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(anyhow!("Failed to download Google certs: {:?}", response.status()));
|
||||
return Err(GoogleSignInError::CertDownloadBadStatus(response.status()));
|
||||
}
|
||||
|
||||
let body = response.text().await?;
|
||||
let body = response.text()
|
||||
.await
|
||||
.map_err(GoogleSignInError::CertDownloadHttpError)?;
|
||||
|
||||
Ok(body)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::certs::key_map::KeyMap;
|
||||
use errors::{anyhow, AnyhowResult};
|
||||
use crate::error::google_sign_in_error::GoogleSignInError;
|
||||
use jwt_simple::prelude::RS256PublicKey;
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
@@ -7,10 +7,11 @@ use serde_derive::Deserialize;
|
||||
/// Grab Google's keys from here: https://www.googleapis.com/oauth2/v3/certs
|
||||
/// These are updated frequently throughout the day and must be re-fetched.
|
||||
/// NB: This implementation is adapted from the crate `signin`.
|
||||
pub fn jwk_to_public_key(json_web_key: &str) -> AnyhowResult<KeyMap> {
|
||||
pub fn jwk_to_public_key(json_web_key: &str) -> Result<KeyMap, GoogleSignInError> {
|
||||
let mut key_map = KeyMap::new();
|
||||
|
||||
let json_keys: JsonKeys = serde_json::from_str(json_web_key)?;
|
||||
let json_keys: JsonKeys = serde_json::from_str(json_web_key)
|
||||
.map_err(GoogleSignInError::JwkJsonParseFailed)?;
|
||||
|
||||
for key in json_keys.keys {
|
||||
if key.use_ != "sig" {
|
||||
@@ -18,11 +19,14 @@ pub fn jwk_to_public_key(json_web_key: &str) -> AnyhowResult<KeyMap> {
|
||||
}
|
||||
match key.alg.as_ref() {
|
||||
"RS256" => {
|
||||
let n_decoded = base64_decode_url(&key.n)?;
|
||||
let e_decoded = base64_decode_url(&key.e)?;
|
||||
let n_decoded = base64_decode_url(&key.n)
|
||||
.map_err(GoogleSignInError::JwkBase64DecodeFailed)?;
|
||||
let e_decoded = base64_decode_url(&key.e)
|
||||
.map_err(GoogleSignInError::JwkBase64DecodeFailed)?;
|
||||
|
||||
let public_key = RS256PublicKey::from_components(&n_decoded, &e_decoded)?
|
||||
.with_key_id(&key.kid);
|
||||
let public_key = RS256PublicKey::from_components(&n_decoded, &e_decoded)
|
||||
.map_err(|err| GoogleSignInError::JwkRsaKeyConstructionFailed(format!("{}", err)))?
|
||||
.with_key_id(&key.kid);
|
||||
|
||||
key_map.insert(key.kid, public_key);
|
||||
}
|
||||
@@ -31,7 +35,7 @@ pub fn jwk_to_public_key(json_web_key: &str) -> AnyhowResult<KeyMap> {
|
||||
}
|
||||
|
||||
if key_map.is_empty() {
|
||||
return Err(anyhow!("No keys found in JWK"));
|
||||
return Err(GoogleSignInError::JwkNoKeysFound);
|
||||
}
|
||||
|
||||
Ok(key_map)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::claims::google_custom_claims::GoogleCustomClaims;
|
||||
use errors::{anyhow, AnyhowResult};
|
||||
use crate::error::google_sign_in_error::GoogleSignInError;
|
||||
use jwt_simple::claims::{Audiences, JWTClaims};
|
||||
|
||||
pub struct Claims {
|
||||
@@ -37,11 +37,11 @@ impl Claims {
|
||||
|
||||
/// Determine if the claim audience is as expected.
|
||||
/// This is necessary so third parties don't send claims signed on their behalf by Google.
|
||||
pub fn audience_matches(&self, audience: &str) -> AnyhowResult<bool> {
|
||||
pub fn audience_matches(&self, audience: &str) -> Result<bool, GoogleSignInError> {
|
||||
match self.claims.audiences.as_ref() {
|
||||
Some(Audiences::AsString(claim_audience)) => Ok(claim_audience.eq(audience)),
|
||||
Some(Audiences::AsSet(audiences)) => Ok(audiences.contains(audience)),
|
||||
_ => Err(anyhow!("Invalid audience type")),
|
||||
_ => Err(GoogleSignInError::AudienceMissing),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use crate::certs::key_map::KeyMap;
|
||||
use crate::claims::claims::Claims;
|
||||
use crate::claims::google_custom_claims::GoogleCustomClaims;
|
||||
use crate::errors::VerifyError;
|
||||
use crate::error::google_sign_in_error::GoogleSignInError;
|
||||
use crate::jwt::decode_jwt_header::decode_jwt_header;
|
||||
use errors::anyhow;
|
||||
use jwt_simple::algorithms::RS256PublicKey;
|
||||
use jwt_simple::algorithms::RSAPublicKeyLike;
|
||||
use jwt_simple::common::VerificationOptions;
|
||||
@@ -12,16 +11,15 @@ 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>) -> Result<Claims, VerifyError> {
|
||||
let header = decode_jwt_header(token)
|
||||
.map_err(|err| VerifyError::JwtDecodeError { source: err })?;
|
||||
pub fn decode_and_verify_token_claims(keys: &KeyMap, token: &str, mut options: Option<VerificationOptions>) -> Result<Claims, GoogleSignInError> {
|
||||
let header = decode_jwt_header(token)?;
|
||||
|
||||
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"))?;
|
||||
.ok_or(GoogleSignInError::JwtNoKeyId)?;
|
||||
|
||||
let key = keys.get(key_id)
|
||||
.ok_or_else(|| VerifyError::JwtKeyMissing { requested_key: key_id.to_string() })?;
|
||||
.ok_or_else(|| GoogleSignInError::JwtKeyMissing { requested_key: key_id.to_string() })?;
|
||||
|
||||
if options.is_none() {
|
||||
options = Some(VerificationOptions {
|
||||
@@ -36,7 +34,7 @@ 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>) -> Result<Claims, VerifyError> {
|
||||
pub fn decode_and_verify_token_claims_with_key(key: &RS256PublicKey, token: &str, options: Option<VerificationOptions>) -> Result<Claims, GoogleSignInError> {
|
||||
let result = key.verify_token::<GoogleCustomClaims>(token, options);
|
||||
|
||||
let claims = match result {
|
||||
@@ -44,22 +42,22 @@ pub fn decode_and_verify_token_claims_with_key(key: &RS256PublicKey, token: &str
|
||||
Err(err) => {
|
||||
if let Some(inner_error) = err.downcast_ref::<JWTError>() {
|
||||
match inner_error {
|
||||
JWTError::RequiredAudienceMismatch => return Err(VerifyError::JwtInvalidAudience),
|
||||
JWTError::RequiredAudienceMissing => return Err(VerifyError::JwtInvalidAudience),
|
||||
JWTError::RequiredIssuerMismatch => return Err(VerifyError::JwtInvalidIssuer),
|
||||
JWTError::RequiredIssuerMissing => return Err(VerifyError::JwtInvalidIssuer),
|
||||
JWTError::TokenHasExpired => return Err(VerifyError::JwtExpired),
|
||||
JWTError::RequiredAudienceMismatch => return Err(GoogleSignInError::JwtInvalidAudience),
|
||||
JWTError::RequiredAudienceMissing => return Err(GoogleSignInError::JwtInvalidAudience),
|
||||
JWTError::RequiredIssuerMismatch => return Err(GoogleSignInError::JwtInvalidIssuer),
|
||||
JWTError::RequiredIssuerMissing => return Err(GoogleSignInError::JwtInvalidIssuer),
|
||||
JWTError::TokenHasExpired => return Err(GoogleSignInError::JwtExpired),
|
||||
_ => {}, // Fall-through
|
||||
}
|
||||
}
|
||||
return Err(VerifyError::AnyhowError(err));
|
||||
return Err(GoogleSignInError::JwtVerifyFailed(format!("{}", err)));
|
||||
}
|
||||
};
|
||||
|
||||
match claims.issuer.as_deref() {
|
||||
Some("https://accounts.google.com" | "accounts.google.com") => {} // Permitted
|
||||
_ => {
|
||||
return Err(VerifyError::JwtInvalidIssuer);
|
||||
return Err(GoogleSignInError::JwtInvalidIssuer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +70,7 @@ pub fn decode_and_verify_token_claims_with_key(key: &RS256PublicKey, token: &str
|
||||
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;
|
||||
use crate::errors::VerifyError;
|
||||
use crate::error::google_sign_in_error::GoogleSignInError;
|
||||
use coarsetime::Duration;
|
||||
use jwt_simple::claims::Audiences;
|
||||
use jwt_simple::prelude::VerificationOptions;
|
||||
@@ -180,7 +178,7 @@ mod tests {
|
||||
};
|
||||
|
||||
match decode_and_verify_token_claims(&key_map, credential, Some(options)) {
|
||||
Err(VerifyError::JwtKeyMissing { requested_key}) => {
|
||||
Err(GoogleSignInError::JwtKeyMissing { requested_key}) => {
|
||||
assert_eq!(requested_key, "b2620d5e7f132b52afe8875cdf3776c064249d04");
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -201,7 +199,7 @@ mod tests {
|
||||
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) => {
|
||||
Err(GoogleSignInError::JwtExpired) => {
|
||||
// Expected error case
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -231,7 +229,7 @@ mod tests {
|
||||
};
|
||||
|
||||
match decode_and_verify_token_claims(&key_map, credential, Some(options)) {
|
||||
Err(VerifyError::JwtInvalidIssuer) => {
|
||||
Err(GoogleSignInError::JwtInvalidIssuer) => {
|
||||
// Expected error case
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -261,7 +259,7 @@ mod tests {
|
||||
};
|
||||
|
||||
match decode_and_verify_token_claims(&key_map, credential, Some(options)) {
|
||||
Err(VerifyError::JwtInvalidAudience) => {
|
||||
Err(GoogleSignInError::JwtInvalidAudience) => {
|
||||
// Expected error case
|
||||
}
|
||||
Err(err) => {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
/// All failure modes surfaced by the `google_sign_in` crate.
|
||||
///
|
||||
/// Kept as a single flat enum on purpose — the crate is small enough that
|
||||
/// splitting into client/api/specific tiers (like beeble_client / gmicloud_client
|
||||
/// do) is overkill here.
|
||||
#[derive(Debug)]
|
||||
pub enum GoogleSignInError {
|
||||
// ── Cert download (Google's JWK endpoint) ──
|
||||
|
||||
/// HTTP error talking to Google's JWK cert endpoint.
|
||||
CertDownloadHttpError(reqwest::Error),
|
||||
|
||||
/// Google's cert endpoint returned a non-2xx status.
|
||||
CertDownloadBadStatus(reqwest::StatusCode),
|
||||
|
||||
// ── JWK parse ──
|
||||
|
||||
/// Failed to parse Google's JWK payload as JSON.
|
||||
JwkJsonParseFailed(serde_json::Error),
|
||||
|
||||
/// Failed to base64-decode a JWK key component (`n` or `e`).
|
||||
JwkBase64DecodeFailed(base64::DecodeError),
|
||||
|
||||
/// Failed to construct an RSA public key from JWK components.
|
||||
/// The wrapped string is `jwt_simple`'s formatted error.
|
||||
JwkRsaKeyConstructionFailed(String),
|
||||
|
||||
/// JWK payload contained no usable signing keys.
|
||||
JwkNoKeysFound,
|
||||
|
||||
// ── JWT header decode ──
|
||||
|
||||
/// JWT was malformed — couldn't split off the header segment.
|
||||
JwtMalformed,
|
||||
|
||||
/// Failed to base64-decode the JWT header segment.
|
||||
/// The wrapped string is `ct_codecs`'s formatted error.
|
||||
JwtHeaderBase64DecodeFailed(String),
|
||||
|
||||
/// Failed to parse the JWT header as JSON.
|
||||
JwtHeaderJsonParseFailed(serde_json::Error),
|
||||
|
||||
// ── JWT claim verification ──
|
||||
|
||||
/// JWT carried no `kid` and the keymap is empty — no key to verify with.
|
||||
JwtNoKeyId,
|
||||
|
||||
/// JWT requested a `kid` not present in the JWK keymap.
|
||||
/// Refreshing the JWK keymap may resolve.
|
||||
JwtKeyMissing { requested_key: String },
|
||||
|
||||
/// JWT has expired (was issued before the current clock + skew tolerance).
|
||||
JwtExpired,
|
||||
|
||||
/// JWT audience claim is absent or doesn't match the configured allowlist.
|
||||
JwtInvalidAudience,
|
||||
|
||||
/// JWT issuer claim is absent or doesn't match the configured allowlist.
|
||||
JwtInvalidIssuer,
|
||||
|
||||
/// JWT signature verification failed for some other reason.
|
||||
/// The wrapped string is `jwt_simple`'s formatted error.
|
||||
JwtVerifyFailed(String),
|
||||
|
||||
// ── Claims helpers ──
|
||||
|
||||
/// `audience_matches()` was called on claims that have no audience field.
|
||||
AudienceMissing,
|
||||
}
|
||||
|
||||
impl Error for GoogleSignInError {}
|
||||
|
||||
impl Display for GoogleSignInError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod google_sign_in_error;
|
||||
@@ -1,59 +0,0 @@
|
||||
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 audience for the JWT claims was absent or unexpected.
|
||||
JwtInvalidAudience,
|
||||
|
||||
/// The issuer of the JWT claims was absent or unexpected.
|
||||
JwtInvalidIssuer,
|
||||
|
||||
/// 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 token has expired")
|
||||
}
|
||||
Self::JwtKeyMissing { requested_key } => {
|
||||
write!(f, "JWT requested key absent from JWK: {}", requested_key)
|
||||
}
|
||||
Self::JwtInvalidAudience => {
|
||||
write!(f, "Unexpected or missing JWT audience")
|
||||
}
|
||||
Self::JwtInvalidIssuer => {
|
||||
write!(f, "Unexpected or missing JWT issuer")
|
||||
}
|
||||
Self::AnyhowError(err) => {
|
||||
write!(f, "AnyhowError: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AnyhowError> for VerifyError {
|
||||
fn from(value: AnyhowError) -> Self {
|
||||
Self::AnyhowError(value)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use errors::{anyhow, AnyhowResult};
|
||||
use crate::error::google_sign_in_error::GoogleSignInError;
|
||||
use jwt_simple::prelude::Base64UrlSafeNoPadding;
|
||||
use jwt_simple::reexports::ct_codecs::Decoder;
|
||||
use serde_derive::Deserialize;
|
||||
@@ -10,13 +10,15 @@ pub(crate) struct JwtHeader {
|
||||
// Other fields absent
|
||||
}
|
||||
|
||||
pub fn decode_jwt_header(jwt: &str) -> AnyhowResult<JwtHeader> {
|
||||
pub fn decode_jwt_header(jwt: &str) -> Result<JwtHeader, GoogleSignInError> {
|
||||
let mut parts = jwt.split('.');
|
||||
let jwt_header_b64 = parts.next().ok_or(anyhow!("could not split jwt"))?;
|
||||
let jwt_header_b64 = parts.next().ok_or(GoogleSignInError::JwtMalformed)?;
|
||||
|
||||
let jwt_header: JwtHeader = serde_json::from_slice(
|
||||
&Base64UrlSafeNoPadding::decode_to_vec(jwt_header_b64, None)?,
|
||||
)?;
|
||||
let decoded = Base64UrlSafeNoPadding::decode_to_vec(jwt_header_b64, None)
|
||||
.map_err(|err| GoogleSignInError::JwtHeaderBase64DecodeFailed(format!("{}", err)))?;
|
||||
|
||||
let jwt_header: JwtHeader = serde_json::from_slice(&decoded)
|
||||
.map_err(GoogleSignInError::JwtHeaderJsonParseFailed)?;
|
||||
|
||||
Ok(jwt_header)
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
pub mod certs;
|
||||
pub mod claims;
|
||||
pub mod decode_and_verify_token_claims;
|
||||
pub mod errors;
|
||||
pub mod error;
|
||||
|
||||
pub(crate) mod jwt;
|
||||
|
||||
|
||||
Reference in new issue
Block a user