Repository navigation
Enforce the terms of use on the server, limit Bearer tokens to the federation's Keycloak clients, and publish the laptop sign-in client - #36
Open
KFilippopolitis wants to merge 4 commits into
Conversation
This was referenced Oct 16, 2026
The terms of use (NDA) were only enforced by the portal's TermsGuard, so any API client with a valid Bearer token (the mip notebook client, or a researcher's own computer) could use every endpoint without accepting them. When authentication is enabled, TermsOfUseFilter now runs in the security chain right after AuthorizationFilter. An authenticated user, session or Bearer, who has not agreed gets TermsOfUseNotAcceptedException, rendered by ControllerExceptionHandler as the standard 403 ErrorMessage (a plain 403 if it cannot be rendered). - Exempt: SecurityConfiguration.PUBLIC_PATHS (one list now shared by permitAll and the filter), GET /activeUser and POST /activeUser/agreeNDA. /activeUser/token is not exempt. - ActiveUserService.agreeToNDA accepts only portal (OIDC session) logins, so an API client cannot accept the terms on a user's behalf. - The check is one findByUsername per request and never creates users; a user who never opened the portal counts as not agreed. - Authentication-disabled mode does not register the filter. TermsOfUseSecurityChainTest runs the real SecurityConfiguration chain with the real ActiveUserAPI and ControllerExceptionHandler, so moving the filter before authentication fails the build.
…wn client The resource server checked only the issuer, so any access token of the realm was accepted, whichever client it was issued to. Once users' own Keycloak tokens reach notebooks and laptops, another federation's portal or notebook token could call this API, and its authorities claim (that federation's roles) would grant access here. Add an OAuth2TokenValidator<Jwt> bean, which Spring Boot applies to the issuer-uri JwtDecoder: a token passes only when its azp (the client Keycloak issued it to) is KEYCLOAK_CLIENT_ID, the client shared by the portal and JupyterHub. aud is deliberately ignored: Keycloak's default roles scope lists every client the user has roles on there. An empty client ID rejects every Bearer token. Other tokens get 401. The security-chain test's JwtDecoder now applies every validator bean, as Spring Boot's does, and checks that a token of another client is rejected.
jwtAuthenticationConverter collected the authorities claim, realm roles (realm_access) and the roles of every client under resource_access. Realm roles and other clients' roles apply across every federation of the realm, so a role such as research_dataset_all defined there granted data access on this platform. Bearer authorities now come only from the authorities claim (read with the same extractAuthorities as portal sessions) and resource_access.<KEYCLOAK_CLIENT_ID>.roles. Portal sessions are unchanged: they read the ID token's authorities claim. Deployment: dataset and experiment roles (research_dataset_*, research_experiment_all) must be client roles of each federation's client, or put into the access token's authorities claim by a mapper of that client. Users whose roles are realm roles lose that access through the notebook and laptop clients.
… accept its tokens Users' own computers now sign in with a public Keycloak client of the federation instead of borrowing tokens from JupyterHub. - KEYCLOAK_CLI_CLIENT_ID (authentication.cli_client_id, empty by default) names that client. Empty keeps the feature off. - GET /auth-config, public and exempt from the terms-of-use filter, returns the issuer and that client ID so the mip client (`mip login`, device flow) and the portal's token page know where to sign in; it answers 404 when the client is not configured. - The Bearer azp validator also accepts tokens issued to that client. Their roles still come only from the authorities claim and resource_access.<KEYCLOAK_CLIENT_ID>.roles, so the Keycloak client needs the platform client's roles and authorities mapper (see the decision log).
KFilippopolitis
force-pushed
the
feat/remote-api-token
branch
from
October 23, 2026 11:42
a86ca83 to
cf7045a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Researchers call the API with Bearer tokens from notebooks and from their own computers (see mip-jupyter #8). This PR makes the backend safe for that, and lets users' own computers sign in with a public Keycloak client.
azp).audis ignored.authoritiesclaim and from this federation's client roles.GET /services/auth-configpublishes the issuer and the public client (KEYCLOAK_CLI_CLIENT_ID), and tokens issued to that client are accepted.Commits
023340f2feat(security): enforce the terms of use on the server6d4ec49dfix(security): accept only Bearer tokens issued to the federation's own clientf6da383bfix(security): take Bearer roles only from the federation's own clientcf7045adfeat(auth): publish the laptop sign-in client at GET /auth-config and accept its tokensWhat changes
Terms of use on the server (
TermsOfUseFilter)Before this PR, the terms of use (NDA) were enforced only by the portal's
TermsGuard, so any API client with a valid Bearer token could skip them.TermsOfUseFilteris added to the security chain afterAuthorizationFilter, and only whenauthentication.enabledis on. An authenticated user, session or Bearer JWT, who hasn't accepted the terms gets 403. It's rendered byControllerExceptionHandlerwith the usual JSON error body: "Accept the MIP terms of use in the portal before using the API".SecurityConfiguration.PUBLIC_PATHS, one list shared bypermitAlland the filter: login, OAuth2,/auth-config, actuator, OpenAPI and Swagger.GET /activeUserandPOST /activeUser/agreeNDA./activeUser/tokenis not exempt.agreeNDAtakes portal sessions only:ActiveUserService.agreeToNDAaccepts only portal sessions (OAuth2AuthenticationToken). A Bearer client gets the same 403, so it can't accept the terms on the user's behalf.hasAgreedToNDAdoes onefindByUsernameper request and never creates users. A Bearer user who has never opened the portal counts as not agreed.Bearer tokens only from this federation's own clients
keycloakClientValidatoris anOAuth2TokenValidator<Jwt>bean, which Spring Boot adds to the issuer-uriJwtDecoder.azpisKEYCLOAK_CLIENT_ID(the platform client, shared by the portal and JupyterHub) orKEYCLOAK_CLI_CLIENT_ID(when set).audis ignored: Keycloak's defaultrolesscope adds toaudevery client the user has roles on. Another federation's token can therefore name this client inaud, and itsauthoritiesclaim would grant that federation's roles here.Bearer roles only from this federation's client
authoritiesclaim andresource_access.<KEYCLOAK_CLIENT_ID>.roles.realm_access) and other clients' roles. They span every federation of the realm, so they grant nothing here.authoritiesclaim.Laptop sign-in client (
AuthConfigAPI)GET /auth-config, served as/services/auth-config, is public and inPUBLIC_PATHS. It returns{"issuer", "clientId"}: the issuer isspring.security.oauth2.resourceserver.jwt.issuer-uriand the client isauthentication.cli_client_id(KEYCLOAK_CLI_CLIENT_ID).mip loginsays laptop sign-in isn't offered.mip login(mip-jupyter fix: Ebrains harbor hanging when publishing images #8) get tokens for this client. The backend only publishes the client and validates the tokens like any other Bearer token.docs/context/decision-log.md, andKEYCLOAK_CLI_CLIENT_IDis documented in the README.Configuration
KEYCLOAK_CLIENT_IDazpaccepted for portal and JupyterHub tokens, and the only client whoseresource_accessroles countKEYCLOAK_CLI_CLIENT_ID(authentication.cli_client_id)/auth-configreturns it and tokens with thatazpare acceptedBehaviour changes
agreeNDA: a Bearer client callingPOST /activeUser/agreeNDAgets 403.azpis any other client, including other federations' clients, get 401, whatever theiraud.resource_accessroles no longer grant anything to Bearer users.GET /services/auth-config, which returns 404 unlessKEYCLOAK_CLI_CLIENT_IDis set.Rollout checklist
Check these in Keycloak for each federation before deploying:
azpequal to the backend'sKEYCLOAK_CLIENT_ID, meaning JupyterHub uses the same client, as in mip/deployment. Otherwise notebooks get 401.research_dataset_*andresearch_experiment_allmust be client roles of the federation's client, or be put into the access token'sauthoritiesclaim by a mapper of that client that adds only its own roles. Otherwise users lose that access from the notebook and the laptop client.profilescope: the CLI client (KEYCLOAK_CLI_CLIENT_ID) should include theprofilescope, so its tokens carrypreferred_username. The terms check and user lookup use it./auth-configreturns (issuer-uri) must be a publichttps://URL that users' computers can reach. The mip client refuses anything else.mippublic, so they still accept each other's tokens. Give each federation its own client.KEYCLOAK_CLI_CLIENT_IDanywhere (mip Feature/experiment folders #28).How it was tested
mvn -B verifyoncf7045adgave 110 tests and 0 failures. These includeTermsOfUseFilterTest(14),TermsOfUseSecurityChainTest(11, the real filter chain with a test JWT decoder),KeycloakClientValidatorTest(4),JwtAuthoritiesTest(3),AuthConfigAPITest(2) andActiveUserServiceAgreeToNDATest(1).mvn -o testpassed on each of the four commits.mastermerges cleanly, and 124 tests pass.Related PRs
mip login,mip login --tokenandMIP_API_TOKEN, all using/auth-config.KEYCLOAK_CLI_CLIENT_IDfor sign-in from users' own computers): chart valuekeycloak.cliClientId.Known follow-ups
azpvalidator is wired into the real issuer-uriJwtDecoder. The chain test uses a test decoder.profilescope: fall back tosub, or fail clearly, when a token has nopreferred_usernamebecause the CLI client lacks theprofilescope./auth-configwhenissuer-uriis an in-cluster URL.docs/context/decision-log.mdentries are out of date order, and the "Accept only Bearer tokens…" entry still describes the abandonedaudapproach in its context. Tidy both.user.usernameis indexed, since the terms check looks it up on every request.SCOPE_prefix, so a scope named like a role (e.g.research_dataset_all) would grant it. Restrict or prefix it.