SSO support (#575)

* support for sso

* resetMasterPassword

* update jslib

* [Enterprise] Added button to launch portal (#570)

* initial commit

* Added Enterprise button and used new business portal bool

* Reverting services module local changes

* Formatted some new lines

* Closed alerts on lock (#572)

Co-authored-by: Addison Beck <addisonbeck@MacBook-Pro.local>

* Updated enterprise URL dev (port) (#574)

Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
Co-authored-by: Addison Beck <addisonbeck1@gmail.com>
Co-authored-by: Addison Beck <addisonbeck@MacBook-Pro.local>
This commit is contained in:
Kyle Spearrin 2020-07-16 09:18:25 -04:00 committed by GitHub
parent 98eaeddbfd
commit 22a1cef498
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 242 additions and 9 deletions

View File

@ -1,4 +1,4 @@
<form (ngSubmit)="submit()" class="container" ngNativeValidate>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="text-center mb-4">
@ -25,9 +25,11 @@
</div>
<hr>
<div class="d-flex">
<button type="submit" class="btn btn-primary btn-block">
<i class="fa fa-unlock-alt" aria-hidden="true"></i>
{{'unlock' | i18n}}
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
<span>
<i class="fa fa-unlock-alt" aria-hidden="true"></i> {{'unlock' | i18n}}
</span>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
<button type="button" class="btn btn-outline-secondary btn-block ml-2 mt-0" (click)="logOut()">
{{'logOut' | i18n}}

View File

@ -1,6 +1,7 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
@ -25,9 +26,9 @@ export class LockComponent extends BaseLockComponent {
userService: UserService, cryptoService: CryptoService,
storageService: StorageService, vaultTimeoutService: VaultTimeoutService,
environmentService: EnvironmentService, private routerService: RouterService,
stateService: StateService) {
stateService: StateService, apiService: ApiService) {
super(router, i18nService, platformUtilsService, messagingService, userService, cryptoService,
storageService, vaultTimeoutService, environmentService, stateService);
storageService, vaultTimeoutService, environmentService, stateService, apiService);
}
async ngOnInit() {

View File

@ -44,6 +44,11 @@
<i class="fa fa-pencil-square-o" aria-hidden="true"></i> {{'createAccount' | i18n}}
</a>
</div>
<div class="d-flex">
<a routerLink="/sso" class="btn btn-outline-secondary btn-block mt-2">
<i class="fa fa-bank" aria-hidden="true"></i> Enterprise Single Sign-On
</a>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,31 @@
<form (ngSubmit)="submit()" class="container" ngNativeValidate>
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<img src="../../images/logo-dark@2x.png" class="logo mb-2" alt="Bitwarden">
<div class="card d-block mt-4">
<div class="card-body" *ngIf="loggingIn">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
Logging in, please wait...
</div>
<div class="card-body" *ngIf="!loggingIn">
<p>Quickly log in using your organization's single sign-on portal. Please enter your organization's
identifier to begin.</p>
<div class="form-group">
<label for="identifier">Organization Identifier</label>
<input id="identifier" class="form-control" type="text" name="Identifier"
[(ngModel)]="identifier" required>
</div>
<hr>
<div class="d-flex">
<button type="submit" class="btn btn-primary btn-block">
<i class="fa fa-sign-in" aria-hidden="true"></i> {{'logIn' | i18n}}
</button>
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{'cancel' | i18n}}
</a>
</div>
</div>
</div>
</div>
</div>
</form>

View File

@ -0,0 +1,120 @@
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 { ConstantsService } from 'jslib/services/constants.service';
import { Utils } from 'jslib/misc/utils';
import { AuthResult } from 'jslib/models/domain/authResult';
@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;
}
}

View File

@ -15,6 +15,7 @@ import { LoginComponent } from './accounts/login.component';
import { RecoverDeleteComponent } from './accounts/recover-delete.component';
import { RecoverTwoFactorComponent } from './accounts/recover-two-factor.component';
import { RegisterComponent } from './accounts/register.component';
import { SsoComponent } from './accounts/sso.component';
import { TwoFactorComponent } from './accounts/two-factor.component';
import { VerifyEmailTokenComponent } from './accounts/verify-email-token.component';
import { VerifyRecoverDeleteComponent } from './accounts/verify-recover-delete.component';
@ -99,6 +100,11 @@ const routes: Routes = [
canActivate: [UnauthGuardService],
data: { titleId: 'createAccount' },
},
{
path: 'sso', component: SsoComponent,
canActivate: [UnauthGuardService],
data: { titleId: 'createAccount' }, // TODO
},
{
path: 'hint', component: HintComponent,
canActivate: [UnauthGuardService],

View File

@ -34,6 +34,7 @@ import { LoginComponent } from './accounts/login.component';
import { RecoverDeleteComponent } from './accounts/recover-delete.component';
import { RecoverTwoFactorComponent } from './accounts/recover-two-factor.component';
import { RegisterComponent } from './accounts/register.component';
import { SsoComponent } from './accounts/sso.component';
import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component';
import { TwoFactorComponent } from './accounts/two-factor.component';
import { VerifyEmailTokenComponent } from './accounts/verify-email-token.component';
@ -349,6 +350,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
SelectCopyDirective,
SettingsComponent,
ShareComponent,
SsoComponent,
StopClickDirective,
StopPropDirective,
TaxInfoComponent,

32
src/connectors/sso.html Normal file
View File

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=1010">
<meta name="theme-color" content="#175DDC">
<title>Logging into Bitwarden...</title>
<link rel="apple-touch-icon" sizes="180x180" href="images/icons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="images/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="images/icons/favicon-16x16.png">
<link rel="mask-icon" href="images/icons/safari-pinned-tab.svg" color="#175DDC">
<link rel="manifest" href="manifest.json">
</head>
<body class="layout_frontend">
<div class="mt-5 d-flex justify-content-center">
<div>
<img src="../images/logo-dark@2x.png" class="mb-4 logo" alt="Bitwarden">
<p class="text-center">
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="Loading" aria-hidden="true"></i>
</p>
<p class="text-center">
Logging into Bitwarden...
</p>
</div>
</div>
</body>
</html>

1
src/connectors/sso.scss Normal file
View File

@ -0,0 +1 @@
@import "../scss/styles.scss";

24
src/connectors/sso.ts Normal file
View File

@ -0,0 +1,24 @@
// tslint:disable-next-line
require('./sso.scss');
document.addEventListener('DOMContentLoaded', (event) => {
const code = getQsParam('code');
const state = getQsParam('state');
window.location.href = window.location.origin + '/#/sso?code=' + code + '&state=' + state;
});
function getQsParam(name: string) {
const url = window.location.href;
name = name.replace(/[\[\]]/g, '\\$&');
const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)');
const results = regex.exec(url);
if (!results) {
return null;
}
if (!results[2]) {
return '';
}
return decodeURIComponent(results[2].replace(/\+/g, ' '));
}

View File

@ -6,7 +6,8 @@ export class HtmlStorageService implements StorageService {
private localStorageKeys = new Set(['appId', 'anonymousAppId', 'rememberedEmail', 'passwordGenerationOptions',
ConstantsService.disableFaviconKey, 'rememberEmail', 'enableGravatars', 'enableFullWidth',
ConstantsService.localeKey, ConstantsService.autoConfirmFingerprints,
ConstantsService.vaultTimeoutKey, ConstantsService.vaultTimeoutActionKey]);
ConstantsService.vaultTimeoutKey, ConstantsService.vaultTimeoutActionKey, ConstantsService.ssoCodeVerifierKey,
ConstantsService.ssoStateKey]);
private localStorageStartsWithKeys = ['twoFactorToken_', ConstantsService.collapsedGroupingsKey + '_'];
constructor(private platformUtilsService: PlatformUtilsService) { }

View File

@ -93,8 +93,10 @@ export class WebPlatformUtilsService implements PlatformUtilsService {
launchUri(uri: string, options?: any): void {
const a = document.createElement('a');
a.href = uri;
a.target = '_blank';
a.rel = 'noreferrer noopener';
if (options == null || !options.sameWindow) {
a.target = '_blank';
a.rel = 'noreferrer noopener';
}
a.classList.add('d-none');
document.body.appendChild(a);
a.click();

View File

@ -88,6 +88,11 @@ const plugins = [
filename: 'u2f-connector.html',
chunks: ['connectors/u2f'],
}),
new HtmlWebpackPlugin({
template: './src/connectors/sso.html',
filename: 'sso-connector.html',
chunks: ['connectors/sso'],
}),
new CopyWebpackPlugin([
{ from: './src/.nojekyll' },
{ from: './src/manifest.json' },
@ -152,6 +157,7 @@ const config = {
'app/main': './src/app/main.ts',
'connectors/u2f': './src/connectors/u2f.js',
'connectors/duo': './src/connectors/duo.ts',
'connectors/sso': './src/connectors/sso.ts',
},
externals: {
'u2f': 'u2f',