Implement API key auth and generation [SCI-6968]

This commit is contained in:
Martin Artnik 2022-07-12 16:45:07 +02:00
parent 19442b6157
commit c5b215af32
11 changed files with 141 additions and 0 deletions

View file

@ -146,3 +146,36 @@
}
}
}
.api-key-container {
border: $border-default;
padding: 1em;
.title {
margin-top: 0;
}
.description {
margin-bottom: .5em;
}
.api-key-display {
margin: 1em 0;
}
.api-key-field {
width: 100%;
}
.api-key-controls {
display: flex;
.btn {
margin-right: .5em;
}
}
.api-key-error {
color: $brand-danger;
}
}

View file

@ -106,6 +106,14 @@ module Api
)
end
rescue_from Api::V1::ApiKeyError do |e|
render_error(
e.message,
I18n.t('api.core.invalid_api_key_detail'),
:unauthorized
)
end
before_action :check_include_param, only: %i(index show)
def index

View file

@ -1,5 +1,12 @@
# frozen_string_literal: true
module Api
module V1
class ApiKeyError < StandardError
end
end
end
module TokenAuthentication
extend ActiveSupport::Concern
@ -13,7 +20,21 @@ module TokenAuthentication
raise JWT::InvalidPayload, I18n.t('api.core.no_azure_user_mapping') unless current_user
end
def authenticate_with_api_key
@api_key = request.headers['Api-Key']
return unless @api_key
@current_user = User.from_api_key(@api_key)
raise Api::V1::ApiKeyError, I18n.t('api.core.invalid_api_key') unless @current_user
@current_user
end
def authenticate_request!
# API key authentication successful
return if authenticate_with_api_key
@token = request.headers['Authorization']&.sub('Bearer ', '')
raise JWT::VerificationError, I18n.t('api.core.missing_token') unless @token

View file

@ -207,6 +207,18 @@ class Users::RegistrationsController < Devise::RegistrationsController
render json: { qr_code: create_2fa_qr_code(current_user) }
end
def regenerate_api_key
current_user.regenerate_api_key!
redirect_to edit_user_registration_path
end
def revoke_api_key
current_user.revoke_api_key!
redirect_to edit_user_registration_path
end
protected
# Called upon creating User (before .save). Permits parameters and extracts

View file

@ -509,6 +509,21 @@ class User < ApplicationRecord
.find_by(user_identities: { provider: provider_conf['provider_name'], uid: token_payload[:sub] })
end
def self.from_api_key(api_key)
where('api_key_expires_at > ?', Time.current).find_by(api_key: api_key)
end
def regenerate_api_key!
update!(
api_key: SecureRandom.urlsafe_base64(33),
api_key_expires_at: Constants::API_KEY_EXPIRES_IN.from_now
)
end
def revoke_api_key!
update!(api_key: nil, api_key_expires_at: nil)
end
def has_linked_account?(provider)
user_identities.exists?(provider: provider)
end

View file

@ -35,6 +35,10 @@
</div>
</div>
<%= render partial: 'users/registrations/edit_partials/2fa' %>
<% if Rails.application.config.x.core_api_v1_enabled %>
<%= render partial: 'users/registrations/edit_partials/api_key' %>
<% end %>
</div>
</div>

View file

@ -0,0 +1,24 @@
<div class="api-key-container">
<h3 class="title"><%= t("users.registrations.edit.api_key.title") %></h3>
<div class="description">
<%= t("users.registrations.edit.api_key.description") %>
</div>
<% if current_user.api_key %>
<div class="api-key-display">
<%= text_field_tag :api_key, current_user.api_key, class: "api-key-field", disabled: "disabled" %>
<% if current_user.api_key_expires_at < Time.current %>
<p class="api-key-error">
<%= t("users.registrations.edit.api_key.expired") %>
</p>
<% end %>
</div>
<% end %>
<div class="api-key-controls">
<% if current_user.api_key %>
<%= button_to t("users.registrations.edit.api_key.regenerate"), users_api_key_regenerate_path, class: "btn btn-primary" %>
<%= button_to t("users.registrations.edit.api_key.revoke"), users_api_key_revoke_path, class: "btn btn-danger" %>
<% else %>
<%= button_to t("users.registrations.edit.api_key.generate"), users_api_key_regenerate_path, class: "btn btn-primary" %>
<% end %>
</div>
</div>

View file

@ -247,6 +247,8 @@ class Constants
TWO_FACTOR_RECOVERY_CODE_COUNT = 6
TWO_FACTOR_RECOVERY_CODE_LENGTH = 12
API_KEY_EXPIRES_IN = 1.year
#=============================================================================
# Protocol importers
#=============================================================================

View file

@ -2825,6 +2825,13 @@ en:
validation: "Are you sure you want to remove it?"
cancel: "Cancel"
remove: "Remove"
api_key:
title: "API Key"
description: "Generate or revoke an API key for use with the SciNote API. Regenerating the API key will invalidate the old one."
generate: "Generate"
regenerate: "Regenerate"
revoke: "Revoke"
expired: "This key has expired!"
new:
head_title: "Sign up"
team_name_label: "Team name"
@ -4038,6 +4045,8 @@ en:
api:
core:
status_ok: "Ok"
invalid_api_key: "API key is invalid or expired"
invalid_api_key_detail: "The API key you are using does not exist or has expired."
expired_token: "Token is expired"
invalid_token: "Token is invalid"
missing_token: "Core: No token in the header"

View file

@ -852,6 +852,9 @@ Rails.application.routes.draw do
post 'users/2fa_enable' => 'users/registrations#two_factor_enable'
post 'users/2fa_disable' => 'users/registrations#two_factor_disable'
get 'users/2fa_qr_code' => 'users/registrations#two_factor_qr_code'
post 'users/api_key_regenerate' => 'users/registrations#regenerate_api_key'
post 'users/api_key_revoke' => 'users/registrations#revoke_api_key'
end
namespace :api, defaults: { format: 'json' } do

View file

@ -0,0 +1,10 @@
# frozen_string_literal: true
class AddApiKeyToUsers < ActiveRecord::Migration[6.1]
def change
change_table :users, bulk: true do |t|
t.string :api_key
t.datetime :api_key_expires_at
end
end
end