Improvement for authentication module

This commit is contained in:
Giuseppe Digilio
2018-02-09 09:55:55 +01:00
parent ae584915cf
commit 2f19f32d91
23 changed files with 431 additions and 124 deletions

View File

@@ -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);

View File

@@ -1,14 +1,18 @@
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: {

View File

@@ -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 {
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 {
.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();
} }
} }

View File

@@ -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';
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
} }
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)
} }
} }

View File

@@ -0,0 +1,34 @@
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;
}
}

View File

@@ -1,3 +1,4 @@
export enum AuthType { export enum AuthType {
Eperson = 'eperson',
Status = 'status' Status = 'status'
} }

View File

@@ -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 {
*/ */
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 {
*/ */
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;
} }
} }

View File

@@ -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 {
@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 {
@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))

View File

@@ -0,0 +1,97 @@
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;
}
}

View File

@@ -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 {
// 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
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
}); });
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
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:

View File

@@ -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 = [
} }
]; ];
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 {
* @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 {
* 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 {
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;
} }
} }

View File

@@ -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 {
/** /**
* @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']);
} }
}); });

View File

@@ -0,0 +1,7 @@
export interface AuthError {
error: string,
message: string,
path: string,
status: number
timestamp: number
}

View File

@@ -1,5 +0,0 @@
export interface AuthInfo {
access_token?: string,
expires?: number,
expires_in?: number
}

View File

@@ -1,4 +1,7 @@
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 {
authenticated: boolean; authenticated: boolean;
error?: AuthError;
eperson: Eperson[];
token?: AuthTokenInfo
} }

View File

@@ -0,0 +1,11 @@
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();
}
}
}

View File

@@ -0,0 +1,26 @@
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[];
}

View File

@@ -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 {
} }
} }
export class AuthSuccessResponse extends RestResponse { export class AuthStatusResponse extends RestResponse {
public toCache = false;
constructor( constructor(
public authResponse: any, public response: AuthStatus,
public statusCode: string
) {
super(true, statusCode);
}
}
export class AuthSuccessResponse extends RestResponse {
public toCache = false;
constructor(
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);
} }

View File

@@ -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 = [
ApiService, ApiService,
AuthenticatedGuard, AuthenticatedGuard,
AuthRequestService, AuthRequestService,
AuthResponseParsingService,
AuthService, AuthService,
AuthStorageService,
CommunityDataService, CommunityDataService,
CollectionDataService, CollectionDataService,
DSOResponseParsingService, DSOResponseParsingService,
@@ -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({

View File

@@ -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> {

View File

@@ -17,6 +17,7 @@ import { RequestConfigureAction, RequestExecuteAction } from './request.actions'
import { GetRequest, RestRequest, RestRequestMethod } from './request.models'; import { GetRequest, RestRequest, RestRequestMethod } from './request.models';
import { RequestEntry, RequestState } from './request.reducer'; import { RequestEntry, RequestState } from './request.reducer';
import { ResponseCacheRemoveAction } from '../cache/response-cache.actions';
@Injectable() @Injectable()
export class RequestService { export class RequestService {
@@ -66,9 +67,9 @@ export class RequestService {
.flatMap((uuid: string) => this.getByUUID(uuid)); .flatMap((uuid: string) => this.getByUUID(uuid));
} }
configure<T extends CacheableObject>(request: RestRequest): void { configure<T extends CacheableObject>(request: RestRequest, overrideRequest: boolean = false): void {
if (request.method !== RestRequestMethod.Get || !this.isCachedOrPending(request)) { if (request.method !== RestRequestMethod.Get || !this.isCachedOrPending(request) || overrideRequest) {
this.dispatchRequest(request); this.dispatchRequest(request, overrideRequest);
} }
} }
@@ -101,10 +102,10 @@ export class RequestService {
return isCached || isPending; return isCached || isPending;
} }
private dispatchRequest(request: RestRequest) { private dispatchRequest(request: RestRequest, overrideRequest: boolean) {
this.store.dispatch(new RequestConfigureAction(request)); this.store.dispatch(new RequestConfigureAction(request));
this.store.dispatch(new RequestExecuteAction(request.uuid)); this.store.dispatch(new RequestExecuteAction(request.uuid));
if (request.method === RestRequestMethod.Get) { if (request.method === RestRequestMethod.Get && !overrideRequest) {
this.trackRequestsOnTheirWayToTheStore(request); this.trackRequestsOnTheirWayToTheStore(request);
} }
} }

View File

@@ -1,3 +1,5 @@
import { HttpHeaders } from '@angular/common/http';
export interface DSpaceRESTV2Response { export interface DSpaceRESTV2Response {
payload: { payload: {
[name: string]: any; [name: string]: any;
@@ -5,5 +7,6 @@ export interface DSpaceRESTV2Response {
_links?: any; _links?: any;
page?: any; page?: any;
}, },
headers: HttpHeaders,
statusCode: string statusCode: string
} }

View File

@@ -61,21 +61,16 @@ export class DSpaceRESTv2Service {
requestOptions.body = body; requestOptions.body = body;
requestOptions.observe = 'response'; requestOptions.observe = 'response';
if (options && options.headers) { if (options && options.headers) {
let headers = new HttpHeaders(); requestOptions.headers = Object.assign(new HttpHeaders(), options.headers);
headers = headers.append('Accept', 'application/json');
headers = headers.append('Content-Type', 'application/x-www-form-urlencoded');
// requestOptions.headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded');
requestOptions.headers = headers;
/* const keys = options.headers.getAll('');
keys.forEach((key) => {
requestOptions.headers.append(key, options.headers.get(key));
})*/
} }
if (options && options.responseType) { if (options && options.responseType) {
// requestOptions.responseType = options.responseType; requestOptions.responseType = options.responseType;
} }
return this.http.request(method, url, requestOptions) return this.http.request(method, url, requestOptions)
.map((res) => ({ payload: res.body, statusCode: res.statusText })) .map((res) => {
console.log(res);
return ({ payload: res.body, headers: res.headers, statusCode: res.statusText })
})
.catch((err) => { .catch((err) => {
console.log('Error: ', err); console.log('Error: ', err);
return Observable.throw(err); return Observable.throw(err);