Compare commits

..

8 Commits

Author SHA1 Message Date
e662d7d123 fix: fix solid-js adapter and exclude spec from build
Some checks failed
Build, Lint & Test Lib / Build, Lint and Test Library (push) Has been cancelled
2025-03-04 23:23:36 +08:00
dff1e1f9a6 feat: add solid-js support 2025-03-04 22:02:42 +08:00
3aabcd6442 fix: fix router navigateUrl
Some checks failed
Build, Lint & Test Lib / Build, Lint and Test Library (push) Has been cancelled
2025-02-21 05:30:31 +08:00
0d957dfb1c feat: support client_secret 2025-02-21 03:53:46 +08:00
144e4c2f97 fix: fix deps 2025-02-21 01:03:03 +08:00
de07175ff4 fix: fix building 2025-02-21 00:52:47 +08:00
41f2b04c45 refactor: switch from jsdom to happy-dom 2025-02-18 16:00:16 +08:00
ba13828093 feat: add more auth features and remove auth module 2025-02-07 16:59:58 +08:00
21 changed files with 1005 additions and 201 deletions

View File

@@ -7,11 +7,11 @@ import {
provideAuth, provideAuth,
withDefaultFeatures, withDefaultFeatures,
} from 'oidc-client-rx'; } from 'oidc-client-rx';
import { withTanstackRouter } from 'oidc-client-rx/adapters/@tanstack/react-router';
import { import {
InjectorContextVoidInjector, InjectorContextVoidInjector,
InjectorProvider, InjectorProvider,
} from 'oidc-client-rx/adapters/react'; } from 'oidc-client-rx/adapters/react';
import { withTanstackRouter } from 'oidc-client-rx/adapters/tanstack-router';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import { routeTree } from './routeTree.gen'; import { routeTree } from './routeTree.gen';

View File

