Commit 82b25b74 authored by Eduardo Sanz García's avatar Eduardo Sanz García
Browse files

test (token): improve coverage

parent a34ae1c4
......@@ -5,7 +5,7 @@ root = true
charset = utf-8
indent_style = space
indent_size = 4
insert_final_newline = true
insert_final_newline = false
trim_trailing_whitespace = true
[*.md]
......
......@@ -18,7 +18,7 @@ import {
describe('AppComponent', () => {
let auth: AuthService;
let fixture: ComponentFixture<AppComponent>;
let fixture: ComponentFixture < AppComponent > ;
let app: AppComponent;
beforeEach(async (() => {
......@@ -42,7 +42,7 @@ describe('AppComponent', () => {
expect(app).toBeTruthy();
}));
fit(`should not be authenticated`, async (() => {
it(`should not be authenticated`, async (() => {
auth.logOut();
app.isAuthenticated$.subscribe(result => {
expect(result).toEqual('Nope');
......@@ -55,4 +55,3 @@ describe('AppComponent', () => {
expect(compiled.querySelector('h1').textContent).toContain('Auth testing app');
}));
});
......@@ -63,22 +63,42 @@ export class AppComponent implements OnInit {
// * test forms
// * add custom sync validator for username
createAAP = this._fb.group({
name: ['', {validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255)]}],
username: ['', {validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255)]}],
password: ['', {validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255)]}],
email: ['', {validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255)]}],
organization: ['', {validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255)]}],
name: ['', {
validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255)]
}],
username: ['', {
validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255)]
}],
password: ['', {
validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255)]
}],
email: ['', {
validators: [Validators.required, Validators.email, Validators.minLength(5), Validators.maxLength(255)]
}],
organization: ['', {
validators: [Validators.maxLength(255)]
}],
});
loginAAP = this._fb.group({
username: ['', {validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255)]}],
password: ['', {validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255)]}],
username: ['', {
validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255)]
}],
password: ['', {
validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255)]
}],
});
changePasswordAAP = this._fb.group({
username: ['', {validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255)]}],
oldPassword: ['', {validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255)]}],
newPassword: ['', {validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255)]}],
username: ['', {
validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255)]
}],
oldPassword: ['', {
validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255)]
}],
newPassword: ['', {
validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255)]
}],
});
domain = this._fb.group({
......@@ -169,24 +189,45 @@ export class AppComponent implements OnInit {
* Create AAP account
*/
createAAPaccount() {
console.log('create AAP account', this.createAAP.value);
// this._http.post < Domain > (this.domainsURL, this.domain.value, {
this.auth.createAAPaccount(this.createAAP.value).pipe(
first(),
filter(Boolean),
tap(_ => this.createAAP.reset({
name: '',
username: '',
password: '',
organization: ''
}))
).subscribe();
}
/**
* Login AAP account
*/
loginAAPaccount() {
console.log('login AAP account', this.loginAAP.value);
// this._http.post < Domain > (this.domainsURL, this.domain.value, {
this.auth.loginAAP(this.loginAAP.value).pipe(
first(),
filter(Boolean),
tap(_ => this.createAAP.reset({
name: '',
username: '',
}))
).subscribe();
}
/**
* Change password AAP account
*/
changePasswordAAPaccount() {
console.log('Password change AAP account', this.changePasswordAAP.value);
// this._http.post < Domain > (this.domainsURL, this.domain.value, {
this.auth.changePasswordAAP(this.changePasswordAAP.value).pipe(
first(),
filter(Boolean),
tap(_ => this.changePasswordAAP.reset({
name: '',
oldPassword: '',
newPassword: ''
}))
).subscribe();
}
/**
......@@ -207,15 +248,18 @@ export class AppComponent implements OnInit {
}
return null;
}),
tap(_ => this.domain.reset({domainName: '', domainDesc: ''})),
tap(_ => this.domain.reset({
domainName: '',
domainDesc: ''
})),
filter(Boolean),
concatMap(gid => this._http.put < Domain > (`${this.domainsURL}/${gid}/${uid}/user`, null, {
observe: 'response',
})),
first(),
map(response => {
this.refresh();
return response;
this.refresh();
return response;
}),
).subscribe();
}
......
......@@ -20,7 +20,7 @@ import {
} from '@angular/forms';
import {
environment
environment
} from 'src/environments/environment';
// Components
......@@ -53,17 +53,17 @@ const domain = environment.aapURL.replace('https://', '');
aapURL: environment.aapURL,
tokenGetter: getToken,
tokenUpdater: updateToken,
// tokenRemover: removeToken // Optional
// tokenRemover: removeToken // Optional
}),
JwtModule.forRoot({
config: {
tokenGetter: getToken,
whitelistedDomains: [environment.aapDomain]
whitelistedDomains: [environment.aapDomain],
blacklistedRoutes: [environment.loginAAP],
}
})
],
providers: [
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
......@@ -52,6 +52,8 @@ export class AuthService {
// Configuration
private readonly _domain: string;
private readonly _appURL: string;
private readonly _tokenURL: string;
private readonly _authURL: string;
private readonly _storageUpdater: (newToken: any) => void;
private readonly _storageRemover: () => void;
......@@ -67,7 +69,11 @@ export class AuthService {
@Inject(AAP_CONFIG) private config: AuthConfig
) {
this._domain = encodeURIComponent(window.location.origin);
this._appURL = config.aapURL.replace(/\/$/, '');
this._authURL = `${this._appURL}/auth`;
this._tokenURL = `${this._appURL}/token`;
this._storageUpdater = config.tokenUpdater;
if (config.tokenRemover) {
this._storageRemover = config.tokenRemover;
......@@ -195,36 +201,6 @@ export class AuthService {
return `${this._appURL}/sso?from=${this._domain}${extra}`;
}
/**
* Filters options that are unsecure.
*
* See the advance options that can be requested through the options parameter:
* https://api.aai.ebi.ac.uk/docs/authentication/authentication.index.html#_common_attributes
*
* The time to live paramenter (ttl) default value is 60 minutes. It is a
* big security risk to request longer ttl. If a third party gets hold of
* such token, means that they could use it for a day, week, year
* (essentially, like having the username/password).
*
* @param loginOptions Options passed as URL parameters to the SSO.
*
*
*/
public _filterLoginOptions(options: LoginOptions) {
if (Object.keys(options).indexOf('ttl') > -1) {
const ttl: number = +options['ttl'];
const softLimit = 60;
const hardLimit = 60 * 24;
if (ttl > hardLimit) {
window.console.error(`Login requested with an expiration longer than ${hardLimit} minutes! This is not allowed.`);
window.console.error(`Expiration request reset to ${hardLimit} minutes.`);
options['ttl'] = '' + hardLimit;
} else if (ttl > softLimit) {
window.console.warn(`Login requested with an expiration longer than ${softLimit} minutes!`);
}
}
}
/**
* Functions that logs out the user.
* It triggers the logout callbacks.
......@@ -239,6 +215,95 @@ export class AuthService {
this._commKeyUpdater();
}
/**
* Create AAP account
*
* @returns uid of the new user when account is successfully created otherwise null
*/
public createAAPaccount(newUser: {
name: string,
username: string,
password: string,
email: string,
organization: string
}): Observable < string | null > {
return this._http.post(this._authURL, newUser, {
observe: 'response',
responseType: 'text'
}).pipe(
map(response => {
if (response.status === 200 && response.body) {
return response.body;
}
return null;
})
);
}
/**
* Login directly through the AAP
*
* @returns true if the user successfully login, otherwise false
*/
public loginAAP(
user: {
username: string,
password: string,
}
): Observable < boolean > {
return this._http.get(this._authURL, {
observe: 'response',
headers: this._createAuthHeader(user),
responseType: 'text'
}).pipe(
map(response => {
if (response.status === 200 && response.body) {
this._storageRemover();
this._storageUpdater(response.body);
this._updateUser();
// Triggers updating other windows
this._commKeyUpdater();
return true;
}
return false;
})
);
}
/**
* Change password AAP account
*
* @returns uid of the new user when account is successfully created otherwise null
*/
public changePasswordAAP({
username,
oldPassword,
newPassword
}: {
username: string,
oldPassword: string,
newPassword: string,
}): Observable < boolean > {
return this._http.patch(this._authURL, {
username,
password: newPassword
}, {
observe: 'response',
headers: this._createAuthHeader({
username,
password: oldPassword
})
}).pipe(
map(response => {
if (response.status === 200) {
return true;
}
return false;
})
);
}
/**
* Add a callback to the LogIn event.
*
......@@ -291,8 +356,7 @@ export class AuthService {
* @returns true when token is successfully refreshed
*/
public refresh(): Observable < boolean > {
const url = `${this._appURL}/token`;
return this._http.get(url, {
return this._http.get(this._tokenURL, {
observe: 'response',
responseType: 'text'
}).pipe(
......@@ -311,6 +375,56 @@ export class AuthService {
);
}
/**
* Filters options that are unsecure.
*
* See the advance options that can be requested through the options parameter:
* https://api.aai.ebi.ac.uk/docs/authentication/authentication.index.html#_common_attributes
*
* The time to live paramenter (ttl) default value is 60 minutes. It is a
* big security risk to request longer ttl. If a third party gets hold of
* such token, means that they could use it for a day, week, year
* (essentially, like having the username/password).
*
* @param loginOptions Options passed as URL parameters to the SSO.
*
*
*/
private _filterLoginOptions(options: LoginOptions) {
if (Object.keys(options).indexOf('ttl') > -1) {
const ttl: number = +options['ttl'];
const softLimit = 60;
const hardLimit = 60 * 24;
if (ttl > hardLimit) {
window.console.error(`Login requested with an expiration longer than ${hardLimit} minutes! This is not allowed.`);
window.console.error(`Expiration request reset to ${hardLimit} minutes.`);
options['ttl'] = '' + hardLimit;
} else if (ttl > softLimit) {
window.console.warn(`Login requested with an expiration longer than ${softLimit} minutes!`);
}
}
}
/**
* Creates Authorization header
*
* @param object with username and password.
*
* @returns New authorization header
*/
private _createAuthHeader({
username,
password
}: {
username: string,
password: string,
}) {
const authToken = btoa(`${username}:${password}`);
return new HttpHeaders({
'Authorization': `Basic ${authToken}`
});
}
/**
* Listen for login messages from other windows.
* These messages contain the tokens from the AAP.
......
......@@ -15,98 +15,156 @@ import {
EXPIRED_TOKEN_1
} from 'testing/tokens';
describe('TokenService (valid token)', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [{
provide: JWT_OPTIONS,
useValue: {
tokenGetter: () => VALID_TOKEN_1
}
},
JwtHelperService,
TokenService
]
describe('TokenService', () => {
let service: TokenService;
describe('with valid token', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [{
provide: JWT_OPTIONS,
useValue: {
tokenGetter: () => VALID_TOKEN_1
}
},
JwtHelperService,
TokenService
]
});
service = TestBed.get(TokenService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('getToken should return valid token', () => {
expect(service.getToken()).toEqual(VALID_TOKEN_1);
});
it('token should be valid', () => {
expect(service.isTokenValid()).toEqual(true);
});
it('token should have correct expired date', () => {
expect(service.getTokenExpirationDate()).toEqual(new Date(1000000000000000));
});
it('getClaim should return correct value', () => {
expect(service.getClaim('iss', 'Dummy')).toEqual('https://aai.ebi.ac.uk/sp');
});
it('getClaim should with non-existing claim should return default value "Dummy"', () => {
expect(service.getClaim('issr', 'Dummy')).toEqual('Dummy');
});
it('getClaim should with non-existing claim should return default value null', () => {
expect(service.getClaim('issr', null)).toEqual(null);
});
it('getClaim should with non-existing claim should return default value null', () => {
expect(service.getClaim('issr', null)).toEqual(null);
});
});
it('should be created', inject([TokenService], (service: TokenService) => {
expect(service).toBeTruthy();
}));
describe('with expired token', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [{
provide: JWT_OPTIONS,
useValue: {
tokenGetter: () => EXPIRED_TOKEN_1
}
},
JwtHelperService,
TokenService
]
});
service = TestBed.get(TokenService);
});
it('getToken should return valid token', inject([TokenService], (service: TokenService) => {
expect(service.getToken()).toBe(VALID_TOKEN_1);
}));
it('should be created', () => {
expect(service).toBeTruthy();
});
it('token should be valid', inject([TokenService], (service: TokenService) => {
expect(service.isTokenValid()).toBeTruthy();
}));
it('getToken should return expired token', () => {
expect(service.getToken()).toEqual(EXPIRED_TOKEN_1);
});
it('token should have correct expired date', inject([TokenService], (service: TokenService) => {
expect(service.getTokenExpirationDate()).toEqual(new Date(1000000000000000));
}));
it('token should not be valid', () => {
expect(service.isTokenValid()).toEqual(false);
});
it('getClaim should return correct value', inject([TokenService], (service: TokenService) => {
expect(service.getClaim('iss', 'Dummy')).toBe('https://aai.ebi.ac.uk/sp');
}));
it('token should have correct expired date', () => {
expect(service.getTokenExpirationDate()).toEqual(new Date(1518083433000));
});
it('getClaim should with non-existing claim should return default value "Dummy"', inject([TokenService], (service: TokenService) => {
expect(service.getClaim('issr', 'Dummy')).toEqual('Dummy');
}));
it('getClaim should return correct value', () => {
expect(service.getClaim('iss', 'Dummy')).toEqual('https://aai.ebi.ac.uk/sp');
});
it('getClaim should with non-existing claim should return default value null', inject([TokenService], (service: TokenService) => {
expect(service.getClaim('issr', null)).toEqual(null);
}));
it('getClaim should with non-existing claim should return default value "Dummy"',
() => {
expect(service.getClaim('issr', 'Dummy')).toEqual('Dummy');
});
it('getClaim should with non-existing claim should return default value null', inject([TokenService], (service: TokenService) => {
expect(service.getClaim('issr', null)).toEqual(null);
}));
});
it('getClaim should with non-existing claim should return default value null', () => {
expect(service.getClaim('issr', null)).toEqual(null);
});
describe('TokenService (expired token)', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [{
provide: JWT_OPTIONS,
useValue: {
tokenGetter: () => EXPIRED_TOKEN_1
}
},
JwtHelperService,
TokenService
]
it('getClaim should with non-existing claim should return default value null', () => {
expect(service.getClaim('issr', null)).toEqual(null);
});
});
it('should be created', inject([TokenService], (service: TokenService) => {
expect(service).toBeTruthy();
}));
describe('with malformed token)', () => {
const malformedToken = 'asdfk.asdf.asdf';
beforeEach(() => {
TestBed.configureTestingModule({
providers: [{
provide: JWT_OPTIONS,
useValue: {
tokenGetter: () => malformedToken
}
},
JwtHelperService,
TokenService
]
});
service = TestBed.get(TokenService);
});
it('getToken should return expired token', inject([TokenService], (service: TokenService) => {
expect(service.getToken()).toBe(EXPIRED_TOKEN_1);
}));
it('should be created', () => {
expect(service).toBeTruthy();
});
it('token should not be valid', inject([TokenService], (service: TokenService) => {
expect(service.isTokenValid()).toBeFalsy();
}));
it('getToken should return expired token', () => {