From 3f1f19a5182e54808f2e50d933ccf3066ff4843e Mon Sep 17 00:00:00 2001 From: mangomac <333602023+mangomac@users.noreply.github.com> Date: Thu, 12 Nov 2026 16:47:57 +0900 Subject: [PATCH] fix(oidc): preserve configured issuer for token verification (#6934) The OidcAuthVerifier initializer stripped a trailing slash from the configured KEEP_OIDC_ISSUER, but that same value is compared verbatim against the token's iss claim. Providers whose issuer ends with a slash (e.g. Authentik) therefore failed every login with 'Invalid token: Invalid issuer'. The issuer now reaches jwt.decode unaltered; the trailing-slash normalization moves to the one place that needs it, the discovery URL join in _discover_jwks_uri, so {issuer}/.well-known/openid-configuration never gets a double slash. Regression coverage: an issuer with a trailing slash verifies a token whose iss carries that slash (previously a 401), and the discovery URL is single-slash-joined with and without a trailing slash. Both tests fail without this change. Fixes #6934 --- .../oidc/oidc_authverifier.py | 8 ++-- tests/test_oidc_auth.py | 45 +++++++++++++++++++ 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/keep/identitymanager/identity_managers/oidc/oidc_authverifier.py b/keep/identitymanager/identity_managers/oidc/oidc_authverifier.py index 44ddf597ca..c311c30eef 100644 --- a/keep/identitymanager/identity_managers/oidc/oidc_authverifier.py +++ b/keep/identitymanager/identity_managers/oidc/oidc_authverifier.py @@ -31,7 +31,9 @@ def _discover_jwks_uri(issuer: str) -> str: be obtained from the provider's discovery document at ``{issuer}/.well-known/openid-configuration``. """ - discovery_url = f"{issuer}/.well-known/openid-configuration" + # Normalize only the URL join; the configured issuer itself must stay + # unaltered for the exact `iss` comparison during token verification. + discovery_url = f"{issuer.rstrip('/')}/.well-known/openid-configuration" resp = requests.get(discovery_url, timeout=10, headers={"User-Agent": "keep-api"}) resp.raise_for_status() jwks_uri = resp.json().get("jwks_uri") @@ -87,10 +89,6 @@ def __init__(self, scopes: list[str] = []) -> None: "Missing KEEP_OIDC_ISSUER or KEEP_OIDC_CLIENT_ID environment variable" ) - # Remove trailing slash if present on issuer - if self.oidc_issuer.endswith("/"): - self.oidc_issuer = self.oidc_issuer[:-1] - # For id_tokens the OIDC spec guarantees aud=client_id, so client_id is # the default audience. KEEP_OIDC_AUDIENCE overrides it for providers # whose access tokens must be validated instead (e.g. aud="account" diff --git a/tests/test_oidc_auth.py b/tests/test_oidc_auth.py index 2c296924e2..afe32699a4 100644 --- a/tests/test_oidc_auth.py +++ b/tests/test_oidc_auth.py @@ -98,6 +98,51 @@ def _get_verifier(): return IdentityManagerFactory.get_auth_verifier([]) +@pytest.mark.parametrize( + "test_app", + [{**OIDC_ENV, "KEEP_OIDC_ISSUER": "https://idp.example.com/"}], + indirect=True, +) +def test_oidc_issuer_trailing_slash_preserved( + db_session, client, test_app, monkeypatch +): + """#6934: an issuer configured with a trailing slash (e.g. Authentik) must + reach the `iss` comparison unaltered - stripping it made every token fail + with "Invalid token: Invalid issuer".""" + private_pem, public_pem = generate_test_keys() + _patch_jwks_client(monkeypatch, public_pem) + + token = _make_token( + private_pem, groups=["keep-admins"], issuer="https://idp.example.com/" + ) + + result = _get_verifier()( + token=token, api_key=None, authorization=None, request=None + ) + + assert isinstance(result, AuthenticatedEntity) + assert result.role == "admin" + + +def test_oidc_discovery_url_normalizes_trailing_slash(monkeypatch): + """#6934: the discovery endpoint keeps a single-slash join whether or not + the configured issuer ends with one.""" + for issuer in ("https://idp.example.com", "https://idp.example.com/"): + mock_get = MagicMock() + mock_get.return_value.json.return_value = { + "jwks_uri": "https://idp.example.com/jwks" + } + monkeypatch.setattr(oidc_authverifier.requests, "get", mock_get) + + jwks_uri = oidc_authverifier._discover_jwks_uri(issuer) + + assert jwks_uri == "https://idp.example.com/jwks" + assert ( + mock_get.call_args.args[0] + == "https://idp.example.com/.well-known/openid-configuration" + ) + + @pytest.mark.parametrize("test_app", [OIDC_ENV], indirect=True) def test_oidc_valid_token_admin_group(db_session, client, test_app, monkeypatch): """Valid id_token with an admin group -> authenticated as admin + JIT user creation"""