Skip to content

Commit

Permalink
Merge branch 'master' into feature/remove_state_from_profile
Browse files Browse the repository at this point in the history
  • Loading branch information
Maksim Litvinov committed May 15, 2018
2 parents 404aeb3 + 77d72df commit 031d752
Show file tree
Hide file tree
Showing 38 changed files with 1,838 additions and 137 deletions.
2 changes: 1 addition & 1 deletion .codeclimate.yml
Expand Up @@ -5,7 +5,7 @@ plugins:
rubocop:
enabled: true
reek:
enabled: true
enabled: false
checks:
FeatureEnvy:
enabled: false
Expand Down
3 changes: 3 additions & 0 deletions Gemfile
Expand Up @@ -33,11 +33,14 @@ gem 'countries', require: 'countries/global'
gem 'fontello_rails_converter'
gem 'bootstrap-datepicker-rails'
gem 'public_suffix'
gem 'devise-security'
gem 'rails_email_validator'

gem 'doorkeeper-jwt', git: 'https://github.com/rubykube/doorkeeper-jwt.git'
gem 'memoist', '~> 0.16'
gem 'jwt', '~> 2.1'
gem 'jwt-multisig', '~> 1.0'
gem 'sentry-raven', '~> 2.7', require: false

group :development, :test do
gem 'listen', '~> 3.1'
Expand Down
10 changes: 10 additions & 0 deletions Gemfile.lock
Expand Up @@ -109,6 +109,9 @@ GEM
railties (>= 4.1.0, < 5.2)
responders
warden (~> 1.2.3)
devise-security (0.12.0)
devise (>= 4.2.0, < 5.0)
rails (>= 4.1.0, < 6.0)
diff-lcs (1.3)
docile (1.3.0)
domain_name (0.5.20170404)
Expand Down Expand Up @@ -276,6 +279,8 @@ GEM
nokogiri (>= 1.6)
rails-html-sanitizer (1.0.4)
loofah (~> 2.2, >= 2.2.2)
rails_email_validator (0.1.4)
activemodel (>= 3.0.0)
railties (5.1.4)
actionpack (= 5.1.4)
activesupport (= 5.1.4)
Expand Down Expand Up @@ -331,6 +336,8 @@ GEM
selenium-webdriver (3.8.0)
childprocess (~> 0.5)
rubyzip (~> 1.0)
sentry-raven (2.7.3)
faraday (>= 0.7.6, < 1.0)
serverengine (1.5.11)
sigdump (~> 0.2.2)
shoulda-matchers (3.1.2)
Expand Down Expand Up @@ -397,6 +404,7 @@ DEPENDENCIES
chromedriver-helper (~> 1.1)
countries
devise (~> 4.4)
devise-security
doorkeeper (~> 4.2.6)
doorkeeper-jwt!
factory_bot_rails (~> 4.8)
Expand Down Expand Up @@ -425,9 +433,11 @@ DEPENDENCIES
rack-cors (~> 1.0.2)
rails (~> 5.1.4)
rails-controller-testing
rails_email_validator
rspec-rails (~> 3.7.1)
sassc-rails (~> 1.3)
selenium-webdriver (~> 3.8)
sentry-raven (~> 2.7)
shoulda-matchers (~> 3.1.2)
simplecov
sneakers (~> 2.6)
Expand Down
18 changes: 18 additions & 0 deletions app/api/entities/api_key.rb
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module Entities
class APIKey < Grape::Entity
format_with(:iso_timestamp) { |d| d.utc.iso8601 }

expose :uid, documentation: { type: 'String' }
expose :public_key, documentation: { type: 'String' }
expose :scopes, documentation: { type: 'String', desc: 'comma separated scopes' }
expose :expires_in, documentation: { type: 'String', desc: 'expires_in duration in seconds. Min 30 seconds, Max 86400 seconds' }
expose :state, documentation: { type: 'String' }

with_options(format_with: :iso_timestamp) do
expose :created_at
expose :updated_at
end
end
end
30 changes: 15 additions & 15 deletions app/api/management_api/v1/jwt_authentication_middleware.rb
Expand Up @@ -36,27 +36,27 @@ def jwt
memoize :jwt

def check_request_method!
unless request.post? || request.put?
raise Exceptions::Authentication, \
message: 'Only POST and PUT verbs are allowed.',
status: 405
end
return if request.post? || request.put?

