diff --git a/Gemfile b/Gemfile index f7d6757..93a199c 100644 --- a/Gemfile +++ b/Gemfile @@ -7,6 +7,7 @@ ruby "3.3.1" gem "rails", "7.1.3.3" gem 'devise' +gem 'webauthn' # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] gem "sprockets-rails" diff --git a/Gemfile.lock b/Gemfile.lock index 8e5d05d..81a40d8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -77,9 +77,12 @@ GEM tzinfo (~> 2.0) addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) + android_key_attestation (0.3.0) + awrence (1.2.1) base64 (0.2.0) bcrypt (3.1.20) bigdecimal (3.1.8) + bindata (2.5.0) bindex (0.8.1) bootsnap (1.18.3) msgpack (~> 1.2) @@ -93,8 +96,12 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) + cbor (0.5.9.8) concurrent-ruby (1.2.3) connection_pool (2.4.1) + cose (1.3.0) + cbor (~> 0.5.9) + openssl-signature_algorithm (~> 1.0) crass (1.0.6) date (3.3.4) debug (1.9.2) @@ -123,6 +130,8 @@ GEM jbuilder (2.12.0) actionview (>= 5.0.0) activesupport (>= 5.0.0) + jwt (2.8.1) + base64 loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) @@ -152,6 +161,9 @@ GEM nokogiri (1.16.5) mini_portile2 (~> 2.8.2) racc (~> 1.4) + openssl (3.2.0) + openssl-signature_algorithm (1.3.0) + openssl (> 2.0) orm_adapter (0.5.0) psych (5.1.2) stringio @@ -207,6 +219,8 @@ GEM railties (>= 5.2) rexml (3.2.6) rubyzip (2.3.2) + safety_net_attestation (0.4.0) + jwt (~> 2.0) selenium-webdriver (4.20.1) base64 (~> 0.2) rexml (~> 3.2, >= 3.2.5) @@ -224,6 +238,10 @@ GEM stringio (3.1.0) thor (1.3.1) timeout (0.4.1) + tpm-key_attestation (0.12.0) + bindata (~> 2.4) + openssl (> 2.0) + openssl-signature_algorithm (~> 1.0) turbo-rails (2.0.5) actionpack (>= 6.0.0) activejob (>= 6.0.0) @@ -237,10 +255,15 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - webdrivers (5.2.0) - nokogiri (~> 1.6) - rubyzip (>= 1.3.0) - selenium-webdriver (~> 4.0) + webauthn (3.1.0) + android_key_attestation (~> 0.3.0) + awrence (~> 1.1) + bindata (~> 2.4) + cbor (~> 0.5.9) + cose (~> 1.1) + openssl (>= 2.2) + safety_net_attestation (~> 0.4.0) + tpm-key_attestation (~> 0.12.0) webrick (1.8.1) websocket (1.2.10) websocket-driver (0.7.6) @@ -269,7 +292,7 @@ DEPENDENCIES turbo-rails tzinfo-data web-console - webdrivers + webauthn RUBY VERSION ruby 3.3.1p55 diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 5d17ca5..8108e38 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -30,7 +30,6 @@ RoR Version <%= Rails.version %> (<%=Rails.env%>) | Ruby <%= "#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}" %> | OS <%= RUBY_PLATFORM %> | App Version <%= `git describe --always` %>

To-Do (In order of importance):

