Repository: apollographql/router
Pull request #6930: Update JWKS handling
Author: login: Velfi
Created: 2025-03-05T18:57:06Z | Merged: 2025-03-18T14:45:55Z

This PR updates JWT-handling in the `AuthenticationPlugin`;

- JWT-related code has been moved around a bit for organizational purposes
- Users may now set a new a new config option `config.authentication.router.jwt.on_error`. When set to the default `Error`, JWT-related errors will be returned to users (the current behavior.) When set to `Continue`, JWT errors will instead be ignored. When a JWT error is encountered, JWT claims will not be set in the request context.
- When JWTs are processed, whether processing fails or succeeds, request context will contain a new variable `apollo::authentication::jwt_status` which notes the result of processing.
- I applied a few lints that RustRover highlighted.

Fixes #6180
<!-- start metadata -->
<!--ROUTER-1119-->
---

**Checklist**

Complete the checklist (and note appropriate exceptions) before the PR is marked ready-for-review.

- [ ] Changes are compatible[^1]
- [ ] Documentation[^2] completed
- [ ] Performance impact assessed and acceptable
- Tests added and passing[^3]
    - [ ] Unit Tests
    - [ ] Integration Tests
    - [ ] Manual Tests

**Exceptions**

*Note any exceptions here*

**Notes**

[^1]: It may be appropriate to bring upcoming changes to the attention of other (impacted) groups. Please endeavour to do this before seeking PR approval. The mechanism for doing this will vary considerably, so use your judgement as to how and when to do this.
[^2]: Configuration is an important part of many changes. Where applicable please try to document configuration examples.
[^3]: Tick whichever testing boxes are applicable. If you are adding Manual Tests, please document the manual testing (extensively) in the Exceptions.


Linked issue #6180: Support JWT authorization without failing request upon validation failure
## Is your feature request related to a problem? Please describe.
Today, if a JWT is present but validation of the JWT fails, the router rejects the request.

We require when a JWT is present, that it be validated, but if that fails, allow the request to continue, but obviously the context `apollo_authentication::JWT::claims` should not be populated.

## Describe the solution you'd like
A router configuration setting, allowing the the processing to continue even if JWT validation fails.

Perhaps a context value `apollo_authentication::JWT::status` to be provided to allow rhai/coprocessor to differentiate when no JWT was provided versus when a JWT could not be validated.

## Describe alternatives you've considered
Having to write our own JWT processing code and not be able to leverage Router's built-in support

## Additional context
Add any other context or screenshots about the feature request here.


Diff:
diff --git a/.changesets/feat_zhessler_make_jwt_auth_failure_configurable.md b/.changesets/feat_zhessler_make_jwt_auth_failure_configurable.md
new file mode 100644
index 0000000000..07cc4e0ed5
--- /dev/null
+++ b/.changesets/feat_zhessler_make_jwt_auth_failure_configurable.md
@@ -0,0 +1,10 @@
+### Update JWKS handling ([PR #6930](https://github.com/apollographql/router/pull/6930))
+
+This PR updates JWT-handling in the `AuthenticationPlugin`;
+
+- JWT-related code has been moved around a bit for organizational purposes
+- Users may now set a new a new config option `config.authentication.router.jwt.on_error`. When set to the default `Error`, JWT-related errors will be returned to users (the current behavior.) When set to `Continue`, JWT errors will instead be ignored. When a JWT error is encountered, JWT claims will not be set in the request context.
+- When JWTs are processed, whether processing fails or succeeds, request context will contain a new variable `apollo::authentication::jwt_status` which notes the result of processing.
+- I applied a few lints that RustRover highlighted.
+
+By [@Velfi](https://github.com/Velfi) in https://github.com/apollographql/router/pull/6930
\ No newline at end of file
diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap
index 4beb5f44be..80478a2b8f 100644
--- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap
+++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap
@@ -1,5 +1,6 @@
 ---
 source: apollo-router/src/configuration/tests.rs
+assertion_line: 26
 expression: "&schema"
 ---
 {
@@ -4012,6 +4013,10 @@ expression: "&schema"
           },
           "type": "array"
         },
+        "on_error": {
+          "$ref": "#/definitions/OnError",
+          "description": "#/definitions/OnError"
+        },
         "sources": {
           "description": "Alternative sources to extract the JWT",
           "items": {
@@ -4417,6 +4422,13 @@ expression: "&schema"
         }
       ]
     },
