Skip to content

Commit 3d7fcef

Browse files
authored
Merge pull request #1470 from hardyoyo/port-oidc-auth-plugin-from-dspace-cris
Port OIDC (OpenID Connect) auth plugin from DSpace-CRIS
2 parents ccdba9b + 56e7d4b commit 3d7fcef

File tree

7 files changed

+280
-1
lines changed

7 files changed

+280
-1
lines changed

src/app/core/auth/models/auth.method-type.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ export enum AuthMethodType {
33
Shibboleth = 'shibboleth',
44
Ldap = 'ldap',
55
Ip = 'ip',
6-
X509 = 'x509'
6+
X509 = 'x509',
7+
Oidc = 'oidc'
78
}

src/app/core/auth/models/auth.method.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ export class AuthMethod {
2929
this.authMethodType = AuthMethodType.Password;
3030
break;
3131
}
32+
case 'oidc': {
33+
this.authMethodType = AuthMethodType.Oidc;
34+
this.location = location;
35+
break;
36+
}
3237

3338
default: {
3439
break;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<button class="btn btn-lg btn-primary btn-block mt-2 text-white" (click)="redirectToOidc()">
2+
<i class="fas fa-sign-in-alt"></i> {{"login.form.oidc" | translate}}
3+
</button>
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
2+
import { ActivatedRoute, Router } from '@angular/router';
3+
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
4+
5+
import { provideMockStore } from '@ngrx/store/testing';
6+
import { Store, StoreModule } from '@ngrx/store';
7+
import { TranslateModule } from '@ngx-translate/core';
8+
9+
import { EPerson } from '../../../../core/eperson/models/eperson.model';
10+
import { EPersonMock } from '../../../testing/eperson.mock';
11+
import { authReducer } from '../../../../core/auth/auth.reducer';
12+
import { AuthService } from '../../../../core/auth/auth.service';
13+
import { AuthServiceStub } from '../../../testing/auth-service.stub';
14+
import { storeModuleConfig } from '../../../../app.reducer';
15+
import { AuthMethod } from '../../../../core/auth/models/auth.method';
16+
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
17+
import { LogInOidcComponent } from './log-in-oidc.component';
18+
import { NativeWindowService } from '../../../../core/services/window.service';
19+
import { RouterStub } from '../../../testing/router.stub';
20+
import { ActivatedRouteStub } from '../../../testing/active-router.stub';
21+
import { NativeWindowMockFactory } from '../../../mocks/mock-native-window-ref';
22+
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
23+
24+
25+
describe('LogInOidcComponent', () => {
26+
27+
let component: LogInOidcComponent;
28+
let fixture: ComponentFixture<LogInOidcComponent>;
29+
let page: Page;
30+
let user: EPerson;
31+
let componentAsAny: any;
32+
let setHrefSpy;
33+
let oidcBaseUrl;
34+
let location;
35+
let initialState: any;
36+
let hardRedirectService: HardRedirectService;
37+
38+
beforeEach(() => {
39+
user = EPersonMock;
40+
oidcBaseUrl = 'dspace-rest.test/oidc?redirectUrl=';
41+
location = oidcBaseUrl + 'http://dspace-angular.test/home';
42+
43+
hardRedirectService = jasmine.createSpyObj('hardRedirectService', {
44+
getCurrentRoute: {},
45+
redirect: {}
46+
});
47+
48+
initialState = {
49+
core: {
50+
auth: {
51+
authenticated: false,
52+
loaded: false,
53+
blocking: false,
54+
loading: false,
55+
authMethods: []
56+
}
57+
}
58+
};
59+
});
60+
61+
beforeEach(waitForAsync(() => {
62+
// refine the test module by declaring the test component
63+
TestBed.configureTestingModule({
64+
imports: [
65+
StoreModule.forRoot({ auth: authReducer }, storeModuleConfig),
66+
TranslateModule.forRoot()
67+
],
68+
declarations: [
69+
LogInOidcComponent
70+
],
71+
providers: [
72+
{ provide: AuthService, useClass: AuthServiceStub },
73+
{ provide: 'authMethodProvider', useValue: new AuthMethod(AuthMethodType.Oidc, location) },
74+
{ provide: 'isStandalonePage', useValue: true },
75+
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
76+
{ provide: Router, useValue: new RouterStub() },
77+
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
78+
{ provide: HardRedirectService, useValue: hardRedirectService },
79+
provideMockStore({ initialState }),
80+
],
81+
schemas: [
82+
CUSTOM_ELEMENTS_SCHEMA
83+
]
84+
})
85+
.compileComponents();
86+
87+
}));
88+
89+
beforeEach(() => {
90+
// create component and test fixture
91+
fixture = TestBed.createComponent(LogInOidcComponent);
92+
93+
// get test component from the fixture
94+
component = fixture.componentInstance;
95+
componentAsAny = component;
96+
97+
// create page
98+
page = new Page(component, fixture);
99+
setHrefSpy = spyOnProperty(componentAsAny._window.nativeWindow.location, 'href', 'set').and.callThrough();
100+
101+
});
102+
103+
it('should set the properly a new redirectUrl', () => {
104+
const currentUrl = 'http://dspace-angular.test/collections/12345';
105+
componentAsAny._window.nativeWindow.location.href = currentUrl;
106+
107+
fixture.detectChanges();
108+
109+
expect(componentAsAny.injectedAuthMethodModel.location).toBe(location);
110+
expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl);
111+
112+
component.redirectToOidc();
113+
114+
expect(setHrefSpy).toHaveBeenCalledWith(currentUrl);
115+
116+
});
117+
118+
it('should not set a new redirectUrl', () => {
119+
const currentUrl = 'http://dspace-angular.test/home';
120+
componentAsAny._window.nativeWindow.location.href = currentUrl;
121+
122+
fixture.detectChanges();
123+
124+
expect(componentAsAny.injectedAuthMethodModel.location).toBe(location);
125+
expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl);
126+
127+
component.redirectToOidc();
128+
129+
expect(setHrefSpy).toHaveBeenCalledWith(currentUrl);
130+
131+
});
132+
133+
});
134+
135+
/**
136+
* I represent the DOM elements and attach spies.
137+
*
138+
* @class Page
139+
*/
140+
class Page {
141+
142+
public emailInput: HTMLInputElement;
143+
public navigateSpy: jasmine.Spy;
144+
public passwordInput: HTMLInputElement;
145+
146+
constructor(private component: LogInOidcComponent, private fixture: ComponentFixture<LogInOidcComponent>) {
147+
// use injector to get services
148+
const injector = fixture.debugElement.injector;
149+
const store = injector.get(Store);
150+
151+
// add spies
152+
this.navigateSpy = spyOn(store, 'dispatch');
153+
}
154+
155+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { Component, Inject, OnInit, } from '@angular/core';
2+
3+
import { Observable } from 'rxjs';
4+
import { select, Store } from '@ngrx/store';
5+
6+
import { renderAuthMethodFor } from '../log-in.methods-decorator';
7+
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
8+
import { AuthMethod } from '../../../../core/auth/models/auth.method';
9+
10+
import { CoreState } from '../../../../core/core.reducers';
11+
import { isAuthenticated, isAuthenticationLoading } from '../../../../core/auth/selectors';
12+
import { NativeWindowRef, NativeWindowService } from '../../../../core/services/window.service';
13+
import { isNotNull, isEmpty } from '../../../empty.util';
14+
import { AuthService } from '../../../../core/auth/auth.service';
15+
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
16+
import { take } from 'rxjs/operators';
17+
import { URLCombiner } from '../../../../core/url-combiner/url-combiner';
18+
19+
@Component({
20+
selector: 'ds-log-in-oidc',
21+
templateUrl: './log-in-oidc.component.html',
22+
})
23+
@renderAuthMethodFor(AuthMethodType.Oidc)
24+
export class LogInOidcComponent implements OnInit {
25+
26+
/**
27+
* The authentication method data.
28+
* @type {AuthMethod}
29+
*/
30+
public authMethod: AuthMethod;
31+
32+
/**
33+
* True if the authentication is loading.
34+
* @type {boolean}
35+
*/
36+
public loading: Observable<boolean>;
37+
38+
/**
39+
* The oidc authentication location url.
40+
* @type {string}
41+
*/
42+
public location: string;
43+
44+
/**
45+
* Whether user is authenticated.
46+
* @type {Observable<string>}
47+
*/
48+
public isAuthenticated: Observable<boolean>;
49+
50+
/**
51+
* @constructor
52+
* @param {AuthMethod} injectedAuthMethodModel
53+
* @param {boolean} isStandalonePage
54+
* @param {NativeWindowRef} _window
55+
* @param {AuthService} authService
56+
* @param {HardRedirectService} hardRedirectService
57+
* @param {Store<State>} store
58+
*/
59+
constructor(
60+
@Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod,
61+
@Inject('isStandalonePage') public isStandalonePage: boolean,
62+
@Inject(NativeWindowService) protected _window: NativeWindowRef,
63+
private authService: AuthService,
64+
private hardRedirectService: HardRedirectService,
65+
private store: Store<CoreState>
66+
) {
67+
this.authMethod = injectedAuthMethodModel;
68+
}
69+
70+
ngOnInit(): void {
71+
// set isAuthenticated
72+
this.isAuthenticated = this.store.pipe(select(isAuthenticated));
73+
74+
// set loading
75+
this.loading = this.store.pipe(select(isAuthenticationLoading));
76+
77+
// set location
78+
this.location = decodeURIComponent(this.injectedAuthMethodModel.location);
79+
80+
}
81+
82+
redirectToOidc() {
83+
84+
this.authService.getRedirectUrl().pipe(take(1)).subscribe((redirectRoute) => {
85+
if (!this.isStandalonePage) {
86+
redirectRoute = this.hardRedirectService.getCurrentRoute();
87+
} else if (isEmpty(redirectRoute)) {
88+
redirectRoute = '/';
89+
}
90+
const correctRedirectUrl = new URLCombiner(this._window.nativeWindow.origin, redirectRoute).toString();
91+
92+
let oidcServerUrl = this.location;
93+
const myRegexp = /\?redirectUrl=(.*)/g;
94+
const match = myRegexp.exec(this.location);
95+
const redirectUrlFromServer = (match && match[1]) ? match[1] : null;
96+
97+
// Check whether the current page is different from the redirect url received from rest
98+
if (isNotNull(redirectUrlFromServer) && redirectUrlFromServer !== correctRedirectUrl) {
99+
// change the redirect url with the current page url
100+
const newRedirectUrl = `?redirectUrl=${correctRedirectUrl}`;
101+
oidcServerUrl = this.location.replace(/\?redirectUrl=(.*)/g, newRedirectUrl);
102+
}
103+
104+
// redirect to oidc authentication url
105+
this.hardRedirectService.redirect(oidcServerUrl);
106+
});
107+
108+
}
109+
110+
}

src/app/shared/shared.module.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ import { ItemVersionsDeleteModalComponent } from './item/item-versions/item-vers
176176
import { ScopeSelectorModalComponent } from './search-form/scope-selector-modal/scope-selector-modal.component';
177177
import { BitstreamRequestACopyPageComponent } from './bitstream-request-a-copy-page/bitstream-request-a-copy-page.component';
178178
import { DsSelectComponent } from './ds-select/ds-select.component';
179+
import { LogInOidcComponent } from './log-in/methods/oidc/log-in-oidc.component';
179180

180181
const MODULES = [
181182
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -307,6 +308,7 @@ const COMPONENTS = [
307308
ImportableListItemControlComponent,
308309

309310
LogInShibbolethComponent,
311+
LogInOidcComponent,
310312
LogInPasswordComponent,
311313
LogInContainerComponent,
312314
ItemVersionsComponent,
@@ -378,6 +380,7 @@ const ENTRY_COMPONENTS = [
378380
ItemMetadataRepresentationListElementComponent,
379381
LogInPasswordComponent,
380382
LogInShibbolethComponent,
383+
LogInOidcComponent,
381384
BundleListElementComponent,
382385
ClaimedTaskActionsApproveComponent,
383386
ClaimedTaskActionsRejectComponent,

src/assets/i18n/en.json5

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2369,6 +2369,8 @@
23692369

23702370
"login.form.or-divider": "or",
23712371

2372+
"login.form.oidc": "Log in with OIDC",
2373+
23722374
"login.form.password": "Password",
23732375

23742376
"login.form.shibboleth": "Log in with Shibboleth",

0 commit comments

Comments
 (0)