diff --git a/config/importmap.rb b/config/importmap.rb index 8dce42d..89b4798 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -5,3 +5,4 @@ pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true pin_all_from "app/javascript/controllers", under: "controllers" +pin "@github/webauthn-json", to: "@github--webauthn-json.js" # @2.1.1 diff --git a/config/initializers/webauthn.rb b/config/initializers/webauthn.rb new file mode 100644 index 0000000..9e35e51 --- /dev/null +++ b/config/initializers/webauthn.rb @@ -0,0 +1,35 @@ +WebAuthn.configure do |config| + # This value needs to match `window.location.origin` evaluated by + # the User Agent during registration and authentication ceremonies. + config.origin = "https://mailadmin.hiddenagenda.ltd.uk" + + # Relying Party name for display purposes + config.rp_name = "Hidden Agenda Ltd" + + # Optionally configure a client timeout hint, in milliseconds. + # This hint specifies how long the browser should wait for any + # interaction with the user. + # This hint may be overridden by the browser. + # https://www.w3.org/TR/webauthn/#dom-publickeycredentialcreationoptions-timeout + # config.credential_options_timeout = 120_000 + + # You can optionally specify a different Relying Party ID + # (https://www.w3.org/TR/webauthn/#relying-party-identifier) + # if it differs from the default one. + # + # In this case the default would be "auth.example.com", but you can set it to + # the suffix "example.com" + # + # config.rp_id = "example.com" + + # Configure preferred binary-to-text encoding scheme. This should match the encoding scheme + # used in your client-side (user agent) code before sending the credential to the server. + # Supported values: `:base64url` (default), `:base64` or `false` to disable all encoding. + # + # config.encoding = :base64url + + # Possible values: "ES256", "ES384", "ES512", "PS256", "PS384", "PS512", "RS256", "RS384", "RS512", "RS1" + # Default: ["ES256", "PS256", "RS256"] + # + # config.algorithms << "ES384" +end diff --git a/db/migrate/20240521084152_add_web_authn_id_to_user.rb b/db/migrate/20240521084152_add_web_authn_id_to_user.rb new file mode 100644 index 0000000..1943dd3 --- /dev/null +++ b/db/migrate/20240521084152_add_web_authn_id_to_user.rb @@ -0,0 +1,5 @@ +class AddWebAuthnIdToUser < ActiveRecord::Migration[7.1] + def change + add_column :users, :webauthn_id, :string + end +end diff --git a/vendor/javascript/@github--webauthn-json.js b/vendor/javascript/@github--webauthn-json.js new file mode 100644 index 0000000..53097d9 --- /dev/null +++ b/vendor/javascript/@github--webauthn-json.js @@ -0,0 +1,2 @@ +function base64urlToBuffer(e){const r="==".slice(0,(4-e.length%4)%4);const t=e.replace(/-/g,"+").replace(/_/g,"/")+r;const n=atob(t);const i=new ArrayBuffer(n.length);const o=new Uint8Array(i);for(let e=0;econvert(t,n[0],e)));if(n instanceof Object){const e={};for(const[r,o]of Object.entries(n)){if(o.derive){const e=o.derive(i);void 0!==e&&(i[r]=e)}if(r in i)null!=i[r]?e[r]=convert(t,o.schema,i[r]):e[r]=null;else if(o.required)throw new Error(`Missing key: ${r}`)}return e}}function derived(e,r){return{required:true,schema:e,derive:r}}function required(e){return{required:true,schema:e}}function optional(e){return{required:false,schema:e}}var t={type:required(e),id:required(r),transports:optional(e)};var n={appid:optional(e),appidExclude:optional(e),credProps:optional(e)};var i={appid:optional(e),appidExclude:optional(e),credProps:optional(e)};var o={publicKey:required({rp:required(e),user:required({id:required(r),name:required(e),displayName:required(e)}),challenge:required(r),pubKeyCredParams:required(e),timeout:optional(e),excludeCredentials:optional([t]),authenticatorSelection:optional(e),attestation:optional(e),extensions:optional(n)}),signal:optional(e)};var a={type:required(e),id:required(e),rawId:required(r),authenticatorAttachment:optional(e),response:required({clientDataJSON:required(r),attestationObject:required(r),transports:derived(e,(e=>{var r;return(null==(r=e.getTransports)?void 0:r.call(e))||[]}))}),clientExtensionResults:derived(i,(e=>e.getClientExtensionResults()))};var u={mediation:optional(e),publicKey:required({challenge:required(r),timeout:optional(e),rpId:optional(e),allowCredentials:optional([t]),userVerification:optional(e),extensions:optional(n)}),signal:optional(e)};var s={type:required(e),id:required(e),rawId:required(r),authenticatorAttachment:optional(e),response:required({clientDataJSON:required(r),authenticatorData:required(r),signature:required(r),userHandle:required(r)}),clientExtensionResults:derived(i,(e=>e.getClientExtensionResults()))};var c={credentialCreationOptions:o,publicKeyCredentialWithAttestation:a,credentialRequestOptions:u,publicKeyCredentialWithAssertion:s};function createRequestFromJSON(e){return convert(base64urlToBuffer,o,e)}function createResponseToJSON(e){return convert(bufferToBase64url,a,e)}async function create(e){const r=await navigator.credentials.create(createRequestFromJSON(e));return createResponseToJSON(r)}function getRequestFromJSON(e){return convert(base64urlToBuffer,u,e)}function getResponseToJSON(e){return convert(bufferToBase64url,s,e)}async function get(e){const r=await navigator.credentials.get(getRequestFromJSON(e));return getResponseToJSON(r)}function supported(){return!!(navigator.credentials&&navigator.credentials.create&&navigator.credentials.get&&window.PublicKeyCredential)}export{create,get,c as schema,supported}; +