[ADF-2795] SSO implicitflow (#3332)

* Enable OAUTH2

* Create SSO services

* SSO improvements

* Rollback sso login change

* Add SSO configuration from Setting component

* Refactoring

* Remove login ECM/BPM toggle and move use the userpreference instead of store

* fix host setting unit test

* Fix unit test missing instance

* use the Js api oauth

* add logout component and clean sso not used class

* fix dependencies cicle

* add translation settings

* fix style setting page

* clean

* JS APi should receive the oauth config from the userPreference and not from the config file

* change login if SSO is present

* missing spaces

* add sso test in login component

* add logout directive new properties test

* Improve host setting and remove library reference

* fix login test

* Remove unused code

* Fix authentication unit test

* fix authguard unit test

* fix csrf check login component

* fix unit test core and demo shell

* remove
This commit is contained in:
Maurizio Vitale
2018-06-07 23:19:58 +01:00
committed by Eugenio Romano
parent 3a6c12e624
commit f8e92b2fb0
57 changed files with 1295 additions and 681 deletions

View File

@@ -1,6 +1,6 @@
<div class="adf-login-content" [style.background-image]="'url(' + backgroundImageUrl + ')'">
<div class="ie11FixerParent">
<div class="ie11FixerChild">
<div class="ie11FixerParent">
<div class="ie11FixerChild">
<mat-card class="adf-login-card-wide">
<form id="adf-login-form" [formGroup]="form" (submit)="onSubmit(form.value)" autocomplete="off">
@@ -10,107 +10,128 @@
<div class="adf-alfresco-logo">
<!--HEADER TEMPLATE-->
<ng-template *ngIf="headerTemplate"
ngFor [ngForOf]="[data]"
[ngForTemplate]="headerTemplate">
ngFor [ngForOf]="[data]"
[ngForTemplate]="headerTemplate">
</ng-template>
<img *ngIf="!headerTemplate" class="adf-img-logo" [src]="logoImageUrl"
alt="{{'LOGIN.LOGO' | translate }}">
alt="{{'LOGIN.LOGO' | translate }}">
</div>
</mat-card-title>
</mat-card-header>
<mat-card-content class="adf-login-controls">
<!--ERRORS AREA-->
<div class="adf-error-container">
<div *ngIf="isError" id="login-error" data-automation-id="login-error"
class="error adf-error-message">
<mat-icon class="error-icon">warning</mat-icon>
<span class="login-error-message">{{errorMsg | translate }}</span>
</div>
</div>
<!--USERNAME FIELD-->
<div class="adf-login__field" [ngClass]="{'is-invalid': isErrorStyle(form.controls.username)}">
<mat-form-field class="adf-full-width" floatPlaceholder="never" color="primary">
<input matInput placeholder="{{'LOGIN.LABEL.USERNAME' | translate }}"
type="text"
class="adf-full-width"
[formControl]="form.controls['username']"
autocapitalize="none"
id="username"
data-automation-id="username"
(blur)="trimUsername($event)"
tabindex="1">
</mat-form-field>
<span class="adf-login-validation" for="username" *ngIf="formError.username">
<span id="username-error" class="adf-login-error" data-automation-id="username-error">{{formError.username | translate }}</span>
</span>
</div>
<!--PASSWORD FIELD-->
<div class="adf-login__field">
<mat-form-field class="adf-full-width" floatPlaceholder="never" color="primary">
<input matInput placeholder="{{'LOGIN.LABEL.PASSWORD' | translate }}"
type="password"
[formControl]="form.controls['password']"
id="password"
data-automation-id="password"
tabindex="2">
<mat-icon *ngIf="isPasswordShow" matSuffix class="adf-login-password-icon"
data-automation-id="hide_password" (click)="toggleShowPassword()">visibility
</mat-icon>
<mat-icon *ngIf="!isPasswordShow" matSuffix class="adf-login-password-icon"
data-automation-id="show_password" (click)="toggleShowPassword()">visibility_off
</mat-icon>
</mat-form-field>
<span class="adf-login-validation" for="password" *ngIf="formError.password">
<span id="password-required" class="adf-login-error"
data-automation-id="password-required">{{formError.password | translate }}</span>
</span>
</div>
<!--CUSTOM CONTENT-->
<ng-content></ng-content>
<br>
<button type="submit" id="login-button" tabindex="3"
class="adf-login-button"
mat-raised-button color="primary"
[class.isChecking]="actualLoginStep === LoginSteps.Checking"
[class.isWelcome]="actualLoginStep === LoginSteps.Welcome"
data-automation-id="login-button" [disabled]="!form.valid">
<span *ngIf="actualLoginStep === LoginSteps.Landing" class="adf-login-button-label">{{ 'LOGIN.BUTTON.LOGIN' | translate }}</span>
<div *ngIf="actualLoginStep === LoginSteps.Checking" class="adf-interactive-login-label">
<span class="adf-login-button-label">{{ 'LOGIN.BUTTON.CHECKING' | translate }}</span>
<div class="adf-login-spinner-container">
<mat-spinner id="checking-spinner" class="adf-login-checking-spinner" [diameter]="25"></mat-spinner>
<div *ngIf="!implicitFlow">
<!--ERRORS AREA-->
<div class="adf-error-container">
<div *ngIf="isError" id="login-error" data-automation-id="login-error"
class="error adf-error-message">
<mat-icon class="error-icon">warning</mat-icon>
<span class="login-error-message">{{errorMsg | translate }}</span>
</div>
</div>
<!--USERNAME FIELD-->
<div class="adf-login__field"
[ngClass]="{'is-invalid': isErrorStyle(form.controls.username)}">
<mat-form-field class="adf-full-width" floatPlaceholder="never" color="primary">
<input matInput placeholder="{{'LOGIN.LABEL.USERNAME' | translate }}"
type="text"
class="adf-full-width"
[formControl]="form.controls['username']"
autocapitalize="none"
id="username"
data-automation-id="username"
(blur)="trimUsername($event)"
tabindex="1">
</mat-form-field>
<div *ngIf="actualLoginStep === LoginSteps.Welcome" class="adf-interactive-login-label">
<span class="adf-login-button-label">{{ 'LOGIN.BUTTON.WELCOME' | translate }}</span>
<mat-icon class="welcome-icon">done</mat-icon>
<span class="adf-login-validation" for="username" *ngIf="formError.username">
<span id="username-error" class="adf-login-error" data-automation-id="username-error">{{formError.username | translate }}</span>
</span>
</div>
</button>
<div *ngIf="showRememberMe" class="adf-login__remember-me">
<mat-checkbox id="adf-login-remember" color="primary" class="adf-login-rememberme" [checked]="rememberMe"
(change)="rememberMe = !rememberMe">{{ 'LOGIN.LABEL.REMEMBER' | translate }}
</mat-checkbox>
<!--PASSWORD FIELD-->
<div class="adf-login__field">
<mat-form-field class="adf-full-width" floatPlaceholder="never" color="primary">
<input matInput placeholder="{{'LOGIN.LABEL.PASSWORD' | translate }}"
type="password"
[formControl]="form.controls['password']"
id="password"
data-automation-id="password"
tabindex="2">
<mat-icon *ngIf="isPasswordShow" matSuffix class="adf-login-password-icon"
data-automation-id="hide_password" (click)="toggleShowPassword()">
visibility
</mat-icon>
<mat-icon *ngIf="!isPasswordShow" matSuffix class="adf-login-password-icon"
data-automation-id="show_password" (click)="toggleShowPassword()">
visibility_off
</mat-icon>
</mat-form-field>
<span class="adf-login-validation" for="password" *ngIf="formError.password">
<span id="password-required" class="adf-login-error"
data-automation-id="password-required">{{formError.password | translate }}</span>
</span>
</div>
<!--CUSTOM CONTENT-->
<ng-content></ng-content>
<br>
<button type="submit" id="login-button" tabindex="3"
class="adf-login-button"
mat-raised-button color="primary"
[class.isChecking]="actualLoginStep === LoginSteps.Checking"
[class.isWelcome]="actualLoginStep === LoginSteps.Welcome"
data-automation-id="login-button" [disabled]="!form.valid">
<span *ngIf="actualLoginStep === LoginSteps.Landing" class="adf-login-button-label">{{ 'LOGIN.BUTTON.LOGIN' | translate }}</span>
<div *ngIf="actualLoginStep === LoginSteps.Checking"
class="adf-interactive-login-label">
<span
class="adf-login-button-label">{{ 'LOGIN.BUTTON.CHECKING' | translate }}</span>
<div class="adf-login-spinner-container">
<mat-spinner id="checking-spinner" class="adf-login-checking-spinner"
[diameter]="25"></mat-spinner>
</div>
</div>
<div *ngIf="actualLoginStep === LoginSteps.Welcome" class="adf-interactive-login-label">
<span class="adf-login-button-label">{{ 'LOGIN.BUTTON.WELCOME' | translate }}</span>
<mat-icon class="welcome-icon">done</mat-icon>
</div>
</button>
<div *ngIf="showRememberMe" class="adf-login__remember-me">
<mat-checkbox id="adf-login-remember" color="primary" class="adf-login-rememberme"
[checked]="rememberMe"
(change)="rememberMe = !rememberMe">{{ 'LOGIN.LABEL.REMEMBER' | translate
}}
</mat-checkbox>
</div>
</div>
<div *ngIf="implicitFlow">
<button type="button" (click)="implicitLogin()" id="login-button-sso" tabindex="1"
class="adf-login-button"
mat-raised-button color="primary"
data-automation-id="login-button-sso">
<span class="adf-login-button-label">{{ 'LOGIN.BUTTON.SSO' | translate }}</span>
</button>
</div>
</mat-card-content>
<mat-card-actions *ngIf="footerTemplate || showLoginActions">
<div class="adf-login-action-container">
<!--FOOTER TEMPLATE-->
<ng-template *ngIf="footerTemplate"
ngFor [ngForOf]="[data]"
[ngForTemplate]="footerTemplate">
ngFor [ngForOf]="[data]"
[ngForTemplate]="footerTemplate">
</ng-template>
<div class="adf-login-action" *ngIf="!footerTemplate && showLoginActions">
<div id="adf-login-action-left" class="adf-login-action-left">
@@ -131,5 +152,5 @@
</div>
</div>
</div>
</div>
</div>

View File

@@ -58,7 +58,7 @@ describe('LoginComponent', () => {
imports: [CoreTestingModule]
});
beforeEach(() => {
beforeEach(async(() => {
fixture = TestBed.createComponent(LoginComponent);
element = fixture.nativeElement;
@@ -66,15 +66,17 @@ describe('LoginComponent', () => {
component.showRememberMe = true;
component.showLoginActions = true;
usernameInput = element.querySelector('#username');
passwordInput = element.querySelector('#password');
authService = TestBed.get(AuthenticationService);
router = TestBed.get(Router);
userPreferences = TestBed.get(UserPreferencesService);
fixture.detectChanges();
});
fixture.whenStable().then(() => {
usernameInput = element.querySelector('#username');
passwordInput = element.querySelector('#password');
});
}));
afterEach(() => {
fixture.destroy();
@@ -98,7 +100,7 @@ describe('LoginComponent', () => {
});
it('should redirect to route on successful login', () => {
spyOn(authService, 'login').and.returnValue(Observable.of({ type: 'type', ticket: 'ticket'}));
spyOn(authService, 'login').and.returnValue(Observable.of({ type: 'type', ticket: 'ticket' }));
const redirect = '/home';
component.successRoute = redirect;
spyOn(router, 'navigate');
@@ -107,10 +109,10 @@ describe('LoginComponent', () => {
});
it('should redirect to previous route state on successful login', () => {
spyOn(authService, 'login').and.returnValue(Observable.of({ type: 'type', ticket: 'ticket'}));
spyOn(authService, 'login').and.returnValue(Observable.of({ type: 'type', ticket: 'ticket' }));
const redirect = '/home';
component.successRoute = redirect;
authService.setRedirect({ provider: 'ECM', navigation: ['some-route'] } );
authService.setRedirect({ provider: 'ECM', navigation: ['some-route'] });
spyOn(router, 'navigate');
@@ -158,7 +160,7 @@ describe('LoginComponent', () => {
});
it('should be changed to the "welcome key" after a successful login attempt', () => {
spyOn(authService, 'login').and.returnValue(Observable.of({ type: 'type', ticket: 'ticket'}));
spyOn(authService, 'login').and.returnValue(Observable.of({ type: 'type', ticket: 'ticket' }));
loginWithCredentials('fake-username', 'fake-password');
expect(getLoginButtonText()).toEqual('LOGIN.BUTTON.WELCOME');
@@ -382,7 +384,7 @@ describe('LoginComponent', () => {
});
it('should return success event after the login have succeeded', (done) => {
spyOn(authService, 'login').and.returnValue(Observable.of({ type: 'type', ticket: 'ticket'}));
spyOn(authService, 'login').and.returnValue(Observable.of({ type: 'type', ticket: 'ticket' }));
component.providers = 'ECM';
expect(component.isError).toBe(false);
@@ -514,7 +516,7 @@ describe('LoginComponent', () => {
expect(component.isError).toBe(false);
expect(event).toEqual(
new LoginSuccessEvent({type: 'type', ticket: 'ticket'}, 'fake-username', null)
new LoginSuccessEvent({ type: 'type', ticket: 'ticket' }, 'fake-username', null)
);
});
@@ -583,4 +585,47 @@ describe('LoginComponent', () => {
loginWithCredentials('fake-username', 'fake-password');
}));
describe('SSO', () => {
beforeEach(() => {
userPreferences.oauthConfig = { implicitFlow: true };
});
afterEach(() => {
userPreferences.oauthConfig = null;
});
it('should not show login username and password if SSO implicit flow is active', async(() => {
spyOn(authService, 'isOauth').and.returnValue(true);
component.ngOnInit();
fixture.detectChanges();
expect(element.querySelector('#username')).toBeNull();
expect(element.querySelector('#password')).toBeNull();
}));
it('should not show the login base auth button', async(() => {
spyOn(authService, 'isOauth').and.returnValue(true);
userPreferences.oauthConfig = { implicitFlow: true };
component.ngOnInit();
fixture.detectChanges();
expect(element.querySelector('#login-button')).toBeNull();
}));
it('should show the login SSO button', async(() => {
spyOn(authService, 'isOauth').and.returnValue(true);
userPreferences.oauthConfig = { implicitFlow: true };
component.ngOnInit();
fixture.detectChanges();
expect(element.querySelector('#login-button-sso')).toBeDefined();
}));
});
});

View File

@@ -120,6 +120,8 @@ export class LoginComponent implements OnInit {
@Output()
executeSubmit = new EventEmitter<LoginSubmitEvent>();
implicitFlow: boolean = false;
form: FormGroup;
isError: boolean = false;
errorMsg: string;
@@ -154,6 +156,12 @@ export class LoginComponent implements OnInit {
}
ngOnInit() {
if (this.authService.isOauth()) {
if (this.userPreferences.oauthConfig && this.userPreferences.oauthConfig.implicitFlow) {
this.implicitFlow = true;
}
}
if (this.hasCustomFiledsValidation()) {
this.form = this._fb.group(this.fieldsValidation);
} else {
@@ -178,7 +186,7 @@ export class LoginComponent implements OnInit {
this.settingsService.csrfDisabled = this.disableCsrf;
this.disableError();
const args = new LoginSubmitEvent({controls : { username : this.form.controls.username} });
const args = new LoginSubmitEvent({ controls: { username: this.form.controls.username } });
this.executeSubmit.emit(args);
if (args.defaultPrevented) {
@@ -188,6 +196,10 @@ export class LoginComponent implements OnInit {
}
}
implicitLogin() {
this.authService.ssoImplictiLogin();
}
/**
* The method check the error in the form and push the error in the formError object
* @param data