Browse Source

Update Oidc client. (#981)

* Update Oidc client.

* Fix tests

* Update assets.
pull/983/head
Sebastian Stehle 3 years ago
committed by GitHub
parent
commit
f9322bed7b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      backend/src/Squidex.Web/Pipeline/CachingKeysMiddleware.cs
  2. 43
      backend/src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs
  3. 4
      backend/src/Squidex/Areas/IdentityServer/Config/DynamicApplicationStore.cs
  4. 1
      backend/src/Squidex/Startup.cs
  5. 9
      backend/src/Squidex/wwwroot/client-callback-popup.html
  6. 9
      backend/src/Squidex/wwwroot/client-callback-silent.html
  7. 2
      backend/src/Squidex/wwwroot/scripts/editor-json-schema.html
  8. 2
      backend/src/Squidex/wwwroot/scripts/editor-references.html
  9. 2
      backend/src/Squidex/wwwroot/scripts/oidc-client-ts.min.js
  10. 47
      backend/src/Squidex/wwwroot/scripts/oidc-client.min.js
  11. 7
      frontend/angular.json
  12. 72
      frontend/package-lock.json
  13. 2
      frontend/package.json
  14. 58
      frontend/src/app/shared/interceptors/auth.interceptor.spec.ts
  15. 8
      frontend/src/app/shared/interceptors/auth.interceptor.ts
  16. 141
      frontend/src/app/shared/services/auth.service.ts
  17. 2
      frontend/src/app/shell/pages/home/home-page.component.html
  18. 27
      frontend/src/app/shell/pages/home/home-page.component.ts
  19. 18
      frontend/src/app/shell/pages/login/login-page.component.ts
  20. 18
      frontend/src/app/shell/pages/logout/logout-page.component.ts

1
backend/src/Squidex.Web/Pipeline/CachingKeysMiddleware.cs

@ -22,7 +22,6 @@ public sealed class CachingKeysMiddleware
{
this.cachingOptions = cachingOptions.Value;
this.cachingManager = cachingManager;
this.next = next;
}

43
backend/src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs

@ -15,42 +15,37 @@ namespace Squidex.Web.Pipeline;
public sealed class RequestLogPerformanceMiddleware
{
private readonly RequestLogOptions requestLogOptions;
private readonly RequestDelegate next;
public RequestLogPerformanceMiddleware(RequestDelegate next, IOptions<RequestLogOptions> requestLogOptions)
public RequestLogPerformanceMiddleware(RequestDelegate next)
{
this.requestLogOptions = requestLogOptions.Value;
this.next = next;
}
public async Task InvokeAsync(HttpContext context, ISemanticLog log)
public async Task InvokeAsync(HttpContext context, ISemanticLog log, IOptions<RequestLogOptions> requestLogOptions)
{
if (requestLogOptions.LogRequests)
if (!requestLogOptions.Value.LogRequests)
{
var watch = ValueStopwatch.StartNew();
try
{
await next(context);
}
finally
{
var elapsedMs = watch.Stop();
log.LogInformation((elapsedMs, context), (ctx, w) =>
{
w.WriteProperty("message", "HTTP request executed.");
w.WriteProperty("elapsedRequestMs", ctx.elapsedMs);
w.WriteObject("filters", ctx.context, LogFilters);
});
}
await next(context);
return;
}
else
var watch = ValueStopwatch.StartNew();
try
{
await next(context);
}
finally
{
var elapsedMs = watch.Stop();
log.LogInformation((elapsedMs, context), (ctx, w) =>
{
w.WriteProperty("message", "HTTP request executed.");
w.WriteProperty("elapsedRequestMs", ctx.elapsedMs);
w.WriteObject("filters", ctx.context, LogFilters);
});
}
}
private static void LogFilters(HttpContext httpContext, IObjectWriter obj)

4
backend/src/Squidex/Areas/IdentityServer/Config/DynamicApplicationStore.cs

@ -140,8 +140,6 @@ public class DynamicApplicationStore : InMemoryApplicationStore
private static IEnumerable<(string, OpenIddictApplicationDescriptor)> CreateStaticClients(IServiceProvider serviceProvider)
{
var identityOptions = serviceProvider.GetRequiredService<IOptions<MyIdentityOptions>>().Value;
var urlGenerator = serviceProvider.GetRequiredService<IUrlGenerator>();
var frontendId = Constants.ClientFrontendId;
@ -207,6 +205,8 @@ public class DynamicApplicationStore : InMemoryApplicationStore
Type = ClientTypes.Public
});
var identityOptions = serviceProvider.GetRequiredService<IOptions<MyIdentityOptions>>().Value;
if (!identityOptions.IsAdminClientConfigured())
{
yield break;

1
backend/src/Squidex/Startup.cs

@ -123,7 +123,6 @@ public sealed class Startup
});
app.UseFrontend();
app.UsePlugins();
}
}

