diff --git a/app/controllers/api/nextv1/publishers_controller.rb b/app/controllers/api/nextv1/publishers_controller.rb index cd546f4948..32d84d4bc0 100644 --- a/app/controllers/api/nextv1/publishers_controller.rb +++ b/app/controllers/api/nextv1/publishers_controller.rb @@ -5,8 +5,21 @@ class Api::Nextv1::PublishersController < Api::Nextv1::BaseController def me publisher_hash = JSON.parse(current_publisher.to_json) - publisher_hash["two_factor_enabled"] = two_factor_enabled?(current_publisher) - render(json: publisher_hash.to_json, status: 200) + response_data = { + **publisher_hash, + two_factor_enabled: two_factor_enabled?(current_publisher) + } + render(json: response_data.to_json, status: 200) + end + + def security + response_data = { + u2f_enabled: u2f_enabled?(current_publisher), + totp_enabled: totp_enabled?(current_publisher), + u2f_registrations: current_publisher.u2f_registrations + } + + render(json: response_data.to_json, status: 200) end def update diff --git a/app/controllers/api/nextv1/totp_registrations_controller.rb b/app/controllers/api/nextv1/totp_registrations_controller.rb index c31e142493..78a18bc2be 100644 --- a/app/controllers/api/nextv1/totp_registrations_controller.rb +++ b/app/controllers/api/nextv1/totp_registrations_controller.rb @@ -1,5 +1,8 @@ class Api::Nextv1::TotpRegistrationsController < Api::Nextv1::BaseController include QrCodeHelper + include TwoFactorRegistration + include PendingActions + def new @totp_registration = TotpRegistration.new secret: ROTP::Base32.random_base32 @provisioning_url = @totp_registration.totp.provisioning_uri(current_publisher.email) @@ -11,4 +14,46 @@ def new render(json: response_data.to_json, status: 200) end + + class AddTOTP < StepUpAction + call do |publisher_id, password, totp_registration_params| + current_publisher = Publisher.find(publisher_id) + totp_registration = TotpRegistration.new totp_registration_params + + if totp_registration.totp.verify(password, drift_ahead: 60, drift_behind: 60, at: Time.now - 30) + current_publisher.totp_registration.destroy! if current_publisher.totp_registration.present? + totp_registration.publisher = current_publisher + totp_registration.save! + + logout_everybody_else!(current_publisher) + + render(json: {}, status: 200) + else + render(json: {errors: totp_registration.errors}, status: 400) + end + end + end + + def create + AddTOTP.new(current_publisher.id, params[:totp_password], totp_registration_params.to_h).step_up! self + end + + class RemoveTOTP < StepUpAction + call do |publisher_id| + current_publisher = Publisher.find(publisher_id) + current_publisher.totp_registration.destroy! if current_publisher.totp_registration.present? + end + end + + def destroy + RemoveTOTP.new(current_publisher.id).step_up! self + + render(json: {}, status: 200) + end + + private + + def totp_registration_params + params.require(:totp_registration).permit(:secret) + end end diff --git a/app/controllers/api/nextv1/u2f_registrations_controller.rb b/app/controllers/api/nextv1/u2f_registrations_controller.rb new file mode 100644 index 0000000000..6a2e323cc6 --- /dev/null +++ b/app/controllers/api/nextv1/u2f_registrations_controller.rb @@ -0,0 +1,64 @@ +class Api::Nextv1::U2fRegistrationsController < Api::Nextv1::BaseController + include Logout + include TwoFactorRegistration + + include PendingActions + + before_action :authenticate_publisher! + + def new + @u2f_registration = U2fRegistration.new + publisher = current_publisher + + @webauthn_options = WebAuthn::Credential.options_for_create( + user: {id: publisher.id, name: publisher.email}, + exclude: publisher.u2f_registrations.map { |c| c.key_handle }.compact + ) + + session[:creation_challenge] = @webauthn_options.challenge + + render(json: @webauthn_options.to_json, status: 200) + end + + class AddU2F < StepUpAction + call do |publisher_id, webauthn_response, name, challenge| + current_publisher = Publisher.find(publisher_id) + result = TwoFactorAuth::WebauthnRegistrationService.build.call(publisher: current_publisher, + webauthn_response: webauthn_response, + name: name, + challenge: challenge) + + case result + when BSuccess + logout_everybody_else!(current_publisher) + render(json: {}, status: 200) + when BFailure + render(json: {errors: ["Webauthn registration failed"]}, status: 400) && return + else + raise result + end + end + end + + def create + AddU2F.new(current_publisher.id, + params[:webauthn_response], + params.require(:u2f_registration).permit(:name)[:name], + session[:creation_challenge]).step_up! self + end + + class RemoveU2F < StepUpAction + call do |publisher_id, u2f_id| + current_publisher = Publisher.find(publisher_id) + + u2f_registration = current_publisher.u2f_registrations.find(u2f_id) + u2f_registration.destroy + end + end + + def destroy + RemoveU2F.new(current_publisher.id, params[:id]).step_up! self + + render(json: {}, status: 200) + end +end diff --git a/app/services/pending_actions.rb b/app/services/pending_actions.rb index a8adfd374b..6eb04aea68 100644 --- a/app/services/pending_actions.rb +++ b/app/services/pending_actions.rb @@ -121,7 +121,14 @@ def execute! context def step_up! context if context.two_factor_enabled?(current_publisher) save! context - context.redirect_to context.two_factor_authentications_path + if context.class.name.include?("Nextv1") + execute! context + # context.render(json: { + # error: '2fa_required' + # }, status: 200) + else + context.redirect_to context.two_factor_authentications_path + end else execute! context end diff --git a/config/routes.rb b/config/routes.rb index aa37217df2..3c6710b096 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -167,9 +167,18 @@ namespace :nextv1, defaults: {format: :json} do resources :publishers, only: [:update, :destroy] get "publishers/me", to: "publishers#me" + get "publishers/security", to: "publishers#security" namespace :totp_registrations do get :new + post :create + delete :destroy + end + + namespace :u2f_registrations do + get :new + post :create + delete :destroy end end diff --git a/nextjs/package-lock.json b/nextjs/package-lock.json index dfe05c1c29..e0f849561f 100644 --- a/nextjs/package-lock.json +++ b/nextjs/package-lock.json @@ -9,9 +9,11 @@ "version": "0.1.0", "hasInstallScript": true, "dependencies": { - "@brave/leo": "github:brave/leo#86be9379ea55a7e32b56beee05a4e57caa1c0da2", + "@brave/leo": "github:brave/leo#80f2230bcfb7d2a87e7025a4e26c502d900fd938", + "@github/webauthn-json": "^2.1.1", "axios": "^1.5.0", "clsx": "^2.0.0", + "moment": "^2.29.4", "next": "^13.4.12", "next-intl": "^3.0.0-beta.9", "react": "^18.2.0", @@ -2112,11 +2114,12 @@ }, "node_modules/@brave/leo": { "version": "0.0.1", - "resolved": "git+ssh://git@github.com/brave/leo.git#86be9379ea55a7e32b56beee05a4e57caa1c0da2", - "integrity": "sha512-dZwR61uFexQOiQNytbr+VeuFNghY4KjPsOPPU8LfraPkJq8SEtgGJAVvlW1pyDsFUyPvMOpjBHHI1z5pSNAWYw==", + "resolved": "git+ssh://git@github.com/brave/leo.git#80f2230bcfb7d2a87e7025a4e26c502d900fd938", + "integrity": "sha512-n1Qbr5n+oQKpqaU5hVAeov4HVF6FPYvUVg1Dk94NuCU+qcMtYyUckwlLE7Yx+J+olVoqmSEfkGw9vUYYEvDFqw==", "license": "MIT", "dependencies": { "@ctrl/tinycolor": "3.5.1", + "@floating-ui/dom": "1.4.4", "@tsconfig/svelte": "3.0.0", "lodash.camelcase": "4.3.0", "lodash.merge": "4.6.2", @@ -2748,6 +2751,27 @@ "npm": ">=6.14.13" } }, + "node_modules/@floating-ui/core": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz", + "integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==", + "dependencies": { + "@floating-ui/utils": "^0.1.3" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.4.4.tgz", + "integrity": "sha512-21hhDEPOiWkGp0Ys4Wi6Neriah7HweToKra626CIK712B5m9qkdz54OP9gVldUg+URnBTpv/j/bi/skmGdstXQ==", + "dependencies": { + "@floating-ui/core": "^1.3.1" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.4.tgz", + "integrity": "sha512-qprfWkn82Iw821mcKofJ5Pk9wgioHicxcQMxx+5zt5GSKoqdWvgG5AxVmpmUUjzTLPVSH5auBrhI93Deayn/DA==" + }, "node_modules/@formatjs/ecma402-abstract": { "version": "1.17.2", "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.2.tgz", @@ -2834,6 +2858,14 @@ "tslib": "^2.4.0" } }, + "node_modules/@github/webauthn-json": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@github/webauthn-json/-/webauthn-json-2.1.1.tgz", + "integrity": "sha512-XrftRn4z75SnaJOmZQbt7Mk+IIjqVHw+glDGOxuHwXkZBZh/MBoRS7MHjSZMDaLhT4RjN2VqiEU7EOYleuJWSQ==", + "bin": { + "webauthn-json": "dist/bin/main.js" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", @@ -11346,6 +11378,14 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "engines": { + "node": "*" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -17400,11 +17440,12 @@ "dev": true }, "@brave/leo": { - "version": "git+ssh://git@github.com/brave/leo.git#86be9379ea55a7e32b56beee05a4e57caa1c0da2", - "integrity": "sha512-dZwR61uFexQOiQNytbr+VeuFNghY4KjPsOPPU8LfraPkJq8SEtgGJAVvlW1pyDsFUyPvMOpjBHHI1z5pSNAWYw==", - "from": "@brave/leo@github:brave/leo#86be9379ea55a7e32b56beee05a4e57caa1c0da2", + "version": "git+ssh://git@github.com/brave/leo.git#80f2230bcfb7d2a87e7025a4e26c502d900fd938", + "integrity": "sha512-n1Qbr5n+oQKpqaU5hVAeov4HVF6FPYvUVg1Dk94NuCU+qcMtYyUckwlLE7Yx+J+olVoqmSEfkGw9vUYYEvDFqw==", + "from": "@brave/leo@github:brave/leo#80f2230bcfb7d2a87e7025a4e26c502d900fd938", "requires": { "@ctrl/tinycolor": "3.5.1", + "@floating-ui/dom": "1.4.4", "@tsconfig/svelte": "3.0.0", "lodash.camelcase": "4.3.0", "lodash.merge": "4.6.2", @@ -17810,6 +17851,27 @@ "integrity": "sha512-38DT60rumHfBYynif3lmtxMqMqmsOQIxQgEuPZxCk2yUYN0eqWpTACgxi0VpidvsJB8CRxCpvP7B3anK85FjtQ==", "dev": true }, + "@floating-ui/core": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz", + "integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==", + "requires": { + "@floating-ui/utils": "^0.1.3" + } + }, + "@floating-ui/dom": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.4.4.tgz", + "integrity": "sha512-21hhDEPOiWkGp0Ys4Wi6Neriah7HweToKra626CIK712B5m9qkdz54OP9gVldUg+URnBTpv/j/bi/skmGdstXQ==", + "requires": { + "@floating-ui/core": "^1.3.1" + } + }, + "@floating-ui/utils": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.4.tgz", + "integrity": "sha512-qprfWkn82Iw821mcKofJ5Pk9wgioHicxcQMxx+5zt5GSKoqdWvgG5AxVmpmUUjzTLPVSH5auBrhI93Deayn/DA==" + }, "@formatjs/ecma402-abstract": { "version": "1.17.2", "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.2.tgz", @@ -17902,6 +17964,11 @@ "tslib": "^2.4.0" } }, + "@github/webauthn-json": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@github/webauthn-json/-/webauthn-json-2.1.1.tgz", + "integrity": "sha512-XrftRn4z75SnaJOmZQbt7Mk+IIjqVHw+glDGOxuHwXkZBZh/MBoRS7MHjSZMDaLhT4RjN2VqiEU7EOYleuJWSQ==" + }, "@humanwhocodes/config-array": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", @@ -24239,6 +24306,11 @@ "minimist": "^1.2.6" } }, + "moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" + }, "mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", diff --git a/nextjs/package.json b/nextjs/package.json index a6e0537408..4b5f802894 100644 --- a/nextjs/package.json +++ b/nextjs/package.json @@ -18,9 +18,11 @@ "typecheck": "tsc --noEmit --incremental false" }, "dependencies": { - "@brave/leo": "github:brave/leo#86be9379ea55a7e32b56beee05a4e57caa1c0da2", + "@brave/leo": "github:brave/leo#80f2230bcfb7d2a87e7025a4e26c502d900fd938", + "@github/webauthn-json": "^2.1.1", "axios": "^1.5.0", "clsx": "^2.0.0", + "moment": "^2.29.4", "next": "^13.4.12", "next-intl": "^3.0.0-beta.9", "react": "^18.2.0", diff --git a/nextjs/src/app/[locale]/publishers/loading.tsx b/nextjs/src/app/[locale]/publishers/loading.tsx new file mode 100644 index 0000000000..a0e3bf87ce --- /dev/null +++ b/nextjs/src/app/[locale]/publishers/loading.tsx @@ -0,0 +1,29 @@ +export default function Loading() { + // You can add any UI inside Loading, including a Skeleton. + return ( +