Merge pull request #1682 from internetee/1680-registrar-tara-integration

Add TARA support
This commit is contained in:
Timo Võhmar 2020-09-30 19:35:57 +03:00 committed by GitHub
commit a756b52441
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 255 additions and 220 deletions

View file

@ -53,6 +53,11 @@ gem 'digidoc_client',
github: 'tarmotalu/digidoc_client',
ref: '1645e83a5a548addce383f75703b0275c5310c32'
# TARA
gem 'omniauth'
gem 'omniauth-rails_csrf_protection'
gem 'omniauth-tara', github: 'internetee/omniauth-tara'
gem 'epp', github: 'internetee/epp', branch: :master
gem 'epp-xml', '1.1.0', github: 'internetee/epp-xml'

View file

@ -52,6 +52,15 @@ GIT
logger
nokogiri
GIT
remote: https://github.com/internetee/omniauth-tara.git
revision: cec845ec3794532144c4976104a07e206d759aa6
specs:
omniauth-tara (0.3.0)
addressable (~> 2.5)
omniauth (~> 1.3)
openid_connect (~> 1.1)
GIT
remote: https://github.com/tarmotalu/digidoc_client.git
revision: 1645e83a5a548addce383f75703b0275c5310c32
@ -126,6 +135,7 @@ GEM
zeitwerk (~> 2.2, >= 2.2.2)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
aes_key_wrap (1.1.0)
airbrake (11.0.0)
airbrake-ruby (~> 5.0)
airbrake-ruby (5.0.2)
@ -133,9 +143,11 @@ GEM
akami (1.3.1)
gyoku (>= 0.4.0)
nokogiri
attr_required (1.0.1)
autoprefixer-rails (10.0.0.2)
execjs
bcrypt (3.1.16)
bindata (2.4.8)
bootsnap (1.4.8)
msgpack (~> 1.0)
bootstrap-sass (3.4.1)
@ -175,7 +187,7 @@ GEM
data_migrate (6.3.0)
rails (>= 5.0)
database_cleaner (1.8.5)
devise (4.7.2)
devise (4.7.3)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
@ -227,6 +239,7 @@ GEM
temple (>= 0.8.0)
tilt
hashdiff (1.0.1)
hashie (4.1.0)
hpricot (0.8.6)
http-accept (1.7.0)
http-cookie (1.0.3)
@ -247,6 +260,10 @@ GEM
jquery-ui-rails (5.0.5)
railties (>= 3.2.16)
json (2.3.1)
json-jwt (1.13.0)
activesupport (>= 4.2)
aes_key_wrap
bindata
kaminari (1.2.1)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.1)
@ -302,7 +319,23 @@ GEM
nokogiri (1.10.10)
mini_portile2 (~> 2.4.0)
nori (2.6.0)
omniauth (1.9.1)
hashie (>= 3.4.6)
rack (>= 1.6.2, < 3)
omniauth-rails_csrf_protection (0.1.2)
actionpack (>= 4.2)
omniauth (>= 1.3.1)
open4 (1.3.4)
openid_connect (1.2.0)
activemodel
attr_required (>= 1.0.0)
json-jwt (>= 1.5.0)
rack-oauth2 (>= 1.6.1)
swd (>= 1.0.0)
tzinfo
validate_email
validate_url
webfinger (>= 1.0.1)
orm_adapter (0.5.0)
paper_trail (10.3.1)
activerecord (>= 4.2)
@ -326,6 +359,12 @@ GEM
rack (2.2.3)
rack-accept (0.4.5)
rack (>= 0.4)
rack-oauth2 (1.16.0)
activesupport
attr_required
httpclient
json-jwt (>= 1.11.0)
rack (>= 2.1.0)
rack-protection (2.1.0)
rack
rack-test (1.1.0)
@ -366,7 +405,7 @@ GEM
rb-inotify (0.10.1)
ffi (~> 1.0)
rbtree3 (0.6.0)
regexp_parser (1.7.1)
regexp_parser (1.8.0)
request_store (1.5.0)
rack (>= 1.4)
responders (3.0.1)
@ -425,11 +464,15 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
swd (1.2.0)
activesupport (>= 3)
attr_required (>= 0.0.5)
httpclient (>= 2.4)
temple (0.8.2)
thor (0.20.3)
thread_safe (0.3.6)
tilt (2.0.10)
truemail (1.9.0)
truemail (1.9.1)
simpleidn (~> 0.1.1)
tzinfo (1.2.7)
thread_safe (~> 0.1)
@ -439,6 +482,12 @@ GEM
unf_ext
unf_ext (0.0.7.7)
unicode_utils (1.4.0)
validate_email (0.1.6)
activemodel (>= 3.0)
mail (>= 2.2.5)
validate_url (1.0.13)
activemodel (>= 3.0.0)
public_suffix
validates_email_format_of (1.6.3)
i18n
warden (1.2.9)
@ -451,6 +500,9 @@ GEM
nokogiri (~> 1.6)
rubyzip (>= 1.3.0)
selenium-webdriver (>= 3.0, < 4.0)
webfinger (1.1.0)
activesupport
httpclient (>= 2.4)
webmock (3.9.1)
addressable (>= 2.3.6)
crack (>= 0.3.2)
@ -503,6 +555,9 @@ DEPENDENCIES
minitest (~> 5.14)
money-rails
nokogiri
omniauth
omniauth-rails_csrf_protection
omniauth-tara!
paper_trail (~> 10.3)
pdfkit
pg (= 1.2.2)

