Skip to content

Commit

Permalink
[CST-10704][CST-14902] Adds ORCID login flow with private email
Browse files Browse the repository at this point in the history
  • Loading branch information
alisaismailati authored and vins01-4science committed Sep 13, 2024
1 parent 11fad8d commit 590fe70
Show file tree
Hide file tree
Showing 88 changed files with 3,372 additions and 61 deletions.
17 changes: 16 additions & 1 deletion src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NgModule } from '@angular/core';
import { RouterModule, NoPreloading } from '@angular/router';
import { NoPreloading, RouterModule } from '@angular/router';
import { AuthBlockingGuard } from './core/auth/auth-blocking.guard';

import { AuthenticatedGuard } from './core/auth/authenticated.guard';
Expand Down Expand Up @@ -161,6 +161,21 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone
loadChildren: () => import('./login-page/login-page.module')
.then((m) => m.LoginPageModule)
},
{
path: 'external-login/:token',
loadChildren: () => import('./external-login-page/external-login-page.module')
.then((m) => m.ExternalLoginPageModule)
},
{
path: 'review-account/:token',
loadChildren: () => import('./external-login-review-account-info-page/external-login-review-account-info-page.module')
.then((m) => m.ExternalLoginReviewAccountInfoModule)
},
{
path: 'email-confirmation',
loadChildren: () => import('./external-login-email-confirmation-page/external-login-email-confirmation-page.module')
.then((m) => m.ExternalLoginEmailConfirmationPageModule)
},
{
path: 'logout',
loadChildren: () => import('./logout-page/logout-page.module')
Expand Down
38 changes: 35 additions & 3 deletions src/app/core/auth/auth-request.service.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Observable } from 'rxjs';
import { distinctUntilChanged, filter, map, switchMap, tap, take } from 'rxjs/operators';
import { distinctUntilChanged, filter, map, switchMap, take, tap } from 'rxjs/operators';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RequestService } from '../data/request.service';
import { isNotEmpty } from '../../shared/empty.util';
import { GetRequest, PostRequest, } from '../data/request.models';
import { isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
import { DeleteRequest, GetRequest, PostRequest } from '../data/request.models';
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { getFirstCompletedRemoteData } from '../shared/operators';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
Expand All @@ -13,13 +13,17 @@ import { ShortLivedToken } from './models/short-lived-token.model';
import { URLCombiner } from '../url-combiner/url-combiner';
import { RestRequest } from '../data/rest-request.model';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { MachineToken } from './models/machine-token.model';
import { NoContent } from '../shared/NoContent.model';
import { sendRequest } from '../shared/request.operators';

/**
* Abstract service to send authentication requests
*/
export abstract class AuthRequestService {
protected linkName = 'authn';
protected shortlivedtokensEndpoint = 'shortlivedtokens';
protected machinetokenEndpoint = 'machinetokens';

constructor(protected halService: HALEndpointService,
protected requestService: RequestService,
Expand Down Expand Up @@ -128,4 +132,32 @@ export abstract class AuthRequestService {
})
);
}

/**
* Send a post request to create a machine token
*/
public postToMachineTokenEndpoint(): Observable<RemoteData<MachineToken>> {
return this.halService.getEndpoint(this.linkName).pipe(

Check warning on line 140 in src/app/core/auth/auth-request.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/auth/auth-request.service.ts#L140

Added line #L140 was not covered by tests
isNotEmptyOperator(),
distinctUntilChanged(),
map((href: string) => new URLCombiner(href, this.machinetokenEndpoint).toString()),
map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL)),
tap((request: RestRequest) => this.requestService.send(request)),
switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID<MachineToken>(request.uuid))

Check warning on line 146 in src/app/core/auth/auth-request.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/auth/auth-request.service.ts#L143-L146

Added lines #L143 - L146 were not covered by tests
);
}