+    "OnError": {
+      "enum": [
+        "Continue",
+        "Error"
+      ],
+      "type": "string"
+    },
     "Operation": {
       "oneOf": [
         {
diff --git a/apollo-router/src/plugins/authentication/error.rs b/apollo-router/src/plugins/authentication/error.rs
new file mode 100644
index 0000000000..8997b53eda
--- /dev/null
+++ b/apollo-router/src/plugins/authentication/error.rs
@@ -0,0 +1,123 @@
+use displaydoc::Display;
+use jsonwebtoken::Algorithm;
+use jsonwebtoken::errors::Error as JWTError;
+use jsonwebtoken::errors::ErrorKind;
+use jsonwebtoken::jwk::KeyAlgorithm;
+use serde::Deserialize;
+use serde::Serialize;
+use thiserror::Error;
+use tower::BoxError;
+
+#[derive(Debug, Display, Error)]
+pub(crate) enum AuthenticationError {
+    /// Configured header is not convertible to a string
+    CannotConvertToString,
+
+    /// Header Value: '{0}' is not correctly formatted. prefix should be '{1}'
+    InvalidPrefix(String, String),
+
+    /// Header Value: '{0}' is not correctly formatted. Missing JWT
+    MissingJWT(String),
+
+    /// '{0}' is not a valid JWT header: {1}
+    InvalidHeader(String, JWTError),
+
+    /// Cannot create decoding key: {0}
+    CannotCreateDecodingKey(JWTError),
+
+    /// JWK does not contain an algorithm
+    JWKHasNoAlgorithm,
+
+    /// Cannot decode JWT: {0}
+    CannotDecodeJWT(JWTError),
+
+    /// Cannot insert claims into context: {0}
+    CannotInsertClaimsIntoContext(BoxError),
+
+    /// Cannot find kid: '{0:?}' in JWKS list
+    CannotFindKID(String),
+
+    /// Cannot find a suitable key for: alg: '{0:?}', kid: '{1:?}' in JWKS list
+    CannotFindSuitableKey(Algorithm, Option<String>),
+
+    /// Invalid issuer: the token's `iss` was '{token}', but signed with a key from '{expected}'
+    InvalidIssuer { expected: String, token: String },
+
+    /// Unsupported key algorithm: {0}
+    UnsupportedKeyAlgorithm(KeyAlgorithm),
+}
+
+fn jwt_error_to_reason(jwt_err: &JWTError) -> &'static str {
+    let kind = jwt_err.kind();
+    match kind {
+        ErrorKind::InvalidToken => "INVALID_TOKEN",
+        ErrorKind::InvalidSignature => "INVALID_SIGNATURE",
+        ErrorKind::InvalidEcdsaKey => "INVALID_ECDSA_KEY",
+        ErrorKind::InvalidRsaKey(_) => "INVALID_RSA_KEY",
+        ErrorKind::RsaFailedSigning => "RSA_FAILED_SIGNING",
+        ErrorKind::InvalidAlgorithmName => "INVALID_ALGORITHM_NAME",
+        ErrorKind::InvalidKeyFormat => "INVALID_KEY_FORMAT",
+        ErrorKind::MissingRequiredClaim(_) => "MISSING_REQUIRED_CLAIM",
+        ErrorKind::ExpiredSignature => "EXPIRED_SIGNATURE",
+        ErrorKind::InvalidIssuer => "INVALID_ISSUER",
+        ErrorKind::InvalidAudience => "INVALID_AUDIENCE",
+        ErrorKind::InvalidSubject => "INVALID_SUBJECT",
+        ErrorKind::ImmatureSignature => "IMMATURE_SIGNATURE",
+        ErrorKind::InvalidAlgorithm => "INVALID_ALGORITHM",
+        ErrorKind::MissingAlgorithm => "MISSING_ALGORITHM",
+        ErrorKind::Base64(_) => "BASE64_ERROR",
+        ErrorKind::Json(_) => "JSON_ERROR",
+        ErrorKind::Utf8(_) => "UTF8_ERROR",
+        ErrorKind::Crypto(_) => "CRYPTO_ERROR",
+        // ErrorKind is non-exhaustive
+        _ => "UNKNOWN_ERROR",
+    }
+}
+
+impl AuthenticationError {
+    pub(crate) fn as_context_object(&self) -> ErrorContext {
+        let (code, reason) = match self {
+            AuthenticationError::CannotConvertToString => ("CANNOT_CONVERT_TO_STRING", None),
+            AuthenticationError::InvalidPrefix(_, _) => ("INVALID_PREFIX", None),
+            AuthenticationError::MissingJWT(_) => ("MISSING_JWT", None),
+            AuthenticationError::InvalidHeader(_, jwt_err) => {
+                ("INVALID_HEADER", Some(jwt_error_to_reason(jwt_err).into()))
+            }
+            AuthenticationError::CannotCreateDecodingKey(jwt_err) => (
+                "CANNOT_CREATE_DECODING_KEY",
+                Some(jwt_error_to_reason(jwt_err).into()),
+            ),
+            AuthenticationError::JWKHasNoAlgorithm => ("JWK_HAS_NO_ALGORITHM", None),
+            AuthenticationError::CannotDecodeJWT(jwt_err) => (
+                "CANNOT_DECODE_JWT",
+                Some(jwt_error_to_reason(jwt_err).into()),
+            ),
+            AuthenticationError::CannotInsertClaimsIntoContext(_) => {
+                ("CANNOT_INSERT_CLAIMS_INTO_CONTEXT", None)
+            }
+            AuthenticationError::CannotFindKID(_) => ("CANNOT_FIND_KID", None),
+            AuthenticationError::CannotFindSuitableKey(_, _) => ("CANNOT_FIND_SUITABLE_KEY", None),
+            AuthenticationError::InvalidIssuer { .. } => ("INVALID_ISSUER", None),
+            AuthenticationError::UnsupportedKeyAlgorithm(_) => ("UNSUPPORTED_KEY_ALGORITHM", None),
+        };
+
+        ErrorContext {
+            message: self.to_string(),
+            code: code.into(),
+            reason,
+        }
+    }
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub(super) struct ErrorContext {
+    pub(super) message: String,
+    pub(super) code: String,
+    pub(super) reason: Option<String>,
+}
+
+#[derive(Error, Debug)]
+pub(crate) enum Error {
+    #[error("header_value_prefix must not contain whitespace")]
+    BadHeaderValuePrefix,
+}
diff --git a/apollo-router/src/plugins/authentication/jwks.rs b/apollo-router/src/plugins/authentication/jwks.rs
index efa893f749..52f7e42605 100644
--- a/apollo-router/src/plugins/authentication/jwks.rs
+++ b/apollo-router/src/plugins/authentication/jwks.rs
@@ -3,6 +3,8 @@ use std::collections::HashSet;
 use std::mem;
 use std::sync::Arc;
 use std::time::Duration;
+use std::time::SystemTime;
+use std::time::UNIX_EPOCH;
 
 use futures::future::Either;
 use futures::future::join_all;
@@ -10,22 +12,36 @@ use futures::future::select;
 use futures::pin_mut;
 use futures::stream::repeat;
 use futures::stream::select_all;
+use http::HeaderMap;
+use http::StatusCode;
 use http::header::ACCEPT;
 use jsonwebtoken::Algorithm;
+use jsonwebtoken::DecodingKey;
+use jsonwebtoken::TokenData;
+use jsonwebtoken::Validation;
+use jsonwebtoken::decode;
+use jsonwebtoken::jwk::AlgorithmParameters;
+use jsonwebtoken::jwk::EllipticCurve;
 use jsonwebtoken::jwk::Jwk;
 use jsonwebtoken::jwk::JwkSet;
+use jsonwebtoken::jwk::KeyAlgorithm;
+use jsonwebtoken::jwk::KeyOperations;
+use jsonwebtoken::jwk::PublicKeyUse;
 use mime::APPLICATION_JSON;
 use parking_lot::RwLock;
-use serde_json::Value;
 use tokio::fs::read_to_string;
 use tokio::sync::oneshot;
 use tower::BoxError;
 use tracing_futures::Instrument;
 use url::Url;
 
+use super::APOLLO_AUTHENTICATION_JWT_CLAIMS;
 use super::CLIENT;
 use super::DEFAULT_AUTHENTICATION_NETWORK_TIMEOUT;
 use super::Header;
+use super::Source;
+use crate::Context;
+use crate::plugins::authentication::error::AuthenticationError;
 
 #[derive(Clone)]
 pub(super) struct JwksManager {
@@ -137,7 +153,7 @@ async fn poll(
 }
 
 // This function is expected to return an Optional value, but we'd like to let
-// users know the various failure conditions. Hence the various clumsy map_err()
+// users know the various failure conditions. Hence, the various clumsy map_err()
 // scattered through the processing.
 pub(super) async fn get_jwks(url: Url, headers: Vec<Header>) -> Option<JwkSet> {
     let data = if url.scheme() == "file" {
@@ -201,7 +217,7 @@ pub(crate) fn parse_jwks(data: &str) -> Option<JwkSet> {
     // jsonwebtoken and exclude them
     tracing::debug!(data, "parsing JWKS");
 
-    let mut raw_json: Value = serde_json::from_str(data)
+    let mut raw_json: serde_json::Value = serde_json::from_str(data)
         .map_err(|e| {
             tracing::error!(%e, "could not create JSON Value from url content, enable debug logs to see content");
             e
@@ -256,3 +272,420 @@ impl Iterator for Iter<'_> {
         }
     }
 }
+
+#[derive(Debug, Default)]
+pub(super) struct JWTCriteria {
+    pub(super) alg: Algorithm,
+    pub(super) kid: Option<String>,
+}
+
+/// Search the list of JWKS to find a key we can use to decode a JWT.
+///
+/// The search criteria allow us to match a variety of keys depending on which criteria are provided
+/// by the JWT header. The only mandatory parameter is "alg".
+/// Note: "none" is not implemented by jsonwebtoken, so it can't be part of the [`Algorithm`] enum.
+pub(super) fn search_jwks(
+    jwks_manager: &JwksManager,
+    criteria: &JWTCriteria,
+) -> Option<Vec<(Option<String>, Jwk)>> {
+    const HIGHEST_SCORE: usize = 2;
+    let mut candidates = vec![];
+    let mut found_highest_score = false;
+    for JwkSetInfo {
+        jwks,
+        issuer,
+        algorithms,
+    } in jwks_manager.iter_jwks()
+    {
+        // filter accepted algorithms
+        if let Some(algs) = algorithms {
+            if !algs.contains(&criteria.alg) {
+                continue;
+            }
+        }
+
+        // Try to figure out if our jwks contains a candidate key (i.e.: a key which matches our
+        // criteria)
+        for mut key in jwks.keys.into_iter().filter(|key| {
+            // We are only interested in keys which are used for signature verification
+            match (&key.common.public_key_use, &key.common.key_operations) {
+                // "use" https://datatracker.ietf.org/doc/html/rfc7517#section-4.2 and
+                // "key_ops" https://datatracker.ietf.org/doc/html/rfc7517#section-4.3 are both optional
+                (None, None) => true,
+                (None, Some(purpose)) => purpose.contains(&KeyOperations::Verify),
+                (Some(key_use), None) => key_use == &PublicKeyUse::Signature,
+                // The "use" and "key_ops" JWK members SHOULD NOT be used together;
+                // however, if both are used, the information they convey MUST be
+                // consistent
+                (Some(key_use), Some(purpose)) => {
+                    key_use == &PublicKeyUse::Signature && purpose.contains(&KeyOperations::Verify)
+                }
+            }
+        }) {
+            let mut key_score = 0;
+
+            // Let's see if we have a specified kid and if they match
+            if criteria.kid.is_some() && key.common.key_id == criteria.kid {
+                key_score += 1;
+            }
+
+            // Furthermore, we would like our algorithms to match, or at least the kty
+            // If we have an algorithm that matches, boost the score
+            match key.common.key_algorithm {
+                Some(algorithm) => {
+                    if convert_key_algorithm(algorithm) != Some(criteria.alg) {
+                        continue;
+                    }
+                    key_score += 1;
+                }
+                // If a key doesn't have an algorithm, then we match the "alg" specified in the
+                // search criteria against all of the algorithms that we support.  If the
+                // key.algorithm parameters match the type of parameters for the "family" of the
+                // criteria "alg", then we update the key to use the value of "alg" provided in
+                // the search criteria.
+                // If not, then this is not a usable key for this JWT
+                // Note: Matching algorithm parameters may seem unusual, but the appropriate
+                // algorithm details are not structured for easy consumption in jsonwebtoken and
+                // this is the simplest way to determine algorithm family.
+                None => match (criteria.alg, &key.algorithm) {
+                    (
+                        Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512,
+                        AlgorithmParameters::OctetKey(_),
+                    ) => {
+                        key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                    }
+                    (
+                        Algorithm::RS256
+                        | Algorithm::RS384
+                        | Algorithm::RS512
+                        | Algorithm::PS256
+                        | Algorithm::PS384
+                        | Algorithm::PS512,
+                        AlgorithmParameters::RSA(_),
+                    ) => {
+                        key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                    }
+                    (Algorithm::ES256, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::P256 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    (Algorithm::ES384, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::P384 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    (Algorithm::EdDSA, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::Ed25519 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    _ => {
+                        // We'll ignore combinations we don't recognise
+                        continue;
+                    }
+                },
+            };
+
+            // If we get here we have a key that:
+            //  - may be used for signature verification
+            //  - has a matching algorithm, or if JWT has no algorithm, a matching key type
+            // It may have a matching kid if the JWT has a kid and it matches the key kid
+            //
+            // Multiple keys may meet the matching criteria, but they have a score. They get 1
+            // point for having an explicitly matching algorithm and 1 point for an explicitly
+            // matching kid. We will sort our candidates and pick the key with the highest score.
+
+            // If we find a key with a HIGHEST_SCORE, we will filter the list to only keep those
+            // with that score
+            if key_score == HIGHEST_SCORE {
+                found_highest_score = true;
+            }
+
+            candidates.push((key_score, (issuer.clone(), key)));
+        }
+    }
+
+    tracing::debug!(
+        "jwk candidates: {:?}",
+        candidates
+            .iter()
+            .map(|(score, (_, candidate))| (
+                score,
+                &candidate.common.key_id,
+                candidate.common.key_algorithm
+            ))
+            .collect::<Vec<(&usize, &Option<String>, Option<KeyAlgorithm>)>>()
+    );
+
+    if candidates.is_empty() {
+        None
+    } else {
+        // Only sort if we need to
+        if candidates.len() > 1 {
+            candidates.sort_by(|a, b| a.0.cmp(&b.0));
+        }
+
+        if found_highest_score {
+            Some(
+                candidates
+                    .into_iter()
+                    .filter_map(|(score, candidate)| {
+                        if score == HIGHEST_SCORE {
+                            Some(candidate)
+                        } else {
+                            None
+                        }
+                    })
+                    .collect(),
+            )
+        } else {
+            Some(
+                candidates
+                    .into_iter()
+                    .map(|(_score, candidate)| candidate)
+                    .collect(),
+            )
+        }
+    }
+}
+
+pub(super) fn extract_jwt<'a, 'b: 'a>(
+    source: &'a Source,
+    ignore_other_prefixes: bool,
+    headers: &'b HeaderMap,
+) -> Option<Result<&'b str, AuthenticationError>> {
+    match source {
+        Source::Header { name, value_prefix } => {
+            // The http_request is stored in a `Router::Request` context.
+            // We are going to check the headers for the presence of the configured header
+            let jwt_value_result = headers
+                .get(name)?
+                .to_str()
+                .map_err(|_err| AuthenticationError::CannotConvertToString);
+
+            // If we find the header, but can't convert it to a string, let the client know
+            let jwt_value_untrimmed = match jwt_value_result {
+                Ok(value) => value,
+                Err(err) => {
+                    return Some(Err(err));
+                }
+            };
+
+            // Let's trim out leading and trailing whitespace to be accommodating
+            let jwt_value = jwt_value_untrimmed.trim();
+
+            // Make sure the format of our message matches our expectations
+            // Technically, the spec is case-sensitive, but let's accept
+            // case variations
+            let prefix_len = value_prefix.len();
+            if jwt_value.len() < prefix_len
+                || !&jwt_value[..prefix_len].eq_ignore_ascii_case(value_prefix)
+            {
+                return if ignore_other_prefixes {
+                    None
+                } else {
+                    Some(Err(AuthenticationError::InvalidPrefix(
+                        jwt_value_untrimmed.to_owned(),
+                        value_prefix.to_owned(),
+                    )))
+                };
+            }
+            // If there's no header prefix, we avoid splitting the header
+            let jwt = if value_prefix.is_empty() {
+                // check for whitespace — we've already trimmed, so this means the request has a
+                // prefix that shouldn't exist
+                if jwt_value.contains(' ') {
+                    return Some(Err(AuthenticationError::InvalidPrefix(
+                        jwt_value_untrimmed.to_owned(),
+                        value_prefix.to_owned(),
+                    )));
+                }
+
+                // we can simply assign the jwt to the jwt_value; we'll validate down below
+                jwt_value
+            } else {
+                // Otherwise, we need to split our string in (at most 2) sections.
+                let jwt_parts: Vec<&str> = jwt_value.splitn(2, ' ').collect();
+                if jwt_parts.len() != 2 {
+                    return Some(Err(AuthenticationError::MissingJWT(jwt_value.to_owned())));
+                }
+
+                // We have our jwt
+                jwt_parts[1]
+            };
+            Some(Ok(jwt))
+        }
+        Source::Cookie { name } => {
+            for header in headers.get_all("cookie") {
+                let value = match header.to_str() {
+                    Ok(value) => value,
+                    Err(_not_a_string_error) => {
+                        return Some(Err(AuthenticationError::CannotConvertToString));
+                    }
+                };
+                for cookie in cookie::Cookie::split_parse(value) {
+                    match cookie {
+                        Err(_) => continue,
+                        Ok(cookie) => {
+                            if cookie.name() == name {
+                                if let Some(value) = cookie.value_raw() {
+                                    return Some(Ok(value));
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            None
+        }
+    }
+}
+
+pub(super) fn decode_jwt(
+    jwt: &str,
+    keys: Vec<(Option<String>, Jwk)>,
+    criteria: JWTCriteria,
+) -> Result<(Option<String>, TokenData<serde_json::Value>), (AuthenticationError, StatusCode)> {
+    let mut error = None;
+    for (issuer, jwk) in keys.into_iter() {
+        let decoding_key = match DecodingKey::from_jwk(&jwk) {
+            Ok(k) => k,
+            Err(e) => {
+                error = Some((
+                    AuthenticationError::CannotCreateDecodingKey(e),
+                    StatusCode::INTERNAL_SERVER_ERROR,
+                ));
+                continue;
+            }
+        };
+
+        let key_algorithm = match jwk.common.key_algorithm {
+            Some(a) => a,
+            None => {
+                error = Some((
+                    AuthenticationError::JWKHasNoAlgorithm,
+                    StatusCode::INTERNAL_SERVER_ERROR,
+                ));
+                continue;
+            }
+        };
+
+        let algorithm = match convert_key_algorithm(key_algorithm) {
+            Some(a) => a,
+            None => {
+                error = Some((
+                    AuthenticationError::UnsupportedKeyAlgorithm(key_algorithm),
+                    StatusCode::INTERNAL_SERVER_ERROR,
+                ));
+                continue;
+            }
+        };
+
+        let mut validation = Validation::new(algorithm);
+        validation.validate_nbf = true;
+        // if set to true, it will reject tokens containing an `aud` claim if the validation does not specify an audience
+        // we don't validate audience yet, so this is deactivated
+        validation.validate_aud = false;
+
+        match decode::<serde_json::Value>(jwt, &decoding_key, &validation) {
+            Ok(v) => return Ok((issuer, v)),
+            Err(e) => {
+                tracing::trace!("JWT decoding failed with error `{e}`");
+                error = Some((
+                    AuthenticationError::CannotDecodeJWT(e),
+                    StatusCode::UNAUTHORIZED,
+                ));
+            }
+        };
+    }
+
+    match error {
+        Some(e) => Err(e),
+        None => {
+            // We can't find a key to process this JWT.
+            Err((
+                criteria.kid.map_or_else(
+                    || AuthenticationError::CannotFindSuitableKey(criteria.alg, None),
+                    AuthenticationError::CannotFindKID,
+                ),
+                StatusCode::UNAUTHORIZED,
+            ))
+        }
+    }
+}
+
+pub(crate) fn jwt_expires_in(context: &Context) -> Duration {
+    let claims = context
+        .get(APOLLO_AUTHENTICATION_JWT_CLAIMS)
+        .map_err(|err| tracing::error!("could not read JWT claims: {err}"))
+        .ok()
+        .flatten();
+    let ts_opt = claims
+        .as_ref()
+        .and_then(|x: &serde_json::Value| match x.as_object() {
+            Some(claims) => claims.get("exp")?.as_i64(),
+            None => {
+                tracing::error!("expected JWT claims to be an object");
+                None
+            }
+        });
+
+    match ts_opt {
+        Some(ts) => {
+            let now = SystemTime::now()
+                .duration_since(UNIX_EPOCH)
+                .expect("we should not run before EPOCH")
+                .as_secs() as i64;
+            if now < ts {
+                Duration::from_secs((ts - now) as u64)
+            } else {
+                Duration::ZERO
+            }
+        }
+        None => {
+            tracing::error!(
+                "expected JWT 'exp' (expiry) claim to be an integer, defaulting to maximum duration"
+            );
+            Duration::MAX
+        }
+    }
+}
+
+// Apparently the `jsonwebtoken` crate now has 2 different enums for algorithms
+pub(crate) fn convert_key_algorithm(algorithm: KeyAlgorithm) -> Option<Algorithm> {
+    Some(match algorithm {
+        KeyAlgorithm::HS256 => Algorithm::HS256,
+        KeyAlgorithm::HS384 => Algorithm::HS384,
+        KeyAlgorithm::HS512 => Algorithm::HS512,
+        KeyAlgorithm::ES256 => Algorithm::ES256,
+        KeyAlgorithm::ES384 => Algorithm::ES384,
+        KeyAlgorithm::RS256 => Algorithm::RS256,
+        KeyAlgorithm::RS384 => Algorithm::RS384,
+        KeyAlgorithm::RS512 => Algorithm::RS512,
+        KeyAlgorithm::PS256 => Algorithm::PS256,
+        KeyAlgorithm::PS384 => Algorithm::PS384,
+        KeyAlgorithm::PS512 => Algorithm::PS512,
+        KeyAlgorithm::EdDSA => Algorithm::EdDSA,
+        // We don't use these encryption algorithms
+        KeyAlgorithm::RSA1_5 | KeyAlgorithm::RSA_OAEP | KeyAlgorithm::RSA_OAEP_256 => return None,
+    })
+}
+
+fn convert_algorithm(algorithm: Algorithm) -> KeyAlgorithm {
+    match algorithm {
+        Algorithm::HS256 => KeyAlgorithm::HS256,
+        Algorithm::HS384 => KeyAlgorithm::HS384,
+        Algorithm::HS512 => KeyAlgorithm::HS512,
+        Algorithm::ES256 => KeyAlgorithm::ES256,
+        Algorithm::ES384 => KeyAlgorithm::ES384,
+        Algorithm::RS256 => KeyAlgorithm::RS256,
+        Algorithm::RS384 => KeyAlgorithm::RS384,
+        Algorithm::RS512 => KeyAlgorithm::RS512,
+        Algorithm::PS256 => KeyAlgorithm::PS256,
+        Algorithm::PS384 => KeyAlgorithm::PS384,
+        Algorithm::PS512 => KeyAlgorithm::PS512,
+        Algorithm::EdDSA => KeyAlgorithm::EdDSA,
+    }
+}
diff --git a/apollo-router/src/plugins/authentication/mod.rs b/apollo-router/src/plugins/authentication/mod.rs
index dfd36f761d..d8d952eda2 100644
--- a/apollo-router/src/plugins/authentication/mod.rs
+++ b/apollo-router/src/plugins/authentication/mod.rs
@@ -5,34 +5,20 @@ use std::ops::ControlFlow;
 use std::str::FromStr;
 use std::sync::Arc;
 use std::time::Duration;
-use std::time::SystemTime;
-use std::time::UNIX_EPOCH;
 
-use displaydoc::Display;
-use http::HeaderMap;
+use error::AuthenticationError;
+use error::Error;
 use http::HeaderName;
 use http::HeaderValue;
 use http::StatusCode;
 use http::header;
 use jsonwebtoken::Algorithm;
-use jsonwebtoken::DecodingKey;
-use jsonwebtoken::TokenData;
-use jsonwebtoken::Validation;
-use jsonwebtoken::decode;
 use jsonwebtoken::decode_header;
-use jsonwebtoken::errors::Error as JWTError;
-use jsonwebtoken::jwk::AlgorithmParameters;
-use jsonwebtoken::jwk::EllipticCurve;
-use jsonwebtoken::jwk::Jwk;
-use jsonwebtoken::jwk::KeyAlgorithm;
-use jsonwebtoken::jwk::KeyOperations;
-use jsonwebtoken::jwk::PublicKeyUse;
 use once_cell::sync::Lazy;
 use reqwest::Client;
 use schemars::JsonSchema;
 use serde::Deserialize;
-use serde_json::Value;
-use thiserror::Error;
+use serde::Serialize;
 use tower::BoxError;
 use tower::ServiceBuilder;
 use tower::ServiceExt;
@@ -42,7 +28,6 @@ use self::jwks::JwksManager;
 use self::subgraph::SigningParams;
 use self::subgraph::SigningParamsConfig;
 use self::subgraph::SubgraphAuth;
-use crate::Context;
 use crate::graphql;
 use crate::layers::ServiceBuilderExt;
 use crate::plugin::PluginInit;
@@ -50,17 +35,20 @@ use crate::plugin::PluginPrivate;
 use crate::plugin::serde::deserialize_header_name;
 use crate::plugin::serde::deserialize_header_value;
 use crate::plugins::authentication::connector::ConnectorAuth;
-use crate::plugins::authentication::jwks::JwkSetInfo;
+use crate::plugins::authentication::error::ErrorContext;
 use crate::plugins::authentication::jwks::JwksConfig;
 use crate::plugins::authentication::subgraph::make_signing_params;
 use crate::services::APPLICATION_JSON_HEADER_VALUE;
 use crate::services::connector_service::ConnectorSourceRef;
 use crate::services::router;
 
+pub(crate) mod jwks;
+
 pub(crate) mod connector;
-mod jwks;
+
 pub(crate) mod subgraph;
 
+mod error;
 #[cfg(test)]
 mod tests;
 
@@ -70,56 +58,11 @@ pub(crate) const DEPRECATED_APOLLO_AUTHENTICATION_JWT_CLAIMS: &str =
     "apollo_authentication::JWT::claims";
 const HEADER_TOKEN_TRUNCATED: &str = "(truncated)";
 
-#[derive(Debug, Display, Error)]
-pub(crate) enum AuthenticationError<'a> {
-    /// Configured header is not convertible to a string
-    CannotConvertToString,
-
-    /// Header Value: '{0}' is not correctly formatted. prefix should be '{1}'
-    InvalidPrefix(&'a str, &'a str),
-
-    /// Header Value: '{0}' is not correctly formatted. Missing JWT
-    MissingJWT(&'a str),
-
-    /// '{0}' is not a valid JWT header: {1}
-    InvalidHeader(&'a str, JWTError),
-
-    /// Cannot create decoding key: {0}
-    CannotCreateDecodingKey(JWTError),
-
-    /// JWK does not contain an algorithm
-    JWKHasNoAlgorithm,
-
-    /// Cannot decode JWT: {0}
-    CannotDecodeJWT(JWTError),
-
-    /// Cannot insert claims into context: {0}
-    CannotInsertClaimsIntoContext(BoxError),
-
-    /// Cannot find kid: '{0:?}' in JWKS list
-    CannotFindKID(Option<String>),
-
-    /// Cannot find a suitable key for: alg: '{0:?}', kid: '{1:?}' in JWKS list
-    CannotFindSuitableKey(Algorithm, Option<String>),
-
-    /// Invalid issuer: the token's `iss` was '{token}', but signed with a key from '{expected}'
-    InvalidIssuer { expected: String, token: String },
-
-    /// Unsupported key algorithm: {0}
-    UnsupportedKeyAlgorithm(KeyAlgorithm),
-}
-
 const DEFAULT_AUTHENTICATION_NETWORK_TIMEOUT: Duration = Duration::from_secs(15);
 const DEFAULT_AUTHENTICATION_DOWNLOAD_INTERVAL: Duration = Duration::from_secs(60);
 
 static CLIENT: Lazy<Result<Client, BoxError>> = Lazy::new(|| Ok(Client::new()));
 
-#[derive(Error, Debug)]
-pub(crate) enum Error {
-    #[error("header_value_prefix must not contain whitespace")]
-    BadHeaderValuePrefix,
-}
-
 struct Router {
     configuration: JWTConf,
     jwks_manager: JwksManager,
@@ -131,6 +74,18 @@ struct AuthenticationPlugin {
     connector: Option<ConnectorAuth>,
 }
 
+#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq)]
+enum OnError {
+    Continue,
+    Error,
+}
+
+impl Default for OnError {
+    fn default() -> Self {
+        Self::Error
+    }
+}
+
 #[derive(Clone, Debug, Deserialize, JsonSchema, serde_derive_default::Default)]
 #[serde(deny_unknown_fields)]
 struct JWTConf {
@@ -148,6 +103,13 @@ struct JWTConf {
     /// Alternative sources to extract the JWT
     #[serde(default)]
     sources: Vec<Source>,
+    /// Control the behavior when an error occurs during the authentication process.
+    ///
+    /// Defaults to `Error`. When set to `Continue`, requests that fail JWT authentication will
+    /// continue to be processed by the router, but without the JWT claims in the context. When set
+    /// to `Error`, requests that fail JWT authentication will be rejected with a HTTP 403 error.
+    #[serde(default)]
+    on_error: OnError,
 }
 
 #[derive(Clone, Debug, Deserialize, JsonSchema)]
@@ -228,7 +190,7 @@ struct RouterConf {
 }
 
 fn default_header_name() -> String {
-    http::header::AUTHORIZATION.to_string()
+    header::AUTHORIZATION.to_string()
 }
 
 fn default_header_value_prefix() -> String {
@@ -239,344 +201,241 @@ fn default_poll_interval() -> Duration {
     DEFAULT_AUTHENTICATION_DOWNLOAD_INTERVAL
 }
 
-#[derive(Debug, Default)]
-struct JWTCriteria {
-    alg: Algorithm,
-    kid: Option<String>,
-}
+#[async_trait::async_trait]
+impl PluginPrivate for AuthenticationPlugin {
+    type Config = Conf;
 
-/// Search the list of JWKS to find a key we can use to decode a JWT.
-///
-/// The search criteria allow us to match a variety of keys depending on which criteria are provided
-/// by the JWT header. The only mandatory parameter is "alg".
-/// Note: "none" is not implemented by jsonwebtoken, so it can't be part of the [`Algorithm`] enum.
-fn search_jwks(
-    jwks_manager: &JwksManager,
-    criteria: &JWTCriteria,
-) -> Option<Vec<(Option<String>, Jwk)>> {
-    const HIGHEST_SCORE: usize = 2;
-    let mut candidates = vec![];
-    let mut found_highest_score = false;
-    for JwkSetInfo {
-        jwks,
-        issuer,
-        algorithms,
-    } in jwks_manager.iter_jwks()
-    {
-        // filter accepted algorithms
-        if let Some(algs) = algorithms {
-            if !algs.contains(&criteria.alg) {
-                continue;
-            }
-        }
+    async fn new(init: PluginInit<Self::Config>) -> Result<Self, BoxError> {
+        let subgraph = Self::init_subgraph(&init).await?;
+        let router = Self::init_router(&init).await?;
+        let connector = Self::init_connector(init).await?;
 
-        // Try to figure out if our jwks contains a candidate key (i.e.: a key which matches our
-        // criteria)
-        for mut key in jwks.keys.into_iter().filter(|key| {
-            // We are only interested in keys which are used for signature verification
-            match (&key.common.public_key_use, &key.common.key_operations) {
-                // "use" https://datatracker.ietf.org/doc/html/rfc7517#section-4.2 and
-                // "key_ops" https://datatracker.ietf.org/doc/html/rfc7517#section-4.3 are both optional
-                (None, None) => true,
-                (None, Some(purpose)) => purpose.contains(&KeyOperations::Verify),
-                (Some(key_use), None) => key_use == &PublicKeyUse::Signature,
-                // The "use" and "key_ops" JWK members SHOULD NOT be used together;
-                // however, if both are used, the information they convey MUST be
-                // consistent
-                (Some(key_use), Some(purpose)) => {
-                    key_use == &PublicKeyUse::Signature && purpose.contains(&KeyOperations::Verify)
-                }
-            }
-        }) {
-            let mut key_score = 0;
+        Ok(Self {
+            router,
+            subgraph,
+            connector,
+        })
+    }
 
-            // Let's see if we have a specified kid and if they match
-            if criteria.kid.is_some() && key.common.key_id == criteria.kid {
-                key_score += 1;
-            }
+    fn router_service(&self, service: router::BoxService) -> router::BoxService {
+        // Return without layering if no router config was defined
+        let Some(router_config) = &self.router else {
+            return service;
+        };
 
-            // Furthermore, we would like our algorithms to match, or at least the kty
-            // If we have an algorithm that matches, boost the score
-            match key.common.key_algorithm {
-                Some(algorithm) => {
-                    if convert_key_algorithm(algorithm) != Some(criteria.alg) {
-                        continue;
-                    }
-                    key_score += 1;
-                }
-                // If a key doesn't have an algorithm, then we match the "alg" specified in the
-                // search criteria against all of the algorithms that we support.  If the
-                // key.algorithm parameters match the type of parameters for the "family" of the
-                // criteria "alg", then we update the key to use the value of "alg" provided in
-                // the search criteria.
-                // If not, then this is not a usable key for this JWT
-                // Note: Matching algorithm parameters may seem unusual, but the appropriate
-                // algorithm details are not structured for easy consumption in jsonwebtoken and
-                // this is the simplest way to determine algorithm family.
-                None => match (criteria.alg, &key.algorithm) {
-                    (
-                        Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512,
-                        AlgorithmParameters::OctetKey(_),
-                    ) => {
-                        key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
-                    }
-                    (
-                        Algorithm::RS256
-                        | Algorithm::RS384
-                        | Algorithm::RS512
-                        | Algorithm::PS256
-                        | Algorithm::PS384
-                        | Algorithm::PS512,
-                        AlgorithmParameters::RSA(_),
-                    ) => {
-                        key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
-                    }
-                    (Algorithm::ES256, AlgorithmParameters::EllipticCurve(params)) => {
-                        if params.curve == EllipticCurve::P256 {
-                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
-                        }
-                    }
-                    (Algorithm::ES384, AlgorithmParameters::EllipticCurve(params)) => {
-                        if params.curve == EllipticCurve::P384 {
-                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
-                        }
-                    }
-                    (Algorithm::EdDSA, AlgorithmParameters::EllipticCurve(params)) => {
-                        if params.curve == EllipticCurve::Ed25519 {
-                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
-                        }
-                    }
-                    _ => {
-                        // We'll ignore combinations we don't recognise
-                        continue;
-                    }
-                },
-            };
-
-            // If we get here we have a key that:
-            //  - may be used for signature verification
-            //  - has a matching algorithm, or if JWT has no algorithm, a matching key type
-            // It may have a matching kid if the JWT has a kid and it matches the key kid
-            //
-            // Multiple keys may meet the matching criteria, but they have a score. They get 1
-            // point for having an explicitly matching algorithm and 1 point for an explicitly
-            // matching kid. We will sort our candidates and pick the key with the highest score.
-
-            // If we find a key with a HIGHEST_SCORE, we will filter the list to only keep those
-            // with that score
-            if key_score == HIGHEST_SCORE {
-                found_highest_score = true;
+        fn authentication_service_span() -> impl Fn(&router::Request) -> tracing::Span + Clone {
+            move |_request: &router::Request| {
+                tracing::info_span!(
+                    AUTHENTICATION_SPAN_NAME,
+                    "authentication service" = stringify!(router::Request),
+                    "otel.kind" = "INTERNAL"
+                )
             }
-
-            candidates.push((key_score, (issuer.clone(), key)));
         }
+
+        let jwks_manager = router_config.jwks_manager.clone();
+        let configuration = router_config.configuration.clone();
+
+        ServiceBuilder::new()
+            .instrument(authentication_service_span())
+            .checkpoint(move |request: router::Request| {
+                Ok(authenticate(&configuration, &jwks_manager, request))
+            })
+            .service(service)
+            .boxed()
     }
 
-    tracing::debug!(
-        "jwk candidates: {:?}",
-        candidates
-            .iter()
-            .map(|(score, (_, candidate))| (
-                score,
-                &candidate.common.key_id,
-                candidate.common.key_algorithm
-            ))
-            .collect::<Vec<(&usize, &Option<String>, Option<KeyAlgorithm>)>>()
-    );
+    fn subgraph_service(
+        &self,
+        name: &str,
+        service: crate::services::subgraph::BoxService,
+    ) -> crate::services::subgraph::BoxService {
+        // Return without layering if no subgraph config was defined
+        let Some(subgraph) = &self.subgraph else {
+            return service;
+        };
 
-    if candidates.is_empty() {
-        None
-    } else {
-        // Only sort if we need to
-        if candidates.len() > 1 {
-            candidates.sort_by(|a, b| a.0.cmp(&b.0));
-        }
+        subgraph.subgraph_service(name, service)
+    }
 
-        if found_highest_score {
-            Some(
-                candidates
-                    .into_iter()
-                    .filter_map(|(score, candidate)| {
-                        if score == HIGHEST_SCORE {
-                            Some(candidate)
-                        } else {
-                            None
-                        }
-                    })
-                    .collect(),
-            )
-        } else {
-            Some(
-                candidates
-                    .into_iter()
-                    .map(|(_score, candidate)| candidate)
-                    .collect(),
-            )
-        }
+    fn connector_request_service(
+        &self,
+        service: crate::services::connector::request_service::BoxService,
+        _: String,
+    ) -> crate::services::connector::request_service::BoxService {
+        // Return without layering if no connector config was defined
+        let Some(connector_auth) = &self.connector else {
+            return service;
+        };
+
+        connector_auth.connector_request_service(service)
     }
 }
 
-#[async_trait::async_trait]
-impl PluginPrivate for AuthenticationPlugin {
-    type Config = Conf;
-
-    async fn new(init: PluginInit<Self::Config>) -> Result<Self, BoxError> {
-        let subgraph = if let Some(config) = init.config.subgraph {
-            let all = if let Some(config) = &config.all {
-                Some(Arc::new(
-                    subgraph::make_signing_params(config, "all").await?,
-                ))
-            } else {
-                None
-            };
-
-            let mut subgraphs: HashMap<String, Arc<SigningParamsConfig>> = Default::default();
-            for (subgraph_name, config) in &config.subgraphs {
-                subgraphs.insert(
-                    subgraph_name.clone(),
-                    Arc::new(subgraph::make_signing_params(config, subgraph_name.as_str()).await?),
-                );
-            }
+impl AuthenticationPlugin {
+    async fn init_subgraph(init: &PluginInit<Conf>) -> Result<Option<SubgraphAuth>, BoxError> {
+        // if no subgraph config was defined, then return early
+        let Some(subgraph_conf) = init.config.subgraph.clone() else {
+            return Ok(None);
+        };
 
-            Some(SubgraphAuth {
-                signing_params: Arc::new(SigningParams { all, subgraphs }),
-            })
+        let all = if let Some(config) = &subgraph_conf.all {
+            Some(Arc::new(make_signing_params(config, "all").await?))
         } else {
             None
         };
 
-        let router = if let Some(mut router_conf) = init.config.router {
-            if router_conf
-                .jwt
-                .header_value_prefix
-                .as_bytes()
-                .iter()
-                .any(u8::is_ascii_whitespace)
-            {
-                return Err(Error::BadHeaderValuePrefix.into());
-            }
+        let mut subgraphs: HashMap<String, Arc<SigningParamsConfig>> = Default::default();
+        for (subgraph_name, config) in &subgraph_conf.subgraphs {
+            subgraphs.insert(
+                subgraph_name.clone(),
+                Arc::new(make_signing_params(config, subgraph_name.as_str()).await?),
+            );
+        }
 
-            for source in &router_conf.jwt.sources {
-                if let Source::Header { value_prefix, .. } = source {
-                    if value_prefix.as_bytes().iter().any(u8::is_ascii_whitespace) {
-                        return Err(Error::BadHeaderValuePrefix.into());
-                    }
+        Ok(Some(SubgraphAuth {
+            signing_params: Arc::new(SigningParams { all, subgraphs }),
+        }))
+    }
+
+    async fn init_router(init: &PluginInit<Conf>) -> Result<Option<Router>, BoxError> {
+        // if no router config was defined, then return early
+        let Some(mut router_conf) = init.config.router.clone() else {
+            return Ok(None);
+        };
+
+        if router_conf
+            .jwt
+            .header_value_prefix
+            .as_bytes()
+            .iter()
+            .any(u8::is_ascii_whitespace)
+        {
+            return Err(Error::BadHeaderValuePrefix.into());
+        }
+
+        for source in &router_conf.jwt.sources {
+            if let Source::Header { value_prefix, .. } = source {
+                if value_prefix.as_bytes().iter().any(u8::is_ascii_whitespace) {
+                    return Err(Error::BadHeaderValuePrefix.into());
                 }
             }
+        }
 
-            router_conf.jwt.sources.insert(
-                0,
-                Source::Header {
-                    name: router_conf.jwt.header_name.clone(),
-                    value_prefix: router_conf.jwt.header_value_prefix.clone(),
-                },
-            );
+        router_conf.jwt.sources.insert(
+            0,
+            Source::Header {
+                name: router_conf.jwt.header_name.clone(),
+                value_prefix: router_conf.jwt.header_value_prefix.clone(),
+            },
+        );
 
-            let mut list = vec![];
-            for jwks_conf in &router_conf.jwt.jwks {
-                let url: Url = Url::from_str(jwks_conf.url.as_str())?;
-                list.push(JwksConfig {
-                    url,
-                    issuer: jwks_conf.issuer.clone(),
-                    algorithms: jwks_conf
-                        .algorithms
-                        .as_ref()
-                        .map(|algs| algs.iter().cloned().collect()),
-                    poll_interval: jwks_conf.poll_interval,
-                    headers: jwks_conf.headers.clone(),
-                });
-            }
+        let mut list = vec![];
+        for jwks_conf in &router_conf.jwt.jwks {
+            let url: Url = Url::from_str(jwks_conf.url.as_str())?;
+            list.push(JwksConfig {
+                url,
+                issuer: jwks_conf.issuer.clone(),
+                algorithms: jwks_conf
+                    .algorithms
+                    .as_ref()
+                    .map(|algs| algs.iter().cloned().collect()),
+                poll_interval: jwks_conf.poll_interval,
+                headers: jwks_conf.headers.clone(),
+            });
+        }
 
-            tracing::info!(jwks=?router_conf.jwt.jwks, "JWT authentication using JWKSets from");
+        tracing::info!(jwks=?router_conf.jwt.jwks, "JWT authentication using JWKSets from");
 
-            let jwks_manager = JwksManager::new(list).await?;
+        let jwks_manager = JwksManager::new(list).await?;
 
-            Some(Router {
-                configuration: router_conf.jwt,
-                jwks_manager,
-            })
-        } else {
-            None
-        };
+        Ok(Some(Router {
+            configuration: router_conf.jwt,
+            jwks_manager,
+        }))
+    }
 
-        let connector = if let Some(config) = init.config.connector {
-            let mut signing_params: HashMap<ConnectorSourceRef, Arc<SigningParamsConfig>> =
-                Default::default();
-            for (s, source_config) in config.sources {
-                let source_ref: ConnectorSourceRef = s.parse()?;
-                signing_params.insert(
-                    source_ref.clone(),
-                    make_signing_params(&source_config, &source_ref.subgraph_name)
-                        .await
-                        .map(Arc::new)?,
-                );
-            }
-            Some(ConnectorAuth {
-                signing_params: Arc::new(signing_params),
-            })
-        } else {
-            None
+    async fn init_connector(init: PluginInit<Conf>) -> Result<Option<ConnectorAuth>, BoxError> {
+        // if no connector config was defined, then return early
+        let Some(connector_conf) = init.config.connector.clone() else {
+            return Ok(None);
         };
 
-        Ok(Self {
-            router,
-            subgraph,
-            connector,
-        })
+        let mut signing_params: HashMap<ConnectorSourceRef, Arc<SigningParamsConfig>> =
+            Default::default();
+        for (s, source_config) in connector_conf.sources {
+            let source_ref: ConnectorSourceRef = s.parse()?;
+            signing_params.insert(
+                source_ref.clone(),
+                make_signing_params(&source_config, &source_ref.subgraph_name)
+                    .await
+                    .map(Arc::new)?,
+            );
+        }
+
+        Ok(Some(ConnectorAuth {
+            signing_params: Arc::new(signing_params),
+        }))
     }
+}
 
-    fn router_service(&self, service: router::BoxService) -> router::BoxService {
-        if let Some(config) = &self.router {
-            let jwks_manager = config.jwks_manager.clone();
-            let configuration = config.configuration.clone();
-
-            fn authentication_service_span() -> impl Fn(&router::Request) -> tracing::Span + Clone {
-                move |_request: &router::Request| {
-                    tracing::info_span!(
-                        AUTHENTICATION_SPAN_NAME,
-                        "authentication service" = stringify!(router::Request),
-                        "otel.kind" = "INTERNAL"
-                    )
-                }
-            }
+#[derive(Debug, Serialize, Deserialize)]
+enum JwtStatus {
+    Failure {
+        r#type: String,
+        name: String,
+        error: ErrorContext,
+    },
+    Success {
+        r#type: String,
+        name: String,
+    },
+}
 
-            ServiceBuilder::new()
-                .instrument(authentication_service_span())
-                .checkpoint(move |request: router::Request| {
-                    Ok(authenticate(&configuration, &jwks_manager, request))
-                })
-                .service(service)
-                .boxed()
-        } else {
-            service
+impl JwtStatus {
+    fn new_failure(source: Option<&Source>, error_context: ErrorContext) -> Self {
+        let (r#type, name) = match source {
+            Some(Source::Header { name, .. }) => ("header", name.as_str()),
+            Some(Source::Cookie { name }) => ("cookie", name.as_str()),
+            None => ("unknown", "unknown"),
+        };
+
+        Self::Failure {
+            r#type: r#type.into(),
+            name: name.into(),
+            error: error_context,
         }
     }
 
-    fn subgraph_service(
-        &self,
-        name: &str,
-        service: crate::services::subgraph::BoxService,
-    ) -> crate::services::subgraph::BoxService {
-        if let Some(auth) = &self.subgraph {
-            auth.subgraph_service(name, service)
-        } else {
-            service
+    fn new_success(source: Option<&Source>) -> Self {
+        match source {
+            Some(Source::Header { name, .. }) => Self::Success {
+                r#type: "header".into(),
+                name: name.into(),
+            },
+            Some(Source::Cookie { name }) => Self::Success {
+                r#type: "cookie".into(),
+                name: name.into(),
+            },
+            None => Self::Success {
+                r#type: "unknown".into(),
+                name: "unknown".into(),
+            },
         }
     }
 
-    fn connector_request_service(
-        &self,
-        service: crate::services::connector::request_service::BoxService,
-        _: String,
-    ) -> crate::services::connector::request_service::BoxService {
-        if let Some(auth) = &self.connector {
-            auth.connector_request_service(service)
-        } else {
-            service
+    #[cfg(test)]
+    /// Returns the error context if the status is a failure; Otherwise, returns None.
+    fn error(&self) -> Option<&ErrorContext> {
+        match self {
+            Self::Failure { error, .. } => Some(error),
+            _ => None,
         }
     }
 }
 
+const JWT_CONTEXT_KEY: &str = "apollo::authentication::jwt_status";
+
 fn authenticate(
     config: &JWTConf,
     jwks_manager: &JwksManager,
@@ -585,9 +444,11 @@ fn authenticate(
     // We are going to do a lot of similar checking so let's define a local function
     // to help reduce repetition
     fn failure_message(
-        context: Context,
+        request: router::Request,
+        config: &JWTConf,
         error: AuthenticationError,
         status: StatusCode,
+        source: Option<&Source>,
     ) -> ControlFlow<router::Response, router::Request> {
         // This is a metric and will not appear in the logs
         u64_counter!(
@@ -596,36 +457,57 @@ fn authenticate(
             1,
             authentication.jwt.failed = true
         );
-        tracing::info!(message = %error, "jwt authentication failure");
-        let response = router::Response::infallible_builder()
-            .error(
-                graphql::Error::builder()
-                    .message(error.to_string())
-                    .extension_code("AUTH_ERROR")
-                    .build(),
-            )
-            .status_code(status)
-            .header(header::CONTENT_TYPE, APPLICATION_JSON_HEADER_VALUE.clone())
-            .context(context)
-            .build();
-        ControlFlow::Break(response)
+        tracing::error!(message = %error, "jwt authentication failure");
+
+        let _ = request.context.insert_json_value(
+            JWT_CONTEXT_KEY,
+            serde_json_bytes::json!(JwtStatus::new_failure(source, error.as_context_object())),
+        );
+
+        if config.on_error == OnError::Error {
+            let response = router::Response::infallible_builder()
+                .error(
+                    graphql::Error::builder()
+                        .message(error.to_string())
+                        .extension_code("AUTH_ERROR")
+                        .build(),
+                )
+                .status_code(status)
+                .header(header::CONTENT_TYPE, APPLICATION_JSON_HEADER_VALUE.clone())
+                .context(request.context)
+                .build();
+
+            ControlFlow::Break(response)
+        } else {
+            ControlFlow::Continue(request)
+        }
     }
 
     let mut jwt = None;
+    let mut source_of_extracted_jwt = None;
     for source in &config.sources {
-        match extract_jwt(
+        let extracted_jwt = jwks::extract_jwt(
             source,
             config.ignore_other_prefixes,
             request.router_request.headers(),
-        ) {
+        );
+
+        match extracted_jwt {
             None => continue,
-            Some(Err(error)) => {
-                return failure_message(request.context, error, StatusCode::BAD_REQUEST);
-            }
             Some(Ok(extracted_jwt)) => {
+                source_of_extracted_jwt = Some(source);
                 jwt = Some(extracted_jwt);
                 break;
             }
+            Some(Err(error)) => {
+                return failure_message(
+                    request,
+                    config,
+                    error,
+                    StatusCode::BAD_REQUEST,
+                    Some(source),
+                );
+            }
         }
     }
 
@@ -641,15 +523,17 @@ fn authenticate(
             // Don't reflect the jwt on error, just reply with a fixed
             // error message.
             return failure_message(
-                request.context,
-                AuthenticationError::InvalidHeader(HEADER_TOKEN_TRUNCATED, e),
+                request,
+                config,
+                AuthenticationError::InvalidHeader(HEADER_TOKEN_TRUNCATED.to_owned(), e),
                 StatusCode::BAD_REQUEST,
+                source_of_extracted_jwt,
             );
         }
     };
 
     // Extract our search criteria from our jwt
-    let criteria = JWTCriteria {
+    let criteria = jwks::JWTCriteria {
         kid: jwt_header.kid,
         alg: jwt_header.alg,
     };
@@ -657,11 +541,17 @@ fn authenticate(
     // Search our list of JWKS to find the kid and process it
     // Note: This will search through JWKS in the order in which they are defined
     // in configuration.
-    if let Some(keys) = search_jwks(jwks_manager, &criteria) {
-        let (issuer, token_data) = match decode_jwt(jwt, keys, criteria) {
+    if let Some(keys) = jwks::search_jwks(jwks_manager, &criteria) {
+        let (issuer, token_data) = match jwks::decode_jwt(jwt, keys, criteria) {
             Ok(data) => data,
             Err((auth_error, status_code)) => {
-                return failure_message(request.context, auth_error, status_code);
+                return failure_message(
+                    request,
+                    config,
+                    auth_error,
+                    status_code,
+                    source_of_extracted_jwt,
+                );
             }
         };
 
@@ -674,12 +564,14 @@ fn authenticate(
             {
                 if configured_issuer != token_issuer {
                     return failure_message(
-                        request.context,
+                        request,
+                        config,
                         AuthenticationError::InvalidIssuer {
                             expected: configured_issuer,
                             token: token_issuer.to_string(),
                         },
                         StatusCode::INTERNAL_SERVER_ERROR,
+                        source_of_extracted_jwt,
                     );
                 }
             }
@@ -687,12 +579,14 @@ fn authenticate(
 
         if let Err(e) = request
             .context
-            .insert(APOLLO_AUTHENTICATION_JWT_CLAIMS, token_data.claims)
+            .insert(APOLLO_AUTHENTICATION_JWT_CLAIMS, token_data.claims.clone())
         {
             return failure_message(
-                request.context,
+                request,
+                config,
                 AuthenticationError::CannotInsertClaimsIntoContext(e),
                 StatusCode::INTERNAL_SERVER_ERROR,
+                source_of_extracted_jwt,
             );
         }
         // This is a metric and will not appear in the logs
@@ -701,266 +595,28 @@ fn authenticate(
             "Number of requests with JWT authentication",
             1
         );
-        return ControlFlow::Continue(request);
-    }
 
-    // We can't find a key to process this JWT.
-    if criteria.kid.is_some() {
-        failure_message(
-            request.context,
-            AuthenticationError::CannotFindKID(criteria.kid),
-            StatusCode::UNAUTHORIZED,
-        )
-    } else {
-        failure_message(
-            request.context,
-            AuthenticationError::CannotFindSuitableKey(criteria.alg, criteria.kid),
-            StatusCode::UNAUTHORIZED,
-        )
-    }
-}
-
-fn extract_jwt<'a, 'b: 'a>(
-    source: &'a Source,
-    ignore_other_prefixes: bool,
-    headers: &'b HeaderMap,
-) -> Option<Result<&'b str, AuthenticationError<'a>>> {
-    match source {
-        Source::Header { name, value_prefix } => {
-            // The http_request is stored in a `Router::Request` context.
-            // We are going to check the headers for the presence of the configured header
-            let jwt_value_result = match headers.get(name) {
-                Some(value) => value.to_str(),
-                None => return None,
-            };
-
-            // If we find the header, but can't convert it to a string, let the client know
-            let jwt_value_untrimmed = match jwt_value_result {
-                Ok(value) => value,
-                Err(_not_a_string_error) => {
-                    return Some(Err(AuthenticationError::CannotConvertToString));
-                }
-            };
-
-            // Let's trim out leading and trailing whitespace to be accommodating
-            let jwt_value = jwt_value_untrimmed.trim();
-
-            // Make sure the format of our message matches our expectations
-            // Technically, the spec is case sensitive, but let's accept
-            // case variations
-            //
-            let prefix_len = value_prefix.len();
-            if jwt_value.len() < prefix_len
-                || !&jwt_value[..prefix_len].eq_ignore_ascii_case(value_prefix)
-            {
-                if ignore_other_prefixes {
-                    return None;
-                } else {
-                    return Some(Err(AuthenticationError::InvalidPrefix(
-                        jwt_value_untrimmed,
-                        value_prefix,
-                    )));
-                }
-            }
-            // If there's no header prefix, we need to avoid splitting the header
-            let jwt = if value_prefix.is_empty() {
-                // check for whitespace- we've already trimmed, so this means the request has a prefix that shouldn't exist
-                if jwt_value.contains(' ') {
-                    return Some(Err(AuthenticationError::InvalidPrefix(
-                        jwt_value_untrimmed,
-                        value_prefix,
-                    )));
-                }
-
-                // we can simply assign the jwt to the jwt_value; we'll validate down below
-                jwt_value
-            } else {
-                // Otherwise, we need to split our string in (at most 2) sections.
-                let jwt_parts: Vec<&str> = jwt_value.splitn(2, ' ').collect();
-                if jwt_parts.len() != 2 {
-                    return Some(Err(AuthenticationError::MissingJWT(jwt_value)));
-                }
-
-                // We have our jwt
-                jwt_parts[1]
-            };
-            Some(Ok(jwt))
-        }
-        Source::Cookie { name } => {
-            for header in headers.get_all("cookie") {
-                let value = match header.to_str() {
-                    Ok(value) => value,
-                    Err(_not_a_string_error) => {
-                        return Some(Err(AuthenticationError::CannotConvertToString));
-                    }
-                };
-                for cookie in cookie::Cookie::split_parse(value) {
-                    match cookie {
-                        Err(_) => continue,
-                        Ok(cookie) => {
-                            if cookie.name() == name {
-                                if let Some(value) = cookie.value_raw() {
-                                    return Some(Ok(value));
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-
-            None
-        }
-    }
-}
-
-fn decode_jwt(
-    jwt: &str,
-    keys: Vec<(Option<String>, Jwk)>,
-    criteria: JWTCriteria,
-) -> Result<(Option<String>, TokenData<serde_json::Value>), (AuthenticationError, StatusCode)> {
-    let mut error = None;
-    for (issuer, jwk) in keys.into_iter() {
-        let decoding_key = match DecodingKey::from_jwk(&jwk) {
-            Ok(k) => k,
-            Err(e) => {
-                error = Some((
-                    AuthenticationError::CannotCreateDecodingKey(e),
-                    StatusCode::INTERNAL_SERVER_ERROR,
-                ));
-                continue;
-            }
-        };
-
-        let key_algorithm = match jwk.common.key_algorithm {
-            Some(a) => a,
-            None => {
-                error = Some((
-                    AuthenticationError::JWKHasNoAlgorithm,
-                    StatusCode::INTERNAL_SERVER_ERROR,
-                ));
-                continue;
-            }
-        };
-
-        let algorithm = match convert_key_algorithm(key_algorithm) {
-            Some(a) => a,
-            None => {
-                error = Some((
-                    AuthenticationError::UnsupportedKeyAlgorithm(key_algorithm),
-                    StatusCode::INTERNAL_SERVER_ERROR,
-                ));
-                continue;
-            }
-        };
-
-        let mut validation = Validation::new(algorithm);
-        validation.validate_nbf = true;
-        // if set to true, it will reject tokens containing an `aud` claim if the validation does not specify an audience
-        // we don't validate audience yet, so this is deactivated
-        validation.validate_aud = false;
-
-        match decode::<serde_json::Value>(jwt, &decoding_key, &validation) {
-            Ok(v) => return Ok((issuer, v)),
-            Err(e) => {
-                error = Some((
-                    AuthenticationError::CannotDecodeJWT(e),
-                    StatusCode::UNAUTHORIZED,
-                ));
-            }
-        };
-    }
-
-    match error {
-        Some(e) => Err(e),
-        None => {
-            // We can't find a key to process this JWT.
-            if criteria.kid.is_some() {
-                Err((
-                    AuthenticationError::CannotFindKID(criteria.kid),
-                    StatusCode::UNAUTHORIZED,
-                ))
-            } else {
-                Err((
-                    AuthenticationError::CannotFindSuitableKey(criteria.alg, criteria.kid),
-                    StatusCode::UNAUTHORIZED,
-                ))
-            }
-        }
-    }
-}
+        let _ = request.context.insert_json_value(
+            JWT_CONTEXT_KEY,
+            serde_json_bytes::json!(JwtStatus::new_success(source_of_extracted_jwt)),
+        );
 
-pub(crate) fn jwt_expires_in(context: &Context) -> Duration {
-    let claims = context
-        .get(APOLLO_AUTHENTICATION_JWT_CLAIMS)
-        .map_err(|err| tracing::error!("could not read JWT claims: {err}"))
-        .ok()
-        .flatten();
-    let ts_opt = claims.as_ref().and_then(|x: &Value| {
-        if !x.is_object() {
-            tracing::error!("JWT claims should be an object");
-            return None;
-        }
-        let claims = x.as_object().expect("claims should be an object");
-        let exp = claims.get("exp")?;
-        if !exp.is_number() {
-            tracing::error!("JWT 'exp' (expiry) claim should be a number");
-            return None;
-        }
-        exp.as_i64()
-    });
-    match ts_opt {
-        Some(ts) => {
-            let now = SystemTime::now()
-                .duration_since(UNIX_EPOCH)
-                .expect("we should not run before EPOCH")
-                .as_secs() as i64;
-            if now < ts {
-                Duration::from_secs((ts - now) as u64)
-            } else {
-                Duration::ZERO
-            }
-        }
-        None => Duration::MAX,
+        return ControlFlow::Continue(request);
     }
-}
 
-//Apparently the jsonwebtoken crate now has 2 different enums for algorithms
-pub(crate) fn convert_key_algorithm(algorithm: KeyAlgorithm) -> Option<Algorithm> {
-    Some(match algorithm {
-        jsonwebtoken::jwk::KeyAlgorithm::HS256 => jsonwebtoken::Algorithm::HS256,
-        jsonwebtoken::jwk::KeyAlgorithm::HS384 => jsonwebtoken::Algorithm::HS384,
-        jsonwebtoken::jwk::KeyAlgorithm::HS512 => jsonwebtoken::Algorithm::HS512,
-        jsonwebtoken::jwk::KeyAlgorithm::ES256 => jsonwebtoken::Algorithm::ES256,
-        jsonwebtoken::jwk::KeyAlgorithm::ES384 => jsonwebtoken::Algorithm::ES384,
-        jsonwebtoken::jwk::KeyAlgorithm::RS256 => jsonwebtoken::Algorithm::RS256,
-        jsonwebtoken::jwk::KeyAlgorithm::RS384 => jsonwebtoken::Algorithm::RS384,
-        jsonwebtoken::jwk::KeyAlgorithm::RS512 => jsonwebtoken::Algorithm::RS512,
-        jsonwebtoken::jwk::KeyAlgorithm::PS256 => jsonwebtoken::Algorithm::PS256,
-        jsonwebtoken::jwk::KeyAlgorithm::PS384 => jsonwebtoken::Algorithm::PS384,
-        jsonwebtoken::jwk::KeyAlgorithm::PS512 => jsonwebtoken::Algorithm::PS512,
-        jsonwebtoken::jwk::KeyAlgorithm::EdDSA => jsonwebtoken::Algorithm::EdDSA,
-        // we do not use the encryption algorithms
-        jsonwebtoken::jwk::KeyAlgorithm::RSA1_5
-        | jsonwebtoken::jwk::KeyAlgorithm::RSA_OAEP
-        | jsonwebtoken::jwk::KeyAlgorithm::RSA_OAEP_256 => return None,
-    })
-}
+    // We can't find a key to process this JWT.
+    let err = criteria.kid.map_or_else(
+        || AuthenticationError::CannotFindSuitableKey(criteria.alg, None),
+        AuthenticationError::CannotFindKID,
+    );
 
-pub(crate) fn convert_algorithm(algorithm: Algorithm) -> KeyAlgorithm {
-    match algorithm {
-        jsonwebtoken::Algorithm::HS256 => jsonwebtoken::jwk::KeyAlgorithm::HS256,
-        jsonwebtoken::Algorithm::HS384 => jsonwebtoken::jwk::KeyAlgorithm::HS384,
-        jsonwebtoken::Algorithm::HS512 => jsonwebtoken::jwk::KeyAlgorithm::HS512,
-        jsonwebtoken::Algorithm::ES256 => jsonwebtoken::jwk::KeyAlgorithm::ES256,
-        jsonwebtoken::Algorithm::ES384 => jsonwebtoken::jwk::KeyAlgorithm::ES384,
-        jsonwebtoken::Algorithm::RS256 => jsonwebtoken::jwk::KeyAlgorithm::RS256,
-        jsonwebtoken::Algorithm::RS384 => jsonwebtoken::jwk::KeyAlgorithm::RS384,
-        jsonwebtoken::Algorithm::RS512 => jsonwebtoken::jwk::KeyAlgorithm::RS512,
-        jsonwebtoken::Algorithm::PS256 => jsonwebtoken::jwk::KeyAlgorithm::PS256,
-        jsonwebtoken::Algorithm::PS384 => jsonwebtoken::jwk::KeyAlgorithm::PS384,
-        jsonwebtoken::Algorithm::PS512 => jsonwebtoken::jwk::KeyAlgorithm::PS512,
-        jsonwebtoken::Algorithm::EdDSA => jsonwebtoken::jwk::KeyAlgorithm::EdDSA,
-    }
+    failure_message(
+        request,
+        config,
+        err,
+        StatusCode::UNAUTHORIZED,
+        source_of_extracted_jwt,
+    )
 }
 
 // This macro allows us to use it in our plugin registry!
diff --git a/apollo-router/src/plugins/authentication/snapshots/apollo_router__plugins__authentication__tests__parse_failure_logs.snap b/apollo-router/src/plugins/authentication/snapshots/apollo_router__plugins__authentication__tests__parse_failure_logs.snap
index 125add8b16..f7f7359ccf 100644
--- a/apollo-router/src/plugins/authentication/snapshots/apollo_router__plugins__authentication__tests__parse_failure_logs.snap
+++ b/apollo-router/src/plugins/authentication/snapshots/apollo_router__plugins__authentication__tests__parse_failure_logs.snap
@@ -1,5 +1,6 @@
 ---
 source: apollo-router/src/plugins/authentication/tests.rs
+assertion_line: 1432
 expression: jwks
 ---
 keys:
@@ -26,4 +27,3 @@ keys:
     crv: P-256
     x: opFUViwCYVZLmsbG2cJTA9uPvOF5Gg8W7uNhrcorGhI
     y: bPxvCFKmlqTdEFc34OekvpviUUyelGrbi020dlgIsqo
-
diff --git a/apollo-router/src/plugins/authentication/snapshots/apollo_router__plugins__authentication__tests__parse_failure_logs@logs.snap b/apollo-router/src/plugins/authentication/snapshots/apollo_router__plugins__authentication__tests__parse_failure_logs@logs.snap
index f46ba8763c..9cb0c86153 100644
--- a/apollo-router/src/plugins/authentication/snapshots/apollo_router__plugins__authentication__tests__parse_failure_logs@logs.snap
+++ b/apollo-router/src/plugins/authentication/snapshots/apollo_router__plugins__authentication__tests__parse_failure_logs@logs.snap
@@ -1,5 +1,6 @@
 ---
 source: apollo-router/src/plugins/authentication/tests.rs
+assertion_line: 1430
 expression: yaml
 ---
 - fields:
@@ -20,4 +21,3 @@ expression: yaml
     index: 5
   level: WARN
   message: "ignoring a key since it is not valid, enable debug logs to full content"
-
diff --git a/apollo-router/src/plugins/authentication/tests.rs b/apollo-router/src/plugins/authentication/tests.rs
index 0b03fe8253..4f7e51f0a3 100644
--- a/apollo-router/src/plugins/authentication/tests.rs
+++ b/apollo-router/src/plugins/authentication/tests.rs
@@ -1,40 +1,66 @@
 use std::collections::HashMap;
 use std::collections::HashSet;
+use std::ops::ControlFlow;
 use std::path::Path;
+use std::str::FromStr;
 use std::sync::Arc;
 use std::sync::atomic::AtomicBool;
 use std::sync::atomic::Ordering;
+use std::time::Duration;
 
 use axum::handler::HandlerWithoutStateExt;
 use base64::Engine as _;
 use base64::prelude::BASE64_URL_SAFE_NO_PAD;
-use http::header::CONTENT_TYPE;
+use http::HeaderMap;
+use http::HeaderName;
+use http::HeaderValue;
+use http::StatusCode;
 use insta::assert_yaml_snapshot;
+use jsonwebtoken::Algorithm;
 use jsonwebtoken::EncodingKey;
 use jsonwebtoken::encode;
 use jsonwebtoken::get_current_timestamp;
+use jsonwebtoken::jwk::AlgorithmParameters;
 use jsonwebtoken::jwk::CommonParameters;
+use jsonwebtoken::jwk::EllipticCurve;
 use jsonwebtoken::jwk::EllipticCurveKeyParameters;
 use jsonwebtoken::jwk::EllipticCurveKeyType;
+use jsonwebtoken::jwk::Jwk;
 use jsonwebtoken::jwk::JwkSet;
+use jsonwebtoken::jwk::KeyAlgorithm;
+use jsonwebtoken::jwk::KeyOperations;
+use jsonwebtoken::jwk::PublicKeyUse;
 use mime::APPLICATION_JSON;
 use p256::ecdsa::SigningKey;
 use p256::pkcs8::EncodePrivateKey;
 use rand_core::OsRng;
+use serde::Deserialize;
 use serde::Serialize;
-use serde_json::Value;
+use tower::ServiceExt;
 use tracing::subscriber;
+use url::Url;
 
+use super::APOLLO_AUTHENTICATION_JWT_CLAIMS;
+use super::HEADER_TOKEN_TRUNCATED;
 use super::Header;
-use super::*;
+use super::JWT_CONTEXT_KEY;
+use super::JWTConf;
+use super::JwtStatus;
+use super::Source;
+use super::authenticate;
 use crate::assert_snapshot_subscriber;
+use crate::graphql;
 use crate::plugin::test;
+use crate::plugins::authentication::jwks::JWTCriteria;
+use crate::plugins::authentication::jwks::JwksConfig;
+use crate::plugins::authentication::jwks::JwksManager;
 use crate::plugins::authentication::jwks::parse_jwks;
+use crate::plugins::authentication::jwks::search_jwks;
 use crate::services::router;
 use crate::services::router::body::RouterBody;
 use crate::services::supergraph;
 
-fn create_an_url(filename: &str) -> String {
+pub(crate) fn create_an_url(filename: &str) -> String {
     let jwks_base = Path::new("tests");
 
     let jwks_path = jwks_base.join("fixtures").join(filename);
@@ -45,7 +71,7 @@ fn create_an_url(filename: &str) -> String {
 }
 
 async fn build_a_default_test_harness() -> router::BoxCloneService {
-    build_a_test_harness(None, None, false, false).await
+    build_a_test_harness(None, None, false, false, false).await
 }
 
 async fn build_a_test_harness(
@@ -53,6 +79,7 @@ async fn build_a_test_harness(
     header_value_prefix: Option<String>,
     multiple_jwks: bool,
     ignore_other_prefixes: bool,
+    continue_on_error: bool,
 ) -> router::BoxCloneService {
     // create a mock service we will use to test our plugin
     let mut mock_service = test::MockSupergraphService::new();
@@ -123,13 +150,21 @@ async fn build_a_test_harness(
     config["authentication"]["router"]["jwt"]["ignore_other_prefixes"] =
         serde_json::Value::Bool(ignore_other_prefixes);
 
-    crate::TestHarness::builder()
+    if continue_on_error {
+        config["authentication"]["router"]["jwt"]["on_error"] =
+            serde_json::Value::String("Continue".to_string());
+    }
+
+    match crate::TestHarness::builder()
         .configuration_json(config)
         .unwrap()
         .supergraph_hook(move |_| mock_service.clone().boxed())
         .build_router()
         .await
-        .unwrap()
+    {
+        Ok(test_harness) => test_harness,
+        Err(e) => panic!("Failed to build test harness: {}", e),
+    }
 }
 
 #[tokio::test]
@@ -430,7 +465,7 @@ async fn it_accepts_when_auth_prefix_has_correct_format_and_valid_jwt() {
 
 #[tokio::test]
 async fn it_accepts_when_auth_prefix_does_not_match_config_and_is_ignored() {
-    let test_harness = build_a_test_harness(None, None, false, true).await;
+    let test_harness = build_a_test_harness(None, None, false, true, false).await;
     // Let's create a request with our operation name
     let request_with_appropriate_name = supergraph::Request::canned_builder()
         .header(http::header::AUTHORIZATION, "Basic dXNlcjpwYXNzd29yZA==")
@@ -464,7 +499,7 @@ async fn it_accepts_when_auth_prefix_does_not_match_config_and_is_ignored() {
 
 #[tokio::test]
 async fn it_accepts_when_auth_prefix_has_correct_format_multiple_jwks_and_valid_jwt() {
-    let test_harness = build_a_test_harness(None, None, true, false).await;
+    let test_harness = build_a_test_harness(None, None, true, false, false).await;
 
     // Let's create a request with our operation name
     let request_with_appropriate_name = supergraph::Request::canned_builder()
@@ -503,7 +538,7 @@ async fn it_accepts_when_auth_prefix_has_correct_format_multiple_jwks_and_valid_
 #[tokio::test]
 async fn it_accepts_when_auth_prefix_has_correct_format_and_valid_jwt_custom_auth() {
     let test_harness =
-        build_a_test_harness(Some("SOMETHING".to_string()), None, false, false).await;
+        build_a_test_harness(Some("SOMETHING".to_string()), None, false, false, false).await;
 
     // Let's create a request with our operation name
     let request_with_appropriate_name = supergraph::Request::canned_builder()
@@ -542,7 +577,7 @@ async fn it_accepts_when_auth_prefix_has_correct_format_and_valid_jwt_custom_aut
 #[tokio::test]
 async fn it_accepts_when_auth_prefix_has_correct_format_and_valid_jwt_custom_prefix() {
     let test_harness =
-        build_a_test_harness(None, Some("SOMETHING".to_string()), false, false).await;
+        build_a_test_harness(None, Some("SOMETHING".to_string()), false, false, false).await;
 
     // Let's create a request with our operation name
     let request_with_appropriate_name = supergraph::Request::canned_builder()
@@ -580,7 +615,7 @@ async fn it_accepts_when_auth_prefix_has_correct_format_and_valid_jwt_custom_pre
 
 #[tokio::test]
 async fn it_accepts_when_no_auth_prefix_and_valid_jwt_custom_prefix() {
-    let test_harness = build_a_test_harness(None, Some("".to_string()), false, false).await;
+    let test_harness = build_a_test_harness(None, Some("".to_string()), false, false, false).await;
 
     // Let's create a request with our operation name
     let request_with_appropriate_name = supergraph::Request::canned_builder()
@@ -616,18 +651,211 @@ async fn it_accepts_when_no_auth_prefix_and_valid_jwt_custom_prefix() {
     assert_eq!(expected_mock_response_data, response.data.as_ref().unwrap());
 }
 
+#[tokio::test]
+async fn it_inserts_success_jwt_status_into_context() {
+    let test_harness = build_a_test_harness(None, None, false, false, false).await;
+
+    // Let's create a request with our operation name
+    let request_with_appropriate_name = supergraph::Request::canned_builder()
+        .header(
+            http::header::AUTHORIZATION,
+            "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImtleTEifQ.eyJleHAiOjEwMDAwMDAwMDAwLCJhbm90aGVyIGNsYWltIjoidGhpcyBpcyBhbm90aGVyIGNsYWltIn0.4GrmfxuUST96cs0YUC0DfLAG218m7vn8fO_ENfXnu5A",
+        )
+        .build()
+        .unwrap();
+
+    // ...And call our service stack with it
+    let mut service_response = test_harness
+        .oneshot(request_with_appropriate_name.try_into().unwrap())
+        .await
+        .unwrap();
+
+    let jwt_context = service_response
+        .context
+        .get::<_, JwtStatus>(JWT_CONTEXT_KEY)
+        .expect("deserialization succeeds")
+        .expect("a context value was set");
+
+    match jwt_context {
+        JwtStatus::Success { r#type, name } => {
+            assert_eq!(r#type, "header");
+            assert!(name.eq_ignore_ascii_case("Authorization"));
+        }
+        JwtStatus::Failure { .. } => panic!("expected a success but got {:?}", jwt_context),
+    }
+
+    let response: graphql::Response = serde_json::from_slice(
+        service_response
+            .next_response()
+            .await
+            .unwrap()
+            .unwrap()
+            .to_vec()
+            .as_slice(),
+    )
+    .unwrap();
+
+    assert_eq!(response.errors, vec![]);
+
+    assert_eq!(StatusCode::OK, service_response.response.status());
+
+    let expected_mock_response_data = "response created within the mock";
+    // with the expected message
+    assert_eq!(expected_mock_response_data, response.data.as_ref().unwrap());
+
+    let jwt_claims = service_response
+        .context
+        .get::<_, serde_json::Value>(APOLLO_AUTHENTICATION_JWT_CLAIMS)
+        .expect("deserialization succeeds")
+        .expect("a context value was set");
+
+    assert_eq!(
+        jwt_claims,
+        serde_json::json!({
+            "exp": 10_000_000_000i64,
+            "another claim": "this is another claim"
+        })
+    );
+}
+
+#[tokio::test]
+async fn it_inserts_failure_jwt_status_into_context() {
+    let test_harness = build_a_test_harness(None, None, false, false, false).await;
+
+    // Let's create a request with our operation name
+    let request_with_appropriate_name = supergraph::Request::canned_builder()
+        .header(
+            http::header::AUTHORIZATION,
+            "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImtleTEifQ.eyJleHAiOjEwMDAwMDAwMDAwLCJhbm90aGVyIGNsYWltIjoidGhpcyBpcyBhbm90aGVyIGNsYWltIn0.4GrmfxuUST96cs0YUC0DfLAG218m7vn8fO_ENfXnu5B",
+        )
+        .build()
+        .unwrap();
+
+    // ...And call our service stack with it
+    let mut service_response = test_harness
+        .oneshot(request_with_appropriate_name.try_into().unwrap())
+        .await
+        .unwrap();
+
+    let jwt_context = service_response
+        .context
+        .get::<_, JwtStatus>(JWT_CONTEXT_KEY)
+        .expect("deserialization succeeds")
+        .expect("a context value was set");
+
+    let error = jwt_context.error();
+    match error {
+        Some(err) => {
+            assert_eq!(err.code, "CANNOT_DECODE_JWT");
+            assert_eq!(err.message, "Cannot decode JWT: InvalidSignature");
+        }
+        None => panic!("expected an error"),
+    }
+
+    let response: graphql::Response = serde_json::from_slice(
+        service_response
+            .next_response()
+            .await
+            .unwrap()
+            .unwrap()
+            .to_vec()
+            .as_slice(),
+    )
+    .unwrap();
+
+    let expected_error = graphql::Error::builder()
+        .message("Cannot decode JWT: InvalidSignature")
+        .extension_code("AUTH_ERROR")
+        .build();
+
+    assert_eq!(response.errors, vec![expected_error]);
+
+    assert_eq!(StatusCode::UNAUTHORIZED, service_response.response.status());
+
+    let jwt_claims = service_response
+        .context
+        .get::<_, serde_json::Value>(APOLLO_AUTHENTICATION_JWT_CLAIMS)
+        .expect("deserialization succeeds");
+
+    assert!(
+        jwt_claims.is_none(),
+        "because the JWT was invalid, no claims should be set"
+    );
+}
+
+#[tokio::test]
+async fn it_moves_on_after_jwt_errors_when_configured() {
+    let test_harness = build_a_test_harness(None, None, false, false, true).await;
+
+    // Let's create a request with our operation name
+    let request_with_appropriate_name = supergraph::Request::canned_builder()
+        .header(
+            http::header::AUTHORIZATION,
+            "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImtleTEifQ.eyJleHAiOjEwMDAwMDAwMDAwLCJhbm90aGVyIGNsYWltIjoidGhpcyBpcyBhbm90aGVyIGNsYWltIn0.4GrmfxuUST96cs0YUC0DfLAG218m7vn8fO_ENfXnu5B",
+        )
+        .build()
+        .unwrap();
+
+    // ...And call our service stack with it
+    let mut service_response = test_harness
+        .oneshot(request_with_appropriate_name.try_into().unwrap())
+        .await
+        .unwrap();
+
+    let jwt_context = service_response
+        .context
+        .get::<_, JwtStatus>(JWT_CONTEXT_KEY)
+        .expect("deserialization succeeds")
+        .expect("a context value was set");
+
+    let error = jwt_context.error();
+    match error {
+        Some(err) => {
+            assert_eq!(err.code, "CANNOT_DECODE_JWT");
+            assert_eq!(err.message, "Cannot decode JWT: InvalidSignature");
+        }
+        None => panic!("expected an error"),
+    }
+
+    let response: graphql::Response = serde_json::from_slice(
+        service_response
+            .next_response()
+            .await
+            .unwrap()
+            .unwrap()
+            .to_vec()
+            .as_slice(),
+    )
+    .unwrap();
+
+    // JWT decode failure should be ignored
+    assert_eq!(response.errors, vec![]);
+
+    assert_eq!(StatusCode::OK, service_response.response.status());
+
+    let jwt_claims = service_response
+        .context
+        .get::<_, serde_json::Value>(APOLLO_AUTHENTICATION_JWT_CLAIMS)
+        .expect("deserialization succeeds");
+
+    assert!(
+        jwt_claims.is_none(),
+        "because the JWT was invalid, no claims should be set"
+    );
+}
+
 #[tokio::test]
 #[should_panic]
 async fn it_panics_when_auth_prefix_has_correct_format_but_contains_whitespace() {
     let _test_harness =
-        build_a_test_harness(None, Some("SOMET HING".to_string()), false, false).await;
+        build_a_test_harness(None, Some("SOMET HING".to_string()), false, false, false).await;
 }
 
 #[tokio::test]
 #[should_panic]
 async fn it_panics_when_auth_prefix_has_correct_format_but_contains_trailing_whitespace() {
     let _test_harness =
-        build_a_test_harness(None, Some("SOMETHING ".to_string()), false, false).await;
+        build_a_test_harness(None, Some("SOMETHING ".to_string()), false, false, false).await;
 }
 
 #[tokio::test]
@@ -997,7 +1225,7 @@ async fn issuer_check() {
         }
         ControlFlow::Continue(req) => {
             println!("got req with issuer check");
-            let claims: Value = req
+            let claims: serde_json::Value = req
                 .context
                 .get(APOLLO_AUTHENTICATION_JWT_CLAIMS)
                 .unwrap()
@@ -1036,7 +1264,7 @@ async fn issuer_check() {
         }
         ControlFlow::Continue(req) => {
             println!("got req with issuer check");
-            let claims: Value = req
+            let claims: serde_json::Value = req
                 .context
                 .get(APOLLO_AUTHENTICATION_JWT_CLAIMS)
                 .unwrap()
@@ -1109,7 +1337,7 @@ async fn issuer_check() {
         }
         ControlFlow::Continue(req) => {
             println!("got req with issuer check");
-            let claims: Value = req
+            let claims: serde_json::Value = req
                 .context
                 .get(APOLLO_AUTHENTICATION_JWT_CLAIMS)
                 .unwrap()
@@ -1173,7 +1401,7 @@ async fn it_rejects_and_accepts_keys_with_restricted_algorithms_and_unknown_jwks
 
     let jwks_manager = JwksManager::new(urls).await.unwrap();
 
-    // the JWT contains a HMAC key but we configured a restriction to RSA signing
+    // the JWT contains a HMAC key, but we configured a restriction to RSA signing
     let criteria = JWTCriteria {
         kid: None,
         alg: Algorithm::HS256,
@@ -1306,7 +1534,7 @@ async fn jwks_send_headers() {
                 gh.store(true, Ordering::Release);
             }
             http::Response::builder()
-                .header(CONTENT_TYPE, APPLICATION_JSON.essence_str())
+                .header(http::header::CONTENT_TYPE, APPLICATION_JSON.essence_str())
                 .status(StatusCode::OK)
                 .version(http::Version::HTTP_11)
                 .body::<RouterBody>(router::body::from_bytes(include_str!("testdata/jwks.json")))
diff --git a/apollo-router/src/router_factory.rs b/apollo-router/src/router_factory.rs
index 616665ce37..baf32bf583 100644
--- a/apollo-router/src/router_factory.rs
+++ b/apollo-router/src/router_factory.rs
@@ -795,9 +795,16 @@ pub(crate) async fn create_plugins(
             tracing::error!("{:#}", error);
         }
 
+        let errors_list = errors
+            .iter()
+            .map(ToString::to_string)
+            .collect::<Vec<String>>()
+            .join("\n");
+
         Err(BoxError::from(format!(
-            "there were {} configuration errors",
-            errors.len()
+            "there were {} configuration errors\n{}",
+            errors.len(),
+            errors_list
         )))
     } else {
         Ok(plugin_instances)
diff --git a/apollo-router/src/services/supergraph/service.rs b/apollo-router/src/services/supergraph/service.rs
index cc77506987..2c8b2db135 100644
--- a/apollo-router/src/services/supergraph/service.rs
+++ b/apollo-router/src/services/supergraph/service.rs
@@ -559,7 +559,7 @@ async fn subscription_task(
     let mut configuration_updated_rx = notify.subscribe_configuration();
     let mut schema_updated_rx = notify.subscribe_schema();
 
-    let expires_in = crate::plugins::authentication::jwt_expires_in(&supergraph_req.context);
+    let expires_in = crate::plugins::authentication::jwks::jwt_expires_in(&supergraph_req.context);
 
     let mut timeout = Box::pin(tokio::time::sleep(expires_in));
 
diff --git a/apollo-router/src/uplink/license_enforcement.rs b/apollo-router/src/uplink/license_enforcement.rs
index 53da3ab7a5..1407ba4c9a 100644
--- a/apollo-router/src/uplink/license_enforcement.rs
+++ b/apollo-router/src/uplink/license_enforcement.rs
@@ -33,7 +33,7 @@ use thiserror::Error;
 use url::Url;
 
 use crate::Configuration;
-use crate::plugins::authentication::convert_key_algorithm;
+use crate::plugins::authentication::jwks::convert_key_algorithm;
 use crate::spec::LINK_AS_ARGUMENT;
 use crate::spec::LINK_DIRECTIVE_NAME;
 use crate::spec::LINK_URL_ARGUMENT;
diff --git a/docs/source/routing/security/jwt.mdx b/docs/source/routing/security/jwt.mdx
index 1b9a8b394b..8ca2d8ec68 100644
--- a/docs/source/routing/security/jwt.mdx
+++ b/docs/source/routing/security/jwt.mdx
@@ -29,8 +29,9 @@ These are the high-level steps of JWT-based authentication with the GraphOS Rout
    - Your router obtains all of its known JWKs from URLs that you specify in its configuration file. Each URL provides its keys within a single JSON object called a [JWK Set](https://www.rfc-editor.org/rfc/rfc7517#section-5) (or a **JWKS**).
    - **If validation fails, the router rejects the request.** This can occur if the JWT is malformed, or if it's been expired for more than 60 seconds (this window accounts for synchronization issues).
 
-5. The router extracts all **claims** from the validated JWT and includes them in the request's context, making them available to your [router customizations](/router/customizations/overview/), such as Rhai scripts.
-6. Your customizations can handle the request differently depending on the details of the extracted claims, and/or you can propagate the claims to subgraphs to enable more granular access control.
+5. The router extracts all **claims** from the validated JWT and includes them in the request's context (`apollo::authentication::jwt_claims`), making them available to your [router customizations](/router/customizations/overview/), such as Rhai scripts.
+6. The router will insert the status of JWT processing into the request context (`apollo::authentication::jwt_status`). This status is informational and may be used for logging or debugging purposes.
+7. Your customizations can handle the request differently depending on the details of the extracted claims, and/or you can propagate the claims to subgraphs to enable more granular access control.
    - For examples, [see below](#working-with-jwt-claims).
 
 ## Turning it on
@@ -55,6 +56,7 @@ Otherwise, if you issue JWTs via a popular third-party IdP (Auth0, Okta, PingOne
          # These keys are optional. Default values are shown.
          header_name: Authorization
          header_value_prefix: Bearer
+         on_error: Error
          # array of alternative token sources
          sources:
            - type: header
@@ -149,6 +151,45 @@ The default value is `Bearer`.
 <tr>
 <td style="min-width: 150px;">
 
+##### `on_error`
+
+</td>
+<td>
+
+This setting controls the behavior of the router when an error occurs during JWT validation. Possible values are `Error` (default) and `Continue`.
+
+- `Error`: The router responds with an error when an error occurs during JWT validation.
+- `Continue`: The router continues processing the request when an error occurs during JWT validation. Requests with invalid JWTs will be treated as unauthenticated.
+
+Regardless of whether JWT authentication succeeds, the status of JWT processing is inserted into the request context (`apollo::authentication::jwt_status`). This status is informational and may be used for logging or debugging purposes.
+
+```js
+// On failure
+{
+    // Whether the JWT came from a header or cookie source
+    type: string,
+    // The name of the source's field
+    name: string,
+    // Error details
+    error: {
+        // A user-friendly error message
+        message: string,
+        // A machine-readable error code
+        code: string,
+        // The underlying reason for the error, if any
+        reason: string?
+    }
+}
+
+// On success
+{
+    // Whether the JWT came from a header or cookie source
+    type: string,
+    // The name of the source's field
+    name: string
+}
+```
+
 ##### `sources`
 
 </td>
@@ -204,7 +245,9 @@ The default value is `false`.
 After the GraphOS Router validates a client request's JWT, it adds that token's **claims** to the request's context at this key: `apollo::authentication::jwt_claims`
 
 > - If no JWT is present for a client request, this context value is the empty tuple, `()`.
-> - If a JWT _is_ present but validation of the JWT fails, the router _rejects_ the request.
+> - If a JWT _is_ present but validation of the JWT fails,
+>     - When `on_error` is set to `Error`, the router _rejects_ the request.
+>     - When `on_error` is set to `Continue`, the router _continues_ processing the request, and the context value is the empty tuple, `()`.
 
 If unauthenticated requests should be rejected, the router can be configured like this:
 
diff --git a/docs/source/routing/security/router-authentication.mdx b/docs/source/routing/security/router-authentication.mdx
index 9fb6a40fa6..cccb68e9b5 100644
--- a/docs/source/routing/security/router-authentication.mdx
+++ b/docs/source/routing/security/router-authentication.mdx
@@ -125,11 +125,12 @@ authentication:
   jwt:
     jwks:
       - url: https://dev-zzp5enui.us.auth0.com/.well-known/jwks.json
+    on_error: Error
 ```
 
 Pros:
 
-- The router prevents unauthenticated requests from reaching your subgraphs.
+- The router prevents unauthenticated requests from reaching your subgraphs (except when `on_error` is set to `Continue`.)
 - The router can extract claims from the JWT and pass them to your subgraphs as headers, reducing logic needed in your subgraphs.
 
 Cons:


Reviews:
user: login: BrynCooke
body: Minor config stuff.
Docs need to be added.
Shame to not use the new test harness, but I understand why. I'll refactor the tests separately.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T13:28:48Z
user: login: bnjjj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T09:49:28Z
user: login: BrynCooke
body: Docs need updating with new functionality here: 
https://github.com/apollographql/router/blob/dev/docs/source/routing/security/jwt.mdx

and possibly here: 
https://github.com/apollographql/router/blob/dev/docs/source/routing/security/router-authentication.mdx


state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T11:37:25Z
user: login: Velfi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T14:31:24Z
user: login: Velfi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T15:11:11Z
user: login: Velfi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T17:02:38Z
user: login: BrynCooke
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T09:04:35Z
user: login: bnjjj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T09:38:01Z
user: login: bnjjj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T09:38:27Z
user: login: bnjjj
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T09:39:59Z

Review comments:
diff_hunk: @@ -150,6 +98,13 @@ struct JWTConf {
     /// Alternative sources to extract the JWT
     #[serde(default)]
     sources: Vec<Source>,
+    /// Control the behavior when an error occurs during the authentication process.
+    ///
+    /// Defaults to `Error`. When set to `Continue`, requests that fail JWT authentication will
+    /// continue to be processed by the router, but without the JWT claims in the context. When set
+    /// to `Error`, requests that fail JWT authentication will be rejected with a HTTP 403 error.
+    #[serde(default = "OnError::default")]
path: apollo-router/src/plugins/authentication/mod.rs
user: login: BrynCooke
body: If JWTConf and OnError implementsDefault you don't need this
```suggestion
```
Same for the other fields
created_at: 2025-03-13T13:26:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -133,7 +74,14 @@ struct AuthenticationPlugin {
     connector: Option<ConnectorAuth>,
 }
 
-#[derive(Clone, Debug, Deserialize, JsonSchema, serde_derive_default::Default)]
+#[derive(Clone, Debug, Deserialize, JsonSchema, Default, PartialEq)]
+enum OnError {
+    Continue,
+    #[default]
+    Error,
+}
+
+#[derive(Clone, Debug, Deserialize, JsonSchema, Default)]
path: apollo-router/src/plugins/authentication/mod.rs
user: login: BrynCooke
body: We should implement Default for this manually
created_at: 2025-03-13T13:27:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -256,3 +273,419 @@ impl Iterator for Iter<'_> {
         }
     }
 }
+
+#[derive(Debug, Default)]
+pub(super) struct JWTCriteria {
+    pub(super) alg: Algorithm,
+    pub(super) kid: Option<String>,
+}
+
+/// Search the list of JWKS to find a key we can use to decode a JWT.
+///
+/// The search criteria allow us to match a variety of keys depending on which criteria are provided
+/// by the JWT header. The only mandatory parameter is "alg".
+/// Note: "none" is not implemented by jsonwebtoken, so it can't be part of the [`Algorithm`] enum.
+pub(super) fn search_jwks(
+    jwks_manager: &JwksManager,
+    criteria: &JWTCriteria,
+) -> Option<Vec<(Option<String>, Jwk)>> {
+    const HIGHEST_SCORE: usize = 2;
+    let mut candidates = vec![];
+    let mut found_highest_score = false;
+    for JwkSetInfo {
+        jwks,
+        issuer,
+        algorithms,
+    } in jwks_manager.iter_jwks()
+    {
+        // filter accepted algorithms
+        if let Some(algs) = algorithms {
+            if !algs.contains(&criteria.alg) {
+                continue;
+            }
+        }
+
+        // Try to figure out if our jwks contains a candidate key (i.e.: a key which matches our
+        // criteria)
+        for mut key in jwks.keys.into_iter().filter(|key| {
+            // We are only interested in keys which are used for signature verification
+            match (&key.common.public_key_use, &key.common.key_operations) {
+                // "use" https://datatracker.ietf.org/doc/html/rfc7517#section-4.2 and
+                // "key_ops" https://datatracker.ietf.org/doc/html/rfc7517#section-4.3 are both optional
+                (None, None) => true,
+                (None, Some(purpose)) => purpose.contains(&KeyOperations::Verify),
+                (Some(key_use), None) => key_use == &PublicKeyUse::Signature,
+                // The "use" and "key_ops" JWK members SHOULD NOT be used together;
+                // however, if both are used, the information they convey MUST be
+                // consistent
+                (Some(key_use), Some(purpose)) => {
+                    key_use == &PublicKeyUse::Signature && purpose.contains(&KeyOperations::Verify)
+                }
+            }
+        }) {
+            let mut key_score = 0;
+
+            // Let's see if we have a specified kid and if they match
+            if criteria.kid.is_some() && key.common.key_id == criteria.kid {
+                key_score += 1;
+            }
+
+            // Furthermore, we would like our algorithms to match, or at least the kty
+            // If we have an algorithm that matches, boost the score
+            match key.common.key_algorithm {
+                Some(algorithm) => {
+                    if convert_key_algorithm(algorithm) != Some(criteria.alg) {
+                        continue;
+                    }
+                    key_score += 1;
+                }
+                // If a key doesn't have an algorithm, then we match the "alg" specified in the
+                // search criteria against all of the algorithms that we support.  If the
+                // key.algorithm parameters match the type of parameters for the "family" of the
+                // criteria "alg", then we update the key to use the value of "alg" provided in
+                // the search criteria.
+                // If not, then this is not a usable key for this JWT
+                // Note: Matching algorithm parameters may seem unusual, but the appropriate
+                // algorithm details are not structured for easy consumption in jsonwebtoken and
+                // this is the simplest way to determine algorithm family.
+                None => match (criteria.alg, &key.algorithm) {
+                    (
+                        Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512,
+                        AlgorithmParameters::OctetKey(_),
+                    ) => {
+                        key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                    }
+                    (
+                        Algorithm::RS256
+                        | Algorithm::RS384
+                        | Algorithm::RS512
+                        | Algorithm::PS256
+                        | Algorithm::PS384
+                        | Algorithm::PS512,
+                        AlgorithmParameters::RSA(_),
+                    ) => {
+                        key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                    }
+                    (Algorithm::ES256, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::P256 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    (Algorithm::ES384, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::P384 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    (Algorithm::EdDSA, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::Ed25519 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    _ => {
+                        // We'll ignore combinations we don't recognise
+                        continue;
+                    }
+                },
+            };
+
+            // If we get here we have a key that:
+            //  - may be used for signature verification
+            //  - has a matching algorithm, or if JWT has no algorithm, a matching key type
+            // It may have a matching kid if the JWT has a kid and it matches the key kid
+            //
+            // Multiple keys may meet the matching criteria, but they have a score. They get 1
+            // point for having an explicitly matching algorithm and 1 point for an explicitly
+            // matching kid. We will sort our candidates and pick the key with the highest score.
+
+            // If we find a key with a HIGHEST_SCORE, we will filter the list to only keep those
+            // with that score
+            if key_score == HIGHEST_SCORE {
+                found_highest_score = true;
+            }
+
+            candidates.push((key_score, (issuer.clone(), key)));
+        }
+    }
+
+    tracing::debug!(
+        "jwk candidates: {:?}",
+        candidates
+            .iter()
+            .map(|(score, (_, candidate))| (
+                score,
+                &candidate.common.key_id,
+                candidate.common.key_algorithm
+            ))
+            .collect::<Vec<(&usize, &Option<String>, Option<KeyAlgorithm>)>>()
+    );
+
+    if candidates.is_empty() {
+        None
+    } else {
+        // Only sort if we need to
+        if candidates.len() > 1 {
+            candidates.sort_by(|a, b| a.0.cmp(&b.0));
+        }
+
+        if found_highest_score {
+            Some(
+                candidates
+                    .into_iter()
+                    .filter_map(|(score, candidate)| {
+                        if score == HIGHEST_SCORE {
+                            Some(candidate)
+                        } else {
+                            None
+                        }
+                    })
+                    .collect(),
+            )
+        } else {
+            Some(
+                candidates
+                    .into_iter()
+                    .map(|(_score, candidate)| candidate)
+                    .collect(),
+            )
+        }
+    }
+}
+
+pub(super) fn extract_jwt<'a, 'b: 'a>(
+    source: &'a Source,
+    ignore_other_prefixes: bool,
+    headers: &'b HeaderMap,
+) -> Option<Result<&'b str, AuthenticationError>> {
+    match source {
+        Source::Header { name, value_prefix } => {
+            // The http_request is stored in a `Router::Request` context.
+            // We are going to check the headers for the presence of the configured header
+            let jwt_value_result = match headers.get(name) {
+                Some(value) => value.to_str(),
+                None => return None,
+            };
path: apollo-router/src/plugins/authentication/jwks.rs
user: login: bnjjj
body: NIT: 
```suggestion
            let jwt_value_result = headers.get(name)?.to_str();
```
created_at: 2025-03-17T09:21:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -256,3 +273,419 @@ impl Iterator for Iter<'_> {
         }
     }
 }
+
+#[derive(Debug, Default)]
+pub(super) struct JWTCriteria {
+    pub(super) alg: Algorithm,
+    pub(super) kid: Option<String>,
+}
+
+/// Search the list of JWKS to find a key we can use to decode a JWT.
+///
+/// The search criteria allow us to match a variety of keys depending on which criteria are provided
+/// by the JWT header. The only mandatory parameter is "alg".
+/// Note: "none" is not implemented by jsonwebtoken, so it can't be part of the [`Algorithm`] enum.
+pub(super) fn search_jwks(
+    jwks_manager: &JwksManager,
+    criteria: &JWTCriteria,
+) -> Option<Vec<(Option<String>, Jwk)>> {
+    const HIGHEST_SCORE: usize = 2;
+    let mut candidates = vec![];
+    let mut found_highest_score = false;
+    for JwkSetInfo {
+        jwks,
+        issuer,
+        algorithms,
+    } in jwks_manager.iter_jwks()
+    {
+        // filter accepted algorithms
+        if let Some(algs) = algorithms {
+            if !algs.contains(&criteria.alg) {
+                continue;
+            }
+        }
+
+        // Try to figure out if our jwks contains a candidate key (i.e.: a key which matches our
+        // criteria)
+        for mut key in jwks.keys.into_iter().filter(|key| {
+            // We are only interested in keys which are used for signature verification
+            match (&key.common.public_key_use, &key.common.key_operations) {
+                // "use" https://datatracker.ietf.org/doc/html/rfc7517#section-4.2 and
+                // "key_ops" https://datatracker.ietf.org/doc/html/rfc7517#section-4.3 are both optional
+                (None, None) => true,
+                (None, Some(purpose)) => purpose.contains(&KeyOperations::Verify),
+                (Some(key_use), None) => key_use == &PublicKeyUse::Signature,
+                // The "use" and "key_ops" JWK members SHOULD NOT be used together;
+                // however, if both are used, the information they convey MUST be
+                // consistent
+                (Some(key_use), Some(purpose)) => {
+                    key_use == &PublicKeyUse::Signature && purpose.contains(&KeyOperations::Verify)
+                }
+            }
+        }) {
+            let mut key_score = 0;
+
+            // Let's see if we have a specified kid and if they match
+            if criteria.kid.is_some() && key.common.key_id == criteria.kid {
+                key_score += 1;
+            }
+
+            // Furthermore, we would like our algorithms to match, or at least the kty
+            // If we have an algorithm that matches, boost the score
+            match key.common.key_algorithm {
+                Some(algorithm) => {
+                    if convert_key_algorithm(algorithm) != Some(criteria.alg) {
+                        continue;
+                    }
+                    key_score += 1;
+                }
+                // If a key doesn't have an algorithm, then we match the "alg" specified in the
+                // search criteria against all of the algorithms that we support.  If the
+                // key.algorithm parameters match the type of parameters for the "family" of the
+                // criteria "alg", then we update the key to use the value of "alg" provided in
+                // the search criteria.
+                // If not, then this is not a usable key for this JWT
+                // Note: Matching algorithm parameters may seem unusual, but the appropriate
+                // algorithm details are not structured for easy consumption in jsonwebtoken and
+                // this is the simplest way to determine algorithm family.
+                None => match (criteria.alg, &key.algorithm) {
+                    (
+                        Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512,
+                        AlgorithmParameters::OctetKey(_),
+                    ) => {
+                        key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                    }
+                    (
+                        Algorithm::RS256
+                        | Algorithm::RS384
+                        | Algorithm::RS512
+                        | Algorithm::PS256
+                        | Algorithm::PS384
+                        | Algorithm::PS512,
+                        AlgorithmParameters::RSA(_),
+                    ) => {
+                        key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                    }
+                    (Algorithm::ES256, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::P256 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    (Algorithm::ES384, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::P384 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    (Algorithm::EdDSA, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::Ed25519 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    _ => {
+                        // We'll ignore combinations we don't recognise
+                        continue;
+                    }
+                },
+            };
+
+            // If we get here we have a key that:
+            //  - may be used for signature verification
+            //  - has a matching algorithm, or if JWT has no algorithm, a matching key type
+            // It may have a matching kid if the JWT has a kid and it matches the key kid
+            //
+            // Multiple keys may meet the matching criteria, but they have a score. They get 1
+            // point for having an explicitly matching algorithm and 1 point for an explicitly
+            // matching kid. We will sort our candidates and pick the key with the highest score.
+
+            // If we find a key with a HIGHEST_SCORE, we will filter the list to only keep those
+            // with that score
+            if key_score == HIGHEST_SCORE {
+                found_highest_score = true;
+            }
+
+            candidates.push((key_score, (issuer.clone(), key)));
+        }
+    }
+
+    tracing::debug!(
+        "jwk candidates: {:?}",
+        candidates
+            .iter()
+            .map(|(score, (_, candidate))| (
+                score,
+                &candidate.common.key_id,
+                candidate.common.key_algorithm
+            ))
+            .collect::<Vec<(&usize, &Option<String>, Option<KeyAlgorithm>)>>()
+    );
+
+    if candidates.is_empty() {
+        None
+    } else {
+        // Only sort if we need to
+        if candidates.len() > 1 {
+            candidates.sort_by(|a, b| a.0.cmp(&b.0));
+        }
+
+        if found_highest_score {
+            Some(
+                candidates
+                    .into_iter()
+                    .filter_map(|(score, candidate)| {
+                        if score == HIGHEST_SCORE {
+                            Some(candidate)
+                        } else {
+                            None
+                        }
+                    })
+                    .collect(),
+            )
+        } else {
+            Some(
+                candidates
+                    .into_iter()
+                    .map(|(_score, candidate)| candidate)
+                    .collect(),
+            )
+        }
+    }
+}
+
+pub(super) fn extract_jwt<'a, 'b: 'a>(
path: apollo-router/src/plugins/authentication/jwks.rs
user: login: bnjjj
body: I think it would be useful to document what's the meaning of the return type here. Like what does that mean when it's `None` or `Some(Err)`  and so one 
created_at: 2025-03-17T09:41:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -256,3 +273,419 @@ impl Iterator for Iter<'_> {
         }
     }
 }
+
+#[derive(Debug, Default)]
+pub(super) struct JWTCriteria {
+    pub(super) alg: Algorithm,
+    pub(super) kid: Option<String>,
+}
+
+/// Search the list of JWKS to find a key we can use to decode a JWT.
+///
+/// The search criteria allow us to match a variety of keys depending on which criteria are provided
+/// by the JWT header. The only mandatory parameter is "alg".
+/// Note: "none" is not implemented by jsonwebtoken, so it can't be part of the [`Algorithm`] enum.
+pub(super) fn search_jwks(
+    jwks_manager: &JwksManager,
+    criteria: &JWTCriteria,
+) -> Option<Vec<(Option<String>, Jwk)>> {
+    const HIGHEST_SCORE: usize = 2;
+    let mut candidates = vec![];
+    let mut found_highest_score = false;
+    for JwkSetInfo {
+        jwks,
+        issuer,
+        algorithms,
+    } in jwks_manager.iter_jwks()
+    {
+        // filter accepted algorithms
+        if let Some(algs) = algorithms {
+            if !algs.contains(&criteria.alg) {
+                continue;
+            }
+        }
+
+        // Try to figure out if our jwks contains a candidate key (i.e.: a key which matches our
+        // criteria)
+        for mut key in jwks.keys.into_iter().filter(|key| {
+            // We are only interested in keys which are used for signature verification
+            match (&key.common.public_key_use, &key.common.key_operations) {
+                // "use" https://datatracker.ietf.org/doc/html/rfc7517#section-4.2 and
+                // "key_ops" https://datatracker.ietf.org/doc/html/rfc7517#section-4.3 are both optional
+                (None, None) => true,
+                (None, Some(purpose)) => purpose.contains(&KeyOperations::Verify),
+                (Some(key_use), None) => key_use == &PublicKeyUse::Signature,
+                // The "use" and "key_ops" JWK members SHOULD NOT be used together;
+                // however, if both are used, the information they convey MUST be
+                // consistent
+                (Some(key_use), Some(purpose)) => {
+                    key_use == &PublicKeyUse::Signature && purpose.contains(&KeyOperations::Verify)
+                }
+            }
+        }) {
+            let mut key_score = 0;
+
+            // Let's see if we have a specified kid and if they match
+            if criteria.kid.is_some() && key.common.key_id == criteria.kid {
+                key_score += 1;
+            }
+
+            // Furthermore, we would like our algorithms to match, or at least the kty
+            // If we have an algorithm that matches, boost the score
+            match key.common.key_algorithm {
+                Some(algorithm) => {
+                    if convert_key_algorithm(algorithm) != Some(criteria.alg) {
+                        continue;
+                    }
+                    key_score += 1;
+                }
+                // If a key doesn't have an algorithm, then we match the "alg" specified in the
+                // search criteria against all of the algorithms that we support.  If the
+                // key.algorithm parameters match the type of parameters for the "family" of the
+                // criteria "alg", then we update the key to use the value of "alg" provided in
+                // the search criteria.
+                // If not, then this is not a usable key for this JWT
+                // Note: Matching algorithm parameters may seem unusual, but the appropriate
+                // algorithm details are not structured for easy consumption in jsonwebtoken and
+                // this is the simplest way to determine algorithm family.
+                None => match (criteria.alg, &key.algorithm) {
+                    (
+                        Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512,
+                        AlgorithmParameters::OctetKey(_),
+                    ) => {
+                        key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                    }
+                    (
+                        Algorithm::RS256
+                        | Algorithm::RS384
+                        | Algorithm::RS512
+                        | Algorithm::PS256
+                        | Algorithm::PS384
+                        | Algorithm::PS512,
+                        AlgorithmParameters::RSA(_),
+                    ) => {
+                        key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                    }
+                    (Algorithm::ES256, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::P256 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    (Algorithm::ES384, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::P384 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    (Algorithm::EdDSA, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::Ed25519 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    _ => {
+                        // We'll ignore combinations we don't recognise
+                        continue;
+                    }
+                },
+            };
+
+            // If we get here we have a key that:
+            //  - may be used for signature verification
+            //  - has a matching algorithm, or if JWT has no algorithm, a matching key type
+            // It may have a matching kid if the JWT has a kid and it matches the key kid
+            //
+            // Multiple keys may meet the matching criteria, but they have a score. They get 1
+            // point for having an explicitly matching algorithm and 1 point for an explicitly
+            // matching kid. We will sort our candidates and pick the key with the highest score.
+
+            // If we find a key with a HIGHEST_SCORE, we will filter the list to only keep those
+            // with that score
+            if key_score == HIGHEST_SCORE {
+                found_highest_score = true;
+            }
+
+            candidates.push((key_score, (issuer.clone(), key)));
+        }
+    }
+
+    tracing::debug!(
+        "jwk candidates: {:?}",
+        candidates
+            .iter()
+            .map(|(score, (_, candidate))| (
+                score,
+                &candidate.common.key_id,
+                candidate.common.key_algorithm
+            ))
+            .collect::<Vec<(&usize, &Option<String>, Option<KeyAlgorithm>)>>()
+    );
+
+    if candidates.is_empty() {
+        None
+    } else {
+        // Only sort if we need to
+        if candidates.len() > 1 {
+            candidates.sort_by(|a, b| a.0.cmp(&b.0));
+        }
+
+        if found_highest_score {
+            Some(
+                candidates
+                    .into_iter()
+                    .filter_map(|(score, candidate)| {
+                        if score == HIGHEST_SCORE {
+                            Some(candidate)
+                        } else {
+                            None
+                        }
+                    })
+                    .collect(),
+            )
+        } else {
+            Some(
+                candidates
+                    .into_iter()
+                    .map(|(_score, candidate)| candidate)
+                    .collect(),
+            )
+        }
+    }
+}
+
+pub(super) fn extract_jwt<'a, 'b: 'a>(
+    source: &'a Source,
+    ignore_other_prefixes: bool,
+    headers: &'b HeaderMap,
+) -> Option<Result<&'b str, AuthenticationError>> {
+    match source {
+        Source::Header { name, value_prefix } => {
+            // The http_request is stored in a `Router::Request` context.
+            // We are going to check the headers for the presence of the configured header
+            let jwt_value_result = match headers.get(name) {
+                Some(value) => value.to_str(),
+                None => return None,
+            };
+
+            // If we find the header, but can't convert it to a string, let the client know
+            let jwt_value_untrimmed = match jwt_value_result {
+                Ok(value) => value,
+                Err(_not_a_string_error) => {
+                    return Some(Err(AuthenticationError::CannotConvertToString));
+                }
+            };
+
+            // Let's trim out leading and trailing whitespace to be accommodating
+            let jwt_value = jwt_value_untrimmed.trim();
+
+            // Make sure the format of our message matches our expectations
+            // Technically, the spec is case-sensitive, but let's accept
+            // case variations
+            //
+            let prefix_len = value_prefix.len();
+            if jwt_value.len() < prefix_len
+                || !&jwt_value[..prefix_len].eq_ignore_ascii_case(value_prefix)
+            {
+                return if ignore_other_prefixes {
+                    None
+                } else {
+                    Some(Err(AuthenticationError::InvalidPrefix(
+                        jwt_value_untrimmed.to_owned(),
+                        value_prefix.to_owned(),
+                    )))
+                };
+            }
+            // If there's no header prefix, we avoid splitting the header
+            let jwt = if value_prefix.is_empty() {
+                // check for whitespace — we've already trimmed, so this means the request has a
+                // prefix that shouldn't exist
+                if jwt_value.contains(' ') {
+                    return Some(Err(AuthenticationError::InvalidPrefix(
+                        jwt_value_untrimmed.to_owned(),
+                        value_prefix.to_owned(),
+                    )));
+                }
+
+                // we can simply assign the jwt to the jwt_value; we'll validate down below
+                jwt_value
+            } else {
+                // Otherwise, we need to split our string in (at most 2) sections.
+                let jwt_parts: Vec<&str> = jwt_value.splitn(2, ' ').collect();
+                if jwt_parts.len() != 2 {
+                    return Some(Err(AuthenticationError::MissingJWT(jwt_value.to_owned())));
+                }
+
+                // We have our jwt
+                jwt_parts[1]
+            };
+            Some(Ok(jwt))
+        }
+        Source::Cookie { name } => {
+            for header in headers.get_all("cookie") {
+                let value = match header.to_str() {
+                    Ok(value) => value,
+                    Err(_not_a_string_error) => {
+                        return Some(Err(AuthenticationError::CannotConvertToString));
+                    }
+                };
+                for cookie in cookie::Cookie::split_parse(value) {
+                    match cookie {
+                        Err(_) => continue,
+                        Ok(cookie) => {
+                            if cookie.name() == name {
+                                if let Some(value) = cookie.value_raw() {
+                                    return Some(Ok(value));
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            None
+        }
+    }
+}
+
+pub(super) fn decode_jwt(
+    jwt: &str,
+    keys: Vec<(Option<String>, Jwk)>,
+    criteria: JWTCriteria,
+) -> Result<(Option<String>, TokenData<serde_json::Value>), (AuthenticationError, StatusCode)> {
+    let mut error = None;
+    for (issuer, jwk) in keys.into_iter() {
+        let decoding_key = match DecodingKey::from_jwk(&jwk) {
+            Ok(k) => k,
+            Err(e) => {
+                error = Some((
+                    AuthenticationError::CannotCreateDecodingKey(e),
+                    StatusCode::INTERNAL_SERVER_ERROR,
+                ));
+                continue;
+            }
+        };
+
+        let key_algorithm = match jwk.common.key_algorithm {
+            Some(a) => a,
+            None => {
+                error = Some((
+                    AuthenticationError::JWKHasNoAlgorithm,
+                    StatusCode::INTERNAL_SERVER_ERROR,
+                ));
+                continue;
+            }
+        };
+
+        let algorithm = match convert_key_algorithm(key_algorithm) {
+            Some(a) => a,
+            None => {
+                error = Some((
+                    AuthenticationError::UnsupportedKeyAlgorithm(key_algorithm),
+                    StatusCode::INTERNAL_SERVER_ERROR,
+                ));
+                continue;
+            }
+        };
+
+        let mut validation = Validation::new(algorithm);
+        validation.validate_nbf = true;
+        // if set to true, it will reject tokens containing an `aud` claim if the validation does not specify an audience
+        // we don't validate audience yet, so this is deactivated
+        validation.validate_aud = false;
+
+        match decode::<serde_json::Value>(jwt, &decoding_key, &validation) {
+            Ok(v) => return Ok((issuer, v)),
+            Err(e) => {
+                tracing::trace!("JWT decoding failed with error `{e}`");
+                error = Some((
+                    AuthenticationError::CannotDecodeJWT(e),
+                    StatusCode::UNAUTHORIZED,
+                ));
path: apollo-router/src/plugins/authentication/jwks.rs
user: login: bnjjj
body: `error` could potentially be overridden by another one here. Is it expected ? Should we have a Vec of error instead to keep all of them ?
created_at: 2025-03-17T09:42:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -256,3 +273,419 @@ impl Iterator for Iter<'_> {
         }
     }
 }
+
+#[derive(Debug, Default)]
+pub(super) struct JWTCriteria {
+    pub(super) alg: Algorithm,
+    pub(super) kid: Option<String>,
+}
+
+/// Search the list of JWKS to find a key we can use to decode a JWT.
+///
+/// The search criteria allow us to match a variety of keys depending on which criteria are provided
+/// by the JWT header. The only mandatory parameter is "alg".
+/// Note: "none" is not implemented by jsonwebtoken, so it can't be part of the [`Algorithm`] enum.
+pub(super) fn search_jwks(
+    jwks_manager: &JwksManager,
+    criteria: &JWTCriteria,
+) -> Option<Vec<(Option<String>, Jwk)>> {
+    const HIGHEST_SCORE: usize = 2;
+    let mut candidates = vec![];
+    let mut found_highest_score = false;
+    for JwkSetInfo {
+        jwks,
+        issuer,
+        algorithms,
+    } in jwks_manager.iter_jwks()
+    {
+        // filter accepted algorithms
+        if let Some(algs) = algorithms {
+            if !algs.contains(&criteria.alg) {
+                continue;
+            }
+        }
+
+        // Try to figure out if our jwks contains a candidate key (i.e.: a key which matches our
+        // criteria)
+        for mut key in jwks.keys.into_iter().filter(|key| {
+            // We are only interested in keys which are used for signature verification
+            match (&key.common.public_key_use, &key.common.key_operations) {
+                // "use" https://datatracker.ietf.org/doc/html/rfc7517#section-4.2 and
+                // "key_ops" https://datatracker.ietf.org/doc/html/rfc7517#section-4.3 are both optional
+                (None, None) => true,
+                (None, Some(purpose)) => purpose.contains(&KeyOperations::Verify),
+                (Some(key_use), None) => key_use == &PublicKeyUse::Signature,
+                // The "use" and "key_ops" JWK members SHOULD NOT be used together;
+                // however, if both are used, the information they convey MUST be
+                // consistent
+                (Some(key_use), Some(purpose)) => {
+                    key_use == &PublicKeyUse::Signature && purpose.contains(&KeyOperations::Verify)
+                }
+            }
+        }) {
+            let mut key_score = 0;
+
+            // Let's see if we have a specified kid and if they match
+            if criteria.kid.is_some() && key.common.key_id == criteria.kid {
+                key_score += 1;
+            }
+
+            // Furthermore, we would like our algorithms to match, or at least the kty
+            // If we have an algorithm that matches, boost the score
+            match key.common.key_algorithm {
+                Some(algorithm) => {
+                    if convert_key_algorithm(algorithm) != Some(criteria.alg) {
+                        continue;
+                    }
+                    key_score += 1;
+                }
+                // If a key doesn't have an algorithm, then we match the "alg" specified in the
+                // search criteria against all of the algorithms that we support.  If the
+                // key.algorithm parameters match the type of parameters for the "family" of the
+                // criteria "alg", then we update the key to use the value of "alg" provided in
+                // the search criteria.
+                // If not, then this is not a usable key for this JWT
+                // Note: Matching algorithm parameters may seem unusual, but the appropriate
+                // algorithm details are not structured for easy consumption in jsonwebtoken and
+                // this is the simplest way to determine algorithm family.
+                None => match (criteria.alg, &key.algorithm) {
+                    (
+                        Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512,
+                        AlgorithmParameters::OctetKey(_),
+                    ) => {
+                        key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                    }
+                    (
+                        Algorithm::RS256
+                        | Algorithm::RS384
+                        | Algorithm::RS512
+                        | Algorithm::PS256
+                        | Algorithm::PS384
+                        | Algorithm::PS512,
+                        AlgorithmParameters::RSA(_),
+                    ) => {
+                        key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                    }
+                    (Algorithm::ES256, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::P256 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    (Algorithm::ES384, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::P384 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    (Algorithm::EdDSA, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::Ed25519 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    _ => {
+                        // We'll ignore combinations we don't recognise
+                        continue;
+                    }
+                },
+            };
+
+            // If we get here we have a key that:
+            //  - may be used for signature verification
+            //  - has a matching algorithm, or if JWT has no algorithm, a matching key type
+            // It may have a matching kid if the JWT has a kid and it matches the key kid
+            //
+            // Multiple keys may meet the matching criteria, but they have a score. They get 1
+            // point for having an explicitly matching algorithm and 1 point for an explicitly
+            // matching kid. We will sort our candidates and pick the key with the highest score.
+
+            // If we find a key with a HIGHEST_SCORE, we will filter the list to only keep those
+            // with that score
+            if key_score == HIGHEST_SCORE {
+                found_highest_score = true;
+            }
+
+            candidates.push((key_score, (issuer.clone(), key)));
+        }
+    }
+
+    tracing::debug!(
+        "jwk candidates: {:?}",
+        candidates
+            .iter()
+            .map(|(score, (_, candidate))| (
+                score,
+                &candidate.common.key_id,
+                candidate.common.key_algorithm
+            ))
+            .collect::<Vec<(&usize, &Option<String>, Option<KeyAlgorithm>)>>()
+    );
+
+    if candidates.is_empty() {
+        None
+    } else {
+        // Only sort if we need to
+        if candidates.len() > 1 {
+            candidates.sort_by(|a, b| a.0.cmp(&b.0));
+        }
+
+        if found_highest_score {
+            Some(
+                candidates
+                    .into_iter()
+                    .filter_map(|(score, candidate)| {
+                        if score == HIGHEST_SCORE {
+                            Some(candidate)
+                        } else {
+                            None
+                        }
+                    })
+                    .collect(),
+            )
+        } else {
+            Some(
+                candidates
+                    .into_iter()
+                    .map(|(_score, candidate)| candidate)
+                    .collect(),
+            )
+        }
+    }
+}
+
+pub(super) fn extract_jwt<'a, 'b: 'a>(
+    source: &'a Source,
+    ignore_other_prefixes: bool,
+    headers: &'b HeaderMap,
+) -> Option<Result<&'b str, AuthenticationError>> {
+    match source {
+        Source::Header { name, value_prefix } => {
+            // The http_request is stored in a `Router::Request` context.
+            // We are going to check the headers for the presence of the configured header
+            let jwt_value_result = match headers.get(name) {
+                Some(value) => value.to_str(),
+                None => return None,
+            };
+
+            // If we find the header, but can't convert it to a string, let the client know
+            let jwt_value_untrimmed = match jwt_value_result {
+                Ok(value) => value,
+                Err(_not_a_string_error) => {
+                    return Some(Err(AuthenticationError::CannotConvertToString));
+                }
+            };
+
+            // Let's trim out leading and trailing whitespace to be accommodating
+            let jwt_value = jwt_value_untrimmed.trim();
+
+            // Make sure the format of our message matches our expectations
+            // Technically, the spec is case-sensitive, but let's accept
+            // case variations
+            //
+            let prefix_len = value_prefix.len();
+            if jwt_value.len() < prefix_len
+                || !&jwt_value[..prefix_len].eq_ignore_ascii_case(value_prefix)
+            {
+                return if ignore_other_prefixes {
+                    None
+                } else {
+                    Some(Err(AuthenticationError::InvalidPrefix(
+                        jwt_value_untrimmed.to_owned(),
+                        value_prefix.to_owned(),
+                    )))
+                };
+            }
+            // If there's no header prefix, we avoid splitting the header
+            let jwt = if value_prefix.is_empty() {
+                // check for whitespace — we've already trimmed, so this means the request has a
+                // prefix that shouldn't exist
+                if jwt_value.contains(' ') {
+                    return Some(Err(AuthenticationError::InvalidPrefix(
+                        jwt_value_untrimmed.to_owned(),
+                        value_prefix.to_owned(),
+                    )));
+                }
+
+                // we can simply assign the jwt to the jwt_value; we'll validate down below
+                jwt_value
+            } else {
+                // Otherwise, we need to split our string in (at most 2) sections.
+                let jwt_parts: Vec<&str> = jwt_value.splitn(2, ' ').collect();
+                if jwt_parts.len() != 2 {
+                    return Some(Err(AuthenticationError::MissingJWT(jwt_value.to_owned())));
+                }
+
+                // We have our jwt
+                jwt_parts[1]
+            };
+            Some(Ok(jwt))
+        }
+        Source::Cookie { name } => {
+            for header in headers.get_all("cookie") {
+                let value = match header.to_str() {
+                    Ok(value) => value,
+                    Err(_not_a_string_error) => {
+                        return Some(Err(AuthenticationError::CannotConvertToString));
+                    }
+                };
+                for cookie in cookie::Cookie::split_parse(value) {
+                    match cookie {
+                        Err(_) => continue,
+                        Ok(cookie) => {
+                            if cookie.name() == name {
+                                if let Some(value) = cookie.value_raw() {
+                                    return Some(Ok(value));
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            None
+        }
+    }
+}
+
+pub(super) fn decode_jwt(
+    jwt: &str,
+    keys: Vec<(Option<String>, Jwk)>,
+    criteria: JWTCriteria,
+) -> Result<(Option<String>, TokenData<serde_json::Value>), (AuthenticationError, StatusCode)> {
+    let mut error = None;
+    for (issuer, jwk) in keys.into_iter() {
+        let decoding_key = match DecodingKey::from_jwk(&jwk) {
+            Ok(k) => k,
+            Err(e) => {
+                error = Some((
+                    AuthenticationError::CannotCreateDecodingKey(e),
+                    StatusCode::INTERNAL_SERVER_ERROR,
+                ));
+                continue;
+            }
+        };
+
+        let key_algorithm = match jwk.common.key_algorithm {
+            Some(a) => a,
+            None => {
+                error = Some((
+                    AuthenticationError::JWKHasNoAlgorithm,
+                    StatusCode::INTERNAL_SERVER_ERROR,
+                ));
+                continue;
+            }
+        };
+
+        let algorithm = match convert_key_algorithm(key_algorithm) {
+            Some(a) => a,
+            None => {
+                error = Some((
+                    AuthenticationError::UnsupportedKeyAlgorithm(key_algorithm),
+                    StatusCode::INTERNAL_SERVER_ERROR,
+                ));
+                continue;
+            }
+        };
+
+        let mut validation = Validation::new(algorithm);
+        validation.validate_nbf = true;
+        // if set to true, it will reject tokens containing an `aud` claim if the validation does not specify an audience
+        // we don't validate audience yet, so this is deactivated
+        validation.validate_aud = false;
+
+        match decode::<serde_json::Value>(jwt, &decoding_key, &validation) {
+            Ok(v) => return Ok((issuer, v)),
+            Err(e) => {
+                tracing::trace!("JWT decoding failed with error `{e}`");
+                error = Some((
+                    AuthenticationError::CannotDecodeJWT(e),
+                    StatusCode::UNAUTHORIZED,
+                ));
+            }
+        };
+    }
+
+    match error {
+        Some(e) => Err(e),
+        None => {
+            // We can't find a key to process this JWT.
+            Err((
+                criteria.kid.map_or_else(
+                    || AuthenticationError::CannotFindSuitableKey(criteria.alg, None),
+                    AuthenticationError::CannotFindKID,
+                ),
+                StatusCode::UNAUTHORIZED,
+            ))
+        }
+    }
+}
+
+pub(crate) fn jwt_expires_in(context: &Context) -> Duration {
+    let claims = context
+        .get(APOLLO_AUTHENTICATION_JWT_CLAIMS)
+        .map_err(|err| tracing::error!("could not read JWT claims: {err}"))
+        .ok()
+        .flatten();
+    let ts_opt = claims.as_ref().and_then(|x: &serde_json::Value| {
+        if !x.is_object() {
+            tracing::error!("JWT claims should be an object");
+            return None;
+        }
+        let claims = x.as_object().expect("claims should be an object");
path: apollo-router/src/plugins/authentication/jwks.rs
user: login: bnjjj
body: As we try as much as possible to get rid of `expect` and `unwrap` in our code, maybe that could be worth it to use pattern matching here instead of an expect ?
created_at: 2025-03-17T09:45:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -256,3 +273,419 @@ impl Iterator for Iter<'_> {
         }
     }
 }
+
+#[derive(Debug, Default)]
+pub(super) struct JWTCriteria {
+    pub(super) alg: Algorithm,
+    pub(super) kid: Option<String>,
+}
+
+/// Search the list of JWKS to find a key we can use to decode a JWT.
+///
+/// The search criteria allow us to match a variety of keys depending on which criteria are provided
+/// by the JWT header. The only mandatory parameter is "alg".
+/// Note: "none" is not implemented by jsonwebtoken, so it can't be part of the [`Algorithm`] enum.
+pub(super) fn search_jwks(
+    jwks_manager: &JwksManager,
+    criteria: &JWTCriteria,
+) -> Option<Vec<(Option<String>, Jwk)>> {
+    const HIGHEST_SCORE: usize = 2;
+    let mut candidates = vec![];
+    let mut found_highest_score = false;
+    for JwkSetInfo {
+        jwks,
+        issuer,
+        algorithms,
+    } in jwks_manager.iter_jwks()
+    {
+        // filter accepted algorithms
+        if let Some(algs) = algorithms {
+            if !algs.contains(&criteria.alg) {
+                continue;
+            }
+        }
+
+        // Try to figure out if our jwks contains a candidate key (i.e.: a key which matches our
+        // criteria)
+        for mut key in jwks.keys.into_iter().filter(|key| {
+            // We are only interested in keys which are used for signature verification
+            match (&key.common.public_key_use, &key.common.key_operations) {
+                // "use" https://datatracker.ietf.org/doc/html/rfc7517#section-4.2 and
+                // "key_ops" https://datatracker.ietf.org/doc/html/rfc7517#section-4.3 are both optional
+                (None, None) => true,
+                (None, Some(purpose)) => purpose.contains(&KeyOperations::Verify),
+                (Some(key_use), None) => key_use == &PublicKeyUse::Signature,
+                // The "use" and "key_ops" JWK members SHOULD NOT be used together;
+                // however, if both are used, the information they convey MUST be
+                // consistent
+                (Some(key_use), Some(purpose)) => {
+                    key_use == &PublicKeyUse::Signature && purpose.contains(&KeyOperations::Verify)
+                }
+            }
+        }) {
+            let mut key_score = 0;
+
+            // Let's see if we have a specified kid and if they match
+            if criteria.kid.is_some() && key.common.key_id == criteria.kid {
+                key_score += 1;
+            }
+
+            // Furthermore, we would like our algorithms to match, or at least the kty
+            // If we have an algorithm that matches, boost the score
+            match key.common.key_algorithm {
+                Some(algorithm) => {
+                    if convert_key_algorithm(algorithm) != Some(criteria.alg) {
+                        continue;
+                    }
+                    key_score += 1;
+                }
+                // If a key doesn't have an algorithm, then we match the "alg" specified in the
+                // search criteria against all of the algorithms that we support.  If the
+                // key.algorithm parameters match the type of parameters for the "family" of the
+                // criteria "alg", then we update the key to use the value of "alg" provided in
+                // the search criteria.
+                // If not, then this is not a usable key for this JWT
+                // Note: Matching algorithm parameters may seem unusual, but the appropriate
+                // algorithm details are not structured for easy consumption in jsonwebtoken and
+                // this is the simplest way to determine algorithm family.
+                None => match (criteria.alg, &key.algorithm) {
+                    (
+                        Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512,
+                        AlgorithmParameters::OctetKey(_),
+                    ) => {
+                        key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                    }
+                    (
+                        Algorithm::RS256
+                        | Algorithm::RS384
+                        | Algorithm::RS512
+                        | Algorithm::PS256
+                        | Algorithm::PS384
+                        | Algorithm::PS512,
+                        AlgorithmParameters::RSA(_),
+                    ) => {
+                        key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                    }
+                    (Algorithm::ES256, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::P256 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    (Algorithm::ES384, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::P384 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    (Algorithm::EdDSA, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::Ed25519 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    _ => {
+                        // We'll ignore combinations we don't recognise
+                        continue;
+                    }
+                },
+            };
+
+            // If we get here we have a key that:
+            //  - may be used for signature verification
+            //  - has a matching algorithm, or if JWT has no algorithm, a matching key type
+            // It may have a matching kid if the JWT has a kid and it matches the key kid
+            //
+            // Multiple keys may meet the matching criteria, but they have a score. They get 1
+            // point for having an explicitly matching algorithm and 1 point for an explicitly
+            // matching kid. We will sort our candidates and pick the key with the highest score.
+
+            // If we find a key with a HIGHEST_SCORE, we will filter the list to only keep those
+            // with that score
+            if key_score == HIGHEST_SCORE {
+                found_highest_score = true;
+            }
+
+            candidates.push((key_score, (issuer.clone(), key)));
+        }
+    }
+
+    tracing::debug!(
+        "jwk candidates: {:?}",
+        candidates
+            .iter()
+            .map(|(score, (_, candidate))| (
+                score,
+                &candidate.common.key_id,
+                candidate.common.key_algorithm
+            ))
+            .collect::<Vec<(&usize, &Option<String>, Option<KeyAlgorithm>)>>()
+    );
+
+    if candidates.is_empty() {
+        None
+    } else {
+        // Only sort if we need to
+        if candidates.len() > 1 {
+            candidates.sort_by(|a, b| a.0.cmp(&b.0));
+        }
+
+        if found_highest_score {
+            Some(
+                candidates
+                    .into_iter()
+                    .filter_map(|(score, candidate)| {
+                        if score == HIGHEST_SCORE {
+                            Some(candidate)
+                        } else {
+                            None
+                        }
+                    })
+                    .collect(),
+            )
+        } else {
+            Some(
+                candidates
+                    .into_iter()
+                    .map(|(_score, candidate)| candidate)
+                    .collect(),
+            )
+        }
+    }
+}
+
+pub(super) fn extract_jwt<'a, 'b: 'a>(
+    source: &'a Source,
+    ignore_other_prefixes: bool,
+    headers: &'b HeaderMap,
+) -> Option<Result<&'b str, AuthenticationError>> {
+    match source {
+        Source::Header { name, value_prefix } => {
+            // The http_request is stored in a `Router::Request` context.
+            // We are going to check the headers for the presence of the configured header
+            let jwt_value_result = match headers.get(name) {
+                Some(value) => value.to_str(),
+                None => return None,
+            };
+
+            // If we find the header, but can't convert it to a string, let the client know
+            let jwt_value_untrimmed = match jwt_value_result {
+                Ok(value) => value,
+                Err(_not_a_string_error) => {
+                    return Some(Err(AuthenticationError::CannotConvertToString));
+                }
+            };
+
+            // Let's trim out leading and trailing whitespace to be accommodating
+            let jwt_value = jwt_value_untrimmed.trim();
+
+            // Make sure the format of our message matches our expectations
+            // Technically, the spec is case-sensitive, but let's accept
+            // case variations
+            //
+            let prefix_len = value_prefix.len();
+            if jwt_value.len() < prefix_len
+                || !&jwt_value[..prefix_len].eq_ignore_ascii_case(value_prefix)
+            {
+                return if ignore_other_prefixes {
+                    None
+                } else {
+                    Some(Err(AuthenticationError::InvalidPrefix(
+                        jwt_value_untrimmed.to_owned(),
+                        value_prefix.to_owned(),
+                    )))
+                };
+            }
+            // If there's no header prefix, we avoid splitting the header
+            let jwt = if value_prefix.is_empty() {
+                // check for whitespace — we've already trimmed, so this means the request has a
+                // prefix that shouldn't exist
+                if jwt_value.contains(' ') {
+                    return Some(Err(AuthenticationError::InvalidPrefix(
+                        jwt_value_untrimmed.to_owned(),
+                        value_prefix.to_owned(),
+                    )));
+                }
+
+                // we can simply assign the jwt to the jwt_value; we'll validate down below
+                jwt_value
+            } else {
+                // Otherwise, we need to split our string in (at most 2) sections.
+                let jwt_parts: Vec<&str> = jwt_value.splitn(2, ' ').collect();
+                if jwt_parts.len() != 2 {
+                    return Some(Err(AuthenticationError::MissingJWT(jwt_value.to_owned())));
+                }
+
+                // We have our jwt
+                jwt_parts[1]
+            };
+            Some(Ok(jwt))
+        }
+        Source::Cookie { name } => {
+            for header in headers.get_all("cookie") {
+                let value = match header.to_str() {
+                    Ok(value) => value,
+                    Err(_not_a_string_error) => {
+                        return Some(Err(AuthenticationError::CannotConvertToString));
+                    }
+                };
+                for cookie in cookie::Cookie::split_parse(value) {
+                    match cookie {
+                        Err(_) => continue,
+                        Ok(cookie) => {
+                            if cookie.name() == name {
+                                if let Some(value) = cookie.value_raw() {
+                                    return Some(Ok(value));
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            None
+        }
+    }
+}
+
+pub(super) fn decode_jwt(
+    jwt: &str,
+    keys: Vec<(Option<String>, Jwk)>,
+    criteria: JWTCriteria,
+) -> Result<(Option<String>, TokenData<serde_json::Value>), (AuthenticationError, StatusCode)> {
+    let mut error = None;
+    for (issuer, jwk) in keys.into_iter() {
+        let decoding_key = match DecodingKey::from_jwk(&jwk) {
+            Ok(k) => k,
+            Err(e) => {
+                error = Some((
+                    AuthenticationError::CannotCreateDecodingKey(e),
+                    StatusCode::INTERNAL_SERVER_ERROR,
+                ));
+                continue;
+            }
+        };
+
+        let key_algorithm = match jwk.common.key_algorithm {
+            Some(a) => a,
+            None => {
+                error = Some((
+                    AuthenticationError::JWKHasNoAlgorithm,
+                    StatusCode::INTERNAL_SERVER_ERROR,
+                ));
+                continue;
+            }
+        };
+
+        let algorithm = match convert_key_algorithm(key_algorithm) {
+            Some(a) => a,
+            None => {
+                error = Some((
+                    AuthenticationError::UnsupportedKeyAlgorithm(key_algorithm),
+                    StatusCode::INTERNAL_SERVER_ERROR,
+                ));
+                continue;
+            }
+        };
+
+        let mut validation = Validation::new(algorithm);
+        validation.validate_nbf = true;
+        // if set to true, it will reject tokens containing an `aud` claim if the validation does not specify an audience
+        // we don't validate audience yet, so this is deactivated
+        validation.validate_aud = false;
+
+        match decode::<serde_json::Value>(jwt, &decoding_key, &validation) {
+            Ok(v) => return Ok((issuer, v)),
+            Err(e) => {
+                tracing::trace!("JWT decoding failed with error `{e}`");
+                error = Some((
+                    AuthenticationError::CannotDecodeJWT(e),
+                    StatusCode::UNAUTHORIZED,
+                ));
+            }
+        };
+    }
+
+    match error {
+        Some(e) => Err(e),
+        None => {
+            // We can't find a key to process this JWT.
+            Err((
+                criteria.kid.map_or_else(
+                    || AuthenticationError::CannotFindSuitableKey(criteria.alg, None),
+                    AuthenticationError::CannotFindKID,
+                ),
+                StatusCode::UNAUTHORIZED,
+            ))
+        }
+    }
+}
+
+pub(crate) fn jwt_expires_in(context: &Context) -> Duration {
+    let claims = context
+        .get(APOLLO_AUTHENTICATION_JWT_CLAIMS)
+        .map_err(|err| tracing::error!("could not read JWT claims: {err}"))
+        .ok()
+        .flatten();
+    let ts_opt = claims.as_ref().and_then(|x: &serde_json::Value| {
+        if !x.is_object() {
+            tracing::error!("JWT claims should be an object");
+            return None;
+        }
+        let claims = x.as_object().expect("claims should be an object");
+        let exp = claims.get("exp")?;
+        if !exp.is_number() {
+            tracing::error!("JWT 'exp' (expiry) claim should be a number");
+            return None;
+        }
+        exp.as_i64()
+    });
+    match ts_opt {
+        Some(ts) => {
+            let now = SystemTime::now()
+                .duration_since(UNIX_EPOCH)
+                .expect("we should not run before EPOCH")
+                .as_secs() as i64;
+            if now < ts {
+                Duration::from_secs((ts - now) as u64)
+            } else {
+                Duration::ZERO
+            }
+        }
+        None => Duration::MAX,
+    }
path: apollo-router/src/plugins/authentication/jwks.rs
user: login: bnjjj
body: Maybe it could also be interesting to comment this behavior in function's doc. To document what are our assumptions here. Mainly explain that if there is no `exp` in the claim or if it's not a number then we set it to MAX for example
created_at: 2025-03-17T09:48:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -256,3 +273,419 @@ impl Iterator for Iter<'_> {
         }
     }
 }
+
+#[derive(Debug, Default)]
+pub(super) struct JWTCriteria {
+    pub(super) alg: Algorithm,
+    pub(super) kid: Option<String>,
+}
+
+/// Search the list of JWKS to find a key we can use to decode a JWT.
+///
+/// The search criteria allow us to match a variety of keys depending on which criteria are provided
+/// by the JWT header. The only mandatory parameter is "alg".
+/// Note: "none" is not implemented by jsonwebtoken, so it can't be part of the [`Algorithm`] enum.
+pub(super) fn search_jwks(
+    jwks_manager: &JwksManager,
+    criteria: &JWTCriteria,
+) -> Option<Vec<(Option<String>, Jwk)>> {
+    const HIGHEST_SCORE: usize = 2;
+    let mut candidates = vec![];
+    let mut found_highest_score = false;
+    for JwkSetInfo {
+        jwks,
+        issuer,
+        algorithms,
+    } in jwks_manager.iter_jwks()
+    {
+        // filter accepted algorithms
+        if let Some(algs) = algorithms {
+            if !algs.contains(&criteria.alg) {
+                continue;
+            }
+        }
+
+        // Try to figure out if our jwks contains a candidate key (i.e.: a key which matches our
+        // criteria)
+        for mut key in jwks.keys.into_iter().filter(|key| {
+            // We are only interested in keys which are used for signature verification
+            match (&key.common.public_key_use, &key.common.key_operations) {
+                // "use" https://datatracker.ietf.org/doc/html/rfc7517#section-4.2 and
+                // "key_ops" https://datatracker.ietf.org/doc/html/rfc7517#section-4.3 are both optional
+                (None, None) => true,
+                (None, Some(purpose)) => purpose.contains(&KeyOperations::Verify),
+                (Some(key_use), None) => key_use == &PublicKeyUse::Signature,
+                // The "use" and "key_ops" JWK members SHOULD NOT be used together;
+                // however, if both are used, the information they convey MUST be
+                // consistent
+                (Some(key_use), Some(purpose)) => {
+                    key_use == &PublicKeyUse::Signature && purpose.contains(&KeyOperations::Verify)
+                }
+            }
+        }) {
+            let mut key_score = 0;
+
+            // Let's see if we have a specified kid and if they match
+            if criteria.kid.is_some() && key.common.key_id == criteria.kid {
+                key_score += 1;
+            }
+
+            // Furthermore, we would like our algorithms to match, or at least the kty
+            // If we have an algorithm that matches, boost the score
+            match key.common.key_algorithm {
+                Some(algorithm) => {
+                    if convert_key_algorithm(algorithm) != Some(criteria.alg) {
+                        continue;
+                    }
+                    key_score += 1;
+                }
+                // If a key doesn't have an algorithm, then we match the "alg" specified in the
+                // search criteria against all of the algorithms that we support.  If the
+                // key.algorithm parameters match the type of parameters for the "family" of the
+                // criteria "alg", then we update the key to use the value of "alg" provided in
+                // the search criteria.
+                // If not, then this is not a usable key for this JWT
+                // Note: Matching algorithm parameters may seem unusual, but the appropriate
+                // algorithm details are not structured for easy consumption in jsonwebtoken and
+                // this is the simplest way to determine algorithm family.
+                None => match (criteria.alg, &key.algorithm) {
+                    (
+                        Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512,
+                        AlgorithmParameters::OctetKey(_),
+                    ) => {
+                        key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                    }
+                    (
+                        Algorithm::RS256
+                        | Algorithm::RS384
+                        | Algorithm::RS512
+                        | Algorithm::PS256
+                        | Algorithm::PS384
+                        | Algorithm::PS512,
+                        AlgorithmParameters::RSA(_),
+                    ) => {
+                        key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                    }
+                    (Algorithm::ES256, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::P256 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    (Algorithm::ES384, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::P384 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    (Algorithm::EdDSA, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::Ed25519 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    _ => {
+                        // We'll ignore combinations we don't recognise
+                        continue;
+                    }
+                },
+            };
+
+            // If we get here we have a key that:
+            //  - may be used for signature verification
+            //  - has a matching algorithm, or if JWT has no algorithm, a matching key type
+            // It may have a matching kid if the JWT has a kid and it matches the key kid
+            //
+            // Multiple keys may meet the matching criteria, but they have a score. They get 1
+            // point for having an explicitly matching algorithm and 1 point for an explicitly
+            // matching kid. We will sort our candidates and pick the key with the highest score.
+
+            // If we find a key with a HIGHEST_SCORE, we will filter the list to only keep those
+            // with that score
+            if key_score == HIGHEST_SCORE {
+                found_highest_score = true;
+            }
+
+            candidates.push((key_score, (issuer.clone(), key)));
+        }
+    }
+
+    tracing::debug!(
+        "jwk candidates: {:?}",
+        candidates
+            .iter()
+            .map(|(score, (_, candidate))| (
+                score,
+                &candidate.common.key_id,
+                candidate.common.key_algorithm
+            ))
+            .collect::<Vec<(&usize, &Option<String>, Option<KeyAlgorithm>)>>()
+    );
+
+    if candidates.is_empty() {
+        None
+    } else {
+        // Only sort if we need to
+        if candidates.len() > 1 {
+            candidates.sort_by(|a, b| a.0.cmp(&b.0));
+        }
+
+        if found_highest_score {
+            Some(
+                candidates
+                    .into_iter()
+                    .filter_map(|(score, candidate)| {
+                        if score == HIGHEST_SCORE {
+                            Some(candidate)
+                        } else {
+                            None
+                        }
+                    })
+                    .collect(),
+            )
+        } else {
+            Some(
+                candidates
+                    .into_iter()
+                    .map(|(_score, candidate)| candidate)
+                    .collect(),
+            )
+        }
+    }
+}
+
+pub(super) fn extract_jwt<'a, 'b: 'a>(
+    source: &'a Source,
+    ignore_other_prefixes: bool,
+    headers: &'b HeaderMap,
+) -> Option<Result<&'b str, AuthenticationError>> {
+    match source {
+        Source::Header { name, value_prefix } => {
+            // The http_request is stored in a `Router::Request` context.
+            // We are going to check the headers for the presence of the configured header
+            let jwt_value_result = match headers.get(name) {
+                Some(value) => value.to_str(),
+                None => return None,
+            };
+
+            // If we find the header, but can't convert it to a string, let the client know
+            let jwt_value_untrimmed = match jwt_value_result {
+                Ok(value) => value,
+                Err(_not_a_string_error) => {
+                    return Some(Err(AuthenticationError::CannotConvertToString));
+                }
+            };
+
+            // Let's trim out leading and trailing whitespace to be accommodating
+            let jwt_value = jwt_value_untrimmed.trim();
+
+            // Make sure the format of our message matches our expectations
+            // Technically, the spec is case-sensitive, but let's accept
+            // case variations
+            //
+            let prefix_len = value_prefix.len();
+            if jwt_value.len() < prefix_len
+                || !&jwt_value[..prefix_len].eq_ignore_ascii_case(value_prefix)
+            {
+                return if ignore_other_prefixes {
+                    None
+                } else {
+                    Some(Err(AuthenticationError::InvalidPrefix(
+                        jwt_value_untrimmed.to_owned(),
+                        value_prefix.to_owned(),
+                    )))
+                };
+            }
+            // If there's no header prefix, we avoid splitting the header
+            let jwt = if value_prefix.is_empty() {
+                // check for whitespace — we've already trimmed, so this means the request has a
+                // prefix that shouldn't exist
+                if jwt_value.contains(' ') {
+                    return Some(Err(AuthenticationError::InvalidPrefix(
+                        jwt_value_untrimmed.to_owned(),
+                        value_prefix.to_owned(),
+                    )));
+                }
+
+                // we can simply assign the jwt to the jwt_value; we'll validate down below
+                jwt_value
+            } else {
+                // Otherwise, we need to split our string in (at most 2) sections.
+                let jwt_parts: Vec<&str> = jwt_value.splitn(2, ' ').collect();
+                if jwt_parts.len() != 2 {
+                    return Some(Err(AuthenticationError::MissingJWT(jwt_value.to_owned())));
+                }
+
+                // We have our jwt
+                jwt_parts[1]
+            };
+            Some(Ok(jwt))
+        }
+        Source::Cookie { name } => {
+            for header in headers.get_all("cookie") {
+                let value = match header.to_str() {
+                    Ok(value) => value,
+                    Err(_not_a_string_error) => {
+                        return Some(Err(AuthenticationError::CannotConvertToString));
+                    }
+                };
+                for cookie in cookie::Cookie::split_parse(value) {
+                    match cookie {
+                        Err(_) => continue,
+                        Ok(cookie) => {
+                            if cookie.name() == name {
+                                if let Some(value) = cookie.value_raw() {
+                                    return Some(Ok(value));
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            None
+        }
+    }
+}
+
+pub(super) fn decode_jwt(
+    jwt: &str,
+    keys: Vec<(Option<String>, Jwk)>,
+    criteria: JWTCriteria,
+) -> Result<(Option<String>, TokenData<serde_json::Value>), (AuthenticationError, StatusCode)> {
+    let mut error = None;
+    for (issuer, jwk) in keys.into_iter() {
+        let decoding_key = match DecodingKey::from_jwk(&jwk) {
+            Ok(k) => k,
+            Err(e) => {
+                error = Some((
+                    AuthenticationError::CannotCreateDecodingKey(e),
+                    StatusCode::INTERNAL_SERVER_ERROR,
+                ));
+                continue;
+            }
+        };
+
+        let key_algorithm = match jwk.common.key_algorithm {
+            Some(a) => a,
+            None => {
+                error = Some((
+                    AuthenticationError::JWKHasNoAlgorithm,
+                    StatusCode::INTERNAL_SERVER_ERROR,
+                ));
+                continue;
+            }
+        };
+
+        let algorithm = match convert_key_algorithm(key_algorithm) {
+            Some(a) => a,
+            None => {
+                error = Some((
+                    AuthenticationError::UnsupportedKeyAlgorithm(key_algorithm),
+                    StatusCode::INTERNAL_SERVER_ERROR,
+                ));
+                continue;
+            }
+        };
+
+        let mut validation = Validation::new(algorithm);
+        validation.validate_nbf = true;
+        // if set to true, it will reject tokens containing an `aud` claim if the validation does not specify an audience
+        // we don't validate audience yet, so this is deactivated
+        validation.validate_aud = false;
+
+        match decode::<serde_json::Value>(jwt, &decoding_key, &validation) {
+            Ok(v) => return Ok((issuer, v)),
+            Err(e) => {
+                tracing::trace!("JWT decoding failed with error `{e}`");
+                error = Some((
+                    AuthenticationError::CannotDecodeJWT(e),
+                    StatusCode::UNAUTHORIZED,
+                ));
path: apollo-router/src/plugins/authentication/jwks.rs
user: login: Velfi
body: I moved this code but did not write it. As such, I'm not sure what the best thing to do is.
created_at: 2025-03-17T14:31:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -256,3 +273,419 @@ impl Iterator for Iter<'_> {
         }
     }
 }
+
+#[derive(Debug, Default)]
+pub(super) struct JWTCriteria {
+    pub(super) alg: Algorithm,
+    pub(super) kid: Option<String>,
+}
+
+/// Search the list of JWKS to find a key we can use to decode a JWT.
+///
+/// The search criteria allow us to match a variety of keys depending on which criteria are provided
+/// by the JWT header. The only mandatory parameter is "alg".
+/// Note: "none" is not implemented by jsonwebtoken, so it can't be part of the [`Algorithm`] enum.
+pub(super) fn search_jwks(
+    jwks_manager: &JwksManager,
+    criteria: &JWTCriteria,
+) -> Option<Vec<(Option<String>, Jwk)>> {
+    const HIGHEST_SCORE: usize = 2;
+    let mut candidates = vec![];
+    let mut found_highest_score = false;
+    for JwkSetInfo {
+        jwks,
+        issuer,
+        algorithms,
+    } in jwks_manager.iter_jwks()
+    {
+        // filter accepted algorithms
+        if let Some(algs) = algorithms {
+            if !algs.contains(&criteria.alg) {
+                continue;
+            }
+        }
+
+        // Try to figure out if our jwks contains a candidate key (i.e.: a key which matches our
+        // criteria)
+        for mut key in jwks.keys.into_iter().filter(|key| {
+            // We are only interested in keys which are used for signature verification
+            match (&key.common.public_key_use, &key.common.key_operations) {
+                // "use" https://datatracker.ietf.org/doc/html/rfc7517#section-4.2 and
+                // "key_ops" https://datatracker.ietf.org/doc/html/rfc7517#section-4.3 are both optional
+                (None, None) => true,
+                (None, Some(purpose)) => purpose.contains(&KeyOperations::Verify),
+                (Some(key_use), None) => key_use == &PublicKeyUse::Signature,
+                // The "use" and "key_ops" JWK members SHOULD NOT be used together;
+                // however, if both are used, the information they convey MUST be
+                // consistent
+                (Some(key_use), Some(purpose)) => {
+                    key_use == &PublicKeyUse::Signature && purpose.contains(&KeyOperations::Verify)
+                }
+            }
+        }) {
+            let mut key_score = 0;
+
+            // Let's see if we have a specified kid and if they match
+            if criteria.kid.is_some() && key.common.key_id == criteria.kid {
+                key_score += 1;
+            }
+
+            // Furthermore, we would like our algorithms to match, or at least the kty
+            // If we have an algorithm that matches, boost the score
+            match key.common.key_algorithm {
+                Some(algorithm) => {
+                    if convert_key_algorithm(algorithm) != Some(criteria.alg) {
+                        continue;
+                    }
+                    key_score += 1;
+                }
+                // If a key doesn't have an algorithm, then we match the "alg" specified in the
+                // search criteria against all of the algorithms that we support.  If the
+                // key.algorithm parameters match the type of parameters for the "family" of the
+                // criteria "alg", then we update the key to use the value of "alg" provided in
+                // the search criteria.
+                // If not, then this is not a usable key for this JWT
+                // Note: Matching algorithm parameters may seem unusual, but the appropriate
+                // algorithm details are not structured for easy consumption in jsonwebtoken and
+                // this is the simplest way to determine algorithm family.
+                None => match (criteria.alg, &key.algorithm) {
+                    (
+                        Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512,
+                        AlgorithmParameters::OctetKey(_),
+                    ) => {
+                        key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                    }
+                    (
+                        Algorithm::RS256
+                        | Algorithm::RS384
+                        | Algorithm::RS512
+                        | Algorithm::PS256
+                        | Algorithm::PS384
+                        | Algorithm::PS512,
+                        AlgorithmParameters::RSA(_),
+                    ) => {
+                        key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                    }
+                    (Algorithm::ES256, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::P256 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    (Algorithm::ES384, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::P384 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    (Algorithm::EdDSA, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::Ed25519 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    _ => {
+                        // We'll ignore combinations we don't recognise
+                        continue;
+                    }
+                },
+            };
+
+            // If we get here we have a key that:
+            //  - may be used for signature verification
+            //  - has a matching algorithm, or if JWT has no algorithm, a matching key type
+            // It may have a matching kid if the JWT has a kid and it matches the key kid
+            //
+            // Multiple keys may meet the matching criteria, but they have a score. They get 1
+            // point for having an explicitly matching algorithm and 1 point for an explicitly
+            // matching kid. We will sort our candidates and pick the key with the highest score.
+
+            // If we find a key with a HIGHEST_SCORE, we will filter the list to only keep those
+            // with that score
+            if key_score == HIGHEST_SCORE {
+                found_highest_score = true;
+            }
+
+            candidates.push((key_score, (issuer.clone(), key)));
+        }
+    }
+
+    tracing::debug!(
+        "jwk candidates: {:?}",
+        candidates
+            .iter()
+            .map(|(score, (_, candidate))| (
+                score,
+                &candidate.common.key_id,
+                candidate.common.key_algorithm
+            ))
+            .collect::<Vec<(&usize, &Option<String>, Option<KeyAlgorithm>)>>()
+    );
+
+    if candidates.is_empty() {
+        None
+    } else {
+        // Only sort if we need to
+        if candidates.len() > 1 {
+            candidates.sort_by(|a, b| a.0.cmp(&b.0));
+        }
+
+        if found_highest_score {
+            Some(
+                candidates
+                    .into_iter()
+                    .filter_map(|(score, candidate)| {
+                        if score == HIGHEST_SCORE {
+                            Some(candidate)
+                        } else {
+                            None
+                        }
+                    })
+                    .collect(),
+            )
+        } else {
+            Some(
+                candidates
+                    .into_iter()
+                    .map(|(_score, candidate)| candidate)
+                    .collect(),
+            )
+        }
+    }
+}
+
+pub(super) fn extract_jwt<'a, 'b: 'a>(
+    source: &'a Source,
+    ignore_other_prefixes: bool,
+    headers: &'b HeaderMap,
+) -> Option<Result<&'b str, AuthenticationError>> {
+    match source {
+        Source::Header { name, value_prefix } => {
+            // The http_request is stored in a `Router::Request` context.
+            // We are going to check the headers for the presence of the configured header
+            let jwt_value_result = match headers.get(name) {
+                Some(value) => value.to_str(),
+                None => return None,
+            };
+
+            // If we find the header, but can't convert it to a string, let the client know
+            let jwt_value_untrimmed = match jwt_value_result {
+                Ok(value) => value,
+                Err(_not_a_string_error) => {
+                    return Some(Err(AuthenticationError::CannotConvertToString));
+                }
+            };
+
+            // Let's trim out leading and trailing whitespace to be accommodating
+            let jwt_value = jwt_value_untrimmed.trim();
+
+            // Make sure the format of our message matches our expectations
+            // Technically, the spec is case-sensitive, but let's accept
+            // case variations
+            //
+            let prefix_len = value_prefix.len();
+            if jwt_value.len() < prefix_len
+                || !&jwt_value[..prefix_len].eq_ignore_ascii_case(value_prefix)
+            {
+                return if ignore_other_prefixes {
+                    None
+                } else {
+                    Some(Err(AuthenticationError::InvalidPrefix(
+                        jwt_value_untrimmed.to_owned(),
+                        value_prefix.to_owned(),
+                    )))
+                };
+            }
+            // If there's no header prefix, we avoid splitting the header
+            let jwt = if value_prefix.is_empty() {
+                // check for whitespace — we've already trimmed, so this means the request has a
+                // prefix that shouldn't exist
+                if jwt_value.contains(' ') {
+                    return Some(Err(AuthenticationError::InvalidPrefix(
+                        jwt_value_untrimmed.to_owned(),
+                        value_prefix.to_owned(),
+                    )));
+                }
+
+                // we can simply assign the jwt to the jwt_value; we'll validate down below
+                jwt_value
+            } else {
+                // Otherwise, we need to split our string in (at most 2) sections.
+                let jwt_parts: Vec<&str> = jwt_value.splitn(2, ' ').collect();
+                if jwt_parts.len() != 2 {
+                    return Some(Err(AuthenticationError::MissingJWT(jwt_value.to_owned())));
+                }
+
+                // We have our jwt
+                jwt_parts[1]
+            };
+            Some(Ok(jwt))
+        }
+        Source::Cookie { name } => {
+            for header in headers.get_all("cookie") {
+                let value = match header.to_str() {
+                    Ok(value) => value,
+                    Err(_not_a_string_error) => {
+                        return Some(Err(AuthenticationError::CannotConvertToString));
+                    }
+                };
+                for cookie in cookie::Cookie::split_parse(value) {
+                    match cookie {
+                        Err(_) => continue,
+                        Ok(cookie) => {
+                            if cookie.name() == name {
+                                if let Some(value) = cookie.value_raw() {
+                                    return Some(Ok(value));
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            None
+        }
+    }
+}
+
+pub(super) fn decode_jwt(
+    jwt: &str,
+    keys: Vec<(Option<String>, Jwk)>,
+    criteria: JWTCriteria,
+) -> Result<(Option<String>, TokenData<serde_json::Value>), (AuthenticationError, StatusCode)> {
+    let mut error = None;
+    for (issuer, jwk) in keys.into_iter() {
+        let decoding_key = match DecodingKey::from_jwk(&jwk) {
+            Ok(k) => k,
+            Err(e) => {
+                error = Some((
+                    AuthenticationError::CannotCreateDecodingKey(e),
+                    StatusCode::INTERNAL_SERVER_ERROR,
+                ));
+                continue;
+            }
+        };
+
+        let key_algorithm = match jwk.common.key_algorithm {
+            Some(a) => a,
+            None => {
+                error = Some((
+                    AuthenticationError::JWKHasNoAlgorithm,
+                    StatusCode::INTERNAL_SERVER_ERROR,
+                ));
+                continue;
+            }
+        };
+
+        let algorithm = match convert_key_algorithm(key_algorithm) {
+            Some(a) => a,
+            None => {
+                error = Some((
+                    AuthenticationError::UnsupportedKeyAlgorithm(key_algorithm),
+                    StatusCode::INTERNAL_SERVER_ERROR,
+                ));
+                continue;
+            }
+        };
+
+        let mut validation = Validation::new(algorithm);
+        validation.validate_nbf = true;
+        // if set to true, it will reject tokens containing an `aud` claim if the validation does not specify an audience
+        // we don't validate audience yet, so this is deactivated
+        validation.validate_aud = false;
+
+        match decode::<serde_json::Value>(jwt, &decoding_key, &validation) {
+            Ok(v) => return Ok((issuer, v)),
+            Err(e) => {
+                tracing::trace!("JWT decoding failed with error `{e}`");
+                error = Some((
+                    AuthenticationError::CannotDecodeJWT(e),
+                    StatusCode::UNAUTHORIZED,
+                ));
+            }
+        };
+    }
+
+    match error {
+        Some(e) => Err(e),
+        None => {
+            // We can't find a key to process this JWT.
+            Err((
+                criteria.kid.map_or_else(
+                    || AuthenticationError::CannotFindSuitableKey(criteria.alg, None),
+                    AuthenticationError::CannotFindKID,
+                ),
+                StatusCode::UNAUTHORIZED,
+            ))
+        }
+    }
+}
+
+pub(crate) fn jwt_expires_in(context: &Context) -> Duration {
+    let claims = context
+        .get(APOLLO_AUTHENTICATION_JWT_CLAIMS)
+        .map_err(|err| tracing::error!("could not read JWT claims: {err}"))
+        .ok()
+        .flatten();
+    let ts_opt = claims.as_ref().and_then(|x: &serde_json::Value| {
+        if !x.is_object() {
+            tracing::error!("JWT claims should be an object");
+            return None;
+        }
+        let claims = x.as_object().expect("claims should be an object");
path: apollo-router/src/plugins/authentication/jwks.rs
user: login: Velfi
body: I'll rewrite this as a `match`
created_at: 2025-03-17T15:11:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -256,3 +273,419 @@ impl Iterator for Iter<'_> {
         }
     }
 }
+
+#[derive(Debug, Default)]
+pub(super) struct JWTCriteria {
+    pub(super) alg: Algorithm,
+    pub(super) kid: Option<String>,
+}
+
+/// Search the list of JWKS to find a key we can use to decode a JWT.
+///
+/// The search criteria allow us to match a variety of keys depending on which criteria are provided
+/// by the JWT header. The only mandatory parameter is "alg".
+/// Note: "none" is not implemented by jsonwebtoken, so it can't be part of the [`Algorithm`] enum.
+pub(super) fn search_jwks(
+    jwks_manager: &JwksManager,
+    criteria: &JWTCriteria,
+) -> Option<Vec<(Option<String>, Jwk)>> {
+    const HIGHEST_SCORE: usize = 2;
+    let mut candidates = vec![];
+    let mut found_highest_score = false;
+    for JwkSetInfo {
+        jwks,
+        issuer,
+        algorithms,
+    } in jwks_manager.iter_jwks()
+    {
+        // filter accepted algorithms
+        if let Some(algs) = algorithms {
+            if !algs.contains(&criteria.alg) {
+                continue;
+            }
+        }
+
+        // Try to figure out if our jwks contains a candidate key (i.e.: a key which matches our
+        // criteria)
+        for mut key in jwks.keys.into_iter().filter(|key| {
+            // We are only interested in keys which are used for signature verification
+            match (&key.common.public_key_use, &key.common.key_operations) {
+                // "use" https://datatracker.ietf.org/doc/html/rfc7517#section-4.2 and
+                // "key_ops" https://datatracker.ietf.org/doc/html/rfc7517#section-4.3 are both optional
+                (None, None) => true,
+                (None, Some(purpose)) => purpose.contains(&KeyOperations::Verify),
+                (Some(key_use), None) => key_use == &PublicKeyUse::Signature,
+                // The "use" and "key_ops" JWK members SHOULD NOT be used together;
+                // however, if both are used, the information they convey MUST be
+                // consistent
+                (Some(key_use), Some(purpose)) => {
+                    key_use == &PublicKeyUse::Signature && purpose.contains(&KeyOperations::Verify)
+                }
+            }
+        }) {
+            let mut key_score = 0;
+
+            // Let's see if we have a specified kid and if they match
+            if criteria.kid.is_some() && key.common.key_id == criteria.kid {
+                key_score += 1;
+            }
+
+            // Furthermore, we would like our algorithms to match, or at least the kty
+            // If we have an algorithm that matches, boost the score
+            match key.common.key_algorithm {
+                Some(algorithm) => {
+                    if convert_key_algorithm(algorithm) != Some(criteria.alg) {
+                        continue;
+                    }
+                    key_score += 1;
+                }
+                // If a key doesn't have an algorithm, then we match the "alg" specified in the
+                // search criteria against all of the algorithms that we support.  If the
+                // key.algorithm parameters match the type of parameters for the "family" of the
+                // criteria "alg", then we update the key to use the value of "alg" provided in
+                // the search criteria.
+                // If not, then this is not a usable key for this JWT
+                // Note: Matching algorithm parameters may seem unusual, but the appropriate
+                // algorithm details are not structured for easy consumption in jsonwebtoken and
+                // this is the simplest way to determine algorithm family.
+                None => match (criteria.alg, &key.algorithm) {
+                    (
+                        Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512,
+                        AlgorithmParameters::OctetKey(_),
+                    ) => {
+                        key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                    }
+                    (
+                        Algorithm::RS256
+                        | Algorithm::RS384
+                        | Algorithm::RS512
+                        | Algorithm::PS256
+                        | Algorithm::PS384
+                        | Algorithm::PS512,
+                        AlgorithmParameters::RSA(_),
+                    ) => {
+                        key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                    }
+                    (Algorithm::ES256, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::P256 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    (Algorithm::ES384, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::P384 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    (Algorithm::EdDSA, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::Ed25519 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    _ => {
+                        // We'll ignore combinations we don't recognise
+                        continue;
+                    }
+                },
+            };
+
+            // If we get here we have a key that:
+            //  - may be used for signature verification
+            //  - has a matching algorithm, or if JWT has no algorithm, a matching key type
+            // It may have a matching kid if the JWT has a kid and it matches the key kid
+            //
+            // Multiple keys may meet the matching criteria, but they have a score. They get 1
+            // point for having an explicitly matching algorithm and 1 point for an explicitly
+            // matching kid. We will sort our candidates and pick the key with the highest score.
+
+            // If we find a key with a HIGHEST_SCORE, we will filter the list to only keep those
+            // with that score
+            if key_score == HIGHEST_SCORE {
+                found_highest_score = true;
+            }
+
+            candidates.push((key_score, (issuer.clone(), key)));
+        }
+    }
+
+    tracing::debug!(
+        "jwk candidates: {:?}",
+        candidates
+            .iter()
+            .map(|(score, (_, candidate))| (
+                score,
+                &candidate.common.key_id,
+                candidate.common.key_algorithm
+            ))
+            .collect::<Vec<(&usize, &Option<String>, Option<KeyAlgorithm>)>>()
+    );
+
+    if candidates.is_empty() {
+        None
+    } else {
+        // Only sort if we need to
+        if candidates.len() > 1 {
+            candidates.sort_by(|a, b| a.0.cmp(&b.0));
+        }
+
+        if found_highest_score {
+            Some(
+                candidates
+                    .into_iter()
+                    .filter_map(|(score, candidate)| {
+                        if score == HIGHEST_SCORE {
+                            Some(candidate)
+                        } else {
+                            None
+                        }
+                    })
+                    .collect(),
+            )
+        } else {
+            Some(
+                candidates
+                    .into_iter()
+                    .map(|(_score, candidate)| candidate)
+                    .collect(),
+            )
+        }
+    }
+}
+
+pub(super) fn extract_jwt<'a, 'b: 'a>(
path: apollo-router/src/plugins/authentication/jwks.rs
user: login: Velfi
body: I moved this code but did not write it. As such, I'd rather leave it as it is.
created_at: 2025-03-17T17:02:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -256,3 +273,419 @@ impl Iterator for Iter<'_> {
         }
     }
 }
+
+#[derive(Debug, Default)]
+pub(super) struct JWTCriteria {
+    pub(super) alg: Algorithm,
+    pub(super) kid: Option<String>,
+}
+
+/// Search the list of JWKS to find a key we can use to decode a JWT.
+///
+/// The search criteria allow us to match a variety of keys depending on which criteria are provided
+/// by the JWT header. The only mandatory parameter is "alg".
+/// Note: "none" is not implemented by jsonwebtoken, so it can't be part of the [`Algorithm`] enum.
+pub(super) fn search_jwks(
+    jwks_manager: &JwksManager,
+    criteria: &JWTCriteria,
+) -> Option<Vec<(Option<String>, Jwk)>> {
+    const HIGHEST_SCORE: usize = 2;
+    let mut candidates = vec![];
+    let mut found_highest_score = false;
+    for JwkSetInfo {
+        jwks,
+        issuer,
+        algorithms,
+    } in jwks_manager.iter_jwks()
+    {
+        // filter accepted algorithms
+        if let Some(algs) = algorithms {
+            if !algs.contains(&criteria.alg) {
+                continue;
+            }
+        }
+
+        // Try to figure out if our jwks contains a candidate key (i.e.: a key which matches our
+        // criteria)
+        for mut key in jwks.keys.into_iter().filter(|key| {
+            // We are only interested in keys which are used for signature verification
+            match (&key.common.public_key_use, &key.common.key_operations) {
+                // "use" https://datatracker.ietf.org/doc/html/rfc7517#section-4.2 and
+                // "key_ops" https://datatracker.ietf.org/doc/html/rfc7517#section-4.3 are both optional
+                (None, None) => true,
+                (None, Some(purpose)) => purpose.contains(&KeyOperations::Verify),
+                (Some(key_use), None) => key_use == &PublicKeyUse::Signature,
+                // The "use" and "key_ops" JWK members SHOULD NOT be used together;
+                // however, if both are used, the information they convey MUST be
+                // consistent
+                (Some(key_use), Some(purpose)) => {
+                    key_use == &PublicKeyUse::Signature && purpose.contains(&KeyOperations::Verify)
+                }
+            }
+        }) {
+            let mut key_score = 0;
+
+            // Let's see if we have a specified kid and if they match
+            if criteria.kid.is_some() && key.common.key_id == criteria.kid {
+                key_score += 1;
+            }
+
+            // Furthermore, we would like our algorithms to match, or at least the kty
+            // If we have an algorithm that matches, boost the score
+            match key.common.key_algorithm {
+                Some(algorithm) => {
+                    if convert_key_algorithm(algorithm) != Some(criteria.alg) {
+                        continue;
+                    }
+                    key_score += 1;
+                }
+                // If a key doesn't have an algorithm, then we match the "alg" specified in the
+                // search criteria against all of the algorithms that we support.  If the
+                // key.algorithm parameters match the type of parameters for the "family" of the
+                // criteria "alg", then we update the key to use the value of "alg" provided in
+                // the search criteria.
+                // If not, then this is not a usable key for this JWT
+                // Note: Matching algorithm parameters may seem unusual, but the appropriate
+                // algorithm details are not structured for easy consumption in jsonwebtoken and
+                // this is the simplest way to determine algorithm family.
+                None => match (criteria.alg, &key.algorithm) {
+                    (
+                        Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512,
+                        AlgorithmParameters::OctetKey(_),
+                    ) => {
+                        key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                    }
+                    (
+                        Algorithm::RS256
+                        | Algorithm::RS384
+                        | Algorithm::RS512
+                        | Algorithm::PS256
+                        | Algorithm::PS384
+                        | Algorithm::PS512,
+                        AlgorithmParameters::RSA(_),
+                    ) => {
+                        key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                    }
+                    (Algorithm::ES256, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::P256 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    (Algorithm::ES384, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::P384 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    (Algorithm::EdDSA, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::Ed25519 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    _ => {
+                        // We'll ignore combinations we don't recognise
+                        continue;
+                    }
+                },
+            };
+
+            // If we get here we have a key that:
+            //  - may be used for signature verification
+            //  - has a matching algorithm, or if JWT has no algorithm, a matching key type
+            // It may have a matching kid if the JWT has a kid and it matches the key kid
+            //
+            // Multiple keys may meet the matching criteria, but they have a score. They get 1
+            // point for having an explicitly matching algorithm and 1 point for an explicitly
+            // matching kid. We will sort our candidates and pick the key with the highest score.
+
+            // If we find a key with a HIGHEST_SCORE, we will filter the list to only keep those
+            // with that score
+            if key_score == HIGHEST_SCORE {
+                found_highest_score = true;
+            }
+
+            candidates.push((key_score, (issuer.clone(), key)));
+        }
+    }
+
+    tracing::debug!(
+        "jwk candidates: {:?}",
+        candidates
+            .iter()
+            .map(|(score, (_, candidate))| (
+                score,
+                &candidate.common.key_id,
+                candidate.common.key_algorithm
+            ))
+            .collect::<Vec<(&usize, &Option<String>, Option<KeyAlgorithm>)>>()
+    );
+
+    if candidates.is_empty() {
+        None
+    } else {
+        // Only sort if we need to
+        if candidates.len() > 1 {
+            candidates.sort_by(|a, b| a.0.cmp(&b.0));
+        }
+
+        if found_highest_score {
+            Some(
+                candidates
+                    .into_iter()
+                    .filter_map(|(score, candidate)| {
+                        if score == HIGHEST_SCORE {
+                            Some(candidate)
+                        } else {
+                            None
+                        }
+                    })
+                    .collect(),
+            )
+        } else {
+            Some(
+                candidates
+                    .into_iter()
+                    .map(|(_score, candidate)| candidate)
+                    .collect(),
+            )
+        }
+    }
+}
+
+pub(super) fn extract_jwt<'a, 'b: 'a>(
+    source: &'a Source,
+    ignore_other_prefixes: bool,
+    headers: &'b HeaderMap,
+) -> Option<Result<&'b str, AuthenticationError>> {
+    match source {
+        Source::Header { name, value_prefix } => {
+            // The http_request is stored in a `Router::Request` context.
+            // We are going to check the headers for the presence of the configured header
+            let jwt_value_result = match headers.get(name) {
+                Some(value) => value.to_str(),
+                None => return None,
+            };
+
+            // If we find the header, but can't convert it to a string, let the client know
+            let jwt_value_untrimmed = match jwt_value_result {
+                Ok(value) => value,
+                Err(_not_a_string_error) => {
+                    return Some(Err(AuthenticationError::CannotConvertToString));
+                }
+            };
+
+            // Let's trim out leading and trailing whitespace to be accommodating
+            let jwt_value = jwt_value_untrimmed.trim();
+
+            // Make sure the format of our message matches our expectations
+            // Technically, the spec is case-sensitive, but let's accept
+            // case variations
+            //
+            let prefix_len = value_prefix.len();
+            if jwt_value.len() < prefix_len
+                || !&jwt_value[..prefix_len].eq_ignore_ascii_case(value_prefix)
+            {
+                return if ignore_other_prefixes {
+                    None
+                } else {
+                    Some(Err(AuthenticationError::InvalidPrefix(
+                        jwt_value_untrimmed.to_owned(),
+                        value_prefix.to_owned(),
+                    )))
+                };
+            }
+            // If there's no header prefix, we avoid splitting the header
+            let jwt = if value_prefix.is_empty() {
+                // check for whitespace — we've already trimmed, so this means the request has a
+                // prefix that shouldn't exist
+                if jwt_value.contains(' ') {
+                    return Some(Err(AuthenticationError::InvalidPrefix(
+                        jwt_value_untrimmed.to_owned(),
+                        value_prefix.to_owned(),
+                    )));
+                }
+
+                // we can simply assign the jwt to the jwt_value; we'll validate down below
+                jwt_value
+            } else {
+                // Otherwise, we need to split our string in (at most 2) sections.
+                let jwt_parts: Vec<&str> = jwt_value.splitn(2, ' ').collect();
+                if jwt_parts.len() != 2 {
+                    return Some(Err(AuthenticationError::MissingJWT(jwt_value.to_owned())));
+                }
+
+                // We have our jwt
+                jwt_parts[1]
+            };
+            Some(Ok(jwt))
+        }
+        Source::Cookie { name } => {
+            for header in headers.get_all("cookie") {
+                let value = match header.to_str() {
+                    Ok(value) => value,
+                    Err(_not_a_string_error) => {
+                        return Some(Err(AuthenticationError::CannotConvertToString));
+                    }
+                };
+                for cookie in cookie::Cookie::split_parse(value) {
+                    match cookie {
+                        Err(_) => continue,
+                        Ok(cookie) => {
+                            if cookie.name() == name {
+                                if let Some(value) = cookie.value_raw() {
+                                    return Some(Ok(value));
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            None
+        }
+    }
+}
+
+pub(super) fn decode_jwt(
+    jwt: &str,
+    keys: Vec<(Option<String>, Jwk)>,
+    criteria: JWTCriteria,
+) -> Result<(Option<String>, TokenData<serde_json::Value>), (AuthenticationError, StatusCode)> {
+    let mut error = None;
+    for (issuer, jwk) in keys.into_iter() {
+        let decoding_key = match DecodingKey::from_jwk(&jwk) {
+            Ok(k) => k,
+            Err(e) => {
+                error = Some((
+                    AuthenticationError::CannotCreateDecodingKey(e),
+                    StatusCode::INTERNAL_SERVER_ERROR,
+                ));
+                continue;
+            }
+        };
+
+        let key_algorithm = match jwk.common.key_algorithm {
+            Some(a) => a,
+            None => {
+                error = Some((
+                    AuthenticationError::JWKHasNoAlgorithm,
+                    StatusCode::INTERNAL_SERVER_ERROR,
+                ));
+                continue;
+            }
+        };
+
+        let algorithm = match convert_key_algorithm(key_algorithm) {
+            Some(a) => a,
+            None => {
+                error = Some((
+                    AuthenticationError::UnsupportedKeyAlgorithm(key_algorithm),
+                    StatusCode::INTERNAL_SERVER_ERROR,
+                ));
+                continue;
+            }
+        };
+
+        let mut validation = Validation::new(algorithm);
+        validation.validate_nbf = true;
+        // if set to true, it will reject tokens containing an `aud` claim if the validation does not specify an audience
+        // we don't validate audience yet, so this is deactivated
+        validation.validate_aud = false;
+
+        match decode::<serde_json::Value>(jwt, &decoding_key, &validation) {
+            Ok(v) => return Ok((issuer, v)),
+            Err(e) => {
+                tracing::trace!("JWT decoding failed with error `{e}`");
+                error = Some((
+                    AuthenticationError::CannotDecodeJWT(e),
+                    StatusCode::UNAUTHORIZED,
+                ));
path: apollo-router/src/plugins/authentication/jwks.rs
user: login: bnjjj
body: Oh ok, sorry. Unfortunately it hasn't be done before :p now it's like dark magic
created_at: 2025-03-18T09:38:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -256,3 +273,419 @@ impl Iterator for Iter<'_> {
         }
     }
 }
+
+#[derive(Debug, Default)]
+pub(super) struct JWTCriteria {
+    pub(super) alg: Algorithm,
+    pub(super) kid: Option<String>,
+}
+
+/// Search the list of JWKS to find a key we can use to decode a JWT.
+///
+/// The search criteria allow us to match a variety of keys depending on which criteria are provided
+/// by the JWT header. The only mandatory parameter is "alg".
+/// Note: "none" is not implemented by jsonwebtoken, so it can't be part of the [`Algorithm`] enum.
+pub(super) fn search_jwks(
+    jwks_manager: &JwksManager,
+    criteria: &JWTCriteria,
+) -> Option<Vec<(Option<String>, Jwk)>> {
+    const HIGHEST_SCORE: usize = 2;
+    let mut candidates = vec![];
+    let mut found_highest_score = false;
+    for JwkSetInfo {
+        jwks,
+        issuer,
+        algorithms,
+    } in jwks_manager.iter_jwks()
+    {
+        // filter accepted algorithms
+        if let Some(algs) = algorithms {
+            if !algs.contains(&criteria.alg) {
+                continue;
+            }
+        }
+
+        // Try to figure out if our jwks contains a candidate key (i.e.: a key which matches our
+        // criteria)
+        for mut key in jwks.keys.into_iter().filter(|key| {
+            // We are only interested in keys which are used for signature verification
+            match (&key.common.public_key_use, &key.common.key_operations) {
+                // "use" https://datatracker.ietf.org/doc/html/rfc7517#section-4.2 and
+                // "key_ops" https://datatracker.ietf.org/doc/html/rfc7517#section-4.3 are both optional
+                (None, None) => true,
+                (None, Some(purpose)) => purpose.contains(&KeyOperations::Verify),
+                (Some(key_use), None) => key_use == &PublicKeyUse::Signature,
+                // The "use" and "key_ops" JWK members SHOULD NOT be used together;
+                // however, if both are used, the information they convey MUST be
+                // consistent
+                (Some(key_use), Some(purpose)) => {
+                    key_use == &PublicKeyUse::Signature && purpose.contains(&KeyOperations::Verify)
+                }
+            }
+        }) {
+            let mut key_score = 0;
+
+            // Let's see if we have a specified kid and if they match
+            if criteria.kid.is_some() && key.common.key_id == criteria.kid {
+                key_score += 1;
+            }
+
+            // Furthermore, we would like our algorithms to match, or at least the kty
+            // If we have an algorithm that matches, boost the score
+            match key.common.key_algorithm {
+                Some(algorithm) => {
+                    if convert_key_algorithm(algorithm) != Some(criteria.alg) {
+                        continue;
+                    }
+                    key_score += 1;
+                }
+                // If a key doesn't have an algorithm, then we match the "alg" specified in the
+                // search criteria against all of the algorithms that we support.  If the
+                // key.algorithm parameters match the type of parameters for the "family" of the
+                // criteria "alg", then we update the key to use the value of "alg" provided in
+                // the search criteria.
+                // If not, then this is not a usable key for this JWT
+                // Note: Matching algorithm parameters may seem unusual, but the appropriate
+                // algorithm details are not structured for easy consumption in jsonwebtoken and
+                // this is the simplest way to determine algorithm family.
+                None => match (criteria.alg, &key.algorithm) {
+                    (
+                        Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512,
+                        AlgorithmParameters::OctetKey(_),
+                    ) => {
+                        key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                    }
+                    (
+                        Algorithm::RS256
+                        | Algorithm::RS384
+                        | Algorithm::RS512
+                        | Algorithm::PS256
+                        | Algorithm::PS384
+                        | Algorithm::PS512,
+                        AlgorithmParameters::RSA(_),
+                    ) => {
+                        key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                    }
+                    (Algorithm::ES256, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::P256 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    (Algorithm::ES384, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::P384 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    (Algorithm::EdDSA, AlgorithmParameters::EllipticCurve(params)) => {
+                        if params.curve == EllipticCurve::Ed25519 {
+                            key.common.key_algorithm = Some(convert_algorithm(criteria.alg));
+                        }
+                    }
+                    _ => {
+                        // We'll ignore combinations we don't recognise
+                        continue;
+                    }
+                },
+            };
+
+            // If we get here we have a key that:
+            //  - may be used for signature verification
+            //  - has a matching algorithm, or if JWT has no algorithm, a matching key type
+            // It may have a matching kid if the JWT has a kid and it matches the key kid
+            //
+            // Multiple keys may meet the matching criteria, but they have a score. They get 1
+            // point for having an explicitly matching algorithm and 1 point for an explicitly
+            // matching kid. We will sort our candidates and pick the key with the highest score.
+
+            // If we find a key with a HIGHEST_SCORE, we will filter the list to only keep those
+            // with that score
+            if key_score == HIGHEST_SCORE {
+                found_highest_score = true;
+            }
+
+            candidates.push((key_score, (issuer.clone(), key)));
+        }
+    }
+
+    tracing::debug!(
+        "jwk candidates: {:?}",
+        candidates
+            .iter()
+            .map(|(score, (_, candidate))| (
+                score,
+                &candidate.common.key_id,
+                candidate.common.key_algorithm
+            ))
+            .collect::<Vec<(&usize, &Option<String>, Option<KeyAlgorithm>)>>()
+    );
+
+    if candidates.is_empty() {
+        None
+    } else {
+        // Only sort if we need to
+        if candidates.len() > 1 {
+            candidates.sort_by(|a, b| a.0.cmp(&b.0));
+        }
+
+        if found_highest_score {
+            Some(
+                candidates
+                    .into_iter()
+                    .filter_map(|(score, candidate)| {
+                        if score == HIGHEST_SCORE {
+                            Some(candidate)
+                        } else {
+                            None
+                        }
+                    })
+                    .collect(),
+            )
+        } else {
+            Some(
+                candidates
+                    .into_iter()
+                    .map(|(_score, candidate)| candidate)
+                    .collect(),
+            )
+        }
+    }
+}
+
+pub(super) fn extract_jwt<'a, 'b: 'a>(
path: apollo-router/src/plugins/authentication/jwks.rs
user: login: bnjjj
body: Oh ok, sorry. Unfortunately it hasn't be done before :p now it's like dark magic
created_at: 2025-03-18T09:38:26Z
author_association: CONTRIBUTOR

PR comments:
user: login: svc-apollo-docs
created_at: 2025-03-05T18:57:10Z
body: ### ✅ Docs preview ready

The preview is ready to be viewed. [View the preview](https://www.apollographql.com/docs/deploy-preview/9c5005b62824de58b17b5d71)

File Changes

<details>
<summary>0 new, 3 changed, 0 removed</summary>

```
* graphos/reference/router/(latest)/rhai.mdx
* graphos/routing/(latest)/security/jwt.mdx
* graphos/routing/(latest)/security/router-authentication.mdx
```

</details>

**Build ID**: 9c5005b62824de58b17b5d71

**URL**: [https://www.apollographql.com/docs/deploy-preview/9c5005b62824de58b17b5d71](https://www.apollographql.com/docs/deploy-preview/9c5005b62824de58b17b5d71)
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-05T18:57:19Z
body: @Velfi, please consider creating a changeset entry in `/.changesets/`. [These instructions](https://github.com/apollographql/router/blob/HEAD/.changesets/README.md) describe the process and tooling.
author_association: CONTRIBUTOR
user: login: dupham-indeed
created_at: 2025-03-10T14:46:25Z
body: Can we make sure in the `continue` option to either send an error code value or send the `jwt_claims` context over to the coprocessor no matter whether the token has expired or not? We might need that for observability purposes
author_association: NONE
user: login: theJC
created_at: 2025-03-11T21:32:34Z
body: > Can we make sure in the `continue` option to either send an error code value or send the `jwt_claims` context over to the coprocessor no matter whether the token has expired or not? We might need that for observability purposes

Historically when we had a case where we had to dig into incorrectly minted JWT's, having access to the claims (even when the JWT was expired or invalid in some way) to be able to provide details via metrics or logs was instrumental into pinpointing what was wrong.
author_association: CONTRIBUTOR
user: login: BrynCooke
created_at: 2025-03-13T12:25:28Z
body: @dupham-indeed You'll be able to configure the coprocessor to send the new context key over. 
author_association: CONTRIBUTOR
user: login: BrynCooke
created_at: 2025-03-18T09:07:09Z
body: @Velfi I've approved, but please update the docs to also include the shape of what will end up in context for the user, otherwise they won't know how to interact with it in Rhai.
author_association: CONTRIBUTOR
user: login: Velfi
created_at: 2025-03-18T13:50:31Z
body: > @Velfi I've approved, but please update the docs to also include the shape of what will end up in context for the user, otherwise they won't know how to interact with it in Rhai.

Ah yes, that would be important 😅. I'll add that before merging.
author_association: CONTRIBUTOR