Repository: electron/electron (language: cpp)

Issue:
Device chooser for WebUSB
**Is your feature request related to a problem? Please describe.**
Electron has a chooser for Web Bluetooth (#5565), but no chooser for WebUSB. Even if there is only one device, WebUSB still requires the chooser to be used, currently resulting in a `DOMException: No device selected` when calling `navigator.usb.requestDevice()`.

WebUSB has been available (without an experimental flag) since Chromium 61.

**Describe the solution you'd like**
Implement a chooser window like the one in Chromium (see https://cs.chromium.org/chromium/src/chrome/browser/usb/web_usb_chooser.h) to request permission from a user to access a USB device.

**Describe alternatives you've considered**
`node-usb` is an alternative that can be used, but it is barely maintained. WebUSB is well supported by the Chrome team and seems to be the way forward.

**Additional context**
- Link to spec: https://wicg.github.io/webusb/
- Screenshot:
![](https://developers.google.com/web/updates/images/2016-03-02-access-usb-devices-on-the-web/usb-device-chooser.png)



Discussion / hints:
👋 Thanks for opening your first issue here! If you're reporting a 🐞 bug, please make sure you include steps to reproduce it. We get a lot of issues on this repo, so please be patient and we will get back to you as soon as we can.

To help make it easier for us to investigate your issue, please follow the [contributing guidelines](https://github.com/electron/electron/blob/master/CONTRIBUTING.md).

> Implement a chooser window like the one in Chromium (see 

Just commenting here that we won't implement any UI for this, if someone gets to implementing this it would follow the same pattern as the bluetooth API which emits an event with the possible bluetooth devices and leaves the UI up to individual apps.
Let me know if you have any questions about enabling this. Right now WebUSB isn't hooked up in the easiest way for content embedders but we are refactoring it right now so it should get better.
@reillyeon Yeah I took a look at this and it's not hooked up to webcontents observer like blueooth is.  Might just wait for a refactor :smile: 
Any update on this? Would be helpful for the Electron app of a website I'm building which needs to get the output from a TRNG (true/hardware random-number-generator) usb device.

Like mentioned above, there are some NodeJS alternatives, but it would be nice to use the web standard if possible since it means the USB-access code can be shared between the Electron app and the web-only version.
While you are waiting for this you can use [webusb](https://www.npmjs.com/package/webusb) which is a polyfill of WebUSB on top of the Node-specific [usb](https://www.npmjs.com/package/usb) package.
Any update on this issue? Seems NW.js fixed this over a year ago
https://github.com/nwjs/nw.js/issues/6500
Hi @MarshallOfSound @reillyeon Would you kindly take a look at this and let us know if the refactor has taken place at this point to enable this to go ahead? I checked the USB code in the chromium browser folder and I did not see a WebContentsObserver. Also, I did not see a ticket open to implement this on bugs.chromium.org. I'd like to open a ticket but I do not have your expertise on this matter. Your guidance would be appreciate so that we can get this working in electron.
The refactoring in Chromium is tracked by [issue 991759](https://crbug.com/991759). This unblocks a whole bunch of things that will help embedders like Electron as well as Headless Chrome and Android WebView.
@reillyeon Looking at the issue you linked to, am I understanding correctly that Web Serial and WebHID are hooked up to WebContentsObserver?
I don't believe that either API registers as a WebContentsObserver. They do both access WebContents, but only to update whether or not there are open connections to Serial or HID devices.
As a subclass of `content::FrameServiceBase` the `content::HidService` class implementing the browser-process side of the WebHID API is a `content::WebContentsObserver`. For the WebUSB API, instances of `WebUsbServiceImpl` are owned by a `UsbTabHelper` which is a `content::WebContentsObserver`.
Sorry to bring back this dead issue, but there is no other real discussion of it anywhere on the internet. I am attempting to use the webusb polyfill method, and when I try to call requestDevice(), it disconnects the debugger and I do not get a chooser. I have manually specified a device, but when it attempts the requestDevice, it crashes.

With the polyfill is there a way to over-ride and select the device manually or was the suggestion above of using a polyfill library misguided? Thank you in advance for any advice.
 This is the script to easily recreate the issue:
https://github.com/thegecko/webusb/blob/master/examples/selector.js
@robertguyser this issue is not dead but your question is off-topic. For issues with [thegecko/webusb](https://github.com/thegecko/webusb) please use that repository's issue tracker.
Any update on this? Looking to use Electron to talk to a piece of hardware I'm making, would love to use `navigator.usb` as described [here.](https://developers.google.com/web/updates/2016/03/access-usb-devices-on-the-web)
Any updates here? Working on an electron app which connects to a mobile device and would like to use the WebUSB api for it.
I've been looking into using the [WebHID](https://web.dev/hid/) API available in Chromium 89 (Electron 12) and later. It looks like this inherits the same permission problem as WebUSB. If anyone wants to kill two birds with one stone, we could get an API similar to [`webContents.on('select-bluetooth-device')`](https://www.electronjs.org/docs/api/web-contents#event-select-bluetooth-device) for both `navigator.usb.requestDevice()` and `navigator.hid.requestDevice()`. Maybe this could all be consolidated into `webContents.on('request-device')`? Or not.
ah shit here we go again

On Sun, Jul 11, 2021 at 12:18 PM Jeff Robbins ***@***.***>
wrote:

> I've been looking into using the WebHID <https://web.dev/hid/> API
> available in Chromium 89 (Electron 12) and later. It looks like this
> inherits the same permission problem as WebUSB. If anyone wants to kill two
> birds with one stone, we could get an API similar to
> webContents.on('select-bluetooth-device')
> <https://www.electronjs.org/docs/api/web-contents#event-select-bluetooth-device>
> for both navigator.usb.requestDevice() and navigator.hid.requestDevice().
> Maybe this could all be consolidated into webContents.on('request-device')?
> Or not.
>
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/electron/electron/issues/14545#issuecomment-877827112>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AR3X2BS43BMQXXURBMC6JKLTXG75ZANCNFSM4FUM25JQ>
> .
>

+1 on this

It should be fairly trivial to implement the permission and device chooser now that we have webContents.setDevicePermissionHandler through #30213 and #22478
How will access to [protected interface classes][1] work? It seems ok to ignore that aspect of the spec entirely. Same for [the device blocklist][2].

I would like to use Electron for an app the supports discontinued (non-class-compliant) MIDI devices and soundcards, so would really appreciate WebUSB support.

[1]: https://wicg.github.io/webusb/#protected-interface-classes.
[2]: https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/usb/usb_blocklist.cc;l=60?q=%22UsbBlocklist::Entry%20kStaticEntries%22&ss=chromium
I have a [work-in-progress Chromium change](https://chromium-review.googlesource.com/c/chromium/src/+/3214442) which does the refactoring necessary for //content embedders like Electron to support WebUSB. In that change the new `UsbDelegate` interface has an `AdjustProtectedInterfaceClasses` method which Electron can override to control how that part of the specification is implemented.

I unfortunately don't have cycles to work on this right now (which is why I uploaded the patch as WIP). If someone wants to take this on I am happy to do code reviews.
Thank you, @reillyeon. Much appreciated. It's not something I would be competent to take on.

<strike>I guess I can just use the Node USB library for now (I believe there's a WebUSB shim), and move to WebUSB once it's ready. It's just a hobby project, so it's not the end of the world if it gets delayed.</strike> The Node USB libraries don't support isochronous transfers :(

Thanks again.
The Chromium [refactor](https://crbug.com/991759) is complete. Content embedders should implement the `UsbDelegate` interface.

https://source.chromium.org/chromium/chromium/src/+/main:content/public/browser/usb_delegate.h
@nornagon Since the Chromium refactor is compelete, I guess the "blocked/upstream" tag can now be removed?
Is there a conclusion? If not, I'll ask again later
Also very interested in this. 
I have been monitoring this issue for a few years now. It appears all the backend work in Chromium has been completed. Now the next step is for someone to implement the functionality in Electron.

Fix patch:
diff --git a/docs/api/session.md b/docs/api/session.md
index 764243c7b14d3..bc86c21713bfd 100644
--- a/docs/api/session.md
+++ b/docs/api/session.md
@@ -239,7 +239,7 @@ app.whenReady().then(() => {
     const selectedDevice = details.deviceList.find((device) => {
       return device.vendorId === '9025' && device.productId === '67'
     })
-    callback(selectedPort?.deviceId)
+    callback(selectedDevice?.deviceId)
   })
 })
 ```
@@ -429,6 +429,118 @@ const portConnect = async () => {
 }
 ```
 
+#### Event: 'select-usb-device'
+
+Returns:
+
+* `event` Event
+* `details` Object
+  * `deviceList` [USBDevice[]](structures/usb-device.md)
+  * `frame` [WebFrameMain](web-frame-main.md)
+* `callback` Function
+  * `deviceId` string (optional)
+
+Emitted when a USB device needs to be selected when a call to
+`navigator.usb.requestDevice` is made. `callback` should be called with
+`deviceId` to be selected; passing no arguments to `callback` will
+cancel the request.  Additionally, permissioning on `navigator.usb` can
+be further managed by using [ses.setPermissionCheckHandler(handler)](#sessetpermissioncheckhandlerhandler)
+and [ses.setDevicePermissionHandler(handler)`](#sessetdevicepermissionhandlerhandler).
+
+```javascript
+const { app, BrowserWindow } = require('electron')
+
+let win = null
+
+app.whenReady().then(() => {
+  win = new BrowserWindow()
+
+  win.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => {
+    if (permission === 'usb') {
+      // Add logic here to determine if permission should be given to allow USB selection
+      return true
+    }
+    return false
+  })
+
+  // Optionally, retrieve previously persisted devices from a persistent store (fetchGrantedDevices needs to be implemented by developer to fetch persisted permissions)
+  const grantedDevices = fetchGrantedDevices()
+
+  win.webContents.session.setDevicePermissionHandler((details) => {
+    if (new URL(details.origin).hostname === 'some-host' && details.deviceType === 'usb') {
+      if (details.device.vendorId === 123 && details.device.productId === 345) {
+        // Always allow this type of device (this allows skipping the call to `navigator.usb.requestDevice` first)
+        return true
+      }
+
+      // Search through the list of devices that have previously been granted permission
+      return grantedDevices.some((grantedDevice) => {
+        return grantedDevice.vendorId === details.device.vendorId &&
+              grantedDevice.productId === details.device.productId &&
+              grantedDevice.serialNumber && grantedDevice.serialNumber === details.device.serialNumber
+      })
+    }
+    return false
+  })
+
+  win.webContents.session.on('select-usb-device', (event, details, callback) => {
+    event.preventDefault()
+    const selectedDevice = details.deviceList.find((device) => {
+      return device.vendorId === '9025' && device.productId === '67'
+    })
+    if (selectedDevice) {
+      // Optionally, add this to the persisted devices (updateGrantedDevices needs to be implemented by developer to persist permissions)
+      grantedDevices.push(selectedDevice)
+      updateGrantedDevices(grantedDevices)
+    }
+    callback(selectedDevice?.deviceId)
+  })
+})
+```
+
+#### Event: 'usb-device-added'
+
+Returns:
+
+* `event` Event
+* `details` Object
+  * `device` [USBDevice](structures/usb-device.md)
+  * `frame` [WebFrameMain](web-frame-main.md)
+
+Emitted after `navigator.usb.requestDevice` has been called and
+`select-usb-device` has fired if a new device becomes available before
+the callback from `select-usb-device` is called.  This event is intended for
+use when using a UI to ask users to pick a device so that the UI can be updated
+with the newly added device.
+
+#### Event: 'usb-device-removed'
+
+Returns:
+
+* `event` Event
+* `details` Object
+  * `device` [USBDevice](structures/usb-device.md)
+  * `frame` [WebFrameMain](web-frame-main.md)
+
+Emitted after `navigator.usb.requestDevice` has been called and
+`select-usb-device` has fired if a device has been removed before the callback
+from `select-usb-device` is called.  This event is intended for use when using
+a UI to ask users to pick a device so that the UI can be updated to remove the
+specified device.
+
+#### Event: 'usb-device-revoked'
+
+Returns:
+
+* `event` Event
+* `details` Object
+  * `device` [USBDevice[]](structures/usb-device.md)
+  * `origin` string (optional) - The origin that the device has been revoked from.
+
+Emitted after `USBDevice.forget()` has been called.  This event can be used
+to help maintain persistent storage of permissions when
+`setDevicePermissionHandler` is used.
+
 ### Instance Methods
 
 The following methods are available on instances of `Session`:
@@ -714,7 +826,7 @@ session.fromPartition('some-partition').setPermissionRequestHandler((webContents
 
 * `handler` Function\<boolean> | null
   * `webContents` ([WebContents](web-contents.md) | null) - WebContents checking the permission.  Please note that if the request comes from a subframe you should use `requestingUrl` to check the request origin.  All cross origin sub frames making permission checks will pass a `null` webContents to this handler, while certain other permission checks such as `notifications` checks will always pass `null`.  You should use `embeddingOrigin` and `requestingOrigin` to determine what origin the owning frame and the requesting frame are on respectively.
-  * `permission` string - Type of permission check.  Valid values are `midiSysex`, `notifications`, `geolocation`, `media`,`mediaKeySystem`,`midi`, `pointerLock`, `fullscreen`, `openExternal`, `hid`, or `serial`.
+  * `permission` string - Type of permission check.  Valid values are `midiSysex`, `notifications`, `geolocation`, `media`,`mediaKeySystem`,`midi`, `pointerLock`, `fullscreen`, `openExternal`, `hid`, `serial`, or `usb`.
   * `requestingOrigin` string - The origin URL of the permission check
   * `details` Object - Some properties are only available on certain permission types.
     * `embeddingOrigin` string (optional) - The origin of the frame embedding the frame that made the permission check.  Only set for cross-origin sub frames making permission checks.
@@ -800,7 +912,7 @@ Passing `null` instead of a function resets the handler to its default state.
 
 * `handler` Function\<boolean> | null
   * `details` Object
-    * `deviceType` string - The type of device that permission is being requested on, can be `hid` or `serial`.
+    * `deviceType` string - The type of device that permission is being requested on, can be `hid`, `serial`, or `usb`.
     * `origin` string - The origin URL of the device permission check.
     * `device` [HIDDevice](structures/hid-device.md) | [SerialPort](structures/serial-port.md)- the device that permission is being requested for.
 
@@ -828,6 +940,8 @@ app.whenReady().then(() => {
       return true
     } else if (permission === 'serial') {
       // Add logic here to determine if permission should be given to allow serial port selection
+    } else if (permission === 'usb') {
+      // Add logic here to determine if permission should be given to allow USB device selection
     }
     return false
   })
diff --git a/docs/api/structures/usb-device.md b/docs/api/structures/usb-device.md
new file mode 100644
index 0000000000000..e1f427fb4edbf
--- /dev/null
+++ b/docs/api/structures/usb-device.md
@@ -0,0 +1,17 @@
+# USBDevice Object
+
+* `deviceId` string - Unique identifier for the device.
+* `vendorId` Integer - The USB vendor ID.
+* `productId` Integer - The USB product ID.
+* `productName` string (optional) - Name of the device.
+* `serialNumber` string (optional) - The USB device serial number.
+* `manufacturerName` string (optional) - The manufacturer name of the device.
+* `usbVersionMajor` Integer - The USB protocol major version supported by the device
+* `usbVersionMinor` Integer - The USB protocol minor version supported by the device
+* `usbVersionSubminor` Integer - The USB protocol subminor version supported by the device
+* `deviceClass` Integer - The device class for the communication interface supported by the device
+* `deviceSubclass` Integer - The device subclass for the communication interface supported by the device
+* `deviceProtocol` Integer - The device protocol for the communication interface supported by the device
+* `deviceVersionMajor` Integer - The major version number of the device as defined by the device manufacturer.
+* `deviceVersionMinor` Integer - The minor version number of the device as defined by the device manufacturer.
+* `deviceVersionSubminor` Integer - The subminor version number of the device as defined by the device manufacturer.
diff --git a/docs/fiddles/features/web-usb/index.html b/docs/fiddles/features/web-usb/index.html
new file mode 100644
index 0000000000000..95541d9a1fee1
--- /dev/null
+++ b/docs/fiddles/features/web-usb/index.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="UTF-8">
+    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
+    <title>WebUSB API</title>
+  </head>
+  <body>
+    <h1>WebUSB API</h1>
+
+    <button id="clickme">Test WebUSB</button>
+
+    <h3>USB devices automatically granted access via <i>setDevicePermissionHandler</i></h3>
+    <div id="granted-devices"></div>
+    
+    <h3>USB devices automatically granted access via <i>select-usb-device</i></h3>
+    <div id="granted-devices2"></div>
+
+    <script src="./renderer.js"></script>
+  </body>
+</html>
diff --git a/docs/fiddles/features/web-usb/main.js b/docs/fiddles/features/web-usb/main.js
new file mode 100644
index 0000000000000..316a0dbdf86a8
--- /dev/null
+++ b/docs/fiddles/features/web-usb/main.js
@@ -0,0 +1,72 @@
+const {app, BrowserWindow} = require('electron')
+const e = require('express')
+const path = require('path')
+
+function createWindow () {
+  const mainWindow = new BrowserWindow({
+    width: 800,
+    height: 600
+  })
+  
+  let grantedDeviceThroughPermHandler
+
+  mainWindow.webContents.session.on('select-usb-device', (event, details, callback) => {
+    //Add events to handle devices being added or removed before the callback on
+    //`select-usb-device` is called.
+    mainWindow.webContents.session.on('usb-device-added', (event, device) => {    
+      console.log('usb-device-added FIRED WITH', device)
+      //Optionally update details.deviceList
+    })
+  
+    mainWindow.webContents.session.on('usb-device-removed', (event, device) => {
+      console.log('usb-device-removed FIRED WITH', device)
+      //Optionally update details.deviceList
+    })
+
+    event.preventDefault()
+    if (details.deviceList && details.deviceList.length > 0) {
+      const deviceToReturn  = details.deviceList.find((device) => {
+        if (!grantedDeviceThroughPermHandler || (device.deviceId != grantedDeviceThroughPermHandler.deviceId)) {
+          return true
+        }
+      })
+      if (deviceToReturn) {
+        callback(deviceToReturn.deviceId)        
+      } else {
+        callback()
+      }
+    }
+  })
+
+  mainWindow.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => {
+    if (permission === 'usb' && details.securityOrigin === 'file:///') {
+      return true
+    }
+  })
+
+  
+  mainWindow.webContents.session.setDevicePermissionHandler((details) => {
+    if (details.deviceType === 'usb' && details.origin === 'file://') {
+      if (!grantedDeviceThroughPermHandler) {        
+        grantedDeviceThroughPermHandler = details.device
+        return true
+      } else {
+        return false
+      }
+    }
+  })
+  
+  mainWindow.loadFile('index.html')
+}
+
+app.whenReady().then(() => {
+  createWindow()
+  
+  app.on('activate', function () {
+    if (BrowserWindow.getAllWindows().length === 0) createWindow()
+  })
+})
+
+app.on('window-all-closed', function () {
+  if (process.platform !== 'darwin') app.quit()
+})
diff --git a/docs/fiddles/features/web-usb/renderer.js b/docs/fiddles/features/web-usb/renderer.js
new file mode 100644
index 0000000000000..f1aa3be30b46f
--- /dev/null
+++ b/docs/fiddles/features/web-usb/renderer.js
@@ -0,0 +1,33 @@
+function getDeviceDetails(device) {
+  return grantedDevice.productName || `Unknown device ${grantedDevice.deviceId}`
+}
+
+async function testIt() {
+  const noDevicesFoundMsg = 'No devices found'
+  const grantedDevices = await navigator.usb.getDevices()
+  let grantedDeviceList = ''
+  if (grantedDevices.length > 0) {    
+    grantedDevices.forEach(device => {
+      grantedDeviceList += `<hr>${getDeviceDetails(device)}</hr>`
+    })    
+  } else {
+    grantedDeviceList = noDevicesFoundMsg
+  }
+  document.getElementById('granted-devices').innerHTML = grantedDeviceList
+
+  grantedDeviceList = ''
+  try {
+    const grantedDevice = await navigator.usb.requestDevice({
+      filters: []
+    })
+    grantedDeviceList += `<hr>${getDeviceDetails(device)}</hr>`
+    
+  } catch (ex) {
+    if (ex.name === 'NotFoundError') {
+      grantedDeviceList = noDevicesFoundMsg
+    }
+  }
+  document.getElementById('granted-devices2').innerHTML = grantedDeviceList
+}
+
+document.getElementById('clickme').addEventListener('click',testIt)
diff --git a/docs/tutorial/devices.md b/docs/tutorial/devices.md
index 6f11fec760208..fc988c522424b 100644
--- a/docs/tutorial/devices.md
+++ b/docs/tutorial/devices.md
@@ -115,3 +115,41 @@ when the `Test Web Serial` button is clicked.
 ```javascript fiddle='docs/fiddles/features/web-serial'
 
 ```
+
+## WebUSB API
+
+The [WebUSB API](https://web.dev/usb/) can be used to access USB devices.
+Electron provides several APIs for working with the WebUSB API:
+
+* The [`select-usb-device` event on the Session](../api/session.md#event-select-usb-device)
+  can be used to select a USB device when a call to
+  `navigator.usb.requestDevice` is made.  Additionally the [`usb-device-added`](../api/session.md#event-usb-device-added)
+  and [`usb-device-removed`](../api/session.md#event-usb-device-removed) events
+  on the Session can be used to handle devices being plugged in or unplugged
+  when handling the `select-usb-device` event.
+  **Note:** These two events only fire until the callback from `select-usb-device`
+  is called.  They are not intended to be used as a generic usb device listener.
+* The [`usb-device-revoked' event on the Session](../api/session.md#event-usb-device-revoked) can
+  be used to respond when [device.forget()](https://developer.chrome.com/articles/usb/#revoke-access)
+  is called on a USB device.
+* [`ses.setDevicePermissionHandler(handler)`](../api/session.md#sessetdevicepermissionhandlerhandler)
+  can be used to provide default permissioning to devices without first calling
+  for permission to devices via `navigator.usb.requestDevice`.  Additionally,
+  the default behavior of Electron is to store granted device permission through
+  the lifetime of the corresponding WebContents.  If longer term storage is
+  needed, a developer can store granted device permissions (eg when handling
+  the `select-usb-device` event) and then read from that storage with
+  `setDevicePermissionHandler`.
+* [`ses.setPermissionCheckHandler(handler)`](../api/session.md#sessetpermissioncheckhandlerhandler)
+  can be used to disable USB access for specific origins.
+
+### Example
+
+This example demonstrates an Electron application that automatically selects
+USB devices (if they are attached) through [`ses.setDevicePermissionHandler(handler)`](../api/session.md#sessetdevicepermissionhandlerhandler)
+and through [`select-usb-device` event on the Session](../api/session.md#event-select-usb-device)
+when the `Test WebUSB` button is clicked.
+
+```javascript fiddle='docs/fiddles/features/web-usb'
+
+```
diff --git a/filenames.auto.gni b/filenames.auto.gni
index f34e4c88ccd05..274e7270a1a4f 100644
--- a/filenames.auto.gni
+++ b/filenames.auto.gni
@@ -132,6 +132,7 @@ auto_filenames = {
     "docs/api/structures/upload-data.md",
     "docs/api/structures/upload-file.md",
     "docs/api/structures/upload-raw-data.md",
+    "docs/api/structures/usb-device.md",
     "docs/api/structures/user-default-types.md",
     "docs/api/structures/web-request-filter.md",
     "docs/api/structures/web-source.md",
diff --git a/filenames.gni b/filenames.gni
index c87fa8744e0ec..d68a04943b430 100644
--- a/filenames.gni
+++ b/filenames.gni
@@ -505,6 +505,14 @@ filenames = {
     "shell/browser/ui/tray_icon_observer.h",
     "shell/browser/ui/webui/accessibility_ui.cc",
     "shell/browser/ui/webui/accessibility_ui.h",
+    "shell/browser/usb/electron_usb_delegate.cc",
+    "shell/browser/usb/electron_usb_delegate.h",
+    "shell/browser/usb/usb_chooser_context.cc",
+    "shell/browser/usb/usb_chooser_context.h",
+    "shell/browser/usb/usb_chooser_context_factory.cc",
+    "shell/browser/usb/usb_chooser_context_factory.h",
+    "shell/browser/usb/usb_chooser_controller.cc",
+    "shell/browser/usb/usb_chooser_controller.h",
     "shell/browser/web_contents_permission_helper.cc",
     "shell/browser/web_contents_permission_helper.h",
     "shell/browser/web_contents_preferences.cc",
@@ -586,6 +594,7 @@ filenames = {
     "shell/common/gin_converters/std_converter.h",
     "shell/common/gin_converters/time_converter.cc",
     "shell/common/gin_converters/time_converter.h",
+    "shell/common/gin_converters/usb_device_info_converter.h",
     "shell/common/gin_converters/value_converter.cc",
     "shell/common/gin_converters/value_converter.h",
     "shell/common/gin_helper/arguments.cc",
diff --git a/shell/browser/electron_browser_client.cc b/shell/browser/electron_browser_client.cc
index 51faa6ae8ab16..89e3603fd11ab 100644
--- a/shell/browser/electron_browser_client.cc
+++ b/shell/browser/electron_browser_client.cc
@@ -1715,6 +1715,12 @@ content::BluetoothDelegate* ElectronBrowserClient::GetBluetoothDelegate() {
   return bluetooth_delegate_.get();
 }
 
+content::UsbDelegate* ElectronBrowserClient::GetUsbDelegate() {
+  if (!usb_delegate_)
+    usb_delegate_ = std::make_unique<ElectronUsbDelegate>();
+  return usb_delegate_.get();
+}
+
 void BindBadgeServiceForServiceWorker(
     const content::ServiceWorkerVersionBaseInfo& info,
     mojo::PendingReceiver<blink::mojom::BadgeService> receiver) {
diff --git a/shell/browser/electron_browser_client.h b/shell/browser/electron_browser_client.h
index 840ff7b31e64f..a410ef9781f94 100644
--- a/shell/browser/electron_browser_client.h
+++ b/shell/browser/electron_browser_client.h
@@ -22,6 +22,7 @@
 #include "shell/browser/bluetooth/electron_bluetooth_delegate.h"
 #include "shell/browser/hid/electron_hid_delegate.h"
 #include "shell/browser/serial/electron_serial_delegate.h"
+#include "shell/browser/usb/electron_usb_delegate.h"
 #include "third_party/blink/public/mojom/badging/badging.mojom-forward.h"
 
 namespace content {
@@ -103,6 +104,7 @@ class ElectronBrowserClient : public content::ContentBrowserClient,
   content::BluetoothDelegate* GetBluetoothDelegate() override;
 
   content::HidDelegate* GetHidDelegate() override;
+  content::UsbDelegate* GetUsbDelegate() override;
 
   content::WebAuthenticationDelegate* GetWebAuthenticationDelegate() override;
 
@@ -326,6 +328,7 @@ class ElectronBrowserClient : public content::ContentBrowserClient,
 
   std::unique_ptr<ElectronSerialDelegate> serial_delegate_;
   std::unique_ptr<ElectronBluetoothDelegate> bluetooth_delegate_;
+  std::unique_ptr<ElectronUsbDelegate> usb_delegate_;
   std::unique_ptr<ElectronHidDelegate> hid_delegate_;
   std::unique_ptr<ElectronWebAuthenticationDelegate>
       web_authentication_delegate_;
diff --git a/shell/browser/electron_browser_context.cc b/shell/browser/electron_browser_context.cc
index 14e4e15149843..3e1706747ce7e 100644
--- a/shell/browser/electron_browser_context.cc
+++ b/shell/browser/electron_browser_context.cc
@@ -51,6 +51,7 @@
 #include "shell/browser/web_view_manager.h"
 #include "shell/browser/zoom_level_delegate.h"
 #include "shell/common/application_info.h"
+#include "shell/common/electron_constants.h"
 #include "shell/common/electron_paths.h"
 #include "shell/common/gin_converters/frame_converter.h"
 #include "shell/common/gin_helper/error_thrower.h"
@@ -583,19 +584,22 @@ bool ElectronBrowserContext::DoesDeviceMatch(
     const base::Value* device_to_compare,
     blink::PermissionType permission_type) {
   if (permission_type ==
-      static_cast<blink::PermissionType>(
-          WebContentsPermissionHelper::PermissionType::HID)) {
-    if (device.GetDict().FindInt(kHidVendorIdKey) !=
-            device_to_compare->GetDict().FindInt(kHidVendorIdKey) ||
-        device.GetDict().FindInt(kHidProductIdKey) !=
-            device_to_compare->GetDict().FindInt(kHidProductIdKey)) {
+          static_cast<blink::PermissionType>(
+              WebContentsPermissionHelper::PermissionType::HID) ||
+      permission_type ==
+          static_cast<blink::PermissionType>(
+              WebContentsPermissionHelper::PermissionType::USB)) {
+    if (device.GetDict().FindInt(kDeviceVendorIdKey) !=
+            device_to_compare->GetDict().FindInt(kDeviceVendorIdKey) ||
+        device.GetDict().FindInt(kDeviceProductIdKey) !=
+            device_to_compare->GetDict().FindInt(kDeviceProductIdKey)) {
       return false;
     }
 
     const auto* serial_number =
-        device_to_compare->GetDict().FindString(kHidSerialNumberKey);
+        device_to_compare->GetDict().FindString(kDeviceSerialNumberKey);
     const auto* device_serial_number =
-        device.GetDict().FindString(kHidSerialNumberKey);
+        device.GetDict().FindString(kDeviceSerialNumberKey);
 
     if (serial_number && device_serial_number &&
         *device_serial_number == *serial_number)
diff --git a/shell/browser/feature_list.cc b/shell/browser/feature_list.cc
index 47c057fdae4b2..0cf9374876bcd 100644
--- a/shell/browser/feature_list.cc
+++ b/shell/browser/feature_list.cc
@@ -17,6 +17,10 @@
 #include "net/base/features.h"
 #include "services/network/public/cpp/features.h"
 
+#if BUILDFLAG(IS_MAC)
+#include "device/base/features.h"  // nogncheck
+#endif
+
 namespace electron {
 
 void InitializeFeatureList() {
@@ -32,6 +36,11 @@ void InitializeFeatureList() {
   disable_features +=
       std::string(",") + features::kSpareRendererForSitePerProcess.name;
 
+#if BUILDFLAG(IS_MAC)
+  // Needed for WebUSB implementation
+  enable_features += std::string(",") + device::kNewUsbBackend.name;
+#endif
+
 #if !BUILDFLAG(ENABLE_PICTURE_IN_PICTURE)
   disable_features += std::string(",") + media::kPictureInPicture.name;
 #endif
diff --git a/shell/browser/hid/hid_chooser_context.cc b/shell/browser/hid/hid_chooser_context.cc
index 108ecb28f49fb..4445a2b7b142e 100644
--- a/shell/browser/hid/hid_chooser_context.cc
+++ b/shell/browser/hid/hid_chooser_context.cc
@@ -22,6 +22,7 @@
 #include "shell/browser/api/electron_api_session.h"
 #include "shell/browser/electron_permission_manager.h"
 #include "shell/browser/web_contents_permission_helper.h"
+#include "shell/common/electron_constants.h"
 #include "shell/common/gin_converters/content_converter.h"
 #include "shell/common/gin_converters/frame_converter.h"
 #include "shell/common/gin_converters/hid_device_info_converter.h"
@@ -35,9 +36,6 @@ namespace electron {
 
 const char kHidDeviceNameKey[] = "name";
 const char kHidGuidKey[] = "guid";
-const char kHidVendorIdKey[] = "vendorId";
-const char kHidProductIdKey[] = "productId";
-const char kHidSerialNumberKey[] = "serialNumber";
 
 HidChooserContext::HidChooserContext(ElectronBrowserContext* context)
     : browser_context_(context) {}
@@ -76,12 +74,12 @@ base::Value HidChooserContext::DeviceInfoToValue(
   value.SetStringKey(
       kHidDeviceNameKey,
       base::UTF16ToUTF8(HidChooserContext::DisplayNameFromDeviceInfo(device)));
-  value.SetIntKey(kHidVendorIdKey, device.vendor_id);
-  value.SetIntKey(kHidProductIdKey, device.product_id);
+  value.SetIntKey(kDeviceVendorIdKey, device.vendor_id);
+  value.SetIntKey(kDeviceProductIdKey, device.product_id);
   if (HidChooserContext::CanStorePersistentEntry(device)) {
     // Use the USB serial number as a persistent identifier. If it is
     // unavailable, only ephemeral permissions may be granted.
-    value.SetStringKey(kHidSerialNumberKey, device.serial_number);
+    value.SetStringKey(kDeviceSerialNumberKey, device.serial_number);
   } else {
     // The GUID is a temporary ID created on connection that remains valid until
     // the device is disconnected. Ephemeral permissions are keyed by this ID
diff --git a/shell/browser/hid/hid_chooser_context.h b/shell/browser/hid/hid_chooser_context.h
index e748eeb396153..6c174427f4529 100644
--- a/shell/browser/hid/hid_chooser_context.h
+++ b/shell/browser/hid/hid_chooser_context.h
@@ -33,9 +33,7 @@ namespace electron {
 
 extern const char kHidDeviceNameKey[];
 extern const char kHidGuidKey[];
-extern const char kHidVendorIdKey[];
 extern const char kHidProductIdKey[];
-extern const char kHidSerialNumberKey[];
 
 // Manages the internal state and connection to the device service for the
 // Human Interface Device (HID) chooser UI.
diff --git a/shell/browser/serial/serial_chooser_context.h b/shell/browser/serial/serial_chooser_context.h
index 1c3e2d7641dce..f64597b6677d3 100644
--- a/shell/browser/serial/serial_chooser_context.h
+++ b/shell/browser/serial/serial_chooser_context.h
@@ -29,9 +29,6 @@ class Value;
 
 namespace electron {
 
-extern const char kHidVendorIdKey[];
-extern const char kHidProductIdKey[];
-
 #if BUILDFLAG(IS_WIN)
 extern const char kDeviceInstanceIdKey[];
 #else
diff --git a/shell/browser/usb/electron_usb_delegate.cc b/shell/browser/usb/electron_usb_delegate.cc
new file mode 100644
index 0000000000000..a5bb4bc0e3750
--- /dev/null
+++ b/shell/browser/usb/electron_usb_delegate.cc
@@ -0,0 +1,317 @@
+// Copyright (c) 2022 Microsoft, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#include "shell/browser/usb/electron_usb_delegate.h"
+
+#include <utility>
+
+#include "base/containers/contains.h"
+#include "base/containers/cxx20_erase.h"
+#include "base/observer_list.h"
+#include "base/observer_list_types.h"
+#include "base/scoped_observation.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/web_contents.h"
+#include "extensions/buildflags/buildflags.h"
+#include "services/device/public/mojom/usb_enumeration_options.mojom.h"
+#include "shell/browser/electron_permission_manager.h"
+#include "shell/browser/usb/usb_chooser_context.h"
+#include "shell/browser/usb/usb_chooser_context_factory.h"
+#include "shell/browser/usb/usb_chooser_controller.h"
+#include "shell/browser/web_contents_permission_helper.h"
+
+#if BUILDFLAG(ENABLE_EXTENSIONS)
+#include "base/containers/fixed_flat_set.h"
+#include "chrome/common/chrome_features.h"
+#include "extensions/browser/extension_registry.h"
+#include "extensions/common/constants.h"
+#include "extensions/common/extension.h"
+#include "services/device/public/mojom/usb_device.mojom.h"
+#endif
+
+namespace {
+
+using ::content::UsbChooser;
+
+electron::UsbChooserContext* GetChooserContext(
+    content::BrowserContext* browser_context) {
+  return electron::UsbChooserContextFactory::GetForBrowserContext(
+      browser_context);
+}
+
+#if BUILDFLAG(ENABLE_EXTENSIONS)
+// These extensions can claim the smart card USB class and automatically gain
+// permissions for devices that have an interface with this class.
+constexpr auto kSmartCardPrivilegedExtensionIds =
+    base::MakeFixedFlatSet<base::StringPiece>({
+        // Smart Card Connector Extension and its Beta version, see
+        // crbug.com/1233881.
+        "khpfeaanjngmcnplbdlpegiifgpfgdco",
+        "mockcojkppdndnhgonljagclgpkjbkek",
+    });
+
+bool DeviceHasInterfaceWithClass(
+    const device::mojom::UsbDeviceInfo& device_info,
+    uint8_t interface_class) {
+  for (const auto& configuration : device_info.configurations) {
+    for (const auto& interface : configuration->interfaces) {
+      for (const auto& alternate : interface->alternates) {
+        if (alternate->class_code == interface_class)
+          return true;
+      }
+    }
+  }
+  return false;
+}
+#endif  // BUILDFLAG(ENABLE_EXTENSIONS)
+
+bool IsDevicePermissionAutoGranted(
+    const url::Origin& origin,
+    const device::mojom::UsbDeviceInfo& device_info) {
+#if BUILDFLAG(ENABLE_EXTENSIONS)
+  // Note: The `DeviceHasInterfaceWithClass()` call is made after checking the
+  // origin, since that method call is expensive.
+  if (origin.scheme() == extensions::kExtensionScheme &&
+      base::Contains(kSmartCardPrivilegedExtensionIds, origin.host()) &&
+      DeviceHasInterfaceWithClass(device_info,
+                                  device::mojom::kUsbSmartCardClass)) {
+    return true;
+  }
+#endif  // BUILDFLAG(ENABLE_EXTENSIONS)
+
+  return false;
+}
+
+}  // namespace
+
+namespace electron {
+
+// Manages the UsbDelegate observers for a single browser context.
+class ElectronUsbDelegate::ContextObservation
+    : public UsbChooserContext::DeviceObserver {
+ public:
+  ContextObservation(ElectronUsbDelegate* parent,
+                     content::BrowserContext* browser_context)
+      : parent_(parent), browser_context_(browser_context) {
+    auto* chooser_context = GetChooserContext(browser_context_);
+    device_observation_.Observe(chooser_context);
+  }
+  ContextObservation(ContextObservation&) = delete;
+  ContextObservation& operator=(ContextObservation&) = delete;
+  ~ContextObservation() override = default;
+
+  // UsbChooserContext::DeviceObserver:
+  void OnDeviceAdded(const device::mojom::UsbDeviceInfo& device_info) override {
+    for (auto& observer : observer_list_)
+      observer.OnDeviceAdded(device_info);
+  }
+
+  void OnDeviceRemoved(
+      const device::mojom::UsbDeviceInfo& device_info) override {
+    for (auto& observer : observer_list_)
+      observer.OnDeviceRemoved(device_info);
+  }
+
+  void OnDeviceManagerConnectionError() override {
+    for (auto& observer : observer_list_)
+      observer.OnDeviceManagerConnectionError();
+  }
+
+  void OnBrowserContextShutdown() override {
+    parent_->observations_.erase(browser_context_);
+    // Return since `this` is now deleted.
+  }
+
+  void AddObserver(content::UsbDelegate::Observer* observer) {
+    observer_list_.AddObserver(observer);
+  }
+
+  void RemoveObserver(content::UsbDelegate::Observer* observer) {
+    observer_list_.RemoveObserver(observer);
+  }
+
+ private:
+  // Safe because `parent_` owns `this`.
+  const raw_ptr<ElectronUsbDelegate> parent_;
+
+  // Safe because `this` is destroyed when the context is lost.
+  const raw_ptr<content::BrowserContext> browser_context_;
+
+  base::ScopedObservation<UsbChooserContext, UsbChooserContext::DeviceObserver>
+      device_observation_{this};
+  base::ObserverList<content::UsbDelegate::Observer> observer_list_;
+};
+
+ElectronUsbDelegate::ElectronUsbDelegate() = default;
+
+ElectronUsbDelegate::~ElectronUsbDelegate() = default;
+
+void ElectronUsbDelegate::AdjustProtectedInterfaceClasses(
+    content::BrowserContext* browser_context,
+    const url::Origin& origin,
+    content::RenderFrameHost* frame,
+    std::vector<uint8_t>& classes) {
+  // Isolated Apps have unrestricted access to any USB interface class.
+  if (frame && frame->GetWebExposedIsolationLevel() >=
+                   content::RenderFrameHost::WebExposedIsolationLevel::
+                       kMaybeIsolatedApplication) {
+    // TODO(https://crbug.com/1236706): Should the list of interface classes the
+    // app expects to claim be encoded in the Web App Manifest?
+    classes.clear();
+    return;
+  }
+
+#if BUILDFLAG(ENABLE_EXTENSIONS)
+  // Don't enforce protected interface classes for Chrome Apps since the
+  // chrome.usb API has no such restriction.
+  if (origin.scheme() == extensions::kExtensionScheme) {
+    auto* extension_registry =
+        extensions::ExtensionRegistry::Get(browser_context);
+    if (extension_registry) {
+      const extensions::Extension* extension =
+          extension_registry->enabled_extensions().GetByID(origin.host());
+      if (extension && extension->is_platform_app()) {
+        classes.clear();
+        return;
+      }
+    }
+  }
+
+  if (origin.scheme() == extensions::kExtensionScheme &&
+      base::Contains(kSmartCardPrivilegedExtensionIds, origin.host())) {
+    base::Erase(classes, device::mojom::kUsbSmartCardClass);
+  }
+#endif  // BUILDFLAG(ENABLE_EXTENSIONS)
+}
+
+std::unique_ptr<UsbChooser> ElectronUsbDelegate::RunChooser(
+    content::RenderFrameHost& frame,
+    std::vector<device::mojom::UsbDeviceFilterPtr> filters,
+    blink::mojom::WebUsbService::GetPermissionCallback callback) {
+  UsbChooserController* controller = ControllerForFrame(&frame);
+  if (controller) {
+    DeleteControllerForFrame(&frame);
+  }
+  AddControllerForFrame(&frame, std::move(filters), std::move(callback));
+  // Return a nullptr because the return value isn't used for anything. The
+  // return value is simply used in Chromium to cleanup the chooser UI once the
+  // usb service is destroyed.
+  return nullptr;
+}
+
+bool ElectronUsbDelegate::CanRequestDevicePermission(
+    content::BrowserContext* browser_context,
+    const url::Origin& origin) {
+  base::Value::Dict details;
+  details.Set("securityOrigin", origin.GetURL().spec());
+  auto* permission_manager = static_cast<ElectronPermissionManager*>(
+      browser_context->GetPermissionControllerDelegate());
+  return permission_manager->CheckPermissionWithDetails(
+      static_cast<blink::PermissionType>(
+          WebContentsPermissionHelper::PermissionType::USB),
+      nullptr, origin.GetURL(), std::move(details));
+}
+
+void ElectronUsbDelegate::RevokeDevicePermissionWebInitiated(
+    content::BrowserContext* browser_context,
+    const url::Origin& origin,
+    const device::mojom::UsbDeviceInfo& device) {
+  GetChooserContext(browser_context)
+      ->RevokeDevicePermissionWebInitiated(origin, device);
+}
+
+const device::mojom::UsbDeviceInfo* ElectronUsbDelegate::GetDeviceInfo(
+    content::BrowserContext* browser_context,
+    const std::string& guid) {
+  return GetChooserContext(browser_context)->GetDeviceInfo(guid);
+}
+
+bool ElectronUsbDelegate::HasDevicePermission(
+    content::BrowserContext* browser_context,
+    const url::Origin& origin,
+    const device::mojom::UsbDeviceInfo& device) {
+  if (IsDevicePermissionAutoGranted(origin, device))
+    return true;
+
+  return GetChooserContext(browser_context)
+      ->HasDevicePermission(origin, device);
+}
+
+void ElectronUsbDelegate::GetDevices(
+    content::BrowserContext* browser_context,
+    blink::mojom::WebUsbService::GetDevicesCallback callback) {
+  GetChooserContext(browser_context)->GetDevices(std::move(callback));
+}
+
+void ElectronUsbDelegate::GetDevice(
+    content::BrowserContext* browser_context,
+    const std::string& guid,
+    base::span<const uint8_t> blocked_interface_classes,
+    mojo::PendingReceiver<device::mojom::UsbDevice> device_receiver,
+    mojo::PendingRemote<device::mojom::UsbDeviceClient> device_client) {
+  GetChooserContext(browser_context)
+      ->GetDevice(guid, blocked_interface_classes, std::move(device_receiver),
+                  std::move(device_client));
+}
+
+void ElectronUsbDelegate::AddObserver(content::BrowserContext* browser_context,
+                                      Observer* observer) {
+  GetContextObserver(browser_context)->AddObserver(observer);
+}
+
+void ElectronUsbDelegate::RemoveObserver(
+    content::BrowserContext* browser_context,
+    Observer* observer) {
+  GetContextObserver(browser_context)->RemoveObserver(observer);
+}
+
+ElectronUsbDelegate::ContextObservation*
+ElectronUsbDelegate::GetContextObserver(
+    content::BrowserContext* browser_context) {
+  if (!base::Contains(observations_, browser_context)) {
+    observations_.emplace(browser_context, std::make_unique<ContextObservation>(
+                                               this, browser_context));
+  }
+  return observations_[browser_context].get();
+}
+
+bool ElectronUsbDelegate::IsServiceWorkerAllowedForOrigin(
+    const url::Origin& origin) {
+#if BUILDFLAG(ENABLE_EXTENSIONS)
+  // WebUSB is only available on extension service workers for now.
+  if (base::FeatureList::IsEnabled(
+          features::kEnableWebUsbOnExtensionServiceWorker) &&
+      origin.scheme() == extensions::kExtensionScheme) {
+    return true;
+  }
+#endif  // BUILDFLAG(ENABLE_EXTENSIONS)
+  return false;
+}
+
+UsbChooserController* ElectronUsbDelegate::ControllerForFrame(
+    content::RenderFrameHost* render_frame_host) {
+  auto mapping = controller_map_.find(render_frame_host);
+  return mapping == controller_map_.end() ? nullptr : mapping->second.get();
+}
+
+UsbChooserController* ElectronUsbDelegate::AddControllerForFrame(
+    content::RenderFrameHost* render_frame_host,
+    std::vector<device::mojom::UsbDeviceFilterPtr> filters,
+    blink::mojom::WebUsbService::GetPermissionCallback callback) {
+  auto* web_contents =
+      content::WebContents::FromRenderFrameHost(render_frame_host);
+  auto controller = std::make_unique<UsbChooserController>(
+      render_frame_host, std::move(filters), std::move(callback), web_contents,
+      weak_factory_.GetWeakPtr());
+  controller_map_.insert(
+      std::make_pair(render_frame_host, std::move(controller)));
+  return ControllerForFrame(render_frame_host);
+}
+
+void ElectronUsbDelegate::DeleteControllerForFrame(
+    content::RenderFrameHost* render_frame_host) {
+  controller_map_.erase(render_frame_host);
+}
+
+}  // namespace electron
diff --git a/shell/browser/usb/electron_usb_delegate.h b/shell/browser/usb/electron_usb_delegate.h
new file mode 100644
index 0000000000000..943921bfb70ba
--- /dev/null
+++ b/shell/browser/usb/electron_usb_delegate.h
@@ -0,0 +1,106 @@
+// Copyright (c) 2022 Microsoft, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef ELECTRON_SHELL_BROWSER_USB_ELECTRON_USB_DELEGATE_H_
+#define ELECTRON_SHELL_BROWSER_USB_ELECTRON_USB_DELEGATE_H_
+
+#include <memory>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "base/containers/span.h"
+#include "base/memory/weak_ptr.h"
+#include "content/public/browser/usb_chooser.h"
+#include "content/public/browser/usb_delegate.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "services/device/public/mojom/usb_device.mojom-forward.h"
+#include "services/device/public/mojom/usb_enumeration_options.mojom-forward.h"
+#include "services/device/public/mojom/usb_manager.mojom-forward.h"
+#include "third_party/blink/public/mojom/usb/web_usb_service.mojom.h"
+#include "url/origin.h"
+
+namespace content {
+class BrowserContext;
+class RenderFrameHost;
+}  // namespace content
+
+namespace electron {
+
+class UsbChooserController;
+
+class ElectronUsbDelegate : public content::UsbDelegate {
+ public:
+  ElectronUsbDelegate();
+  ElectronUsbDelegate(ElectronUsbDelegate&&) = delete;
+  ElectronUsbDelegate& operator=(ElectronUsbDelegate&) = delete;
+  ~ElectronUsbDelegate() override;
+
+  // content::UsbDelegate:
+  void AdjustProtectedInterfaceClasses(content::BrowserContext* browser_context,
+                                       const url::Origin& origin,
+                                       content::RenderFrameHost* frame,
+                                       std::vector<uint8_t>& classes) override;
+  std::unique_ptr<content::UsbChooser> RunChooser(
+      content::RenderFrameHost& frame,
+      std::vector<device::mojom::UsbDeviceFilterPtr> filters,
+      blink::mojom::WebUsbService::GetPermissionCallback callback) override;
+  bool CanRequestDevicePermission(content::BrowserContext* browser_context,
+                                  const url::Origin& origin) override;
+  void RevokeDevicePermissionWebInitiated(
+      content::BrowserContext* browser_context,
+      const url::Origin& origin,
+      const device::mojom::UsbDeviceInfo& device) override;
+  const device::mojom::UsbDeviceInfo* GetDeviceInfo(
+      content::BrowserContext* browser_context,
+      const std::string& guid) override;
+  bool HasDevicePermission(content::BrowserContext* browser_context,
+                           const url::Origin& origin,
+                           const device::mojom::UsbDeviceInfo& device) override;
+  void GetDevices(
+      content::BrowserContext* browser_context,
+      blink::mojom::WebUsbService::GetDevicesCallback callback) override;
+  void GetDevice(
+      content::BrowserContext* browser_context,
+      const std::string& guid,
+      base::span<const uint8_t> blocked_interface_classes,
+      mojo::PendingReceiver<device::mojom::UsbDevice> device_receiver,
+      mojo::PendingRemote<device::mojom::UsbDeviceClient> device_client)
+      override;
+  void AddObserver(content::BrowserContext* browser_context,
+                   Observer* observer) override;
+  void RemoveObserver(content::BrowserContext* browser_context,
+                      Observer* observer) override;
+  bool IsServiceWorkerAllowedForOrigin(const url::Origin& origin) override;
+
+  void DeleteControllerForFrame(content::RenderFrameHost* render_frame_host);
+
+ private:
+  UsbChooserController* ControllerForFrame(
+      content::RenderFrameHost* render_frame_host);
+
+  UsbChooserController* AddControllerForFrame(
+      content::RenderFrameHost* render_frame_host,
+      std::vector<device::mojom::UsbDeviceFilterPtr> filters,
+      blink::mojom::WebUsbService::GetPermissionCallback callback);
+
+  class ContextObservation;
+
+  ContextObservation* GetContextObserver(
+      content::BrowserContext* browser_context);
+
+  base::flat_map<content::BrowserContext*, std::unique_ptr<ContextObservation>>
+      observations_;
+
+  std::unordered_map<content::RenderFrameHost*,
+                     std::unique_ptr<UsbChooserController>>
+      controller_map_;
+
+  base::WeakPtrFactory<ElectronUsbDelegate> weak_factory_{this};
+};
+
+}  // namespace electron
+
+#endif  // ELECTRON_SHELL_BROWSER_USB_ELECTRON_USB_DELEGATE_H_
diff --git a/shell/browser/usb/usb_chooser_context.cc b/shell/browser/usb/usb_chooser_context.cc
new file mode 100644
index 0000000000000..79f101d8c0ce0
--- /dev/null
+++ b/shell/browser/usb/usb_chooser_context.cc
@@ -0,0 +1,354 @@
+// Copyright (c) 2022 Microsoft, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#include "shell/browser/usb/usb_chooser_context.h"
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "base/bind.h"
+#include "base/containers/contains.h"
+#include "base/observer_list.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/threading/sequenced_task_runner_handle.h"
+#include "base/values.h"
+#include "build/build_config.h"
+#include "components/content_settings/core/common/content_settings.h"
+#include "content/public/browser/device_service.h"
+#include "services/device/public/cpp/usb/usb_ids.h"
+#include "services/device/public/mojom/usb_device.mojom.h"
+#include "shell/browser/api/electron_api_session.h"
+#include "shell/browser/electron_permission_manager.h"
+#include "shell/browser/web_contents_permission_helper.h"
+#include "shell/common/electron_constants.h"
+#include "shell/common/gin_converters/usb_device_info_converter.h"
+#include "shell/common/node_includes.h"
+#include "ui/base/l10n/l10n_util.h"
+
+namespace {
+
+constexpr char kDeviceNameKey[] = "productName";
+constexpr char kDeviceIdKey[] = "deviceId";
+constexpr int kUsbClassMassStorage = 0x08;
+
+bool CanStorePersistentEntry(const device::mojom::UsbDeviceInfo& device_info) {
+  return device_info.serial_number && !device_info.serial_number->empty();
+}
+
+bool IsMassStorageInterface(const device::mojom::UsbInterfaceInfo& interface) {
+  for (auto& alternate : interface.alternates) {
+    if (alternate->class_code == kUsbClassMassStorage)
+      return true;
+  }
+  return false;
+}
+
+bool ShouldExposeDevice(const device::mojom::UsbDeviceInfo& device_info) {
+  // blink::USBDevice::claimInterface() disallows claiming mass storage
+  // interfaces, but explicitly prevent access in the browser process as
+  // ChromeOS would allow these interfaces to be claimed.
+  for (auto& configuration : device_info.configurations) {
+    if (configuration->interfaces.size() == 0) {
+      return true;
+    }
+    for (auto& interface : configuration->interfaces) {
+      if (!IsMassStorageInterface(*interface))
+        return true;
+    }
+  }
+  return false;
+}
+
+}  // namespace
+
+namespace electron {
+
+void UsbChooserContext::DeviceObserver::OnDeviceAdded(
+    const device::mojom::UsbDeviceInfo& device_info) {}
+
+void UsbChooserContext::DeviceObserver::OnDeviceRemoved(
+    const device::mojom::UsbDeviceInfo& device_info) {}
+
+void UsbChooserContext::DeviceObserver::OnDeviceManagerConnectionError() {}
+
+UsbChooserContext::UsbChooserContext(ElectronBrowserContext* context)
+    : browser_context_(context) {}
+
+// static
+base::Value UsbChooserContext::DeviceInfoToValue(
+    const device::mojom::UsbDeviceInfo& device_info) {
+  base::Value device_value(base::Value::Type::DICTIONARY);
+  device_value.SetStringKey(kDeviceNameKey, device_info.product_name
+                                                ? *device_info.product_name
+                                                : base::StringPiece16());
+  device_value.SetIntKey(kDeviceVendorIdKey, device_info.vendor_id);
+  device_value.SetIntKey(kDeviceProductIdKey, device_info.product_id);
+
+  if (device_info.manufacturer_name) {
+    device_value.SetStringKey("manufacturerName",
+                              *device_info.manufacturer_name);
+  }
+
+  // CanStorePersistentEntry checks if |device_info.serial_number| is not empty.
+  if (CanStorePersistentEntry(device_info)) {
+    device_value.SetStringKey(kDeviceSerialNumberKey,
+                              *device_info.serial_number);
+  }
+
+  device_value.SetStringKey(kDeviceIdKey, device_info.guid);
+
+  device_value.SetIntKey("usbVersionMajor", device_info.usb_version_major);
+  device_value.SetIntKey("usbVersionMinor", device_info.usb_version_minor);
+  device_value.SetIntKey("usbVersionSubminor",
+                         device_info.usb_version_subminor);
+  device_value.SetIntKey("deviceClass", device_info.class_code);
+  device_value.SetIntKey("deviceSubclass", device_info.subclass_code);
+  device_value.SetIntKey("deviceProtocol", device_info.protocol_code);
+  device_value.SetIntKey("deviceVersionMajor",
+                         device_info.device_version_major);
+  device_value.SetIntKey("deviceVersionMinor",
+                         device_info.device_version_minor);
+  device_value.SetIntKey("deviceVersionSubminor",
+                         device_info.device_version_subminor);
+  return device_value;
+}
+
+void UsbChooserContext::InitDeviceList(
+    std::vector<device::mojom::UsbDeviceInfoPtr> devices) {
+  for (auto& device_info : devices) {
+    DCHECK(device_info);
+    if (ShouldExposeDevice(*device_info)) {
+      devices_.insert(
+          std::make_pair(device_info->guid, std::move(device_info)));
+    }
+  }
+  is_initialized_ = true;
+
+  while (!pending_get_devices_requests_.empty()) {
+    std::vector<device::mojom::UsbDeviceInfoPtr> device_list;
+    for (const auto& entry : devices_) {
+      device_list.push_back(entry.second->Clone());
+    }
+    std::move(pending_get_devices_requests_.front())
+        .Run(std::move(device_list));
+    pending_get_devices_requests_.pop();
+  }
+}
+
+void UsbChooserContext::EnsureConnectionWithDeviceManager() {
+  if (device_manager_)
+    return;
+
+  // Receive mojo::Remote<UsbDeviceManager> from DeviceService.
+  content::GetDeviceService().BindUsbDeviceManager(
+      device_manager_.BindNewPipeAndPassReceiver());
+
+  SetUpDeviceManagerConnection();
+}
+
+void UsbChooserContext::SetUpDeviceManagerConnection() {
+  DCHECK(device_manager_);
+  device_manager_.set_disconnect_handler(
+      base::BindOnce(&UsbChooserContext::OnDeviceManagerConnectionError,
+                     base::Unretained(this)));
+
+  // Listen for added/removed device events.
+  DCHECK(!client_receiver_.is_bound());
+  device_manager_->EnumerateDevicesAndSetClient(
+      client_receiver_.BindNewEndpointAndPassRemote(),
+      base::BindOnce(&UsbChooserContext::InitDeviceList,
+                     weak_factory_.GetWeakPtr()));
+}
+
+UsbChooserContext::~UsbChooserContext() {
+  OnDeviceManagerConnectionError();
+  for (auto& observer : device_observer_list_) {
+    observer.OnBrowserContextShutdown();
+    DCHECK(!device_observer_list_.HasObserver(&observer));
+  }
+}
+
+void UsbChooserContext::RevokeDevicePermissionWebInitiated(
+    const url::Origin& origin,
+    const device::mojom::UsbDeviceInfo& device) {
+  DCHECK(base::Contains(devices_, device.guid));
+  RevokeObjectPermissionInternal(origin, DeviceInfoToValue(device),
+                                 /*revoked_by_website=*/true);
+}
+
+void UsbChooserContext::RevokeObjectPermissionInternal(
+    const url::Origin& origin,
+    const base::Value& object,
+    bool revoked_by_website = false) {
+  if (object.FindStringKey(kDeviceSerialNumberKey)) {
+    auto* permission_manager = static_cast<ElectronPermissionManager*>(
+        browser_context_->GetPermissionControllerDelegate());
+    permission_manager->RevokeDevicePermission(
+        static_cast<blink::PermissionType>(
+            WebContentsPermissionHelper::PermissionType::USB),
+        origin, object, browser_context_);
+  } else {
+    const std::string* guid = object.FindStringKey(kDeviceIdKey);
+    auto it = ephemeral_devices_.find(origin);
+    if (it != ephemeral_devices_.end()) {
+      it->second.erase(*guid);
+      if (it->second.empty())
+        ephemeral_devices_.erase(it);
+    }
+  }
+
+  api::Session* session = api::Session::FromBrowserContext(browser_context_);
+  if (session) {
+    v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
+    v8::HandleScope scope(isolate);
+    gin_helper::Dictionary details =
+        gin_helper::Dictionary::CreateEmpty(isolate);
+    details.Set("device", object.Clone());
+    details.Set("origin", origin.Serialize());
+    session->Emit("usb-device-revoked", details);
+  }
+}
+
+void UsbChooserContext::GrantDevicePermission(
+    const url::Origin& origin,
+    const device::mojom::UsbDeviceInfo& device_info) {
+  if (CanStorePersistentEntry(device_info)) {
+    auto* permission_manager = static_cast<ElectronPermissionManager*>(
+        browser_context_->GetPermissionControllerDelegate());
+    permission_manager->GrantDevicePermission(
+        static_cast<blink::PermissionType>(
+            WebContentsPermissionHelper::PermissionType::USB),
+        origin, DeviceInfoToValue(device_info), browser_context_);
+  } else {
+    ephemeral_devices_[origin].insert(device_info.guid);
+  }
+}
+
+bool UsbChooserContext::HasDevicePermission(
+    const url::Origin& origin,
+    const device::mojom::UsbDeviceInfo& device_info) {
+  auto it = ephemeral_devices_.find(origin);
+  if (it != ephemeral_devices_.end() &&
+      base::Contains(it->second, device_info.guid)) {
+    return true;
+  }
+
+  auto* permission_manager = static_cast<ElectronPermissionManager*>(
+      browser_context_->GetPermissionControllerDelegate());
+
+  return permission_manager->CheckDevicePermission(
+      static_cast<blink::PermissionType>(
+          WebContentsPermissionHelper::PermissionType::USB),
+      origin, DeviceInfoToValue(device_info), browser_context_);
+}
+
+void UsbChooserContext::GetDevices(
+    device::mojom::UsbDeviceManager::GetDevicesCallback callback) {
+  if (!is_initialized_) {
+    EnsureConnectionWithDeviceManager();
+    pending_get_devices_requests_.push(std::move(callback));
+    return;
+  }
+
+  std::vector<device::mojom::UsbDeviceInfoPtr> device_list;
+  for (const auto& pair : devices_) {
+    device_list.push_back(pair.second->Clone());
+  }
+  base::SequencedTaskRunnerHandle::Get()->PostTask(
+      FROM_HERE, base::BindOnce(std::move(callback), std::move(device_list)));
+}
+
+void UsbChooserContext::GetDevice(
+    const std::string& guid,
+    base::span<const uint8_t> blocked_interface_classes,
+    mojo::PendingReceiver<device::mojom::UsbDevice> device_receiver,
+    mojo::PendingRemote<device::mojom::UsbDeviceClient> device_client) {
+  EnsureConnectionWithDeviceManager();
+  device_manager_->GetDevice(
+      guid,
+      std::vector<uint8_t>(blocked_interface_classes.begin(),
+                           blocked_interface_classes.end()),
+      std::move(device_receiver), std::move(device_client));
+}
+
+const device::mojom::UsbDeviceInfo* UsbChooserContext::GetDeviceInfo(
+    const std::string& guid) {
+  DCHECK(is_initialized_);
+  auto it = devices_.find(guid);
+  return it == devices_.end() ? nullptr : it->second.get();
+}
+
+void UsbChooserContext::AddObserver(DeviceObserver* observer) {
+  EnsureConnectionWithDeviceManager();
+  device_observer_list_.AddObserver(observer);
+}
+
+void UsbChooserContext::RemoveObserver(DeviceObserver* observer) {
+  device_observer_list_.RemoveObserver(observer);
+}
+
+base::WeakPtr<UsbChooserContext> UsbChooserContext::AsWeakPtr() {
+  return weak_factory_.GetWeakPtr();
+}
+
+void UsbChooserContext::OnDeviceAdded(
+    device::mojom::UsbDeviceInfoPtr device_info) {
+  DCHECK(device_info);
+  // Update the device list.
+  DCHECK(!base::Contains(devices_, device_info->guid));
+  if (!ShouldExposeDevice(*device_info))
+    return;
+  devices_.insert(std::make_pair(device_info->guid, device_info->Clone()));
+
+  // Notify all observers.
+  for (auto& observer : device_observer_list_)
+    observer.OnDeviceAdded(*device_info);
+}
+
+void UsbChooserContext::OnDeviceRemoved(
+    device::mojom::UsbDeviceInfoPtr device_info) {
+  DCHECK(device_info);
+
+  if (!ShouldExposeDevice(*device_info)) {
+    DCHECK(!base::Contains(devices_, device_info->guid));
+    return;
+  }
+
+  // Update the device list.
+  DCHECK(base::Contains(devices_, device_info->guid));
+  devices_.erase(device_info->guid);
+
+  // Notify all device observers.
+  for (auto& observer : device_observer_list_)
+    observer.OnDeviceRemoved(*device_info);
+
+  // If the device was persistent, return. Otherwise, notify all permission
+  // observers that its permissions were revoked.
+  if (device_info->serial_number &&
+      !device_info->serial_number.value().empty()) {
+    return;
+  }
+  for (auto& map_entry : ephemeral_devices_) {
+    map_entry.second.erase(device_info->guid);
+  }
+}
+
+void UsbChooserContext::OnDeviceManagerConnectionError() {
+  device_manager_.reset();
+  client_receiver_.reset();
+  devices_.clear();
+  is_initialized_ = false;
+
+  ephemeral_devices_.clear();
+
+  // Notify all device observers.
+  for (auto& observer : device_observer_list_)
+    observer.OnDeviceManagerConnectionError();
+}
+
+}  // namespace electron
diff --git a/shell/browser/usb/usb_chooser_context.h b/shell/browser/usb/usb_chooser_context.h
new file mode 100644
index 0000000000000..76c55b0dd16c2
--- /dev/null
+++ b/shell/browser/usb/usb_chooser_context.h
@@ -0,0 +1,122 @@
+// Copyright (c) 2022 Microsoft, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef ELECTRON_SHELL_BROWSER_USB_USB_CHOOSER_CONTEXT_H_
+#define ELECTRON_SHELL_BROWSER_USB_USB_CHOOSER_CONTEXT_H_
+
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "base/containers/queue.h"
+#include "base/observer_list.h"
+#include "base/values.h"
+#include "build/build_config.h"
+#include "components/keyed_service/core/keyed_service.h"
+#include "mojo/public/cpp/bindings/associated_receiver.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "services/device/public/mojom/usb_manager.mojom.h"
+#include "services/device/public/mojom/usb_manager_client.mojom.h"
+#include "shell/browser/electron_browser_context.h"
+#include "url/origin.h"
+
+namespace electron {
+
+class UsbChooserContext : public KeyedService,
+                          public device::mojom::UsbDeviceManagerClient {
+ public:
+  explicit UsbChooserContext(ElectronBrowserContext* context);
+
+  UsbChooserContext(const UsbChooserContext&) = delete;
+  UsbChooserContext& operator=(const UsbChooserContext&) = delete;
+
+  ~UsbChooserContext() override;
+
+  // This observer can be used to be notified of changes to USB devices that are
+  // connected.
+  class DeviceObserver : public base::CheckedObserver {
+   public:
+    virtual void OnDeviceAdded(const device::mojom::UsbDeviceInfo&);
+    virtual void OnDeviceRemoved(const device::mojom::UsbDeviceInfo&);
+    virtual void OnDeviceManagerConnectionError();
+
+    // Called when the BrowserContext is shutting down. Observers must remove
+    // themselves before returning.
+    virtual void OnBrowserContextShutdown() = 0;
+  };
+
+  static base::Value DeviceInfoToValue(
+      const device::mojom::UsbDeviceInfo& device_info);
+
+  // Grants |origin| access to the USB device.
+  void GrantDevicePermission(const url::Origin& origin,
+                             const device::mojom::UsbDeviceInfo& device_info);
+
+  // Checks if |origin| has access to a device with |device_info|.
+  bool HasDevicePermission(const url::Origin& origin,
+                           const device::mojom::UsbDeviceInfo& device_info);
+
+  // Revokes |origin| access to the USB device ordered by website.
+  void RevokeDevicePermissionWebInitiated(
+      const url::Origin& origin,
+      const device::mojom::UsbDeviceInfo& device);
+
+  void AddObserver(DeviceObserver* observer);
+  void RemoveObserver(DeviceObserver* observer);
+
+  // Forward UsbDeviceManager methods.
+  void GetDevices(device::mojom::UsbDeviceManager::GetDevicesCallback callback);
+  void GetDevice(
+      const std::string& guid,
+      base::span<const uint8_t> blocked_interface_classes,
+      mojo::PendingReceiver<device::mojom::UsbDevice> device_receiver,
+      mojo::PendingRemote<device::mojom::UsbDeviceClient> device_client);
+
+  // This method should only be called when you are sure that |devices_| has
+  // been initialized. It will return nullptr if the guid cannot be found.
+  const device::mojom::UsbDeviceInfo* GetDeviceInfo(const std::string& guid);
+
+  base::WeakPtr<UsbChooserContext> AsWeakPtr();
+
+  void InitDeviceList(std::vector<::device::mojom::UsbDeviceInfoPtr> devices);
+
+ private:
+  // device::mojom::UsbDeviceManagerClient implementation.
+  void OnDeviceAdded(device::mojom::UsbDeviceInfoPtr device_info) override;
+  void OnDeviceRemoved(device::mojom::UsbDeviceInfoPtr device_info) override;
+
+  void RevokeObjectPermissionInternal(const url::Origin& origin,
+                                      const base::Value& object,
+                                      bool revoked_by_website);
+
+  void OnDeviceManagerConnectionError();
+  void EnsureConnectionWithDeviceManager();
+  void SetUpDeviceManagerConnection();
+
+  bool is_initialized_ = false;
+  base::queue<device::mojom::UsbDeviceManager::GetDevicesCallback>
+      pending_get_devices_requests_;
+
+  std::map<url::Origin, std::set<std::string>> ephemeral_devices_;
+  std::map<std::string, device::mojom::UsbDeviceInfoPtr> devices_;
+
+  // Connection to |device_manager_instance_|.
+  mojo::Remote<device::mojom::UsbDeviceManager> device_manager_;
+  mojo::AssociatedReceiver<device::mojom::UsbDeviceManagerClient>
+      client_receiver_{this};
+  base::ObserverList<DeviceObserver> device_observer_list_;
+
+  ElectronBrowserContext* browser_context_;
+
+  base::WeakPtrFactory<UsbChooserContext> weak_factory_{this};
+};
+
+}  // namespace electron
+
+#endif  // ELECTRON_SHELL_BROWSER_USB_USB_CHOOSER_CONTEXT_H_
diff --git a/shell/browser/usb/usb_chooser_context_factory.cc b/shell/browser/usb/usb_chooser_context_factory.cc
new file mode 100644
index 0000000000000..28fa16f5d10f2
--- /dev/null
+++ b/shell/browser/usb/usb_chooser_context_factory.cc
@@ -0,0 +1,45 @@
+// Copyright (c) 2022 Microsoft, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#include "shell/browser/usb/usb_chooser_context_factory.h"
+
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+#include "shell/browser/electron_browser_context.h"
+#include "shell/browser/usb/usb_chooser_context.h"
+
+namespace electron {
+
+UsbChooserContextFactory::UsbChooserContextFactory()
+    : BrowserContextKeyedServiceFactory(
+          "UsbChooserContext",
+          BrowserContextDependencyManager::GetInstance()) {}
+
+UsbChooserContextFactory::~UsbChooserContextFactory() {}
+
+KeyedService* UsbChooserContextFactory::BuildServiceInstanceFor(
+    content::BrowserContext* context) const {
+  auto* browser_context =
+      static_cast<electron::ElectronBrowserContext*>(context);
+  return new UsbChooserContext(browser_context);
+}
+
+// static
+UsbChooserContextFactory* UsbChooserContextFactory::GetInstance() {
+  return base::Singleton<UsbChooserContextFactory>::get();
+}
+
+// static
+UsbChooserContext* UsbChooserContextFactory::GetForBrowserContext(
+    content::BrowserContext* context) {
+  return static_cast<UsbChooserContext*>(
+      GetInstance()->GetServiceForBrowserContext(context, /*create=*/true));
+}
+
+UsbChooserContext* UsbChooserContextFactory::GetForBrowserContextIfExists(
+    content::BrowserContext* context) {
+  return static_cast<UsbChooserContext*>(
+      GetInstance()->GetServiceForBrowserContext(context, /*create=*/false));
+}
+
+}  // namespace electron
diff --git a/shell/browser/usb/usb_chooser_context_factory.h b/shell/browser/usb/usb_chooser_context_factory.h
new file mode 100644
index 0000000000000..14e18df2e8311
--- /dev/null
+++ b/shell/browser/usb/usb_chooser_context_factory.h
@@ -0,0 +1,39 @@
+// Copyright (c) 2022 Microsoft, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef ELECTRON_SHELL_BROWSER_USB_USB_CHOOSER_CONTEXT_FACTORY_H_
+#define ELECTRON_SHELL_BROWSER_USB_USB_CHOOSER_CONTEXT_FACTORY_H_
+
+#include "base/memory/singleton.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+namespace electron {
+
+class UsbChooserContext;
+
+class UsbChooserContextFactory : public BrowserContextKeyedServiceFactory {
+ public:
+  static UsbChooserContext* GetForBrowserContext(
+      content::BrowserContext* context);
+  static UsbChooserContext* GetForBrowserContextIfExists(
+      content::BrowserContext* context);
+  static UsbChooserContextFactory* GetInstance();
+
+  UsbChooserContextFactory(const UsbChooserContextFactory&) = delete;
+  UsbChooserContextFactory& operator=(const UsbChooserContextFactory&) = delete;
+
+ private:
+  friend struct base::DefaultSingletonTraits<UsbChooserContextFactory>;
+
+  UsbChooserContextFactory();
+  ~UsbChooserContextFactory() override;
+
+  // BrowserContextKeyedServiceFactory methods:
+  KeyedService* BuildServiceInstanceFor(
+      content::BrowserContext* profile) const override;
+};
+
+}  // namespace electron
+
+#endif  // ELECTRON_SHELL_BROWSER_USB_USB_CHOOSER_CONTEXT_FACTORY_H_
diff --git a/shell/browser/usb/usb_chooser_controller.cc b/shell/browser/usb/usb_chooser_controller.cc
new file mode 100644
index 0000000000000..87e5457884a2c
--- /dev/null
+++ b/shell/browser/usb/usb_chooser_controller.cc
@@ -0,0 +1,165 @@
+// Copyright (c) 2022 Microsoft, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#include "shell/browser/usb/usb_chooser_controller.h"
+
+#include <stddef.h>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/strings/stringprintf.h"
+#include "base/strings/utf_string_conversions.h"
+#include "build/build_config.h"
+#include "components/strings/grit/components_strings.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/web_contents.h"
+#include "gin/data_object_builder.h"
+#include "services/device/public/cpp/usb/usb_utils.h"
+#include "services/device/public/mojom/usb_enumeration_options.mojom.h"
+#include "shell/browser/javascript_environment.h"
+#include "shell/browser/usb/usb_chooser_context_factory.h"
+#include "shell/common/gin_converters/callback_converter.h"
+#include "shell/common/gin_converters/content_converter.h"
+#include "shell/common/gin_converters/frame_converter.h"
+#include "shell/common/gin_converters/usb_device_info_converter.h"
+#include "shell/common/node_includes.h"
+#include "shell/common/process_util.h"
+#include "ui/base/l10n/l10n_util.h"
+#include "url/gurl.h"
+
+using content::RenderFrameHost;
+using content::WebContents;
+
+namespace electron {
+
+UsbChooserController::UsbChooserController(
+    RenderFrameHost* render_frame_host,
+    std::vector<device::mojom::UsbDeviceFilterPtr> device_filters,
+    blink::mojom::WebUsbService::GetPermissionCallback callback,
+    content::WebContents* web_contents,
+    base::WeakPtr<ElectronUsbDelegate> usb_delegate)
+    : WebContentsObserver(web_contents),
+      filters_(std::move(device_filters)),
+      callback_(std::move(callback)),
+      origin_(render_frame_host->GetMainFrame()->GetLastCommittedOrigin()),
+      usb_delegate_(usb_delegate),
+      render_frame_host_id_(render_frame_host->GetGlobalId()) {
+  chooser_context_ = UsbChooserContextFactory::GetForBrowserContext(
+                         web_contents->GetBrowserContext())
+                         ->AsWeakPtr();
+  DCHECK(chooser_context_);
+  chooser_context_->GetDevices(base::BindOnce(
+      &UsbChooserController::GotUsbDeviceList, weak_factory_.GetWeakPtr()));
+}
+
+UsbChooserController::~UsbChooserController() {
+  RunCallback(/*device=*/nullptr);
+}
+
+api::Session* UsbChooserController::GetSession() {
+  if (!web_contents()) {
+    return nullptr;
+  }
+  return api::Session::FromBrowserContext(web_contents()->GetBrowserContext());
+}
+
+void UsbChooserController::OnDeviceAdded(
+    const device::mojom::UsbDeviceInfo& device_info) {
+  if (DisplayDevice(device_info)) {
+    api::Session* session = GetSession();
+    if (session) {
+      session->Emit("usb-device-added", device_info.Clone(), web_contents());
+    }
+  }
+}
+
+void UsbChooserController::OnDeviceRemoved(
+    const device::mojom::UsbDeviceInfo& device_info) {
+  api::Session* session = GetSession();
+  if (session) {
+    session->Emit("usb-device-removed", device_info.Clone(), web_contents());
+  }
+}
+
+void UsbChooserController::OnDeviceChosen(gin::Arguments* args) {
+  std::string device_id;
+  if (!args->GetNext(&device_id) || device_id.empty()) {
+    RunCallback(/*device=*/nullptr);
+  } else {
+    auto* device_info = chooser_context_->GetDeviceInfo(device_id);
+    if (device_info) {
+      RunCallback(device_info->Clone());
+    } else {
+      v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
+      node::Environment* env = node::Environment::GetCurrent(isolate);
+      EmitWarning(env, "The device id " + device_id + " was not found.",
+                  "UnknownUsbDeviceId");
+      RunCallback(/*device=*/nullptr);
+    }
+  }
+}
+
+void UsbChooserController::OnBrowserContextShutdown() {
+  observation_.Reset();
+}
+
+// Get a list of devices that can be shown in the chooser bubble UI for
+// user to grant permsssion.
+void UsbChooserController::GotUsbDeviceList(
+    std::vector<::device::mojom::UsbDeviceInfoPtr> devices) {
+  // Listen to UsbChooserContext for OnDeviceAdded/Removed events after the
+  // enumeration.
+  if (chooser_context_)
+    observation_.Observe(chooser_context_.get());
+
+  bool prevent_default = false;
+  api::Session* session = GetSession();
+  if (session) {
+    auto* rfh = content::RenderFrameHost::FromID(render_frame_host_id_);
+    v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
+    v8::HandleScope scope(isolate);
+    v8::Local<v8::Object> details = gin::DataObjectBuilder(isolate)
+                                        .Set("deviceList", devices)
+                                        .Set("frame", rfh)
+                                        .Build();
+
+    prevent_default =
+        session->Emit("select-usb-device", details,
+                      base::AdaptCallbackForRepeating(
+                          base::BindOnce(&UsbChooserController::OnDeviceChosen,
+                                         weak_factory_.GetWeakPtr())));
+  }
+  if (!prevent_default) {
+    RunCallback(/*port=*/nullptr);
+  }
+}
+
+bool UsbChooserController::DisplayDevice(
+    const device::mojom::UsbDeviceInfo& device_info) const {
+  if (!device::UsbDeviceFilterMatchesAny(filters_, device_info))
+    return false;
+
+  return true;
+}
+
+void UsbChooserController::RenderFrameDeleted(
+    content::RenderFrameHost* render_frame_host) {
+  if (usb_delegate_) {
+    usb_delegate_->DeleteControllerForFrame(render_frame_host);
+  }
+}
+
+void UsbChooserController::RunCallback(
+    device::mojom::UsbDeviceInfoPtr device_info) {
+  if (callback_) {
+    if (!chooser_context_ || !device_info) {
+      std::move(callback_).Run(nullptr);
+    } else {
+      chooser_context_->GrantDevicePermission(origin_, *device_info);
+      std::move(callback_).Run(std::move(device_info));
+    }
+  }
+}
+
+}  // namespace electron
diff --git a/shell/browser/usb/usb_chooser_controller.h b/shell/browser/usb/usb_chooser_controller.h
new file mode 100644
index 0000000000000..4e71bc17c110f
--- /dev/null
+++ b/shell/browser/usb/usb_chooser_controller.h
@@ -0,0 +1,81 @@
+// Copyright (c) 2022 Microsoft, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef ELECTRON_SHELL_BROWSER_USB_USB_CHOOSER_CONTROLLER_H_
+#define ELECTRON_SHELL_BROWSER_USB_USB_CHOOSER_CONTROLLER_H_
+
+#include <string>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+#include "base/memory/raw_ptr.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/weak_ptr.h"
+#include "base/scoped_observation.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/browser/web_contents_observer.h"
+#include "services/device/public/mojom/usb_device.mojom.h"
+#include "shell/browser/api/electron_api_session.h"
+#include "shell/browser/usb/electron_usb_delegate.h"
+#include "shell/browser/usb/usb_chooser_context.h"
+#include "third_party/blink/public/mojom/usb/web_usb_service.mojom.h"
+#include "url/origin.h"
+
+namespace content {
+class RenderFrameHost;
+}
+
+namespace electron {
+
+// UsbChooserController creates a chooser for WebUSB.
+class UsbChooserController final : public UsbChooserContext::DeviceObserver,
+                                   public content::WebContentsObserver {
+ public:
+  UsbChooserController(
+      content::RenderFrameHost* render_frame_host,
+      std::vector<device::mojom::UsbDeviceFilterPtr> device_filters,
+      blink::mojom::WebUsbService::GetPermissionCallback callback,
+      content::WebContents* web_contents,
+      base::WeakPtr<ElectronUsbDelegate> usb_delegate);
+
+  UsbChooserController(const UsbChooserController&) = delete;
+  UsbChooserController& operator=(const UsbChooserController&) = delete;
+
+  ~UsbChooserController() override;
+
+  // UsbChooserContext::DeviceObserver implementation:
+  void OnDeviceAdded(const device::mojom::UsbDeviceInfo& device_info) override;
+  void OnDeviceRemoved(
+      const device::mojom::UsbDeviceInfo& device_info) override;
+  void OnBrowserContextShutdown() override;
+
+  // content::WebContentsObserver:
+  void RenderFrameDeleted(content::RenderFrameHost* render_frame_host) override;
+
+ private:
+  api::Session* GetSession();
+  void GotUsbDeviceList(std::vector<device::mojom::UsbDeviceInfoPtr> devices);
+  bool DisplayDevice(const device::mojom::UsbDeviceInfo& device) const;
+  void RunCallback(device::mojom::UsbDeviceInfoPtr device_info);
+  void OnDeviceChosen(gin::Arguments* args);
+
+  std::vector<device::mojom::UsbDeviceFilterPtr> filters_;
+  blink::mojom::WebUsbService::GetPermissionCallback callback_;
+  url::Origin origin_;
+
+  base::WeakPtr<UsbChooserContext> chooser_context_;
+  base::ScopedObservation<UsbChooserContext, UsbChooserContext::DeviceObserver>
+      observation_{this};
+
+  base::WeakPtr<ElectronUsbDelegate> usb_delegate_;
+
+  content::GlobalRenderFrameHostId render_frame_host_id_;
+
+  base::WeakPtrFactory<UsbChooserController> weak_factory_{this};
+};
+
+}  // namespace electron
+
+#endif  // ELECTRON_SHELL_BROWSER_USB_USB_CHOOSER_CONTROLLER_H_
diff --git a/shell/browser/web_contents_permission_helper.h b/shell/browser/web_contents_permission_helper.h
index 43a5ca63ec307..972675c19f9b7 100644
--- a/shell/browser/web_contents_permission_helper.h
+++ b/shell/browser/web_contents_permission_helper.h
@@ -29,7 +29,8 @@ class WebContentsPermissionHelper
     FULLSCREEN,
     OPEN_EXTERNAL,
     SERIAL,
-    HID
+    HID,
+    USB
   };
 
   // Asynchronous Requests
diff --git a/shell/common/electron_constants.cc b/shell/common/electron_constants.cc
index 3d6eda0e9eea8..763c2e18e5c5d 100644
--- a/shell/common/electron_constants.cc
+++ b/shell/common/electron_constants.cc
@@ -25,6 +25,10 @@ const char kSecureProtocolDescription[] =
     "The connection to this site is using a strong protocol version "
     "and cipher suite.";
 
+const char kDeviceVendorIdKey[] = "vendorId";
+const char kDeviceProductIdKey[] = "productId";
+const char kDeviceSerialNumberKey[] = "serialNumber";
+
 #if BUILDFLAG(ENABLE_RUN_AS_NODE)
 const char kRunAsNode[] = "ELECTRON_RUN_AS_NODE";
 #endif
diff --git a/shell/common/electron_constants.h b/shell/common/electron_constants.h
index 4218b3e47ffc7..6927411006a57 100644
--- a/shell/common/electron_constants.h
+++ b/shell/common/electron_constants.h
@@ -25,6 +25,11 @@ extern const char kValidCertificateDescription[];
 extern const char kSecureProtocol[];
 extern const char kSecureProtocolDescription[];
 
+// Keys for Device APIs
+extern const char kDeviceVendorIdKey[];
+extern const char kDeviceProductIdKey[];
+extern const char kDeviceSerialNumberKey[];
+
 #if BUILDFLAG(ENABLE_RUN_AS_NODE)
 extern const char kRunAsNode[];
 #endif
diff --git a/shell/common/gin_converters/content_converter.cc b/shell/common/gin_converters/content_converter.cc
index 71c2e9bb247b3..2c1bf9c52a573 100644
--- a/shell/common/gin_converters/content_converter.cc
+++ b/shell/common/gin_converters/content_converter.cc
@@ -209,6 +209,8 @@ v8::Local<v8::Value> Converter<blink::PermissionType>::ToV8(
       return StringToV8(isolate, "serial");
     case PermissionType::HID:
       return StringToV8(isolate, "hid");
+    case PermissionType::USB:
+      return StringToV8(isolate, "usb");
     default:
       return StringToV8(isolate, "unknown");
   }
diff --git a/shell/common/gin_converters/usb_device_info_converter.h b/shell/common/gin_converters/usb_device_info_converter.h
new file mode 100644
index 0000000000000..d8ffda006b2de
--- /dev/null
+++ b/shell/common/gin_converters/usb_device_info_converter.h
@@ -0,0 +1,27 @@
+// Copyright (c) 2022 Microsoft, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef ELECTRON_SHELL_COMMON_GIN_CONVERTERS_USB_DEVICE_INFO_CONVERTER_H_
+#define ELECTRON_SHELL_COMMON_GIN_CONVERTERS_USB_DEVICE_INFO_CONVERTER_H_
+
+#include "gin/converter.h"
+#include "services/device/public/mojom/usb_device.mojom.h"
+#include "shell/browser/usb/usb_chooser_context.h"
+#include "shell/common/gin_converters/value_converter.h"
+
+namespace gin {
+
+template <>
+struct Converter<device::mojom::UsbDeviceInfoPtr> {
+  static v8::Local<v8::Value> ToV8(
+      v8::Isolate* isolate,
+      const device::mojom::UsbDeviceInfoPtr& device) {
+    base::Value value = electron::UsbChooserContext::DeviceInfoToValue(*device);
+    return gin::ConvertToV8(isolate, value);
+  }
+};
+
+}  // namespace gin
+
+#endif  // ELECTRON_SHELL_COMMON_GIN_CONVERTERS_USB_DEVICE_INFO_CONVERTER_H_
diff --git a/spec/chromium-spec.ts b/spec/chromium-spec.ts
index 53281f660447d..dc8f8fc86a896 100644
--- a/spec/chromium-spec.ts
+++ b/spec/chromium-spec.ts
@@ -2857,3 +2857,164 @@ describe('navigator.hid', () => {
     }
   });
 });
+
+describe('navigator.usb', () => {
+  let w: BrowserWindow;
+  let server: http.Server;
+  let serverUrl: string;
+  before(async () => {
+    w = new BrowserWindow({
+      show: false
+    });
+    await w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
+    server = http.createServer((req, res) => {
+      res.setHeader('Content-Type', 'text/html');
+      res.end('<body>');
+    });
+    await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
+    serverUrl = `http://localhost:${(server.address() as any).port}`;
+  });
+
+  const requestDevices: any = () => {
+    return w.webContents.executeJavaScript(`
+      navigator.usb.requestDevice({filters: []}).then(device => device.toString()).catch(err => err.toString());
+    `, true);
+  };
+
+  const notFoundError = 'NotFoundError: Failed to execute \'requestDevice\' on \'USB\': No device selected.';
+
+  after(() => {
+    server.close();
+    closeAllWindows();
+  });
+  afterEach(() => {
+    session.defaultSession.setPermissionCheckHandler(null);
+    session.defaultSession.setDevicePermissionHandler(null);
+    session.defaultSession.removeAllListeners('select-usb-device');
+  });
+
+  it('does not return a device if select-usb-device event is not defined', async () => {
+    w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
+    const device = await requestDevices();
+    expect(device).to.equal(notFoundError);
+  });
+
+  it('does not return a device when permission denied', async () => {
+    let selectFired = false;
+    w.webContents.session.on('select-usb-device', (event, details, callback) => {
+      selectFired = true;
+      callback();
+    });
+    session.defaultSession.setPermissionCheckHandler(() => false);
+    const device = await requestDevices();
+    expect(selectFired).to.be.false();
+    expect(device).to.equal(notFoundError);
+  });
+
+  it('returns a device when select-usb-device event is defined', async () => {
+    let haveDevices = false;
+    let selectFired = false;
+    w.webContents.session.on('select-usb-device', (event, details, callback) => {
+      expect(details.frame).to.have.ownProperty('frameTreeNodeId').that.is.a('number');
+      selectFired = true;
+      if (details.deviceList.length > 0) {
+        haveDevices = true;
+        callback(details.deviceList[0].deviceId);
+      } else {
+        callback();
+      }
+    });
+    const device = await requestDevices();
+    expect(selectFired).to.be.true();
+    if (haveDevices) {
+      expect(device).to.contain('[object USBDevice]');
+    } else {
+      expect(device).to.equal(notFoundError);
+    }
+    if (haveDevices) {
+      // Verify that navigation will clear device permissions
+      const grantedDevices = await w.webContents.executeJavaScript('navigator.usb.getDevices()');
+      expect(grantedDevices).to.not.be.empty();
+      w.loadURL(serverUrl);
+      const [,,,,, frameProcessId, frameRoutingId] = await emittedOnce(w.webContents, 'did-frame-navigate');
+      const frame = webFrameMain.fromId(frameProcessId, frameRoutingId);
+      expect(frame).to.not.be.empty();
+      if (frame) {
+        const grantedDevicesOnNewPage = await frame.executeJavaScript('navigator.usb.getDevices()');
+        expect(grantedDevicesOnNewPage).to.be.empty();
+      }
+    }
+  });
+
+  it('returns a device when DevicePermissionHandler is defined', async () => {
+    let haveDevices = false;
+    let selectFired = false;
+    let gotDevicePerms = false;
+    w.webContents.session.on('select-usb-device', (event, details, callback) => {
+      selectFired = true;
+      if (details.deviceList.length > 0) {
+        const foundDevice = details.deviceList.find((device) => {
+          if (device.productName && device.productName !== '' && device.serialNumber && device.serialNumber !== '') {
+            haveDevices = true;
+            return true;
+          }
+        });
+        if (foundDevice) {
+          callback(foundDevice.deviceId);
+          return;
+        }
+      }
+      callback();
+    });
+    session.defaultSession.setDevicePermissionHandler(() => {
+      gotDevicePerms = true;
+      return true;
+    });
+    await w.webContents.executeJavaScript('navigator.usb.getDevices();', true);
+    const device = await requestDevices();
+    expect(selectFired).to.be.true();
+    if (haveDevices) {
+      expect(device).to.contain('[object USBDevice]');
+      expect(gotDevicePerms).to.be.true();
+    } else {
+      expect(device).to.equal(notFoundError);
+    }
+  });
+
+  it('supports device.forget()', async () => {
+    let deletedDeviceFromEvent;
+    let haveDevices = false;
+    w.webContents.session.on('select-usb-device', (event, details, callback) => {
+      if (details.deviceList.length > 0) {
+        haveDevices = true;
+        callback(details.deviceList[0].deviceId);
+      } else {
+        callback();
+      }
+    });
+    w.webContents.session.on('usb-device-revoked', (event, details) => {
+      deletedDeviceFromEvent = details.device;
+    });
+    await requestDevices();
+    if (haveDevices) {
+      const grantedDevices = await w.webContents.executeJavaScript('navigator.usb.getDevices()');
+      if (grantedDevices.length > 0) {
+        const deletedDevice = await w.webContents.executeJavaScript(`
+          navigator.usb.getDevices().then(devices => {
+            devices[0].forget();
+            return {
+              vendorId: devices[0].vendorId,
+              productId: devices[0].productId,
+              productName: devices[0].productName
+            }
+          })
+        `);
+        const grantedDevices2 = await w.webContents.executeJavaScript('navigator.usb.getDevices()');
+        expect(grantedDevices2.length).to.be.lessThan(grantedDevices.length);
+        if (deletedDevice.name !== '' && deletedDevice.productId && deletedDevice.vendorId) {
+          expect(deletedDeviceFromEvent).to.include(deletedDevice);
+        }
+      }
+    }
+  });
+});


Test patch: