mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-27 12:36:14 +01:00
Feature/use hcaptcha if bot (#430)
* Handle hcaptch required identity response * Refactor iframe component for captcha and webauthn * Send captcha token to server * Add captcha callback * Clear captcha state * Remove captcha storage * linter fixes * Rename iframe components to include IFrame * Remove callback in favor of extenting submit * Limit publickey credentials access * Use captcha bypass token to bypass captcha for twofactor auth flows * Linter fixes * Set iframe version in components
This commit is contained in:
parent
00acbce556
commit
1006f50ef3
@ -19,6 +19,7 @@ import { StorageService } from 'jslib-common/abstractions/storage.service';
|
|||||||
|
|
||||||
import { ConstantsService } from 'jslib-common/services/constants.service';
|
import { ConstantsService } from 'jslib-common/services/constants.service';
|
||||||
|
|
||||||
|
import { CaptchaIFrame } from 'jslib-common/misc/captcha_iframe';
|
||||||
import { Utils } from 'jslib-common/misc/utils';
|
import { Utils } from 'jslib-common/misc/utils';
|
||||||
|
|
||||||
const Keys = {
|
const Keys = {
|
||||||
@ -33,6 +34,9 @@ export class LoginComponent implements OnInit {
|
|||||||
|
|
||||||
masterPassword: string = '';
|
masterPassword: string = '';
|
||||||
showPassword: boolean = false;
|
showPassword: boolean = false;
|
||||||
|
captchaSiteKey: string = null;
|
||||||
|
captchaToken: string = null;
|
||||||
|
captcha: CaptchaIFrame;
|
||||||
formPromise: Promise<AuthResult>;
|
formPromise: Promise<AuthResult>;
|
||||||
onSuccessfulLogin: () => Promise<any>;
|
onSuccessfulLogin: () => Promise<any>;
|
||||||
onSuccessfulLoginNavigate: () => Promise<any>;
|
onSuccessfulLoginNavigate: () => Promise<any>;
|
||||||
@ -61,6 +65,20 @@ export class LoginComponent implements OnInit {
|
|||||||
if (Utils.isBrowser && !Utils.isNode) {
|
if (Utils.isBrowser && !Utils.isNode) {
|
||||||
this.focusInput();
|
this.focusInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let webVaultUrl = this.environmentService.getWebVaultUrl();
|
||||||
|
if (webVaultUrl == null) {
|
||||||
|
webVaultUrl = 'https://vault.bitwarden.com';
|
||||||
|
}
|
||||||
|
this.captcha = new CaptchaIFrame(window, webVaultUrl,
|
||||||
|
this.i18nService, (token: string) => {
|
||||||
|
this.captchaToken = token;
|
||||||
|
}, (error: string) => {
|
||||||
|
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), error);
|
||||||
|
}, (info: string) => {
|
||||||
|
this.platformUtilsService.showToast('info', this.i18nService.t('info'), info);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit() {
|
async submit() {
|
||||||
@ -81,7 +99,7 @@ export class LoginComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.formPromise = this.authService.logIn(this.email, this.masterPassword);
|
this.formPromise = this.authService.logIn(this.email, this.masterPassword, this.captchaToken);
|
||||||
const response = await this.formPromise;
|
const response = await this.formPromise;
|
||||||
await this.storageService.save(Keys.rememberEmail, this.rememberEmail);
|
await this.storageService.save(Keys.rememberEmail, this.rememberEmail);
|
||||||
if (this.rememberEmail) {
|
if (this.rememberEmail) {
|
||||||
@ -89,7 +107,10 @@ export class LoginComponent implements OnInit {
|
|||||||
} else {
|
} else {
|
||||||
await this.storageService.remove(Keys.rememberedEmail);
|
await this.storageService.remove(Keys.rememberedEmail);
|
||||||
}
|
}
|
||||||
if (response.twoFactor) {
|
if (!Utils.isNullOrWhitespace(response.captchaSiteKey)) {
|
||||||
|
this.captchaSiteKey = response.captchaSiteKey;
|
||||||
|
this.captcha.init(response.captchaSiteKey);
|
||||||
|
} else if (response.twoFactor) {
|
||||||
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
||||||
this.onSuccessfulLoginTwoFactorNavigate();
|
this.onSuccessfulLoginTwoFactorNavigate();
|
||||||
} else {
|
} else {
|
||||||
@ -144,6 +165,9 @@ export class LoginComponent implements OnInit {
|
|||||||
'&state=' + state + '&codeChallenge=' + codeChallenge);
|
'&state=' + state + '&codeChallenge=' + codeChallenge);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showCaptcha() {
|
||||||
|
return !Utils.isNullOrWhitespace(this.captchaSiteKey);
|
||||||
|
}
|
||||||
protected focusInput() {
|
protected focusInput() {
|
||||||
document.getElementById(this.email == null || this.email === '' ? 'email' : 'masterPassword').focus();
|
document.getElementById(this.email == null || this.email === '' ? 'email' : 'masterPassword').focus();
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ import { TwoFactorProviders } from 'jslib-common/services/auth.service';
|
|||||||
import { ConstantsService } from 'jslib-common/services/constants.service';
|
import { ConstantsService } from 'jslib-common/services/constants.service';
|
||||||
|
|
||||||
import * as DuoWebSDK from 'duo_web_sdk';
|
import * as DuoWebSDK from 'duo_web_sdk';
|
||||||
import { WebAuthn } from 'jslib-common/misc/webauthn';
|
import { WebAuthnIFrame } from 'jslib-common/misc/webauthn_iframe';
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export class TwoFactorComponent implements OnInit, OnDestroy {
|
export class TwoFactorComponent implements OnInit, OnDestroy {
|
||||||
@ -35,7 +35,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
|
|||||||
providerType = TwoFactorProviderType;
|
providerType = TwoFactorProviderType;
|
||||||
selectedProviderType: TwoFactorProviderType = TwoFactorProviderType.Authenticator;
|
selectedProviderType: TwoFactorProviderType = TwoFactorProviderType.Authenticator;
|
||||||
webAuthnSupported: boolean = false;
|
webAuthnSupported: boolean = false;
|
||||||
webAuthn: WebAuthn = null;
|
webAuthn: WebAuthnIFrame = null;
|
||||||
title: string = '';
|
title: string = '';
|
||||||
twoFactorEmail: string = null;
|
twoFactorEmail: string = null;
|
||||||
formPromise: Promise<any>;
|
formPromise: Promise<any>;
|
||||||
@ -80,7 +80,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
|
|||||||
if (webVaultUrl == null) {
|
if (webVaultUrl == null) {
|
||||||
webVaultUrl = 'https://vault.bitwarden.com';
|
webVaultUrl = 'https://vault.bitwarden.com';
|
||||||
}
|
}
|
||||||
this.webAuthn = new WebAuthn(this.win, webVaultUrl, this.webAuthnNewTab, this.platformUtilsService,
|
this.webAuthn = new WebAuthnIFrame(this.win, webVaultUrl, this.webAuthnNewTab, this.platformUtilsService,
|
||||||
this.i18nService, (token: string) => {
|
this.i18nService, (token: string) => {
|
||||||
this.token = token;
|
this.token = token;
|
||||||
this.submit();
|
this.submit();
|
||||||
|
@ -108,6 +108,7 @@ import {
|
|||||||
GroupDetailsResponse,
|
GroupDetailsResponse,
|
||||||
GroupResponse,
|
GroupResponse,
|
||||||
} from '../models/response/groupResponse';
|
} from '../models/response/groupResponse';
|
||||||
|
import { IdentityCaptchaResponse } from '../models/response/identityCaptchaResponse';
|
||||||
import { IdentityTokenResponse } from '../models/response/identityTokenResponse';
|
import { IdentityTokenResponse } from '../models/response/identityTokenResponse';
|
||||||
import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse';
|
import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse';
|
||||||
import { ListResponse } from '../models/response/listResponse';
|
import { ListResponse } from '../models/response/listResponse';
|
||||||
@ -158,7 +159,7 @@ export abstract class ApiService {
|
|||||||
eventsBaseUrl: string;
|
eventsBaseUrl: string;
|
||||||
|
|
||||||
setUrls: (urls: EnvironmentUrls) => void;
|
setUrls: (urls: EnvironmentUrls) => void;
|
||||||
postIdentityToken: (request: TokenRequest) => Promise<IdentityTokenResponse | IdentityTwoFactorResponse>;
|
postIdentityToken: (request: TokenRequest) => Promise<IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse>;
|
||||||
refreshIdentityToken: () => Promise<any>;
|
refreshIdentityToken: () => Promise<any>;
|
||||||
|
|
||||||
getProfile: () => Promise<ProfileResponse>;
|
getProfile: () => Promise<ProfileResponse>;
|
||||||
|
@ -14,7 +14,7 @@ export abstract class AuthService {
|
|||||||
twoFactorProvidersData: Map<TwoFactorProviderType, { [key: string]: string; }>;
|
twoFactorProvidersData: Map<TwoFactorProviderType, { [key: string]: string; }>;
|
||||||
selectedTwoFactorProviderType: TwoFactorProviderType;
|
selectedTwoFactorProviderType: TwoFactorProviderType;
|
||||||
|
|
||||||
logIn: (email: string, masterPassword: string) => Promise<AuthResult>;
|
logIn: (email: string, masterPassword: string, captchaToken?: string) => Promise<AuthResult>;
|
||||||
logInSso: (code: string, codeVerifier: string, redirectUrl: string) => Promise<AuthResult>;
|
logInSso: (code: string, codeVerifier: string, redirectUrl: string) => Promise<AuthResult>;
|
||||||
logInApiKey: (clientId: string, clientSecret: string) => Promise<AuthResult>;
|
logInApiKey: (clientId: string, clientSecret: string) => Promise<AuthResult>;
|
||||||
logInTwoFactor: (twoFactorProvider: TwoFactorProviderType, twoFactorToken: string,
|
logInTwoFactor: (twoFactorProvider: TwoFactorProviderType, twoFactorToken: string,
|
||||||
|
22
common/src/misc/captcha_iframe.ts
Normal file
22
common/src/misc/captcha_iframe.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { I18nService } from '../abstractions/i18n.service';
|
||||||
|
import { IFrameComponent } from './iframe_component';
|
||||||
|
|
||||||
|
export class CaptchaIFrame extends IFrameComponent {
|
||||||
|
constructor(win: Window, webVaultUrl: string,
|
||||||
|
private i18nService: I18nService, successCallback: (message: string) => any, errorCallback: (message: string) => any,
|
||||||
|
infoCallback: (message: string) => any) {
|
||||||
|
super(win, webVaultUrl, 'captcha-connector.html', 'hcaptcha_iframe', successCallback, errorCallback, (message: string) => {
|
||||||
|
const parsedMessage = JSON.parse(message);
|
||||||
|
if (typeof (parsedMessage) !== 'string') {
|
||||||
|
this.iframe.height = (parsedMessage.height).toString();
|
||||||
|
this.iframe.width = (parsedMessage.width).toString();
|
||||||
|
} else {
|
||||||
|
infoCallback(parsedMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
init(siteKey: string): void {
|
||||||
|
super.initComponent(this.createParams({ siteKey: siteKey, locale: this.i18nService.translationLocale }, 1));
|
||||||
|
}
|
||||||
|
}
|
@ -1,39 +1,16 @@
|
|||||||
import { I18nService } from '../abstractions/i18n.service';
|
import { I18nService } from '../abstractions/i18n.service';
|
||||||
import { PlatformUtilsService } from '../abstractions/platformUtils.service';
|
|
||||||
|
|
||||||
export class WebAuthn {
|
export abstract class IFrameComponent {
|
||||||
private iframe: HTMLIFrameElement = null;
|
iframe: HTMLIFrameElement;
|
||||||
private connectorLink: HTMLAnchorElement;
|
private connectorLink: HTMLAnchorElement;
|
||||||
private parseFunction = this.parseMessage.bind(this);
|
private parseFunction = this.parseMessage.bind(this);
|
||||||
|
|
||||||
constructor(private win: Window, private webVaultUrl: string, private webAuthnNewTab: boolean,
|
constructor(private win: Window, protected webVaultUrl: string, private path: string, private iframeId: string,
|
||||||
private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
|
public successCallback?: (message: string) => any,
|
||||||
private successCallback: Function, private errorCallback: Function, private infoCallback: Function) {
|
public errorCallback?: (message: string) => any, public infoCallback?: (message: string) => any) {
|
||||||
this.connectorLink = win.document.createElement('a');
|
this.connectorLink = win.document.createElement('a');
|
||||||
}
|
}
|
||||||
|
|
||||||
init(data: any): void {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
data: this.base64Encode(JSON.stringify(data)),
|
|
||||||
parent: encodeURIComponent(this.win.document.location.href),
|
|
||||||
btnText: encodeURIComponent(this.i18nService.t('webAuthnAuthenticate')),
|
|
||||||
v: '1',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.webAuthnNewTab) {
|
|
||||||
// Firefox fallback which opens the webauthn page in a new tab
|
|
||||||
params.append('locale', this.i18nService.translationLocale);
|
|
||||||
this.platformUtilsService.launchUri(`${this.webVaultUrl}/webauthn-fallback-connector.html?${params}`);
|
|
||||||
} else {
|
|
||||||
this.connectorLink.href = `${this.webVaultUrl}/webauthn-connector.html?${params}`;
|
|
||||||
this.iframe = this.win.document.getElementById('webauthn_iframe') as HTMLIFrameElement;
|
|
||||||
this.iframe.allow = 'publickey-credentials-get ' + new URL(this.webVaultUrl).origin;
|
|
||||||
this.iframe.src = this.connectorLink.href;
|
|
||||||
|
|
||||||
this.win.addEventListener('message', this.parseFunction, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
this.sendMessage('stop');
|
this.sendMessage('stop');
|
||||||
}
|
}
|
||||||
@ -60,6 +37,22 @@ export class WebAuthn {
|
|||||||
this.win.removeEventListener('message', this.parseFunction, false);
|
this.win.removeEventListener('message', this.parseFunction, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected createParams(data: any, version: number) {
|
||||||
|
return new URLSearchParams({
|
||||||
|
data: this.base64Encode(JSON.stringify(data)),
|
||||||
|
parent: encodeURIComponent(this.win.document.location.href),
|
||||||
|
v: version.toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected initComponent(params: URLSearchParams): void {
|
||||||
|
this.connectorLink.href = `${this.webVaultUrl}/${this.path}?${params}`;
|
||||||
|
this.iframe = this.win.document.getElementById(this.iframeId) as HTMLIFrameElement;
|
||||||
|
this.iframe.src = this.connectorLink.href;
|
||||||
|
|
||||||
|
this.win.addEventListener('message', this.parseFunction, false);
|
||||||
|
}
|
||||||
|
|
||||||
private parseMessage(event: MessageEvent) {
|
private parseMessage(event: MessageEvent) {
|
||||||
if (!this.validMessage(event)) {
|
if (!this.validMessage(event)) {
|
||||||
return;
|
return;
|
26
common/src/misc/webauthn_iframe.ts
Normal file
26
common/src/misc/webauthn_iframe.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { I18nService } from '../abstractions/i18n.service';
|
||||||
|
import { PlatformUtilsService } from '../abstractions/platformUtils.service';
|
||||||
|
import { IFrameComponent } from './iframe_component';
|
||||||
|
|
||||||
|
export class WebAuthnIFrame extends IFrameComponent {
|
||||||
|
constructor(win: Window, webVaultUrl: string, private webAuthnNewTab: boolean,
|
||||||
|
private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
|
||||||
|
successCallback: (message: string) => any, errorCallback: (message: string) => any,
|
||||||
|
infoCallback: (message: string) => any) {
|
||||||
|
super(win, webVaultUrl, 'webauthn-connector.html', 'webauthn_iframe', successCallback, errorCallback, infoCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
init(data: any): void {
|
||||||
|
const params = this.createParams({ data: JSON.stringify(data), btnText: this.i18nService.t('webAuthnAuthenticate') }, 2);
|
||||||
|
|
||||||
|
if (this.webAuthnNewTab) {
|
||||||
|
// Firefox fallback which opens the webauthn page in a new tab
|
||||||
|
params.append('locale', this.i18nService.translationLocale);
|
||||||
|
this.platformUtilsService.launchUri(`${this.webVaultUrl}/webauthn-fallback-connector.html?${params}`);
|
||||||
|
} else {
|
||||||
|
super.initComponent(params);
|
||||||
|
this.iframe.allow = 'publickey-credentials-get ' + new URL(this.webVaultUrl).origin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ import { TwoFactorProviderType } from '../../enums/twoFactorProviderType';
|
|||||||
|
|
||||||
export class AuthResult {
|
export class AuthResult {
|
||||||
twoFactor: boolean = false;
|
twoFactor: boolean = false;
|
||||||
|
captchaSiteKey: string = '';
|
||||||
resetMasterPassword: boolean = false;
|
resetMasterPassword: boolean = false;
|
||||||
twoFactorProviders: Map<TwoFactorProviderType, { [key: string]: string; }> = null;
|
twoFactorProviders: Map<TwoFactorProviderType, { [key: string]: string; }> = null;
|
||||||
}
|
}
|
||||||
|
@ -13,10 +13,11 @@ export class TokenRequest {
|
|||||||
token: string;
|
token: string;
|
||||||
provider: TwoFactorProviderType;
|
provider: TwoFactorProviderType;
|
||||||
remember: boolean;
|
remember: boolean;
|
||||||
|
captchaToken: string;
|
||||||
device?: DeviceRequest;
|
device?: DeviceRequest;
|
||||||
|
|
||||||
constructor(credentials: string[], codes: string[], clientIdClientSecret: string[], provider: TwoFactorProviderType,
|
constructor(credentials: string[], codes: string[], clientIdClientSecret: string[], provider: TwoFactorProviderType,
|
||||||
token: string, remember: boolean, device?: DeviceRequest) {
|
token: string, remember: boolean, captchaToken: string, device?: DeviceRequest) {
|
||||||
if (credentials != null && credentials.length > 1) {
|
if (credentials != null && credentials.length > 1) {
|
||||||
this.email = credentials[0];
|
this.email = credentials[0];
|
||||||
this.masterPasswordHash = credentials[1];
|
this.masterPasswordHash = credentials[1];
|
||||||
@ -32,6 +33,7 @@ export class TokenRequest {
|
|||||||
this.provider = provider;
|
this.provider = provider;
|
||||||
this.remember = remember;
|
this.remember = remember;
|
||||||
this.device = device != null ? device : null;
|
this.device = device != null ? device : null;
|
||||||
|
this.captchaToken = captchaToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
toIdentityToken(clientId: string) {
|
toIdentityToken(clientId: string) {
|
||||||
@ -71,6 +73,11 @@ export class TokenRequest {
|
|||||||
obj.twoFactorRemember = this.remember ? '1' : '0';
|
obj.twoFactorRemember = this.remember ? '1' : '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.captchaToken != null) {
|
||||||
|
obj.captchaResponse = this.captchaToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
10
common/src/models/response/identityCaptchaResponse.ts
Normal file
10
common/src/models/response/identityCaptchaResponse.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { BaseResponse } from './baseResponse';
|
||||||
|
|
||||||
|
export class IdentityCaptchaResponse extends BaseResponse {
|
||||||
|
siteKey: string;
|
||||||
|
|
||||||
|
constructor(response: any) {
|
||||||
|
super(response);
|
||||||
|
this.siteKey = this.getResponseProperty('HCaptcha_SiteKey');
|
||||||
|
}
|
||||||
|
}
|
@ -5,9 +5,11 @@ import { TwoFactorProviderType } from '../../enums/twoFactorProviderType';
|
|||||||
export class IdentityTwoFactorResponse extends BaseResponse {
|
export class IdentityTwoFactorResponse extends BaseResponse {
|
||||||
twoFactorProviders: TwoFactorProviderType[];
|
twoFactorProviders: TwoFactorProviderType[];
|
||||||
twoFactorProviders2 = new Map<TwoFactorProviderType, { [key: string]: string; }>();
|
twoFactorProviders2 = new Map<TwoFactorProviderType, { [key: string]: string; }>();
|
||||||
|
captchaToken: string;
|
||||||
|
|
||||||
constructor(response: any) {
|
constructor(response: any) {
|
||||||
super(response);
|
super(response);
|
||||||
|
this.captchaToken = this.getResponseProperty('HCaptcha_BypassKey');
|
||||||
this.twoFactorProviders = this.getResponseProperty('TwoFactorProviders');
|
this.twoFactorProviders = this.getResponseProperty('TwoFactorProviders');
|
||||||
const twoFactorProviders2 = this.getResponseProperty('TwoFactorProviders2');
|
const twoFactorProviders2 = this.getResponseProperty('TwoFactorProviders2');
|
||||||
if (twoFactorProviders2 != null) {
|
if (twoFactorProviders2 != null) {
|
||||||
|
@ -160,6 +160,7 @@ import { ChallengeResponse } from '../models/response/twoFactorWebAuthnResponse'
|
|||||||
import { TwoFactorYubiKeyResponse } from '../models/response/twoFactorYubiKeyResponse';
|
import { TwoFactorYubiKeyResponse } from '../models/response/twoFactorYubiKeyResponse';
|
||||||
import { UserKeyResponse } from '../models/response/userKeyResponse';
|
import { UserKeyResponse } from '../models/response/userKeyResponse';
|
||||||
|
|
||||||
|
import { IdentityCaptchaResponse } from '../models/response/identityCaptchaResponse';
|
||||||
import { SendAccessView } from '../models/view/sendAccessView';
|
import { SendAccessView } from '../models/view/sendAccessView';
|
||||||
|
|
||||||
export class ApiService implements ApiServiceAbstraction {
|
export class ApiService implements ApiServiceAbstraction {
|
||||||
@ -215,7 +216,7 @@ export class ApiService implements ApiServiceAbstraction {
|
|||||||
|
|
||||||
// Auth APIs
|
// Auth APIs
|
||||||
|
|
||||||
async postIdentityToken(request: TokenRequest): Promise<IdentityTokenResponse | IdentityTwoFactorResponse> {
|
async postIdentityToken(request: TokenRequest): Promise<IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse> {
|
||||||
const headers = new Headers({
|
const headers = new Headers({
|
||||||
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
|
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
@ -245,6 +246,9 @@ export class ApiService implements ApiServiceAbstraction {
|
|||||||
Object.keys(responseJson.TwoFactorProviders2).length) {
|
Object.keys(responseJson.TwoFactorProviders2).length) {
|
||||||
await this.tokenService.clearTwoFactorToken(request.email);
|
await this.tokenService.clearTwoFactorToken(request.email);
|
||||||
return new IdentityTwoFactorResponse(responseJson);
|
return new IdentityTwoFactorResponse(responseJson);
|
||||||
|
} else if (response.status === 400 && responseJson.HCaptcha_SiteKey &&
|
||||||
|
Object.keys(responseJson.HCaptcha_SiteKey).length) {
|
||||||
|
return new IdentityCaptchaResponse(responseJson);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +87,7 @@ export class AuthService implements AuthServiceAbstraction {
|
|||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
twoFactorProvidersData: Map<TwoFactorProviderType, { [key: string]: string; }>;
|
twoFactorProvidersData: Map<TwoFactorProviderType, { [key: string]: string; }>;
|
||||||
selectedTwoFactorProviderType: TwoFactorProviderType = null;
|
selectedTwoFactorProviderType: TwoFactorProviderType = null;
|
||||||
|
captchaToken: string;
|
||||||
|
|
||||||
private key: SymmetricCryptoKey;
|
private key: SymmetricCryptoKey;
|
||||||
|
|
||||||
@ -120,14 +121,14 @@ export class AuthService implements AuthServiceAbstraction {
|
|||||||
TwoFactorProviders[TwoFactorProviderType.Yubikey].description = this.i18nService.t('yubiKeyDesc');
|
TwoFactorProviders[TwoFactorProviderType.Yubikey].description = this.i18nService.t('yubiKeyDesc');
|
||||||
}
|
}
|
||||||
|
|
||||||
async logIn(email: string, masterPassword: string): Promise<AuthResult> {
|
async logIn(email: string, masterPassword: string, captchaToken?: string): Promise<AuthResult> {
|
||||||
this.selectedTwoFactorProviderType = null;
|
this.selectedTwoFactorProviderType = null;
|
||||||
const key = await this.makePreloginKey(masterPassword, email);
|
const key = await this.makePreloginKey(masterPassword, email);
|
||||||
const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key);
|
const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key);
|
||||||
const localHashedPassword = await this.cryptoService.hashPassword(masterPassword, key,
|
const localHashedPassword = await this.cryptoService.hashPassword(masterPassword, key,
|
||||||
HashPurpose.LocalAuthorization);
|
HashPurpose.LocalAuthorization);
|
||||||
return await this.logInHelper(email, hashedPassword, localHashedPassword, null, null, null, null, null,
|
return await this.logInHelper(email, hashedPassword, localHashedPassword, null, null, null, null, null,
|
||||||
key, null, null, null);
|
key, null, null, null, captchaToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
async logInSso(code: string, codeVerifier: string, redirectUrl: string): Promise<AuthResult> {
|
async logInSso(code: string, codeVerifier: string, redirectUrl: string): Promise<AuthResult> {
|
||||||
@ -146,7 +147,7 @@ export class AuthService implements AuthServiceAbstraction {
|
|||||||
remember?: boolean): Promise<AuthResult> {
|
remember?: boolean): Promise<AuthResult> {
|
||||||
return await this.logInHelper(this.email, this.masterPasswordHash, this.localMasterPasswordHash, this.code,
|
return await this.logInHelper(this.email, this.masterPasswordHash, this.localMasterPasswordHash, this.code,
|
||||||
this.codeVerifier, this.ssoRedirectUrl, this.clientId, this.clientSecret, this.key, twoFactorProvider,
|
this.codeVerifier, this.ssoRedirectUrl, this.clientId, this.clientSecret, this.key, twoFactorProvider,
|
||||||
twoFactorToken, remember);
|
twoFactorToken, remember, this.captchaToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
async logInComplete(email: string, masterPassword: string, twoFactorProvider: TwoFactorProviderType,
|
async logInComplete(email: string, masterPassword: string, twoFactorProvider: TwoFactorProviderType,
|
||||||
@ -272,7 +273,7 @@ export class AuthService implements AuthServiceAbstraction {
|
|||||||
|
|
||||||
private async logInHelper(email: string, hashedPassword: string, localHashedPassword: string, code: string,
|
private async logInHelper(email: string, hashedPassword: string, localHashedPassword: string, code: string,
|
||||||
codeVerifier: string, redirectUrl: string, clientId: string, clientSecret: string, key: SymmetricCryptoKey,
|
codeVerifier: string, redirectUrl: string, clientId: string, clientSecret: string, key: SymmetricCryptoKey,
|
||||||
twoFactorProvider?: TwoFactorProviderType, twoFactorToken?: string, remember?: boolean): Promise<AuthResult> {
|
twoFactorProvider?: TwoFactorProviderType, twoFactorToken?: string, remember?: boolean, captchaToken?: string): Promise<AuthResult> {
|
||||||
const storedTwoFactorToken = await this.tokenService.getTwoFactorToken(email);
|
const storedTwoFactorToken = await this.tokenService.getTwoFactorToken(email);
|
||||||
const appId = await this.appIdService.getAppId();
|
const appId = await this.appIdService.getAppId();
|
||||||
const deviceRequest = new DeviceRequest(appId, this.platformUtilsService);
|
const deviceRequest = new DeviceRequest(appId, this.platformUtilsService);
|
||||||
@ -300,24 +301,27 @@ export class AuthService implements AuthServiceAbstraction {
|
|||||||
let request: TokenRequest;
|
let request: TokenRequest;
|
||||||
if (twoFactorToken != null && twoFactorProvider != null) {
|
if (twoFactorToken != null && twoFactorProvider != null) {
|
||||||
request = new TokenRequest(emailPassword, codeCodeVerifier, clientIdClientSecret, twoFactorProvider,
|
request = new TokenRequest(emailPassword, codeCodeVerifier, clientIdClientSecret, twoFactorProvider,
|
||||||
twoFactorToken, remember, deviceRequest);
|
twoFactorToken, remember, captchaToken, deviceRequest);
|
||||||
} else if (storedTwoFactorToken != null) {
|
} else if (storedTwoFactorToken != null) {
|
||||||
request = new TokenRequest(emailPassword, codeCodeVerifier, clientIdClientSecret, TwoFactorProviderType.Remember,
|
request = new TokenRequest(emailPassword, codeCodeVerifier, clientIdClientSecret,
|
||||||
storedTwoFactorToken, false, deviceRequest);
|
TwoFactorProviderType.Remember, storedTwoFactorToken, false, captchaToken, deviceRequest);
|
||||||
} else {
|
} else {
|
||||||
request = new TokenRequest(emailPassword, codeCodeVerifier, clientIdClientSecret, null,
|
request = new TokenRequest(emailPassword, codeCodeVerifier, clientIdClientSecret, null,
|
||||||
null, false, deviceRequest);
|
null, false, captchaToken, deviceRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await this.apiService.postIdentityToken(request);
|
const response = await this.apiService.postIdentityToken(request);
|
||||||
|
|
||||||
this.clearState();
|
this.clearState();
|
||||||
const result = new AuthResult();
|
const result = new AuthResult();
|
||||||
result.twoFactor = !(response as any).accessToken;
|
result.captchaSiteKey = (response as any).siteKey;
|
||||||
|
if (!!result.captchaSiteKey) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
result.twoFactor = !!(response as any).twoFactorProviders2;
|
||||||
|
|
||||||
if (result.twoFactor) {
|
if (result.twoFactor) {
|
||||||
// two factor required
|
// two factor required
|
||||||
const twoFactorResponse = response as IdentityTwoFactorResponse;
|
|
||||||
this.email = email;
|
this.email = email;
|
||||||
this.masterPasswordHash = hashedPassword;
|
this.masterPasswordHash = hashedPassword;
|
||||||
this.localMasterPasswordHash = localHashedPassword;
|
this.localMasterPasswordHash = localHashedPassword;
|
||||||
@ -327,8 +331,10 @@ export class AuthService implements AuthServiceAbstraction {
|
|||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
this.clientSecret = clientSecret;
|
this.clientSecret = clientSecret;
|
||||||
this.key = this.setCryptoKeys ? key : null;
|
this.key = this.setCryptoKeys ? key : null;
|
||||||
|
const twoFactorResponse = response as IdentityTwoFactorResponse;
|
||||||
this.twoFactorProvidersData = twoFactorResponse.twoFactorProviders2;
|
this.twoFactorProvidersData = twoFactorResponse.twoFactorProviders2;
|
||||||
result.twoFactorProviders = twoFactorResponse.twoFactorProviders2;
|
result.twoFactorProviders = twoFactorResponse.twoFactorProviders2;
|
||||||
|
this.captchaToken = twoFactorResponse.captchaToken;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user