Add Internal v2 API Access Token Generation for Users by aaronskiba · Pull Request #3597 · DMPRoadmap/roadmap

momo3404

@aaronskiba

- Creates a first-party Doorkeeper client for issuing internal v2 API tokens
- Sets redirect_uri to OOB, scopes to 'read', and marks it as confidential
- Ensures the internal application exists in all environments before token service is used
This service manages user-scoped v2 API access tokens for internal app users.

- Tokens are equivalent to first-party Personal Access Tokens (PATs) and are issued directly to authenticated users, bypassing the full OAuth 2.0 authorization_code flow.
- Supports token creation, rotation, and revocation.
- Uses Doorkeeper::AccessToken records for consistent scoping, expiry, and revocation handling.
- Designed strictly for internal usage; third-party OAuth clients are not supported.
Adds `Api::V2::InternalUserAccessTokensController#create` with Pundit authorization and routing. Also reuses the existing `users/refresh_token.js.erb` response to update the UI via JS.

`@success` is read by `app/views/users/refresh_token.js.erb` (similar approach as `UsersController#refresh_token`)
This change updates `app/views/devise/registrations/_api_token.html.erb` to include support for the v2 API access token. Existing v0/v1 token support is retained.
- Introduce V2 token lookup via `Api::V2::InternalUserAccessTokenService`
- Display a dedicated V2 API access token section with its own
  regeneration action
This change breaks refactors `_api_token.html.erb` into additional separate partials:
1) app/views/devise/registrations/_legacy_api_token.html.erb
2) app/views/devise/registrations/_v2_api_token.html.erb

In addition to the refactor, the following changes have been made:
- `<div id="api-token"` has been renamed to `<div id="legacy-api-token"`
- A `<div id="api-tokens">` wrapper has been added in app/views/devise/registrations/_api_token.html.erb.
  - `app/views/users/refresh_token.js.erb` now references the '#api-tokens' wrapper.
The API Access tab is now visible to all users to support the new v2 API token,
which is accessible to everyone.

The existing v0/v1 legacy token remains restricted and continues to use the
previous authorization and rendering logic within the tab.
Styling changes can be viewed at /users/edit#api-details
`InternalUserAccessTokenService`: add `application!` (lookup + raise) and `application_present?` (safe check with logging)

`_v2_api_token.html.erb`: gate token UI on `application_present?` and show a warning when missing.
Add request specs for InternalUserAccessTokensController
- Include both authenticated & unauthenticated user scenarios
- Include both present & absent internal OAuth app scenarios

Add service specs for InternalUserAccessTokenService
- Test token retrieval, rotation, and OAuth app presence
- Verify old token revocation when rotating

Add view specs for API token partials
- Test legacy partial rendering based on `user.can_use_api?`
- Test OAuth application availability scenarios
Add `defaults: { format: :js }` to the internal_user_access_token route, allowing callers to omit the explicit format parameter.

@aaronskiba aaronskiba changed the title Aaron/feature/v2 api token for internal users Add Internal v2 API Access Token Generation for Users

Feb 17, 2026

@aaronskiba

@aaronskiba

Commit 6e7c21c allows us to have a NULL `redirect_uri`. Previously, this value was merely a placeholder due to the NOT NULL constraint.

@aaronskiba

With `hash_token_secrets` enabled, `token.plaintext_token` is only available at creation or rotation time and cannot be retrieved later. This change ensures secure handling of API tokens in line with best practices for hashed token storage.

- Update `InternalUserAccessTokenService#rotate!` to return `token.plaintext_token` at creation time
- Pass the plaintext token from InternalUserAccessTokensController#create to `app/views/devise/registrations/_v2_api_token.html.erb`
- In `_v2_api_token.html.erb`, render the plaintext token when available and display a warning to users to copy and store the token securely, as it will not be shown again after leaving or refreshing the page.
- Updated all affected Spec files as well.
  - The `context 'when user is not authenticated' do` test has been updated. It now enables CSRF protection, enabling the test to accurately capture the behaviour that will be captured in production.
Replace the "Regenerate token" link with a real `<button>` for improved accessibility and native disabled styling.
The button is disabled when a token is present, preventing users from generating multiple tokens in rapid succession.
This change improves user experience and prevents token spamming.

@momo3404 @aaronskiba

- The button initially displays 'Copy', then a check mark after it is clicked for two seconds
- 'code' was replaced by `api-token-val`

@momo3404 @aaronskiba

@momo3404 @aaronskiba