diff --git a/src/connectors/captcha.html b/src/connectors/captcha.html
new file mode 100644
index 00000000000..16aa5bdb63a
--- /dev/null
+++ b/src/connectors/captcha.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Bitwarden Captcha Connector
+
+
+
+
+
+
+
+
diff --git a/src/connectors/captcha.scss b/src/connectors/captcha.scss
new file mode 100644
index 00000000000..4a6ebdf9dc3
--- /dev/null
+++ b/src/connectors/captcha.scss
@@ -0,0 +1,6 @@
+body {
+ min-width: 0px !important;
+ padding: 0;
+ margin: 0;
+ background: transparent;
+}
diff --git a/src/connectors/captcha.ts b/src/connectors/captcha.ts
new file mode 100644
index 00000000000..810189fc420
--- /dev/null
+++ b/src/connectors/captcha.ts
@@ -0,0 +1,85 @@
+import { getQsParam } from './common';
+
+declare var hcaptcha: any;
+
+// tslint:disable-next-line
+require('./captcha.scss');
+
+document.addEventListener('DOMContentLoaded', () => {
+ init();
+});
+
+(window as any).captchaSuccess = captchaSuccess;
+(window as any).captchaError = captchaError;
+
+let parentUrl: string = null;
+let parentOrigin: string = null;
+let sentSuccess = false;
+
+function init() {
+ start();
+ onMessage();
+ info('ready');
+}
+
+function start() {
+ sentSuccess = false;
+
+ const data = getQsParam('data');
+ if (!data) {
+ error('No data.');
+ return;
+ }
+
+ parentUrl = getQsParam('parent');
+ if (!parentUrl) {
+ error('No parent.');
+ return;
+ } else {
+ parentUrl = decodeURIComponent(parentUrl);
+ parentOrigin = new URL(parentUrl).origin;
+ }
+
+ hcaptcha.render('captcha', {
+ sitekey: 'bc38c8a2-5311-4e8c-9dfc-49e99f6df417',
+ callback: 'captchaSuccess',
+ 'error-callback': 'captchaError',
+ });
+}
+
+function captchaSuccess(response: string) {
+ success(response);
+}
+
+function captchaError() {
+ error('An error occurred with the captcha. Try again.');
+}
+
+function onMessage() {
+ window.addEventListener('message', event => {
+ if (!event.origin || event.origin === '' || event.origin !== parentOrigin) {
+ return;
+ }
+
+ if (event.data === 'start') {
+ start();
+ }
+ }, false);
+}
+
+function error(message: string) {
+ parent.postMessage('error|' + message, parentUrl);
+}
+
+function success(data: string) {
+ if (sentSuccess) {
+ return;
+ }
+ parent.postMessage('success|' + data, parentUrl);
+ sentSuccess = true;
+}
+
+function info(message: string) {
+ parent.postMessage('info|' + message, parentUrl);
+}
+
diff --git a/webpack.config.js b/webpack.config.js
index 37a84a624f8..58a656d42aa 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -107,6 +107,11 @@ const plugins = [
filename: 'sso-connector.html',
chunks: ['connectors/sso'],
}),
+ new HtmlWebpackPlugin({
+ template: './src/connectors/captcha.html',
+ filename: 'captcha-connector.html',
+ chunks: ['connectors/captcha'],
+ }),
new CopyWebpackPlugin({
patterns:[
{ from: './src/.nojekyll' },
@@ -198,6 +203,7 @@ const webpackConfig = {
'connectors/webauthn-fallback': './src/connectors/webauthn-fallback.ts',
'connectors/duo': './src/connectors/duo.ts',
'connectors/sso': './src/connectors/sso.ts',
+ 'connectors/captcha': './src/connectors/captcha.ts',
},
externals: {
'u2f': 'u2f',