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"""