Browse Source

Merge pull request #24050 from abpframework/feat/#23930

Angular - Improve Authentication Token Handling
pull/24492/head
Yağmur Çelik 1 month ago
committed by GitHub
parent
commit
bd63ae42d4
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      npm/ng-packs/packages/oauth/src/lib/services/index.ts
  2. 255
      npm/ng-packs/packages/oauth/src/lib/services/memory-token-storage.service.ts
  3. 7
      npm/ng-packs/packages/oauth/src/lib/services/oauth.service.ts
  4. 6
      npm/ng-packs/packages/oauth/src/lib/utils/storage.factory.ts

1
npm/ng-packs/packages/oauth/src/lib/services/index.ts

@ -3,3 +3,4 @@ export * from './oauth-error-filter.service';
export * from './remember-me.service';
export * from './browser-token-storage.service';
export * from './server-token-storage.service';
export * from './memory-token-storage.service';

255
npm/ng-packs/packages/oauth/src/lib/services/memory-token-storage.service.ts

@ -0,0 +1,255 @@
import { DestroyRef, DOCUMENT, inject, Injectable } from '@angular/core';
import { OAuthStorage } from 'angular-oauth2-oidc';
import { AbpLocalStorageService } from '@abp/ng.core';
import { fromEvent } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Injectable({
providedIn: 'root',
})
export class MemoryTokenStorageService implements OAuthStorage {
private static workerUrl: string | null = null;
private keysShouldStoreInMemory = [
'access_token',
'id_token',
'expires_at',
'id_token_claims_obj',
'id_token_expires_at',
'id_token_stored_at',
'access_token_stored_at',
'abpOAuthClientId',
'granted_scopes',
];
private worker?: any;
private port?: MessagePort;
private cache = new Map<string, string>();
private localStorageService = inject(AbpLocalStorageService);
private _document = inject(DOCUMENT);
private destroyRef = inject(DestroyRef);
private useSharedWorker = false;
constructor() {
this.initializeStorage();
this.setupCleanup();
}
private initializeStorage(): void {
console.log('Initialize Storage -->>', typeof SharedWorker !== 'undefined');
// @ts-ignore
if (typeof SharedWorker !== 'undefined') {
try {
console.log('Shared worker is loaded');
console.log('refresh view');
// Create worker from data URL to avoid path resolution issues in consuming apps
// Data URLs are deterministic - same content produces same URL across all tabs
if (!MemoryTokenStorageService.workerUrl) {
MemoryTokenStorageService.workerUrl = this.createWorkerDataUrl();
}
// @ts-ignore
this.worker = new SharedWorker(MemoryTokenStorageService.workerUrl, {
name: 'oauth-token-storage',
});
console.log('loaded worker -->>', this.worker);
this.port = this.worker.port;
this.port.start();
this.useSharedWorker = true;
this.port.onmessage = event => {
const { action, key, value } = event.data;
switch (action) {
case 'set':
this.checkAuthStateChanges(key);
this.cache.set(key, value);
break;
case 'remove':
this.cache.delete(key);
this.refreshDocument();
break;
case 'clear':
this.cache.clear();
this.refreshDocument();
break;
case 'get':
if (value !== null) {
this.cache.set(key, value);
}
break;
}
};
// Load all tokens from SharedWorker on initialization
this.keysShouldStoreInMemory.forEach(key => {
this.port?.postMessage({ action: 'get', key });
});
} catch (error) {
console.log(error);
this.useSharedWorker = false;
}
} else {
this.useSharedWorker = false;
}
}
getItem(key: string): string | null {
if (!this.keysShouldStoreInMemory.includes(key)) {
return this.localStorageService.getItem(key);
}
return this.cache.get(key) || null;
}
setItem(key: string, value: string): void {
if (!this.keysShouldStoreInMemory.includes(key)) {
this.localStorageService.setItem(key, value);
return;
}
if (this.useSharedWorker && this.port) {
this.cache.set(key, value);
this.port.postMessage({ action: 'set', key, value });
} else {
this.cache.set(key, value);
}
}
removeItem(key: string): void {
if (!this.keysShouldStoreInMemory.includes(key)) {
this.localStorageService.removeItem(key);
return;
}
if (this.useSharedWorker && this.port) {
this.cache.delete(key);
this.port.postMessage({ action: 'remove', key });
} else {
this.cache.delete(key);
}
}
clear(): void {
if (this.useSharedWorker && this.port) {
this.port.postMessage({ action: 'clear' });
}
this.cache.clear();
}
private cleanupPort(): void {
if (this.useSharedWorker && this.port) {
try {
this.port.postMessage({ action: 'disconnect' });
} catch (error) {
console.log('Error disconnecting port:', error);
}
}
}
private setupCleanup(): void {
if (this._document.defaultView) {
fromEvent(this._document.defaultView, 'beforeunload')
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => this.cleanupPort());
fromEvent(this._document.defaultView, 'pagehide')
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => this.cleanupPort());
}
}
private checkAuthStateChanges = (key: string) => {
if (key === 'access_token' && !this.cache.get('access_token')) {
this.refreshDocument();
}
};
private refreshDocument(): void {
this.cleanupPort();
setTimeout(() => {
this._document.defaultView?.location.reload();
}, 100);
}
private createWorkerDataUrl(): string {
const workerScript = `const tokenStore = new Map();
const ports = new Set();
function broadcastToOtherPorts(senderPort, message) {
const deadPorts = [];
ports.forEach(p => {
if (p !== senderPort) {
try {
p.postMessage(message);
} catch (error) {
console.log('Dead port detected during broadcast, removing...');
deadPorts.push(p);
}
}
});
deadPorts.forEach(p => ports.delete(p));
if (deadPorts.length > 0) {
console.log('Cleaned up', deadPorts.length, 'dead ports. Total ports:', ports.size);
}
}
function removePort(port) {
if (ports.has(port)) {
ports.delete(port);
console.log('Port disconnected. Total ports:', ports.size);
}
}
self.onconnect = (event) => {
const port = event.ports[0];
ports.add(port);
console.log('Port connected. Total ports:', ports.size);
port.addEventListener('messageerror', () => {
removePort(port);
});
port.onmessage = (e) => {
const { action, key, value } = e.data;
switch (action) {
case 'set':
if (key && value !== undefined) {
tokenStore.set(key, value);
broadcastToOtherPorts(port, { action: 'set', key, value });
}
break;
case 'remove':
if (key) {
tokenStore.delete(key);
broadcastToOtherPorts(port, { action: 'remove', key });
}
break;
case 'clear':
tokenStore.clear();
broadcastToOtherPorts(port, { action: 'clear' });
break;
case 'get':
if (key) {
const value = tokenStore.get(key) ?? null;
port.postMessage({ action: 'get', key, value });
}
break;
case 'disconnect':
console.log('Port requested disconnect');
removePort(port);
break;
default:
console.warn('Unknown action:', action);
}
};
port.start();
};`;
return 'data:application/javascript;base64,' + btoa(workerScript);
}
}

