Open
Conversation
Allow editors to authenticate via JWT tokens and access API endpoints on behalf of authorization requests they have active delegations for. The delegation is verified per-request by matching the editor's delegations against the recipient SIRET parameter. Revoked delegations are excluded. Regular (non-editor) tokens are unaffected.
An editor can have multiple active delegations for the same recipient SIRET (from different DataPass requests). Without disambiguation, the API cannot determine which delegation to use. Add a `delegation_id` query parameter that matches `EditorDelegation#id` (a non-inferable UUID) to uniquely identify the delegation. When multiple delegations exist for the same SIRET and no `delegation_id` is provided, return a 422 with error code 00212. Using the EditorDelegation UUID instead of the DataPass external id avoids leaking an enumerable identifier in the URL.
…I access Editors managing multiple client administrations need a single token that, combined with a recipient SIRET, grants access to delegated authorization data — instead of one token per authorization request. - Create editor_tokens table (migration + FK validation) - EditorToken model: JWT with `editor: true` flag, rehash/active/expired/blacklisted - Admin: create tokens from editor edit page, list with status badges - Editor: display tokens table with copy-to-clipboard on delegations page - Routes, i18n, factory, and specs
Both models duplicated validation, scopes (active, unexpired, not_blacklisted) and instance methods (rehash, expired?, blacklisted?, active?). Each model now only defines its own jwt_data payload.
Explain how editor JWTs resolve to a specific authorization request via the recipient SIRET, why delegation_id (non-inferable UUID) is required to disambiguate multiple delegations on the same SIRET, and how revocation and legacy tokens behave.
Historically Rack::Attack discriminated counters by SHA256 of the JWT. With editor tokens, a single JWT can legitimately target many authorization requests via delegations, so per-signature counting gives either an overly generous or underly generous budget depending on the shape of the traffic. Move the discriminator to the authorization request id so that: - classical and editor tokens pointing to the same habilitation share the counter (consistent "per habilitation" semantics); - each delegation of an editor token gets its own counter; - rate_limit_per_minute is sourced from the resolved habilitation rather than from a value baked into the JWT. For classical tokens the id is embedded in the cached JwtUser payload; for editor tokens it is resolved per-request via EditorDelegation using recipient (+ optional delegation_id), memoized in req.env to avoid redundant SQL across throttle rules. When resolution fails (missing or unknown recipient), fall back to "editor:<id>" / "token:<id>" so that global throttles still apply. Public documentation is updated accordingly: wording switches from "par jeton" to "par habilitation", with an explicit note that multiple tokens on the same habilitation share the counter.
The API side of the apistration now reads editor_token.scopes during authentication (JwtTokenService#enhanced_jwt_data_for_editor_token), but the site-side migration and admin creation path never persisted any scope. Tokens issued from the site would therefore be unusable on protected endpoints as soon as the database schema tracks what the migrations actually create. Add the missing jsonb column (with a gin index mirroring tokens), ship it in the JWT payload, and default newly minted editor tokens to the union of scopes granted by the editor's active delegations so they can exercise every habilitation they are delegated for. Runtime authorization remains the source of truth and will narrow this set per delegated authorization request in a follow-up commit.
When an editor token hit a v3+ endpoint with a missing or malformed recipient, the HandleEditorDelegation callback ran before verify_recipient_is_a_siret! and raised NotAuthorizedError because no delegation could be resolved for a non-SIRET value. Bad input was therefore surfaced as a 403 only for editor tokens, diverging from the 422 Invalid Recipient response every other caller gets. Move the include below the recipient validation so that the callback chain halts with the normal 422 before the delegation lookup runs.
HandleEditorDelegation only asserted that a matching EditorDelegation existed; it never projected the chosen authorization request back onto current_user. HandleTokens#authorize_access_to_resource! therefore evaluated scopes against the editor token's own, possibly broader, scope set, and RateLimitingService#ip_forbidden_access? / custom rate limits kept reading the empty values baked into the editor JWT. The effect was that calls outside the delegated DataPass scopes were accepted, and any IP whitelist or per-habilitation rate limit declared on the delegated authorization request was silently skipped. Swap current_user for a JwtUser rebuilt from the delegation's authorization request (scopes, allowed_ips, rate_limit_per_minute, authorization_request_id) and re-run authorize_access_to_resource! after the delegation callback so the scope check sees the narrowed values. For classical tokens the chain is effectively unchanged beyond a minor reordering of callbacks.
The AR-based discriminator silently returned nil whenever the bearer token was not a decodable JWT, which is exactly the FranceConnect flow (opaque access tokens handed straight to the IdP) and the legacy HTTP_X_API_KEY flow. Rack::Attack therefore skipped the throttle, and the RateLimitHeadersMiddleware had nothing to hang RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset on. Fall back to the previous SHA256-of-the-raw-token discriminator when JwtTokenService cannot resolve a user, so these flows keep a stable per-caller bucket and our rswag contracts keep seeing the rate limit headers.
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.
No description provided.