DEV: Add Windows Hello webauthn authentication spec (#15871)

Follow up to 6f7364e48b to add a spec
that tests the full authentication of a Windows Hello algorithm (-257)
webauthn verification. The test added in that commit only tested that
we know about that algorithm, not whether it was actually usable.
This commit is contained in:
Martin Brennan 2022-02-09 12:47:47 +10:00 committed by GitHub
parent 8abc4a0fd6
commit 59343c3057
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -3,18 +3,54 @@ require 'rails_helper'
require 'webauthn' require 'webauthn'
require 'webauthn/security_key_registration_service' 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 describe Webauthn::SecurityKeyAuthenticationService do
let(:security_key_user) { current_user } let(:security_key_user) { current_user }
let(:security_key) do let!(:security_key) do
Fabricate( Fabricate(
:user_security_key, :user_security_key,
credential_id: 'mJAJ4CznTO0SuLkJbYwpgK75ao4KMNIPlU5KWM92nq39kRbXzI9mSv6GxTcsMYoiPgaouNw7b7zBiS4vsQaO6A==', credential_id: credential_id,
public_key: 'pQECAyYgASFYIMNgw4GCpwBUlR2SznJ1yY7B9yFvsuxhfo+C9kcA4IitIlggRdofrCezymy2B/YarX+gfB6gZKg648/cHIMjf6wWmmU=', public_key: public_key,
user: security_key_user, user: security_key_user,
last_used: nil last_used: nil
) )
end end
let(:credential_id) { security_key.credential_id } let(:public_key) { 'pQECAyYgASFYIMNgw4GCpwBUlR2SznJ1yY7B9yFvsuxhfo+C9kcA4IitIlggRdofrCezymy2B/YarX+gfB6gZKg648/cHIMjf6wWmmU=' }
let(:credential_id) { 'mJAJ4CznTO0SuLkJbYwpgK75ao4KMNIPlU5KWM92nq39kRbXzI9mSv6GxTcsMYoiPgaouNw7b7zBiS4vsQaO6A==' }
let(:challenge) { '81d4acfbd69eafa8f02bc2ecbec5267be8c9b28c1e0ba306d52b79f0f13d' } let(:challenge) { '81d4acfbd69eafa8f02bc2ecbec5267be8c9b28c1e0ba306d52b79f0f13d' }
let(:client_data_challenge) { Base64.strict_encode64(challenge) } let(:client_data_challenge) { Base64.strict_encode64(challenge) }
let(:client_data_webauthn_type) { 'webauthn.get' } 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 # 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(:signature) { "MEUCIBppPyK8blxBDoktU54mI1vWEY96r1V5H1rEBtPDxwcGAiEAoi7LCmMoEAuWYu0krZpflZlULsbURCGcqOwP06amXYE=" }
let(:authenticator_data) { "SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MBAAAAVw==" } let(:authenticator_data) { "SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MBAAAAVw==" }
let(:params) do let(:params) do
@ -47,18 +83,19 @@ describe Webauthn::SecurityKeyAuthenticationService do
## ##
# The original key was generated in localhost # The original key was generated in localhost
let(:rp_id) { 'localhost' } let(:rp_id) { 'localhost' }
let(:challenge_params_origin) { 'http://localhost:3000' }
let(:challenge_params) do let(:challenge_params) do
{ {
challenge: challenge, challenge: challenge,
rp_id: rp_id, rp_id: rp_id,
origin: 'http://localhost:3000' origin: challenge_params_origin
} }
end end
let(:current_user) { Fabricate(:user) } let(:current_user) { Fabricate(:user) }
let(:subject) { described_class.new(current_user, params, challenge_params) } let(:subject) { described_class.new(current_user, params, challenge_params) }
it 'updates last_used when valid' do it 'updates last_used when the security key and params are valid' do
subject.authenticate_security_key expect(subject.authenticate_security_key).to eq(true)
expect(security_key.reload.last_used).not_to eq(nil) expect(security_key.reload.last_used).not_to eq(nil)
end end
@ -77,7 +114,9 @@ describe Webauthn::SecurityKeyAuthenticationService do
end end
context 'when the credential ID does not match any user security key in the database' do 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 it 'raises a NotFoundError' do
expect { subject.authenticate_security_key }.to raise_error( expect { subject.authenticate_security_key }.to raise_error(
@ -158,6 +197,36 @@ describe Webauthn::SecurityKeyAuthenticationService do
end end
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 it 'all supported algorithms are implemented' do
Webauthn::SUPPORTED_ALGORITHMS.each do |alg| Webauthn::SUPPORTED_ALGORITHMS.each do |alg|
expect(COSE::Algorithm.find(alg)).not_to be_nil expect(COSE::Algorithm.find(alg)).not_to be_nil