/**
* Send a delete request to destroy a machine token
*/
public deleteToMachineTokenEndpoint(): Observable<RemoteData<NoContent>> {
return this.halService.getEndpoint(this.linkName).pipe(

Check warning on line 154 in src/app/core/auth/auth-request.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/auth/auth-request.service.ts#L154

Added line #L154 was not covered by tests
isNotEmptyOperator(),
distinctUntilChanged(),
map((href: string) => new URLCombiner(href, this.machinetokenEndpoint).toString()),
map((endpointURL: string) => new DeleteRequest(this.requestService.generateRequestId(), endpointURL)),

Check warning on line 158 in src/app/core/auth/auth-request.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/auth/auth-request.service.ts#L157-L158

Added lines #L157 - L158 were not covered by tests
sendRequest(this.requestService),
switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID<MachineToken>(request.uuid)),

Check warning on line 160 in src/app/core/auth/auth-request.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/auth/auth-request.service.ts#L160

Added line #L160 was not covered by tests
);
}
}
48 changes: 46 additions & 2 deletions src/app/core/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import {
import { CookieService } from '../services/cookie.service';
import {
getAuthenticatedUserId,
getAuthenticationToken, getExternalAuthCookieStatus,
getAuthenticationToken,
getExternalAuthCookieStatus,
getRedirectUrl,
isAuthenticated,
isAuthenticatedLoaded,
Expand All @@ -36,7 +37,8 @@ import { AppState } from '../../app.reducer';
import {
CheckAuthenticationTokenAction,
RefreshTokenAction,
ResetAuthenticationMessagesAction, SetAuthCookieStatus,
ResetAuthenticationMessagesAction,
SetAuthCookieStatus,
SetRedirectUrlAction,
SetUserAsIdleAction,
UnsetUserAsIdleAction
Expand All @@ -56,6 +58,9 @@ import { Group } from '../eperson/models/group.model';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { PageInfo } from '../shared/page-info.model';
import { followLink } from '../../shared/utils/follow-link-config.model';
import { NoContent } from '../shared/NoContent.model';
import { URLCombiner } from '../url-combiner/url-combiner';
import { MachineToken } from './models/machine-token.model';

export const LOGIN_ROUTE = '/login';
export const LOGOUT_ROUTE = '/logout';
Expand Down Expand Up @@ -548,6 +553,31 @@ export class AuthService {
});
}

/**
* Returns the external server redirect URL.
* @param origin - The origin route.
* @param redirectRoute - The redirect route.
* @param location - The location.
* @returns The external server redirect URL.
*/
getExternalServerRedirectUrl(origin: string, redirectRoute: string, location: string): string {
const correctRedirectUrl = new URLCombiner(origin, redirectRoute).toString();

Check warning on line 564 in src/app/core/auth/auth.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/auth/auth.service.ts#L564

Added line #L564 was not covered by tests

let externalServerUrl = location;
const myRegexp = /\?redirectUrl=(.*)/g;
const match = myRegexp.exec(location);

Check warning on line 568 in src/app/core/auth/auth.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/auth/auth.service.ts#L566-L568

Added lines #L566 - L568 were not covered by tests
const redirectUrlFromServer = (match && match[1]) ? match[1] : null;

// Check whether the current page is different from the redirect url received from rest
if (isNotNull(redirectUrlFromServer) && redirectUrlFromServer !== correctRedirectUrl) {
// change the redirect url with the current page url
const newRedirectUrl = `?redirectUrl=${correctRedirectUrl}`;
externalServerUrl = location.replace(/\?redirectUrl=(.*)/g, newRedirectUrl);

Check warning on line 575 in src/app/core/auth/auth.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/auth/auth.service.ts#L574-L575

Added lines #L574 - L575 were not covered by tests
}

return externalServerUrl;

Check warning on line 578 in src/app/core/auth/auth.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/auth/auth.service.ts#L578

Added line #L578 was not covered by tests
}

/**
* Clear redirect url
*/
Expand Down Expand Up @@ -633,4 +663,18 @@ export class AuthService {
}
}

/**
* Create a new machine token for the current user
*/
public createMachineToken(): Observable<RemoteData<MachineToken>> {
return this.authRequestService.postToMachineTokenEndpoint();

Check warning on line 670 in src/app/core/auth/auth.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/auth/auth.service.ts#L670

Added line #L670 was not covered by tests
}