View file

@ -49,102 +49,6 @@ class Registrar
end
end
def id_card
self.resource = warden.authenticate!(auth_options)
restricted_ip = Authorization::RestrictedIP.new(request.ip)
ip_allowed = restricted_ip.can_access_registrar_area?(resource.registrar)
unless ip_allowed
render plain: t('registrar.authorization.ip_not_allowed', ip: request.ip)
warden.logout(:registrar_user)
return
end
set_flash_message!(:notice, :signed_in)
sign_in(resource_name, resource)
yield resource if block_given?
respond_with resource, location: after_sign_in_path_for(resource)
end
def login_mid
@user = User.new
end
def mid
phone = params[:user][:phone]
endpoint = "#{ENV['sk_digi_doc_service_endpoint']}"
client = Digidoc::Client.new(endpoint)
client.logger = Rails.application.config.logger unless Rails.env.test?
# country_codes = {'+372' => 'EST'}
phone.gsub!('+372', '')
response = client.authenticate(
phone: "+372#{phone}",
message_to_display: 'Authenticating',
service_name: ENV['sk_digi_doc_service_name'] || 'Testing'
)
if response.faultcode
render json: { message: response.detail.message }, status: :unauthorized
return
end
if Setting.registrar_ip_whitelist_enabled
@user = find_user_by_idc_and_allowed(response.user_id_code)
else
@user = find_user_by_idc(response.user_id_code)
end
if @user.persisted?
session[:user_id_code] = response.user_id_code
session[:mid_session_code] = client.session_code
render json: {
message: t(:confirmation_sms_was_sent_to_your_phone_verification_code_is, { code: response.challenge_id })
}, status: :ok
else
render json: { message: t(:no_such_user) }, status: :unauthorized
end
end
def mid_status
endpoint = "#{ENV['sk_digi_doc_service_endpoint']}"
client = Digidoc::Client.new(endpoint)
client.logger = Rails.application.config.logger unless Rails.env.test?
client.session_code = session[:mid_session_code]
auth_status = client.authentication_status
case auth_status.status
when 'OUTSTANDING_TRANSACTION'
render json: { message: t(:check_your_phone_for_confirmation_code) }, status: :ok
when 'USER_AUTHENTICATED'
@user = find_user_by_idc_and_allowed(session[:user_id_code])
sign_in(:registrar_user, @user)
flash[:notice] = t(:welcome)
flash.keep(:notice)
render js: "window.location = '#{after_sign_in_path_for(@user)}'"
when 'NOT_VALID'
render json: { message: t(:user_signature_is_invalid) }, status: :bad_request
when 'EXPIRED_TRANSACTION'
render json: { message: t(:session_timeout) }, status: :bad_request
when 'USER_CANCEL'
render json: { message: t(:user_cancelled) }, status: :bad_request
when 'MID_NOT_READY'
render json: { message: t(:mid_not_ready) }, status: :bad_request
when 'PHONE_ABSENT'
render json: { message: t(:phone_absent) }, status: :bad_request
when 'SENDING_ERROR'
render json: { message: t(:sending_error) }, status: :bad_request
when 'SIM_ERROR'
render json: { message: t(:sim_error) }, status: :bad_request
when 'INTERNAL_ERROR'
render json: { message: t(:internal_error) }, status: :bad_request
else
render json: { message: t(:internal_error) }, status: :bad_request
end
end
private
def depp_controller?