@@ -1,5 +1,6 @@
{ {
"name": "oidc-client-rx", "name": "oidc-client-rx",
"version": "0.1.0-alpha.8",
"homepage": "https://github.com/lonelyhentxi/oidc-client-rx", "homepage": "https://github.com/lonelyhentxi/oidc-client-rx",
"author": "lonelyhentxi", "author": "lonelyhentxi",
"description": "ReactiveX enhanced OIDC and OAuth2 protocol support for browser-based JavaScript applications", "description": "ReactiveX enhanced OIDC and OAuth2 protocol support for browser-based JavaScript applications",
@@ -10,7 +11,6 @@
"bugs": { "bugs": {
"url": "https://github.com/lonelyhentxi/oidc-client-rx/issues" "url": "https://github.com/lonelyhentxi/oidc-client-rx/issues"
}, },
"version": "0.1.0",
"type": "module", "type": "module",
"exports": { "exports": {
".": { ".": {
@@ -23,59 +23,71 @@
"import": "./dist/adapters/react/index.js", "import": "./dist/adapters/react/index.js",
"require": "./dist/adapters/react.cjs" "require": "./dist/adapters/react.cjs"
}, },
"./adapters/tanstack-router": { "./adapters/solid-js": {
"types": "./dist/adapters/tanstack-router/index.d.ts", "types": "./dist/adapters/solid-js/index.d.ts",
"import": "./dist/adapters/tanstack-router/index.js", "import": "./dist/adapters/solid-js/index.js",
"require": "./dist/adapters/tanstack-router.cjs" "require": "./dist/adapters/solid-js.cjs"
},
"./adapters/@tanstack/react-router": {
"types": "./dist/adapters/@tanstack/react-router.d.ts",
"import": "./dist/adapters/@tanstack/react-router.js",
"require": "./dist/adapters/@tanstack/react-router.cjs"
},
"./adapters/@tanstack/solid-router": {
"types": "./dist/adapters/@tanstack/solid-router.d.ts",
"import": "./dist/adapters/@tanstack/solid-router.js",
"require": "./dist/adapters/@tanstack/solid-router.cjs"
} }
}, },
"main": "./dist/index.cjs", "main": "./dist/index.cjs",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"files": ["dist"], "files": ["dist", "licenses", "LICENSE", "README.md"],
"scripts": { "scripts": {
"build": "rslib build", "build": "rslib build",
"dev": "rslib build --watch", "dev": "rslib build --watch",
"test": "vitest --coverage", "test": "vitest --coverage",
"test-ci": "vitest --watch=false --coverage", "test-ci": "vitest --watch=false --coverage",
"pack": "npm run build && npm pack ./dist", "prepublishOnly": "npm run build",
"publish": "npm run build && npm publish ./dist",
"lint": "ultracite lint", "lint": "ultracite lint",
"format": "ultracite format", "format": "ultracite format",
"cli": "tsx scripts/cli.ts" "cli": "tsx scripts/cli.ts"
}, },
"dependencies": { "dependencies": {
"@ngify/http": "^2.0.4", "@ngify/http": "^2.0.4",
"@outposts/injection-js": "^2.5.1" "@outposts/injection-js": "^2.5.1",
"rfc4648": "^1.5.0"
}, },
"peerDependencies": { "peerDependencies": {
"@tanstack/react-router": "*", "@tanstack/react-router": "*",
"@tanstack/solid-router": "*",
"react": ">=16.8.0", "react": ">=16.8.0",
"rxjs": "^7.4.0||>=8.0.0" "rxjs": "^7.4.0||>=8.0.0",
"solid-js": "^1"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.9.4", "@biomejs/biome": "1.9.4",
"@biomejs/js-api": "0.7.1", "@biomejs/js-api": "0.7.1",
"@biomejs/wasm-nodejs": "^1.9.4", "@biomejs/wasm-nodejs": "^1.9.4",
"@playwright/test": "^1.49.1", "@playwright/test": "^1.49.1",
"@rslib/core": "^0.4.0", "@rslib/core": "^0.5.3",
"@swc/core": "^1.10.12", "@swc/core": "^1.10.12",
"@tanstack/react-router": "^1.99.6", "@tanstack/react-router": "^1.112.11",
"@types/jsdom": "^21.1.7", "@tanstack/solid-router": "^1.112.11",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^22.12.0", "@types/node": "^22.12.0",
"@types/react": "^19.0.8", "@types/react": "^19.0.8",
"@vitest/coverage-v8": "^3.0.4", "@vitest/coverage-v8": "^3.0.4",
"commander": "^13.1.0", "commander": "^13.1.0",
"jsdom": "^26.0.0", "happy-dom": "^17.1.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"oxc-parser": "^0.48.1", "oxc-parser": "^0.54.0",
"oxc-walker": "^0.2.2", "oxc-walker": "^0.2.2",
"playwright": "^1.50.0", "playwright": "^1.50.0",
"react": "^19.0.0", "react": "^19.0.0",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rfc4648": "^1.5.0",
"rxjs": "^7.4.0", "rxjs": "^7.4.0",
"solid-js": "^1.9.5",
"tsx": "^4.19.2", "tsx": "^4.19.2",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"ultracite": "^4.1.15", "ultracite": "^4.1.15",
@@ -89,6 +101,12 @@
}, },
"@tanstack/react-router": { "@tanstack/react-router": {
"optional": true "optional": true
},
"@tanstack/solid-router": {
"optional": true
},
"solid-js": {
"optional": true
} }
}, },
"keywords": [ "keywords": [

829
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,9 +11,14 @@ export default defineConfig({
bundle: false, bundle: false,
dts: { dts: {
bundle: false, bundle: false,
build: true, build: false,
distPath: './dist', distPath: './dist',
}, },
source: {
entry: {
index: ['src/**/*.ts', '!**/*.spec.ts', '!src/testing/**/*'],
},
},
}, },
{ {
format: 'cjs', format: 'cjs',
@@ -24,7 +29,11 @@ export default defineConfig({
entry: { entry: {
index: './src/index.ts', index: './src/index.ts',
'adapters/react': './src/adapters/react/index.ts', 'adapters/react': './src/adapters/react/index.ts',
'adapters/tanstack-router': './src/adapters/tanstack-router/index.ts', 'adapters/solid-js': './src/adapters/solid-js/index.ts',
'adapters/@tanstack/react-router':
'./src/adapters/@tanstack/react-router.ts',
'adapters/@tanstack/solid-router':
'./src/adapters/@tanstack/solid-router.ts',
}, },
}, },
}, },

View File

@@ -1,9 +1,9 @@
import { InjectionToken, inject } from '@outposts/injection-js'; import { InjectionToken, inject } from '@outposts/injection-js';
import type { Router } from '@tanstack/react-router'; import type { AnyRouter } from '@tanstack/react-router';
import type { AuthFeature } from '../../features'; import type { AuthFeature } from '../../features';
import { AbstractRouter } from '../../router'; import { AbstractRouter, ROUTER_ABS_PATH_PATTERN } from '../../router';
export type TanStackRouter = Router<any, any, any, any, any, any>; export type TanStackRouter = AnyRouter;
export const TANSTACK_ROUTER = new InjectionToken<TanStackRouter>( export const TANSTACK_ROUTER = new InjectionToken<TanStackRouter>(
'TANSTACK_ROUTER' 'TANSTACK_ROUTER'
@@ -14,7 +14,7 @@ export class TanStackRouterAdapter implements AbstractRouter<string> {
navigateByUrl(url: string): void { navigateByUrl(url: string): void {
this.router.navigate({ this.router.navigate({
href: url, href: ROUTER_ABS_PATH_PATTERN.test(url) ? url : `/${url}`,
}); });
} }

View File

@@ -0,0 +1,41 @@
import { InjectionToken, inject } from '@outposts/injection-js';
import type { AnyRouter } from '@tanstack/solid-router';
import type { AuthFeature } from '../../features';
import { AbstractRouter, ROUTER_ABS_PATH_PATTERN } from '../../router';
export type TanStackRouter = AnyRouter;
export const TANSTACK_ROUTER = new InjectionToken<TanStackRouter>(
'TANSTACK_ROUTER'
);
export class TanStackRouterAdapter implements AbstractRouter<string> {
private router = inject(TANSTACK_ROUTER);
navigateByUrl(url: string): void {
this.router.navigate({
href: ROUTER_ABS_PATH_PATTERN.test(url) ? url : `/${url}`,
});
}
getCurrentNavigation() {
return {
extractedUrl: this.router.state.location.href,
};
}
}
export function withTanstackRouter(router: TanStackRouter): AuthFeature {
return {
ɵproviders: [
{
provide: TANSTACK_ROUTER,
useValue: router,
},
{
provide: AbstractRouter,
useClass: TanStackRouterAdapter,
},
],
};
}

View File

@@ -6,7 +6,7 @@ import {
useContext, useContext,
useMemo, useMemo,
} from 'react'; } from 'react';
import { OidcSecurityService } from '../..'; import { OidcSecurityService } from '../../oidc.security.service';
export const InjectorContextVoidInjector: Injector = { export const InjectorContextVoidInjector: Injector = {
get: <T>(_token: Type<T> | InjectionToken<T>, _notFoundValue?: T): T => { get: <T>(_token: Type<T> | InjectionToken<T>, _notFoundValue?: T): T => {

View File

@@ -0,0 +1,43 @@
import type { InjectionToken, Injector, Type } from '@outposts/injection-js';
import {
type FlowProps,
createContext,
createMemo,
mergeProps,
splitProps,
useContext,
} from 'solid-js';
import { OidcSecurityService } from '../../oidc.security.service';
export const InjectorContextVoidInjector: Injector = {
get: <T>(_token: Type<T> | InjectionToken<T>, _notFoundValue?: T): T => {
throw new Error('Please wrap with a InjectorContext.Provider first');
},
};
export const InjectorContext = createContext<Injector>(
InjectorContextVoidInjector
);
export function InjectorProvider(props: FlowProps<{ injector: Injector }>) {
const [local, others] = splitProps(props, ['injector']);
const providerProps = mergeProps(others, { value: local.injector });
return InjectorContext.Provider(providerProps);
}
export function useInjector() {
return useContext(InjectorContext);
}
export function useOidcClient() {
const injector = useInjector();
const oidcSecurityService = createMemo(() =>
injector.get(OidcSecurityService)
);
return {
injector,
oidcSecurityService: oidcSecurityService(),
};
}

View File

@@ -1,6 +1,6 @@
import { Injectable, inject } from '@outposts/injection-js'; import { Injectable, inject } from '@outposts/injection-js';
import type { Observable } from 'rxjs'; import { type Observable, of } from 'rxjs';
import { map } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
import type { AuthOptions } from '../auth-options'; import type { AuthOptions } from '../auth-options';
import { AuthStateService } from '../auth-state/auth-state.service'; import { AuthStateService } from '../auth-state/auth-state.service';
import { ConfigurationService } from '../config/config.service'; import { ConfigurationService } from '../config/config.service';
@@ -126,7 +126,7 @@ function checkAuth(
configId?: string configId?: string
): Observable<boolean> { ): Observable<boolean> {
return configurationService.getOpenIDConfiguration(configId).pipe( return configurationService.getOpenIDConfiguration(configId).pipe(
map((configuration) => { switchMap((configuration) => {
const isAuthenticated = const isAuthenticated =
authStateService.areAuthStorageTokensValid(configuration); authStateService.areAuthStorageTokensValid(configuration);
@@ -137,13 +137,16 @@ function checkAuth(
if (!isAuthenticated) { if (!isAuthenticated) {
autoLoginService.saveRedirectRoute(configuration, url); autoLoginService.saveRedirectRoute(configuration, url);
if (authOptions) { if (authOptions) {
loginService.login(configuration, authOptions); return loginService
} else { .login(configuration, authOptions)
loginService.login(configuration); .pipe(switchMap(() => of(isAuthenticated)));
} }
return loginService
.login(configuration)
.pipe(switchMap(() => of(isAuthenticated)));
} }
return isAuthenticated; return of(isAuthenticated);
}) })
); );
} }

View File

@@ -40,6 +40,12 @@ export interface OpenIdConfiguration {
* or if it contains additional audiences not trusted by the Client. * or if it contains additional audiences not trusted by the Client.
*/ */
clientId?: string; clientId?: string;
/**
* @dangerous
* @see [client secret is missing](https://github.com/damienbod/angular-auth-oidc-client/issues/399)
* The client secret. For some oidc service the must provide this.
*/
clientSecret?: string;
/** /**
* `code`, `id_token token` or `id_token`. * `code`, `id_token token` or `id_token`.
* Name of the flow which can be configured. * Name of the flow which can be configured.

View File

@@ -1,16 +1,16 @@
import type { HttpFeature } from '@ngify/http'; import type { HttpFeature } from '@ngify/http';
import type { Provider } from '@outposts/injection-js'; import type { Provider } from '@outposts/injection-js';
import { DOCUMENT } from './dom'; import { DOCUMENT } from '../dom';
import { provideHttpClient } from './http'; import { provideHttpClient } from '../http';
import { import {
AbstractRouter, AbstractRouter,
VanillaHistoryRouter, VanillaHistoryRouter,
VanillaLocationRouter, VanillaLocationRouter,
} from './router'; } from '../router';
import { AbstractSecurityStorage } from './storage/abstract-security-storage'; import { AbstractSecurityStorage } from '../storage/abstract-security-storage';
import { DefaultLocalStorageService } from './storage/default-localstorage.service'; import { DefaultLocalStorageService } from '../storage/default-localstorage.service';
import { DefaultSessionStorageService } from './storage/default-sessionstorage.service'; import { DefaultSessionStorageService } from '../storage/default-sessionstorage.service';
import { PLATFORM_ID } from './utils/platform-provider/platform.provider'; import { PLATFORM_ID } from '../utils/platform-provider/platform.provider';
/** /**
* A feature to be used with `provideAuth`. * A feature to be used with `provideAuth`.

8
src/features/index.ts Normal file
View File

@@ -0,0 +1,8 @@
export type * from './core';
export * from './core';
export {
CHECK_AUTH_RESULT_EVENT,
withCheckAuthResultEvent,
type CheckAuthResultEventType,
type WithCheckAuthResultEventProps,
} from './with-check-auth-result-event';

View File

@@ -0,0 +1,45 @@
import { InjectionToken, inject } from '@outposts/injection-js';
import { type Observable, filter, shareReplay } from 'rxjs';
import { EventTypes } from '../public-events/event-types';
import { PublicEventsService } from '../public-events/public-events.service';
import type { AuthFeature } from './core';
export type CheckAuthResultEventType =
| { type: EventTypes.CheckingAuthFinished }
| {
type: EventTypes.CheckingAuthFinishedWithError;
value: string;
};
export const CHECK_AUTH_RESULT_EVENT = new InjectionToken<
Observable<CheckAuthResultEventType>
>('CHECK_AUTH_RESULT_EVENT');
export interface WithCheckAuthResultEventProps {
shareReplayCount?: number;
}
export function withCheckAuthResultEvent({
shareReplayCount = 1,
}: WithCheckAuthResultEventProps = {}): AuthFeature {
return {
ɵproviders: [
{
provide: CHECK_AUTH_RESULT_EVENT,
useFactory: () => {
const publishEventService = inject(PublicEventsService);
return publishEventService.registerForEvents().pipe(
filter(
(e) =>
e.type === EventTypes.CheckingAuthFinishedWithError ||
e.type === EventTypes.CheckingAuthFinished
),
shareReplay(shareReplayCount)
);
},
deps: [PublicEventsService],
},
],
};
}

View File

@@ -61,12 +61,11 @@ describe('RefreshSessionIframeService ', () => {
); );
(refreshSessionIframeService as any).initSilentRenewRequest(); (refreshSessionIframeService as any).initSilentRenewRequest();
expect(dispatchEventSpy).toHaveBeenCalledOnce();
expect(dispatchEventSpy).toHaveBeenCalledExactlyOnceWith( expect(dispatchEventSpy.mock.calls[0][0]).toBeInstanceOf(CustomEvent);
new CustomEvent('oidc-silent-renew-init', { expect(
detail: expect.any(Number), (dispatchEventSpy.mock.calls[0][0] as CustomEvent).detail
}) ).toBeTypeOf('number');
);
}); });
}); });
}); });

View File

@@ -1,5 +1,6 @@
import { Injectable, inject } from '@outposts/injection-js'; import { Injectable, inject } from '@outposts/injection-js';
import { type Observable, of, throwError } from 'rxjs'; import { BehaviorSubject, type Observable, of, throwError } from 'rxjs';
import { MockUtil } from 'src/utils/reflect';
import type { AuthOptions } from '../auth-options'; import type { AuthOptions } from '../auth-options';
import type { OpenIdConfiguration } from '../config/openid-configuration'; import type { OpenIdConfiguration } from '../config/openid-configuration';
import { StoragePersistenceService } from '../storage/storage-persistence.service'; import { StoragePersistenceService } from '../storage/storage-persistence.service';
@@ -24,6 +25,7 @@ export class LoginService {
private readonly popupService = inject(PopUpService); private readonly popupService = inject(PopUpService);
@MockUtil({ implementation: () => new BehaviorSubject(undefined) })
login( login(
configuration: OpenIdConfiguration | null, configuration: OpenIdConfiguration | null,
authOptions?: AuthOptions authOptions?: AuthOptions

View File

@@ -30,8 +30,10 @@ export abstract class AbstractRouter<
abstract getCurrentNavigation(): NAVIGATION; abstract getCurrentNavigation(): NAVIGATION;
} }
export class VanillaLocationRouter extends AbstractRouter { export const ROUTER_ABS_PATH_PATTERN = /^\//;
private document = inject(DOCUMENT);
export class VanillaLocationRouter extends AbstractRouter<string> {
protected document = inject(DOCUMENT);
private get location(): Location { private get location(): Location {
const location = this.document.defaultView?.window?.location; const location = this.document.defaultView?.window?.location;
@@ -42,7 +44,7 @@ export class VanillaLocationRouter extends AbstractRouter {
} }
navigateByUrl(url: string): void { navigateByUrl(url: string): void {
this.location.href = url; this.location.href = ROUTER_ABS_PATH_PATTERN.test(url) ? url : `/${url}`;
} }
getCurrentNavigation() { getCurrentNavigation() {
@@ -72,7 +74,11 @@ export class VanillaHistoryRouter extends AbstractRouter<string> {
} }
navigateByUrl(url: string): void { navigateByUrl(url: string): void {
this.history.pushState({}, '', url); this.history.pushState(
{},
'',
ROUTER_ABS_PATH_PATTERN.test(url) ? url : `/${url}`
);
} }
getCurrentNavigation() { getCurrentNavigation() {

View File

@@ -2,6 +2,10 @@ import { TestBed } from '@/testing';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { DefaultLocalStorageService } from './default-localstorage.service'; import { DefaultLocalStorageService } from './default-localstorage.service';
/**
* if use jsdom, then use Storage.prototype, https://github.com/jsdom/jsdom/issues/2318
*/
describe('DefaultLocalStorageService', () => { describe('DefaultLocalStorageService', () => {
let service: DefaultLocalStorageService; let service: DefaultLocalStorageService;
@@ -18,8 +22,7 @@ describe('DefaultLocalStorageService', () => {
describe('read', () => { describe('read', () => {
it('should call localstorage.getItem', () => { it('should call localstorage.getItem', () => {
// https://github.com/jsdom/jsdom/issues/2318 const spy = vi.spyOn(localStorage, 'getItem');
const spy = vi.spyOn(Storage.prototype, 'getItem');
service.read('henlo'); service.read('henlo');
@@ -29,8 +32,7 @@ describe('DefaultLocalStorageService', () => {
describe('write', () => { describe('write', () => {
it('should call localstorage.setItem', () => { it('should call localstorage.setItem', () => {
// https://github.com/jsdom/jsdom/issues/2318 const spy = vi.spyOn(localStorage, 'setItem');
const spy = vi.spyOn(Storage.prototype, 'setItem');
service.write('henlo', 'furiend'); service.write('henlo', 'furiend');
@@ -40,8 +42,7 @@ describe('DefaultLocalStorageService', () => {
describe('remove', () => { describe('remove', () => {
it('should call localstorage.removeItem', () => { it('should call localstorage.removeItem', () => {
// https://github.com/jsdom/jsdom/issues/2318 const spy = vi.spyOn(localStorage, 'removeItem');
const spy = vi.spyOn(Storage.prototype, 'removeItem');
service.remove('henlo'); service.remove('henlo');
@@ -51,8 +52,7 @@ describe('DefaultLocalStorageService', () => {
describe('clear', () => { describe('clear', () => {
it('should call localstorage.clear', () => { it('should call localstorage.clear', () => {
// https://github.com/jsdom/jsdom/issues/2318 const spy = vi.spyOn(localStorage, 'clear');
const spy = vi.spyOn(Storage.prototype, 'clear');
service.clear(); service.clear();

View File

@@ -2,6 +2,9 @@ import { TestBed } from '@/testing';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { DefaultSessionStorageService } from './default-sessionstorage.service'; import { DefaultSessionStorageService } from './default-sessionstorage.service';
/**
* if use jsdom, then use Storage.prototype, https://github.com/jsdom/jsdom/issues/2318
*/
describe('DefaultSessionStorageService', () => { describe('DefaultSessionStorageService', () => {
let service: DefaultSessionStorageService; let service: DefaultSessionStorageService;
@@ -18,8 +21,7 @@ describe('DefaultSessionStorageService', () => {
describe('read', () => { describe('read', () => {
it('should call sessionstorage.getItem', () => { it('should call sessionstorage.getItem', () => {
// https://github.com/jsdom/jsdom/issues/2318 const spy = vi.spyOn(sessionStorage, 'getItem');
const spy = vi.spyOn(Storage.prototype, 'getItem');
service.read('henlo'); service.read('henlo');
@@ -29,8 +31,7 @@ describe('DefaultSessionStorageService', () => {
describe('write', () => { describe('write', () => {
it('should call sessionstorage.setItem', () => { it('should call sessionstorage.setItem', () => {
// https://github.com/jsdom/jsdom/issues/2318 const spy = vi.spyOn(sessionStorage, 'setItem');
const spy = vi.spyOn(Storage.prototype, 'setItem');
service.write('henlo', 'furiend'); service.write('henlo', 'furiend');
@@ -40,8 +41,7 @@ describe('DefaultSessionStorageService', () => {
describe('remove', () => { describe('remove', () => {
it('should call sessionstorage.removeItem', () => { it('should call sessionstorage.removeItem', () => {
// https://github.com/jsdom/jsdom/issues/2318 const spy = vi.spyOn(sessionStorage, 'removeItem');
const spy = vi.spyOn(Storage.prototype, 'removeItem');
service.remove('henlo'); service.remove('henlo');
@@ -51,8 +51,7 @@ describe('DefaultSessionStorageService', () => {
describe('clear', () => { describe('clear', () => {
it('should call sessionstorage.clear', () => { it('should call sessionstorage.clear', () => {
// https://github.com/jsdom/jsdom/issues/2318 const spy = vi.spyOn(sessionStorage, 'clear');
const spy = vi.spyOn(Storage.prototype, 'clear');
service.clear(); service.clear();

View File

@@ -1,29 +1,13 @@
import type { Provider } from '@outposts/injection-js'; import type { Provider } from '@outposts/injection-js';
import { JSDOM } from 'jsdom'; import {
import { AbstractRouter, type Navigation, type UrlTree } from 'oidc-client-rx'; AbstractRouter,
type UrlTree,
VanillaLocationRouter,
} from 'oidc-client-rx';
export class MockRouter extends AbstractRouter { export class MockRouter extends VanillaLocationRouter {
dom = new JSDOM('', {
url: 'http://localhost',
});
navigation: Navigation = {
extractedUrl: this.parseUrl(this.dom.window.location.href),
};
navigateByUrl(url: string): void {
this.dom.reconfigure({
url: new URL(url, this.dom.window.location.href).href,
});
this.navigation = {
extractedUrl: this.parseUrl(this.dom.window.location.href),
};
}
getCurrentNavigation(): Navigation {
return this.navigation;
}
parseUrl(url: string): UrlTree { parseUrl(url: string): UrlTree {
const u = new URL(url, this.dom.window.location.href); const u = new URL(url, this.document.baseURI);
return `${u.pathname}${u.search}${u.hash}`; return `${u.pathname}${u.search}${u.hash}`;
} }
} }

View File

@@ -249,6 +249,7 @@ export class UrlService {
configuration: OpenIdConfiguration configuration: OpenIdConfiguration
): string | null { ): string | null {
const clientId = this.getClientId(configuration); const clientId = this.getClientId(configuration);
const clientSecret = this.getClientSecret(configuration);
if (!clientId) { if (!clientId) {
return null; return null;
@@ -259,6 +260,9 @@ export class UrlService {
params = params.set('client_id', clientId); params = params.set('client_id', clientId);
params = params.set('token', token); params = params.set('token', token);
params = params.set('token_type_hint', 'access_token'); params = params.set('token_type_hint', 'access_token');
if (clientSecret) {
params = params.set('client_secret', clientSecret);
}
return params.toString(); return params.toString();
} }
@@ -268,6 +272,7 @@ export class UrlService {
configuration: OpenIdConfiguration configuration: OpenIdConfiguration
): string | null { ): string | null {
const clientId = this.getClientId(configuration); const clientId = this.getClientId(configuration);
const clientSecret = this.getClientSecret(configuration);
if (!clientId) { if (!clientId) {
return null; return null;
@@ -278,6 +283,9 @@ export class UrlService {
params = params.set('client_id', clientId); params = params.set('client_id', clientId);
params = params.set('token', token); params = params.set('token', token);
params = params.set('token_type_hint', 'refresh_token'); params = params.set('token_type_hint', 'refresh_token');
if (clientSecret) {
params = params.set('client_secret', clientSecret);
}
return params.toString(); return params.toString();
} }
@@ -304,6 +312,7 @@ export class UrlService {
customTokenParams?: { [p: string]: string | number | boolean } customTokenParams?: { [p: string]: string | number | boolean }
): string | null { ): string | null {
const clientId = this.getClientId(configuration); const clientId = this.getClientId(configuration);
const clientSecret = this.getClientSecret(configuration);
if (!clientId) { if (!clientId) {
return null; return null;
@@ -313,6 +322,9 @@ export class UrlService {
params = params.set('grant_type', 'authorization_code'); params = params.set('grant_type', 'authorization_code');
params = params.set('client_id', clientId); params = params.set('client_id', clientId);
if (clientSecret) {
params = params.set('client_secret', clientSecret);
}
if (!configuration.disablePkce) { if (!configuration.disablePkce) {
const codeVerifier = this.flowsDataService.getCodeVerifier(configuration); const codeVerifier = this.flowsDataService.getCodeVerifier(configuration);
@@ -364,6 +376,7 @@ export class UrlService {
customParamsRefresh?: { [key: string]: string | number | boolean } customParamsRefresh?: { [key: string]: string | number | boolean }
): string | null { ): string | null {
const clientId = this.getClientId(configuration); const clientId = this.getClientId(configuration);
const clientSecret = this.getClientSecret(configuration);
if (!clientId) { if (!clientId) {
return null; return null;
@@ -374,6 +387,9 @@ export class UrlService {
params = params.set('grant_type', 'refresh_token'); params = params.set('grant_type', 'refresh_token');
params = params.set('client_id', clientId); params = params.set('client_id', clientId);
params = params.set('refresh_token', refreshToken); params = params.set('refresh_token', refreshToken);
if (clientSecret) {
params = params.set('client_secret', clientSecret);
}
if (customParamsRefresh) { if (customParamsRefresh) {
params = this.appendCustomParams({ ...customParamsRefresh }, params); params = this.appendCustomParams({ ...customParamsRefresh }, params);
@@ -851,6 +867,18 @@ export class UrlService {
return clientId; return clientId;
} }
private getClientSecret(configuration: OpenIdConfiguration): string | null {
const { clientSecret } = configuration;
if (!clientSecret) {
this.loggerService.logDebug(configuration, 'could not get clientSecret');
return null;
}
return clientSecret;
}
private appendCustomParams( private appendCustomParams(
customParams: { [key: string]: string | number | boolean }, customParams: { [key: string]: string | number | boolean },
params: HttpParams params: HttpParams

View File

@@ -6,7 +6,7 @@ export default defineConfig({
cacheDir: '.vitest', cacheDir: '.vitest',
test: { test: {
setupFiles: ['src/testing/init-test.ts'], setupFiles: ['src/testing/init-test.ts'],
environment: 'jsdom', environment: 'happy-dom',
include: ['src/**/*.spec.ts'], include: ['src/**/*.spec.ts'],
globals: true, globals: true,
restoreMocks: true, restoreMocks: true,
@@ -15,6 +15,14 @@ export default defineConfig({
reporter: ['text', 'json-summary', 'json'], reporter: ['text', 'json-summary', 'json'],
// If you want a coverage reports even if your tests are failing, include the reportOnFailure option // If you want a coverage reports even if your tests are failing, include the reportOnFailure option
reportOnFailure: true, reportOnFailure: true,
exclude: [
'vitest.config.ts',
'playwright.config.ts',
'rslib.config.ts',
'scripts/**',
'examples/**',
'dist/**',
],
}, },
}, },
plugins: [ plugins: [