9
backend/src/Squidex/wwwroot/client-callback-popup.html

@ -4,12 +4,13 @@
<base href="/">
</head>
<body>
<script src="scripts/oidc-client.min.js"></script>
<script src="scripts/oidc-client-ts.min.js"></script>
<script>
Oidc.Log.logger = console;
Oidc.Log.logLevel = Oidc.Log.INFO;
oidc.Log.setLogger(console);
new Oidc.UserManager().signinPopupCallback();
new oidc.UserManager({ response_mode: 'query' }).signinPopupCallback().catch(error => {
console.error(error);
});
</script>
</body>
</html>

9
backend/src/Squidex/wwwroot/client-callback-silent.html

@ -4,12 +4,13 @@
<base href="/">
</head>
<body>
<script src="scripts/oidc-client.min.js"></script>
<script src="scripts/oidc-client-ts.min.js"></script>
<script>
Oidc.Log.logger = console;
Oidc.Log.logLevel = Oidc.Log.INFO;
oidc.Log.setLogger(console);
new Oidc.UserManager().signinSilentCallback();
new oidc.UserManager({ response_mode: 'query' }).signinSilentCallback().catch(error => {
console.error(error);
});
</script>
</body>
</html>

2
backend/src/Squidex/wwwroot/scripts/editor-json-schema.html