View file

@ -0,0 +1,33 @@
class Registrar
class TaraController < ApplicationController
skip_authorization_check
# rubocop:disable Style/AndOr
def callback
session[:omniauth_hash] = user_hash
@api_user = ApiUser.from_omniauth(user_hash)
if @api_user
flash[:notice] = t(:signed_in_successfully)
sign_in_and_redirect(:registrar_user, @api_user)
else
show_error and return
end
end
# rubocop:enable Style/AndOr
def cancel
redirect_to root_path, notice: t(:sign_in_cancelled)
end
def show_error
redirect_to new_registrar_user_session_url, alert: t(:no_such_user)
end
private
def user_hash
request.env['omniauth.auth']
end
end
end

View file

@ -2,7 +2,7 @@ require 'open3'
class ApiUser < User
include EppErrors
devise :database_authenticatable, :trackable, :timeoutable, :id_card_authenticatable,
devise :database_authenticatable, :trackable, :timeoutable,
authentication_keys: [:username]
def epp_code_map

View file

@ -11,4 +11,11 @@ class User < ApplicationRecord
"#{self.id}-#{self.class}: #{self.username}"
end
def self.from_omniauth(omniauth_hash)
uid = omniauth_hash['uid']
identity_code = uid.slice(2..-1)
# country_code = uid.slice(0..1)
User.find_by(identity_code: identity_code)
end
end

View file

@ -19,12 +19,10 @@
<hr>
<%= link_to '/registrar/login/mid', id: 'login-with-mobile-id-btn' do %>
<%= image_tag 'mid.gif' %>
<% end %>
<%= link_to registrar_id_card_sign_in_path, method: :post do %>
<%= image_tag 'id_card.gif' %>
<% end %>
<div id="tara-sign-in" class="login-block ui segment">
<h3><%= t('.sign_in_with_identity_document') %></h3>
<p><%= t('.identity_document_text')%></p>
<%= link_to t(:sign_in), "/auth/tara", method: :post, class: 'btn btn-lg btn-primary btn-block' %>
</div>
</div>
</div>
</div>

View file

@ -156,6 +156,13 @@ lhv_dev_mode: 'false'
epp_session_timeout_seconds: '300'
contact_archivation_log_file_dir:
tara_host: 'tara-test.ria.ee'
tara_issuer: 'https://tara-test.ria.ee'
tara_identifier: 'identifier'
tara_secret: 'secret'
tara_redirect_uri: 'redirect_url'
tara_keys: "{\"kty\":\"RSA\",\"kid\":\"de6cc4\",\"n\":\"jWwAjT_03ypme9ZWeSe7c-jY26NO50Wo5I1LBnPW2JLc0dPMj8v7y4ehiRpClYNTaSWcLd4DJmlKXDXXudEUWwXa7TtjBFJfzlZ-1u0tDvJ-H9zv9MzO7UhUFytztUEMTrtStdhGbzkzdEZZCgFYeo2i33eXxzIR1nGvI05d9Y-e_LHnNE2ZKTa89BC7ZiCXq5nfAaCgQna_knh4kFAX-KgiPRAtsiDHcAWKcBY3qUVcb-5XAX8p668MlGLukzsh5tFkQCbJVyNtmlbIHdbGvVHPb8C0H3oLYciv1Fjy_tS1lO7OT_cb3GVp6Ql-CG0uED_8pkpVtfsGRviub4_ElQ\",\"e\":\"AQAB\"}"
# Since the keys for staging are absent from the repo, we need to supply them separate for testing.
test:
payments_seb_bank_certificate: 'test/fixtures/files/seb_bank_cert.pem'

View file

@ -43,7 +43,7 @@ Rails.application.configure do
# config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
# config.force_ssl = true
config.force_ssl = true
# Use the lowest log level to ensure availability of diagnostic information
# when problems arise.

View file

@ -9,7 +9,7 @@ Devise.setup do |config|
# Devise will use the `secret_key_base` as its `secret_key`
# by default. You can change it below and use your own secret key.
config.secret_key = ENV['devise_secret']
# ==> Controller configuration
# Configure the parent class to the devise controllers.
# config.parent_controller = 'DeviseController'

View file

