@nekm/sveltekit-armor 0.1.0
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.md +20 -0
- package/README.md +1 -0
- package/dist/contracts.d.ts +38 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.esm.js +221 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +224 -0
- package/dist/index.js.map +1 -0
- package/dist/routes/login.d.ts +3 -0
- package/dist/routes/logout.d.ts +3 -0
- package/dist/routes/redirect-login.d.ts +3 -0
- package/dist/routes/redirect-logout.d.ts +3 -0
- package/dist/routes/routes.d.ts +8 -0
- package/dist/utils/cookie.d.ts +6 -0
- package/dist/utils/jwt.d.ts +4 -0
- package/dist/utils/utils.d.ts +3 -0
- package/dist/utils/utils.test.d.ts +1 -0
- package/package.json +80 -0
- package/src/contracts.ts +51 -0
- package/src/index.ts +44 -0
- package/src/routes/login.ts +31 -0
- package/src/routes/logout.ts +29 -0
- package/src/routes/redirect-login.ts +103 -0
- package/src/routes/redirect-logout.ts +25 -0
- package/src/routes/routes.ts +30 -0
- package/src/utils/cookie.ts +41 -0
- package/src/utils/jwt.ts +35 -0
- package/src/utils/utils.test.ts +18 -0
- package/src/utils/utils.ts +23 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Niklas Ekman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# SvelteKit Armor
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { RequestEvent } from "@sveltejs/kit";
|
|
2
|
+
import type { JWTPayload } from "jose";
|
|
3
|
+
export interface ArmorTokenExchange {
|
|
4
|
+
readonly access_token: string;
|
|
5
|
+
readonly id_token: string;
|
|
6
|
+
readonly token_type: "Bearer";
|
|
7
|
+
readonly expires_in: number;
|
|
8
|
+
readonly refresh_token?: string;
|
|
9
|
+
readonly scope?: string;
|
|
10
|
+
}
|
|
11
|
+
export type ArmorIdToken = Required<Pick<JWTPayload, "iss" | "sub" | "aud" | "exp" | "iat">> & Omit<JWTPayload, "iss" | "sub" | "aud" | "exp" | "iat">;
|
|
12
|
+
export interface ArmorAccessToken extends JWTPayload {
|
|
13
|
+
client_id?: string;
|
|
14
|
+
scope?: string;
|
|
15
|
+
version?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface ArmorTokens {
|
|
18
|
+
readonly exchange: ArmorTokenExchange;
|
|
19
|
+
readonly idToken: ArmorIdToken;
|
|
20
|
+
readonly accessToken: ArmorAccessToken;
|
|
21
|
+
}
|
|
22
|
+
export interface ArmorConfig {
|
|
23
|
+
readonly session: {
|
|
24
|
+
readonly exists?: (event: RequestEvent) => Promise<boolean> | boolean;
|
|
25
|
+
readonly login?: (event: RequestEvent, tokens: ArmorTokens) => Promise<void> | void;
|
|
26
|
+
readonly logout?: (event: RequestEvent) => Promise<void> | void;
|
|
27
|
+
};
|
|
28
|
+
readonly oauth: {
|
|
29
|
+
readonly clientId: string;
|
|
30
|
+
readonly clientSecret: string;
|
|
31
|
+
readonly baseUrl: string;
|
|
32
|
+
readonly jwksUrl?: string;
|
|
33
|
+
readonly issuer: string;
|
|
34
|
+
readonly authorizePath?: string;
|
|
35
|
+
readonly logoutPath?: string;
|
|
36
|
+
readonly tokenPath?: string;
|
|
37
|
+
};
|
|
38
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Handle, Cookies } from "@sveltejs/kit";
|
|
2
|
+
import type { ArmorConfig, ArmorTokens } from "./contracts";
|
|
3
|
+
export type { ArmorConfig, ArmorTokens };
|
|
4
|
+
export declare const ARMOR_LOGIN = "/_auth/login";
|
|
5
|
+
export declare const ARMOR_LOGOUT = "/_auth/logout";
|
|
6
|
+
export declare function armor(config: ArmorConfig): Handle;
|
|
7
|
+
export declare function armorCookiesGetTokens(cookies: Cookies): ArmorTokens;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { redirect, error } from '@sveltejs/kit';
|
|
2
|
+
import { strTrimEnd, throwIfUndefined, queryParamsCreate, noop } from '@nekm/core';
|
|
3
|
+
import { jwtVerify, createRemoteJWKSet } from 'jose';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
function urlConcat(origin, path) {
|
|
7
|
+
return `${strTrimEnd(origin, "/")}/${path}`;
|
|
8
|
+
}
|
|
9
|
+
function isTokenExchange(value) {
|
|
10
|
+
if (typeof value !== "object" || value === null) return false;
|
|
11
|
+
const obj = value;
|
|
12
|
+
return typeof obj.access_token === "string" && obj.token_type === "Bearer" && typeof obj.expires_in === "number" && (
|
|
13
|
+
// Optional fields
|
|
14
|
+
typeof obj.id_token === "string" || obj.id_token === undefined) && (typeof obj.refresh_token === "string" || obj.refresh_token === undefined) && (typeof obj.scope === "string" || obj.scope === undefined);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const COOKIE_TOKENS = "tokens";
|
|
18
|
+
const COOKIE_STATE = "state";
|
|
19
|
+
const cookieDeleteOptions = Object.freeze({
|
|
20
|
+
path: "/"
|
|
21
|
+
});
|
|
22
|
+
const cookieSetOptions = Object.freeze({
|
|
23
|
+
...cookieDeleteOptions,
|
|
24
|
+
httpOnly: true,
|
|
25
|
+
secure: true,
|
|
26
|
+
sameSite: "lax",
|
|
27
|
+
maxAge: 1800 // 30 minutes
|
|
28
|
+
});
|
|
29
|
+
function cookieSet(cookies, key, value) {
|
|
30
|
+
cookies.set(key, JSON.stringify(value), cookieSetOptions);
|
|
31
|
+
}
|
|
32
|
+
function cookieGetAndDelete(cookies, key) {
|
|
33
|
+
const value = cookieGet(cookies, key);
|
|
34
|
+
if (value) {
|
|
35
|
+
cookies.delete(key, cookieDeleteOptions);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
function cookieGet(cookies, key) {
|
|
40
|
+
const value = cookies.get(key);
|
|
41
|
+
return !value ? undefined : JSON.parse(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function jwtVerifyIdToken(config, jwks, idToken) {
|
|
45
|
+
return jwtVerifyToken(jwks, {
|
|
46
|
+
issuer: config.oauth.issuer,
|
|
47
|
+
audience: config.oauth.clientId
|
|
48
|
+
}, idToken);
|
|
49
|
+
}
|
|
50
|
+
function jwtVerifyAccessToken(config, jwks, accessToken) {
|
|
51
|
+
return jwtVerifyToken(jwks, {
|
|
52
|
+
issuer: config.oauth.issuer
|
|
53
|
+
}, accessToken);
|
|
54
|
+
}
|
|
55
|
+
async function jwtVerifyToken(jwks, options, token) {
|
|
56
|
+
const {
|
|
57
|
+
payload
|
|
58
|
+
} = await jwtVerify(token, jwks, options);
|
|
59
|
+
return payload;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const ROUTE_PATH_REDIRECT_LOGIN = "/_auth/redirect/login";
|
|
63
|
+
const routeRedirectLoginFactory = config => {
|
|
64
|
+
var _config$oauth$jwksUrl, _config$oauth$tokenPa, _config$session$login;
|
|
65
|
+
const jwksUrl = new URL((_config$oauth$jwksUrl = config.oauth.jwksUrl) != null ? _config$oauth$jwksUrl : `${strTrimEnd(config.oauth.issuer, "/")}/.well-known/jwks.json`);
|
|
66
|
+
const tokenUrl = `${config.oauth.baseUrl}/${(_config$oauth$tokenPa = config.oauth.tokenPath) != null ? _config$oauth$tokenPa : "oauth2/token"}`;
|
|
67
|
+
const sessionLogin = (_config$session$login = config.session.login) != null ? _config$session$login : (event, tokens) => cookieSet(event.cookies, COOKIE_TOKENS, tokens);
|
|
68
|
+
async function exchangeCodeForToken(fetch, origin, code) {
|
|
69
|
+
const response = await fetch(tokenUrl, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: {
|
|
72
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
73
|
+
Accept: "application/json"
|
|
74
|
+
},
|
|
75
|
+
body: new URLSearchParams({
|
|
76
|
+
grant_type: "authorization_code",
|
|
77
|
+
client_id: config.oauth.clientId,
|
|
78
|
+
client_secret: config.oauth.clientSecret,
|
|
79
|
+
code,
|
|
80
|
+
redirect_uri: urlConcat(origin, ROUTE_PATH_REDIRECT_LOGIN)
|
|
81
|
+
}).toString()
|
|
82
|
+
});
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
const error = await response.text();
|
|
85
|
+
throw new Error(`Token exchange failed: ${error}`);
|
|
86
|
+
}
|
|
87
|
+
const token = await response.json();
|
|
88
|
+
if (!isTokenExchange(token)) {
|
|
89
|
+
throw new Error("Response is not a valid token exchange.");
|
|
90
|
+
}
|
|
91
|
+
return token;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
path: ROUTE_PATH_REDIRECT_LOGIN,
|
|
95
|
+
async handle({
|
|
96
|
+
event
|
|
97
|
+
}) {
|
|
98
|
+
var _event$url$searchPara, _event$url$searchPara2;
|
|
99
|
+
const state = (_event$url$searchPara = event.url.searchParams.get("state")) != null ? _event$url$searchPara : undefined;
|
|
100
|
+
const stateCookie = cookieGetAndDelete(event.cookies, COOKIE_STATE);
|
|
101
|
+
if (state !== stateCookie) {
|
|
102
|
+
throw new Error("State do not match");
|
|
103
|
+
}
|
|
104
|
+
const code = (_event$url$searchPara2 = event.url.searchParams.get("code")) != null ? _event$url$searchPara2 : undefined;
|
|
105
|
+
throwIfUndefined(code);
|
|
106
|
+
const exchange = await exchangeCodeForToken(fetch, event.url.origin, code);
|
|
107
|
+
const jwks = createRemoteJWKSet(jwksUrl);
|
|
108
|
+
const [idToken, accessToken] = await Promise.all([jwtVerifyIdToken(config, jwks, exchange.id_token), jwtVerifyAccessToken(config, jwks, exchange.access_token)]);
|
|
109
|
+
await sessionLogin(event, {
|
|
110
|
+
exchange,
|
|
111
|
+
idToken: idToken,
|
|
112
|
+
accessToken
|
|
113
|
+
});
|
|
114
|
+
throw redirect(302, "/");
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const ROUTE_PATH_LOGIN = "/_auth/login";
|
|
120
|
+
const routeLoginFactory = config => {
|
|
121
|
+
var _config$oauth$authori;
|
|
122
|
+
const authorizeUrl = `${config.oauth.baseUrl}/${(_config$oauth$authori = config.oauth.authorizePath) != null ? _config$oauth$authori : "/oauth2/authorize"}`;
|
|
123
|
+
return {
|
|
124
|
+
path: ROUTE_PATH_LOGIN,
|
|
125
|
+
async handle({
|
|
126
|
+
event
|
|
127
|
+
}) {
|
|
128
|
+
const state = randomUUID();
|
|
129
|
+
cookieSet(event.cookies, COOKIE_STATE, state);
|
|
130
|
+
const params = queryParamsCreate({
|
|
131
|
+
client_id: config.oauth.clientId,
|
|
132
|
+
response_type: "code",
|
|
133
|
+
redirect_uri: urlConcat(event.url.origin, ROUTE_PATH_REDIRECT_LOGIN),
|
|
134
|
+
state
|
|
135
|
+
});
|
|
136
|
+
throw redirect(302, `${authorizeUrl}?${params}`);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const ROUTE_PATH_REDIRECT_LOGOUT = "/_auth/redirect/logout";
|
|
142
|
+
const routeRedirectLogoutFactory = config => {
|
|
143
|
+
var _config$session$logou;
|
|
144
|
+
// Check if the oauth provider supports a logout path.
|
|
145
|
+
if (!config.oauth.logoutPath) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
const logout = (_config$session$logou = config.session.logout) != null ? _config$session$logou : noop;
|
|
149
|
+
return {
|
|
150
|
+
path: ROUTE_PATH_REDIRECT_LOGOUT,
|
|
151
|
+
async handle({
|
|
152
|
+
event
|
|
153
|
+
}) {
|
|
154
|
+
await logout(event);
|
|
155
|
+
throw redirect(302, "/");
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const ROUTE_PATH_LOGOUT = "/_auth/logout";
|
|
161
|
+
const routeLogoutFactory = config => {
|
|
162
|
+
// Check if the oauth provider supports a logout path.
|
|
163
|
+
if (!config.oauth.logoutPath) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
const logoutUrl = `${config.oauth.baseUrl}/${config.oauth.logoutPath}`;
|
|
167
|
+
return {
|
|
168
|
+
path: ROUTE_PATH_LOGOUT,
|
|
169
|
+
async handle({
|
|
170
|
+
event
|
|
171
|
+
}) {
|
|
172
|
+
const params = queryParamsCreate({
|
|
173
|
+
logout_uri: urlConcat(event.url.origin, ROUTE_PATH_REDIRECT_LOGOUT),
|
|
174
|
+
client_id: config.oauth.clientId
|
|
175
|
+
});
|
|
176
|
+
throw redirect(302, `${logoutUrl}?${params}`);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const routeFactories = Object.freeze([routeLoginFactory, routeLogoutFactory, routeRedirectLoginFactory, routeRedirectLogoutFactory]);
|
|
182
|
+
function routeCreate(config) {
|
|
183
|
+
return new Map(routeFactories.map(routeFactory => routeFactory(config)).filter(route => Boolean(route))
|
|
184
|
+
// @ts-expect-error Incorrect typing error.
|
|
185
|
+
.map(route => [route.path, route.handle]));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const ARMOR_LOGIN = ROUTE_PATH_LOGIN;
|
|
189
|
+
const ARMOR_LOGOUT = ROUTE_PATH_LOGOUT;
|
|
190
|
+
function armor(config) {
|
|
191
|
+
var _config$session$exist;
|
|
192
|
+
const routes = routeCreate(config);
|
|
193
|
+
const sessionExists = (_config$session$exist = config.session.exists) != null ? _config$session$exist : event => Boolean(event.cookies.get(COOKIE_TOKENS));
|
|
194
|
+
return async ({
|
|
195
|
+
event,
|
|
196
|
+
resolve
|
|
197
|
+
}) => {
|
|
198
|
+
const routeHandle = routes.get(event.url.pathname);
|
|
199
|
+
if (routeHandle) {
|
|
200
|
+
await routeHandle({
|
|
201
|
+
event,
|
|
202
|
+
resolve
|
|
203
|
+
});
|
|
204
|
+
// Handle should redirect. If it doesn't, something is wrong.
|
|
205
|
+
throw error(500, "Illegal state");
|
|
206
|
+
}
|
|
207
|
+
const exists = await sessionExists(event);
|
|
208
|
+
if (!exists) {
|
|
209
|
+
throw redirect(302, ROUTE_PATH_LOGIN);
|
|
210
|
+
}
|
|
211
|
+
return resolve(event);
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function armorCookiesGetTokens(cookies) {
|
|
215
|
+
const tokens = cookieGet(cookies, COOKIE_TOKENS);
|
|
216
|
+
throwIfUndefined(tokens);
|
|
217
|
+
return tokens;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export { ARMOR_LOGIN, ARMOR_LOGOUT, armor, armorCookiesGetTokens };
|
|
221
|
+
//# sourceMappingURL=index.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.esm.js","sources":["../src/utils/utils.ts","../src/utils/cookie.ts","../src/utils/jwt.ts","../src/routes/redirect-login.ts","../src/routes/login.ts","../src/routes/redirect-logout.ts","../src/routes/logout.ts","../src/routes/routes.ts","../src/index.ts"],"sourcesContent":["import { strTrimEnd } from \"@nekm/core\";\nimport type { ArmorTokenExchange } from \"../contracts\";\n\nexport function urlConcat(origin: string, path: string): string {\n\treturn `${strTrimEnd(origin, \"/\")}/${path}`;\n}\n\nexport function isTokenExchange(value: unknown): value is ArmorTokenExchange {\n\tif (typeof value !== \"object\" || value === null) return false;\n\n\tconst obj = value as Record<string, unknown>;\n\n\treturn (\n\t\ttypeof obj.access_token === \"string\" &&\n\t\tobj.token_type === \"Bearer\" &&\n\t\ttypeof obj.expires_in === \"number\" &&\n\t\t// Optional fields\n\t\t(typeof obj.id_token === \"string\" || obj.id_token === undefined) &&\n\t\t(typeof obj.refresh_token === \"string\" ||\n\t\t\tobj.refresh_token === undefined) &&\n\t\t(typeof obj.scope === \"string\" || obj.scope === undefined)\n\t);\n}\n","import { Cookies } from \"@sveltejs/kit\";\n\nexport const COOKIE_TOKENS = \"tokens\";\nexport const COOKIE_STATE = \"state\";\n\nconst cookieDeleteOptions = Object.freeze({ path: \"/\" });\n\nconst cookieSetOptions = Object.freeze({\n\t...cookieDeleteOptions,\n\thttpOnly: true,\n\tsecure: true,\n\tsameSite: \"lax\",\n\tmaxAge: 1800, // 30 minutes\n});\n\nexport function cookieSet(\n\tcookies: Cookies,\n\tkey: string,\n\tvalue: string | object,\n) {\n\tcookies.set(key, JSON.stringify(value), cookieSetOptions);\n}\n\nexport function cookieGetAndDelete<T>(\n\tcookies: Cookies,\n\tkey: string,\n): T | undefined {\n\tconst value = cookieGet<T>(cookies, key);\n\n\tif (value) {\n\t\tcookies.delete(key, cookieDeleteOptions);\n\t}\n\n\treturn value;\n}\n\nexport function cookieGet<T>(cookies: Cookies, key: string): T | undefined {\n\tconst value = cookies.get(key);\n\n\treturn !value ? undefined : JSON.parse(value);\n}\n","import { ArmorConfig } from \"../contracts\";\nimport { jwtVerify, JWTVerifyGetKey, JWTVerifyOptions } from \"jose\";\n\nexport function jwtVerifyIdToken(\n\tconfig: ArmorConfig,\n\tjwks: JWTVerifyGetKey,\n\tidToken: string,\n) {\n\treturn jwtVerifyToken(\n\t\tjwks,\n\t\t{\n\t\t\tissuer: config.oauth.issuer,\n\t\t\taudience: config.oauth.clientId,\n\t\t},\n\t\tidToken,\n\t);\n}\n\nexport function jwtVerifyAccessToken(\n\tconfig: ArmorConfig,\n\tjwks: JWTVerifyGetKey,\n\taccessToken: string,\n) {\n\treturn jwtVerifyToken(jwks, { issuer: config.oauth.issuer }, accessToken);\n}\n\nasync function jwtVerifyToken(\n\tjwks: JWTVerifyGetKey,\n\toptions: JWTVerifyOptions,\n\ttoken: string,\n) {\n\tconst { payload } = await jwtVerify(token, jwks, options);\n\n\treturn payload;\n}\n","import { redirect } from \"@sveltejs/kit\";\nimport type {\n\tArmorConfig,\n\tArmorIdToken,\n\tArmorTokenExchange,\n} from \"../contracts\";\nimport { strTrimEnd, throwIfUndefined } from \"@nekm/core\";\nimport { createRemoteJWKSet } from \"jose\";\nimport type { RouteFactory } from \"./routes\";\nimport { urlConcat, isTokenExchange } from \"../utils/utils\";\nimport {\n\tCOOKIE_STATE,\n\tCOOKIE_TOKENS,\n\tcookieGetAndDelete,\n\tcookieSet,\n} from \"../utils/cookie\";\nimport { jwtVerifyAccessToken, jwtVerifyIdToken } from \"../utils/jwt\";\n\nexport const ROUTE_PATH_REDIRECT_LOGIN = \"/_auth/redirect/login\";\n\nexport const routeRedirectLoginFactory: RouteFactory = (\n\tconfig: ArmorConfig,\n) => {\n\tconst jwksUrl = new URL(\n\t\tconfig.oauth.jwksUrl ??\n\t\t\t`${strTrimEnd(config.oauth.issuer, \"/\")}/.well-known/jwks.json`,\n\t);\n\tconst tokenUrl = `${config.oauth.baseUrl}/${config.oauth.tokenPath ?? \"oauth2/token\"}`;\n\n\tconst sessionLogin =\n\t\tconfig.session.login ??\n\t\t((event, tokens) => cookieSet(event.cookies, COOKIE_TOKENS, tokens));\n\n\tasync function exchangeCodeForToken(\n\t\tfetch: typeof global.fetch,\n\t\torigin: string,\n\t\tcode: string,\n\t): Promise<ArmorTokenExchange> {\n\t\tconst response = await fetch(tokenUrl, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\t\tAccept: \"application/json\",\n\t\t\t},\n\t\t\tbody: new URLSearchParams({\n\t\t\t\tgrant_type: \"authorization_code\",\n\t\t\t\tclient_id: config.oauth.clientId,\n\t\t\t\tclient_secret: config.oauth.clientSecret,\n\t\t\t\tcode,\n\t\t\t\tredirect_uri: urlConcat(origin, ROUTE_PATH_REDIRECT_LOGIN),\n\t\t\t}).toString(),\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tconst error = await response.text();\n\t\t\tthrow new Error(`Token exchange failed: ${error}`);\n\t\t}\n\n\t\tconst token = await response.json();\n\n\t\tif (!isTokenExchange(token)) {\n\t\t\tthrow new Error(\"Response is not a valid token exchange.\");\n\t\t}\n\n\t\treturn token;\n\t}\n\n\treturn {\n\t\tpath: ROUTE_PATH_REDIRECT_LOGIN,\n\t\tasync handle({ event }) {\n\t\t\tconst state = event.url.searchParams.get(\"state\") ?? undefined;\n\t\t\tconst stateCookie = cookieGetAndDelete(event.cookies, COOKIE_STATE);\n\n\t\t\tif (state !== stateCookie) {\n\t\t\t\tthrow new Error(\"State do not match\");\n\t\t\t}\n\n\t\t\tconst code = event.url.searchParams.get(\"code\") ?? undefined;\n\t\t\tthrowIfUndefined(code);\n\n\t\t\tconst exchange = await exchangeCodeForToken(\n\t\t\t\tfetch,\n\t\t\t\tevent.url.origin,\n\t\t\t\tcode,\n\t\t\t);\n\n\t\t\tconst jwks = createRemoteJWKSet(jwksUrl);\n\n\t\t\tconst [idToken, accessToken] = await Promise.all([\n\t\t\t\tjwtVerifyIdToken(config, jwks, exchange.id_token),\n\t\t\t\tjwtVerifyAccessToken(config, jwks, exchange.access_token),\n\t\t\t]);\n\n\t\t\tawait sessionLogin(event, {\n\t\t\t\texchange,\n\t\t\t\tidToken: idToken as ArmorIdToken,\n\t\t\t\taccessToken,\n\t\t\t});\n\n\t\t\tthrow redirect(302, \"/\");\n\t\t},\n\t};\n};\n","import { redirect } from \"@sveltejs/kit\";\nimport type { ArmorConfig } from \"../contracts\";\nimport { queryParamsCreate } from \"@nekm/core\";\nimport { ROUTE_PATH_REDIRECT_LOGIN } from \"./redirect-login\";\nimport { randomUUID } from \"node:crypto\";\nimport type { RouteFactory } from \"./routes\";\nimport { COOKIE_STATE, cookieSet } from \"../utils/cookie\";\nimport { urlConcat } from \"../utils/utils\";\n\nexport const ROUTE_PATH_LOGIN = \"/_auth/login\";\n\nexport const routeLoginFactory: RouteFactory = (config: ArmorConfig) => {\n\tconst authorizeUrl = `${config.oauth.baseUrl}/${config.oauth.authorizePath ?? \"/oauth2/authorize\"}`;\n\n\treturn {\n\t\tpath: ROUTE_PATH_LOGIN,\n\t\tasync handle({ event }) {\n\t\t\tconst state = randomUUID();\n\t\t\tcookieSet(event.cookies, COOKIE_STATE, state);\n\n\t\t\tconst params = queryParamsCreate({\n\t\t\t\tclient_id: config.oauth.clientId,\n\t\t\t\tresponse_type: \"code\",\n\t\t\t\tredirect_uri: urlConcat(event.url.origin, ROUTE_PATH_REDIRECT_LOGIN),\n\t\t\t\tstate,\n\t\t\t});\n\n\t\t\tthrow redirect(302, `${authorizeUrl}?${params}`);\n\t\t},\n\t};\n};\n","import { redirect } from \"@sveltejs/kit\";\nimport type { ArmorConfig } from \"../contracts\";\nimport { noop } from \"@nekm/core\";\nimport type { RouteFactory } from \"./routes\";\n\nexport const ROUTE_PATH_REDIRECT_LOGOUT = \"/_auth/redirect/logout\";\n\nexport const routeRedirectLogoutFactory: RouteFactory = (\n\tconfig: ArmorConfig,\n) => {\n\t// Check if the oauth provider supports a logout path.\n\tif (!config.oauth.logoutPath) {\n\t\treturn undefined;\n\t}\n\n\tconst logout = config.session.logout ?? noop;\n\n\treturn {\n\t\tpath: ROUTE_PATH_REDIRECT_LOGOUT,\n\t\tasync handle({ event }) {\n\t\t\tawait logout(event);\n\t\t\tthrow redirect(302, \"/\");\n\t\t},\n\t};\n};\n","import { redirect } from \"@sveltejs/kit\";\nimport type { ArmorConfig } from \"../contracts\";\nimport { queryParamsCreate } from \"@nekm/core\";\nimport { ROUTE_PATH_REDIRECT_LOGOUT } from \"./redirect-logout\";\nimport type { RouteFactory } from \"./routes\";\nimport { urlConcat } from \"../utils/utils\";\n\nexport const ROUTE_PATH_LOGOUT = \"/_auth/logout\";\n\nexport const routeLogoutFactory: RouteFactory = (config: ArmorConfig) => {\n\t// Check if the oauth provider supports a logout path.\n\tif (!config.oauth.logoutPath) {\n\t\treturn undefined;\n\t}\n\n\tconst logoutUrl = `${config.oauth.baseUrl}/${config.oauth.logoutPath}`;\n\n\treturn {\n\t\tpath: ROUTE_PATH_LOGOUT,\n\t\tasync handle({ event }) {\n\t\t\tconst params = queryParamsCreate({\n\t\t\t\tlogout_uri: urlConcat(event.url.origin, ROUTE_PATH_REDIRECT_LOGOUT),\n\t\t\t\tclient_id: config.oauth.clientId,\n\t\t\t});\n\n\t\t\tthrow redirect(302, `${logoutUrl}?${params}`);\n\t\t},\n\t};\n};\n","import type { Handle } from \"@sveltejs/kit\";\nimport type { ArmorConfig } from \"../contracts\";\nimport { routeLoginFactory } from \"./login\";\nimport { routeLogoutFactory } from \"./logout\";\nimport { routeRedirectLogoutFactory } from \"./redirect-logout\";\nimport { routeRedirectLoginFactory } from \"./redirect-login\";\n\nexport interface Route {\n\treadonly path: string;\n\treadonly handle: Handle;\n}\n\nexport type RouteFactory = (config: ArmorConfig) => Route | undefined;\n\nconst routeFactories = Object.freeze([\n\trouteLoginFactory,\n\trouteLogoutFactory,\n\trouteRedirectLoginFactory,\n\trouteRedirectLogoutFactory,\n]);\n\nexport function routeCreate(config: ArmorConfig): Map<string, Handle> {\n\treturn new Map(\n\t\trouteFactories\n\t\t\t.map((routeFactory) => routeFactory(config))\n\t\t\t.filter((route) => Boolean(route))\n\t\t\t// @ts-expect-error Incorrect typing error.\n\t\t\t.map((route) => [route.path, route.handle]),\n\t);\n}\n","import { error, redirect, type Handle, Cookies } from \"@sveltejs/kit\";\nimport { ROUTE_PATH_LOGIN } from \"./routes/login\";\nimport type { ArmorConfig, ArmorTokens } from \"./contracts\";\nimport { ROUTE_PATH_LOGOUT } from \"./routes/logout\";\nimport { routeCreate } from \"./routes/routes\";\nimport { COOKIE_TOKENS, cookieGet } from \"./utils/cookie\";\nimport { throwIfUndefined } from \"@nekm/core\";\n\nexport type { ArmorConfig, ArmorTokens };\n\nexport const ARMOR_LOGIN = ROUTE_PATH_LOGIN;\nexport const ARMOR_LOGOUT = ROUTE_PATH_LOGOUT;\n\nexport function armor(config: ArmorConfig): Handle {\n\tconst routes = routeCreate(config);\n\tconst sessionExists =\n\t\tconfig.session.exists ??\n\t\t((event) => Boolean(event.cookies.get(COOKIE_TOKENS)));\n\n\treturn async ({ event, resolve }) => {\n\t\tconst routeHandle = routes.get(event.url.pathname);\n\n\t\tif (routeHandle) {\n\t\t\tawait routeHandle({ event, resolve });\n\n\t\t\t// Handle should redirect. If it doesn't, something is wrong.\n\t\t\tthrow error(500, \"Illegal state\");\n\t\t}\n\n\t\tconst exists = await sessionExists(event);\n\n\t\tif (!exists) {\n\t\t\tthrow redirect(302, ROUTE_PATH_LOGIN);\n\t\t}\n\n\t\treturn resolve(event);\n\t};\n}\n\nexport function armorCookiesGetTokens(cookies: Cookies): ArmorTokens {\n\tconst tokens = cookieGet<ArmorTokens>(cookies, COOKIE_TOKENS);\n\tthrowIfUndefined(tokens);\n\treturn tokens;\n}\n"],"names":["urlConcat","origin","path","strTrimEnd","isTokenExchange","value","obj","access_token","token_type","expires_in","id_token","undefined","refresh_token","scope","COOKIE_TOKENS","COOKIE_STATE","cookieDeleteOptions","Object","freeze","cookieSetOptions","httpOnly","secure","sameSite","maxAge","cookieSet","cookies","key","set","JSON","stringify","cookieGetAndDelete","cookieGet","delete","get","parse","jwtVerifyIdToken","config","jwks","idToken","jwtVerifyToken","issuer","oauth","audience","clientId","jwtVerifyAccessToken","accessToken","options","token","payload","jwtVerify","ROUTE_PATH_REDIRECT_LOGIN","routeRedirectLoginFactory","_config$oauth$jwksUrl","_config$oauth$tokenPa","_config$session$login","jwksUrl","URL","tokenUrl","baseUrl","tokenPath","sessionLogin","session","login","event","tokens","exchangeCodeForToken","fetch","code","response","method","headers","Accept","body","URLSearchParams","grant_type","client_id","client_secret","clientSecret","redirect_uri","toString","ok","error","text","Error","json","handle","_event$url$searchPara","_event$url$searchPara2","state","url","searchParams","stateCookie","throwIfUndefined","exchange","createRemoteJWKSet","Promise","all","redirect","ROUTE_PATH_LOGIN","routeLoginFactory","_config$oauth$authori","authorizeUrl","authorizePath","randomUUID","params","queryParamsCreate","response_type","ROUTE_PATH_REDIRECT_LOGOUT","routeRedirectLogoutFactory","_config$session$logou","logoutPath","logout","noop","ROUTE_PATH_LOGOUT","routeLogoutFactory","logoutUrl","logout_uri","routeFactories","routeCreate","Map","map","routeFactory","filter","route","Boolean","ARMOR_LOGIN","ARMOR_LOGOUT","armor","_config$session$exist","routes","sessionExists","exists","resolve","routeHandle","pathname","armorCookiesGetTokens"],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
var kit = require('@sveltejs/kit');
|
|
2
|
+
var core = require('@nekm/core');
|
|
3
|
+
var jose = require('jose');
|
|
4
|
+
var node_crypto = require('node:crypto');
|
|
5
|
+
|
|
6
|
+
function urlConcat(origin, path) {
|
|
7
|
+
return `${core.strTrimEnd(origin, "/")}/${path}`;
|
|
8
|
+
}
|
|
9
|
+
function isTokenExchange(value) {
|
|
10
|
+
if (typeof value !== "object" || value === null) return false;
|
|
11
|
+
const obj = value;
|
|
12
|
+
return typeof obj.access_token === "string" && obj.token_type === "Bearer" && typeof obj.expires_in === "number" && (
|
|
13
|
+
// Optional fields
|
|
14
|
+
typeof obj.id_token === "string" || obj.id_token === undefined) && (typeof obj.refresh_token === "string" || obj.refresh_token === undefined) && (typeof obj.scope === "string" || obj.scope === undefined);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const COOKIE_TOKENS = "tokens";
|
|
18
|
+
const COOKIE_STATE = "state";
|
|
19
|
+
const cookieDeleteOptions = Object.freeze({
|
|
20
|
+
path: "/"
|
|
21
|
+
});
|
|
22
|
+
const cookieSetOptions = Object.freeze({
|
|
23
|
+
...cookieDeleteOptions,
|
|
24
|
+
httpOnly: true,
|
|
25
|
+
secure: true,
|
|
26
|
+
sameSite: "lax",
|
|
27
|
+
maxAge: 1800 // 30 minutes
|
|
28
|
+
});
|
|
29
|
+
function cookieSet(cookies, key, value) {
|
|
30
|
+
cookies.set(key, JSON.stringify(value), cookieSetOptions);
|
|
31
|
+
}
|
|
32
|
+
function cookieGetAndDelete(cookies, key) {
|
|
33
|
+
const value = cookieGet(cookies, key);
|
|
34
|
+
if (value) {
|
|
35
|
+
cookies.delete(key, cookieDeleteOptions);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
function cookieGet(cookies, key) {
|
|
40
|
+
const value = cookies.get(key);
|
|
41
|
+
return !value ? undefined : JSON.parse(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function jwtVerifyIdToken(config, jwks, idToken) {
|
|
45
|
+
return jwtVerifyToken(jwks, {
|
|
46
|
+
issuer: config.oauth.issuer,
|
|
47
|
+
audience: config.oauth.clientId
|
|
48
|
+
}, idToken);
|
|
49
|
+
}
|
|
50
|
+
function jwtVerifyAccessToken(config, jwks, accessToken) {
|
|
51
|
+
return jwtVerifyToken(jwks, {
|
|
52
|
+
issuer: config.oauth.issuer
|
|
53
|
+
}, accessToken);
|
|
54
|
+
}
|
|
55
|
+
async function jwtVerifyToken(jwks, options, token) {
|
|
56
|
+
const {
|
|
57
|
+
payload
|
|
58
|
+
} = await jose.jwtVerify(token, jwks, options);
|
|
59
|
+
return payload;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const ROUTE_PATH_REDIRECT_LOGIN = "/_auth/redirect/login";
|
|
63
|
+
const routeRedirectLoginFactory = config => {
|
|
64
|
+
var _config$oauth$jwksUrl, _config$oauth$tokenPa, _config$session$login;
|
|
65
|
+
const jwksUrl = new URL((_config$oauth$jwksUrl = config.oauth.jwksUrl) != null ? _config$oauth$jwksUrl : `${core.strTrimEnd(config.oauth.issuer, "/")}/.well-known/jwks.json`);
|
|
66
|
+
const tokenUrl = `${config.oauth.baseUrl}/${(_config$oauth$tokenPa = config.oauth.tokenPath) != null ? _config$oauth$tokenPa : "oauth2/token"}`;
|
|
67
|
+
const sessionLogin = (_config$session$login = config.session.login) != null ? _config$session$login : (event, tokens) => cookieSet(event.cookies, COOKIE_TOKENS, tokens);
|
|
68
|
+
async function exchangeCodeForToken(fetch, origin, code) {
|
|
69
|
+
const response = await fetch(tokenUrl, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: {
|
|
72
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
73
|
+
Accept: "application/json"
|
|
74
|
+
},
|
|
75
|
+
body: new URLSearchParams({
|
|
76
|
+
grant_type: "authorization_code",
|
|
77
|
+
client_id: config.oauth.clientId,
|
|
78
|
+
client_secret: config.oauth.clientSecret,
|
|
79
|
+
code,
|
|
80
|
+
redirect_uri: urlConcat(origin, ROUTE_PATH_REDIRECT_LOGIN)
|
|
81
|
+
}).toString()
|
|
82
|
+
});
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
const error = await response.text();
|
|
85
|
+
throw new Error(`Token exchange failed: ${error}`);
|
|
86
|
+
}
|
|
87
|
+
const token = await response.json();
|
|
88
|
+
if (!isTokenExchange(token)) {
|
|
89
|
+
throw new Error("Response is not a valid token exchange.");
|
|
90
|
+
}
|
|
91
|
+
return token;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
path: ROUTE_PATH_REDIRECT_LOGIN,
|
|
95
|
+
async handle({
|
|
96
|
+
event
|
|
97
|
+
}) {
|
|
98
|
+
var _event$url$searchPara, _event$url$searchPara2;
|
|
99
|
+
const state = (_event$url$searchPara = event.url.searchParams.get("state")) != null ? _event$url$searchPara : undefined;
|
|
100
|
+
const stateCookie = cookieGetAndDelete(event.cookies, COOKIE_STATE);
|
|
101
|
+
if (state !== stateCookie) {
|
|
102
|
+
throw new Error("State do not match");
|
|
103
|
+
}
|
|
104
|
+
const code = (_event$url$searchPara2 = event.url.searchParams.get("code")) != null ? _event$url$searchPara2 : undefined;
|
|
105
|
+
core.throwIfUndefined(code);
|
|
106
|
+
const exchange = await exchangeCodeForToken(fetch, event.url.origin, code);
|
|
107
|
+
const jwks = jose.createRemoteJWKSet(jwksUrl);
|
|
108
|
+
const [idToken, accessToken] = await Promise.all([jwtVerifyIdToken(config, jwks, exchange.id_token), jwtVerifyAccessToken(config, jwks, exchange.access_token)]);
|
|
109
|
+
await sessionLogin(event, {
|
|
110
|
+
exchange,
|
|
111
|
+
idToken: idToken,
|
|
112
|
+
accessToken
|
|
113
|
+
});
|
|
114
|
+
throw kit.redirect(302, "/");
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const ROUTE_PATH_LOGIN = "/_auth/login";
|
|
120
|
+
const routeLoginFactory = config => {
|
|
121
|
+
var _config$oauth$authori;
|
|
122
|
+
const authorizeUrl = `${config.oauth.baseUrl}/${(_config$oauth$authori = config.oauth.authorizePath) != null ? _config$oauth$authori : "/oauth2/authorize"}`;
|
|
123
|
+
return {
|
|
124
|
+
path: ROUTE_PATH_LOGIN,
|
|
125
|
+
async handle({
|
|
126
|
+
event
|
|
127
|
+
}) {
|
|
128
|
+
const state = node_crypto.randomUUID();
|
|
129
|
+
cookieSet(event.cookies, COOKIE_STATE, state);
|
|
130
|
+
const params = core.queryParamsCreate({
|
|
131
|
+
client_id: config.oauth.clientId,
|
|
132
|
+
response_type: "code",
|
|
133
|
+
redirect_uri: urlConcat(event.url.origin, ROUTE_PATH_REDIRECT_LOGIN),
|
|
134
|
+
state
|
|
135
|
+
});
|
|
136
|
+
throw kit.redirect(302, `${authorizeUrl}?${params}`);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const ROUTE_PATH_REDIRECT_LOGOUT = "/_auth/redirect/logout";
|
|
142
|
+
const routeRedirectLogoutFactory = config => {
|
|
143
|
+
var _config$session$logou;
|
|
144
|
+
// Check if the oauth provider supports a logout path.
|
|
145
|
+
if (!config.oauth.logoutPath) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
const logout = (_config$session$logou = config.session.logout) != null ? _config$session$logou : core.noop;
|
|
149
|
+
return {
|
|
150
|
+
path: ROUTE_PATH_REDIRECT_LOGOUT,
|
|
151
|
+
async handle({
|
|
152
|
+
event
|
|
153
|
+
}) {
|
|
154
|
+
await logout(event);
|
|
155
|
+
throw kit.redirect(302, "/");
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const ROUTE_PATH_LOGOUT = "/_auth/logout";
|
|
161
|
+
const routeLogoutFactory = config => {
|
|
162
|
+
// Check if the oauth provider supports a logout path.
|
|
163
|
+
if (!config.oauth.logoutPath) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
const logoutUrl = `${config.oauth.baseUrl}/${config.oauth.logoutPath}`;
|
|
167
|
+
return {
|
|
168
|
+
path: ROUTE_PATH_LOGOUT,
|
|
169
|
+
async handle({
|
|
170
|
+
event
|
|
171
|
+
}) {
|
|
172
|
+
const params = core.queryParamsCreate({
|
|
173
|
+
logout_uri: urlConcat(event.url.origin, ROUTE_PATH_REDIRECT_LOGOUT),
|
|
174
|
+
client_id: config.oauth.clientId
|
|
175
|
+
});
|
|
176
|
+
throw kit.redirect(302, `${logoutUrl}?${params}`);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const routeFactories = Object.freeze([routeLoginFactory, routeLogoutFactory, routeRedirectLoginFactory, routeRedirectLogoutFactory]);
|
|
182
|
+
function routeCreate(config) {
|
|
183
|
+
return new Map(routeFactories.map(routeFactory => routeFactory(config)).filter(route => Boolean(route))
|
|
184
|
+
// @ts-expect-error Incorrect typing error.
|
|
185
|
+
.map(route => [route.path, route.handle]));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const ARMOR_LOGIN = ROUTE_PATH_LOGIN;
|
|
189
|
+
const ARMOR_LOGOUT = ROUTE_PATH_LOGOUT;
|
|
190
|
+
function armor(config) {
|
|
191
|
+
var _config$session$exist;
|
|
192
|
+
const routes = routeCreate(config);
|
|
193
|
+
const sessionExists = (_config$session$exist = config.session.exists) != null ? _config$session$exist : event => Boolean(event.cookies.get(COOKIE_TOKENS));
|
|
194
|
+
return async ({
|
|
195
|
+
event,
|
|
196
|
+
resolve
|
|
197
|
+
}) => {
|
|
198
|
+
const routeHandle = routes.get(event.url.pathname);
|
|
199
|
+
if (routeHandle) {
|
|
200
|
+
await routeHandle({
|
|
201
|
+
event,
|
|
202
|
+
resolve
|
|
203
|
+
});
|
|
204
|
+
// Handle should redirect. If it doesn't, something is wrong.
|
|
205
|
+
throw kit.error(500, "Illegal state");
|
|
206
|
+
}
|
|
207
|
+
const exists = await sessionExists(event);
|
|
208
|
+
if (!exists) {
|
|
209
|
+
throw kit.redirect(302, ROUTE_PATH_LOGIN);
|
|
210
|
+
}
|
|
211
|
+
return resolve(event);
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function armorCookiesGetTokens(cookies) {
|
|
215
|
+
const tokens = cookieGet(cookies, COOKIE_TOKENS);
|
|
216
|
+
core.throwIfUndefined(tokens);
|
|
217
|
+
return tokens;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
exports.ARMOR_LOGIN = ARMOR_LOGIN;
|
|
221
|
+
exports.ARMOR_LOGOUT = ARMOR_LOGOUT;
|
|
222
|
+
exports.armor = armor;
|
|
223
|
+
exports.armorCookiesGetTokens = armorCookiesGetTokens;
|
|
224
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/utils/utils.ts","../src/utils/cookie.ts","../src/utils/jwt.ts","../src/routes/redirect-login.ts","../src/routes/login.ts","../src/routes/redirect-logout.ts","../src/routes/logout.ts","../src/routes/routes.ts","../src/index.ts"],"sourcesContent":["import { strTrimEnd } from \"@nekm/core\";\nimport type { ArmorTokenExchange } from \"../contracts\";\n\nexport function urlConcat(origin: string, path: string): string {\n\treturn `${strTrimEnd(origin, \"/\")}/${path}`;\n}\n\nexport function isTokenExchange(value: unknown): value is ArmorTokenExchange {\n\tif (typeof value !== \"object\" || value === null) return false;\n\n\tconst obj = value as Record<string, unknown>;\n\n\treturn (\n\t\ttypeof obj.access_token === \"string\" &&\n\t\tobj.token_type === \"Bearer\" &&\n\t\ttypeof obj.expires_in === \"number\" &&\n\t\t// Optional fields\n\t\t(typeof obj.id_token === \"string\" || obj.id_token === undefined) &&\n\t\t(typeof obj.refresh_token === \"string\" ||\n\t\t\tobj.refresh_token === undefined) &&\n\t\t(typeof obj.scope === \"string\" || obj.scope === undefined)\n\t);\n}\n","import { Cookies } from \"@sveltejs/kit\";\n\nexport const COOKIE_TOKENS = \"tokens\";\nexport const COOKIE_STATE = \"state\";\n\nconst cookieDeleteOptions = Object.freeze({ path: \"/\" });\n\nconst cookieSetOptions = Object.freeze({\n\t...cookieDeleteOptions,\n\thttpOnly: true,\n\tsecure: true,\n\tsameSite: \"lax\",\n\tmaxAge: 1800, // 30 minutes\n});\n\nexport function cookieSet(\n\tcookies: Cookies,\n\tkey: string,\n\tvalue: string | object,\n) {\n\tcookies.set(key, JSON.stringify(value), cookieSetOptions);\n}\n\nexport function cookieGetAndDelete<T>(\n\tcookies: Cookies,\n\tkey: string,\n): T | undefined {\n\tconst value = cookieGet<T>(cookies, key);\n\n\tif (value) {\n\t\tcookies.delete(key, cookieDeleteOptions);\n\t}\n\n\treturn value;\n}\n\nexport function cookieGet<T>(cookies: Cookies, key: string): T | undefined {\n\tconst value = cookies.get(key);\n\n\treturn !value ? undefined : JSON.parse(value);\n}\n","import { ArmorConfig } from \"../contracts\";\nimport { jwtVerify, JWTVerifyGetKey, JWTVerifyOptions } from \"jose\";\n\nexport function jwtVerifyIdToken(\n\tconfig: ArmorConfig,\n\tjwks: JWTVerifyGetKey,\n\tidToken: string,\n) {\n\treturn jwtVerifyToken(\n\t\tjwks,\n\t\t{\n\t\t\tissuer: config.oauth.issuer,\n\t\t\taudience: config.oauth.clientId,\n\t\t},\n\t\tidToken,\n\t);\n}\n\nexport function jwtVerifyAccessToken(\n\tconfig: ArmorConfig,\n\tjwks: JWTVerifyGetKey,\n\taccessToken: string,\n) {\n\treturn jwtVerifyToken(jwks, { issuer: config.oauth.issuer }, accessToken);\n}\n\nasync function jwtVerifyToken(\n\tjwks: JWTVerifyGetKey,\n\toptions: JWTVerifyOptions,\n\ttoken: string,\n) {\n\tconst { payload } = await jwtVerify(token, jwks, options);\n\n\treturn payload;\n}\n","import { redirect } from \"@sveltejs/kit\";\nimport type {\n\tArmorConfig,\n\tArmorIdToken,\n\tArmorTokenExchange,\n} from \"../contracts\";\nimport { strTrimEnd, throwIfUndefined } from \"@nekm/core\";\nimport { createRemoteJWKSet } from \"jose\";\nimport type { RouteFactory } from \"./routes\";\nimport { urlConcat, isTokenExchange } from \"../utils/utils\";\nimport {\n\tCOOKIE_STATE,\n\tCOOKIE_TOKENS,\n\tcookieGetAndDelete,\n\tcookieSet,\n} from \"../utils/cookie\";\nimport { jwtVerifyAccessToken, jwtVerifyIdToken } from \"../utils/jwt\";\n\nexport const ROUTE_PATH_REDIRECT_LOGIN = \"/_auth/redirect/login\";\n\nexport const routeRedirectLoginFactory: RouteFactory = (\n\tconfig: ArmorConfig,\n) => {\n\tconst jwksUrl = new URL(\n\t\tconfig.oauth.jwksUrl ??\n\t\t\t`${strTrimEnd(config.oauth.issuer, \"/\")}/.well-known/jwks.json`,\n\t);\n\tconst tokenUrl = `${config.oauth.baseUrl}/${config.oauth.tokenPath ?? \"oauth2/token\"}`;\n\n\tconst sessionLogin =\n\t\tconfig.session.login ??\n\t\t((event, tokens) => cookieSet(event.cookies, COOKIE_TOKENS, tokens));\n\n\tasync function exchangeCodeForToken(\n\t\tfetch: typeof global.fetch,\n\t\torigin: string,\n\t\tcode: string,\n\t): Promise<ArmorTokenExchange> {\n\t\tconst response = await fetch(tokenUrl, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\t\tAccept: \"application/json\",\n\t\t\t},\n\t\t\tbody: new URLSearchParams({\n\t\t\t\tgrant_type: \"authorization_code\",\n\t\t\t\tclient_id: config.oauth.clientId,\n\t\t\t\tclient_secret: config.oauth.clientSecret,\n\t\t\t\tcode,\n\t\t\t\tredirect_uri: urlConcat(origin, ROUTE_PATH_REDIRECT_LOGIN),\n\t\t\t}).toString(),\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tconst error = await response.text();\n\t\t\tthrow new Error(`Token exchange failed: ${error}`);\n\t\t}\n\n\t\tconst token = await response.json();\n\n\t\tif (!isTokenExchange(token)) {\n\t\t\tthrow new Error(\"Response is not a valid token exchange.\");\n\t\t}\n\n\t\treturn token;\n\t}\n\n\treturn {\n\t\tpath: ROUTE_PATH_REDIRECT_LOGIN,\n\t\tasync handle({ event }) {\n\t\t\tconst state = event.url.searchParams.get(\"state\") ?? undefined;\n\t\t\tconst stateCookie = cookieGetAndDelete(event.cookies, COOKIE_STATE);\n\n\t\t\tif (state !== stateCookie) {\n\t\t\t\tthrow new Error(\"State do not match\");\n\t\t\t}\n\n\t\t\tconst code = event.url.searchParams.get(\"code\") ?? undefined;\n\t\t\tthrowIfUndefined(code);\n\n\t\t\tconst exchange = await exchangeCodeForToken(\n\t\t\t\tfetch,\n\t\t\t\tevent.url.origin,\n\t\t\t\tcode,\n\t\t\t);\n\n\t\t\tconst jwks = createRemoteJWKSet(jwksUrl);\n\n\t\t\tconst [idToken, accessToken] = await Promise.all([\n\t\t\t\tjwtVerifyIdToken(config, jwks, exchange.id_token),\n\t\t\t\tjwtVerifyAccessToken(config, jwks, exchange.access_token),\n\t\t\t]);\n\n\t\t\tawait sessionLogin(event, {\n\t\t\t\texchange,\n\t\t\t\tidToken: idToken as ArmorIdToken,\n\t\t\t\taccessToken,\n\t\t\t});\n\n\t\t\tthrow redirect(302, \"/\");\n\t\t},\n\t};\n};\n","import { redirect } from \"@sveltejs/kit\";\nimport type { ArmorConfig } from \"../contracts\";\nimport { queryParamsCreate } from \"@nekm/core\";\nimport { ROUTE_PATH_REDIRECT_LOGIN } from \"./redirect-login\";\nimport { randomUUID } from \"node:crypto\";\nimport type { RouteFactory } from \"./routes\";\nimport { COOKIE_STATE, cookieSet } from \"../utils/cookie\";\nimport { urlConcat } from \"../utils/utils\";\n\nexport const ROUTE_PATH_LOGIN = \"/_auth/login\";\n\nexport const routeLoginFactory: RouteFactory = (config: ArmorConfig) => {\n\tconst authorizeUrl = `${config.oauth.baseUrl}/${config.oauth.authorizePath ?? \"/oauth2/authorize\"}`;\n\n\treturn {\n\t\tpath: ROUTE_PATH_LOGIN,\n\t\tasync handle({ event }) {\n\t\t\tconst state = randomUUID();\n\t\t\tcookieSet(event.cookies, COOKIE_STATE, state);\n\n\t\t\tconst params = queryParamsCreate({\n\t\t\t\tclient_id: config.oauth.clientId,\n\t\t\t\tresponse_type: \"code\",\n\t\t\t\tredirect_uri: urlConcat(event.url.origin, ROUTE_PATH_REDIRECT_LOGIN),\n\t\t\t\tstate,\n\t\t\t});\n\n\t\t\tthrow redirect(302, `${authorizeUrl}?${params}`);\n\t\t},\n\t};\n};\n","import { redirect } from \"@sveltejs/kit\";\nimport type { ArmorConfig } from \"../contracts\";\nimport { noop } from \"@nekm/core\";\nimport type { RouteFactory } from \"./routes\";\n\nexport const ROUTE_PATH_REDIRECT_LOGOUT = \"/_auth/redirect/logout\";\n\nexport const routeRedirectLogoutFactory: RouteFactory = (\n\tconfig: ArmorConfig,\n) => {\n\t// Check if the oauth provider supports a logout path.\n\tif (!config.oauth.logoutPath) {\n\t\treturn undefined;\n\t}\n\n\tconst logout = config.session.logout ?? noop;\n\n\treturn {\n\t\tpath: ROUTE_PATH_REDIRECT_LOGOUT,\n\t\tasync handle({ event }) {\n\t\t\tawait logout(event);\n\t\t\tthrow redirect(302, \"/\");\n\t\t},\n\t};\n};\n","import { redirect } from \"@sveltejs/kit\";\nimport type { ArmorConfig } from \"../contracts\";\nimport { queryParamsCreate } from \"@nekm/core\";\nimport { ROUTE_PATH_REDIRECT_LOGOUT } from \"./redirect-logout\";\nimport type { RouteFactory } from \"./routes\";\nimport { urlConcat } from \"../utils/utils\";\n\nexport const ROUTE_PATH_LOGOUT = \"/_auth/logout\";\n\nexport const routeLogoutFactory: RouteFactory = (config: ArmorConfig) => {\n\t// Check if the oauth provider supports a logout path.\n\tif (!config.oauth.logoutPath) {\n\t\treturn undefined;\n\t}\n\n\tconst logoutUrl = `${config.oauth.baseUrl}/${config.oauth.logoutPath}`;\n\n\treturn {\n\t\tpath: ROUTE_PATH_LOGOUT,\n\t\tasync handle({ event }) {\n\t\t\tconst params = queryParamsCreate({\n\t\t\t\tlogout_uri: urlConcat(event.url.origin, ROUTE_PATH_REDIRECT_LOGOUT),\n\t\t\t\tclient_id: config.oauth.clientId,\n\t\t\t});\n\n\t\t\tthrow redirect(302, `${logoutUrl}?${params}`);\n\t\t},\n\t};\n};\n","import type { Handle } from \"@sveltejs/kit\";\nimport type { ArmorConfig } from \"../contracts\";\nimport { routeLoginFactory } from \"./login\";\nimport { routeLogoutFactory } from \"./logout\";\nimport { routeRedirectLogoutFactory } from \"./redirect-logout\";\nimport { routeRedirectLoginFactory } from \"./redirect-login\";\n\nexport interface Route {\n\treadonly path: string;\n\treadonly handle: Handle;\n}\n\nexport type RouteFactory = (config: ArmorConfig) => Route | undefined;\n\nconst routeFactories = Object.freeze([\n\trouteLoginFactory,\n\trouteLogoutFactory,\n\trouteRedirectLoginFactory,\n\trouteRedirectLogoutFactory,\n]);\n\nexport function routeCreate(config: ArmorConfig): Map<string, Handle> {\n\treturn new Map(\n\t\trouteFactories\n\t\t\t.map((routeFactory) => routeFactory(config))\n\t\t\t.filter((route) => Boolean(route))\n\t\t\t// @ts-expect-error Incorrect typing error.\n\t\t\t.map((route) => [route.path, route.handle]),\n\t);\n}\n","import { error, redirect, type Handle, Cookies } from \"@sveltejs/kit\";\nimport { ROUTE_PATH_LOGIN } from \"./routes/login\";\nimport type { ArmorConfig, ArmorTokens } from \"./contracts\";\nimport { ROUTE_PATH_LOGOUT } from \"./routes/logout\";\nimport { routeCreate } from \"./routes/routes\";\nimport { COOKIE_TOKENS, cookieGet } from \"./utils/cookie\";\nimport { throwIfUndefined } from \"@nekm/core\";\n\nexport type { ArmorConfig, ArmorTokens };\n\nexport const ARMOR_LOGIN = ROUTE_PATH_LOGIN;\nexport const ARMOR_LOGOUT = ROUTE_PATH_LOGOUT;\n\nexport function armor(config: ArmorConfig): Handle {\n\tconst routes = routeCreate(config);\n\tconst sessionExists =\n\t\tconfig.session.exists ??\n\t\t((event) => Boolean(event.cookies.get(COOKIE_TOKENS)));\n\n\treturn async ({ event, resolve }) => {\n\t\tconst routeHandle = routes.get(event.url.pathname);\n\n\t\tif (routeHandle) {\n\t\t\tawait routeHandle({ event, resolve });\n\n\t\t\t// Handle should redirect. If it doesn't, something is wrong.\n\t\t\tthrow error(500, \"Illegal state\");\n\t\t}\n\n\t\tconst exists = await sessionExists(event);\n\n\t\tif (!exists) {\n\t\t\tthrow redirect(302, ROUTE_PATH_LOGIN);\n\t\t}\n\n\t\treturn resolve(event);\n\t};\n}\n\nexport function armorCookiesGetTokens(cookies: Cookies): ArmorTokens {\n\tconst tokens = cookieGet<ArmorTokens>(cookies, COOKIE_TOKENS);\n\tthrowIfUndefined(tokens);\n\treturn tokens;\n}\n"],"names":["urlConcat","origin","path","strTrimEnd","isTokenExchange","value","obj","access_token","token_type","expires_in","id_token","undefined","refresh_token","scope","COOKIE_TOKENS","COOKIE_STATE","cookieDeleteOptions","Object","freeze","cookieSetOptions","httpOnly","secure","sameSite","maxAge","cookieSet","cookies","key","set","JSON","stringify","cookieGetAndDelete","cookieGet","delete","get","parse","jwtVerifyIdToken","config","jwks","idToken","jwtVerifyToken","issuer","oauth","audience","clientId","jwtVerifyAccessToken","accessToken","options","token","payload","jwtVerify","ROUTE_PATH_REDIRECT_LOGIN","routeRedirectLoginFactory","_config$oauth$jwksUrl","_config$oauth$tokenPa","_config$session$login","jwksUrl","URL","tokenUrl","baseUrl","tokenPath","sessionLogin","session","login","event","tokens","exchangeCodeForToken","fetch","code","response","method","headers","Accept","body","URLSearchParams","grant_type","client_id","client_secret","clientSecret","redirect_uri","toString","ok","error","text","Error","json","handle","_event$url$searchPara","_event$url$searchPara2","state","url","searchParams","stateCookie","throwIfUndefined","exchange","createRemoteJWKSet","Promise","all","redirect","ROUTE_PATH_LOGIN","routeLoginFactory","_config$oauth$authori","authorizeUrl","authorizePath","randomUUID","params","queryParamsCreate","response_type","ROUTE_PATH_REDIRECT_LOGOUT","routeRedirectLogoutFactory","_config$session$logou","logoutPath","logout","noop","ROUTE_PATH_LOGOUT","routeLogoutFactory","logoutUrl","logout_uri","routeFactories","routeCreate","Map","map","routeFactory","filter","route","Boolean","ARMOR_LOGIN","ARMOR_LOGOUT","armor","_config$session$exist","routes","sessionExists","exists","resolve","routeHandle","pathname","armorCookiesGetTokens"],"mappings":";;;;;AAGgB,SAAAA,SAASA,CAACC,MAAc,EAAEC,IAAY,EAAA;EACrD,OAAO,CAAA,EAAGC,eAAU,CAACF,MAAM,EAAE,GAAG,CAAC,CAAIC,CAAAA,EAAAA,IAAI,CAAE,CAAA,CAAA;AAC5C,CAAA;AAEM,SAAUE,eAAeA,CAACC,KAAc,EAAA;EAC7C,IAAI,OAAOA,KAAK,KAAK,QAAQ,IAAIA,KAAK,KAAK,IAAI,EAAE,OAAO,KAAK,CAAA;EAE7D,MAAMC,GAAG,GAAGD,KAAgC,CAAA;AAE5C,EAAA,OACC,OAAOC,GAAG,CAACC,YAAY,KAAK,QAAQ,IACpCD,GAAG,CAACE,UAAU,KAAK,QAAQ,IAC3B,OAAOF,GAAG,CAACG,UAAU,KAAK,QAAQ;AAClC;AACC,EAAA,OAAOH,GAAG,CAACI,QAAQ,KAAK,QAAQ,IAAIJ,GAAG,CAACI,QAAQ,KAAKC,SAAS,CAAC,KAC/D,OAAOL,GAAG,CAACM,aAAa,KAAK,QAAQ,IACrCN,GAAG,CAACM,aAAa,KAAKD,SAAS,CAAC,KAChC,OAAOL,GAAG,CAACO,KAAK,KAAK,QAAQ,IAAIP,GAAG,CAACO,KAAK,KAAKF,SAAS,CAAC,CAAA;AAE5D;;ACpBO,MAAMG,aAAa,GAAG,QAAQ,CAAA;AAC9B,MAAMC,YAAY,GAAG,OAAO,CAAA;AAEnC,MAAMC,mBAAmB,GAAGC,MAAM,CAACC,MAAM,CAAC;AAAEhB,EAAAA,IAAI,EAAE,GAAA;AAAK,CAAA,CAAC,CAAA;AAExD,MAAMiB,gBAAgB,GAAGF,MAAM,CAACC,MAAM,CAAC;AACtC,EAAA,GAAGF,mBAAmB;AACtBI,EAAAA,QAAQ,EAAE,IAAI;AACdC,EAAAA,MAAM,EAAE,IAAI;AACZC,EAAAA,QAAQ,EAAE,KAAK;EACfC,MAAM,EAAE,IAAI;AACZ,CAAA,CAAC,CAAA;SAEcC,SAASA,CACxBC,OAAgB,EAChBC,GAAW,EACXrB,KAAsB,EAAA;AAEtBoB,EAAAA,OAAO,CAACE,GAAG,CAACD,GAAG,EAAEE,IAAI,CAACC,SAAS,CAACxB,KAAK,CAAC,EAAEc,gBAAgB,CAAC,CAAA;AAC1D,CAAA;AAEgB,SAAAW,kBAAkBA,CACjCL,OAAgB,EAChBC,GAAW,EAAA;AAEX,EAAA,MAAMrB,KAAK,GAAG0B,SAAS,CAAIN,OAAO,EAAEC,GAAG,CAAC,CAAA;AAExC,EAAA,IAAIrB,KAAK,EAAE;AACVoB,IAAAA,OAAO,CAACO,MAAM,CAACN,GAAG,EAAEV,mBAAmB,CAAC,CAAA;AACzC,GAAA;AAEA,EAAA,OAAOX,KAAK,CAAA;AACb,CAAA;AAEgB,SAAA0B,SAASA,CAAIN,OAAgB,EAAEC,GAAW,EAAA;AACzD,EAAA,MAAMrB,KAAK,GAAGoB,OAAO,CAACQ,GAAG,CAACP,GAAG,CAAC,CAAA;EAE9B,OAAO,CAACrB,KAAK,GAAGM,SAAS,GAAGiB,IAAI,CAACM,KAAK,CAAC7B,KAAK,CAAC,CAAA;AAC9C;;SCrCgB8B,gBAAgBA,CAC/BC,MAAmB,EACnBC,IAAqB,EACrBC,OAAe,EAAA;EAEf,OAAOC,cAAc,CACpBF,IAAI,EACJ;AACCG,IAAAA,MAAM,EAAEJ,MAAM,CAACK,KAAK,CAACD,MAAM;AAC3BE,IAAAA,QAAQ,EAAEN,MAAM,CAACK,KAAK,CAACE,QAAAA;GACvB,EACDL,OAAO,CACP,CAAA;AACF,CAAA;SAEgBM,oBAAoBA,CACnCR,MAAmB,EACnBC,IAAqB,EACrBQ,WAAmB,EAAA;EAEnB,OAAON,cAAc,CAACF,IAAI,EAAE;AAAEG,IAAAA,MAAM,EAAEJ,MAAM,CAACK,KAAK,CAACD,MAAAA;GAAQ,EAAEK,WAAW,CAAC,CAAA;AAC1E,CAAA;AAEA,eAAeN,cAAcA,CAC5BF,IAAqB,EACrBS,OAAyB,EACzBC,KAAa,EAAA;EAEb,MAAM;AAAEC,IAAAA,OAAAA;GAAS,GAAG,MAAMC,cAAS,CAACF,KAAK,EAAEV,IAAI,EAAES,OAAO,CAAC,CAAA;AAEzD,EAAA,OAAOE,OAAO,CAAA;AACf;;AChBO,MAAME,yBAAyB,GAAG,uBAAuB,CAAA;AAEzD,MAAMC,yBAAyB,GACrCf,MAAmB,IAChB;AAAA,EAAA,IAAAgB,qBAAA,EAAAC,qBAAA,EAAAC,qBAAA,CAAA;EACH,MAAMC,OAAO,GAAG,IAAIC,GAAG,CAAA,CAAAJ,qBAAA,GACtBhB,MAAM,CAACK,KAAK,CAACc,OAAO,KAAA,IAAA,GAAAH,qBAAA,GACnB,CAAGjD,EAAAA,eAAU,CAACiC,MAAM,CAACK,KAAK,CAACD,MAAM,EAAE,GAAG,CAAC,CAAA,sBAAA,CAAwB,CAChE,CAAA;EACD,MAAMiB,QAAQ,GAAG,CAAGrB,EAAAA,MAAM,CAACK,KAAK,CAACiB,OAAO,CAAAL,CAAAA,EAAAA,CAAAA,qBAAA,GAAIjB,MAAM,CAACK,KAAK,CAACkB,SAAS,YAAAN,qBAAA,GAAI,cAAc,CAAE,CAAA,CAAA;EAEtF,MAAMO,YAAY,GAAAN,CAAAA,qBAAA,GACjBlB,MAAM,CAACyB,OAAO,CAACC,KAAK,KAAAR,IAAAA,GAAAA,qBAAA,GACnB,CAACS,KAAK,EAAEC,MAAM,KAAKxC,SAAS,CAACuC,KAAK,CAACtC,OAAO,EAAEX,aAAa,EAAEkD,MAAM,CAAE,CAAA;AAErE,EAAA,eAAeC,oBAAoBA,CAClCC,KAA0B,EAC1BjE,MAAc,EACdkE,IAAY,EAAA;AAEZ,IAAA,MAAMC,QAAQ,GAAG,MAAMF,KAAK,CAACT,QAAQ,EAAE;AACtCY,MAAAA,MAAM,EAAE,MAAM;AACdC,MAAAA,OAAO,EAAE;AACR,QAAA,cAAc,EAAE,mCAAmC;AACnDC,QAAAA,MAAM,EAAE,kBAAA;OACR;MACDC,IAAI,EAAE,IAAIC,eAAe,CAAC;AACzBC,QAAAA,UAAU,EAAE,oBAAoB;AAChCC,QAAAA,SAAS,EAAEvC,MAAM,CAACK,KAAK,CAACE,QAAQ;AAChCiC,QAAAA,aAAa,EAAExC,MAAM,CAACK,KAAK,CAACoC,YAAY;QACxCV,IAAI;AACJW,QAAAA,YAAY,EAAE9E,SAAS,CAACC,MAAM,EAAEiD,yBAAyB,CAAA;OACzD,CAAC,CAAC6B,QAAQ,EAAE;AACb,KAAA,CAAC,CAAA;AAEF,IAAA,IAAI,CAACX,QAAQ,CAACY,EAAE,EAAE;AACjB,MAAA,MAAMC,KAAK,GAAG,MAAMb,QAAQ,CAACc,IAAI,EAAE,CAAA;AACnC,MAAA,MAAM,IAAIC,KAAK,CAAC,CAA0BF,uBAAAA,EAAAA,KAAK,EAAE,CAAC,CAAA;AACnD,KAAA;AAEA,IAAA,MAAMlC,KAAK,GAAG,MAAMqB,QAAQ,CAACgB,IAAI,EAAE,CAAA;AAEnC,IAAA,IAAI,CAAChF,eAAe,CAAC2C,KAAK,CAAC,EAAE;AAC5B,MAAA,MAAM,IAAIoC,KAAK,CAAC,yCAAyC,CAAC,CAAA;AAC3D,KAAA;AAEA,IAAA,OAAOpC,KAAK,CAAA;AACb,GAAA;EAEA,OAAO;AACN7C,IAAAA,IAAI,EAAEgD,yBAAyB;AAC/B,IAAA,MAAMmC,MAAMA,CAAC;AAAEtB,MAAAA,KAAAA;AAAO,KAAA,EAAA;MAAA,IAAAuB,qBAAA,EAAAC,sBAAA,CAAA;AACrB,MAAA,MAAMC,KAAK,GAAAF,CAAAA,qBAAA,GAAGvB,KAAK,CAAC0B,GAAG,CAACC,YAAY,CAACzD,GAAG,CAAC,OAAO,CAAC,KAAAqD,IAAAA,GAAAA,qBAAA,GAAI3E,SAAS,CAAA;MAC9D,MAAMgF,WAAW,GAAG7D,kBAAkB,CAACiC,KAAK,CAACtC,OAAO,EAAEV,YAAY,CAAC,CAAA;MAEnE,IAAIyE,KAAK,KAAKG,WAAW,EAAE;AAC1B,QAAA,MAAM,IAAIR,KAAK,CAAC,oBAAoB,CAAC,CAAA;AACtC,OAAA;AAEA,MAAA,MAAMhB,IAAI,GAAAoB,CAAAA,sBAAA,GAAGxB,KAAK,CAAC0B,GAAG,CAACC,YAAY,CAACzD,GAAG,CAAC,MAAM,CAAC,KAAAsD,IAAAA,GAAAA,sBAAA,GAAI5E,SAAS,CAAA;MAC5DiF,qBAAgB,CAACzB,IAAI,CAAC,CAAA;AAEtB,MAAA,MAAM0B,QAAQ,GAAG,MAAM5B,oBAAoB,CAC1CC,KAAK,EACLH,KAAK,CAAC0B,GAAG,CAACxF,MAAM,EAChBkE,IAAI,CACJ,CAAA;AAED,MAAA,MAAM9B,IAAI,GAAGyD,uBAAkB,CAACvC,OAAO,CAAC,CAAA;AAExC,MAAA,MAAM,CAACjB,OAAO,EAAEO,WAAW,CAAC,GAAG,MAAMkD,OAAO,CAACC,GAAG,CAAC,CAChD7D,gBAAgB,CAACC,MAAM,EAAEC,IAAI,EAAEwD,QAAQ,CAACnF,QAAQ,CAAC,EACjDkC,oBAAoB,CAACR,MAAM,EAAEC,IAAI,EAAEwD,QAAQ,CAACtF,YAAY,CAAC,CACzD,CAAC,CAAA;MAEF,MAAMqD,YAAY,CAACG,KAAK,EAAE;QACzB8B,QAAQ;AACRvD,QAAAA,OAAO,EAAEA,OAAuB;AAChCO,QAAAA,WAAAA;AACA,OAAA,CAAC,CAAA;AAEF,MAAA,MAAMoD,YAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;AACzB,KAAA;GACA,CAAA;AACF,CAAC;;AC7FM,MAAMC,gBAAgB,GAAG,cAAc,CAAA;AAEvC,MAAMC,iBAAiB,GAAkB/D,MAAmB,IAAI;AAAA,EAAA,IAAAgE,qBAAA,CAAA;EACtE,MAAMC,YAAY,GAAG,CAAGjE,EAAAA,MAAM,CAACK,KAAK,CAACiB,OAAO,CAAA0C,CAAAA,EAAAA,CAAAA,qBAAA,GAAIhE,MAAM,CAACK,KAAK,CAAC6D,aAAa,YAAAF,qBAAA,GAAI,mBAAmB,CAAE,CAAA,CAAA;EAEnG,OAAO;AACNlG,IAAAA,IAAI,EAAEgG,gBAAgB;AACtB,IAAA,MAAMb,MAAMA,CAAC;AAAEtB,MAAAA,KAAAA;AAAO,KAAA,EAAA;AACrB,MAAA,MAAMyB,KAAK,GAAGe,sBAAU,EAAE,CAAA;MAC1B/E,SAAS,CAACuC,KAAK,CAACtC,OAAO,EAAEV,YAAY,EAAEyE,KAAK,CAAC,CAAA;MAE7C,MAAMgB,MAAM,GAAGC,sBAAiB,CAAC;AAChC9B,QAAAA,SAAS,EAAEvC,MAAM,CAACK,KAAK,CAACE,QAAQ;AAChC+D,QAAAA,aAAa,EAAE,MAAM;QACrB5B,YAAY,EAAE9E,SAAS,CAAC+D,KAAK,CAAC0B,GAAG,CAACxF,MAAM,EAAEiD,yBAAyB,CAAC;AACpEsC,QAAAA,KAAAA;AACA,OAAA,CAAC,CAAA;MAEF,MAAMS,YAAQ,CAAC,GAAG,EAAE,GAAGI,YAAY,CAAA,CAAA,EAAIG,MAAM,CAAA,CAAE,CAAC,CAAA;AACjD,KAAA;GACA,CAAA;AACF,CAAC;;ACzBM,MAAMG,0BAA0B,GAAG,wBAAwB,CAAA;AAE3D,MAAMC,0BAA0B,GACtCxE,MAAmB,IAChB;AAAA,EAAA,IAAAyE,qBAAA,CAAA;AACH;AACA,EAAA,IAAI,CAACzE,MAAM,CAACK,KAAK,CAACqE,UAAU,EAAE;AAC7B,IAAA,OAAOnG,SAAS,CAAA;AACjB,GAAA;AAEA,EAAA,MAAMoG,MAAM,GAAA,CAAAF,qBAAA,GAAGzE,MAAM,CAACyB,OAAO,CAACkD,MAAM,KAAA,IAAA,GAAAF,qBAAA,GAAIG,SAAI,CAAA;EAE5C,OAAO;AACN9G,IAAAA,IAAI,EAAEyG,0BAA0B;AAChC,IAAA,MAAMtB,MAAMA,CAAC;AAAEtB,MAAAA,KAAAA;AAAO,KAAA,EAAA;MACrB,MAAMgD,MAAM,CAAChD,KAAK,CAAC,CAAA;AACnB,MAAA,MAAMkC,YAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;AACzB,KAAA;GACA,CAAA;AACF,CAAC;;ACjBM,MAAMgB,iBAAiB,GAAG,eAAe,CAAA;AAEzC,MAAMC,kBAAkB,GAAkB9E,MAAmB,IAAI;AACvE;AACA,EAAA,IAAI,CAACA,MAAM,CAACK,KAAK,CAACqE,UAAU,EAAE;AAC7B,IAAA,OAAOnG,SAAS,CAAA;AACjB,GAAA;AAEA,EAAA,MAAMwG,SAAS,GAAG,CAAG/E,EAAAA,MAAM,CAACK,KAAK,CAACiB,OAAO,CAAA,CAAA,EAAItB,MAAM,CAACK,KAAK,CAACqE,UAAU,CAAE,CAAA,CAAA;EAEtE,OAAO;AACN5G,IAAAA,IAAI,EAAE+G,iBAAiB;AACvB,IAAA,MAAM5B,MAAMA,CAAC;AAAEtB,MAAAA,KAAAA;AAAO,KAAA,EAAA;MACrB,MAAMyC,MAAM,GAAGC,sBAAiB,CAAC;QAChCW,UAAU,EAAEpH,SAAS,CAAC+D,KAAK,CAAC0B,GAAG,CAACxF,MAAM,EAAE0G,0BAA0B,CAAC;AACnEhC,QAAAA,SAAS,EAAEvC,MAAM,CAACK,KAAK,CAACE,QAAAA;AACxB,OAAA,CAAC,CAAA;MAEF,MAAMsD,YAAQ,CAAC,GAAG,EAAE,GAAGkB,SAAS,CAAA,CAAA,EAAIX,MAAM,CAAA,CAAE,CAAC,CAAA;AAC9C,KAAA;GACA,CAAA;AACF,CAAC;;ACdD,MAAMa,cAAc,GAAGpG,MAAM,CAACC,MAAM,CAAC,CACpCiF,iBAAiB,EACjBe,kBAAkB,EAClB/D,yBAAyB,EACzByD,0BAA0B,CAC1B,CAAC,CAAA;AAEI,SAAUU,WAAWA,CAAClF,MAAmB,EAAA;EAC9C,OAAO,IAAImF,GAAG,CACbF,cAAc,CACZG,GAAG,CAAEC,YAAY,IAAKA,YAAY,CAACrF,MAAM,CAAC,CAAC,CAC3CsF,MAAM,CAAEC,KAAK,IAAKC,OAAO,CAACD,KAAK,CAAC,CAAA;AACjC;AAAA,GACCH,GAAG,CAAEG,KAAK,IAAK,CAACA,KAAK,CAACzH,IAAI,EAAEyH,KAAK,CAACtC,MAAM,CAAC,CAAC,CAC5C,CAAA;AACF;;ACnBO,MAAMwC,WAAW,GAAG3B,iBAAgB;AACpC,MAAM4B,YAAY,GAAGb,kBAAiB;AAEvC,SAAUc,KAAKA,CAAC3F,MAAmB,EAAA;AAAA,EAAA,IAAA4F,qBAAA,CAAA;AACxC,EAAA,MAAMC,MAAM,GAAGX,WAAW,CAAClF,MAAM,CAAC,CAAA;EAClC,MAAM8F,aAAa,GAAAF,CAAAA,qBAAA,GAClB5F,MAAM,CAACyB,OAAO,CAACsE,MAAM,KAAA,IAAA,GAAAH,qBAAA,GACnBjE,KAAK,IAAK6D,OAAO,CAAC7D,KAAK,CAACtC,OAAO,CAACQ,GAAG,CAACnB,aAAa,CAAC,CAAE,CAAA;AAEvD,EAAA,OAAO,OAAO;IAAEiD,KAAK;AAAEqE,IAAAA,OAAAA;AAAO,GAAE,KAAI;IACnC,MAAMC,WAAW,GAAGJ,MAAM,CAAChG,GAAG,CAAC8B,KAAK,CAAC0B,GAAG,CAAC6C,QAAQ,CAAC,CAAA;AAElD,IAAA,IAAID,WAAW,EAAE;AAChB,MAAA,MAAMA,WAAW,CAAC;QAAEtE,KAAK;AAAEqE,QAAAA,OAAAA;AAAO,OAAE,CAAC,CAAA;AAErC;AACA,MAAA,MAAMnD,SAAK,CAAC,GAAG,EAAE,eAAe,CAAC,CAAA;AAClC,KAAA;AAEA,IAAA,MAAMkD,MAAM,GAAG,MAAMD,aAAa,CAACnE,KAAK,CAAC,CAAA;IAEzC,IAAI,CAACoE,MAAM,EAAE;AACZ,MAAA,MAAMlC,YAAQ,CAAC,GAAG,EAAEC,gBAAgB,CAAC,CAAA;AACtC,KAAA;IAEA,OAAOkC,OAAO,CAACrE,KAAK,CAAC,CAAA;GACrB,CAAA;AACF,CAAA;AAEM,SAAUwE,qBAAqBA,CAAC9G,OAAgB,EAAA;AACrD,EAAA,MAAMuC,MAAM,GAAGjC,SAAS,CAAcN,OAAO,EAAEX,aAAa,CAAC,CAAA;EAC7D8E,qBAAgB,CAAC5B,MAAM,CAAC,CAAA;AACxB,EAAA,OAAOA,MAAM,CAAA;AACd;;;;;;;"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Handle } from "@sveltejs/kit";
|
|
2
|
+
import type { ArmorConfig } from "../contracts";
|
|
3
|
+
export interface Route {
|
|
4
|
+
readonly path: string;
|
|
5
|
+
readonly handle: Handle;
|
|
6
|
+
}
|
|
7
|
+
export type RouteFactory = (config: ArmorConfig) => Route | undefined;
|
|
8
|
+
export declare function routeCreate(config: ArmorConfig): Map<string, Handle>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Cookies } from "@sveltejs/kit";
|
|
2
|
+
export declare const COOKIE_TOKENS = "tokens";
|
|
3
|
+
export declare const COOKIE_STATE = "state";
|
|
4
|
+
export declare function cookieSet(cookies: Cookies, key: string, value: string | object): void;
|
|
5
|
+
export declare function cookieGetAndDelete<T>(cookies: Cookies, key: string): T | undefined;
|
|
6
|
+
export declare function cookieGet<T>(cookies: Cookies, key: string): T | undefined;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { ArmorConfig } from "../contracts";
|
|
2
|
+
import { JWTVerifyGetKey } from "jose";
|
|
3
|
+
export declare function jwtVerifyIdToken(config: ArmorConfig, jwks: JWTVerifyGetKey, idToken: string): Promise<import("jose").JWTPayload>;
|
|
4
|
+
export declare function jwtVerifyAccessToken(config: ArmorConfig, jwks: JWTVerifyGetKey, accessToken: string): Promise<import("jose").JWTPayload>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nekm/sveltekit-armor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zero-config OAuth protection for SvelteKit",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"source": "./src/index.ts",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
"import": "./dist/index.esm.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"module": "./dist/index.esm.js",
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "microbundle --format esm,cjs --target node",
|
|
17
|
+
"test": "jest"
|
|
18
|
+
},
|
|
19
|
+
"maintainers": [
|
|
20
|
+
{
|
|
21
|
+
"email": "niklas@niklasekman.solutions",
|
|
22
|
+
"name": "Niklas Ekman",
|
|
23
|
+
"url": "https://www.niklasekman.solutions"
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/Ekman/TypeScript-Core.git"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/Ekman/TypeScript-Core#readme",
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"src"
|
|
34
|
+
],
|
|
35
|
+
"author": {
|
|
36
|
+
"name": "Niklas Ekman",
|
|
37
|
+
"email": "niklas@niklasekman.solutions"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"sveltekit",
|
|
41
|
+
"svelte",
|
|
42
|
+
"auth",
|
|
43
|
+
"authentication",
|
|
44
|
+
"oauth",
|
|
45
|
+
"oauth2",
|
|
46
|
+
"oidc",
|
|
47
|
+
"openid-connect",
|
|
48
|
+
"cognito",
|
|
49
|
+
"auth0",
|
|
50
|
+
"keycloak",
|
|
51
|
+
"security",
|
|
52
|
+
"protect",
|
|
53
|
+
"session",
|
|
54
|
+
"authorization",
|
|
55
|
+
"sveltekit-auth",
|
|
56
|
+
"sveltekit-oauth"
|
|
57
|
+
],
|
|
58
|
+
"sideEffects": false,
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@tsconfig/recommended": "^1.0.13",
|
|
61
|
+
"@types/jest": "^30.0.0",
|
|
62
|
+
"@types/node": "^25.0.10",
|
|
63
|
+
"@typescript-eslint/eslint-plugin": "^8.53.1",
|
|
64
|
+
"@typescript-eslint/parser": "^8.53.1",
|
|
65
|
+
"eslint": "^9.39.2",
|
|
66
|
+
"globals": "^17.1.0",
|
|
67
|
+
"jest": "^30.2.0",
|
|
68
|
+
"jest-junit": "^16.0.0",
|
|
69
|
+
"microbundle": "^0.15.1",
|
|
70
|
+
"prettier": "^3.8.1",
|
|
71
|
+
"ts-jest": "^29.4.6",
|
|
72
|
+
"ts-node": "^10.9.2",
|
|
73
|
+
"typescript": "^5.9.3"
|
|
74
|
+
},
|
|
75
|
+
"dependencies": {
|
|
76
|
+
"@nekm/core": "^1.7.0",
|
|
77
|
+
"@sveltejs/kit": "^2.50.1",
|
|
78
|
+
"jose": "^6.1.3"
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/contracts.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { RequestEvent } from "@sveltejs/kit";
|
|
2
|
+
import type { JWTPayload } from "jose";
|
|
3
|
+
|
|
4
|
+
// OAuth 2.0 Token Response (RFC 6749 §5.1)
|
|
5
|
+
export interface ArmorTokenExchange {
|
|
6
|
+
readonly access_token: string;
|
|
7
|
+
readonly id_token: string;
|
|
8
|
+
readonly token_type: "Bearer";
|
|
9
|
+
readonly expires_in: number;
|
|
10
|
+
readonly refresh_token?: string;
|
|
11
|
+
readonly scope?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Generic OIDC ID Token Claims
|
|
15
|
+
export type ArmorIdToken = Required<
|
|
16
|
+
Pick<JWTPayload, "iss" | "sub" | "aud" | "exp" | "iat">
|
|
17
|
+
> &
|
|
18
|
+
Omit<JWTPayload, "iss" | "sub" | "aud" | "exp" | "iat">;
|
|
19
|
+
|
|
20
|
+
export interface ArmorAccessToken extends JWTPayload {
|
|
21
|
+
client_id?: string;
|
|
22
|
+
scope?: string;
|
|
23
|
+
version?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ArmorTokens {
|
|
27
|
+
readonly exchange: ArmorTokenExchange;
|
|
28
|
+
readonly idToken: ArmorIdToken;
|
|
29
|
+
readonly accessToken: ArmorAccessToken;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ArmorConfig {
|
|
33
|
+
readonly session: {
|
|
34
|
+
readonly exists?: (event: RequestEvent) => Promise<boolean> | boolean;
|
|
35
|
+
readonly login?: (
|
|
36
|
+
event: RequestEvent,
|
|
37
|
+
tokens: ArmorTokens,
|
|
38
|
+
) => Promise<void> | void;
|
|
39
|
+
readonly logout?: (event: RequestEvent) => Promise<void> | void;
|
|
40
|
+
};
|
|
41
|
+
readonly oauth: {
|
|
42
|
+
readonly clientId: string;
|
|
43
|
+
readonly clientSecret: string;
|
|
44
|
+
readonly baseUrl: string;
|
|
45
|
+
readonly jwksUrl?: string;
|
|
46
|
+
readonly issuer: string;
|
|
47
|
+
readonly authorizePath?: string;
|
|
48
|
+
readonly logoutPath?: string;
|
|
49
|
+
readonly tokenPath?: string;
|
|
50
|
+
};
|
|
51
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { error, redirect, type Handle, Cookies } from "@sveltejs/kit";
|
|
2
|
+
import { ROUTE_PATH_LOGIN } from "./routes/login";
|
|
3
|
+
import type { ArmorConfig, ArmorTokens } from "./contracts";
|
|
4
|
+
import { ROUTE_PATH_LOGOUT } from "./routes/logout";
|
|
5
|
+
import { routeCreate } from "./routes/routes";
|
|
6
|
+
import { COOKIE_TOKENS, cookieGet } from "./utils/cookie";
|
|
7
|
+
import { throwIfUndefined } from "@nekm/core";
|
|
8
|
+
|
|
9
|
+
export type { ArmorConfig, ArmorTokens };
|
|
10
|
+
|
|
11
|
+
export const ARMOR_LOGIN = ROUTE_PATH_LOGIN;
|
|
12
|
+
export const ARMOR_LOGOUT = ROUTE_PATH_LOGOUT;
|
|
13
|
+
|
|
14
|
+
export function armor(config: ArmorConfig): Handle {
|
|
15
|
+
const routes = routeCreate(config);
|
|
16
|
+
const sessionExists =
|
|
17
|
+
config.session.exists ??
|
|
18
|
+
((event) => Boolean(event.cookies.get(COOKIE_TOKENS)));
|
|
19
|
+
|
|
20
|
+
return async ({ event, resolve }) => {
|
|
21
|
+
const routeHandle = routes.get(event.url.pathname);
|
|
22
|
+
|
|
23
|
+
if (routeHandle) {
|
|
24
|
+
await routeHandle({ event, resolve });
|
|
25
|
+
|
|
26
|
+
// Handle should redirect. If it doesn't, something is wrong.
|
|
27
|
+
throw error(500, "Illegal state");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const exists = await sessionExists(event);
|
|
31
|
+
|
|
32
|
+
if (!exists) {
|
|
33
|
+
throw redirect(302, ROUTE_PATH_LOGIN);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return resolve(event);
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function armorCookiesGetTokens(cookies: Cookies): ArmorTokens {
|
|
41
|
+
const tokens = cookieGet<ArmorTokens>(cookies, COOKIE_TOKENS);
|
|
42
|
+
throwIfUndefined(tokens);
|
|
43
|
+
return tokens;
|
|
44
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { redirect } from "@sveltejs/kit";
|
|
2
|
+
import type { ArmorConfig } from "../contracts";
|
|
3
|
+
import { queryParamsCreate } from "@nekm/core";
|
|
4
|
+
import { ROUTE_PATH_REDIRECT_LOGIN } from "./redirect-login";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import type { RouteFactory } from "./routes";
|
|
7
|
+
import { COOKIE_STATE, cookieSet } from "../utils/cookie";
|
|
8
|
+
import { urlConcat } from "../utils/utils";
|
|
9
|
+
|
|
10
|
+
export const ROUTE_PATH_LOGIN = "/_auth/login";
|
|
11
|
+
|
|
12
|
+
export const routeLoginFactory: RouteFactory = (config: ArmorConfig) => {
|
|
13
|
+
const authorizeUrl = `${config.oauth.baseUrl}/${config.oauth.authorizePath ?? "/oauth2/authorize"}`;
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
path: ROUTE_PATH_LOGIN,
|
|
17
|
+
async handle({ event }) {
|
|
18
|
+
const state = randomUUID();
|
|
19
|
+
cookieSet(event.cookies, COOKIE_STATE, state);
|
|
20
|
+
|
|
21
|
+
const params = queryParamsCreate({
|
|
22
|
+
client_id: config.oauth.clientId,
|
|
23
|
+
response_type: "code",
|
|
24
|
+
redirect_uri: urlConcat(event.url.origin, ROUTE_PATH_REDIRECT_LOGIN),
|
|
25
|
+
state,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
throw redirect(302, `${authorizeUrl}?${params}`);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { redirect } from "@sveltejs/kit";
|
|
2
|
+
import type { ArmorConfig } from "../contracts";
|
|
3
|
+
import { queryParamsCreate } from "@nekm/core";
|
|
4
|
+
import { ROUTE_PATH_REDIRECT_LOGOUT } from "./redirect-logout";
|
|
5
|
+
import type { RouteFactory } from "./routes";
|
|
6
|
+
import { urlConcat } from "../utils/utils";
|
|
7
|
+
|
|
8
|
+
export const ROUTE_PATH_LOGOUT = "/_auth/logout";
|
|
9
|
+
|
|
10
|
+
export const routeLogoutFactory: RouteFactory = (config: ArmorConfig) => {
|
|
11
|
+
// Check if the oauth provider supports a logout path.
|
|
12
|
+
if (!config.oauth.logoutPath) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const logoutUrl = `${config.oauth.baseUrl}/${config.oauth.logoutPath}`;
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
path: ROUTE_PATH_LOGOUT,
|
|
20
|
+
async handle({ event }) {
|
|
21
|
+
const params = queryParamsCreate({
|
|
22
|
+
logout_uri: urlConcat(event.url.origin, ROUTE_PATH_REDIRECT_LOGOUT),
|
|
23
|
+
client_id: config.oauth.clientId,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
throw redirect(302, `${logoutUrl}?${params}`);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { redirect } from "@sveltejs/kit";
|
|
2
|
+
import type {
|
|
3
|
+
ArmorConfig,
|
|
4
|
+
ArmorIdToken,
|
|
5
|
+
ArmorTokenExchange,
|
|
6
|
+
} from "../contracts";
|
|
7
|
+
import { strTrimEnd, throwIfUndefined } from "@nekm/core";
|
|
8
|
+
import { createRemoteJWKSet } from "jose";
|
|
9
|
+
import type { RouteFactory } from "./routes";
|
|
10
|
+
import { urlConcat, isTokenExchange } from "../utils/utils";
|
|
11
|
+
import {
|
|
12
|
+
COOKIE_STATE,
|
|
13
|
+
COOKIE_TOKENS,
|
|
14
|
+
cookieGetAndDelete,
|
|
15
|
+
cookieSet,
|
|
16
|
+
} from "../utils/cookie";
|
|
17
|
+
import { jwtVerifyAccessToken, jwtVerifyIdToken } from "../utils/jwt";
|
|
18
|
+
|
|
19
|
+
export const ROUTE_PATH_REDIRECT_LOGIN = "/_auth/redirect/login";
|
|
20
|
+
|
|
21
|
+
export const routeRedirectLoginFactory: RouteFactory = (
|
|
22
|
+
config: ArmorConfig,
|
|
23
|
+
) => {
|
|
24
|
+
const jwksUrl = new URL(
|
|
25
|
+
config.oauth.jwksUrl ??
|
|
26
|
+
`${strTrimEnd(config.oauth.issuer, "/")}/.well-known/jwks.json`,
|
|
27
|
+
);
|
|
28
|
+
const tokenUrl = `${config.oauth.baseUrl}/${config.oauth.tokenPath ?? "oauth2/token"}`;
|
|
29
|
+
|
|
30
|
+
const sessionLogin =
|
|
31
|
+
config.session.login ??
|
|
32
|
+
((event, tokens) => cookieSet(event.cookies, COOKIE_TOKENS, tokens));
|
|
33
|
+
|
|
34
|
+
async function exchangeCodeForToken(
|
|
35
|
+
fetch: typeof global.fetch,
|
|
36
|
+
origin: string,
|
|
37
|
+
code: string,
|
|
38
|
+
): Promise<ArmorTokenExchange> {
|
|
39
|
+
const response = await fetch(tokenUrl, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: {
|
|
42
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
43
|
+
Accept: "application/json",
|
|
44
|
+
},
|
|
45
|
+
body: new URLSearchParams({
|
|
46
|
+
grant_type: "authorization_code",
|
|
47
|
+
client_id: config.oauth.clientId,
|
|
48
|
+
client_secret: config.oauth.clientSecret,
|
|
49
|
+
code,
|
|
50
|
+
redirect_uri: urlConcat(origin, ROUTE_PATH_REDIRECT_LOGIN),
|
|
51
|
+
}).toString(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
const error = await response.text();
|
|
56
|
+
throw new Error(`Token exchange failed: ${error}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const token = await response.json();
|
|
60
|
+
|
|
61
|
+
if (!isTokenExchange(token)) {
|
|
62
|
+
throw new Error("Response is not a valid token exchange.");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return token;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
path: ROUTE_PATH_REDIRECT_LOGIN,
|
|
70
|
+
async handle({ event }) {
|
|
71
|
+
const state = event.url.searchParams.get("state") ?? undefined;
|
|
72
|
+
const stateCookie = cookieGetAndDelete(event.cookies, COOKIE_STATE);
|
|
73
|
+
|
|
74
|
+
if (state !== stateCookie) {
|
|
75
|
+
throw new Error("State do not match");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const code = event.url.searchParams.get("code") ?? undefined;
|
|
79
|
+
throwIfUndefined(code);
|
|
80
|
+
|
|
81
|
+
const exchange = await exchangeCodeForToken(
|
|
82
|
+
fetch,
|
|
83
|
+
event.url.origin,
|
|
84
|
+
code,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const jwks = createRemoteJWKSet(jwksUrl);
|
|
88
|
+
|
|
89
|
+
const [idToken, accessToken] = await Promise.all([
|
|
90
|
+
jwtVerifyIdToken(config, jwks, exchange.id_token),
|
|
91
|
+
jwtVerifyAccessToken(config, jwks, exchange.access_token),
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
await sessionLogin(event, {
|
|
95
|
+
exchange,
|
|
96
|
+
idToken: idToken as ArmorIdToken,
|
|
97
|
+
accessToken,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
throw redirect(302, "/");
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { redirect } from "@sveltejs/kit";
|
|
2
|
+
import type { ArmorConfig } from "../contracts";
|
|
3
|
+
import { noop } from "@nekm/core";
|
|
4
|
+
import type { RouteFactory } from "./routes";
|
|
5
|
+
|
|
6
|
+
export const ROUTE_PATH_REDIRECT_LOGOUT = "/_auth/redirect/logout";
|
|
7
|
+
|
|
8
|
+
export const routeRedirectLogoutFactory: RouteFactory = (
|
|
9
|
+
config: ArmorConfig,
|
|
10
|
+
) => {
|
|
11
|
+
// Check if the oauth provider supports a logout path.
|
|
12
|
+
if (!config.oauth.logoutPath) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const logout = config.session.logout ?? noop;
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
path: ROUTE_PATH_REDIRECT_LOGOUT,
|
|
20
|
+
async handle({ event }) {
|
|
21
|
+
await logout(event);
|
|
22
|
+
throw redirect(302, "/");
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Handle } from "@sveltejs/kit";
|
|
2
|
+
import type { ArmorConfig } from "../contracts";
|
|
3
|
+
import { routeLoginFactory } from "./login";
|
|
4
|
+
import { routeLogoutFactory } from "./logout";
|
|
5
|
+
import { routeRedirectLogoutFactory } from "./redirect-logout";
|
|
6
|
+
import { routeRedirectLoginFactory } from "./redirect-login";
|
|
7
|
+
|
|
8
|
+
export interface Route {
|
|
9
|
+
readonly path: string;
|
|
10
|
+
readonly handle: Handle;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type RouteFactory = (config: ArmorConfig) => Route | undefined;
|
|
14
|
+
|
|
15
|
+
const routeFactories = Object.freeze([
|
|
16
|
+
routeLoginFactory,
|
|
17
|
+
routeLogoutFactory,
|
|
18
|
+
routeRedirectLoginFactory,
|
|
19
|
+
routeRedirectLogoutFactory,
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export function routeCreate(config: ArmorConfig): Map<string, Handle> {
|
|
23
|
+
return new Map(
|
|
24
|
+
routeFactories
|
|
25
|
+
.map((routeFactory) => routeFactory(config))
|
|
26
|
+
.filter((route) => Boolean(route))
|
|
27
|
+
// @ts-expect-error Incorrect typing error.
|
|
28
|
+
.map((route) => [route.path, route.handle]),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Cookies } from "@sveltejs/kit";
|
|
2
|
+
|
|
3
|
+
export const COOKIE_TOKENS = "tokens";
|
|
4
|
+
export const COOKIE_STATE = "state";
|
|
5
|
+
|
|
6
|
+
const cookieDeleteOptions = Object.freeze({ path: "/" });
|
|
7
|
+
|
|
8
|
+
const cookieSetOptions = Object.freeze({
|
|
9
|
+
...cookieDeleteOptions,
|
|
10
|
+
httpOnly: true,
|
|
11
|
+
secure: true,
|
|
12
|
+
sameSite: "lax",
|
|
13
|
+
maxAge: 1800, // 30 minutes
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export function cookieSet(
|
|
17
|
+
cookies: Cookies,
|
|
18
|
+
key: string,
|
|
19
|
+
value: string | object,
|
|
20
|
+
) {
|
|
21
|
+
cookies.set(key, JSON.stringify(value), cookieSetOptions);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function cookieGetAndDelete<T>(
|
|
25
|
+
cookies: Cookies,
|
|
26
|
+
key: string,
|
|
27
|
+
): T | undefined {
|
|
28
|
+
const value = cookieGet<T>(cookies, key);
|
|
29
|
+
|
|
30
|
+
if (value) {
|
|
31
|
+
cookies.delete(key, cookieDeleteOptions);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function cookieGet<T>(cookies: Cookies, key: string): T | undefined {
|
|
38
|
+
const value = cookies.get(key);
|
|
39
|
+
|
|
40
|
+
return !value ? undefined : JSON.parse(value);
|
|
41
|
+
}
|
package/src/utils/jwt.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ArmorConfig } from "../contracts";
|
|
2
|
+
import { jwtVerify, JWTVerifyGetKey, JWTVerifyOptions } from "jose";
|
|
3
|
+
|
|
4
|
+
export function jwtVerifyIdToken(
|
|
5
|
+
config: ArmorConfig,
|
|
6
|
+
jwks: JWTVerifyGetKey,
|
|
7
|
+
idToken: string,
|
|
8
|
+
) {
|
|
9
|
+
return jwtVerifyToken(
|
|
10
|
+
jwks,
|
|
11
|
+
{
|
|
12
|
+
issuer: config.oauth.issuer,
|
|
13
|
+
audience: config.oauth.clientId,
|
|
14
|
+
},
|
|
15
|
+
idToken,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function jwtVerifyAccessToken(
|
|
20
|
+
config: ArmorConfig,
|
|
21
|
+
jwks: JWTVerifyGetKey,
|
|
22
|
+
accessToken: string,
|
|
23
|
+
) {
|
|
24
|
+
return jwtVerifyToken(jwks, { issuer: config.oauth.issuer }, accessToken);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function jwtVerifyToken(
|
|
28
|
+
jwks: JWTVerifyGetKey,
|
|
29
|
+
options: JWTVerifyOptions,
|
|
30
|
+
token: string,
|
|
31
|
+
) {
|
|
32
|
+
const { payload } = await jwtVerify(token, jwks, options);
|
|
33
|
+
|
|
34
|
+
return payload;
|
|
35
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { isTokenExchange, urlConcat } from "./utils";
|
|
2
|
+
|
|
3
|
+
describe("utils", () => {
|
|
4
|
+
it("should be able to concat URL with path", () => {
|
|
5
|
+
expect(urlConcat("https://foo.example/", "bar")).toBe(
|
|
6
|
+
"https://foo.example/bar",
|
|
7
|
+
);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("can detect a valid ArmorTokenExchange", () => {
|
|
11
|
+
const token = {
|
|
12
|
+
access_token: "abc123",
|
|
13
|
+
token_type: "Bearer",
|
|
14
|
+
expires_in: 3600,
|
|
15
|
+
};
|
|
16
|
+
expect(isTokenExchange(token)).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { strTrimEnd } from "@nekm/core";
|
|
2
|
+
import type { ArmorTokenExchange } from "../contracts";
|
|
3
|
+
|
|
4
|
+
export function urlConcat(origin: string, path: string): string {
|
|
5
|
+
return `${strTrimEnd(origin, "/")}/${path}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function isTokenExchange(value: unknown): value is ArmorTokenExchange {
|
|
9
|
+
if (typeof value !== "object" || value === null) return false;
|
|
10
|
+
|
|
11
|
+
const obj = value as Record<string, unknown>;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
typeof obj.access_token === "string" &&
|
|
15
|
+
obj.token_type === "Bearer" &&
|
|
16
|
+
typeof obj.expires_in === "number" &&
|
|
17
|
+
// Optional fields
|
|
18
|
+
(typeof obj.id_token === "string" || obj.id_token === undefined) &&
|
|
19
|
+
(typeof obj.refresh_token === "string" ||
|
|
20
|
+
obj.refresh_token === undefined) &&
|
|
21
|
+
(typeof obj.scope === "string" || obj.scope === undefined)
|
|
22
|
+
);
|
|
23
|
+
}
|