/**
* Delete the machine token for the current user
*/
public deleteMachineToken(): Observable<RemoteData<NoContent>> {
return this.authRequestService.deleteToMachineTokenEndpoint();

Check warning on line 677 in src/app/core/auth/auth.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/auth/auth.service.ts#L677

Added line #L677 was not covered by tests
}

}
2 changes: 1 addition & 1 deletion src/app/core/auth/models/auth.method-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ export enum AuthMethodType {
Ip = 'ip',
X509 = 'x509',
Oidc = 'oidc',
Orcid = 'orcid'
Orcid = 'orcid',
}
4 changes: 4 additions & 0 deletions src/app/core/auth/models/auth.registration-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum AuthRegistrationType {
Orcid = 'ORCID',
Validation = 'VALIDATION_',
}
36 changes: 36 additions & 0 deletions src/app/core/auth/models/machine-token.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { autoserialize, autoserializeAs, deserialize } from 'cerialize';

import { typedObject } from '../../cache/builders/build-decorators';
import { CacheableObject } from '../../cache/cacheable-object.model';
import { excludeFromEquals } from '../../utilities/equals.decorators';
import { ResourceType } from '../../shared/resource-type';
import { HALLink } from '../../shared/hal-link.model';
import { MACHINE_TOKEN } from './machine-token.resource-type';

/**
* A machine token that can be used to authenticate a rest request
*/
@typedObject
export class MachineToken implements CacheableObject {
static type = MACHINE_TOKEN;
/**
* The type for this MachineToken
*/
@excludeFromEquals
@autoserialize
type: ResourceType;

/**
* The value for this MachineToken
*/
@autoserializeAs('token')
value: string;

/**
* The {@link HALLink}s for this MachineToken
*/
@deserialize
_links: {
self: HALLink;
};
}
9 changes: 9 additions & 0 deletions src/app/core/auth/models/machine-token.resource-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ResourceType } from '../../shared/resource-type';