@ -0,0 +1,46 @@
OpenIDConnect.logger = Rails.logger
OpenIDConnect.debug!
OmniAuth.config.on_failure = Proc.new { |env|
OmniAuth::FailureEndpoint.new(env).redirect_to_failure
}
OmniAuth.config.logger = Rails.logger
# Block GET requests to avoid exposing self to CVE-2015-9284
OmniAuth.config.allowed_request_methods = [:post]
signing_keys = ENV['tara_keys']
issuer = ENV['tara_issuer']
host = ENV['tara_host']
identifier = ENV['tara_identifier']
secret = ENV['tara_secret']
redirect_uri = ENV['tara_redirect_uri']
Rails.application.config.middleware.use OmniAuth::Builder do
provider "tara", {
callback_path: '/registrar/open_id/callback',
name: 'tara',
scope: ['openid'],
state: Proc.new{ SecureRandom.hex(10) },
client_signing_alg: :RS256,
client_jwk_signing_key: signing_keys,
send_scope_to_token_endpoint: false,
send_nonce: true,
issuer: issuer,
client_options: {
scheme: 'https',
host: host,
authorization_endpoint: '/oidc/authorize',
token_endpoint: '/oidc/token',
userinfo_endpoint: nil, # Not implemented
jwks_uri: '/oidc/jwks',
# Registry
identifier: identifier,
secret: secret,
redirect_uri: redirect_uri,
},
}
end

View file

@ -2,4 +2,4 @@ en:
activerecord:
attributes:
account_activity:
created_at: Receipt date
created_at: Receipt date

View file

@ -295,6 +295,8 @@ en:
authentication_error: 'Authentication error'
sign_in_cancelled: "Sign in cancelled"
transfer_requested: 'Transfer requested.'
message_was_not_found: 'Message was not found'
only_one_parameter_allowed: 'Only one parameter allowed: %{param_1} or %{param_2}'
@ -658,3 +660,5 @@ en:
ipv6: IPv6
reference_no: Reference number
iban: IBAN
sign_in: "Sign in"
signed_in_successfully: "Signed in successfully"

View file

@ -4,6 +4,14 @@ en:
new:
header_html: Eesti Interneti SA<br>Registrar Portal
submit_btn: Login
sign_in_with_identity_document: "Sign in with identity document"
identity_document_text: |
Sign in using Estonian (incl. e-residents) ID card, mobile ID,
Bank link or other EU citizen's electronic ID supported by EIDAS.
login_mid:
header: Log in with mobile-id
submit_btn: Login
tara:
callback:
header_html: "Eesti Interneti SA<br>Registrar Portal"
submit_btn: Login

View file

@ -0,0 +1,14 @@
en:
auth:
tara:
tampering: "Tampering detected. Sign in cancelled."
callback:
title: "Create a user"
errors: "prohibited this user from being saved"
form:
contact_data: "Contact Data"
data_from_identity_document: "Data from identity document"
new_password: "New password"
sign_up: "Sign up"

View file

@ -0,0 +1,14 @@
et:
auth:
tara:
tampering: "Avastatud urkimine. Sisselogimine tühistatud."
callback:
title: "Loo kasutaja"
errors: "seda kasutajat ei saa salvestada"
form:
contact_data: "Kontaktandmed"
data_from_identity_document: "Andmed elektroonselt isikutunnistuselt"
new_password: "Uus salasõna"
sign_up: "Registreeru"

View file

@ -77,14 +77,9 @@ Rails.application.routes.draw do
devise_for :users, path: '', class_name: 'ApiUser', skip: %i[sessions]
devise_scope :registrar_user do
get 'login/mid' => 'sessions#login_mid'
post 'login/mid' => 'sessions#mid'
post 'login/mid_status' => 'sessions#mid_status'
# /registrar/id path is hardcoded in Apache config for authentication with Estonian ID-card
post 'id' => 'sessions#id_card', as: :id_card_sign_in
post 'mid' => 'sessions#mid'
match '/open_id/callback', via: %i[get post], to: 'tara#callback', as: :tara_callback
match '/open_id/cancel', via: %i[get post delete], to: 'tara#cancel',
as: :tara_cancel
end
resources :invoices, except: %i[new create edit update destroy] do

View file

@ -46,4 +46,4 @@ module Devise
end
end
Warden::Strategies.add(:id_card_authenticatable, Devise::Strategies::IdCardAuthenticatable)
Warden::Strategies.add(:id_card_authenticatable, Devise::Strategies::IdCardAuthenticatable)

View file

