Skip to content

Commit 66ed9f2

Browse files
Rough implementation of OpenID Connect. (#1135)
* Rough implementation of OpenID Connect. * Implement refresh token for OIDC. * Use child class for tokens to override parsing implementations. * Override token URL to use the backend API. * feat: implement middleware system & basic middlewares --------- Co-authored-by: Christopher Debove <christopher@checkmarble.com>
1 parent 900f61e commit 66ed9f2

35 files changed

Lines changed: 1150 additions & 268 deletions

bun.lock

Lines changed: 42 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/app-builder/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"@tanstack/react-query-devtools": "5.83.1",
7979
"@tanstack/react-table": "^8.21.2",
8080
"@tanstack/react-virtual": "3.13.12",
81+
"arctic": "^3.7.0",
8182
"autosuggest-highlight": "^3.3.4",
8283
"class-variance-authority": "^0.7.1",
8384
"clsx": "^2.1.1",
@@ -111,6 +112,8 @@
111112
"react-i18next": "^15.4.1",
112113
"reactflow": "^11.11.4",
113114
"remeda": "^2.21.2",
115+
"remix-auth-oauth2": "^3.4.1",
116+
"remix-auth-openid": "^0.3.0",
114117
"remix-i18next": "^6.4.1",
115118
"remix-utils": "^7.7.0",
116119
"sharpstate": "^0.0.13",
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface MiddlewareConfig {
2+
GlobalMiddlewares: readonly [];
3+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
export type Expand<T> = T extends object
2+
? T extends infer O
3+
? O extends Function
4+
? O
5+
: {
6+
[K in keyof O]: O[K];
7+
}
8+
: never
9+
: T;
10+
11+
export type DataReturnType<Data, TContext> = {
12+
data: Data;
13+
pushHeader: (name: string, value: string) => void;
14+
__context: TContext;
15+
__headers: HeaderEntry[];
16+
};
17+
18+
export type HeaderEntry = [string, string];
19+
20+
export type NextFunctionArgs<TContext = Record<string, unknown>> = {
21+
context: TContext;
22+
headers?: HeaderEntry[];
23+
};
24+
25+
export type NextFunction = <TOutContext = {}>(
26+
args?: NextFunctionArgs<TOutContext>,
27+
) => Promise<DataReturnType<any, TOutContext>>;
28+
29+
export type ExitFunction = <TData, TOutContext>(
30+
exitValue: TData | DataWithOptions<TData>,
31+
) => DataReturnType<TData, TOutContext>;
32+
33+
export type DataFunctionArgs<TInContext> = {
34+
request: Request;
35+
params: Record<string, string>;
36+
context: TInContext;
37+
};
38+
39+
export type MiddlewareFunction<in out TInContext = any, TOutContext = any> = (
40+
args: DataFunctionArgs<TInContext>,
41+
next: NextFunction,
42+
exit: ExitFunction,
43+
) => Promise<DataReturnType<any, TOutContext>>;
44+
45+
export type MiddlewareObject<
46+
TDependencies extends readonly MiddlewareObject[] = any,
47+
TOutContext = any,
48+
> = {
49+
deps: readonly [...TDependencies];
50+
fn: MiddlewareFunction<Expand<MergeMiddlewareContext<TDependencies>>, TOutContext>;
51+
};
52+
53+
export type MergeMiddlewareContext<T extends readonly MiddlewareObject[]> = T extends readonly [
54+
infer M,
55+
]
56+
? M extends MiddlewareObject<any, infer TOutContext>
57+
? TOutContext
58+
: never
59+
: T extends readonly [infer M, ...infer R extends readonly MiddlewareObject[]]
60+
? M extends MiddlewareObject<any, infer TOutContext>
61+
? TOutContext & MergeMiddlewareContext<R>
62+
: never
63+
: {};
64+
65+
export type ExecutionEnvironmentQueueItem = {
66+
data: { context: any } | undefined;
67+
};
68+
69+
export type ExecutionEnvironment = {
70+
queue: Map<Function, ExecutionEnvironmentQueueItem>;
71+
request: Request;
72+
params: Record<string, string>;
73+
context: Record<string, unknown>;
74+
};
75+
76+
export type DataWithOptions<Data> = {
77+
__dataObject: true;
78+
data: Data;
79+
headers?: HeaderEntry[];
80+
};
81+
82+
export type ServerFnResult<Data> = Promise<Data | DataWithOptions<Data>>;
83+
84+
export type ServerFunction<TContext, Data> = (
85+
args: DataFunctionArgs<TContext>,
86+
) => ServerFnResult<Data>;
87+
88+
export type RemixDataFunctionArgs = {
89+
request: Request;
90+
params: Record<string, string>;
91+
};
92+
93+
export type TypedResponse<T = unknown> = T extends Response
94+
? T
95+
: Omit<Response, 'json'> & { json(): Promise<T> };
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { afterEach, expect, test, vi } from 'vitest';
2+
import type { MiddlewareFunction, ServerFunction } from './middleware-types';
3+
import {
4+
cleanGlobalMiddlewares,
5+
createMiddleware,
6+
createMiddlewareWithGlobalContext,
7+
createServerFn,
8+
setGlobalMiddlewares,
9+
} from './requests';
10+
11+
afterEach(() => {
12+
cleanGlobalMiddlewares();
13+
});
14+
15+
test('server function should be called with the correct context', async () => {
16+
const mfn = vi.fn().mockImplementation(async function m1(_, next) {
17+
return next({ context: { foo: 'bar' } });
18+
}) satisfies MiddlewareFunction<any, any>;
19+
const m1 = createMiddleware([], mfn);
20+
21+
const sfn = vi.fn().mockImplementation(async function fn() {
22+
return null;
23+
}) satisfies ServerFunction<any, any>;
24+
const s = createServerFn([m1], sfn);
25+
26+
const req = new Request('https://example.com');
27+
const args = { request: req, params: {}, context: {} };
28+
const res = await s(args);
29+
const json = await res.json();
30+
31+
expect(json).toEqual(null);
32+
expect(sfn).toHaveBeenCalledTimes(1);
33+
expect(sfn).toHaveBeenCalledWith(expect.objectContaining({ context: { foo: 'bar' } }));
34+
});
35+
36+
test('server function with 2 middlewares depending on same dep propagates context correctly', async () => {
37+
const depFn = vi.fn().mockImplementation(async function dep({ context }, next) {
38+
// This dependency sets base context.
39+
return next({ context: { depValue: 'value-from-dep' } });
40+
}) satisfies MiddlewareFunction<any, any>;
41+
const dep = createMiddleware([], depFn);
42+
43+
// Middlewares that depend on 'dep'
44+
const aFn = vi.fn().mockImplementation(async function a({ context }, next) {
45+
// Expects depValue set by dependency
46+
expect(context.depValue).toBe('value-from-dep');
47+
return next({ context: { aPassed: true } });
48+
}) satisfies MiddlewareFunction<any, any>;
49+
const a = createMiddleware([dep], aFn);
50+
51+
const bFn = vi.fn().mockImplementation(async function b({ context }, next) {
52+
// Expects depValue set by dependency
53+
expect(context.depValue).toBe('value-from-dep');
54+
return next({ context: { bPassed: true } });
55+
}) satisfies MiddlewareFunction<any, any>;
56+
const b = createMiddleware([dep], bFn);
57+
58+
const sFn = vi.fn<ServerFunction<any, any>>().mockImplementation(async function handler({
59+
context,
60+
}) {
61+
// All context from previous middlewares must be present
62+
expect(context).toEqual(
63+
expect.objectContaining({
64+
aPassed: true,
65+
bPassed: true,
66+
}),
67+
);
68+
return { ok: true, context };
69+
});
70+
const serverFn = createServerFn([a, b], sFn);
71+
72+
const req = new Request('https://example.com');
73+
const args = { request: req, params: {}, context: {} };
74+
const res = await serverFn(args);
75+
const json = await res.json();
76+
77+
expect(json.ok).toBe(true);
78+
expect(json.context).toEqual(
79+
expect.objectContaining({
80+
aPassed: true,
81+
bPassed: true,
82+
}),
83+
);
84+
85+
// depFn called once per middleware (since both depend on it)
86+
expect(depFn).toHaveBeenCalledTimes(1);
87+
expect(aFn).toHaveBeenCalledTimes(1);
88+
expect(bFn).toHaveBeenCalledTimes(1);
89+
expect(sFn).toHaveBeenCalledTimes(1);
90+
});
91+
92+
test('should propagate global middleware contexts correctly', async () => {
93+
const gFn = vi.fn().mockImplementation(async function g(_, next) {
94+
return next({ context: { gValue: 'value-from-global' } });
95+
}) satisfies MiddlewareFunction<any, any>;
96+
const g = createMiddleware([], gFn);
97+
98+
setGlobalMiddlewares(g);
99+
100+
const mFn = vi.fn<MiddlewareFunction<any, any>>().mockImplementation(async function m(
101+
{ context },
102+
next,
103+
) {
104+
expect(context.gValue).toBe('value-from-global');
105+
return next({ context: { mValue: 'value-from-middleware' } });
106+
}) satisfies MiddlewareFunction<any, any>;
107+
const m = createMiddlewareWithGlobalContext([], mFn);
108+
109+
const sFn = vi.fn<ServerFunction<any, any>>().mockImplementation(async function handler({
110+
context,
111+
}) {
112+
expect(context).toEqual(expect.objectContaining({ gValue: 'value-from-global' }));
113+
return { ok: true, context };
114+
});
115+
const serverFn = createServerFn([m], sFn);
116+
117+
const req = new Request('https://example.com');
118+
const args = { request: req, params: {}, context: {} };
119+
const res = await serverFn(args);
120+
const json = await res.json();
121+
122+
expect(json.ok).toBe(true);
123+
expect(json.context).toEqual(
124+
expect.objectContaining({
125+
gValue: 'value-from-global',
126+
mValue: 'value-from-middleware',
127+
}),
128+
);
129+
});

0 commit comments

Comments
 (0)