From c30d3e40b19c571cb51335f1de624448ddf53d60 Mon Sep 17 00:00:00 2001 From: E-larex <43210814+E-larex@users.noreply.github.com> Date: Thu, 13 Aug 2026 00:46:51 +0800 Subject: [PATCH] fix(oauth2): accept trailing slash in OIDC issuer (#401) Fix OIDC ID Token validation when the provider returns an issuer with a trailing slash. SyncTV removes trailing slashes from the configured issuer, but `jsonwebtoken` previously performed an exact issuer check before SyncTV's normalized issuer comparison. This caused valid tokens from providers such as Authentik to fail with `InvalidIssuer`. The JWT validation now accepts both trailing-slash variants of the same issuer while continuing to reject unrelated issuers. Added RS256/JWKS regression tests covering both accepted configurations and a different-issuer rejection case. --- synctv-core/src/oauth2/providers/oidc.rs | 86 +++++++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/synctv-core/src/oauth2/providers/oidc.rs b/synctv-core/src/oauth2/providers/oidc.rs index 885b589a..092f5207 100644 --- a/synctv-core/src/oauth2/providers/oidc.rs +++ b/synctv-core/src/oauth2/providers/oidc.rs @@ -903,7 +903,11 @@ impl OidcProvider { fn id_token_validation(&self, alg: Algorithm) -> Validation { let mut validation = Validation::new(alg); if self.issuer_policy == OidcIssuerPolicy::Exact { - validation.set_issuer(&[self.init_config.issuer.as_str()]); + let issuer_with_trailing_slash = format!("{}/", self.init_config.issuer); + validation.set_issuer(&[ + self.init_config.issuer.as_str(), + issuer_with_trailing_slash.as_str(), + ]); } validation.set_audience(&[self.init_config.client_id.as_str()]); validation.set_required_spec_claims(&["exp", "iat", "iss", "sub", "aud"]); @@ -2064,6 +2068,86 @@ oFnGY0OFksX/ye0/XGpy2SFxYRwGU98HPYeBvAQQrVjdkzfy7BmXQQ== assert_eq!(id_token_claims.sub, "subject-123"); } + async fn validate_test_id_token( + configured_issuer: &str, + token_issuer: &str, + ) -> Result { + crate::install_process_crypto_provider(); + + let jwks_uri = + spawn_jwks_server(jwk_set_with_key(test_signing_jwk(Some("test-kid")))).await; + let provider = OidcProvider::create_with_endpoints_and_ssrf_guard( + "id".to_string(), + "secret".to_string(), + configured_issuer, + OidcEndpointOverrides { + auth_url: Some("https://issuer.example.com/authorize".to_string()), + token_url: Some("https://issuer.example.com/token".to_string()), + userinfo_url: None, + jwks_url: Some(jwks_uri), + }, + &synctv_common::ssrf::SsrfGuard::disabled(), + ) + .checked("test OIDC provider should be created"); + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .checked("system clock should be after epoch") + .as_secs(); + let claims = serde_json::json!({ + "iss": token_issuer, + "sub": "subject-123", + "aud": "id", + "iat": now, + "exp": now + 300, + "nonce": "nonce-123" + }); + let token = jsonwebtoken::encode( + &Header { + kid: Some("test-kid".to_string()), + ..Header::new(Algorithm::RS256) + }, + &claims, + &EncodingKey::from_rsa_pem(TEST_RSA_PRIVATE_KEY).checked("test RSA key should parse"), + ) + .checked("test ID token should encode"); + let resolved = provider + .get_resolved() + .await + .checked("test OIDC provider should resolve"); + + provider + .validate_id_token(resolved, &token, "nonce-123") + .await + } + + #[tokio::test] + async fn test_validate_id_token_accepts_trailing_slash_issuer() { + for configured_issuer in [ + "https://issuer.example.com/application/o/synctv", + "https://issuer.example.com/application/o/synctv/", + ] { + let claims = validate_test_id_token( + configured_issuer, + "https://issuer.example.com/application/o/synctv/", + ) + .await + .checked("issuer should match regardless of configured trailing slash"); + + assert_eq!(claims.sub, "subject-123"); + } + } + + #[tokio::test] + async fn test_validate_id_token_rejects_different_issuer() { + let result = validate_test_id_token( + "https://issuer.example.com/application/o/synctv/", + "https://different.example.com/application/o/synctv/", + ) + .await; + + assert!(matches!(result, Err(Error::Authentication(_)))); + } + #[test] fn test_validate_jwk_for_id_token_rejects_algorithm_mismatch() { let jwk = jwk_with_algorithm(Some(KeyAlgorithm::RS512));