/**
* The resource type for MachineToken
*
* Needs to be in a separate file to prevent circular
* dependencies in webpack.
*/
export const MACHINE_TOKEN = new ResourceType('machinetoken');
4 changes: 2 additions & 2 deletions src/app/core/data/eperson-registration.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ describe('EpersonRegistrationService', () => {

describe('searchByToken', () => {
it('should return a registration corresponding to the provided token', () => {
const expected = service.searchByToken('test-token');
const expected = service.searchByTokenAndUpdateData('test-token');

expect(expected).toBeObservable(cold('(a|)', {
a: jasmine.objectContaining({
Expand All @@ -122,7 +122,7 @@ describe('EpersonRegistrationService', () => {
testScheduler.run(({ cold, expectObservable }) => {
rdbService.buildSingle.and.returnValue(cold('a', { a: rd }));

service.searchByToken('test-token');
service.searchByTokenAndUpdateData('test-token');

expect(requestService.send).toHaveBeenCalledWith(
jasmine.objectContaining({
Expand Down
86 changes: 78 additions & 8 deletions src/app/core/data/eperson-registration.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { GetRequest, PostRequest } from './request.models';
import { GetRequest, PatchRequest, PostRequest } from './request.models';
import { Observable } from 'rxjs';
import { filter, find, map } from 'rxjs/operators';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
Expand All @@ -13,8 +13,9 @@ import { RegistrationResponseParsingService } from './registration-response-pars
import { RemoteData } from './remote-data';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { HttpHeaders } from '@angular/common/http';
import { HttpParams } from '@angular/common/http';
import { HttpHeaders, HttpParams } from '@angular/common/http';
import { Operation } from 'fast-json-patch';
import { NoContent } from '../shared/NoContent.model';

@Injectable({
providedIn: 'root',
Expand All @@ -32,7 +33,6 @@ export class EpersonRegistrationService {
protected rdbService: RemoteDataBuildService,
protected halService: HALEndpointService,
) {

}

/**
Expand Down Expand Up @@ -90,10 +90,11 @@ export class EpersonRegistrationService {
}

/**
* Search a registration based on the provided token
* @param token
* Searches for a registration based on the provided token.
* @param token The token to search for.
* @returns An observable of remote data containing the registration.
*/
searchByToken(token: string): Observable<RemoteData<Registration>> {
searchByTokenAndUpdateData(token: string): Observable<RemoteData<Registration>> {
const requestId = this.requestService.generateRequestId();

const href$ = this.getTokenSearchEndpoint(token).pipe(
Expand All @@ -113,12 +114,81 @@ export class EpersonRegistrationService {
return this.rdbService.buildSingle<Registration>(href$).pipe(
map((rd) => {
if (rd.hasSucceeded && hasValue(rd.payload)) {
return Object.assign(rd, { payload: Object.assign(rd.payload, { token }) });
return Object.assign(rd, { payload: Object.assign(new Registration(), {
email: rd.payload.email,
token: token,
user: rd.payload.user,
}) });
} else {
return rd;
}
})
);
}

/**
* Searches for a registration by token and handles any errors that may occur.
* @param token The token to search for.
* @returns An observable of remote data containing the registration.
*/
searchByTokenAndHandleError(token: string): Observable<RemoteData<Registration>> {
const requestId = this.requestService.generateRequestId();

Check warning on line 135 in src/app/core/data/eperson-registration.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/data/eperson-registration.service.ts#L135

Added line #L135 was not covered by tests

const href$ = this.getTokenSearchEndpoint(token).pipe(
find((href: string) => hasValue(href)),

Check warning on line 138 in src/app/core/data/eperson-registration.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/data/eperson-registration.service.ts#L137-L138

Added lines #L137 - L138 were not covered by tests
);

href$.subscribe((href: string) => {
const request = new GetRequest(requestId, href);
Object.assign(request, {

Check warning on line 143 in src/app/core/data/eperson-registration.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/data/eperson-registration.service.ts#L141-L143

Added lines #L141 - L143 were not covered by tests
getResponseParser(): GenericConstructor<ResponseParsingService> {
return RegistrationResponseParsingService;

Check warning on line 145 in src/app/core/data/eperson-registration.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/data/eperson-registration.service.ts#L145

Added line #L145 was not covered by tests
}
});
this.requestService.send(request, true);

Check warning on line 148 in src/app/core/data/eperson-registration.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/data/eperson-registration.service.ts#L148

Added line #L148 was not covered by tests
});
return this.rdbService.buildSingle<Registration>(href$);

Check warning on line 150 in src/app/core/data/eperson-registration.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/data/eperson-registration.service.ts#L150

Added line #L150 was not covered by tests
}

/**
* Patch the registration object to update the email address
* @param value provided by the user during the registration confirmation process
* @param registrationId The id of the registration object
* @param token The token of the registration object
* @param updateValue Flag to indicate if the email should be updated or added
* @returns Remote Data state of the patch request
*/
patchUpdateRegistration(values: string[], field: string, registrationId: string, token: string, operator: 'add' | 'replace'): Observable<RemoteData<NoContent>> {
const requestId = this.requestService.generateRequestId();

Check warning on line 162 in src/app/core/data/eperson-registration.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/data/eperson-registration.service.ts#L162

Added line #L162 was not covered by tests

const href$ = this.getRegistrationEndpoint().pipe(
find((href: string) => hasValue(href)),
map((href: string) => `${href}/${registrationId}?token=${token}`),

Check warning on line 166 in src/app/core/data/eperson-registration.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/data/eperson-registration.service.ts#L164-L166

Added lines #L164 - L166 were not covered by tests
);

href$.subscribe((href: string) => {
const operations = this.generateOperations(values, field, operator);
const patchRequest = new PatchRequest(requestId, href, operations);
this.requestService.send(patchRequest);

Check warning on line 172 in src/app/core/data/eperson-registration.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/data/eperson-registration.service.ts#L169-L172

Added lines #L169 - L172 were not covered by tests
});

return this.rdbService.buildFromRequestUUID(requestId);

Check warning on line 175 in src/app/core/data/eperson-registration.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/data/eperson-registration.service.ts#L175

Added line #L175 was not covered by tests
}

/**
* Custom method to generate the operations to be performed on the registration object
* @param value provided by the user during the registration confirmation process
* @param updateValue Flag to indicate if the email should be updated or added
* @returns Operations to be performed on the registration object
*/
private generateOperations(values: string[], field: string, operator: 'add' | 'replace'): Operation[] {
let operations = [];

Check warning on line 185 in src/app/core/data/eperson-registration.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/data/eperson-registration.service.ts#L185

Added line #L185 was not covered by tests
if (values.length > 0 && hasValue(field) ) {
operations = [{

Check warning on line 187 in src/app/core/data/eperson-registration.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/data/eperson-registration.service.ts#L187

Added line #L187 was not covered by tests
op: operator, path: `/${field}`, value: values
}];
}

return operations;

Check warning on line 192 in src/app/core/data/eperson-registration.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/data/eperson-registration.service.ts#L192

Added line #L192 was not covered by tests
}
}
Loading

0 comments on commit 590fe70

Please sign in to comment.