oidc-mock 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +382 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +74 -0
- package/dist/config.d.ts +59 -0
- package/dist/config.js +157 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/keys.d.ts +13 -0
- package/dist/keys.js +36 -0
- package/dist/pages.d.ts +10 -0
- package/dist/pages.js +134 -0
- package/dist/provider.d.ts +14 -0
- package/dist/provider.js +412 -0
- package/dist/server.d.ts +34 -0
- package/dist/server.js +77 -0
- package/dist/tokens.d.ts +36 -0
- package/dist/tokens.js +44 -0
- package/dist/vite.d.ts +25 -0
- package/dist/vite.js +114 -0
- package/examples/oidc-mock.yaml +58 -0
- package/package.json +72 -0
package/dist/vite.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { attachToRunning, startServer } from './server.js';
|
|
3
|
+
/**
|
|
4
|
+
* One mock per config and port for the whole process. Vite re-creates plugins on every restart
|
|
5
|
+
* (config change, `server.restart()`), but the port stays taken until the old server is closed –
|
|
6
|
+
* reusing it is simpler and keeps the app's session valid across restarts.
|
|
7
|
+
*/
|
|
8
|
+
const running = (globalThis.__oidcMock ??=
|
|
9
|
+
new Map());
|
|
10
|
+
function acquire(configFile, port) {
|
|
11
|
+
const id = `${configFile}#${port ?? ''}`;
|
|
12
|
+
let server = running.get(id);
|
|
13
|
+
if (!server) {
|
|
14
|
+
server = startServer({ config: configFile, port }).catch(async (error) => {
|
|
15
|
+
if (error.code !== 'EADDRINUSE')
|
|
16
|
+
throw error;
|
|
17
|
+
const attached = await attachToRunning({ config: configFile, port });
|
|
18
|
+
if (!attached) {
|
|
19
|
+
throw new Error(`Port ${error.port} is taken by something that is not this oidc-mock. ` +
|
|
20
|
+
'Pick another port in the config or with oidcMock({ port }).');
|
|
21
|
+
}
|
|
22
|
+
return attached;
|
|
23
|
+
});
|
|
24
|
+
server.catch(() => running.delete(id));
|
|
25
|
+
running.set(id, server);
|
|
26
|
+
}
|
|
27
|
+
return server;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Runs the mock provider next to the Vite dev server.
|
|
31
|
+
*
|
|
32
|
+
* The provider listens on its own loopback port (the back channel: discovery, token, JWKS –
|
|
33
|
+
* called by the app's server), and the Vite server serves the same endpoints under `base_path`
|
|
34
|
+
* (the front channel: the login page, called by the browser). Redirects the app sends to the
|
|
35
|
+
* back channel are rewritten to relative URLs, so the browser stays on whatever host it used to
|
|
36
|
+
* reach Vite – `localhost`, a LAN address with `--host`, or a forwarded port.
|
|
37
|
+
*/
|
|
38
|
+
export function oidcMock(options = {}) {
|
|
39
|
+
let root = process.cwd();
|
|
40
|
+
let mock;
|
|
41
|
+
function install(server) {
|
|
42
|
+
const configFile = resolve(root, options.config ?? 'oidc-mock.yaml');
|
|
43
|
+
const pending = acquire(configFile, options.port);
|
|
44
|
+
mock = pending;
|
|
45
|
+
pending.then((started) => {
|
|
46
|
+
const log = server.config.logger;
|
|
47
|
+
log.info(` ➜ oidc-mock: ${started.discoveryUrl}\n` +
|
|
48
|
+
` ${started.config().users.length} users from ${configFile}` +
|
|
49
|
+
(started.server
|
|
50
|
+
? ''
|
|
51
|
+
: '\n port taken by another oidc-mock with this issuer – sharing its back channel'));
|
|
52
|
+
}, (error) => server.config.logger.error(`[oidc-mock] ${error.message}`));
|
|
53
|
+
server.middlewares.use(middleware);
|
|
54
|
+
}
|
|
55
|
+
const middleware = (req, res, next) => {
|
|
56
|
+
if (!mock)
|
|
57
|
+
return next();
|
|
58
|
+
mock.then(async (started) => {
|
|
59
|
+
if (options.rewriteRedirects !== false)
|
|
60
|
+
rewriteLocation(res, started);
|
|
61
|
+
if (await started.provider.handle(req, res))
|
|
62
|
+
return;
|
|
63
|
+
next();
|
|
64
|
+
}, () => next());
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
name: 'oidc-mock',
|
|
68
|
+
configResolved(config) {
|
|
69
|
+
root = config.root;
|
|
70
|
+
},
|
|
71
|
+
configureServer(server) {
|
|
72
|
+
install(server);
|
|
73
|
+
},
|
|
74
|
+
configurePreviewServer(server) {
|
|
75
|
+
if (options.preview !== false)
|
|
76
|
+
install(server);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Turns `Location: <issuer origin>/<base_path>/…` into `/<base_path>/…`, both for
|
|
82
|
+
* `setHeader` and for `writeHead`, which is what SvelteKit, Express and friends use.
|
|
83
|
+
*/
|
|
84
|
+
function rewriteLocation(res, mock) {
|
|
85
|
+
const issuer = new URL(mock.issuer);
|
|
86
|
+
const prefix = issuer.origin + mock.config().base_path;
|
|
87
|
+
const rewrite = (value) => typeof value === 'string' && (value === prefix || value.startsWith(prefix + '/') || value.startsWith(prefix + '?'))
|
|
88
|
+
? value.slice(issuer.origin.length)
|
|
89
|
+
: value;
|
|
90
|
+
const setHeader = res.setHeader.bind(res);
|
|
91
|
+
res.setHeader = (name, value) => setHeader(name, name.toLowerCase() === 'location' ? rewrite(value) : value);
|
|
92
|
+
const writeHead = res.writeHead.bind(res);
|
|
93
|
+
res.writeHead = ((...args) => {
|
|
94
|
+
const headersIndex = typeof args[1] === 'string' ? 2 : 1;
|
|
95
|
+
const headers = args[headersIndex];
|
|
96
|
+
if (Array.isArray(headers)) {
|
|
97
|
+
// Either [[name, value], …] or a flat [name, value, name, value, …].
|
|
98
|
+
if (Array.isArray(headers[0])) {
|
|
99
|
+
args[headersIndex] = headers.map(([name, value]) => [name, name.toLowerCase() === 'location' ? rewrite(value) : value]);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
args[headersIndex] = headers.map((value, i) => i % 2 === 1 && String(headers[i - 1]).toLowerCase() === 'location' ? rewrite(value) : value);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else if (headers && typeof headers === 'object') {
|
|
106
|
+
args[headersIndex] = Object.fromEntries(Object.entries(headers).map(([name, value]) => [
|
|
107
|
+
name,
|
|
108
|
+
name.toLowerCase() === 'location' ? rewrite(value) : value
|
|
109
|
+
]));
|
|
110
|
+
}
|
|
111
|
+
return writeHead(...args);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
export default oidcMock;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# oidc-mock configuration
|
|
2
|
+
#
|
|
3
|
+
# Edits to `users`, `clients`, `tokens` and `custom_login` apply without a restart.
|
|
4
|
+
# `host`, `port`, `base_path`, `issuer` and `key_file` need one.
|
|
5
|
+
|
|
6
|
+
# Back channel: where the app's server reaches the provider. The issuer is
|
|
7
|
+
# http://<host>:<port><base_path>, the discovery URL that plus /.well-known/openid-configuration.
|
|
8
|
+
host: 127.0.0.1
|
|
9
|
+
port: 8090
|
|
10
|
+
base_path: /oidc
|
|
11
|
+
|
|
12
|
+
# Leave `clients` out to accept any client_id without a secret.
|
|
13
|
+
# clients:
|
|
14
|
+
# - client_id: my-app
|
|
15
|
+
# client_secret: dev-secret # omit for public clients (PKCE)
|
|
16
|
+
# redirect_uris: ['http://*:5173/*', 'https://*:5173/*']
|
|
17
|
+
|
|
18
|
+
tokens:
|
|
19
|
+
access_token_ttl: 1h
|
|
20
|
+
id_token_ttl: 1h
|
|
21
|
+
refresh_token_ttl: 30d
|
|
22
|
+
|
|
23
|
+
# Show the “custom claims” box on the login page.
|
|
24
|
+
custom_login: true
|
|
25
|
+
|
|
26
|
+
# One button per user on the login page. `claims` go into the id token, the access token and
|
|
27
|
+
# userinfo; `sub` is fixed per user, so the app sees the same account every time.
|
|
28
|
+
users:
|
|
29
|
+
- sub: admin
|
|
30
|
+
label: Admin
|
|
31
|
+
description: Every permission
|
|
32
|
+
claims:
|
|
33
|
+
email: admin@example.org
|
|
34
|
+
email_verified: true
|
|
35
|
+
given_name: Ada
|
|
36
|
+
family_name: Admin
|
|
37
|
+
preferred_username: ada
|
|
38
|
+
locale: en
|
|
39
|
+
roles: [admin]
|
|
40
|
+
|
|
41
|
+
- sub: member
|
|
42
|
+
label: Member
|
|
43
|
+
description: Plain account without roles
|
|
44
|
+
claims:
|
|
45
|
+
email: member@example.org
|
|
46
|
+
email_verified: true
|
|
47
|
+
given_name: Max
|
|
48
|
+
family_name: Member
|
|
49
|
+
preferred_username: max
|
|
50
|
+
locale: en
|
|
51
|
+
roles: []
|
|
52
|
+
|
|
53
|
+
- sub: no-email
|
|
54
|
+
label: Without email
|
|
55
|
+
description: Edge case – the email claim is missing
|
|
56
|
+
claims:
|
|
57
|
+
given_name: Nora
|
|
58
|
+
family_name: Noemail
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "oidc-mock",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "OpenID Connect provider for local development: users and claims in one YAML file, a login page to pick them, and a Vite plugin that makes it work behind --host.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Tade Strehk <tade@strehk.eu>",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/strehk/oidc-mock.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/strehk/oidc-mock#readme",
|
|
12
|
+
"bugs": "https://github.com/strehk/oidc-mock/issues",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"oidc",
|
|
16
|
+
"openid-connect",
|
|
17
|
+
"oauth2",
|
|
18
|
+
"mock",
|
|
19
|
+
"dev-server",
|
|
20
|
+
"vite-plugin"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
},
|
|
25
|
+
"bin": {
|
|
26
|
+
"oidc-mock": "dist/cli.js"
|
|
27
|
+
},
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./vite": {
|
|
34
|
+
"types": "./dist/vite.d.ts",
|
|
35
|
+
"default": "./dist/vite.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"examples/oidc-mock.yaml"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
44
|
+
"dev": "bun --watch src/cli.ts --config examples/oidc-mock.yaml",
|
|
45
|
+
"test": "bun test",
|
|
46
|
+
"typecheck": "tsc --noEmit",
|
|
47
|
+
"prepublishOnly": "bun run typecheck && bun test && bun run build"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"jose": "^6.2.12",
|
|
51
|
+
"yaml": "^2.9.1",
|
|
52
|
+
"zod": "^4.6.5"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"vite": ">=5"
|
|
56
|
+
},
|
|
57
|
+
"peerDependenciesMeta": {
|
|
58
|
+
"vite": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/bun": "latest",
|
|
64
|
+
"@types/node": "^26.6.3",
|
|
65
|
+
"openid-client": "^6.8.8",
|
|
66
|
+
"typescript": "^7.0.2",
|
|
67
|
+
"vite": "^8.3.1"
|
|
68
|
+
},
|
|
69
|
+
"publishConfig": {
|
|
70
|
+
"access": "public"
|
|
71
|
+
}
|
|
72
|
+
}
|