Skip to content
Snippets Groups Projects
Commit 2f19f32d authored by Giuseppe Digilio's avatar Giuseppe Digilio
Browse files

Improvement for authentication module

parent ae584915
Branches
Tags
No related merge requests found
Showing
with 415 additions and 107 deletions
...@@ -12,7 +12,9 @@ import { Store } from '@ngrx/store'; ...@@ -12,7 +12,9 @@ import { Store } from '@ngrx/store';
export class HomePageComponent implements OnInit { export class HomePageComponent implements OnInit {
public isAuthenticated: Observable<boolean>; public isAuthenticated: Observable<boolean>;
constructor(private store: Store<AppState>) {} constructor(private store: Store<AppState>) {
}
ngOnInit() { ngOnInit() {
// set loading // set loading
this.isAuthenticated = this.store.select(isAuthenticated); this.isAuthenticated = this.store.select(isAuthenticated);
......
import { AuthType } from './auth-type'; import { AuthType } from './auth-type';
import { AuthStatus } from './models/auth-status.model';
import { GenericConstructor } from '../shared/generic-constructor'; import { GenericConstructor } from '../shared/generic-constructor';
import { DSpaceObject } from '../shared/dspace-object.model'; import { NormalizedAuthStatus } from './models/normalized-auth-status.model';
import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model';
import { NormalizedEpersonModel } from '../eperson/models/NormalizedEperson.model';
export class AuthObjectFactory { export class AuthObjectFactory {
public static getConstructor(type): GenericConstructor<DSpaceObject> { public static getConstructor(type): GenericConstructor<NormalizedDSpaceObject> {
switch (type) { switch (type) {
case AuthType.Eperson: {
return NormalizedEpersonModel
}
case AuthType.Status: { case AuthType.Status: {
return AuthStatus return NormalizedAuthStatus
} }
default: { default: {
......
...@@ -6,7 +6,7 @@ import { GLOBAL_CONFIG } from '../../../config'; ...@@ -6,7 +6,7 @@ import { GLOBAL_CONFIG } from '../../../config';
import { GlobalConfig } from '../../../config/global-config.interface'; import { GlobalConfig } from '../../../config/global-config.interface';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { isNotEmpty } from '../../shared/empty.util'; import { isNotEmpty } from '../../shared/empty.util';
import { AuthPostRequest, PostRequest, RestRequest } from '../data/request.models'; import { AuthGetRequest, AuthPostRequest, PostRequest, RestRequest } from '../data/request.models';
import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { AuthSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models'; import { AuthSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
...@@ -28,16 +28,16 @@ export class AuthRequestService extends HALEndpointService { ...@@ -28,16 +28,16 @@ export class AuthRequestService extends HALEndpointService {
super(); super();
} }
protected submitRequest(request: RestRequest): Observable<any> { protected fetchRequest(request: RestRequest): Observable<any> {
const [successResponse, errorResponse] = this.responseCache.get(request.href) const [successResponse, errorResponse] = this.responseCache.get(request.href)
.map((entry: ResponseCacheEntry) => entry.response) .map((entry: ResponseCacheEntry) => entry.response)
.partition((response: RestResponse) => response.isSuccessful); .partition((response: RestResponse) => response.isSuccessful);
return Observable.merge( return Observable.merge(
errorResponse.flatMap((response: ErrorResponse) => errorResponse.flatMap((response: ErrorResponse) =>
Observable.throw(new Error(`Couldn't send data to server`))), Observable.throw(new Error(response.errorMessage))),
successResponse successResponse
.filter((response: AuthSuccessResponse) => isNotEmpty(response)) .filter((response: AuthSuccessResponse) => isNotEmpty(response))
.map((response: AuthSuccessResponse) => response.authResponse) .map((response: AuthSuccessResponse) => response.response)
.distinctUntilChanged()); .distinctUntilChanged());
} }
...@@ -51,8 +51,19 @@ export class AuthRequestService extends HALEndpointService { ...@@ -51,8 +51,19 @@ export class AuthRequestService extends HALEndpointService {
.map((endpointURL) => this.getEndpointByMethod(endpointURL, method)) .map((endpointURL) => this.getEndpointByMethod(endpointURL, method))
.distinctUntilChanged() .distinctUntilChanged()
.map((endpointURL: string) => new AuthPostRequest(this.requestService.generateRequestId(), endpointURL, body, options)) .map((endpointURL: string) => new AuthPostRequest(this.requestService.generateRequestId(), endpointURL, body, options))
.do((request: PostRequest) => this.requestService.configure(request)) .do((request: PostRequest) => this.requestService.configure(request, true))
.flatMap((request: PostRequest) => this.submitRequest(request)) .flatMap((request: PostRequest) => this.fetchRequest(request))
.distinctUntilChanged();
}
public getRequest(method: string, options?: HttpOptions): Observable<any> {
return this.getEndpoint()
.filter((href: string) => isNotEmpty(href))
.map((endpointURL) => this.getEndpointByMethod(endpointURL, method))
.distinctUntilChanged()
.map((endpointURL: string) => new AuthGetRequest(this.requestService.generateRequestId(), endpointURL, options))
.do((request: PostRequest) => this.requestService.configure(request, true))
.flatMap((request: PostRequest) => this.fetchRequest(request))
.distinctUntilChanged(); .distinctUntilChanged();
} }
} }
...@@ -3,6 +3,8 @@ import { Inject, Injectable } from '@angular/core'; ...@@ -3,6 +3,8 @@ import { Inject, Injectable } from '@angular/core';
import { AuthObjectFactory } from './auth-object-factory'; import { AuthObjectFactory } from './auth-object-factory';
import { BaseResponseParsingService } from '../data/base-response-parsing.service'; import { BaseResponseParsingService } from '../data/base-response-parsing.service';
import { import {
AuthErrorResponse,
AuthStatusResponse,
AuthSuccessResponse, ConfigSuccessResponse, ErrorResponse, AuthSuccessResponse, ConfigSuccessResponse, ErrorResponse,
RestResponse RestResponse
} from '../cache/response-cache.models'; } from '../cache/response-cache.models';
...@@ -11,10 +13,15 @@ import { ConfigObject } from '../shared/config/config.model'; ...@@ -11,10 +13,15 @@ import { ConfigObject } from '../shared/config/config.model';
import { ConfigType } from '../shared/config/config-type'; import { ConfigType } from '../shared/config/config-type';
import { GLOBAL_CONFIG } from '../../../config'; import { GLOBAL_CONFIG } from '../../../config';
import { GlobalConfig } from '../../../config/global-config.interface'; import { GlobalConfig } from '../../../config/global-config.interface';
import { isNotEmpty } from '../../shared/empty.util'; import { isEmpty, isNotEmpty } from '../../shared/empty.util';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { ResponseParsingService } from '../data/parsing.service'; import { ResponseParsingService } from '../data/parsing.service';
import { RestRequest } from '../data/request.models'; import { RestRequest } from '../data/request.models';
import { AuthType } from './auth-type';
import { NormalizedObject } from '../cache/models/normalized-object.model';
import { AuthTokenInfo } from './models/auth-token-info.model';
import { NormalizedAuthStatus } from './models/normalized-auth-status.model';
import { AuthStatus } from './models/auth-status.model';
@Injectable() @Injectable()
export class AuthResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { export class AuthResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
...@@ -29,19 +36,14 @@ export class AuthResponseParsingService extends BaseResponseParsingService imple ...@@ -29,19 +36,14 @@ export class AuthResponseParsingService extends BaseResponseParsingService imple
} }
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
/*if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && data.statusCode === '200') { if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && data.statusCode === '200') {
const configDefinition = this.process<ConfigObject,ConfigType>(data.payload, request.href); const response = this.process<AuthStatus,AuthType>(data.payload, request.href);
return new ConfigSuccessResponse(configDefinition[Object.keys(configDefinition)[0]], data.statusCode, this.processPageInfo(data.payload.page)); return new AuthStatusResponse(response[Object.keys(response)[0]][0], data.statusCode);
} else if (isEmpty(data.payload) && isNotEmpty(data.headers.get('authorization')) && data.statusCode === '200') {
return new AuthSuccessResponse(new AuthTokenInfo(data.headers.get('authorization')), data.statusCode);
} else { } else {
return new ErrorResponse( return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode);
Object.assign( }
new Error('Unexpected response from config endpoint'),
{statusText: data.statusCode}
)
);
}*/
console.log(data);
return new AuthSuccessResponse(data.payload, data.statusCode)
} }
} }
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
/**
* The auth service.
*/
@Injectable()
export class AuthStorageService {
constructor(@Inject(PLATFORM_ID) private platformId: string) {}
public get(key: string): any {
let item = null;
if (isPlatformBrowser(this.platformId)) {
item = JSON.parse(localStorage.getItem(key));
}
return item;
}
public store(key: string, item: any) {
if (isPlatformBrowser(this.platformId)) {
localStorage.setItem(key, JSON.stringify(item));
}
return true;
}
public remove(key: string) {
if (isPlatformBrowser(this.platformId)) {
localStorage.removeItem(key);
}
return true;
}
}
export enum AuthType { export enum AuthType {
Eperson = 'eperson',
Status = 'status' Status = 'status'
} }
...@@ -6,6 +6,7 @@ import { type } from '../../shared/ngrx/type'; ...@@ -6,6 +6,7 @@ import { type } from '../../shared/ngrx/type';
// import models // import models
import { Eperson } from '../eperson/models/eperson.model'; import { Eperson } from '../eperson/models/eperson.model';
import { AuthTokenInfo } from './models/auth-token-info.model';
export const AuthActionTypes = { export const AuthActionTypes = {
AUTHENTICATE: type('dspace/auth/AUTHENTICATE'), AUTHENTICATE: type('dspace/auth/AUTHENTICATE'),
...@@ -49,9 +50,9 @@ export class AuthenticateAction implements Action { ...@@ -49,9 +50,9 @@ export class AuthenticateAction implements Action {
*/ */
export class AuthenticatedAction implements Action { export class AuthenticatedAction implements Action {
public type: string = AuthActionTypes.AUTHENTICATED; public type: string = AuthActionTypes.AUTHENTICATED;
payload: string; payload: AuthTokenInfo;
constructor(token: string) { constructor(token: AuthTokenInfo) {
this.payload = token; this.payload = token;
} }
} }
...@@ -108,10 +109,10 @@ export class AuthenticationErrorAction implements Action { ...@@ -108,10 +109,10 @@ export class AuthenticationErrorAction implements Action {
*/ */
export class AuthenticationSuccessAction implements Action { export class AuthenticationSuccessAction implements Action {
public type: string = AuthActionTypes.AUTHENTICATE_SUCCESS; public type: string = AuthActionTypes.AUTHENTICATE_SUCCESS;
payload: Eperson; payload: AuthTokenInfo;
constructor(user: Eperson) { constructor(token: AuthTokenInfo) {
this.payload = user; this.payload = token;
} }
} }
......
...@@ -23,23 +23,7 @@ import { ...@@ -23,23 +23,7 @@ import {
RegistrationSuccessAction RegistrationSuccessAction
} from './auth.actions'; } from './auth.actions';
import { Eperson } from '../eperson/models/eperson.model'; import { Eperson } from '../eperson/models/eperson.model';
import { AuthStatus } from './models/auth-status.model';
/**
* Effects offer a way to isolate and easily test side-effects within your
* application.
* The `toPayload` helper function returns just
* the payload of the currently dispatched action, useful in
* instances where the current state is not necessary.
*
* Documentation on `toPayload` can be found here:
* https://github.com/ngrx/effects/blob/master/docs/api.md#topayload
*
* If you are unfamiliar with the operators being used in these examples, please
* check out the sources below:
*
* Official Docs: http://reactivex.io/rxjs/manual/overview.html#categories-of-operators
* RxJS 5 Operators By Example: https://gist.github.com/btroncone/d6cf141d6f2c00dc6b35
*/
@Injectable() @Injectable()
export class AuthEffects { export class AuthEffects {
...@@ -51,18 +35,29 @@ export class AuthEffects { ...@@ -51,18 +35,29 @@ export class AuthEffects {
@Effect() @Effect()
public authenticate: Observable<Action> = this.actions$ public authenticate: Observable<Action> = this.actions$
.ofType(AuthActionTypes.AUTHENTICATE) .ofType(AuthActionTypes.AUTHENTICATE)
.debounceTime(500)
.switchMap((action: AuthenticateAction) => { .switchMap((action: AuthenticateAction) => {
return this.authService.authenticate(action.payload.email, action.payload.password) return this.authService.authenticate(action.payload.email, action.payload.password)
.map((user: Eperson) => new AuthenticationSuccessAction(user)) .map((response: AuthStatus) => new AuthenticationSuccessAction(response.token))
.catch((error) => Observable.of(new AuthenticationErrorAction(error))); .catch((error) => Observable.of(new AuthenticationErrorAction(error)));
}); });
// It means "reacts to this action but don't send another"
@Effect()
public authenticateSuccess: Observable<Action> = this.actions$
.ofType(AuthActionTypes.AUTHENTICATE_SUCCESS)
.do((action: AuthenticationSuccessAction) => this.authService.storeToken(action.payload))
.map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload))
@Effect({dispatch: false})
public logOutSuccess: Observable<Action> = this.actions$
.ofType(AuthActionTypes.LOG_OUT_SUCCESS)
.do((action: LogOutSuccessAction) => this.authService.removeToken());
@Effect() @Effect()
public authenticated: Observable<Action> = this.actions$ public authenticated: Observable<Action> = this.actions$
.ofType(AuthActionTypes.AUTHENTICATED) .ofType(AuthActionTypes.AUTHENTICATED)
.switchMap((action: AuthenticatedAction) => { .switchMap((action: AuthenticatedAction) => {
return this.authService.authenticatedUser() return this.authService.authenticatedUser(action.payload)
.map((user: Eperson) => new AuthenticatedSuccessAction((user !== null), user)) .map((user: Eperson) => new AuthenticatedSuccessAction((user !== null), user))
.catch((error) => Observable.of(new AuthenticatedErrorAction(error))); .catch((error) => Observable.of(new AuthenticatedErrorAction(error)));
}); });
...@@ -70,7 +65,7 @@ export class AuthEffects { ...@@ -70,7 +65,7 @@ export class AuthEffects {
@Effect() @Effect()
public createUser: Observable<Action> = this.actions$ public createUser: Observable<Action> = this.actions$
.ofType(AuthActionTypes.REGISTRATION) .ofType(AuthActionTypes.REGISTRATION)
.debounceTime(500) .debounceTime(500) // to remove when functionality is implemented
.switchMap((action: RegistrationAction) => { .switchMap((action: RegistrationAction) => {
return this.authService.create(action.payload) return this.authService.create(action.payload)
.map((user: Eperson) => new RegistrationSuccessAction(user)) .map((user: Eperson) => new RegistrationSuccessAction(user))
......
import { Injectable, Injector } from '@angular/core';
import { Router } from '@angular/router';
import {
HttpClient, HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse,
HttpErrorResponse
} from '@angular/common/http';
import { Observable } from 'rxjs/Rx';
import 'rxjs/add/observable/throw'
import 'rxjs/add/operator/catch';
import { AuthService } from './auth.service';
import { AuthStatus } from './models/auth-status.model';
import { AuthType } from './auth-type';
import { ResourceType } from '../shared/resource-type';
import { AuthTokenInfo } from './models/auth-token-info.model';
import { isNotEmpty } from '../../shared/empty.util';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private inj: Injector, private router: Router) { }
private isUnauthorized(status: number): boolean {
return status === 401 || status === 403;
}
private isLoginResponse(url: string): boolean {
return url.endsWith('/authn/login');
}
private isLogoutResponse(url: string): boolean {
return url.endsWith('/authn/logout');
}
private makeAuthStatusObject(authenticated:boolean, accessToken?: string, error?: string): AuthStatus {
const authStatus = new AuthStatus();
authStatus.id = null;
authStatus.okay = true;
if (authenticated) {
authStatus.authenticated = true;
authStatus.token = new AuthTokenInfo(accessToken);
} else {
authStatus.authenticated = false;
authStatus.error = JSON.parse(error);
}
return authStatus;
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const authService = this.inj.get(AuthService);
// Get the auth header from the service.
const Authorization = authService.getAuthHeader();
let authReq;
if (isNotEmpty(Authorization)) {
// Clone the request to add the new header.
authReq = req.clone({headers: req.headers.set('authorization', Authorization)});
} else {
authReq = req.clone();
}
// Pass on the cloned request instead of the original request.
return next.handle(authReq)
.map((response) => {
if (response instanceof HttpResponse && response.status === 200 && (this.isLoginResponse(response.url) || this.isLogoutResponse(response.url))) {
let authRes: HttpResponse<any>;
if (this.isLoginResponse(response.url)) {
const token = response.headers.get('authorization');
authRes = response.clone({body: this.makeAuthStatusObject(true, token)});
} else {
authRes = response.clone({body: this.makeAuthStatusObject(false)});
}
return authRes;
} else {
return response;
}
})
.catch((error, caught) => {
// Intercept an unauthorized error response
if (error instanceof HttpErrorResponse && this.isUnauthorized(error.status)) {
// Create a new HttpResponse and return it, so it can be handle properly by AuthService.
const authResponse = new HttpResponse({
body: this.makeAuthStatusObject(false, null, error.error),
headers: error.headers,
status: error.status,
statusText: error.statusText,
url: error.url
});
return Observable.of(authResponse);
} else {
// Return error response as is.
return Observable.throw(error);
}
}) as any;
}
}
...@@ -6,6 +6,7 @@ import { ...@@ -6,6 +6,7 @@ import {
// import models // import models
import { Eperson } from '../eperson/models/eperson.model'; import { Eperson } from '../eperson/models/eperson.model';
import { AuthTokenInfo } from './models/auth-token-info.model';
/** /**
* The auth state. * The auth state.
...@@ -25,6 +26,9 @@ export interface AuthState { ...@@ -25,6 +26,9 @@ export interface AuthState {
// true when loading // true when loading
loading: boolean; loading: boolean;
// access token
token?: AuthTokenInfo;
// the authenticated user // the authenticated user
user?: Eperson; user?: Eperson;
} }
...@@ -62,8 +66,10 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut ...@@ -62,8 +66,10 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
case AuthActionTypes.AUTHENTICATED_SUCCESS: case AuthActionTypes.AUTHENTICATED_SUCCESS:
return Object.assign({}, state, { return Object.assign({}, state, {
authenticated: (action as AuthenticatedSuccessAction).payload.authenticated, authenticated: true,
loaded: true, loaded: true,
error: undefined,
loading: false,
user: (action as AuthenticatedSuccessAction).payload.user user: (action as AuthenticatedSuccessAction).payload.user
}); });
...@@ -76,26 +82,26 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut ...@@ -76,26 +82,26 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
}); });
case AuthActionTypes.AUTHENTICATE_SUCCESS: case AuthActionTypes.AUTHENTICATE_SUCCESS:
case AuthActionTypes.REGISTRATION_SUCCESS: const token: AuthTokenInfo = (action as AuthenticationSuccessAction).payload;
const user: Eperson = (action as AuthenticationSuccessAction).payload;
// verify user is not null // verify token is not null
if (user === null) { if (token === null) {
return state; return state;
} }
return Object.assign({}, state, { return Object.assign({}, state, {
authenticated: true, token: token
error: undefined,
loading: false,
user: user
}); });
case AuthActionTypes.REGISTRATION_SUCCESS:
return state;
case AuthActionTypes.RESET_ERROR: case AuthActionTypes.RESET_ERROR:
return Object.assign({}, state, { return Object.assign({}, state, {
authenticated: null, authenticated: null,
error: undefined,
loaded: false, loaded: false,
loading: false loading: false,
}); });
case AuthActionTypes.LOG_OUT_ERROR: case AuthActionTypes.LOG_OUT_ERROR:
...@@ -109,7 +115,10 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut ...@@ -109,7 +115,10 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
return Object.assign({}, state, { return Object.assign({}, state, {
authenticated: false, authenticated: false,
error: undefined, error: undefined,
user: undefined loaded: false,
loading: false,
user: undefined,
token: undefined
}); });
case AuthActionTypes.REGISTRATION: case AuthActionTypes.REGISTRATION:
......
...@@ -6,6 +6,10 @@ import { Eperson } from '../eperson/models/eperson.model'; ...@@ -6,6 +6,10 @@ import { Eperson } from '../eperson/models/eperson.model';
import { AuthRequestService } from './auth-request.service'; import { AuthRequestService } from './auth-request.service';
import { HttpHeaders } from '@angular/common/http'; import { HttpHeaders } from '@angular/common/http';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { AuthStatus } from './models/auth-status.model';
import { AuthTokenInfo } from './models/auth-token-info.model';
import { isNotEmpty, isNotNull } from '../../shared/empty.util';
import { AuthStorageService } from './auth-storage.service';
export const MOCK_USER = new Eperson(); export const MOCK_USER = new Eperson();
MOCK_USER.id = '92a59227-ccf7-46da-9776-86c3fc64147f'; MOCK_USER.id = '92a59227-ccf7-46da-9776-86c3fc64147f';
...@@ -30,21 +34,28 @@ MOCK_USER.metadata = [ ...@@ -30,21 +34,28 @@ MOCK_USER.metadata = [
} }
]; ];
export const TOKENITEM = 'ds-token'; export const TOKENITEM = 'dsAuthInfo';
/** /**
* The user service. * The auth service.
*/ */
@Injectable() @Injectable()
export class AuthService { export class AuthService {
/** /**
* True if authenticated * True if authenticated
* @type * @type boolean
*/ */
private _authenticated = false; private _authenticated = false;
constructor(private authRequestService: AuthRequestService) {} /**
* The url to redirect after login
* @type string
*/
private _redirectUrl: string;
constructor(private authRequestService: AuthRequestService, private storage: AuthStorageService) {
}
/** /**
* Authenticate the user * Authenticate the user
...@@ -53,32 +64,28 @@ export class AuthService { ...@@ -53,32 +64,28 @@ export class AuthService {
* @param {string} password The user's password * @param {string} password The user's password
* @returns {Observable<User>} The authenticated user observable. * @returns {Observable<User>} The authenticated user observable.
*/ */
public authenticate(user: string, password: string): Observable<Eperson> { public authenticate(user: string, password: string): Observable<AuthStatus> {
// Normally you would do an HTTP request to determine to // Normally you would do an HTTP request to determine to
// attempt authenticating the user using the supplied credentials. // attempt authenticating the user using the supplied credentials.
// const body = `user=${user}&password=${password}`; // const body = `user=${user}&password=${password}`;
// const body = encodeURI('password=test&user=vera.aloe@mailinator.com'); // const body = encodeURI('password=test&user=vera.aloe@mailinator.com');
// const body = [{user}, {password}]; // const body = [{user}, {password}];
const formData: FormData = new FormData(); // const body = encodeURI('password=' + password.toString() + '&user=' + user.toString());
formData.append('user', user); const body = encodeURI(`password=${password}&user=${user}`);
formData.append('password', password);
const body = 'password=' + password.toString() + '&user=' + user.toString();
const options: HttpOptions = Object.create({}); const options: HttpOptions = Object.create({});
let headers = new HttpHeaders(); let headers = new HttpHeaders();
headers = headers.append('Content-Type', 'application/x-www-form-urlencoded'); headers = headers.append('Content-Type', 'application/x-www-form-urlencoded');
headers = headers.append('Accept', 'application/json');
options.headers = headers; options.headers = headers;
options.responseType = 'text'; options.responseType = 'text';
this.authRequestService.postToEndpoint('login', body, options) return this.authRequestService.postToEndpoint('login', body, options)
.subscribe((r) => { .map((status: AuthStatus) => {
console.log(r); if (status.authenticated) {
return status;
} else {
throw(new Error('Invalid email or password'));
}
}) })
if (user === 'test' && password === 'password') {
this._authenticated = true;
return Observable.of(MOCK_USER);
}
return Observable.throw(new Error('Invalid email or password'));
} }
/** /**
...@@ -93,11 +100,25 @@ export class AuthService { ...@@ -93,11 +100,25 @@ export class AuthService {
* Returns the authenticated user * Returns the authenticated user
* @returns {User} * @returns {User}
*/ */
public authenticatedUser(): Observable<Eperson> { public authenticatedUser(token: AuthTokenInfo): Observable<Eperson> {
// Normally you would do an HTTP request to determine if // Normally you would do an HTTP request to determine if
// the user has an existing auth session on the server // the user has an existing auth session on the server
// but, let's just return the mock user for this example. // but, let's just return the mock user for this example.
return Observable.of(MOCK_USER); const options: HttpOptions = Object.create({});
let headers = new HttpHeaders();
headers = headers.append('Accept', 'application/json');
headers = headers.append('Authorization', `Bearer ${token.accessToken}`);
options.headers = headers;
return this.authRequestService.getRequest('status', options)
.map((status: AuthStatus) => {
if (status.authenticated) {
this._authenticated = true;
return status.eperson[0];
} else {
this._authenticated = false;
throw(new Error('Not authenticated'));
}
});
} }
/** /**
...@@ -119,7 +140,47 @@ export class AuthService { ...@@ -119,7 +140,47 @@ export class AuthService {
public signout(): Observable<boolean> { public signout(): Observable<boolean> {
// Normally you would do an HTTP request sign end the session // Normally you would do an HTTP request sign end the session
// but, let's just return an observable of true. // but, let's just return an observable of true.
this._authenticated = false; let headers = new HttpHeaders();
return Observable.of(true); headers = headers.append('Content-Type', 'application/x-www-form-urlencoded');
const options: HttpOptions = Object.create({headers, responseType: 'text'});
return this.authRequestService.getRequest('logout', options)
.map((status: AuthStatus) => {
if (!status.authenticated) {
this._authenticated = false;
return true;
} else {
throw(new Error('Invalid email or password'));
}
})
}
public getAuthHeader(): string {
// Retrieve authentication token info
const token = this.storage.get(TOKENITEM);
return (isNotNull(token) && this._authenticated) ? `Bearer ${token.accessToken}` : '';
}
public getToken(): AuthTokenInfo {
// Retrieve authentication token info
return this.storage.get(TOKENITEM);
}
public storeToken(token: AuthTokenInfo) {
// Save authentication token info
return this.storage.store(TOKENITEM, JSON.stringify(token));
}
public removeToken() {
// Remove authentication token info
return this.storage.remove(TOKENITEM);
}
get redirectUrl(): string {
return this._redirectUrl;
}
set redirectUrl(value: string) {
this._redirectUrl = value;
} }
} }
...@@ -7,6 +7,7 @@ import { Store } from '@ngrx/store'; ...@@ -7,6 +7,7 @@ import { Store } from '@ngrx/store';
// reducers // reducers
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { isAuthenticated } from './selectors'; import { isAuthenticated } from './selectors';
import { AuthService } from './auth.service';
/** /**
* Prevent unauthorized activating and loading of routes * Prevent unauthorized activating and loading of routes
...@@ -18,37 +19,44 @@ export class AuthenticatedGuard implements CanActivate, CanLoad { ...@@ -18,37 +19,44 @@ export class AuthenticatedGuard implements CanActivate, CanLoad {
/** /**
* @constructor * @constructor
*/ */
constructor(private router: Router, private store: Store<CoreState>) {} constructor(private authService: AuthService, private router: Router, private store: Store<CoreState>) {}
/** /**
* True when user is authenticated * True when user is authenticated
* @method canActivate * @method canActivate
*/ */
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean { canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
// get observable const url = state.url;
const observable = this.store.select(isAuthenticated);
// redirect to sign in page if user is not authenticated return this.handleAuth(url);
observable.subscribe((authenticated) => { }
if (!authenticated) {
this.router.navigate(['/login']);
}
});
return observable; /**
* True when user is authenticated
* @method canActivateChild
*/
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return this.canActivate(route, state);
} }
/** /**
* True when user is authenticated * True when user is authenticated
* @method canLoad * @method canLoad
*/ */
canLoad(route: Route): Observable<boolean> | Promise<boolean> | boolean { canLoad(route: Route): Observable<boolean> {
const url = `/${route.path}`;
return this.handleAuth(url);
}
private handleAuth(url: string): Observable<boolean> {
// get observable // get observable
const observable = this.store.select(isAuthenticated); const observable = this.store.select(isAuthenticated);
// redirect to sign in page if user is not authenticated // redirect to sign in page if user is not authenticated
observable.subscribe((authenticated) => { observable.subscribe((authenticated) => {
if (!authenticated) { if (!authenticated) {
this.authService.redirectUrl = url;
this.router.navigate(['/login']); this.router.navigate(['/login']);
} }
}); });
......
export interface AuthError {
error: string,
message: string,
path: string,
status: number
timestamp: number
}
export interface AuthInfo {
access_token?: string,
expires?: number,
expires_in?: number
}
import { AuthError } from './auth-error.model';
import { AuthTokenInfo } from './auth-token-info.model';
import { DSpaceObject } from '../../shared/dspace-object.model'; import { DSpaceObject } from '../../shared/dspace-object.model';
import { Eperson } from '../../eperson/models/eperson.model';
export class AuthStatus extends DSpaceObject { export class AuthStatus extends DSpaceObject {
...@@ -6,4 +9,9 @@ export class AuthStatus extends DSpaceObject { ...@@ -6,4 +9,9 @@ export class AuthStatus extends DSpaceObject {
authenticated: boolean; authenticated: boolean;
error?: AuthError;
eperson: Eperson[];
token?: AuthTokenInfo
} }
export class AuthTokenInfo {
public accessToken: string;
public expires?: number;
constructor(token: string, expiresIn?: number) {
this.accessToken = token.replace('Bearer ', '');
if (expiresIn) {
this.expires = expiresIn * 1000 + Date.now();
}
}
}
import { AuthStatus } from './auth-status.model';
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
import { mapsTo } from '../../cache/builders/build-decorators';
import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
import { Eperson } from '../../eperson/models/eperson.model';
@mapsTo(AuthStatus)
@inheritSerialization(NormalizedDSpaceObject)
export class NormalizedAuthStatus extends NormalizedDSpaceObject {
/**
* True if REST API is up and running, should never return false
*/
@autoserialize
okay: boolean;
/**
* True if the token is valid, false if there was no token or the token wasn't valid
*/
@autoserialize
authenticated: boolean;
@autoserializeAs(Eperson)
eperson: Eperson[];
}
...@@ -2,12 +2,16 @@ import { RequestError } from '../data/request.models'; ...@@ -2,12 +2,16 @@ import { RequestError } from '../data/request.models';
import { PageInfo } from '../shared/page-info.model'; import { PageInfo } from '../shared/page-info.model';
import { BrowseDefinition } from '../shared/browse-definition.model'; import { BrowseDefinition } from '../shared/browse-definition.model';
import { ConfigObject } from '../shared/config/config.model'; import { ConfigObject } from '../shared/config/config.model';
import { AuthTokenInfo } from '../auth/models/auth-token-info.model';
import { NormalizedAuthStatus } from '../auth/models/normalized-auth-status.model';
import { AuthStatus } from '../auth/models/auth-status.model';
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */
export class RestResponse { export class RestResponse {
public toCache = true;
constructor( constructor(
public isSuccessful: boolean, public isSuccessful: boolean,
public statusCode: string public statusCode: string,
) { } ) { }
} }
...@@ -63,11 +67,31 @@ export class ConfigSuccessResponse extends RestResponse { ...@@ -63,11 +67,31 @@ export class ConfigSuccessResponse extends RestResponse {
} }
} }
export class AuthStatusResponse extends RestResponse {
public toCache = false;
constructor(
public response: AuthStatus,
public statusCode: string
) {
super(true, statusCode);
}
}
export class AuthSuccessResponse extends RestResponse { export class AuthSuccessResponse extends RestResponse {
public toCache = false;
constructor( constructor(
public authResponse: any, public response: AuthTokenInfo,
public statusCode: string
) {
super(true, statusCode);
}
}
export class AuthErrorResponse extends RestResponse {
public toCache = false;
constructor(
public response: any,
public statusCode: string, public statusCode: string,
public pageInfo?: PageInfo
) { ) {
super(true, statusCode); super(true, statusCode);
} }
......
...@@ -41,6 +41,10 @@ import { UUIDService } from './shared/uuid.service'; ...@@ -41,6 +41,10 @@ import { UUIDService } from './shared/uuid.service';
import { AuthService } from './auth/auth.service'; import { AuthService } from './auth/auth.service';
import { AuthenticatedGuard } from './auth/authenticated.guard'; import { AuthenticatedGuard } from './auth/authenticated.guard';
import { AuthRequestService } from './auth/auth-request.service'; import { AuthRequestService } from './auth/auth-request.service';
import { AuthResponseParsingService } from './auth/auth-response-parsing.service';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthInterceptor } from './auth/auth.interceptor';
import { AuthStorageService } from './auth/auth-storage.service';
const IMPORTS = [ const IMPORTS = [
CommonModule, CommonModule,
...@@ -60,7 +64,9 @@ const PROVIDERS = [ ...@@ -60,7 +64,9 @@ const PROVIDERS = [
ApiService, ApiService,
AuthenticatedGuard, AuthenticatedGuard,
AuthRequestService, AuthRequestService,
AuthResponseParsingService,
AuthService, AuthService,
AuthStorageService,
CommunityDataService, CommunityDataService,
CollectionDataService, CollectionDataService,
DSOResponseParsingService, DSOResponseParsingService,
...@@ -83,7 +89,13 @@ const PROVIDERS = [ ...@@ -83,7 +89,13 @@ const PROVIDERS = [
SubmissionFormsConfigService, SubmissionFormsConfigService,
SubmissionSectionsConfigService, SubmissionSectionsConfigService,
UUIDService, UUIDService,
{ provide: NativeWindowService, useFactory: NativeWindowFactory } { provide: NativeWindowService, useFactory: NativeWindowFactory },
// register TokenInterceptor as HttpInterceptor
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
]; ];
@NgModule({ @NgModule({
......
...@@ -193,8 +193,8 @@ export class AuthPostRequest extends PostRequest { ...@@ -193,8 +193,8 @@ export class AuthPostRequest extends PostRequest {
} }
export class AuthGetRequest extends GetRequest { export class AuthGetRequest extends GetRequest {
constructor(uuid: string, href: string) { constructor(uuid: string, href: string, public options?: HttpOptions) {
super(uuid, href); super(uuid, href, null, options);
} }
getResponseParser(): GenericConstructor<ResponseParsingService> { getResponseParser(): GenericConstructor<ResponseParsingService> {
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment