1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-20 02:03:39 +00: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:
Kyle Spearrin
2020-07-16 09:18:25 -04:00
committed by GitHub
parent 98eaeddbfd
commit 22a1cef498
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;
}
}