mirror of
https://github.com/bitwarden/browser
synced 2025-01-23 09:42:06 +01:00
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:
parent
98eaeddbfd
commit
22a1cef498
@ -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}}
|
||||
|
@ -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() {
|
||||
|
@ -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>
|
||||
|
31
src/app/accounts/sso.component.html
Normal file
31
src/app/accounts/sso.component.html
Normal 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>
|
120
src/app/accounts/sso.component.ts
Normal file
120
src/app/accounts/sso.component.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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],
|
||||
|
@ -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
32
src/connectors/sso.html
Normal 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
1
src/connectors/sso.scss
Normal file
@ -0,0 +1 @@
|
||||
@import "../scss/styles.scss";
|
24
src/connectors/sso.ts
Normal file
24
src/connectors/sso.ts
Normal 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, ' '));
|
||||
}
|
@ -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) { }
|
||||
|
@ -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();
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user