@ -10,7 +10,7 @@
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@rjsf/core/dist/react-jsonschema-form.js"></script>
<link rel="stylesheet" type="text/css" href="https://cloud.squidex.io/build/styles.css">
<link rel="stylesheet" type="text/css" href="https://cloud.squidex.io/styles.css">
<style>
body {

2
backend/src/Squidex/wwwroot/scripts/editor-references.html

@ -7,7 +7,7 @@
<!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js -->
<script src="editor-sdk.js"></script>
<link rel="stylesheet" type="text/css" href="https://cloud.squidex.io/build/styles.css">
<link rel="stylesheet" type="text/css" href="https://cloud.squidex.io/styles.css">
<style>
body {

2
backend/src/Squidex/wwwroot/scripts/oidc-client-ts.min.js

File diff suppressed because one or more lines are too long

47
backend/src/Squidex/wwwroot/scripts/oidc-client.min.js

File diff suppressed because one or more lines are too long

7
frontend/angular.json

@ -31,9 +31,14 @@
"allowedCommonJsDependencies": [
"@tweenjs/tween.js",
"cropperjs",
"crypto-js",
"crypto-js/core.js",
"crypto-js/enc-base64.js",
"crypto-js/enc-utf8.js",
"crypto-js/sha256.js",
"mousetrap",
"mersenne-twister",
"oidc-client",
"pikaday",
"pikaday/pikaday",
"progressbar.js",
"slugify"

72
frontend/package-lock.json

@ -47,7 +47,7 @@
"ngx-color-picker": "13.0.0",
"ngx-doc-viewer": "15.0.1",
"ngx-virtual-scroller": "^4.0.3",
"oidc-client": "1.11.5",
"oidc-client-ts": "^2.2.2",
"pikaday": "1.8.2",
"progressbar.js": "1.1.0",
"react": "18.2.0",
@ -15018,6 +15018,7 @@
},
"node_modules/base64-js": {
"version": "1.5.1",
"dev": true,
"funding": [
{
"type": "github",
@ -23249,6 +23250,11 @@
"node": ">=8"
}
},
"node_modules/jwt-decode": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
},
"node_modules/karma": {
"version": "6.4.1",
"dev": true,
@ -26131,32 +26137,16 @@
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
"dev": true
},
"node_modules/oidc-client": {
"version": "1.11.5",
"license": "Apache-2.0",
"node_modules/oidc-client-ts": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-2.2.2.tgz",
"integrity": "sha512-tEpnC1xQX6PMhnDMNEvDhpqnsu0buh29b/sA9a1I5tq6lD87wRAW1rTj49B2+5925I0sQlvU64DoK0Pxeiu7Dg==",
"dependencies": {
"acorn": "^7.4.1",
"base64-js": "^1.5.1",
"core-js": "^3.8.3",
"crypto-js": "^4.0.0",
"serialize-javascript": "^4.0.0"
}
},
"node_modules/oidc-client/node_modules/acorn": {
"version": "7.4.1",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
"crypto-js": "^4.1.1",
"jwt-decode": "^3.1.2"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/oidc-client/node_modules/serialize-javascript": {
"version": "4.0.0",
"license": "BSD-3-Clause",
"dependencies": {
"randombytes": "^2.1.0"
"node": ">=12.13.0"
}
},
"node_modules/on-finished": {
@ -27795,6 +27785,7 @@
},
"node_modules/randombytes": {
"version": "2.1.0",
"dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "^5.1.0"
@ -44925,7 +44916,8 @@
}
},
"base64-js": {
"version": "1.5.1"
"version": "1.5.1",
"dev": true
},
"base64id": {
"version": "2.0.0",
@ -50682,6 +50674,11 @@
"integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==",
"dev": true
},
"jwt-decode": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
},
"karma": {
"version": "6.4.1",
"dev": true,
@ -52796,25 +52793,13 @@
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
"dev": true
},
"oidc-client": {
"version": "1.11.5",
"oidc-client-ts": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-2.2.2.tgz",
"integrity": "sha512-tEpnC1xQX6PMhnDMNEvDhpqnsu0buh29b/sA9a1I5tq6lD87wRAW1rTj49B2+5925I0sQlvU64DoK0Pxeiu7Dg==",
"requires": {
"acorn": "^7.4.1",
"base64-js": "^1.5.1",
"core-js": "^3.8.3",
"crypto-js": "^4.0.0",
"serialize-javascript": "^4.0.0"
},
"dependencies": {
"acorn": {
"version": "7.4.1"
},
"serialize-javascript": {
"version": "4.0.0",
"requires": {
"randombytes": "^2.1.0"
}
}
"crypto-js": "^4.1.1",
"jwt-decode": "^3.1.2"
}
},
"on-finished": {
@ -53940,6 +53925,7 @@
},
"randombytes": {
"version": "2.1.0",
"dev": true,
"requires": {
"safe-buffer": "^5.1.0"
}

2
frontend/package.json

@ -54,7 +54,7 @@
"ngx-color-picker": "13.0.0",
"ngx-doc-viewer": "15.0.1",
"ngx-virtual-scroller": "^4.0.3",
"oidc-client": "1.11.5",
"oidc-client-ts": "^2.2.2",
"pikaday": "1.8.2",
"progressbar.js": "1.1.0",
"react": "18.2.0",

58
frontend/src/app/shared/interceptors/auth.interceptor.spec.ts

@ -56,20 +56,24 @@ describe('AuthInterceptor', () => {
it('should append headers to request',
inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => {
authService.setup(x => x.userChanges).returns(() => of(<any>{ authorization: 'letmein' }));
authService.setup(x => x.userChanges)
.returns(() => of(<any>{ authorization: 'token1' }));
http.get('http://service/p/apps').subscribe();
const req = httpMock.expectOne('http://service/p/apps');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('Authorization')).toEqual('letmein');
expect(req.request.headers.get('Authorization')).toEqual('token1');
expect(req.request.headers.get('Accept')).toBeNull();
expect(req.request.headers.get('Accept-Language')).toBeNull();
expect(req.request.headers.get('Pragma')).toEqual('no-cache');
}));
it('should not append headers for no auth headers',
inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => {
authService.setup(x => x.userChanges).returns(() => of(<any>{ authToken: 'letmein' }));
authService.setup(x => x.userChanges)
.returns(() => of(<any>{ authToauthorizationken: 'token1' }));
http.get('http://service/p/apps', { headers: new HttpHeaders().set('NoAuth', '') }).subscribe();
@ -77,13 +81,15 @@ describe('AuthInterceptor', () => {
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('Authorization')).toBeNull();
expect(req.request.headers.get('Accept')).toBeNull();
expect(req.request.headers.get('Accept-Language')).toBeNull();
expect(req.request.headers.get('Pragma')).toBeNull();
}));
it('should not append headers for other requests',
inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => {
authService.setup(x => x.userChanges).returns(() => of(<any>{ authToken: 'letmein' }));
authService.setup(x => x.userChanges)
.returns(() => of(<any>{ authorization: 'token1' }));
http.get('http://cloud/p/apps').subscribe();
@ -91,19 +97,23 @@ describe('AuthInterceptor', () => {
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('Authorization')).toBeNull();
expect(req.request.headers.get('Accept')).toBeNull();
expect(req.request.headers.get('Accept-Language')).toBeNull();
expect(req.request.headers.get('Pragma')).toBeNull();
}));
it('should logout for 401 status code',
it('should logout for 401 status code after retry',
inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => {
authService.setup(x => x.userChanges).returns(() => of(<any>{ authToken: 'letmein' }));
authService.setup(x => x.loginSilent()).returns(() => of(<any>{ authToken: 'letmereallyin' }));
authService.setup(x => x.userChanges)
.returns(() => of(<any>{ authorization: 'token1' }));
authService.setup(x => x.loginSilent())
.returns(() => ofPromise(<any>{ authorization: 'token2' }));
http.get('http://service/p/apps').pipe(onErrorResumeNextWith()).subscribe();
httpMock.expectOne('http://service/p/apps').error(<any>{}, { status: 401 });
httpMock.expectOne('http://service/p/apps').error(<any>{}, { status: 401 });
httpMock.expectOne('http://service/p/apps').flush({}, { status: 401, statusText: '401' });
httpMock.expectOne('http://service/p/apps').flush({}, { status: 401, statusText: '401' });
expect().nothing();
@ -112,16 +122,15 @@ describe('AuthInterceptor', () => {
const AUTH_ERRORS = [403];
AUTH_ERRORS.forEach(statusCode => {
it(`should redirect for ${statusCode} status code`,
AUTH_ERRORS.forEach(status => {
it(`should redirect for ${status} status code`,
inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => {
authService.setup(x => x.userChanges).returns(() => of(<any>{ authToken: 'letmein' }));
authService.setup(x => x.userChanges)
.returns(() => of(<any>{ authorization: 'token1' }));
http.get('http://service/p/apps').pipe(onErrorResumeNextWith()).subscribe();
const req = httpMock.expectOne('http://service/p/apps');
req.error(<any>{}, { status: statusCode });
httpMock.expectOne('http://service/p/apps').flush({}, { status, statusText: `${status}` });
expect().nothing();
@ -131,16 +140,15 @@ describe('AuthInterceptor', () => {
const SERVER_ERRORS = [500, 404, 405];
SERVER_ERRORS.forEach(statusCode => {
it(`should not logout for ${statusCode} status code`,
SERVER_ERRORS.forEach(status => {
it(`should not logout for ${status} status code`,
inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => {
authService.setup(x => x.userChanges).returns(() => of(<any>{ authToken: 'letmein' }));
authService.setup(x => x.userChanges)
.returns(() => of(<any>{ authorization: 'token1' }));
http.get('http://service/p/apps').pipe(onErrorResumeNextWith()).subscribe();
const req = httpMock.expectOne('http://service/p/apps');
req.error(<any>{}, { status: statusCode });
httpMock.expectOne('http://service/p/apps').flush({}, { status, statusText: `${status}` });
expect().nothing();
@ -148,3 +156,11 @@ describe('AuthInterceptor', () => {
}));
});
});
function ofPromise(value: any): Promise<any> {
return {
then: (onfullfilled: (value: any) => void) => {
onfullfilled(value);
},
} as any;
}

8
frontend/src/app/shared/interceptors/auth.interceptor.ts

@ -9,7 +9,7 @@ import { Location } from '@angular/common';
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { EMPTY, Observable, throwError } from 'rxjs';
import { EMPTY, from, Observable, throwError } from 'rxjs';
import { catchError, switchMap, take } from 'rxjs/operators';
import { ApiUrlConfig, ErrorDto } from '@app/framework';
import { AuthService, Profile } from './../services/auth.service';
@ -42,15 +42,13 @@ export class AuthInterceptor implements HttpInterceptor {
const token = user?.authorization || '';
req = req.clone({
headers: req.headers
.set('Authorization', token)
.set('Pragma', 'no-cache'),
headers: req.headers.set('Authorization', token).set('Pragma', 'no-cache'),
});
return next.handle(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401 && renew) {
return this.authService.loginSilent().pipe(
return from(this.authService.loginSilent()).pipe(
catchError(() => {
this.authService.logoutRedirect(this.location.path());

141
frontend/src/app/shared/services/auth.service.ts

@ -6,45 +6,44 @@
*/
import { Injectable } from '@angular/core';
import { Log, User, UserManager, WebStorageStateStore } from 'oidc-client';
import { concat, Observable, of, ReplaySubject, throwError, TimeoutError } from 'rxjs';
import { delay, mergeMap, retryWhen, take, timeout } from 'rxjs/operators';
import { ApiUrlConfig, Types } from '@app/framework';
import { Log, User, UserManager } from 'oidc-client-ts';
import { Observable, ReplaySubject } from 'rxjs';
import { ApiUrlConfig } from '@app/framework';
export class Profile {
public get id(): string {
public get id() {
return this.user.profile['sub'];
}
public get email(): string {
public get email() {
return this.user.profile['email']!;
}
public get displayName(): string {
return this.user.profile['urn:squidex:name'];
public get displayName() {
return this.user.profile['urn:squidex:name'] as string;
}
public get pictureUrl(): string {
public get pictureUrl() {
return this.user.profile['urn:squidex:picture'];
}
public get notifoToken(): string | undefined {
return this.user.profile['urn:squidex:notifo'];
return this.user.profile['urn:squidex:notifo'] as string | undefined;
}
public get isExpired(): boolean {
public get isExpired() {
return this.user.expired || false;
}
public get accessToken(): string {
public get accessToken() {
return this.user.access_token;
}
public get authorization(): string {
public get authorization() {
return `${this.user!.token_type} ${this.user.access_token}`;
}
public get token(): string {
public get token() {
return `subject:${this.id}`;
}
@ -94,19 +93,16 @@ export class AuthService {
return;
}
Log.logger = console;
Log.setLogger(console);
this.userManager = new UserManager({
client_id: 'squidex-frontend',
scope: 'squidex-api openid profile email permissions',
response_type: 'code',
redirect_uri: apiUrl.buildUrl('login;'),
post_logout_redirect_uri: apiUrl.buildUrl('logout'),
silent_redirect_uri: apiUrl.buildUrl('client-callback-silent.html'),
popup_redirect_uri: apiUrl.buildUrl('client-callback-popup.html'),
authority: apiUrl.buildUrl('identity-server/'),
userStore: new WebStorageStateStore({ store: window.localStorage || window.sessionStorage }),
automaticSilentRenew: true,
});
this.userManager.events.addUserLoaded(user => {
@ -132,80 +128,49 @@ export class AuthService {
this.userManager.signinRedirect({ state: { redirectPath } });
}
public logoutRedirectComplete(): Observable<string | undefined> {
return new Observable<string | undefined>(observer => {
this.userManager.signoutRedirectCallback()
.then(x => {
observer.next(x.state?.redirectPath);
observer.complete();
}, err => {
observer.error(err);
observer.complete();
});
});
public async logoutRedirectComplete() {
const result = await this.userManager.signoutRedirectCallback();
return getRedirectPath(result.userState);
}
public loginPopup(redirectPath: string): Observable<string | undefined> {
return new Observable<string | undefined>(observer => {
this.userManager.signinPopup({ state: { redirectPath } })
.then(x => {
observer.next(x.state?.redirectPath);
observer.complete();
}, err => {
observer.error(err);
observer.complete();
});
});
public async loginPopup(redirectPath: string) {
const result = await this.userManager.signinPopup({ state: { redirectPath } });
return getRedirectPath(result.state);
}
public loginRedirectComplete(): Observable<string | undefined> {
return new Observable<string | undefined>(observer => {
this.userManager.signinRedirectCallback()
.then(x => {
observer.next(x.state?.redirectPath);
observer.complete();
}, err => {
observer.error(err);
observer.complete();
});
});
public async loginRedirectComplete() {
const result = await this.userManager.signinRedirectCallback();
return getRedirectPath(result.state);
}
public loginSilent(): Observable<Profile> {
const observable =
new Observable<Profile>(observer => {
this.userManager.signinSilent()
.then(x => {
observer.next(AuthService.createProfile(x));
observer.complete();
}, err => {
observer.error(err);
observer.complete();
});
});
return observable.pipe(
timeout(2000),
retryWhen(errors =>
concat(
errors.pipe(
mergeMap(e => (Types.is(e, TimeoutError) ? of(e) : throwError(() => e))),
delay(500),
take(5)),
throwError(() => new Error('Retry limit exceeded.')),
),
),
);
}
private static createProfile(user: User): Profile {
return new Profile(user);
public async loginSilent() {
const MAX_ATTEMPTS = 5;
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
const promiseTimeout = delayPromise(2000);
const promiseUser = this.userManager.signinRedirectCallback();
const result = Promise.race([promiseTimeout, promiseUser]);
if (result === promiseUser) {
return getProfile(await promiseUser);
}
if (attempt < MAX_ATTEMPTS) {
await delayPromise(500);
}
}
throw new Error('Retry limit exceeded.');
}
private checkState(promise: Promise<User | null>) {
promise.then(user => {
if (user) {
this.user$.next(AuthService.createProfile(user));
this.user$.next(getProfile(user));
} else {
this.user$.next(null);
}
@ -218,3 +183,17 @@ export class AuthService {
});
}
}
function getProfile(user: User): Profile {
return new Profile(user);
}
function getRedirectPath(state: any) {
return state?.['redirectPath'] as string | undefined;
}
function delayPromise(ms: number) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}

2
frontend/src/app/shell/pages/home/home-page.component.html

@ -9,7 +9,7 @@
</button>
</div>
<div class="login-info login-element" *ngIf="!isIE()">
<div class="login-info login-element" *ngIf="!isInternetExplorer()">
{{ 'start.loginHint' | sqxTranslate }}
</div>
</div>

27
frontend/src/app/shell/pages/home/home-page.component.ts

@ -26,29 +26,26 @@ export class HomePageComponent {
) {
}
public login() {
public async login() {
const redirectPath =
this.route.snapshot.queryParams.redirectPath ||
this.location.path();
if (this.isIE()) {
if (this.isInternetExplorer()) {
this.authService.loginRedirect(redirectPath);
} else {
this.authService.loginPopup(redirectPath)
.subscribe({
next: path => {
path ||= '/app';
this.router.navigateByUrl(path);
},
error: () => {
this.showLoginError = true;
},
});
return;
}
try {
const path = await this.authService.loginPopup(redirectPath);
this.router.navigateByUrl(path || '/app', { replaceUrl: true });
} catch {
this.router.navigate(['/'], { replaceUrl: true });
}
}
public isIE() {
public isInternetExplorer() {
return !!navigator.userAgent.match(/Trident/g) || !!navigator.userAgent.match(/MSIE/g);
}
}

18
frontend/src/app/shell/pages/login/login-page.component.ts

@ -20,17 +20,13 @@ export class LoginPageComponent implements OnInit {
) {
}
public ngOnInit() {
this.authService.loginRedirectComplete()
.subscribe({
next: path => {
path ||= '/app';
public async ngOnInit() {
try {
const path = await this.authService.loginRedirectComplete();
this.router.navigateByUrl(path, { replaceUrl: true });
},
error: () => {
this.router.navigate(['/'], { replaceUrl: true });
},
});
this.router.navigateByUrl(path || '/app', { replaceUrl: true });
} catch {
this.router.navigate(['/'], { replaceUrl: true });
}
}
}

18
frontend/src/app/shell/pages/logout/logout-page.component.ts

@ -20,17 +20,13 @@ export class LogoutPageComponent implements OnInit {
) {
}
public ngOnInit() {
this.authService.logoutRedirectComplete()
.subscribe({
next: path => {
path ||= '/';
public async ngOnInit() {
try {
const path = await this.authService.logoutRedirectComplete();
this.router.navigateByUrl(path, { replaceUrl: true });
},
error: () => {
this.router.navigate(['/'], { replaceUrl: true });
},
});
this.router.navigateByUrl(path || '/app', { replaceUrl: true });
} catch {
this.router.navigate(['/'], { replaceUrl: true });
}
}
}

Loading…
Cancel
Save