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

feat (all): new functionality

parent c46778a5
......@@ -33,11 +33,16 @@ The library exposes user information through `User` objects, which have informat
- The unique identifier (`uid`): if a unique identifier has to be used, use this field.
- Name (`name`): the full name of the user, for display purposes
- Nickname (`nickname`): if the user is an local aap account, it will contain the username, otherwise will have a weird string.
- Email (`email`): the account's email, this is for information only and several accounts may have the same username.
- Domains (`domains`): not directly provided. They may be misused into dong checking if the user has authorization to do some actions.
This should be done always server-side, if the domains information wants to be shown to the user as information it can still be done,
check the [Advanced usage](#advanced-usage) to see how to expose arbitrary token claims, or the embedded app.
- Nickname (`nickname`): if the user is in a local aap account, it will contain
the username, otherwise will have a weird string.
- Email (`email`): the account's email, this is for information only and several
accounts may have the same username.
- Domains (`domains`): not directly provided because they may be misused.
The checking of domains should be done always server-side. If the domains
information wants to be shown to the user as information it can still be done,
check the [Advanced usage](#advanced-usage) to see how to expose arbitrary
token claims, or the embedded app.
In your Angular `AppModule` (app.module.ts):
```typescript
......@@ -48,6 +53,9 @@ import {
NgModule
} from '@angular/core';
import {
HttpClientModule
} from '@angular/common/http';
import {
AuthModule
} from 'ng-ebi-authorization';
......@@ -65,6 +73,7 @@ import {
],
imports: [
BrowserModule,
HttpClientModule,
AuthModule.forRoot(), // Defaults to localStorage `id_token` key.
JwtModule.forRoot({
config: {
......@@ -100,8 +109,8 @@ import {
@Component({
selector: 'app-root',
template: `
<button (click)="auth.windowOpen()">Login small window</button>
<button (click)="auth.tabOpen()">Login new tab</button>
<button (click)="auth.openLoginWindow()">Login small window</button>
<button (click)="auth.openLoginTab()">Login new tab</button>
<button (click)="auth.logOut()">Logout</button>
<div *ngIf="user | async; else loggedOut">
......@@ -145,8 +154,8 @@ import {
} from '@angular/core';
import {
AppComponent
} from './app.component';
HttpClientModule
} from '@angular/common/http';
import {
AuthModule
} from 'ng-ebi-authorization';
......@@ -154,6 +163,10 @@ import {
JwtModule
} from '@auth0/angular-jwt';
import {
AppComponent
} from './app.component';
export function getToken(): string {
return localStorage.getItem('jwt_token') || '';
}
......@@ -171,6 +184,7 @@ export function removeToken(): void {
],
imports: [
BrowserModule,
HttpClientModule
AuthModule.forRoot({
aapURL: 'https://api.aai.ebi.ac.uk',
tokenGetter: getToken,
......@@ -233,33 +247,44 @@ export class AppComponent implements OnInit {
// How to obtain other claims
expiration: Observable < Date | null > ;
domains: Observable < string[] > ;
iss: Observable < string | null > ;
constructor(
// Public for demonstration purposes
private auth: AuthService,
private jwt: JwtHelperService
private token: TokenService
) {
this.user = auth.user();
this.expiration = this.user.pipe(
map(user => {
try {
return jwt.getTokenExpirationDate(<string>user.token);
return token.getTokenExpirationDate();
} catch (e) {
return null;
}
})
);
this.domains = this.user.pipe(
map(_ => {
try {
return token.getClaim<string[], string[]>('domains', []);
} catch (e) {
return [];
}
})
);
this.iss = this.user.pipe(
map(_ => jwt.getClaim < string, null > ('iss', null))
map(_ => token.getClaim < string, null > ('iss', null))
);
}
openLoginWindow() {
// ttl: time of live, and location
this.auth.windowOpen({
this.auth.openLoginWindow({
'ttl': '1'
}, 500, 500, 100, 100);
}
......
......@@ -78,6 +78,14 @@
"assets": [
"src/assets",
"src/favicon.ico"
],
"codeCoverage": true,
"codeCoverageExclude": [
"src/polyfills.ts",
"src/test.ts",
"src/environments/environment.ts",
"src/app/modules/auth/auth.config.ts",
"src/app/modules/auth/auth.module.ts"
]
}
},
......
{
"name": "ng-ebi-authorization",
"version": "1.0.0-beta.4",
"version": "1.0.0-beta.5",
"license": "Apache-2.0",
"scripts": {
"ng": "ng",
"start": "ng serve --aot",
"build": "ng build --prod --aot",
"test": "ng test --karma-config=src/karma_chrome.conf.js --watch",
"test:sr": "ng test --code-coverage=true --progress=false",
"test:sr:chromium": "ng test --code-coverage=true --progress=false --karma-config=src/karma_chromium.conf.js",
"test:sr": "ng test --progress=false",
"test:sr:chromium": "ng test --progress=false --karma-config=src/karma_chromium.conf.js",
"lint": "ng lint",
"docs": "typedoc --module amd --out docs/ src/public_api.ts",
"e2e": "ng e2e",
......@@ -31,43 +31,43 @@
"bugs": {
"url": "https://gitlab.ebi.ac.uk/tools-glue/ng-ebi-authorization/issues"
},
"dependencies": {},
"dependencies": { "tslib": "^1.9.0"
},
"peerDependencies": {
"@angular/core": ">=7 <8",
"@auth0/angular-jwt": "^2.0.0",
"rxjs": ">=6 <7"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.10.0",
"@angular/cli": "7.0.2",
"@angular/common": "7.0.0",
"@angular/compiler": "7.0.0",
"@angular/compiler-cli": "7.0.0",
"@angular/core": "7.0.0",
"@angular/language-service": "7.0.0",
"@angular/platform-browser": "7.0.0",
"@angular/platform-browser-dynamic": "7.0.0",
"@auth0/angular-jwt": "^2.0.0",
"@types/jasmine": "~2.8.8",
"@types/jasminewd2": "~2.0.3",
"@types/node": "~10.12.0",
"@angular-devkit/build-angular": "~0.12.0",
"@angular/cli": "7.2.1",
"@angular/common": "7.2.0",
"@angular/compiler": "7.2.0",
"@angular/compiler-cli": "7.2.0",
"@angular/core": "7.2.0",
"@angular/forms": "^7.2.0",
"@angular/language-service": "7.2.0",
"@angular/platform-browser": "7.2.0",
"@angular/platform-browser-dynamic": "7.2.0",
"@auth0/angular-jwt": "^2.1.0",
"@types/jasmine": "~3.3.5",
"@types/jasminewd2": "~2.0.6",
"@types/node": "~10.12.18",
"codelyzer": "^4.4.4",
"core-js": "^2.5.7",
"jasmine-core": "^3.2.1",
"core-js": "^2.6.2",
"jasmine-core": "^3.3.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~3.1.0",
"karma": "~3.1.4",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "^2.0.4",
"karma-jasmine": "~1.1.2",
"karma-jasmine-html-reporter": "^1.3.1",
"ng-packagr": "^4.2.0",
"protractor": "~5.4.1",
"karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.4.0",
"ng-packagr": "^4.5.0",
"protractor": "~5.4.2",
"rxjs": "^6.3.2",
"ts-node": "~7.0.1",
"tsickle": "^0.33.0",
"tslint": "~5.11.0",
"typedoc": "^0.13.0",
"typescript": "3.1.3",
"zone.js": "^0.8.26"
"tsickle": "^0.34.0",
"tslint": "~5.12.1",
"typedoc": "^0.14.1",
"typescript": "3.2.2",
"zone.js": "^0.8.27"
}
}
......@@ -2,3 +2,8 @@
display: flex;
justify-content: space-between;
}
.separator {
margin-top: 2em;
}
......@@ -4,25 +4,130 @@
Auth testing app
</h1>
</div>
<div class="spread">
<button (click)="auth.windowOpen({ttl: '1'})">Login small window</button>
<button (click)="auth.tabOpen({ttl: '1'})">Login new tab</button>
<div class="spread separator">
<button (click)="auth.openLoginWindow({ttl: '1'})">ELIXIR login small
window
(expire
in 1 minute)</button>
<button (click)="auth.openLoginTab()">ELIXIR login new tab (default
expiration)</button>
<button (click)="auth.logOut()">Logout</button>
</div>
<div *ngIf="(user | async) as user; else loggedOut">
<h3 class="separator">Create AAP account:</h3>
<form [formGroup]="createAAP">
<label>
Name:
<input formControlName="name" />
</label>
<label>
Username:
<input formControlName="username" />
</label>
<label>
Password:
<input formControlName="password" />
</label>
<label>
Email:
<input formControlName="email" />
</label>
<label>
Organization:
<input formControlName="organization" />
</label>
</form>
<button type="button" [disabled]="createAAP.invalid"
(click)="createAAPaccount()">Create account</button>
<h3 class="separator">Login AAP account:</h3>
<form [formGroup]="loginAAP">
<label>
Username:
<input formControlName="username" />
</label>
<label>
Password:
<input formControlName="password" />
</label>
</form>
<button type="button" [disabled]="loginAAP.invalid"
(click)="loginAAPaccount()">AAP login</button>
<h3 class="separator">Change AAP account password:</h3>
<form [formGroup]="changePasswordAAP">
<label>
Username:
<input formControlName="username" />
</label>
<label>
Old password:
<input formControlName="oldPassword" />
</label>
<label>
New password:
<input formControlName="newPassword" />
</label>
</form>
<button type="button" [disabled]="changePasswordAAP.invalid"
(click)="changePasswordAAPaccount()">AAP login</button>
<div *ngIf="(user$ | async) as user; else loggedOut" class="separator">
<h3 class="separator">Refresh token:</h3>
<button type="button" (click)="refresh()">Refresh token
(show new domains)</button>
<h3 class="separator">Create new domain (group/team):</h3>
<form [formGroup]="domain">
<label>
Name:
<input formControlName="domainName" />
</label>
<label>
Description:
<input formControlName="domainDesc" />
</label>
</form>
<button type="button" [disabled]="domain.invalid"
(click)="createDomain(user.uid)">Create
domain</button>
<h3 class="separator">User details:</h3>
<p>Unique ID: {{ user.uid }}</p>
<p>Name: {{ user.name }}</p>
<p>Nickname: {{ user.nickname }}</p>
<p>Email: {{ user.email }}</p>
<p>Token: {{ user.token }}</p>
<h3 class="separator">List of domains:</h3>
<ul>
<li *ngFor="let domain of domains$ | async">
{{ domain }}
</li>
</ul>
<h3 class="separator">List of managed domains:</h3>
<ul>
<li *ngFor="let domain of managedDomains$ | async">
{{ domain.domainName }}: {{ domain.domainDesc }}
<button type="button"
(click)="deleteDomain(domain.domainReference)">Delete
domain</button>
</li>
</ul>
</div>
<h3>Advanced handling of tokens:</h3>
<ng-template #loggedOut>
<h2>Please, log in, either through the federated ELIXIR system or through the
AAP</h2>
</ng-template>
<h3 class="separator">Advanced handling of tokens:</h3>
<div>
<p>Can we present the token to servers? {{ isAuthenticated | async }}</p>
<div *ngIf="(expiration | async) as expiration">
<p>Can we present the token to servers? {{ isAuthenticated$ | async }}</p>
<div *ngIf="(expiration$ | async) as expiration">
<p>Expiration date of the token: {{ expiration }}</p>
</div>
</div>
<ng-template #loggedOut>
<p>Please, log in.</p>
</ng-template>
import {
ComponentFixture,
TestBed,
async
} from '@angular/core/testing';
import {
fakeAsync,
flushMicrotasks
} from '@angular/core/testing';
CommonTestingModule
} from 'testing/common';
import {
AuthService
} from 'src/app/modules/auth/auth.service';
import {
AppComponent
} from './app.component';
import {
CommonStub
} from 'testing/common';
describe('AppComponent', () => {
let auth: AuthService;
let fixture: ComponentFixture<AppComponent>;
let app: AppComponent;
beforeEach(async (() => {
TestBed.configureTestingModule({
imports: [
CommonStub
CommonTestingModule,
],
declarations: [
AppComponent
],
}).compileComponents();
fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
app = fixture.componentInstance;
auth = TestBed.get(AuthService);
}));
it('should create the app', async (() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
}));
it(`should not be authenticated`, async (() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
app.isAuthenticated.subscribe((result: any) => {
fit(`should not be authenticated`, async (() => {
auth.logOut();
app.isAuthenticated$.subscribe(result => {
expect(result).toEqual('Nope');
});
auth.logOut();
}));
it('should render title in a h1 tag', async (() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Auth testing app');
}));
});
......@@ -2,13 +2,32 @@ import {
Component,
OnInit
} from '@angular/core';
import {
FormGroup,
FormBuilder,
Validators
} from '@angular/forms';
import {
HttpClient,
} from '@angular/common/http';
import {
Observable,
of
} from 'rxjs';
import {
map
catchError,
concatMap,
filter,
first,
map,
pluck,
tap
} from 'rxjs/operators';
import {
environment
} from 'src/environments/environment';
import {
AuthService,
User
......@@ -20,40 +39,106 @@ import {
JwtHelperService,
} from '@auth0/angular-jwt';
interface Domain {
domainName: string;
domainDesc: string;
domainReference: string;
}
@Component({
selector: 'auth-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
user: Observable < User | null>;
user$: Observable < User | null > ;
isAuthenticated$: Observable < 'Yes!' | 'Nope' > ;
expiration$: Observable < Date | null > ;
domains$: Observable < string[] > ;
managedDomains$: Observable < Domain[] > ;
isAuthenticated: Observable < string > ;
expiration: Observable < Date | null > ;
// TODO:
// * only trigger logout callbacks if it was previously login
// * display validation messages
// * 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)]}],
});
loginAAP = this._fb.group({
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)]}],
});
domain = this._fb.group({
domainName: ['', Validators.required],
domainDesc: ['']
});
private readonly domainsURL = `${environment.aapURL}/domains`;
private readonly authURL = `${environment.aapURL}/auth`;
constructor(
// Public for demonstration purposes
public auth: AuthService,
private tokens: TokenService,
private jwt: JwtHelperService
private _tokens: TokenService,
// private _jwt: JwtHelperService,
private _fb: FormBuilder,
private _http: HttpClient
) {
this.user = auth.user();
this.user$ = auth.user();
this.isAuthenticated$ = this.user$.pipe(
map(value => value ? 'Yes!' : 'Nope')
);
this.isAuthenticated = this.user.pipe(
map(value => value != null && 'Yes!' || 'Nope')
this.expiration$ = this.user$.pipe(
map(_ => {
try {
return _tokens.getTokenExpirationDate();
} catch (e) {
return null;
}
})
);
this.expiration = this.user.pipe(
/* More complicate version
this.expiration$ = this.user$.pipe(
map(_ => {
const token = this.tokens.getToken();
const token = this._tokens.getToken();
try {
return jwt.getTokenExpirationDate(<string>token);
return _jwt.getTokenExpirationDate( < string > token);
} catch (e) {
return null;
}
})
);
*/
this.domains$ = this.user$.pipe(
map(_ => {
try {
return _tokens.getClaim < string[], string[] > ('domains', []);
} catch (e) {
return [];
}
})
);
this.managedDomains$ = this.user$.pipe(
concatMap(_ => this.listManagedDomains())
);
}