mirror of
https://github.com/bitwarden/browser
synced 2025-12-20 02:03:39 +00:00
SSO feature (#604)
* Update feature/sso jslib261a200-> 2e823ea (#589) * [SSO] Reset master password (#580) * Initial commit reset master password (sso) * Reverted order of two factor/reset password conditional * Added necessary resetMasterPassword flag for potential entry into RMP flow * Complete Revamp: Reverted Register // Deleted reset-master-password // updated sso/(settings)change password to use use super class // Adjust routing/messages // Created (accounts) change-password * Updated button -> Set Master Password * Refactored change password sub classes to use new submit pattern * Cleaned import statements * Update jslib (7fa5178->fe167be) * Update jslibfe167be- >34632e5 * Fixed sso base class import * merge master * Fixed missing semicolon // updated jslib to whats in feature/sso * Fixed two factor formatting * Added new change password component to app module * Updated component selector * updating jslib 34632e5 -> 2e823ea * Fixed lint warning in two-factor component Co-authored-by: Kyle Spearrin <kyle.spearrin@gmail.com> * Update jslib to101c568(#594) * Support for dynamic clientid (#595) * support third party sso clients * jslib update * update jslib * Update change-password.component.ts * Update sso.component.ts * Update app.module.ts Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
This commit is contained in:
45
src/app/accounts/change-password.component.html
Normal file
45
src/app/accounts/change-password.component.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<div class="secondary-header">
|
||||
<h1>{{'setMasterPassword' | i18n}}</h1>
|
||||
</div>
|
||||
<app-callout type="tip">{{'ssoCompleteRegistration' | i18n}}</app-callout>
|
||||
<app-callout type="info" *ngIf="enforcedPolicyOptions">
|
||||
{{'masterPasswordPolicyInEffect' | i18n}}
|
||||
<ul class="mb-0">
|
||||
<li *ngIf="enforcedPolicyOptions?.minComplexity > 0">
|
||||
{{'policyInEffectMinComplexity' | i18n : getPasswordScoreAlertDisplay()}}
|
||||
</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.minLength > 0">
|
||||
{{'policyInEffectMinLength' | i18n : enforcedPolicyOptions?.minLength.toString()}}
|
||||
</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireUpper">{{'policyInEffectUppercase' | i18n}}</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireLower">{{'policyInEffectLowercase' | i18n}}</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireNumbers">{{'policyInEffectNumbers' | i18n}}</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireSpecial">{{'policyInEffectSpecial' | i18n : '!@#$%^&*'}}</li>
|
||||
</ul>
|
||||
</app-callout>
|
||||
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate autocomplete="off">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="newMasterPassword">{{'newMasterPass' | i18n}}</label>
|
||||
<input id="newMasterPassword" type="password" name="NewMasterPasswordHash" class="form-control mb-1"
|
||||
[(ngModel)]="newMasterPassword" (input)="updatePasswordStrength()" required appInputVerbatim
|
||||
autocomplete="new-password">
|
||||
<app-password-strength [score]="masterPasswordScore" [showText]="true"></app-password-strength>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="confirmNewMasterPassword">{{'confirmNewMasterPass' | i18n}}</label>
|
||||
<input id="confirmNewMasterPassword" type="password" name="ConfirmNewMasterPasswordHash"
|
||||
class="form-control" [(ngModel)]="confirmNewMasterPassword" required appInputVerbatim
|
||||
autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'setMasterPassword' | i18n}}</span>
|
||||
</button>
|
||||
</form>
|
||||
65
src/app/accounts/change-password.component.ts
Normal file
65
src/app/accounts/change-password.component.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { FolderService } from 'jslib/abstractions/folder.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { PolicyService } from 'jslib/abstractions/policy.service';
|
||||
import { SyncService } from 'jslib/abstractions/sync.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { CipherString } from 'jslib/models/domain/cipherString';
|
||||
import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey';
|
||||
|
||||
import { SetPasswordRequest } from 'jslib/models/request/setPasswordRequest';
|
||||
|
||||
import {
|
||||
ChangePasswordComponent as BaseChangePasswordComponent,
|
||||
} from 'jslib/angular/components/change-password.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-accounts-change-password',
|
||||
templateUrl: 'change-password.component.html',
|
||||
})
|
||||
export class ChangePasswordComponent extends BaseChangePasswordComponent {
|
||||
onSuccessfulChangePassword: () => Promise<any>;
|
||||
successRoute = 'lock';
|
||||
|
||||
constructor(apiService: ApiService, i18nService: I18nService,
|
||||
cryptoService: CryptoService, messagingService: MessagingService,
|
||||
userService: UserService, passwordGenerationService: PasswordGenerationService,
|
||||
platformUtilsService: PlatformUtilsService, folderService: FolderService,
|
||||
cipherService: CipherService, syncService: SyncService,
|
||||
policyService: PolicyService, router: Router, private route: ActivatedRoute) {
|
||||
super(apiService, i18nService, cryptoService, messagingService, userService, passwordGenerationService,
|
||||
platformUtilsService, folderService, cipherService, syncService, policyService, router);
|
||||
}
|
||||
|
||||
async performSubmitActions(newMasterPasswordHash: string, newKey: SymmetricCryptoKey,
|
||||
newEncKey: [SymmetricCryptoKey, CipherString]) {
|
||||
const setRequest = new SetPasswordRequest();
|
||||
setRequest.newMasterPasswordHash = newMasterPasswordHash;
|
||||
setRequest.key = newEncKey[1].encryptedString;
|
||||
|
||||
try {
|
||||
this.formPromise = this.apiService.setPassword(setRequest);
|
||||
await this.formPromise;
|
||||
|
||||
if (this.onSuccessfulChangePassword != null) {
|
||||
this.onSuccessfulChangePassword();
|
||||
} else {
|
||||
this.router.navigate([this.successRoute]);
|
||||
}
|
||||
} catch {
|
||||
this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,108 +13,22 @@ import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
|
||||
import { ConstantsService } from 'jslib/services/constants.service';
|
||||
|
||||
import { Utils } from 'jslib/misc/utils';
|
||||
|
||||
import { AuthResult } from 'jslib/models/domain/authResult';
|
||||
import { SsoComponent as BaseSsoComponent } from 'jslib/angular/components/sso.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-sso',
|
||||
templateUrl: 'sso.component.html',
|
||||
})
|
||||
export class SsoComponent {
|
||||
identifier: string;
|
||||
loggingIn = false;
|
||||
|
||||
formPromise: Promise<AuthResult>;
|
||||
onSuccessfulLogin: () => Promise<any>;
|
||||
onSuccessfulLoginNavigate: () => Promise<any>;
|
||||
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
|
||||
|
||||
protected twoFactorRoute = '2fa';
|
||||
protected successRoute = 'lock';
|
||||
|
||||
private redirectUri = window.location.origin + '/sso-connector.html';
|
||||
|
||||
constructor(private authService: AuthService, private router: Router,
|
||||
private i18nService: I18nService, private route: ActivatedRoute,
|
||||
private storageService: StorageService, private stateService: StateService,
|
||||
private platformUtilsService: PlatformUtilsService, private apiService: ApiService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
private passwordGenerationService: PasswordGenerationService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
|
||||
if (qParams.code != null && qParams.state != null) {
|
||||
const codeVerifier = await this.storageService.get<string>(ConstantsService.ssoCodeVerifierKey);
|
||||
const state = await this.storageService.get<string>(ConstantsService.ssoStateKey);
|
||||
await this.storageService.remove(ConstantsService.ssoCodeVerifierKey);
|
||||
await this.storageService.remove(ConstantsService.ssoStateKey);
|
||||
if (qParams.code != null && codeVerifier != null && state != null && state === qParams.state) {
|
||||
await this.logIn(qParams.code, codeVerifier);
|
||||
}
|
||||
}
|
||||
if (queryParamsSub != null) {
|
||||
queryParamsSub.unsubscribe();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async submit() {
|
||||
const passwordOptions: any = {
|
||||
type: 'password',
|
||||
length: 64,
|
||||
uppercase: true,
|
||||
lowercase: true,
|
||||
numbers: true,
|
||||
special: false,
|
||||
};
|
||||
const state = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, 'sha256');
|
||||
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
|
||||
|
||||
await this.storageService.save(ConstantsService.ssoCodeVerifierKey, codeVerifier);
|
||||
await this.storageService.save(ConstantsService.ssoStateKey, state);
|
||||
|
||||
const authorizeUrl = this.apiService.identityBaseUrl + '/connect/authorize?' +
|
||||
'client_id=web&redirect_uri=' + this.redirectUri + '&' +
|
||||
'response_type=code&scope=api offline_access&' +
|
||||
'state=' + state + '&code_challenge=' + codeChallenge + '&' +
|
||||
'code_challenge_method=S256&response_mode=query&' +
|
||||
'domain_hint=' + this.identifier;
|
||||
this.platformUtilsService.launchUri(authorizeUrl, { sameWindow: true });
|
||||
}
|
||||
|
||||
private async logIn(code: string, codeVerifier: string) {
|
||||
this.loggingIn = true;
|
||||
try {
|
||||
this.formPromise = this.authService.logInSso(code, codeVerifier, this.redirectUri);
|
||||
const response = await this.formPromise;
|
||||
if (response.twoFactor) {
|
||||
this.platformUtilsService.eventTrack('SSO Logged In To Two-step');
|
||||
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
||||
this.onSuccessfulLoginTwoFactorNavigate();
|
||||
} else {
|
||||
this.router.navigate([this.twoFactorRoute]);
|
||||
}
|
||||
} else if (response.resetMasterPassword) {
|
||||
// TODO: launch reset master password flow
|
||||
} else {
|
||||
const disableFavicon = await this.storageService.get<boolean>(ConstantsService.disableFaviconKey);
|
||||
await this.stateService.save(ConstantsService.disableFaviconKey, !!disableFavicon);
|
||||
if (this.onSuccessfulLogin != null) {
|
||||
this.onSuccessfulLogin();
|
||||
}
|
||||
this.platformUtilsService.eventTrack('SSO Logged In');
|
||||
if (this.onSuccessfulLoginNavigate != null) {
|
||||
this.onSuccessfulLoginNavigate();
|
||||
} else {
|
||||
this.router.navigate([this.successRoute]);
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
this.loggingIn = false;
|
||||
export class SsoComponent extends BaseSsoComponent {
|
||||
constructor(authService: AuthService, router: Router,
|
||||
i18nService: I18nService, route: ActivatedRoute,
|
||||
storageService: StorageService, stateService: StateService,
|
||||
platformUtilsService: PlatformUtilsService, apiService: ApiService,
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
passwordGenerationService: PasswordGenerationService) {
|
||||
super(authService, router, i18nService, route, storageService, stateService, platformUtilsService,
|
||||
apiService, cryptoFunctionService, passwordGenerationService);
|
||||
this.redirectUri = window.location.origin + '/sso-connector.html';
|
||||
this.clientId = 'web';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@ import {
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
|
||||
import { Router } from '@angular/router';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { TwoFactorOptionsComponent } from './two-factor-options.component';
|
||||
|
||||
@@ -34,12 +37,25 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
||||
i18nService: I18nService, apiService: ApiService,
|
||||
platformUtilsService: PlatformUtilsService, stateService: StateService,
|
||||
environmentService: EnvironmentService, private componentFactoryResolver: ComponentFactoryResolver,
|
||||
storageService: StorageService) {
|
||||
storageService: StorageService, private route: ActivatedRoute) {
|
||||
super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService,
|
||||
stateService, storageService);
|
||||
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
const queryParamsSub = this.route.queryParams.subscribe((qParams) => {
|
||||
if (qParams.resetMasterPassword != null) {
|
||||
this.resetMasterPassword = qParams.resetMasterPassword;
|
||||
}
|
||||
|
||||
if (queryParamsSub != null) {
|
||||
queryParamsSub.unsubscribe();
|
||||
}
|
||||
});
|
||||
super.ngOnInit();
|
||||
}
|
||||
|
||||
anotherMethod() {
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
const modal = this.twoFactorOptionsModal.createComponent(factory).instance;
|
||||
@@ -66,7 +82,11 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
||||
this.router.navigate([loginRedirect.route], { queryParams: loginRedirect.qParams });
|
||||
await this.stateService.remove('loginRedirect');
|
||||
} else {
|
||||
this.router.navigate([this.successRoute]);
|
||||
this.router.navigate([this.successRoute], {
|
||||
queryParams: {
|
||||
resetMasterPassword: this.resetMasterPassword,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user