Skip to content

Add Internal v2 API Access Token Generation for Users#1279

Merged
aaronskiba merged 19 commits intoaaron/v2-apifrom
aaron/feature-v2-api-token-for-internal-users-copy
Feb 23, 2026
Merged

Add Internal v2 API Access Token Generation for Users#1279
aaronskiba merged 19 commits intoaaron/v2-apifrom
aaron/feature-v2-api-token-for-internal-users-copy

Conversation

@aaronskiba
Copy link
Collaborator

@aaronskiba aaronskiba commented Feb 17, 2026

NOTE:

This PR is based off of the following: DMPRoadmap#3597. However, that codebase uses Bootstrap 5, whereas this codebase uses Bootstrap 3. To fit the changes to our codebase, the commit Update API Access UI to BS3 (revert after BS5 upgrade) was added.

Changes proposed in this PR:

Summary

This PR introduces support for issuing internal, user-scoped v2 API access tokens, managed entirely within the application (first-party tokens) for internal users.

Key Changes

Create internal Doorkeeper app via rake task

  • Executed via bundle exec rake doorkeeper:ensure_internal_app
  • Creates a first-party Doorkeeper client for issuing internal v2 API tokens
  • Ensures the internal application exists in all environments before token service is used

Create Api::V2::InternalUserAccessTokenService

  • 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.

Add "POST /api/v2/internal_user_access_token" action & route

  • 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)

Add API v2 section to /users/edit#api-details

  • 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

Expose API Access tab to all users / restrict legacy token rendering

  • The API Access tab is now visible to all users to support the new v2 API token,
    which is accessible to everyone.

Add test coverage for internal v2 token generation

  • 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

- 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.
- Replace BS5 classes with BS3 equivalents:
  - data-bs-toggle → data-toggle
  - form-control → form-group
  - card → panel
  - btn-secondary → btn-default
- Add explicit panel borders for V2 and Legacy sections
- Replicate `card-heading` style via `nav nav-tabs`
@github-actions
Copy link

github-actions bot commented Feb 17, 2026

1 Warning
⚠️ There are code changes, but no corresponding tests. Please include tests if this PR introduces any modifications in behavior. \n
Ignore this warning if the PR ONLY contains translation.io synced updates.

Generated by 🚫 Danger

@aaronskiba aaronskiba marked this pull request as ready for review February 17, 2026 21:59
@aaronskiba aaronskiba changed the title Aaron/feature v2 api token for internal users copy Add Internal v2 API Access Token Generation for Users Feb 17, 2026
application_id: application!.id,
resource_owner_id: user.id,
scopes: READ_SCOPE,
expires_in: nil # Overrides Doorkeeper's `access_token_expires_in`
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For security purposes, maybe we should still set some expiry for these tokens?
Also, in config/initializers/doorkeeper.rb, we currently have access_token_expires_in 2.hours. Maybe this token expiry should be extended?

Commit 6e7c21c allows us to have a NULL `redirect_uri`. Previously, this value was merely a placeholder due to the NOT NULL constraint.
@aaronskiba aaronskiba force-pushed the aaron/feature-v2-api-token-for-internal-users-copy branch 4 times, most recently from f8df035 to 047bb92 Compare February 20, 2026 21:15
@aaronskiba
Copy link
Collaborator Author

There are some breaking tests here; however, they are not related to these changes. #1276 resolves these errors with the following commit: 4645f9a

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.
@aaronskiba aaronskiba force-pushed the aaron/feature-v2-api-token-for-internal-users-copy branch from 047bb92 to f63abd1 Compare February 23, 2026 17:08
@aaronskiba aaronskiba merged commit 95c6c89 into aaron/v2-api Feb 23, 2026
9 of 10 checks passed
@aaronskiba aaronskiba deleted the aaron/feature-v2-api-token-for-internal-users-copy branch February 23, 2026 21:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants