merge sso feature branch (#523)

* Update jslib (101c568 -> 14b01f2) (#506)

* Update jslib (14b01f2 -> 1513b25) (#510)

* [jslib] Update (1513b25 -> 7c3a9d6) (#516)

* update jslib (1513b25 -> 7c3a9d6)

* Updated call to constructor super

* [SSO] Added SSO flows & functionality (#513)

* update jslib

* bump version

* Added sso button (wip)

* Added sso & change password // Added modules/routes // Added strings for localization

* Added password strength comp // reverted login route

* Updated sso component to send client id // added routing for sso // added crypto function to services module provider list

* Added deep linking

* First round of UI updates // Added sso browser launching // Added missing strings

* Updated UI and added missing strings

* Removed extra change password style

* Let constructor for WindowMain handle default width/height

* Prepared for jslib update

* Update jslib (1513b25 -> 7c3a9d6)

* Update login super

* Added params for launchSsoBrowser function

* Update jslib (7c3a9d6 -> 4203937)

* Added missing strings, removed unnecessary class param

* Upgrade TypeScript (#517)

* Updated password score // Update styles

* Removed password-strength component files

* Cleaned up module class // Fixed UL/LI formatting issues

* Use exisiting loading string // removed new string

* Update jslib (4203937 -> 9957125)

* Updated class to perform new submit actions

* Upgrade Angular (#520)

* di resolution for CryptoFunctionServiceAbstraction

* Update jslib (9957125 -> 5d874d0) (#521)

* Updated change password flow to match web

* Updated callout style

Co-authored-by: Kyle Spearrin <kyle.spearrin@gmail.com>
Co-authored-by: Oscar Hinton <hinton.oscar@gmail.com>

Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
Co-authored-by: Oscar Hinton <hinton.oscar@gmail.com>
This commit is contained in:
Kyle Spearrin 2020-08-21 09:50:36 -04:00 committed by GitHub
parent 83901e1e7c
commit 0ba2589461
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 382 additions and 3 deletions

View File

@ -33,6 +33,11 @@
<i class="fa fa-pencil-square-o" aria-hidden="true"></i> {{'createAccount' | i18n}}
</a>
</div>
<div class="buttons">
<a (click)="launchSsoBrowser('desktop', 'bitwarden://sso-callback')" class="btn block">
<i class="fa fa-bank" aria-hidden="true"></i> {{'enterpriseSingleSignOn' | i18n}}
</a>
</div>
<div class="sub-options">
<a routerLink="/hint">{{'getMasterPasswordHint' | i18n}}</a>
</div>

View File

@ -4,12 +4,16 @@ import {
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { Router } from '@angular/router';
import { EnvironmentComponent } from './environment.component';
import { AuthService } from 'jslib/abstractions/auth.service';
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StateService } from 'jslib/abstractions/state.service';
import { StorageService } from 'jslib/abstractions/storage.service';
@ -35,7 +39,8 @@ export class LoginComponent extends BaseLoginComponent {
platformUtilsService: PlatformUtilsService, stateService: StateService,
environmentService: EnvironmentService, passwordGenerationService: PasswordGenerationService,
cryptoFunctionService: CryptoFunctionService, storageService: StorageService) {
super(authService, router, platformUtilsService, i18nService, stateService, environmentService, passwordGenerationService, cryptoFunctionService, storageService);
super(authService, router, platformUtilsService, i18nService, stateService, environmentService,
passwordGenerationService, cryptoFunctionService, storageService);
super.onSuccessfulLogin = () => {
return syncService.fullSync(true);
};

View File

@ -0,0 +1,105 @@
<form id="set-password-page" #form>
<div class="content">
<img class="logo-image" alt="Bitwarden">
<p class="lead">{{'setMasterPassword' | i18n}}</p>
<div class="box">
<app-callout type="tip">{{'ssoCompleteRegistration' | i18n}}</app-callout>
<app-callout type="info" *ngIf="enforcedPolicyOptions">
{{'masterPasswordPolicyInEffect' | i18n}}
<ul>
<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>
</div>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate autocomplete="off">
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="masterPassword">{{'masterPass' | i18n}}
<strong class="sub-label text-{{masterPasswordScoreColor}}"
*ngIf="masterPasswordScoreText">
{{masterPasswordScoreText}}
</strong>
</label>
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}"
name="MasterPassword" class="monospaced" [(ngModel)]="masterPassword" required
(input)="updatePasswordStrength()" appInputVerbatim>
</div>
<div class="action-buttons">
<a class="row-btn" href="#" appStopClick appBlurClick role="button"
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePassword(false)">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</a>
</div>
</div>
<div class="progress">
<div class="progress-bar bg-{{masterPasswordScoreColor}}" role="progressbar"
aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"
[ngStyle]="{width: (masterPasswordScoreWidth + '%')}"
attr.aria-valuenow="{{masterPasswordScoreWidth}}">
</div>
</div>
</div>
</div>
<div class="box-footer">
{{'masterPassDesc' | i18n}}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="masterPasswordRetype">{{'reTypeMasterPass' | i18n}}</label>
<input id="masterPasswordRetype" type="password" name="MasterPasswordRetype"
class="monospaced" [(ngModel)]="masterPasswordRetype" required appInputVerbatim
autocomplete="new-password">
</div>
<div class="action-buttons">
<a class="row-btn" href="#" appStopClick appBlurClick role="button"
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePassword(true)">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</a>
</div>
</div>
</div>
</div>
</div>
<div class="box last">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="hint">{{'masterPassHint' | i18n}}</label>
<input id="hint" type="text" name="Hint" [(ngModel)]="hint">
</div>
</div>
<div class="box-footer">
{{'masterPassHintDesc' | i18n}}
</div>
</div>
<div class="buttons">
<button type="submit" class="btn primary block" [disabled]="form.loading">
<i *ngIf="form.loading" class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"
aria-hidden="true"></i>
<span>{{'submit' | i18n}}</span>
</button>
<button class="btn block" (click)="logOut()">
<span>{{'logOut' | i18n}}</span>
</button>
</div>
</form>
</div>
</form>

View File

@ -0,0 +1,68 @@
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 {
SetPasswordComponent as BaseSetPasswordComponent,
} from 'jslib/angular/components/set-password.component';
@Component({
selector: 'app-set-password',
templateUrl: 'set-password.component.html',
})
export class SetPasswordComponent extends BaseSetPasswordComponent {
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, route: ActivatedRoute) {
super(apiService, i18nService, cryptoService, messagingService, userService, passwordGenerationService,
platformUtilsService, folderService, cipherService, syncService, policyService, router, route);
}
get masterPasswordScoreWidth() {
return this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20;
}
get masterPasswordScoreColor() {
switch (this.masterPasswordScore) {
case 4:
return 'success';
case 3:
return 'primary';
case 2:
return 'warning';
default:
return 'danger';
}
}
get masterPasswordScoreText() {
switch (this.masterPasswordScore) {
case 4:
return this.i18nService.t('strong');
case 3:
return this.i18nService.t('good');
case 2:
return this.i18nService.t('weak');
default:
return this.masterPasswordScore != null ? this.i18nService.t('weak') : null;
}
}
}

View File

@ -0,0 +1,9 @@
<form id="sso-page" (ngSubmit)="submit()">
<div class="content">
<img class="logo-image" alt="Bitwarden">
<div class="box">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
{{'loading' | i18n}}
</div>
</div>
</form>

View File

@ -0,0 +1,35 @@
import { Component } from '@angular/core';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { ApiService } from 'jslib/abstractions/api.service';
import { AuthService } from 'jslib/abstractions/auth.service';
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StateService } from 'jslib/abstractions/state.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { SsoComponent as BaseSsoComponent } from 'jslib/angular/components/sso.component';
@Component({
selector: 'app-sso',
templateUrl: 'sso.component.html',
})
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 = 'bitwarden://sso-callback';
this.clientId = 'desktop';
}
}

View File

@ -10,6 +10,8 @@ import { HintComponent } from './accounts/hint.component';
import { LockComponent } from './accounts/lock.component';
import { LoginComponent } from './accounts/login.component';
import { RegisterComponent } from './accounts/register.component';
import { SetPasswordComponent } from './accounts/set-password.component';
import { SsoComponent } from './accounts/sso.component';
import { TwoFactorComponent } from './accounts/two-factor.component';
import { VaultComponent } from './vault/vault.component';
@ -26,6 +28,8 @@ const routes: Routes = [
canActivate: [AuthGuardService],
},
{ path: 'hint', component: HintComponent },
{ path: 'set-password', component: SetPasswordComponent },
{ path: 'sso', component: SsoComponent },
];
@NgModule({

View File

@ -194,6 +194,8 @@ export class AppComponent implements OnInit {
this.systemService.clearClipboard(message.clipboardValue, message.clearMs);
}
break;
case 'ssoCallback':
this.router.navigate(['sso'], { queryParams: { code: message.code, state: message.state } });
default:
}
});

View File

@ -23,7 +23,9 @@ import { LockComponent } from './accounts/lock.component';
import { LoginComponent } from './accounts/login.component';
import { PremiumComponent } from './accounts/premium.component';
import { RegisterComponent } from './accounts/register.component';
import { SetPasswordComponent } from './accounts/set-password.component';
import { SettingsComponent } from './accounts/settings.component';
import { SsoComponent } from './accounts/sso.component';
import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component';
import { TwoFactorComponent } from './accounts/two-factor.component';
@ -176,8 +178,10 @@ registerLocaleData(localeZhTw, 'zh-TW');
RegisterComponent,
SearchCiphersPipe,
SelectCopyDirective,
SetPasswordComponent,
SettingsComponent,
ShareComponent,
SsoComponent,
StopClickDirective,
StopPropDirective,
TrueFalseValueDirective,

View File

@ -210,6 +210,7 @@ export function initFactory(): Function {
{ provide: SystemServiceAbstraction, useValue: systemService },
{ provide: EventServiceAbstraction, useValue: eventService },
{ provide: PolicyServiceAbstraction, useValue: policyService },
{ provide: CryptoFunctionServiceAbstraction, useValue: cryptoFunctionService },
{
provide: APP_INITIALIZER,
useFactory: initFactory,

View File

@ -1344,5 +1344,59 @@
},
"vaultTimeoutLogOutConfirmationTitle": {
"message": "Timeout Action Confirmation"
},
"enterpriseSingleSignOn": {
"message": "Enterprise Single Sign-On"
},
"setMasterPassword": {
"message": "Set Master Password"
},
"ssoCompleteRegistration": {
"message": "In order to complete logging in with SSO, please set a master password to access and protect your vault."
},
"newMasterPass": {
"message": "New Master Password"
},
"confirmNewMasterPass": {
"message": "Confirm New Master Password"
},
"masterPasswordPolicyInEffect": {
"message": "One or more organization policies require your master password to meet the following requirements:"
},
"policyInEffectMinComplexity": {
"message": "Minimum complexity score of $SCORE$",
"placeholders": {
"score": {
"content": "$1",
"example": "4"
}
}
},
"policyInEffectMinLength": {
"message": "Minimum length of $LENGTH$",
"placeholders": {
"length": {
"content": "$1",
"example": "14"
}
}
},
"policyInEffectUppercase": {
"message": "Contain one or more uppercase characters"
},
"policyInEffectLowercase": {
"message": "Contain one or more lowercase characters"
},
"policyInEffectNumbers": {
"message": "Contain one or more numbers"
},
"policyInEffectSpecial": {
"message": "Contain one or more of the following special characters $CHARS$",
"placeholders": {
"chars": {
"content": "$1",
"example": "!@#$%^&*"
}
}
}
}

View File

@ -89,7 +89,8 @@ export class Main {
storageDefaults[ConstantsService.vaultTimeoutActionKey] = 'lock';
this.storageService = new ElectronStorageService(app.getPath('userData'), storageDefaults);
this.windowMain = new WindowMain(this.storageService, true);
this.windowMain = new WindowMain(this.storageService, true, undefined, undefined,
(arg) => this.processDeepLink(arg));
this.messagingMain = new MessagingMain(this, this.storageService);
this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain, 'desktop', () => {
this.menuMain.updateMenuItem.enabled = false;
@ -138,11 +139,32 @@ export class Main {
if (this.biometricMain != null) {
await this.biometricMain.init();
}
if (!app.isDefaultProtocolClient('bitwarden')) {
app.setAsDefaultProtocolClient('bitwarden');
}
// Process protocol for macOS
app.on('open-url', (event, url) => {
event.preventDefault();
this.processDeepLink([url]);
});
}, (e: any) => {
// tslint:disable-next-line
console.error(e);
});
}
private processDeepLink(argv: string[]): void {
argv.filter((s) => s.indexOf('bitwarden://') === 0).forEach((s) => {
const url = new URL(s);
const code = url.searchParams.get('code');
const receivedState = url.searchParams.get('state');
if (code != null && receivedState != null) {
this.messagingService.send('ssoCallback', { code: code, state: receivedState });
}
});
}
}
const main = new Main();

View File

@ -391,4 +391,9 @@ app-root > #loading {
}
}
}
ul {
padding-left: 40px;
margin: 0;
}
}

View File

@ -1,6 +1,6 @@
@import "variables.scss";
#login-page, #lock-page {
#login-page, #lock-page, #sso-page, #set-password-page {
display: flex;
justify-content: center;
align-items: center;
@ -123,6 +123,66 @@
}
}
#sso-page {
.content {
width: 300px;
.box {
margin-top: 30px;
margin-bottom: 30px;
text-align: center;
}
}
}
#set-password-page {
.content {
width: 500px;
p {
text-align: center
}
p.lead, h1 {
font-size: $font-size-large;
text-align: center;
margin-bottom: 20px;
font-weight: normal;
}
.buttons {
&:not(.with-rows), .buttons-row {
display: flex;
margin-bottom: 10px;
}
&:not(.with-rows), .buttons-row:last-child {
margin-bottom: 20px;
}
button {
margin-right: 10px;
&:last-child {
margin-right: 0;
}
}
}
.box {
margin-bottom: 15px;
&.last {
margin-bottom: 20px;
}
}
.box-content {
margin-bottom: 10px;
}
}
}
#register-page {
.content {
width: 400px;