raise Exceptions::Authentication, \
message: 'Only POST and PUT verbs are allowed.',
status: 405
end

def check_query_parameters!
unless request.GET.empty?
raise Exceptions::Authentication, \
message: 'Query parameters are not allowed.',
status: 400
end
return if request.GET.empty?

raise Exceptions::Authentication, \
message: 'Query parameters are not allowed.',
status: 400
end

def check_content_type!
unless request.content_type == 'application/json'
raise Exceptions::Authentication, \
message: 'Only JSON body is accepted.',
status: 400
end
return if request.content_type == 'application/json'

raise Exceptions::Authentication, \
message: 'Only JSON body is accepted.',
status: 400
end

def check_jwt!(jwt)
Expand Down
96 changes: 96 additions & 0 deletions app/api/user_api/v1/api_keys.rb
@@ -0,0 +1,96 @@
# frozen_string_literal: true

module UserApi
module V1
# Responsible for CRUD for api keys
class APIKeys < Grape::API
resource :api_keys do
before do
unless current_account.otp_enabled
error!('Only accounts with enabled 2FA alowed', 400)
end

unless Vault::TOTP.validate?(current_account.uid, params[:totp_code])
error!('Your code is invalid', 422)
end
end

desc 'List all api keys for current account.'
params do
requires :totp_code, type: String, desc: 'Code from Google Authenticator', allow_blank: false
end
get do
present current_account.api_keys, with: Entities::APIKey
end

desc 'Return an api key by uid'
params do
requires :uid, type: String, allow_blank: false
requires :totp_code, type: String, desc: 'Code from Google Authenticator', allow_blank: false
end
get ':uid' do
api_key = current_account.api_keys.find_by!(uid: params[:uid])
present api_key, with: Entities::APIKey
end

desc 'Create an api key'
params do
requires :public_key, type: String,
allow_blank: false
optional :scopes, type: String,
allow_blank: false,
desc: 'comma separated scopes'
optional :expires_in, type: String,
allow_blank: false,
desc: 'expires_in duration in seconds'
requires :totp_code, type: String, desc: 'Code from Google Authenticator', allow_blank: false
end
post do
declared_params = declared(params, include_missing: false).except(:totp_code)
api_key = current_account.api_keys.create(declared_params)
if api_key.errors.any?
error!(api_key.errors.full_messages.to_sentence, 422)
end

present api_key, with: Entities::APIKey
end

desc 'Updates an api key'
params do
requires :uid, type: String, allow_blank: false
optional :public_key, type: String,
allow_blank: false
optional :scopes, type: String,
allow_blank: false,
desc: 'comma separated scopes'
optional :expires_in, type: String,
allow_blank: false,
desc: 'expires_in duration in seconds'
optional :state, type: String, desc: 'State of API Key. "active" state means key is active and can be used for auth',
allow_blank: false
requires :totp_code, type: String, desc: 'Code from Google Authenticator', allow_blank: false
end
patch ':uid' do
declared_params = declared(params, include_missing: false).except(:totp_code)
api_key = current_account.api_keys.find_by!(uid: params[:uid])
unless api_key.update(declared_params)
error!(api_key.errors.full_messages.to_sentence, 422)
end

present api_key, with: Entities::APIKey
end

desc 'Delete an api key'
params do
requires :uid, type: String, allow_blank: false
requires :totp_code, type: String, desc: 'Code from Google Authenticator', allow_blank: false
end
delete ':uid' do
api_key = current_account.api_keys.find_by!(uid: params[:uid])
api_key.destroy
status 204
end
end
end
end
end
3 changes: 2 additions & 1 deletion app/api/user_api/v1/base.rb
Expand Up @@ -15,7 +15,7 @@ class Base < Grape::API

do_not_route_options!

rescue_from(ActiveRecord::RecordNotFound) { error!('Record is not found', 404) }
rescue_from(ActiveRecord::RecordNotFound) { |_e| error!('Record is not found', 404) }
rescue_from(Vault::VaultError) do |error|
error_message = error.message
Rails.logger.error "#{error.class}: #{error_message}"
Expand Down Expand Up @@ -43,6 +43,7 @@ class Base < Grape::API
mount UserApi::V1::Phones
mount UserApi::V1::Sessions
mount UserApi::V1::Labels
mount UserApi::V1::APIKeys

