-
Notifications
You must be signed in to change notification settings - Fork 119
Add Internal v2 API Access Token Generation for Users #3597
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
aaronskiba
wants to merge
21
commits into
api_v2_dmponline
Choose a base branch
from
aaron/feature/v2-api-token-for-internal-users
base: api_v2_dmponline
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
e46519f
Create internal Doorkeeper app via rake task
aaronskiba ccf0f92
Create `Api::V2::InternalUserAccessTokenService`
aaronskiba 797aeb6
Add "POST /api/v2/internal_user_access_token" action & route
aaronskiba 965896b
Add API v2 section to `/users/edit#api-details`
aaronskiba 4bbf690
Refactor api_token into v2 & legacy partials
aaronskiba 3782f02
Expose API Access tab to all users / restrict legacy token rendering
aaronskiba b4fc307
Improve styling for v2 + legacy API displays
aaronskiba 5436190
Add handling for missing internal OAuth app
aaronskiba 8ef06e2
Add test coverage for internal v2 token generation
aaronskiba 6a54ad5
Set default format for internal_user_access_token route
aaronskiba 6fd8a06
Fix string typo in `refresh_token.js.erb`
aaronskiba e315749
Merge branch 'api_v2_dmponline' into aaron/feature/v2-api-token-for-i…
aaronskiba 9904d76
Remove `redirect_uri` from internal OAuth app
aaronskiba fdae153
Merge branch 'api_v2_dmponline' into aaron/feature/v2-api-token-for-i…
aaronskiba 8cb9227
Adapt internal user v2 token handling to hashed tokens
aaronskiba 1928d53
Switch "Regenerate token" to button & prevent spamming
aaronskiba d06523e
Add copy button to v2 api API Access
momo3404 f98d79c
Add copyToken.js to allow for copying token
momo3404 af44c49
Fix breaking tests in `_v2_api_token.html.erb_spec.rb`
momo3404 b3bd4d8
Add v2 API documentation to API Access page
momo3404 41f1f15
Update documentation links for legacy APIs
momo3404 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
20 changes: 20 additions & 0 deletions
20
app/controllers/api/v2/internal_user_access_tokens_controller.rb
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Api | ||
| module V2 | ||
| # Controller for managing the current user's internal V2 API access token. | ||
| # Provides token rotation for authenticated internal users. | ||
| # See Api::V2::InternalUserAccessTokenService for token implementation details. | ||
| class InternalUserAccessTokensController < ApplicationController | ||
| # POST "/api/v2/internal_user_access_token" | ||
| def create | ||
| authorize current_user, :internal_user_v2_access_token? | ||
| @v2_token = Api::V2::InternalUserAccessTokenService.rotate!(current_user) | ||
| @success = true | ||
| respond_to do |format| | ||
| format.js { render 'users/refresh_token' } | ||
aaronskiba marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| end | ||
| end | ||
| end | ||
| end | ||
| end | ||
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| const initCopyToken = () => { | ||
| document.addEventListener('click', function (e) { | ||
| const button = e.target.closest('#copy-token-btn'); | ||
| if (!button) return; | ||
|
|
||
| e.preventDefault(); | ||
|
|
||
| // Prevent spam clicking | ||
| if (button.disabled) return; | ||
|
|
||
| const tokenInput = document.getElementById('api-token-val'); | ||
| if (!tokenInput) return; | ||
|
|
||
| const originalHTML = button.innerHTML; | ||
|
|
||
| // Disable immediately | ||
| button.disabled = true; | ||
|
|
||
| navigator.clipboard.writeText(tokenInput.value).then(() => { | ||
| // Replace button contents with check icon | ||
| button.innerHTML = '<i class="fa fa-circle-check" aria-hidden="true"></i>'; | ||
|
|
||
| // Restore after 2s | ||
| setTimeout(() => { | ||
| button.innerHTML = originalHTML; | ||
| button.disabled = false; | ||
| }, 2000); | ||
| }).catch(() => { | ||
| button.disabled = false; | ||
| alert('Failed to copy token'); | ||
| }); | ||
| }); | ||
| }; | ||
|
|
||
| initCopyToken(); |
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Api | ||
| module V2 | ||
| # Service responsible for user-scoped v2 API access tokens, strictly for | ||
| # internal users of this application. | ||
| # | ||
| # Tokens issued by this service are functionally equivalent to Personal Access | ||
| # Tokens (PATs) for first-party usage. They are minted directly for a user | ||
| # who is already authenticated in the application, bypassing the standard | ||
| # OAuth 2.0 authorization_code redirect and consent flow. | ||
| # | ||
| # This design is intentional: | ||
| # - tokens are internal to this application (first-party) | ||
| # - tokens are owned by a single user and scoped accordingly | ||
| # - token creation, rotation, and revocation happen entirely within the app UI | ||
| # | ||
| # Tokens are stored as Doorkeeper::AccessToken records to leverage existing | ||
| # scoping, expiry, and revocation mechanisms. | ||
| # | ||
| # This service does NOT support third-party OAuth clients or delegated consent flows. | ||
| class InternalUserAccessTokenService | ||
| READ_SCOPE = 'read' | ||
| INTERNAL_OAUTH_APP_NAME = Rails.application.config.x.application.internal_oauth_app_name | ||
|
|
||
| class << self | ||
| def rotate!(user) | ||
aaronskiba marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| revoke_existing!(user) | ||
|
|
||
| token = Doorkeeper::AccessToken.create!( | ||
| application_id: application!.id, | ||
| resource_owner_id: user.id, | ||
| scopes: READ_SCOPE, | ||
| expires_in: nil # Overrides Doorkeeper's `access_token_expires_in` | ||
| ) | ||
| token.plaintext_token | ||
| end | ||
|
|
||
| # Used by views (e.g. devise/registrations/_v2_api_token.html.erb) to safely | ||
| # gate token UI if the internal OAuth application is missing. | ||
| def application_present? | ||
| application! | ||
| true | ||
| rescue StandardError => e | ||
| Rails.logger.error(e.message) | ||
| false | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def application! | ||
| Doorkeeper::Application.find_by(name: INTERNAL_OAUTH_APP_NAME) || | ||
| raise( | ||
| StandardError, | ||
| "Required Doorkeeper application '#{INTERNAL_OAUTH_APP_NAME}' not found. " \ | ||
| 'Please ensure the application exists in the database.' | ||
| ) | ||
| end | ||
|
|
||
| def revoke_existing!(user) | ||
momo3404 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Doorkeeper::AccessToken.revoke_all_for(application!.id, user) | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,25 +1,12 @@ | ||
| <%# locals: user %> | ||
| <% v2_token ||= nil %> | ||
|
|
||
| <% api_wikis = Rails.configuration.x.application.api_documentation_urls %> | ||
| <div id="api-token" class="col-xs-12"> | ||
| <div class="form-control mb-3 col-xs-8"> | ||
| <%= label_tag(:api_token, _('Access token'), class: 'form-label') %> | ||
| <% if user.api_token.present? %> | ||
| <%= user.api_token %> | ||
| <% else %> | ||
| <%= _("Click the button below to generate an API token") %> | ||
| <% end %> | ||
| </div> | ||
| <div class="form-control mb-3 col-xs-12"> | ||
| <%= label_tag(:api_information, _('Documentation'), class: 'form-label') %> | ||
| <br> | ||
| <%= _('See the <a href="%{api_v0_wiki}">documentation for v0</a> for more details on the original API which includes access to statistics, the full text of plans and the ability to connect users with departments.').html_safe % { api_v0_wiki: api_wikis[:v0] } %></a> | ||
| <br><br> | ||
| <%= _('See the <a href="%{api_v1_wiki}">documentation for v1</a> for more details on the API that supports the <a href="%{rda_standard_url}">RDA Common metadata standard for DMPs.</a>').html_safe % { api_v1_wiki: api_wikis[:v1], rda_standard_url: 'https://github.com/RDA-DMP-Common/RDA-DMP-Common-Standard' } %></a> | ||
| </div> | ||
| <div class="form-control mb-3 col-xs-8"> | ||
| <%= link_to _("Regenerate token"), | ||
| refresh_token_user_path(user), | ||
| class: "btn btn-secondary", remote: true %> | ||
| </div> | ||
| <div id="api-tokens"> | ||
| <%# v2 API token %> | ||
| <%= render partial: "devise/registrations/v2_api_token", locals: { user: user, token: v2_token } %> | ||
|
|
||
| <% if user.can_use_api? %> | ||
| <%# v0/v1 API token %> | ||
| <%= render partial: "devise/registrations/legacy_api_token", locals: { user: user } %> | ||
| <% end %> | ||
| </div> |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| <%# locals: user %> | ||
|
|
||
| <% api_wikis = Rails.configuration.x.application.api_documentation_urls %> | ||
| <div id="legacy-api-token" class="card mb-4"> | ||
| <div class="card-heading"> | ||
| <%= _('Legacy API') %> | ||
| </div> | ||
| <div class="card-body"> | ||
| <div class="form-control mb-3 col-xs-8"> | ||
| <%= label_tag(:api_token, _('Access token'), class: 'form-label') %> | ||
| <% if user.api_token.present? %> | ||
| <code><%= user.api_token %></code> | ||
| <% else %> | ||
| <%= _("Click the button below to generate an API token") %> | ||
| <% end %> | ||
| </div> | ||
| <div class="form-control mb-3 col-xs-12"> | ||
| <%= label_tag(:api_information, _('Documentation'), class: 'form-label') %> | ||
| <br> | ||
| <%= sanitize(_('See the <a href="%{api_v0_wiki}">documentation for v0</a> for more details on the original API which includes access to statistics, the full text of plans and the ability to connect users with departments.') % | ||
| { api_v0_wiki: api_wikis[:v0] }, | ||
| attributes: %w[href] | ||
| )%> | ||
| <br><br> | ||
| <%= sanitize(_('See the <a href="%{api_v1_wiki}">documentation for v1</a> for more details on the API that supports the <a href="%{rda_standard_url}">RDA Common metadata standard for DMPs.</a>') % | ||
| { api_v1_wiki: api_wikis[:v1], rda_standard_url: 'https://github.com/RDA-DMP-Common/RDA-DMP-Common-Standard' }, | ||
| attributes: %w[href] | ||
| )%> | ||
| </div> | ||
| <div class="form-control mb-3 col-xs-8"> | ||
| <%= link_to _("Regenerate token"), | ||
| refresh_token_user_path(user), | ||
| class: "btn btn-secondary", remote: true %> | ||
| </div> | ||
| </div> | ||
| </div> |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| <%# locals: user, token %> | ||
| <% api_wikis = Rails.configuration.x.application.api_documentation_urls %> | ||
|
|
||
| <div id="v2-api-token" class="card mb-4"> | ||
| <div class="card-heading"> | ||
| <%= _('V2 API') %> | ||
| </div> | ||
| <div class="card-body"> | ||
| <% if Api::V2::InternalUserAccessTokenService.application_present? %> | ||
| <div class="form-control mb-3 col-xs-8"> | ||
| <%= label_tag(:api_token, _('Access token'), class: 'form-label') %> | ||
| <% if token.present? %> | ||
| <%= text_field_tag( | ||
| :api_token_val, | ||
| token, | ||
| id: 'api-token-val', | ||
| class: 'form-control', | ||
| style: 'width: auto;', | ||
| readonly: true | ||
| ) %> | ||
|
|
||
| <%= button_tag( | ||
| _('Copy'), | ||
| id: 'copy-token-btn', | ||
| type: 'button', | ||
| class: 'btn btn-secondary' | ||
| ) %> | ||
| <div class="alert alert-warning"> | ||
| <%= _( "Please copy this token now and store it somewhere safely." ) %><br> | ||
| <%= _( "It will disappear after you leave or refresh this page." ) %> | ||
| </div> | ||
| <% else %> | ||
| <%= _( "Click the button below to generate an API token" ) %><br> | ||
| <div class="alert alert-warning"> | ||
| <%= _("If you previously generated and saved a token, please continue using that token.") %> | ||
| </div> | ||
| <% end %> | ||
| </div> | ||
|
|
||
| <div class="form-control mb-3 col-xs-8"> | ||
| <%= button_tag _("Regenerate token"), | ||
| type: :submit, | ||
| class: 'btn btn-secondary', | ||
| disabled: token.present?, | ||
| data: { remote: true, url: api_v2_internal_user_access_token_path, method: :post } %> | ||
| </div> | ||
| <div class="form-group mb-3 col-xs-12"> | ||
| <%= label_tag(:api_information, _('Documentation'), class: 'form-label') %> | ||
| <br> | ||
| <%= sanitize(_('See the <a href="%{api_v2_wiki}">documentation for v2</a> for more details on the API.') % | ||
| { api_v2_wiki: api_wikis[:v2] }, | ||
| attributes: %w[href] | ||
| )%> | ||
| </div> | ||
| <% else %> | ||
| <div class="alert alert-warning"> | ||
| <%= _("V2 API token service is currently unavailable. Please contact us for help.") %> | ||
| <%= mail_to Rails.application.config.x.organisation.helpdesk_email %> | ||
| </div> | ||
| <% end %> | ||
| </div> | ||
| </div> |
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,8 @@ | ||
| var msg = '<%= @success ? _("Successfully regenerate your API token.") : _("Unable to regenerate your API token.") %>'; | ||
| // This view is called by both InternalUserAccessTokensController#create (provides @v2_token) | ||
| // and UsersController#refresh_token (does not provide @v2_token). | ||
| var msg = '<%= @success ? _("Successfully regenerated your API token.") : _("Unable to regenerate your API token.") %>'; | ||
|
|
||
| var context = $('#api-token'); | ||
| context.html('<%= escape_javascript(render partial: "/devise/registrations/api_token", locals: { user: current_user }) %>'); | ||
| var context = $('#api-tokens'); | ||
| context.html('<%= escape_javascript(render partial: "/devise/registrations/api_token", locals: { user: current_user, v2_token: @v2_token }) %>'); | ||
| renderNotice(msg); | ||
| toggleSpinner(false); |
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| namespace :doorkeeper do | ||
| desc 'Ensure internal OAuth application exists' | ||
| task ensure_internal_app: :environment do | ||
| app = Doorkeeper::Application.find_or_create_by!( | ||
| name: Rails.application.config.x.application.internal_oauth_app_name | ||
| ) do |a| | ||
| a.scopes = 'read' | ||
| a.confidential = true | ||
| end | ||
|
|
||
| puts "Internal OAuth app ready (id=#{app.id}, uid=#{app.uid})" | ||
| end | ||
| end |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.