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

refactor: WIP general simplification

parent a11452ed
{
"name": "angular-aap-auth",
"version": "0.2.0",
"version": "1.0.0-dev.1",
"license": "Apache-2.0",
"scripts": {
"ng": "ng",
......
......@@ -11,9 +11,8 @@
</div>
<div>
<p>Authenticated: {{ isAuthenticated|async }}</p>
<p>Real name: {{ (credentials|async).realname }}</p>
<!-- Trying alternative, more direct access -->
<p>Real name: {{ realname|async }}</p>
<p>Username: {{ username|async }}</p>
<p>Expiration: {{ (credentials|async).expiration }}</p>
<p>Token: {{ (credentials|async).token }}</p>
<p>Expiration: {{ expiration|async }}</p>
<p>Token: {{ token|async }}</p>
</div>
......@@ -11,8 +11,10 @@ import {
import {
AuthService,
Credentials
} from 'app/modules/auth/auth.service';
import {
JwtHelperService,
} from '@auth0/angular-jwt';
@Component({
selector: 'app-root',
......@@ -20,23 +22,41 @@ import {
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
authURL = 'https://api.aai.ebi.ac.uk';
credentials: Observable < Credentials > ;
username: Observable < string | null > ;
realname: Observable < string | null > ;
token: Observable < string | null > ;
isAuthenticated: Observable < string > ;
// How to obtain other claims
expiration: Observable < Date | null > ;
constructor(
// Public for demostration purposes
public auth: AuthService,
private jwt: JwtHelperService
) {
this.credentials = auth.credentials$;
this.username = auth.username$;
this.isAuthenticated = (auth.isAuthenticated$).pipe(
this.username = auth.username();
this.realname = auth.realname();
this.token = auth.token();
this.isAuthenticated = (auth.isAuthenticated()).pipe(
map(value => value && 'true' || 'false')
);
this.expiration = this.token.pipe(
map(token => {
try {
return jwt.getTokenExpirationDate(<string>token);
} catch (e) {
return null;
}
})
);
}
ngOnInit() {
// Register and unregister login events
// Demostration of register and unregister login events
this.auth.addLogInEventListener(() => console.log('Welcome'));
const firstEventID = this.auth.addLogInEventListener(() => console.log('This should not be visible'));
this.auth.removeLogInEventListener(firstEventID);
......@@ -44,7 +64,7 @@ export class AppComponent implements OnInit {
const secondEventID = this.auth.addLogInEventListener(() => alert('This should never be displayed'));
this.auth.removeLogInEventListener(secondEventID);
// Register and unregister logout events
// Demostration of register and unregister logout events
this.auth.addLogOutEventListener(() => console.log('Bye'));
const thirdEventID = this.auth.addLogOutEventListener(() => console.log('This should not be visible'));
this.auth.removeLogOutEventListener(thirdEventID);
......
......@@ -12,6 +12,19 @@ import {
AuthModule
} from './modules/auth/auth.module';
export function getToken(): string {
const token = localStorage.getItem('id_token');
if (token === null){
throw Error('Unable to access localStorage token');
}
return token;
}
export function removeToken(): void {
return localStorage.removeItem('id_token');
}
export function updateToken(newToken: string): void {
return localStorage.setItem('id_token', newToken);
}
@NgModule({
declarations: [
......@@ -20,9 +33,18 @@ import {
imports: [
BrowserModule,
AuthModule
// AuthModule
AuthModule.forRoot({
aapURL: 'test',
tokenRemover: removeToken,
tokenUpdater: updateToken,
config: {
tokenGetter: getToken,
}
})
],
providers: [
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
import {
InjectionToken
} from '@angular/core';
import {
JwtModuleOptions
} from '@auth0/angular-jwt';
export interface AuthConfig extends JwtModuleOptions {
aapURL: string;
// tokenGetter: () => string | null;
tokenRemover: () => void;
tokenUpdater: (newToken: any) => void;
}
export const AAP_CONFIG = new InjectionToken < AuthConfig > ('AAP_CONFIG');
import {
NgModule
NgModule,
Optional,
SkipSelf,
ModuleWithProviders,
} from '@angular/core';
import {
CommonModule
} from '@angular/common';
import {
AuthConfig,
AAP_CONFIG
} from './auth.config';
import {
AuthService
} from './auth.service';
import {
// JwtModule,
JwtModule,
JWT_OPTIONS,
JwtHelperService
} from '@auth0/angular-jwt';
export let tokenName = 'id_token';
export function getToken(): string | null {
return localStorage.getItem(tokenName);
export function getToken(): string {
const token = localStorage.getItem('id_token');
if (token === null) {
throw Error('Unable to access localStorage token');
}
return token;
}
export function removeToken(): void {
return localStorage.removeItem('id_token');
}
export function updateToken(newToken: string): void {
return localStorage.setItem('id_token', newToken);
}
@NgModule({
imports: [
CommonModule,
// JwtModule.forRoot({
// config: {
// tokenGetter: getToken,
// whitelistedDomains: []
// }
// })
// JwtModule.forRoot({
// config: {
// tokenGetter: getToken,
// whitelistedDomains: []
// }
// })
],
providers: [
providers: [{
provide: AAP_CONFIG,
useValue: {
aapURL: 'https://api.aai.ebi.ac.uk',
tokenRemover: removeToken,
tokenUpdater: updateToken,
}
},
AuthService,
{
provide: JWT_OPTIONS,
......@@ -40,4 +63,57 @@ export function getToken(): string | null {
},
JwtHelperService
]
}) export class AuthModule {}
}) export class AuthModule {
constructor(@Optional() @SkipSelf() parentModule: AuthModule) {
if (parentModule) {
throw new Error('AuthModule is already loaded. It should only be imported in your application\'s main module.');
}
}
static forRoot(options?: AuthConfig): ModuleWithProviders {
const tokenName = 'id_token';
const defaultConf: AuthConfig = {
aapURL: 'https://api.aai.ebi.ac.uk',
tokenRemover: () => localStorage.removeItem(tokenName),
tokenUpdater: (newToken: any) => localStorage.setItem(tokenName, newToken),
config: {
tokenGetter: () => {
return localStorage.getItem(tokenName) || '';
}
}
};
if (options && options.config && options.config.tokenGetter) {
return {
ngModule: AuthModule,
providers: [{
provide: AAP_CONFIG,
useValue: options.config
},
AuthService,
{
provide: JWT_OPTIONS,
useValue: options.config
},
JwtHelperService
]
};
}
return {
ngModule: AuthModule,
providers: [{
provide: AAP_CONFIG,
useValue: defaultConf
},
AuthService,
{
provide: JWT_OPTIONS,
useValue: defaultConf.config
},
JwtHelperService
]
};
}
}
import {
Injectable,
Inject,
RendererFactory2
} from '@angular/core';
import {
......@@ -13,21 +14,14 @@ import {
map
} from 'rxjs/operators';
import {
AAP_CONFIG,
AuthConfig
} from './auth.config';
import {
JwtHelperService
} from '@auth0/angular-jwt';
// TODO: remove dependency
let tokenName = 'id_token';
let authURL = 'https://api.aai.ebi.ac.uk';
export interface Credentials {
realname: string | null;
username: string | null;
token: string | null;
expiration: Date | null;
}
interface LoginOptions {
[key: string]: string
}
......@@ -35,126 +29,59 @@ interface LoginOptions {
@Injectable()
export class AuthService {
static emptyCredentials: Credentials = {
realname: null,
username: null,
token: null,
expiration: null
}
private _realname = new BehaviorSubject < string | null > (null);
private _realname$ = this._realname.asObservable();
private _credentials = new BehaviorSubject < Credentials > (AuthService.emptyCredentials);
public credentials$ = this._credentials.asObservable();
private _username = new BehaviorSubject < string | null > (null);
private _username$ = this._username.asObservable();
public realname$ = this.credentials$.pipe(map(credentials => credentials.realname));
public username$ = this.credentials$.pipe(map(credentials => credentials.username));
public token$ = this.credentials$.pipe(map(credentials => credentials.token));
public expiration$ = this.credentials$.pipe(map(credentials => credentials.expiration));
private _token = new BehaviorSubject < string | null > (null);
private _token$ = this._token.asObservable();
private _isAuthenticated = new BehaviorSubject < boolean > (false);
public isAuthenticated$ = this._isAuthenticated.asObservable();
private _isAuthenticated$ = this._isAuthenticated.asObservable();
private _loginCallbacks: Function[] = [];
private _logoutCallbacks: Function[] = [];
private _timeoutID: number;
// Configuration
readonly domain: string;
readonly aapURL: string;
readonly storageUpdater: (newToken: any) => void;
readonly storageRemover: () => void;
constructor(
private rendererFactory: RendererFactory2,
private jwt: JwtHelperService
// @Inject('AAP_CONFIG') private environment: AuthConfig,
private _rendererFactory: RendererFactory2,
private _jwt: JwtHelperService,
@Inject(AAP_CONFIG) private config: AuthConfig
) {
this.domain = encodeURIComponent(window.location.origin);
this.aapURL = config.aapURL;
this.storageRemover = config.tokenRemover;
this.storageUpdater = config.tokenUpdater;
/**
* Listen for login messages from other windows and firing the callbacks.
* These messages contain the tokens from the AAP.
*
* @param {Function} callback The Function called when the event with the
* JWT token is received and accepted as valid.
*/
const renderer = rendererFactory.createRenderer(null, null);
renderer.listen('window', 'message', (event: MessageEvent) => {
if (!this.messageIsAcceptable(event)) {
return;
}
localStorage.setItem(tokenName, event.data);
event.source.close();
this._updateCredentials();
});
this._listenLoginMessage();
this._updateCredentials();
}
private _updateCredentials() {
const isAuthenticated = this.loggedIn();
if (isAuthenticated) {
this._credentials.next(this._getCredentials());
this._isAuthenticated.next(isAuthenticated);
this._loginCallbacks.map(callback => callback && callback());
// Schedule future logout event base on token expiration
if (this._timeoutID) {
window.clearTimeout(this._timeoutID);
}
// coercing dates to numbers with the unary operator '+'
const delay = + < Date > this.jwt.getTokenExpirationDate() - +new Date();
this._timeoutID = window.setTimeout(this.logOut.bind(this), delay);
} else {
this._isAuthenticated.next(isAuthenticated);
this._credentials.next(AuthService.emptyCredentials);
}
}
/**
* Check if there's a user logging on and whether the token is still valid.
* @returnType { boolean } Whether the application is able to send
* authenticated requests or not.
*/
public loggedIn(): boolean {
try {
return !this.jwt.isTokenExpired();
} catch (error) {
return false
}
}
private _getCredentials(): Credentials {
return {
realname: this.getName(),
username: this.getUserName(),
token: this.getToken(),
expiration: this.getExpiration()
};
}
public getUserName(): string | null {
return this.getClaim < string, null > ('email', null);
public isAuthenticated(): Observable < boolean > {
return this._isAuthenticated$;
}
public getName(): string | null {
return this.getClaim < string, null > ('name', null);
public realname(): Observable < string | null > {
return this._realname$;
}
public getToken(): string | null {
return this.jwt.tokenGetter();
public username(): Observable < string | null > {
return this._username$;
}
public getExpiration(): Date | null {
return this.jwt.getTokenExpirationDate();
public token(): Observable < string | null > {
return this._token$;
}
public logOut(): void {
localStorage.removeItem(tokenName);
this._updateCredentials();
this._logoutCallbacks.map(callback => callback && callback());
if (this._timeoutID) {
window.clearTimeout(this._timeoutID);
}
}
/**
* Functions that opens a window instead of a tab.
*
......@@ -166,7 +93,7 @@ export class AuthService {
* @param {number} left Position of the left corners. If it is a negative
* number it centres the login window on the screen.
*/
public windowOpen(loginOptions ? : LoginOptions, width = 650, height = 1000, top = -1, left = -1) {
public windowOpen(loginOptions?: LoginOptions, width = 650, height = 1000, top = -1, left = -1) {
if (left < 0) {
const screenWidth = screen.width;
if (screenWidth > width) {
......@@ -207,7 +134,7 @@ export class AuthService {
*
* @param {LoginOptions} loginOptions Options passed as URL parameters to the SSO.
*/
public tabOpen(loginOptions ? : LoginOptions) {
public tabOpen(loginOptions?: LoginOptions) {
const loginWindow = window.open(this.getSSOURL(loginOptions), 'Sign in to Elixir');
if (loginWindow) {
loginWindow.focus();
......@@ -222,12 +149,25 @@ export class AuthService {
* @returnType { string } The SSO URL.
*
*/
public getSSOURL(options ? : LoginOptions): string {
public getSSOURL(options?: LoginOptions): string {
let extra = '';
if (options) {
extra = Object.entries(options).reduce((accumulator, keyvalue) => `${accumulator}&${keyvalue[0]}=${keyvalue[1]}`, '');
}
return `${authURL}/sso?from=${this.domain}${extra}`;
return `${this.aapURL}/sso?from=${this.domain}${extra}`;
}
/**
* Functions that logs out the user.
* It triggers the logout callbacks.
*/
public logOut(): void {
this.storageRemover();
this._updateCredentials();
this._logoutCallbacks.map(callback => callback && callback());
if (this._timeoutID) {
window.clearTimeout(this._timeoutID);
}
}
/**
......@@ -276,16 +216,86 @@ export class AuthService {
return delete this._logoutCallbacks[index - 1];
}
/**
* Listen for login messages from other windows.
* These messages contain the tokens from the AAP.
* If a token is received then the callbacks are triggered.
*
* @param {Function} callback The Function called when the event with the
* JWT token is received and accepted as valid.
*/
private _listenLoginMessage() {
const renderer = this._rendererFactory.createRenderer(null, null);
renderer.listen('window', 'message', (event: MessageEvent) => {
if (!this.messageIsAcceptable(event)) {
return;
}
this.storageUpdater(event.data);
event.source.close();
this._updateCredentials();
});
}
/**
* Check if the message is coming from the same domain we use to generate
* the SSO URL, otherwise it's iffy and shouldn't trust it.
*/
private messageIsAcceptable(event: MessageEvent): boolean {
const expectedURL: string = authURL.replace(/\/$/, '');
const expectedURL: string = this.aapURL.replace(/\/$/, '');
return event.origin === expectedURL;
}
private _updateCredentials() {
const isAuthenticated = this._loggedIn();
this._isAuthenticated.next(isAuthenticated);
if (isAuthenticated) {
this._username.next(this._getUserName());
this._realname.next(this._getRealName());
this._token.next(this._getToken());
this._loginCallbacks.map(callback => callback && callback());
// Schedule future logout event base on token expiration
if (this._timeoutID) {
window.clearTimeout(this._timeoutID);
}
// Coercing dates to numbers with the unary operator '+'
const delay = +this._jwt.getTokenExpirationDate() - +new Date();
this._timeoutID = window.setTimeout(this.logOut.bind(this), delay);
} else {
this._username.next(null);
this._realname.next(null);
this._token.next(null);
}
}
/**
* Check if there's a user logging on and whether the token is still valid.
* @returnType { boolean } Whether the application is able to send
* authenticated requests or not.
*/
private _loggedIn(): boolean {
try {
return !this._jwt.isTokenExpired();
} catch (error) {
return false
}
}
private _getUserName(): string | null {
return this._getClaim < string, null > ('email', null);
}
private _getRealName(): string | null {
return this._getClaim < string, null > ('name', null);
}
private _getToken(): string | null {
return this._jwt.tokenGetter();
}
/**
* Get claims from the token.
*
......@@ -294,9 +304,9 @@ export class AuthService {
*
* @returnType { any } Claim
*/
public getClaim<T, C>(claim:string, defaultValue: C): T|C{
private _getClaim < T, C > (claim: string, defaultValue: C): T | C {
try {