[ADF-3735] SSO Role guard and Login error improvement (#4377)

* fix lint and doc

* Update auth-guard-sso-role.service.md

* Update auth-guard-sso-role.service.md

* fix json en

* restore en.json file
This commit is contained in:
Eugenio Romano
2019-03-06 09:53:43 +00:00
committed by GitHub
parent 0802ff7e7d
commit aba5674e80
12 changed files with 387 additions and 69 deletions

View File

@@ -17,7 +17,7 @@
import { ModuleWithProviders } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard, AuthGuardEcm, ErrorContentComponent, AuthGuardBpm } from '@alfresco/adf-core';
import { AuthGuard, AuthGuardEcm, ErrorContentComponent, AuthGuardBpm, AuthGuardSsoRoleService } from '@alfresco/adf-core';
import { AppLayoutComponent } from './components/app-layout/app-layout.component';
import { LoginComponent } from './components/login/login.component';
import { HomeComponent } from './components/home/home.component';
@@ -143,6 +143,8 @@ export const appRoutes: Routes = [
},
{
path: 'cloud',
canActivate: [AuthGuardSsoRoleService],
data: { roles: ['ACTIVITI_USER'], redirectUrl: '/error/403'},
children: [
{
path: '',
@@ -359,6 +361,10 @@ export const appRoutes: Routes = [
path: 'error/:id',
component: ErrorContentComponent
},
{
path: 'error/no-authorization',
component: ErrorContentComponent
},
{
path: '**',
redirectTo: 'error/404'

View File

@@ -103,7 +103,8 @@ for more information about installing and using the source code.
| [Apps process service](apps-process.service.md) | Gets details of the Process Services apps that are deployed for the user. | [Source](../../lib/core/services/apps-process.service.ts) |
| [Auth guard bpm service](auth-guard-bpm.service.md) | Adds authentication with Process Services to a route within the app. | [Source](../../lib/core/services/auth-guard-bpm.service.ts) |
| [Auth guard ecm service](auth-guard-ecm.service.md) | Adds authentication with Content Services to a route within the app. | [Source](../../lib/core/services/auth-guard-ecm.service.ts) |
| [Auth guard service](auth-guard.service.md) | Adds authentication to a route within the app. | [Source](../../lib/core/services/auth-guard.service.ts) |
| [Auth guard service](auth-guard.service.md) | Adds authentication to a route within the app. | [Source](../../lib/core/services/auth-guard.service.ts)
| [Auth guard SSO Role service](auth-guard-sso-role.service.md) | check the roles on a user | [Source](../../lib/core/services/auth-guard-sso-role.service.ts) |
| [Authentication service](authentication.service.md) | Provides authentication to ACS and APS. | [Source](../../lib/core/services/authentication.service.ts) |
| [Comment content service](comment-content.service.md) | Adds and retrieves comments for nodes in Content Services. | [Source](../../lib/core/services/comment-content.service.ts) |
| [Comment process service](comment-process.service.md) | Adds and retrieves comments for task and process instances in Process Services. | [Source](../../lib/core/services/comment-process.service.ts) |

View File

@@ -0,0 +1,57 @@
---
Title: Auth Guard SSO Role service
Added: v3.1.0
Status: Active
---
# [Auth Guard SSO role service](../../lib/core/services/auth-guard-sso-role.service.ts "Defined in auth-guard-sso-role.service.ts")
Allow to check the user roles of a user
## Details
The Auth Guard SSO role service implements an Angular
[route guard](https://angular.io/guide/router#milestone-5-route-guards)
to check the user has the right role permission. This is typically used with the
`canActivate` guard check in the route definition. The roles that user needs to have in order to access the route has to be specified in the roles array as in the example below:
```ts
const appRoutes: Routes = [
...
{
path: 'examplepath',
component: ExampleComponent,
canActivate: [ AuthGuardSsoRoleService ],
data: { roles: ['USER_ROLE1', 'USER_ROLE2']}
},
...
]
```
If the user now clicks on a link or button that follows this route, they will be not able to access to this content if the user does not have the roles.
## Redirect over forbidden
If the you want to redirect the user to a different page over a forbidden error you can use the **redirectUrl** as the example below:
```ts
const appRoutes: Routes = [
...
{
path: 'examplepath',
component: ExampleComponent,
canActivate: [ AuthGuardSsoRoleService ],
data: { roles: ['ACTIVITI_USER'], redirectUrl: '/error/403'}
},
...
]
```
Note: you can use this Guard in and with the other ADF auth guard.
## See also
- [Auth guard ecm service](auth-guard-ecm.service.md)
- [Auth guard bpm service](auth-guard-bpm.service.md)
- [Auth guard service](auth-guard.service.md)

View File

@@ -210,7 +210,8 @@
"LOGIN-ERROR-PROVIDERS": "Providers cannot be undefined",
"LOGIN-ERROR-CORS": "CORS exception, check your server configuration",
"LOGIN-ERROR-CSRF": "CSRF exception, set [disableCsrf]=\"true\" in login.component",
"LOGIN-ECM-LICENSE": "Alfresco Content Services repository is in read-only mode"
"LOGIN-ECM-LICENSE": "Alfresco Content Services repository is in read-only mode",
"SSO-WRONG-CONFIGURATION": "SSO Authentication server unreachable"
},
"BUTTON": {
"LOGIN": "SIGN IN",

View File

@@ -21,7 +21,6 @@
<mat-card-content class="adf-login-controls">
<div *ngIf="!implicitFlow">
<!--ERRORS AREA-->
<div class="adf-error-container">
<div *ngIf="isError" id="login-error" data-automation-id="login-error"
@@ -31,6 +30,8 @@
</div>
</div>
<div *ngIf="!implicitFlow">
<!--USERNAME FIELD-->
<div class="adf-login__field"
[ngClass]="{'adf-is-invalid': isErrorStyle(form.controls.username)}">

View File

@@ -569,7 +569,9 @@ describe('LoginComponent', () => {
loginWithCredentials('fake-username', 'fake-password');
}));
describe('SSO', () => {
describe('SSO ', () => {
describe('implicitFlow ', () => {
beforeEach(() => {
appConfigService.config.oauth2 = <OauthConfigModel> { implicitFlow: true };
@@ -613,5 +615,34 @@ describe('LoginComponent', () => {
});
}));
it('should show the SSO error when the discovery server is unreachable', async(() => {
spyOn(authService, 'isOauth').and.returnValue(true);
spyOn(authService, 'isSSODiscoveryConfigured').and.returnValue(false);
component.ngOnInit();
fixture.detectChanges();
element.querySelector('[data-automation-id="login-button-sso"]').click();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(getLoginErrorMessage()).toEqual('LOGIN.MESSAGES.SSO-WRONG-CONFIGURATION' );
});
}));
it('should not show the SSO error when the discovery server is reachable', async(() => {
spyOn(authService, 'isOauth').and.returnValue(true);
spyOn(authService, 'isSSODiscoveryConfigured').and.returnValue(true);
spyOn(authService, 'ssoImplicitLogin').and.stub();
component.ngOnInit();
fixture.detectChanges();
element.querySelector('[data-automation-id="login-button-sso"]').click();
fixture.whenStable().then(() => {
expect(getLoginErrorMessage()).toBeUndefined();
});
}));
});
});
});

View File

@@ -15,7 +15,8 @@
* limitations under the License.
*/
import { Component, EventEmitter,
import {
Component, EventEmitter,
Input, OnInit, Output, TemplateRef, ViewEncapsulation
} from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
@@ -173,6 +174,11 @@ export class LoginComponent implements OnInit {
*/
onSubmit(values: any) {
this.disableError();
if (this.authService.isOauth() && this.authService.isSSODiscoveryConfigured()) {
this.errorMsg = 'LOGIN.MESSAGES.SSO-WRONG-CONFIGURATION';
this.isError = true;
} else {
const args = new LoginSubmitEvent({
controls: { username: this.form.controls.username }
});
@@ -184,10 +190,16 @@ export class LoginComponent implements OnInit {
this.performLogin(values);
}
}
}
implicitLogin() {
if (this.authService.isOauth() && !this.authService.isSSODiscoveryConfigured()) {
this.errorMsg = 'LOGIN.MESSAGES.SSO-WRONG-CONFIGURATION';
this.isError = true;
} else {
this.authService.ssoImplicitLogin();
}
}
/**
* The method check the error in the form and push the error in the formError object

View File

@@ -0,0 +1,119 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { async, TestBed } from '@angular/core/testing';
import { ActivatedRouteSnapshot, Router } from '@angular/router';
import { setupTestBed } from '../testing/setupTestBed';
import { CoreTestingModule } from '../testing/core.testing.module';
import { AuthGuardSsoRoleService } from './auth-guard-sso-role.service';
import { JwtHelperService } from './jwt-helper.service';
import { StorageService } from './storage.service';
describe('Auth Guard SSO role service', () => {
let authGuard: AuthGuardSsoRoleService;
let storageService: StorageService;
let jwtHelperService: JwtHelperService;
let routerService: Router;
setupTestBed({
imports: [CoreTestingModule]
});
beforeEach(() => {
localStorage.clear();
storageService = TestBed.get(StorageService);
authGuard = TestBed.get(AuthGuardSsoRoleService);
jwtHelperService = TestBed.get(JwtHelperService);
routerService = TestBed.get(Router);
});
it('Should canActivate be true if the Role is present int the JWT token', async(() => {
spyOn(storageService, 'getItem').and.returnValue('my-access_token');
spyOn(jwtHelperService, 'decodeToken').and.returnValue({ 'realm_access': { roles: ['role1'] } });
const router: ActivatedRouteSnapshot = new ActivatedRouteSnapshot();
router.data = { 'roles': ['role1', 'role2'] };
expect(authGuard.canActivate(router, null)).toBeTruthy();
}));
it('Should canActivate be false if the Role is not present int the JWT token', async(() => {
spyOn(storageService, 'getItem').and.returnValue('my-access_token');
spyOn(jwtHelperService, 'decodeToken').and.returnValue({ 'realm_access': { roles: ['role3'] } });
const router: ActivatedRouteSnapshot = new ActivatedRouteSnapshot();
router.data = { 'roles': ['role1', 'role2'] };
expect(authGuard.canActivate(router, null)).toBeFalsy();
}));
it('Should not redirect if canActivate is', async(() => {
spyOn(storageService, 'getItem').and.returnValue('my-access_token');
spyOn(jwtHelperService, 'decodeToken').and.returnValue({ 'realm_access': { roles: ['role1'] } });
spyOn(routerService, 'navigate').and.stub();
const router: ActivatedRouteSnapshot = new ActivatedRouteSnapshot();
router.data = { 'roles': ['role1', 'role2']};
expect(authGuard.canActivate(router, null)).toBeTruthy();
expect(routerService.navigate).not.toHaveBeenCalled();
}));
it('Should canActivate return false if the data Role to check is empty', async(() => {
spyOn(storageService, 'getItem').and.returnValue('my-access_token');
spyOn(jwtHelperService, 'decodeToken').and.returnValue({ 'realm_access': { roles: ['role1', 'role3'] } });
const router: ActivatedRouteSnapshot = new ActivatedRouteSnapshot();
expect(authGuard.canActivate(router, null)).toBeFalsy();
}));
it('Should canActivate return false if the realm_access is not present', async(() => {
spyOn(storageService, 'getItem').and.returnValue('my-access_token');
spyOn(jwtHelperService, 'decodeToken').and.returnValue({ });
const router: ActivatedRouteSnapshot = new ActivatedRouteSnapshot();
expect(authGuard.canActivate(router, null)).toBeFalsy();
}));
it('Should redirect to the redirectURL if canActivate is false and redirectUrl is in data', async(() => {
spyOn(storageService, 'getItem').and.returnValue('my-access_token');
spyOn(jwtHelperService, 'decodeToken').and.returnValue({ });
spyOn(routerService, 'navigate').and.stub();
const router: ActivatedRouteSnapshot = new ActivatedRouteSnapshot();
router.data = { 'roles': ['role1', 'role2'], 'redirectUrl': 'no-role-url'};
expect(authGuard.canActivate(router, null)).toBeFalsy();
expect(routerService.navigate).toHaveBeenCalledWith(['/no-role-url']);
}));
it('Should not redirect if canActivate is false and redirectUrl is not in data', async(() => {
spyOn(storageService, 'getItem').and.returnValue('my-access_token');
spyOn(jwtHelperService, 'decodeToken').and.returnValue({ });
spyOn(routerService, 'navigate').and.stub();
const router: ActivatedRouteSnapshot = new ActivatedRouteSnapshot();
router.data = { 'roles': ['role1', 'role2']};
expect(authGuard.canActivate(router, null)).toBeFalsy();
expect(routerService.navigate).not.toHaveBeenCalled();
}));
});

View File

@@ -0,0 +1,82 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { JwtHelperService } from './jwt-helper.service';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { StorageService } from './storage.service';
@Injectable({
providedIn: 'root'
})
export class AuthGuardSsoRoleService implements CanActivate {
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
let hasRole = false;
if (route.data) {
let rolesToCheck = route.data['roles'];
hasRole = this.hasRoles(rolesToCheck);
}
if (!hasRole && route.data && route.data['redirectUrl']) {
this.router.navigate(['/' + route.data['redirectUrl']]);
}
return hasRole;
}
constructor(private storageService: StorageService, private jwtHelperService: JwtHelperService, private router: Router) {
}
getRoles(): string[] {
const access = this.getValueFromToken<any>('realm_access');
const roles = access ? access['roles'] : [];
return roles;
}
getAccessToken(): string {
return this.storageService.getItem('access_token');
}
hasRole(role: string): boolean {
let hasRole = false;
if (this.getAccessToken()) {
const roles = this.getRoles();
hasRole = roles.some((currentRole) => {
return currentRole === role;
});
}
return hasRole;
}
hasRoles(rolesToCheck: string []): boolean {
return rolesToCheck.some((currentRole) => {
return this.hasRole(currentRole);
});
}
getValueFromToken<T>(key: string): T {
let value;
const accessToken = this.getAccessToken();
if (accessToken) {
const tokenPayload = this.jwtHelperService.decodeToken(accessToken);
value = tokenPayload[key];
}
return <T> value;
}
}

View File

@@ -314,4 +314,11 @@ export class AuthenticationService {
}
});
}
/**
* Check if SSO is configured correctly
*/
isSSODiscoveryConfigured() {
return this.alfrescoApi.getInstance().storage.getItem('discovery') ? true : false;
}
}

View File

@@ -21,6 +21,7 @@ export * from './content.service';
export * from './auth-guard.service';
export * from './auth-guard-ecm.service';
export * from './auth-guard-bpm.service';
export * from './auth-guard-sso-role.service';
export * from './apps-process.service';
export * from './page-title.service';
export * from './storage.service';