Commit 9c7e6c6e authored by mh's avatar mh
Browse files

Merge branch 'wip-update-gems' into 'master'

fix #21 - update gems

Closes #21

See merge request !49
parents cb636fc4 ff1e2d74
Pipeline #7996 passed with stage
in 2 minutes and 35 seconds
source 'https://rubygems.org'
gem 'sinatra', '~> 2.0'
gem 'sinatra', '~> 2.1'
gem 'sinatra-contrib'
gem 'thin'
gem 'activerecord'
gem 'actionmailer'
gem 'actionmailer', '~> 5.2'
gem 'pg'
gem 'bcrypt'
gem 'libcdb-ruby'
......@@ -13,17 +13,15 @@ gem 'delayed_job_active_record'
gem 'delayed_cron_job'
gem 'rotp'
gem "webauthn", "~> 1.17.0"
gem "webauthn", "~> 2.5"
gem 'saml_idp'
gem 'xmlenc'
gem 'composite_primary_keys', '~>11.0'
gem 'composite_primary_keys'
gem 'rest-client'
gem 'nokogiri'
group :development do
gem 'rerun'
gem 'sqlite3', '< 1.4'
......
GEM
remote: https://rubygems.org/
specs:
actionmailer (5.2.4.4)
actionpack (= 5.2.4.4)
actionview (= 5.2.4.4)
activejob (= 5.2.4.4)
actionmailer (5.2.6)
actionpack (= 5.2.6)
actionview (= 5.2.6)
activejob (= 5.2.6)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (5.2.4.4)
actionview (= 5.2.4.4)
activesupport (= 5.2.4.4)
actionpack (5.2.6)
actionview (= 5.2.6)
activesupport (= 5.2.6)
rack (~> 2.0, >= 2.0.8)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.2.4.4)
activesupport (= 5.2.4.4)
actionview (5.2.6)
activesupport (= 5.2.6)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
activejob (5.2.4.4)
activesupport (= 5.2.4.4)
activejob (5.2.6)
activesupport (= 5.2.6)
globalid (>= 0.3.6)
activemodel (5.2.4.4)
activesupport (= 5.2.4.4)
activerecord (5.2.4.4)
activemodel (= 5.2.4.4)
activesupport (= 5.2.4.4)
activemodel (5.2.6)
activesupport (= 5.2.6)
activerecord (5.2.6)
activemodel (= 5.2.6)
activesupport (= 5.2.6)
arel (>= 9.0)
activesupport (5.2.4.4)
activesupport (5.2.6)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
android_key_attestation (0.3.0)
arel (9.0.0)
awrence (1.2.1)
bcrypt (3.1.16)
bindata (2.4.8)
bindata (2.4.10)
builder (3.2.4)
cbor (0.5.9.6)
composite_primary_keys (11.3.1)
activerecord (~> 5.2.4)
concurrent-ruby (1.1.7)
cose (0.7.0)
concurrent-ruby (1.1.9)
cose (1.2.0)
cbor (~> 0.5.9)
openssl-signature_algorithm (~> 1.0)
crass (1.0.6)
daemons (1.3.1)
database_cleaner (1.8.5)
delayed_cron_job (0.7.3)
daemons (1.4.0)
database_cleaner (2.0.1)
database_cleaner-active_record (~> 2.0.0)
database_cleaner-active_record (2.0.1)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
delayed_cron_job (0.7.4)
delayed_job (>= 4.1)
delayed_job (4.1.8)
activesupport (>= 3.0, < 6.1)
delayed_job_active_record (4.1.4)
activerecord (>= 3.0, < 6.1)
delayed_job (4.1.9)
activesupport (>= 3.0, < 6.2)
delayed_job_active_record (4.1.6)
activerecord (>= 3.0, < 6.2)
delayed_job (>= 3.0, < 5)
diff-lcs (1.4.4)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
erubi (1.9.0)
erubi (1.10.0)
eventmachine (1.2.7)
ffi (1.13.1)
ffi (1.15.3)
globalid (0.4.2)
activesupport (>= 4.2.0)
http-accept (1.7.0)
http-cookie (1.0.3)
http-cookie (1.0.4)
domain_name (~> 0.5)
i18n (1.8.5)
i18n (1.8.10)
concurrent-ruby (~> 1.0)
jwt (2.2.2)
jwt (2.2.3)
libcdb-ruby (0.2.1)
listen (3.2.1)
listen (3.5.1)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
loofah (2.7.0)
loofah (2.10.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
macaddr (1.7.2)
systemu (~> 2.6.5)
mail (2.7.1)
mini_mime (>= 0.1.1)
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2020.0512)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.14.2)
mime-types-data (3.2021.0704)
mini_mime (1.1.0)
mini_portile2 (2.5.3)
minitest (5.14.4)
multi_json (1.15.0)
mustermann (1.1.1)
ruby2_keywords (~> 0.0.1)
netrc (0.11.0)
nokogiri (1.10.10)
mini_portile2 (~> 2.4.0)
nokogiri (1.11.7)
mini_portile2 (~> 2.5.0)
racc (~> 1.4)
openssl (2.2.0)
openssl-signature_algorithm (1.1.1)
openssl (~> 2.0)
pg (1.2.3)
racc (1.5.2)
rack (2.2.3)
rack-protection (2.1.0)
rack
......@@ -103,38 +113,41 @@ GEM
nokogiri (>= 1.6)
rails-html-sanitizer (1.3.0)
loofah (~> 2.3)
rb-fsevent (0.10.4)
rb-fsevent (0.11.0)
rb-inotify (0.10.1)
ffi (~> 1.0)
rerun (0.13.0)
rerun (0.13.1)
listen (~> 3.0)
rest-client (2.1.0)
http-accept (>= 1.7.0, < 2.0)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
rotp (6.1.0)
rspec (3.9.0)
rspec-core (~> 3.9.0)
rspec-expectations (~> 3.9.0)
rspec-mocks (~> 3.9.0)
rspec-core (3.9.2)
rspec-support (~> 3.9.3)
rspec-expectations (3.9.2)
rexml (3.2.5)
rotp (6.2.0)
rspec (3.10.0)
rspec-core (~> 3.10.0)
rspec-expectations (~> 3.10.0)
rspec-mocks (~> 3.10.0)
rspec-core (3.10.1)
rspec-support (~> 3.10.0)
rspec-expectations (3.10.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-mocks (3.9.1)
rspec-support (~> 3.10.0)
rspec-mocks (3.10.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-support (3.9.3)
ruby-saml (1.11.0)
nokogiri (>= 1.5.10)
ruby2_keywords (0.0.2)
saml_idp (0.9.0)
rspec-support (~> 3.10.0)
rspec-support (3.10.2)
ruby-saml (1.12.2)
nokogiri (>= 1.10.5)
rexml
ruby2_keywords (0.0.5)
safety_net_attestation (0.4.0)
jwt (~> 2.0)
saml_idp (0.12.0)
activesupport (>= 3.2)
builder (>= 3.0)
nokogiri (>= 1.6.2)
uuid (>= 2.3)
securecompare (1.0.0)
sinatra (2.1.0)
mustermann (~> 1.0)
......@@ -148,27 +161,30 @@ GEM
sinatra (= 2.1.0)
tilt (~> 2.0)
sqlite3 (1.3.13)
systemu (2.6.5)
thin (1.7.2)
thin (1.8.1)
daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4)
rack (>= 1, < 3)
thread_safe (0.3.6)
tilt (2.0.10)
tzinfo (1.2.7)
tpm-key_attestation (0.10.0)
bindata (~> 2.4)
openssl-signature_algorithm (~> 1.0)
tzinfo (1.2.9)
thread_safe (~> 0.1)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.7)
uuid (2.3.9)
macaddr (~> 1.0)
webauthn (1.17.0)
webauthn (2.5.0)
android_key_attestation (~> 0.3.0)
awrence (~> 1.1)
bindata (~> 2.4)
cbor (~> 0.5.9)
cose (~> 0.7.0)
jwt (>= 1.5, < 3.0)
openssl (~> 2.0)
cose (~> 1.1)
openssl (~> 2.1)
safety_net_attestation (~> 0.4.0)
securecompare (~> 1.0)
tpm-key_attestation (~> 0.10.0)
xmlenc (0.7.1)
activemodel (>= 3.0.0)
activesupport (>= 3.0.0)
......@@ -181,15 +197,14 @@ PLATFORMS
ruby
DEPENDENCIES
actionmailer
actionmailer (~> 5.2)
activerecord
bcrypt
composite_primary_keys (~> 11.0)
composite_primary_keys
database_cleaner
delayed_cron_job
delayed_job_active_record
libcdb-ruby
nokogiri
pg
rack-test
rerun
......@@ -199,11 +214,11 @@ DEPENDENCIES
rspec-mocks
ruby-saml
saml_idp
sinatra (~> 2.0)
sinatra (~> 2.1)
sinatra-contrib
sqlite3 (< 1.4)
thin
webauthn (~> 1.17.0)
webauthn (~> 2.5)
xmlenc
BUNDLED WITH
......
require 'webauthn'
class WebauthnAddSignCount < ActiveRecord::Migration[5.2]
def change
add_column :web_authn_credentials, :signcount, :integer, default: 0
end
end
require 'webauthn'
class WebauthnMigrate < ActiveRecord::Migration[5.2]
def up
# We now store keys ids in the original webauthn urlsafe base64
WebAuthnCredential.all.each do |w|
w.external_id = WebAuthn.standard_encoder.encode(Base64.decode64(w.external_id))
w.public_key = WebAuthn.standard_encoder.encode(Base64.decode64(w.public_key))
w.save!
end
end
end
......@@ -104,6 +104,7 @@ class IApi < Sinatra::Base
WebAuthn.configure do |config|
config.rp_name = IApiConf.webauthn_rp_name
config.rp_id = IApiConf.webauthn_rp_id
config.origin = IApiConf.webauthn_verify_origin
end
super do |server|
server.ssl = true
......
......@@ -111,9 +111,6 @@ class IApiConf
def webauthn_rp_id
required_key(config('webauthn',true),'rp_id')
end
def webauthn_rp_icon
required_key(config('webauthn',true),'rp_icon')
end
# https://manage.example.com
def webauthn_register_origin
required_key(config('webauthn',true),'register_origin')
......
......@@ -51,13 +51,9 @@ class AuthManager
def webauthn_challenge(email)
with_active_mailbox(email) do |user|
options = if user.web_authn_credentials.present?
{
challenge: bin_to_str(WebAuthn.credential_request_options[:challenge]),
allow_credentials: user.web_authn_credentials.pluck(:external_id).map{|id|
{ id: id, type: "public-key" }
},
WebAuthn::Credential.options_for_get(
rp_id: IApiConf.webauthn_rp_id,
}
allow: user.web_authn_credentials.pluck(:external_id))
else
{ challenge: false }
end
......@@ -348,28 +344,27 @@ class AuthManager
end
def validate_webauthn(user, response_data)
auth_response = WebAuthn::AuthenticatorAssertionResponse.new(
credential_id: str_to_bin(response_data['credential_id']),
client_data_json: str_to_bin(response_data['client_data_json']),
authenticator_data: str_to_bin(response_data['authenticator_data']),
signature: str_to_bin(response_data['signature']),
)
allowed_credential = user.web_authn_credentials.find_by_external_id(response_data['credential_id'])
unless allowed_credential
IApiLog.info("WebAuthn validation for user #{user.email} failed. No matching credential (response_data['credential_id']) found!")
id = response_data['credential_id']
response = response_data.to_camelback_keys
response['clientDataJSON'] = response['clientDataJson']
webauthn_credential = WebAuthn::Credential.from_get({
"id" => id,
"rawId" => id,
"type" => WebAuthn::TYPE_PUBLIC_KEY,
"response" => response})
stored_credential = user.web_authn_credentials.find_by_external_id(id)
unless stored_credential
IApiLog.info("WebAuthn validation for user #{user.email} failed. No matching credential #{id} found!")
return {state: 'fail'}
end
allowed_credentials = [{
type: "public-key",
id: Base64.strict_decode64(allowed_credential.external_id),
public_key: Base64.strict_decode64(allowed_credential.public_key),
}]
if auth_response.verify(
str_to_bin(response_data['challenge']),
IApiConf.webauthn_verify_origin,
allowed_credentials: allowed_credentials,
rp_id: IApiConf.webauthn_rp_id,
)
if webauthn_credential.verify(
response_data['challenge'],
public_key: stored_credential.public_key,
sign_count: stored_credential.signcount)
stored_credential.update!(signcount: webauthn_credential.sign_count)
return { state: 'success' }
else
IApiLog.info("WebAuthn validation for user #{user.email} failed.")
......
......@@ -10,7 +10,7 @@ class SamlManager
return
end
saml_request = SamlIdp::Request.from_deflated_request(req)
unless saml_request.authn_request?
unless saml_request.valid? && saml_request.authn_request?
IApiLog.warn("SAML request is malformed or not an authn request")
return
end
......@@ -42,7 +42,7 @@ class SamlManager
# the response, since we cannot tell from the request, which one is
# the currently active one
unless attributes[:allow_unsigned_requests]
unless saml_request.service_provider.should_validate_signature?
unless saml_request.service_provider.attributes[:validate_signature]
IApiLog.warn("SP #{saml_request.issuer} is misconfigured. we are "+
"not sure it has to sign")
return
......@@ -81,13 +81,14 @@ class SamlManager
user = EmailUser.active_by_email(email)
return unless user
response_id = UUID.generate
reference_id = UUID.generate
response_id = SecureRandom.uuid
reference_id = SecureRandom.uuid
audience_uri = saml_request.issuer
opt_issuer_uri = SamlIdp.config.base_saml_location
acs_url = saml_request.acs_url
expiry = 60
session_expiry = 60*60
signed_message_opts = true
encryption_opts = {
cert: saml_request.service_provider.cert,
......@@ -126,7 +127,8 @@ class SamlManager
auth_kind,
expiry,
encryption_opts,
session_expiry
session_expiry,
signed_message_opts
).build
return {
......
......@@ -312,18 +312,12 @@ class UserManager
IApiLog.error "Error to init WebAuthn for #{email}: #{msg}"
return { state: 'fail', msg: msg }
end
credential_options = WebAuthn.credential_creation_options(
rp_name: IApiConf.webauthn_rp_name,
user_name: email,
display_name: email,
user_id: bin_to_str(email)
)
credential_options[:challenge] = bin_to_str(credential_options[:challenge])
credential_options[:rp][:id] = IApiConf.webauthn_rp_id
credential_options[:rp][:icon] = IApiConf.webauthn_rp_icon
credential_options[:exclude_credentials] = \
user.web_authn_credentials.select(:external_id).map{|e| { type: 'public-key', id: e.external_id } }
return credential_options
WebAuthn::Credential.options_for_create(
rp: { id: IApiConf.webauthn_rp_id, name: IApiConf.webauthn_rp_name },
user: { id: bin_to_str(email), name: email },
exclude: user.web_authn_credentials.select(:external_id).map do |e|
e.external_id
end).send(:to_hash).to_camelback_keys
end
end
......@@ -334,22 +328,19 @@ class UserManager
IApiLog.error "Error to verify WebAuthn for #{email}: #{msg}"
return { state: 'fail', msg: msg }
end
auth_response = WebAuthn::AuthenticatorAttestationResponse.new(
attestation_object: str_to_bin(attestation_object),
client_data_json: str_to_bin(client_data_json),
)
if auth_response.verify(
str_to_bin(current_challenge),
webauthn_credential = WebAuthn::AuthenticatorAttestationResponse.from_client({
'attestationObject' => attestation_object,
'clientDataJSON' => client_data_json
})
if webauthn_credential.verify(
WebAuthn.standard_encoder.decode(current_challenge),
IApiConf.webauthn_register_origin,
rp_id: IApiConf.webauthn_rp_id
)
rp_id: IApiConf.webauthn_rp_id)
credential = user.web_authn_credentials.find_or_initialize_by(
external_id: bin_to_str(auth_response.credential.id)
)
external_id: WebAuthn.standard_encoder.encode(webauthn_credential.credential.id))
credential.update!(
name: name,
public_key: bin_to_str(auth_response.credential.public_key)
)
public_key: WebAuthn.standard_encoder.encode(webauthn_credential.credential.public_key))
return { state: 'success' }
end
end
......
......@@ -41,17 +41,17 @@ describe "iApi auth route" do
cred_opts = last_response.parsed_body['credential_options']
client = WebAuthn::FakeClient.new(IApiConf.webauthn_register_origin)
public_key_credential = client.create(
challenge: Base64.strict_decode64(cred_opts['challenge']),
challenge: cred_opts['challenge'],
rp_id: cred_opts['rp']['id'],
)
response = public_key_credential[:response]
response = public_key_credential['response']
post_as('users', '/users/verify_webauthn_registration',
email: email,
password: 'pwduser1',
webauthn_name: 'mykey',
attestation_object: Base64.strict_encode64(response[:attestation_object]),
client_data_json: Base64.strict_encode64(response[:client_data_json]),
attestation_object: response['attestationObject'],
client_data_json: response['clientDataJSON'],
current_challenge: cred_opts['challenge'],
)
expect(last_response).to be_ok
......@@ -111,17 +111,17 @@ describe "iApi auth route" do
cred_opts = last_response.parsed_body['credential_options']
client = WebAuthn::FakeClient.new(IApiConf.webauthn_register_origin)
public_key_credential = client.create(
challenge: Base64.strict_decode64(cred_opts['challenge']),
challenge: cred_opts['challenge'],
rp_id: cred_opts['rp']['id'],
)
response = public_key_credential[:response]
response = public_key_credential['response']
post_as('users', '/users/verify_webauthn_registration',
email: email,
password: 'pwduser1',
webauthn_name: 'mykey',
attestation_object: Base64.strict_encode64(response[:attestation_object]),
client_data_json: Base64.strict_encode64(response[:client_data_json]),
attestation_object: response['attestationObject'],
client_data_json: response['clientDataJSON'],
current_challenge: cred_opts['challenge'],
)
expect(last_response).to be_ok
......@@ -164,17 +164,17 @@ describe "iApi auth route" do
cred_opts = last_response.parsed_body['credential_options']
client = WebAuthn::FakeClient.new(IApiConf.webauthn_register_origin)
public_key_credential = client.create(
challenge: Base64.strict_decode64(cred_opts['challenge']),
challenge: cred_opts['challenge'],
rp_id: cred_opts['rp']['id'],
)
response = public_key_credential[:response]
response = public_key_credential['response']
post_as('users', '/users/verify_webauthn_registration',
email: email,
password: 'pwduser1',
webauthn_name: 'mykey',
attestation_object: Base64.strict_encode64(response[:attestation_object]),
client_data_json: Base64.strict_encode64(response[:client_data_json]),
attestation_object: response['attestationObject'],
client_data_json: response['clientDataJSON'],
current_challenge: cred_opts['challenge'],
)
expect(last_response).to be_ok
......@@ -196,18 +196,17 @@ describe "iApi auth route" do
expect(last_response.parsed_body['challenge']).to be_present
client.origin = IApiConf.webauthn_verify_origin
assertion = client.get(challenge: Base64.strict_decode64(last_response.parsed_body['challenge']), rp_id: IApiConf.webauthn_rp_id)
assertion = client.get(challenge: last_response.parsed_body['challenge'], rp_id: IApiConf.webauthn_rp_id)
post_as('login', '/auth/master_saml',
email: email,
password: 'pwduser1',
webauthn: {
challenge: last_response.parsed_body['challenge'],
credential_id: Base64.strict_encode64(assertion[:id]),
client_data_json: Base64.strict_encode64(assertion[:response][:client_data_json]),
authenticator_data: Base64.strict_encode64(assertion[:response][:authenticator_data]),
signature: Base64.strict_encode64(assertion[:response][:signature]),
credential_id: assertion['id'],
client_data_json: assertion['response']['clientDataJSON'],
authenticator_data: assertion['response']['authenticatorData'],
signature: assertion['response']['signature'],
},
saml_request: saml_user_request,
)
......
......@@ -65,6 +65,12 @@ class Rack::MockResponse
end
end
WebAuthn.configure do |config|
config.rp_name = IApiConf.webauthn_rp_name
config.rp_id = IApiConf.webauthn_rp_id
config.origin = IApiConf.webauthn_verify_origin
end