Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ORCID Login flow for private emails #3355

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/app/app-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,20 @@ export const APP_ROUTES: Route[] = [
.then((m) => m.ROUTES),
canActivate: [authenticatedGuard],
},
{
path: 'external-login/:token',
loadChildren: () => import('./external-login-page/external-login-routes').then((m) => m.ROUTES),
},
{
path: 'review-account/:token',
loadChildren: () => import('./external-login-review-account-info-page/external-login-review-account-info-page-routes')
.then((m) => m.ROUTES),
},
{
path: 'email-confirmation',
loadChildren: () => import('./external-login-email-confirmation-page/external-login-email-confirmation-page-routes')
.then((m) => m.ROUTES),
},
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent },
],
},
Expand Down
2 changes: 2 additions & 0 deletions src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import {
import { ClientCookieService } from './core/services/client-cookie.service';
import { ListableModule } from './core/shared/listable.module';
import { XsrfInterceptor } from './core/xsrf/xsrf.interceptor';
import { LOGIN_METHOD_FOR_DECORATOR_MAP } from './external-log-in/decorators/external-log-in.methods-decorator';
import { RootModule } from './root.module';
import { AUTH_METHOD_FOR_DECORATOR_MAP } from './shared/log-in/methods/log-in.methods-decorator';
import { METADATA_REPRESENTATION_COMPONENT_DECORATOR_MAP } from './shared/metadata-representation/metadata-representation.decorator';
Expand Down Expand Up @@ -157,6 +158,7 @@ export const commonAppConfig: ApplicationConfig = {

/* Use models object so all decorators are actually called */
const modelList = models;
const loginMethodForDecoratorMap = LOGIN_METHOD_FOR_DECORATOR_MAP;
const workflowTasks = WORKFLOW_TASK_OPTION_DECORATOR_MAP;
const advancedWorfklowTasks = ADVANCED_WORKFLOW_TASK_OPTION_DECORATOR_MAP;
const metadataRepresentations = METADATA_REPRESENTATION_COMPONENT_DECORATOR_MAP;
Expand Down
38 changes: 37 additions & 1 deletion src/app/core/auth/auth-request.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,28 @@
tap,
} from 'rxjs/operators';

import { isNotEmpty } from '../../shared/empty.util';
import {
isNotEmpty,
isNotEmptyOperator,
} from '../../shared/empty.util';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { RemoteData } from '../data/remote-data';
import {
DeleteRequest,
GetRequest,
PostRequest,
} from '../data/request.models';
import { RequestService } from '../data/request.service';
import { RestRequest } from '../data/rest-request.model';
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NoContent } from '../shared/NoContent.model';
import { getFirstCompletedRemoteData } from '../shared/operators';
import { sendRequest } from '../shared/request.operators';
import { URLCombiner } from '../url-combiner/url-combiner';
import { AuthStatus } from './models/auth-status.model';
import { MachineToken } from './models/machine-token.model';
import { ShortLivedToken } from './models/short-lived-token.model';

/**
Expand All @@ -31,6 +38,7 @@
export abstract class AuthRequestService {
protected linkName = 'authn';
protected shortlivedtokensEndpoint = 'shortlivedtokens';
protected machinetokenEndpoint = 'machinetokens';

constructor(protected halService: HALEndpointService,
protected requestService: RequestService,
Expand Down Expand Up @@ -139,4 +147,32 @@
}),
);
}

/**
* 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 155 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#L155

Added line #L155 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 161 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#L158-L161

Added lines #L158 - L161 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 169 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#L169

Added line #L169 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 173 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#L172-L173

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

Check warning on line 175 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#L175

Added line #L175 was not covered by tests
);
}
}
42 changes: 42 additions & 0 deletions src/app/core/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@
NativeWindowRef,
NativeWindowService,
} from '../services/window.service';
import { NoContent } from '../shared/NoContent.model';
import {
getAllSucceededRemoteDataPayload,
getFirstCompletedRemoteData,
} from '../shared/operators';
import { PageInfo } from '../shared/page-info.model';
import { URLCombiner } from '../url-combiner/url-combiner';
import {
CheckAuthenticationTokenAction,
RefreshTokenAction,
Expand All @@ -78,6 +80,7 @@
AuthTokenInfo,
TOKENITEM,
} from './models/auth-token-info.model';
import { MachineToken } from './models/machine-token.model';
import {
getAuthenticatedUserId,
getAuthenticationToken,
Expand Down Expand Up @@ -579,6 +582,31 @@
});
}

/**
* 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 593 in src/app/core/auth/auth.service.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L593 was not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

src/app/core/auth/auth.service.ts#L595-L597

Added lines #L595 - L597 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 604 in src/app/core/auth/auth.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/auth/auth.service.ts#L603-L604

Added lines #L603 - L604 were not covered by tests
}

return externalServerUrl;

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L607 was not covered by tests
}

/**
* Clear redirect url
*/
Expand Down Expand Up @@ -664,4 +692,18 @@
}
}

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

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L699 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 706 in src/app/core/auth/auth.service.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L706 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_',
}
40 changes: 40 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,40 @@
import {
autoserialize,
autoserializeAs,
deserialize,
} from 'cerialize';

import { typedObject } from '../../cache/builders/build-decorators';
import { CacheableObject } from '../../cache/cacheable-object.model';
import { HALLink } from '../../shared/hal-link.model';
import { ResourceType } from '../../shared/resource-type';
import { excludeFromEquals } from '../../utilities/equals.decorators';
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 @@ -105,7 +105,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 @@ -123,7 +123,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
82 changes: 77 additions & 5 deletions src/app/core/data/eperson-registration.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
HttpParams,
} from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Operation } from 'fast-json-patch';
import { Observable } from 'rxjs';
import {
filter,
Expand All @@ -18,13 +19,15 @@
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { GenericConstructor } from '../shared/generic-constructor';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NoContent } from '../shared/NoContent.model';
import { getFirstCompletedRemoteData } from '../shared/operators';
import { Registration } from '../shared/registration.model';
import { ResponseParsingService } from './parsing.service';
import { RegistrationResponseParsingService } from './registration-response-parsing.service';
import { RemoteData } from './remote-data';
import {
GetRequest,
PatchRequest,
PostRequest,
} from './request.models';
import { RequestService } from './request.service';
Expand All @@ -45,7 +48,6 @@
protected rdbService: RemoteDataBuildService,
protected halService: HALEndpointService,
) {

}

/**
Expand Down Expand Up @@ -103,10 +105,11 @@
}

/**
* 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 @@ -126,12 +129,81 @@
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 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

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

Check warning on line 153 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#L152-L153

Added lines #L152 - L153 were not covered by tests
);

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

Check warning on line 158 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#L156-L158

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

Check warning on line 160 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#L160

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

Check warning on line 163 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#L163

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

Check warning on line 165 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#L165

Added line #L165 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 177 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#L177

Added line #L177 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 181 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#L179-L181

Added lines #L179 - L181 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 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#L184-L187

Added lines #L184 - L187 were not covered by tests
});

return this.rdbService.buildFromRequestUUID(requestId);

Check warning on line 190 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#L190

Added line #L190 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 200 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#L200

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

Check warning on line 202 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#L202

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

return operations;

Check warning on line 207 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#L207

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