From 1635949c443baeadcb0e61fa9abaa65ba081ebc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Tue, 7 Sep 2021 17:19:16 +0200 Subject: [PATCH] Add token query param for private download file --- client/src/app/auth/auth.actions.ts | 1 + client/src/app/auth/auth.effects.ts | 8 +++++--- client/src/app/auth/auth.reducer.ts | 9 ++++++++- client/src/app/auth/auth.selector.ts | 5 +++++ .../components/result/datatable-tab.component.html | 1 + .../components/result/datatable-tab.component.ts | 1 + .../search/containers/result.component.html | 1 + .../instance/search/containers/result.component.ts | 3 +++ .../components/datatable/datatable.component.html | 2 ++ .../components/datatable/datatable.component.ts | 1 + .../renderer/download-renderer.component.ts | 8 +++++++- conf-dev/public_key | 2 +- server/src/Middleware/AuthorizationMiddleware.php | 13 +++++++++---- 13 files changed, 45 insertions(+), 10 deletions(-) diff --git a/client/src/app/auth/auth.actions.ts b/client/src/app/auth/auth.actions.ts index 52da0604..4103bceb 100644 --- a/client/src/app/auth/auth.actions.ts +++ b/client/src/app/auth/auth.actions.ts @@ -16,4 +16,5 @@ export const logout = createAction('[Auth] Logout'); export const authSuccess = createAction('[Auth] Auth Success'); export const loadUserProfileSuccess = createAction('[Auth] Load User Profile Success', props<{ userProfile: UserProfile }>()); export const loadUserRoleSuccess = createAction('[Auth] Load User Roles Success', props<{ userRoles: string[] }>()); +export const loadTokenSuccess = createAction('[Auth] Load Token Success', props<{ token: string }>()); export const openEditProfile = createAction('[Auth] Edit Profile'); diff --git a/client/src/app/auth/auth.effects.ts b/client/src/app/auth/auth.effects.ts index 69cefb0e..a663b0a0 100644 --- a/client/src/app/auth/auth.effects.ts +++ b/client/src/app/auth/auth.effects.ts @@ -10,7 +10,7 @@ import { Injectable } from '@angular/core'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { from } from 'rxjs'; -import { tap, switchMap } from 'rxjs/operators'; +import { tap, switchMap, withLatestFrom } from 'rxjs/operators'; import { KeycloakService } from 'keycloak-angular'; @@ -49,9 +49,11 @@ export class AuthEffects { ofType(authActions.authSuccess), switchMap(() => from(this.keycloak.loadUserProfile()) .pipe( - switchMap(userProfile => [ + withLatestFrom(this.keycloak.getToken()), + switchMap(([userProfile, token]) => [ authActions.loadUserProfileSuccess({ userProfile }), - authActions.loadUserRoleSuccess({ userRoles: this.keycloak.getUserRoles() }) + authActions.loadUserRoleSuccess({ userRoles: this.keycloak.getUserRoles() }), + authActions.loadTokenSuccess({ token }) ]) ) ) diff --git a/client/src/app/auth/auth.reducer.ts b/client/src/app/auth/auth.reducer.ts index d0805aff..d181f446 100644 --- a/client/src/app/auth/auth.reducer.ts +++ b/client/src/app/auth/auth.reducer.ts @@ -16,12 +16,14 @@ export interface State { isAuthenticated: boolean; userProfile: UserProfile; userRoles: string[]; + token: string; } export const initialState: State = { isAuthenticated: false, userProfile: null, - userRoles: [] + userRoles: [], + token: null }; export const authReducer = createReducer( @@ -37,9 +39,14 @@ export const authReducer = createReducer( on(authActions.loadUserRoleSuccess, (state, { userRoles }) => ({ ...state, userRoles + })), + on(authActions.loadTokenSuccess, (state, { token }) => ({ + ...state, + token })) ); export const selectIsAuthenticated = (state: State) => state.isAuthenticated; export const selectUserProfile = (state: State) => state.userProfile; export const selectUserRoles = (state: State) => state.userRoles; +export const selectToken = (state: State) => state.token; diff --git a/client/src/app/auth/auth.selector.ts b/client/src/app/auth/auth.selector.ts index 8b0e4f73..3822a905 100644 --- a/client/src/app/auth/auth.selector.ts +++ b/client/src/app/auth/auth.selector.ts @@ -27,3 +27,8 @@ export const selectUserRoles = createSelector( selectAuthState, fromAuth.selectUserRoles ); + +export const selectToken = createSelector( + selectAuthState, + fromAuth.selectToken +); diff --git a/client/src/app/instance/search/components/result/datatable-tab.component.html b/client/src/app/instance/search/components/result/datatable-tab.component.html index f9f6b3aa..f854be32 100644 --- a/client/src/app/instance/search/components/result/datatable-tab.component.html +++ b/client/src/app/instance/search/components/result/datatable-tab.component.html @@ -19,6 +19,7 @@ [dataIsLoading]="dataIsLoading" [dataIsLoaded]="dataIsLoaded" [selectedData]="selectedData" + [token]="token" (retrieveData)="retrieveData.emit($event)" (addSelectedData)="addSelectedData.emit($event)" (deleteSelectedData)="deleteSelectedData.emit($event)"> diff --git a/client/src/app/instance/search/components/result/datatable-tab.component.ts b/client/src/app/instance/search/components/result/datatable-tab.component.ts index a276dec4..51d18381 100644 --- a/client/src/app/instance/search/components/result/datatable-tab.component.ts +++ b/client/src/app/instance/search/components/result/datatable-tab.component.ts @@ -33,6 +33,7 @@ export class DatatableTabComponent { @Input() dataIsLoading: boolean; @Input() dataIsLoaded: boolean; @Input() selectedData: any[]; + @Input() token: string; @Output() retrieveData: EventEmitter<Pagination> = new EventEmitter(); @Output() addSelectedData: EventEmitter<number | string> = new EventEmitter(); @Output() deleteSelectedData: EventEmitter<number | string> = new EventEmitter(); diff --git a/client/src/app/instance/search/containers/result.component.html b/client/src/app/instance/search/containers/result.component.html index d9b4fdbf..a350d6ef 100644 --- a/client/src/app/instance/search/containers/result.component.html +++ b/client/src/app/instance/search/containers/result.component.html @@ -82,6 +82,7 @@ [dataIsLoading]="dataIsLoading | async" [dataIsLoaded]="dataIsLoaded | async" [selectedData]="selectedData | async" + [token]="token | async" (retrieveData)="retrieveData($event)" (addSelectedData)="addSearchData($event)" (deleteSelectedData)="deleteSearchData($event)"> diff --git a/client/src/app/instance/search/containers/result.component.ts b/client/src/app/instance/search/containers/result.component.ts index f62ca538..16a12784 100644 --- a/client/src/app/instance/search/containers/result.component.ts +++ b/client/src/app/instance/search/containers/result.component.ts @@ -20,6 +20,7 @@ import * as searchActions from '../../store/actions/search.actions'; import * as searchSelector from '../../store/selectors/search.selector'; import * as sampActions from '../../store/actions/samp.actions'; import * as sampSelector from '../../store/selectors/samp.selector'; +import * as authSelector from 'src/app/auth/auth.selector'; @Component({ selector: 'app-result', @@ -42,6 +43,7 @@ export class ResultComponent extends AbstractSearchComponent { public dataIsLoaded: Observable<boolean>; public selectedData: Observable<any>; public sampRegistered: Observable<boolean>; + public token: Observable<string>; private pristineSubscription: Subscription; @@ -56,6 +58,7 @@ export class ResultComponent extends AbstractSearchComponent { this.dataIsLoaded = this.store.select(searchSelector.selectDataIsLoaded); this.selectedData = this.store.select(searchSelector.selectSelectedData); this.sampRegistered = this.store.select(sampSelector.selectRegistered); + this.token = this.store.select(authSelector.selectToken); } ngOnInit() { diff --git a/client/src/app/instance/shared-search/components/datatable/datatable.component.html b/client/src/app/instance/shared-search/components/datatable/datatable.component.html index 7b7ee23a..f97a2bb2 100644 --- a/client/src/app/instance/shared-search/components/datatable/datatable.component.html +++ b/client/src/app/instance/shared-search/components/datatable/datatable.component.html @@ -61,6 +61,8 @@ <app-download-renderer [value]="datum[attribute.label]" [datasetName]="dataset.name" + [datasetPublic]="dataset.public" + [token]="token" [config]="getRendererConfig(attribute)"> </app-download-renderer> </div> diff --git a/client/src/app/instance/shared-search/components/datatable/datatable.component.ts b/client/src/app/instance/shared-search/components/datatable/datatable.component.ts index 1e90851c..83c362b7 100644 --- a/client/src/app/instance/shared-search/components/datatable/datatable.component.ts +++ b/client/src/app/instance/shared-search/components/datatable/datatable.component.ts @@ -34,6 +34,7 @@ export class DatatableComponent implements OnInit { @Input() dataIsLoading: boolean; @Input() dataIsLoaded: boolean; @Input() selectedData: any[] = []; + @Input() token: string; @Output() retrieveData: EventEmitter<Pagination> = new EventEmitter(); @Output() addSelectedData: EventEmitter<number | string> = new EventEmitter(); @Output() deleteSelectedData: EventEmitter<number | string> = new EventEmitter(); diff --git a/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.ts b/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.ts index 19bcc385..40b49a0b 100644 --- a/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.ts +++ b/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.ts @@ -25,6 +25,8 @@ import { AppConfigService } from 'src/app/app-config.service'; export class DownloadRendererComponent { @Input() value: string; @Input() datasetName: string; + @Input() datasetPublic: boolean; + @Input() token: string; @Input() config: DownloadRendererConfig; constructor(private appConfig: AppConfigService) { } @@ -35,7 +37,11 @@ export class DownloadRendererComponent { * @return string */ getHref(): string { - return getHost(this.appConfig.apiUrl) + '/download-file/' + this.datasetName + '/' + this.value; + let href = getHost(this.appConfig.apiUrl) + '/download-file/' + this.datasetName + '/' + this.value; + if (!this.datasetPublic && this.token) { + href += '?token=' + this.token; + } + return href; } /** diff --git a/conf-dev/public_key b/conf-dev/public_key index 7a109409..6d9fd24a 100644 --- a/conf-dev/public_key +++ b/conf-dev/public_key @@ -1,3 +1,3 @@ -----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/mnnVnoX4yuiq1AXklR//PCcNOLibWzoRcyhGQQ+Rfokcxny0uBrVmFb7xNAxgqaf3hyo0789ONUf41xQF8CO0+YdLmOfQDRegohZP4mRvppCQUozroKphGAvYt90xaFJL/IttXeRKmUk6Noc/3V0oUA+1P8SbdpIq8s+L9yXfqWmW+mjlMAU31/3gVM/Q/dTmKFnkVlwprhy4OoGzHlNaFElIjFOEICL6M3ANTBLoZwDHiwvxqXuyA9IA8LeHuVpxWmaShGA0UxA99cF3edCmkc6R4d1R7rWP0XjnjYEOYDcVhJZJlRamNuYtgnr82sgr/bJahHFxAnF3GlBHs+gwIDAQAB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiXxjO2Z+Mnc/L4fcY7oLIvSQXrkADSUQVnAXqfcMzfuNH/clLBMmXIHoPoIOYImIbXFMNaBAFLMWqURSaz2LDEQy5dIfllU9e3kIAV0mJeafjaN3QdxKX8TcJAnrTeQ7soooeZzen7kfPWxfjAaiEtbxo5h2xW4qup+VvADQg15C3dwBS5VV2Lc41z4prQubQNs2WT4IOVfYYhtpp+R/3IuCAFS6qfmAuBUIu8gSq/VpCqJ5Sm/YdmBZDwhyBRYOKJJlkqlNFdGPnHAkBDOL5LeU54cXCBMxPEeomtu22Kv1PIF4w2kfwUie9Qq06yWsTDwyrSAjvQ3xXHAcuAqPRQIDAQAB -----END PUBLIC KEY----- diff --git a/server/src/Middleware/AuthorizationMiddleware.php b/server/src/Middleware/AuthorizationMiddleware.php index d03a3f45..82b5b4e0 100644 --- a/server/src/Middleware/AuthorizationMiddleware.php +++ b/server/src/Middleware/AuthorizationMiddleware.php @@ -57,15 +57,20 @@ final class AuthorizationMiddleware implements MiddlewareInterface { if ( $request->getMethod() === OPTIONS - || !$request->hasHeader('Authorization') + || (!$request->hasHeader('Authorization') && !array_key_exists('token', $request->getQueryParams())) || !boolval($this->settings['enabled']) ) { return $handler->handle($request); } // Get token string from Authorizarion header - $bearer = $request->getHeader('Authorization'); - $data = explode(' ', $bearer[0]); + if ($request->hasHeader('Authorization')) { + $bearer = $request->getHeader('Authorization')[0]; + } else { + $bearer = 'Bearer ' . $request->getQueryParams()['token']; + } + + $data = explode(' ', $bearer); if ($data[0] !== 'Bearer') { return $this->getUnauthorizedResponse( 'HTTP 401: Authorization must contain a string with the following format -> Bearer JWT' @@ -89,7 +94,7 @@ final class AuthorizationMiddleware implements MiddlewareInterface * * @return Response */ - private function getUnauthorizedResponse(string $message): Response + private function getUnauthorizedResponse(string $message): NyholmResponse { $resonse = new NyholmResponse(); $resonse->getBody()->write(json_encode(array( -- GitLab