@ -1,65 +0,0 @@
require 'test_helper'
class RegistrarAreaIdCardSignInTest < ApplicationIntegrationTest
setup do
@user = users(:api_bestnames)
@original_registrar_area_ip_whitelist = Setting.registrar_ip_whitelist_enabled
end
teardown do
Setting.registrar_ip_whitelist_enabled = @original_registrar_area_ip_whitelist
end
def test_signs_in_a_user_when_id_card_owner_is_found
assert_equal '1234', @user.identity_code
post registrar_id_card_sign_in_path, headers: { 'SSL_CLIENT_S_DN_CN' => 'DOE,JOHN,1234' }
follow_redirect!
assert_response :ok
assert_equal registrar_root_path, path
assert_not_nil controller.current_registrar_user
end
def test_does_not_sign_in_a_user_when_id_card_owner_is_not_found
post registrar_id_card_sign_in_path,
headers: { 'SSL_CLIENT_S_DN_CN' => 'DOE,JOHN,unacceptable-personal-code' }
assert_nil controller.current_registrar_user
assert_equal registrar_id_card_sign_in_path, path
assert_includes response.body, 'Failed to Login'
end
def test_does_not_sign_in_a_user_when_id_card_owner_is_found_but_ip_is_not_allowed
allow_access_to_sign_in_page
assert_equal '127.0.0.1', white_ips(:one).ipv4
assert_equal '1234', @user.identity_code
Setting.registrar_ip_whitelist_enabled = true
post registrar_id_card_sign_in_path, headers: { 'SSL_CLIENT_S_DN_CN' => 'DOE,JOHN,1234',
'REMOTE_ADDR' => '127.0.0.2' }
assert_equal registrar_id_card_sign_in_path, path
assert_equal 'Access denied from IP 127.0.0.2', response.body
get registrar_root_path
assert_redirected_to new_registrar_user_session_path
end
def test_does_not_sign_in_a_user_when_certificate_is_absent
post registrar_id_card_sign_in_path, headers: { 'SSL_CLIENT_S_DN_CN' => '' }
assert_nil controller.current_registrar_user
assert_equal registrar_id_card_sign_in_path, path
end
private
def allow_access_to_sign_in_page
another_registrar_white_ip = white_ips(:one).dup
another_registrar_white_ip.ipv4 = '127.0.0.2'
another_registrar_white_ip.registrar = registrars(:goodnames)
another_registrar_white_ip.save!
end
end

View file

@ -1,35 +0,0 @@
require 'application_system_test_case'
class RegistrarAreaMobileIDSignInTest < JavaScriptApplicationSystemTestCase
def setup
super
WebMock.allow_net_connect!
@user = users(:api_bestnames)
@user.identity_code = '1234'
@user.save
end
def test_valid_phone_number
mock_client = Minitest::Mock.new
mock_client.expect(:authenticate,
OpenStruct.new(user_id_code: '1234', challenge_id: '1234'),
[{ phone: "+3721234",
message_to_display: "Authenticating",
service_name: "Testimine" }])
mock_client.expect(:session_code, 1234)
Digidoc::Client.stub(:new, mock_client) do
visit new_registrar_user_session_path
click_on 'login-with-mobile-id-btn'
fill_in 'user[phone]', with: '1234'
click_button 'Login'
flash_message = page.find('div.bg-success')
assert_equal('Confirmation sms was sent to your phone. Verification code is 1234.',
flash_message.text)
end
end
end

View file

@ -0,0 +1,45 @@
require 'application_system_test_case'
class TaraUsersTest < ApplicationSystemTestCase
def setup
super
OmniAuth.config.test_mode = true
@user = users(:api_bestnames)
@existing_user_hash = {
'provider' => 'tara',
'uid' => "EE" + @user.identity_code
}
@new_user_hash = {
'provider' => 'tara',
'uid' => 'EE51007050604'
}
end
def teardown
super
OmniAuth.config.test_mode = false
OmniAuth.config.mock_auth['tara'] = nil
end
def test_existing_user_gets_signed_in
OmniAuth.config.mock_auth[:tara] = OmniAuth::AuthHash.new(@existing_user_hash)
visit new_registrar_user_session_path
click_link('Sign in')
assert_text('Signed in successfully')
end
def test_nonexisting_user_gets_error_message
OmniAuth.config.mock_auth[:tara] = OmniAuth::AuthHash.new(@new_user_hash)
visit new_registrar_user_session_path
click_link('Sign in')
assert_text('No such user')
end
end