diff --git a/spec/lib/webauthn/security_key_authentication_service_spec.rb b/spec/lib/webauthn/security_key_authentication_service_spec.rb index ee83dde77ec..fc319ccb84f 100644 --- a/spec/lib/webauthn/security_key_authentication_service_spec.rb +++ b/spec/lib/webauthn/security_key_authentication_service_spec.rb @@ -3,18 +3,54 @@ require 'rails_helper' require 'webauthn' require 'webauthn/security_key_registration_service' +## +# These tests use the following parameters generated on a local discourse +# instance to test an actual authentication flow: +# +# - credential_id +# - public_key +# - challenge +# - signature +# - authenticator_data +# - client_data_origin +# - challenge_params_origin +# +# To create another test (e.g. for a different COSE algorithm) you need to: +# +# 1. Add a security key for a user on a local discourse instance. Go into +# the console and get the credential_id and public_key params from there. +# 2. Log out and try to log back in to that user to get the security +# key challenge +# 3. Touch the security key. Inside the authenticate_security_key method +# you need to add puts debugger statements (or use binding.pry) like so: +# +# puts client_data +# puts signature +# puts auth_data +# +# The auth_data will have the challenge param, but you must Base64.decode64 to +# use it in the let(:challenge) variable. The signature and auth_data params +# can be used as is. +# +# You also need to make sure that client_data_param has the exact same structure +# and order of keys as auth_data, otherwise even with everything else right the +# public key verification will fail. +# +# The origin params just need to be whatever your localhost URL for Discourse is. + describe Webauthn::SecurityKeyAuthenticationService do let(:security_key_user) { current_user } - let(:security_key) do + let!(:security_key) do Fabricate( :user_security_key, - credential_id: 'mJAJ4CznTO0SuLkJbYwpgK75ao4KMNIPlU5KWM92nq39kRbXzI9mSv6GxTcsMYoiPgaouNw7b7zBiS4vsQaO6A==', - public_key: 'pQECAyYgASFYIMNgw4GCpwBUlR2SznJ1yY7B9yFvsuxhfo+C9kcA4IitIlggRdofrCezymy2B/YarX+gfB6gZKg648/cHIMjf6wWmmU=', + credential_id: credential_id, + public_key: public_key, user: security_key_user, last_used: nil ) end - let(:credential_id) { security_key.credential_id } + let(:public_key) { 'pQECAyYgASFYIMNgw4GCpwBUlR2SznJ1yY7B9yFvsuxhfo+C9kcA4IitIlggRdofrCezymy2B/YarX+gfB6gZKg648/cHIMjf6wWmmU=' } + let(:credential_id) { 'mJAJ4CznTO0SuLkJbYwpgK75ao4KMNIPlU5KWM92nq39kRbXzI9mSv6GxTcsMYoiPgaouNw7b7zBiS4vsQaO6A==' } let(:challenge) { '81d4acfbd69eafa8f02bc2ecbec5267be8c9b28c1e0ba306d52b79f0f13d' } let(:client_data_challenge) { Base64.strict_encode64(challenge) } let(:client_data_webauthn_type) { 'webauthn.get' } @@ -33,7 +69,7 @@ describe Webauthn::SecurityKeyAuthenticationService do } ## # These are sourced from an actual login using the UserSecurityKey credential - # defined in this spec. + # defined in this spec, generated via a local discourse. let(:signature) { "MEUCIBppPyK8blxBDoktU54mI1vWEY96r1V5H1rEBtPDxwcGAiEAoi7LCmMoEAuWYu0krZpflZlULsbURCGcqOwP06amXYE=" } let(:authenticator_data) { "SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MBAAAAVw==" } let(:params) do @@ -47,18 +83,19 @@ describe Webauthn::SecurityKeyAuthenticationService do ## # The original key was generated in localhost let(:rp_id) { 'localhost' } + let(:challenge_params_origin) { 'http://localhost:3000' } let(:challenge_params) do { challenge: challenge, rp_id: rp_id, - origin: 'http://localhost:3000' + origin: challenge_params_origin } end let(:current_user) { Fabricate(:user) } let(:subject) { described_class.new(current_user, params, challenge_params) } - it 'updates last_used when valid' do - subject.authenticate_security_key + it 'updates last_used when the security key and params are valid' do + expect(subject.authenticate_security_key).to eq(true) expect(security_key.reload.last_used).not_to eq(nil) end @@ -77,7 +114,9 @@ describe Webauthn::SecurityKeyAuthenticationService do end context 'when the credential ID does not match any user security key in the database' do - let(:credential_id) { 'badid' } + before do + security_key.destroy + end it 'raises a NotFoundError' do expect { subject.authenticate_security_key }.to raise_error( @@ -158,6 +197,36 @@ describe Webauthn::SecurityKeyAuthenticationService do end end + context 'for windows hello (alg -257)' do + ## + # These are sourced from an actual login using the UserSecurityKey credential + # defined in this spec, generated via a local discourse. + let(:challenge) { "fa7cb122f8713745dc08e16863e087ffa2d3bfda7f1b0386ea4b14635bb6" } + let(:signature) { "OKEP/8oiojjE+LBwg6F37yJzjOTT9mBPukrW1E8Sih5Vh/3p9WHrqZdylxr1x9z/c8GplC0ABayanpAqN/miQezt3wm97gIwoHq/6rrmHDZu6irQhpjeX9yHRlu0lQw+SUEZfoW3iB4oP/d2ryYlafFA9intm++lLlP/qI3mvpCQwkAeotaelx7fn0RwiY767dG+bGVPyYuUicGHcLLvCY2k0G8kRQ7I5SQqB+dIcOINWikC9I2xvUKu6Br7hZZIrDy+soFtdnnCnvi2q/3ocOPYL5jy58wdpCTsh1RRPIEF/fQFVDOXtdS7PVgaa0PMBcWMCe5TimwGlTlICnsm+g==" } + let(:authenticator_data) { "SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MFAAAABA==" } + let(:credential_id) { "8AddFow3jT87k1UPWvjn+rOetCEambpESGZ+z/63hOE=" } + let(:public_key) { "pAEDAzkBACBZAQCqsl50KrR5zVm/QT9vWkeGTGxby32m0QRtCRh2UWseqoG0ZmBhGeWEYvkdoYlB1jObQKEHsAeB+1NBf5q69/88AA5zv4fzrvCydCtL41EUsHYFEbaPGnB61zZmYVLTPI7BYa+fu4F4MzFa924s36tVlU/L7n04peviJVZW2C1YIQfwOGDZJSvUpqJoZMQtw1vGRfrb4cQKlHfrpDZUpa3QLE8phh4ce4nwtX1tUnUGgCy8sOaFVkDNufENGTNr8HdAIHcinUiax3yy/Q8LjSZb8UR2ha6oXSe1vRHhj001B/P/mr5AdVMxSrOT1sUNXWkHv8L8IzS/iTBQpsC8CADZIUMBAAE=" } + let(:challenge_params_origin) { 'http://localhost:4200' } + let(:client_data_origin) { 'http://localhost:4200' } + + # This has to be in the exact same order with the same data as it was originally + # generated. + let(:client_data_param) { + { + type: client_data_webauthn_type, + challenge: client_data_challenge, + origin: client_data_origin, + crossOrigin: false, + other_keys_can_be_added_here: "do not compare clientDataJSON against a template. See https://goo.gl/yabPex" + } + } + + it 'updates last_used when the security key and params are valid' do + expect(subject.authenticate_security_key).to eq(true) + expect(security_key.reload.last_used).not_to eq(nil) + end + end + it 'all supported algorithms are implemented' do Webauthn::SUPPORTED_ALGORITHMS.each do |alg| expect(COSE::Algorithm.find(alg)).not_to be_nil