7
npm/ng-packs/packages/oauth/src/lib/services/oauth.service.ts

@ -110,7 +110,12 @@ export class AbpOAuthService implements IAuthService {
this.document.defaultView?.location.replace('/authorize');
return Promise.resolve();
}
return this.oAuthService.refreshToken();
try {
return this.oAuthService.refreshToken();
} catch (error) {
console.log("Error while refreshing token: ", error);
return Promise.reject();
}
}
getAccessTokenExpiration(): number {

6
npm/ng-packs/packages/oauth/src/lib/utils/storage.factory.ts

@ -1,9 +1,9 @@
import { inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { ServerTokenStorageService } from '../services/server-token-storage.service';
import { BrowserTokenStorageService } from '../services';
import { BrowserTokenStorageService, MemoryTokenStorageService } from '../services';
import { OAuthStorage } from 'angular-oauth2-oidc';
import { AbpLocalStorageService, APP_STARTED_WITH_SSR } from '@abp/ng.core';
import { APP_STARTED_WITH_SSR } from '@abp/ng.core';
export class MockStorage implements Storage {
private data = new Map<string, string>();
@ -35,5 +35,5 @@ export function oAuthStorageFactory(): OAuthStorage {
? inject(BrowserTokenStorageService)
: inject(ServerTokenStorageService);
}
return inject(AbpLocalStorageService);
return inject(MemoryTokenStorageService);
}

Loading…
Cancel
Save