add_swagger_documentation base_path: '/api',
info: {
Expand Down
16 changes: 16 additions & 0 deletions app/api/user_api/v1/security.rb
Expand Up @@ -84,6 +84,22 @@ class Security < Grape::API
error!(account.errors.full_messages.to_sentence, 422)
end
end

desc 'Verify API key'
params do
requires :uid, type: String, desc: 'API Key uid', allow_blank: false
optional :account_uid, type: String, desc: 'Account uid', allow_blank: false
end
post '/verify_api_key' do
status 200
api_key = APIKey.find_by!(uid: params[:uid])

if params[:account_uid].present? && api_key.account.uid != params[:account_uid]
error!('Account has no api key with provided uid', 422)
end

{ state: api_key.state }
end
end
end
end
Expand Down
63 changes: 63 additions & 0 deletions app/api/user_api/v1/session_jwt_generator.rb
@@ -0,0 +1,63 @@
# frozen_string_literal: true

module UserApi
module V1
class SessionJWTGenerator
ALGORITHM = 'RS256'

def initialize(jwt_token:, kid:)
@kid = kid
@jwt_token = jwt_token
@api_key = APIKey.active.find_by!(uid: kid)
end

def verify_payload
payload, = decode_payload
payload.present?
end

def generate_session_jwt
account = @api_key.account
payload = {
iat: Time.current.to_i,
exp: @api_key.expires_in.seconds.from_now.to_i,
sub: 'session',
iss: 'barong',
aud: @api_key.scopes,
jti: SecureRandom.hex(12).upcase,
uid: account.uid,
email: account.email,
role: account.role,
level: account.level,
state: account.state,
api_kid: @api_key.uid
}

JWT.encode(payload, secret_key, ALGORITHM)
end

private

def secret_key
key_path = ENV['JWT_PRIVATE_KEY_PATH']
private_key = if key_path.present?
File.read(key_path)
else
Base64.urlsafe_decode64(Rails.application.secrets.jwt_shared_secret_key)
end

OpenSSL::PKey.read private_key
end

def decode_payload
public_key = OpenSSL::PKey.read(Base64.urlsafe_decode64(@api_key.public_key))
return {} if public_key.private?

JWT.decode(@jwt_token,
public_key,
true,
APIKey::JWT_OPTIONS)
end
end
end
end
16 changes: 16 additions & 0 deletions app/api/user_api/v1/sessions.rb
Expand Up @@ -28,6 +28,22 @@ class Sessions < Grape::API
error!('Invalid Email or password.', 401)
end
end

desc 'Validates client jwt and generates peatio session jwt'
params do
requires :kid, type: String, allow_blank: false, desc: 'API Key uid'
requires :jwt_token, type: String, allow_blank: false
end
post 'generate_jwt' do
status 200
declared_params = declared(params).symbolize_keys
generator = SessionJWTGenerator.new declared_params
error!('Payload is invalid', 401) unless generator.verify_payload

{ token: generator.generate_session_jwt }
rescue JWT::DecodeError => e
error! "Failed to decode and verify JWT: #{e.inspect}", 401
end
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/assets/javascripts/web.js
Expand Up @@ -16,7 +16,7 @@ window.onload = function () {
headers: { 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') },
method: 'POST',
data: { number: number },
url: 'verification',
url: '/phones/verification',
success: function(result){
if (result.success){
$('.loader').css("display", "none");
Expand Down
4 changes: 3 additions & 1 deletion app/models/account.rb
Expand Up @@ -7,7 +7,7 @@ class Account < ApplicationRecord
# Include default devise modules. Others available are:
# :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable,
:recoverable, :rememberable, :trackable, :secure_validatable,
:confirmable, :lockable

acts_as_eventable prefix: 'account', on: %i[create update]
Expand All @@ -16,9 +16,11 @@ class Account < ApplicationRecord
has_many :phones, dependent: :destroy
has_many :documents, dependent: :destroy
has_many :labels
has_many :api_keys, class_name: 'APIKey'

before_validation :assign_uid

validates :email, email: true
validates :email, uniqueness: true
validates :uid, presence: true, uniqueness: true

Expand Down

0 comments on commit 031d752

Please sign in to comment.