@crossauth/sveltekit 1.0.1 → 1.1.1
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/README.md +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +16 -6181
- package/dist/sveltekitadminclientendpoints.d.ts +13 -12
- package/dist/sveltekitadminclientendpoints.js +187 -0
- package/dist/sveltekitadminendpoints.d.ts +5 -4
- package/dist/sveltekitadminendpoints.js +766 -0
- package/dist/sveltekitapikey.d.ts +4 -3
- package/dist/sveltekitapikey.js +81 -0
- package/dist/sveltekitoauthclient.d.ts +6 -4
- package/dist/sveltekitoauthclient.js +2309 -0
- package/dist/sveltekitoauthserver.d.ts +4 -4
- package/dist/sveltekitoauthserver.js +1350 -0
- package/dist/sveltekitresserver.d.ts +6 -4
- package/dist/sveltekitresserver.js +286 -0
- package/dist/sveltekitserver.d.ts +11 -9
- package/dist/sveltekitserver.js +393 -0
- package/dist/sveltekitsession.d.ts +6 -5
- package/dist/sveltekitsession.js +1112 -0
- package/dist/sveltekitsessionadapter.d.ts +2 -3
- package/dist/sveltekitsessionadapter.js +2 -0
- package/dist/sveltekitsharedclientendpoints.d.ts +7 -6
- package/dist/sveltekitsharedclientendpoints.js +630 -0
- package/dist/sveltekituserclientendpoints.d.ts +13 -12
- package/dist/sveltekituserclientendpoints.js +270 -0
- package/dist/sveltekituserendpoints.d.ts +6 -5
- package/dist/sveltekituserendpoints.js +1813 -0
- package/dist/tests/sveltekitadminclientendpoints.test.js +330 -0
- package/dist/tests/sveltekitadminendpoints.test.js +242 -0
- package/dist/tests/sveltekitapikeyserver.test.js +44 -0
- package/dist/tests/sveltekitoauthclient.test.d.ts +5 -5
- package/dist/tests/sveltekitoauthclient.test.js +1016 -0
- package/dist/tests/sveltekitoauthresserver.test.d.ts +4 -4
- package/dist/tests/sveltekitoauthresserver.test.js +185 -0
- package/dist/tests/sveltekitoauthserver.test.js +673 -0
- package/dist/tests/sveltekituserclientendpoints.test.js +244 -0
- package/dist/tests/sveltekituserendpoints.test.js +152 -0
- package/dist/tests/sveltemock.test.js +36 -0
- package/dist/tests/sveltemocks.d.ts +22 -8
- package/dist/tests/sveltemocks.js +114 -0
- package/dist/tests/sveltesessionhooks.test.js +224 -0
- package/dist/tests/testshared.d.ts +8 -8
- package/dist/tests/testshared.js +344 -0
- package/dist/utils.d.ts +1 -2
- package/dist/utils.js +123 -0
- package/package.json +23 -15
- package/dist/index.cjs +0 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { RequestEvent
|
|
2
|
-
import { User } from '@crossauth/common';
|
|
3
|
-
import { OAuthResourceServer, UserStorage
|
|
1
|
+
import type { RequestEvent } from '@sveltejs/kit';
|
|
2
|
+
import { type User } from '@crossauth/common';
|
|
3
|
+
import { OAuthResourceServer, UserStorage } from '@crossauth/backend';
|
|
4
|
+
import type { OAuthResourceServerOptions } from '@crossauth/backend';
|
|
5
|
+
import { OAuthTokenConsumer } from '@crossauth/backend';
|
|
4
6
|
import { SvelteKitSessionAdapter } from './sveltekitsessionadapter';
|
|
5
|
-
|
|
7
|
+
import { type MaybePromise } from './tests/sveltemocks';
|
|
6
8
|
/**
|
|
7
9
|
* Options for {@link SvelteKitOAuthResourceServer}
|
|
8
10
|
*/
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { CrossauthError, CrossauthLogger, j, ErrorCode } from '@crossauth/common';
|
|
2
|
+
import { setParameter, ParamType, OAuthResourceServer, UserStorage } from '@crossauth/backend';
|
|
3
|
+
import { OAuthTokenConsumer } from '@crossauth/backend';
|
|
4
|
+
import { SvelteKitSessionAdapter } from './sveltekitsessionadapter';
|
|
5
|
+
import {} from './tests/sveltemocks';
|
|
6
|
+
/**
|
|
7
|
+
* OAuth resource server.
|
|
8
|
+
*
|
|
9
|
+
* You can subclass this, simply instantiate it, or create it through
|
|
10
|
+
* {@link SvelteKitServer}.
|
|
11
|
+
*
|
|
12
|
+
* There are two way of using this class. If you don't set
|
|
13
|
+
* `protectedEndpoints` in
|
|
14
|
+
* {@link SvelteKitOAuthResourceServer.constructor}, then in your
|
|
15
|
+
* protected endpoints, call {@link SvelteKitOAuthResourceServer.authorized}
|
|
16
|
+
* to check if the access token is valid and get any user credentials.
|
|
17
|
+
*
|
|
18
|
+
* If you do set `protectedEndpoints` in
|
|
19
|
+
* {@link SvelteKitOAuthResourceServer.constructor}
|
|
20
|
+
* then a hook is created.
|
|
21
|
+
*
|
|
22
|
+
* **Middleware**
|
|
23
|
+
* The hook
|
|
24
|
+
* hook will set the `accessTokenPayload`, `user` and `scope` fields
|
|
25
|
+
* on the event locals based on the content
|
|
26
|
+
* of the access token in the `Authorization` header if it is valid.
|
|
27
|
+
* If a user storage is provided,
|
|
28
|
+
* it will be used to look the user up. Otherwise a minimal user object
|
|
29
|
+
* is created.
|
|
30
|
+
* If it is not valid it will set the `authError` and `authErrorDescription`.
|
|
31
|
+
* If the access token is invalid, or there is an error, a 401 or 500
|
|
32
|
+
* response is sent before executing your endpoint code. As per
|
|
33
|
+
* OAuth requirements, if the response is a 401, the WWW-Authenticate header
|
|
34
|
+
* is set. If a scope is required this is included in that header.
|
|
35
|
+
*/
|
|
36
|
+
export class SvelteKitOAuthResourceServer extends OAuthResourceServer {
|
|
37
|
+
userStorage;
|
|
38
|
+
errorBody = {};
|
|
39
|
+
protectedEndpoints = {};
|
|
40
|
+
protectedEndpointPrefixes = [];
|
|
41
|
+
sessionDataName = "oauth";
|
|
42
|
+
tokenLocations = ["header"];
|
|
43
|
+
sessionAdapter;
|
|
44
|
+
/**
|
|
45
|
+
* Hook to check if the user is logged in and set data in `locals`
|
|
46
|
+
* accordingly.
|
|
47
|
+
*/
|
|
48
|
+
hook;
|
|
49
|
+
/**
|
|
50
|
+
* Constructor
|
|
51
|
+
* @param tokenConsumers the token consumers, one per issuer and audience
|
|
52
|
+
* @param options See {@link SvelteKitOAuthResourceServerOptions}
|
|
53
|
+
*/
|
|
54
|
+
constructor(tokenConsumers, options = {}) {
|
|
55
|
+
super(tokenConsumers, options);
|
|
56
|
+
setParameter("errorBody", ParamType.Json, this, options, "OAUTH_RESSERVER_ACCESS_DENIED_BODY");
|
|
57
|
+
setParameter("tokenLocations", ParamType.JsonArray, this, options, "OAUTH_TOKEN_LOCATIONS");
|
|
58
|
+
setParameter("sessionDataName", ParamType.String, this, options, "OAUTH_SESSION_DATA_NAME");
|
|
59
|
+
this.userStorage = options.userStorage;
|
|
60
|
+
this.sessionAdapter = options.sessionAdapter;
|
|
61
|
+
if (options.protectedEndpoints) {
|
|
62
|
+
const regex = /^[!#\$%&'\(\)\*\+,\.\/a-zA-Z\[\]\^_`-]+/;
|
|
63
|
+
for (const [key, value] of Object.entries(options.protectedEndpoints)) {
|
|
64
|
+
if (!key.startsWith("/")) {
|
|
65
|
+
throw new CrossauthError(ErrorCode.Configuration, "protected endpoints must be absolute paths without the protocol and hostname");
|
|
66
|
+
}
|
|
67
|
+
if (value.scope) {
|
|
68
|
+
value.scope.forEach((s) => {
|
|
69
|
+
if (!(regex.test(s)))
|
|
70
|
+
throw new CrossauthError(ErrorCode.Configuration, "Illegal characters in scope " + s);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
this.protectedEndpoints = { ...options.protectedEndpoints };
|
|
75
|
+
for (let name in options.protectedEndpoints) {
|
|
76
|
+
let endpoint = this.protectedEndpoints[name];
|
|
77
|
+
if (endpoint.suburls == true) {
|
|
78
|
+
if (!name.endsWith("/")) {
|
|
79
|
+
name += "/";
|
|
80
|
+
this.protectedEndpoints[name] = endpoint;
|
|
81
|
+
}
|
|
82
|
+
this.protectedEndpointPrefixes.push(name);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (options.protectedEndpoints) {
|
|
87
|
+
// validate access token and put in request, along with any errors
|
|
88
|
+
this.hook = async ({ event }) => {
|
|
89
|
+
// don't authenticate if user already logged in with a session
|
|
90
|
+
//if (request.user && request.authType == "cookie") return;
|
|
91
|
+
const urlWithoutQuery = event.url.pathname;
|
|
92
|
+
let matches = false;
|
|
93
|
+
let matchingEndpoint = "";
|
|
94
|
+
if (urlWithoutQuery in this.protectedEndpoints) {
|
|
95
|
+
matches = true;
|
|
96
|
+
matchingEndpoint = urlWithoutQuery;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
for (let name of this.protectedEndpointPrefixes) {
|
|
100
|
+
if (urlWithoutQuery.startsWith(name))
|
|
101
|
+
matches = true;
|
|
102
|
+
matchingEndpoint = name;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!matches)
|
|
106
|
+
return;
|
|
107
|
+
const authResponse = await this.authorized(event);
|
|
108
|
+
// If we are also we are not allowing authentication by
|
|
109
|
+
// and the user is valid, session cookie for this endpoint
|
|
110
|
+
if (!(event.locals.user && event.locals.authType == "cookie"
|
|
111
|
+
&& this.protectedEndpoints[matchingEndpoint].acceptSessionAuthorization != true)) {
|
|
112
|
+
if (!authResponse) {
|
|
113
|
+
event.locals.authError = "access_denied";
|
|
114
|
+
event.locals.authErrorDescription = "No access token";
|
|
115
|
+
const authenticateHeader = this.authenticateHeader(event);
|
|
116
|
+
return new Response(JSON.stringify(this.errorBody), { headers: {
|
|
117
|
+
"content-type": "application/json",
|
|
118
|
+
'WWW-Authenticate': authenticateHeader
|
|
119
|
+
},
|
|
120
|
+
status: 401 });
|
|
121
|
+
}
|
|
122
|
+
if (!authResponse.authorized) {
|
|
123
|
+
const authenticateHeader = this.authenticateHeader(event);
|
|
124
|
+
return new Response(JSON.stringify(this.errorBody), { headers: {
|
|
125
|
+
"content-type": "application/json",
|
|
126
|
+
'WWW-Authenticate': authenticateHeader
|
|
127
|
+
},
|
|
128
|
+
status: 401 });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (authResponse) {
|
|
132
|
+
// we have a valid token - set the user from it
|
|
133
|
+
event.locals.accessTokenPayload = authResponse.tokenPayload;
|
|
134
|
+
event.locals.user = authResponse.user;
|
|
135
|
+
if (authResponse.tokenPayload?.scope) {
|
|
136
|
+
if (Array.isArray(authResponse.tokenPayload.scope)) {
|
|
137
|
+
let scope = [];
|
|
138
|
+
for (let tokenScope of authResponse.tokenPayload.scope) {
|
|
139
|
+
if (typeof tokenScope == "string") {
|
|
140
|
+
scope.push(tokenScope);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
event.locals.scope = scope;
|
|
144
|
+
}
|
|
145
|
+
else if (typeof authResponse.tokenPayload.scope == "string") {
|
|
146
|
+
event.locals.scope = authResponse.tokenPayload.scope.split(" ");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (this.protectedEndpoints[matchingEndpoint].scope) {
|
|
150
|
+
for (let scope of this.protectedEndpoints[matchingEndpoint].scope ?? []) {
|
|
151
|
+
if (!event.locals.scope || !(event.locals.scope.includes(scope))
|
|
152
|
+
&& this.protectedEndpoints[matchingEndpoint].acceptSessionAuthorization != true) {
|
|
153
|
+
CrossauthLogger.logger.warn(j({ msg: "Access token does not have sufficient scope",
|
|
154
|
+
username: event.locals.user?.username, url: event.request.url }));
|
|
155
|
+
event.locals.scope = undefined;
|
|
156
|
+
event.locals.accessTokenPayload = undefined;
|
|
157
|
+
event.locals.user = undefined;
|
|
158
|
+
event.locals.authError = "access_denied";
|
|
159
|
+
event.locals.authErrorDescription = "Access token does not have sufficient scope";
|
|
160
|
+
const authenticateHeader = this.authenticateHeader(event);
|
|
161
|
+
return new Response(JSON.stringify(this.errorBody), { headers: {
|
|
162
|
+
"content-type": "application/json",
|
|
163
|
+
'WWW-Authenticate': authenticateHeader
|
|
164
|
+
},
|
|
165
|
+
status: 401 });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
event.locals.authType = "oauth";
|
|
170
|
+
event.locals.authError = authResponse?.error;
|
|
171
|
+
if (authResponse?.error == "access_denied") {
|
|
172
|
+
const authenticateHeader = this.authenticateHeader(event);
|
|
173
|
+
return new Response(JSON.stringify(this.errorBody), { headers: {
|
|
174
|
+
"content-type": "application/json",
|
|
175
|
+
'WWW-Authenticate': authenticateHeader
|
|
176
|
+
},
|
|
177
|
+
status: 401 });
|
|
178
|
+
}
|
|
179
|
+
else if (authResponse?.error) {
|
|
180
|
+
return new Response(JSON.stringify(this.errorBody), { headers: {
|
|
181
|
+
"content-type": "application/json",
|
|
182
|
+
},
|
|
183
|
+
status: 500 });
|
|
184
|
+
}
|
|
185
|
+
event.locals.authErrorDescription = authResponse?.error_description;
|
|
186
|
+
CrossauthLogger.logger.debug(j({ msg: "Resource server url", url: event.request.url, authorized: event.locals.accessTokenPayload != undefined }));
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
authenticateHeader(event) {
|
|
192
|
+
const urlWithoutQuery = event.url.pathname;
|
|
193
|
+
if (urlWithoutQuery in this.protectedEndpoints) {
|
|
194
|
+
let header = "Bearer";
|
|
195
|
+
if (this.protectedEndpoints[urlWithoutQuery].scope) {
|
|
196
|
+
header += ' scope="' + (this.protectedEndpoints[urlWithoutQuery].scope ?? []).join(" ");
|
|
197
|
+
}
|
|
198
|
+
return header;
|
|
199
|
+
}
|
|
200
|
+
return "";
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* If there is no bearer token, returns `undefinerd`. If there is a
|
|
204
|
+
* bearer token and it is a valid access token, returns the token
|
|
205
|
+
* payload. If there was an error, returns it in OAuth form.
|
|
206
|
+
*
|
|
207
|
+
* @param event the SvelteKit request event
|
|
208
|
+
* @returns an object with the following fiekds
|
|
209
|
+
* - `authorized` : `true` or `false`
|
|
210
|
+
* - `tokenPayload` : the token payload if the token is valid
|
|
211
|
+
* - `error` : if the token is not valid
|
|
212
|
+
* - `error_description` : if the token is not valid
|
|
213
|
+
* - `user` set if `sub` is defined in the token, a userStorage has
|
|
214
|
+
* been defined and it matches
|
|
215
|
+
*/
|
|
216
|
+
async authorized(event) {
|
|
217
|
+
try {
|
|
218
|
+
let payload = undefined;
|
|
219
|
+
for (let loc of this.tokenLocations) {
|
|
220
|
+
if (loc == "header") {
|
|
221
|
+
const resp = await this.tokenFromHeader(event);
|
|
222
|
+
if (resp) {
|
|
223
|
+
payload = resp;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
const resp = await this.tokenFromSession(event);
|
|
229
|
+
if (resp) {
|
|
230
|
+
payload = resp;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
let user = undefined;
|
|
236
|
+
if (payload) {
|
|
237
|
+
if (payload.sub && this.userStorage) {
|
|
238
|
+
const userResp = await this.userStorage.getUserByUsername(payload.sub);
|
|
239
|
+
if (userResp)
|
|
240
|
+
user = userResp.user;
|
|
241
|
+
}
|
|
242
|
+
else if (payload.sub) {
|
|
243
|
+
event.locals.user = {
|
|
244
|
+
id: payload.userid ?? payload.sub,
|
|
245
|
+
username: payload.sub,
|
|
246
|
+
state: payload.state ?? "active"
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return { authorized: true, tokenPayload: payload, user: user };
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
return { authorized: false };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
const ce = e;
|
|
257
|
+
CrossauthLogger.logger.debug(j({ err: e }));
|
|
258
|
+
CrossauthLogger.logger.error(j({ cerr: ce }));
|
|
259
|
+
event.locals.authError = "server_error";
|
|
260
|
+
event.locals.authErrorDescription = ce.message;
|
|
261
|
+
return { authorized: false, error: "server_error", error_description: ce.message };
|
|
262
|
+
}
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
async tokenFromHeader(event) {
|
|
266
|
+
const header = event.request.headers.get("authorization");
|
|
267
|
+
if (header && header.startsWith("Bearer ")) {
|
|
268
|
+
const parts = header.split(" ");
|
|
269
|
+
if (parts.length == 2) {
|
|
270
|
+
return await this.accessTokenAuthorized(parts[1]);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
async tokenFromSession(event) {
|
|
276
|
+
if (!this.sessionAdapter)
|
|
277
|
+
throw new CrossauthError(ErrorCode.Configuration, "Cannot get session data if sessions not enabled");
|
|
278
|
+
const oauthData = await this.sessionAdapter.getSessionData(event, this.sessionDataName);
|
|
279
|
+
if (oauthData?.session_token) {
|
|
280
|
+
if (oauthData.expires_at && oauthData.expires_at < Date.now())
|
|
281
|
+
return undefined;
|
|
282
|
+
return await this.accessTokenAuthorized(oauthData.session_token);
|
|
283
|
+
}
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { SvelteKitSessionServer, SvelteKitSessionServerOptions } from './sveltekitsession';
|
|
2
|
-
import { SvelteKitApiKeyServer, SvelteKitApiKeyServerOptions } from './sveltekitapikey';
|
|
3
|
-
import { SvelteKitAuthorizationServer, SvelteKitAuthorizationServerOptions } from './sveltekitoauthserver';
|
|
1
|
+
import { SvelteKitSessionServer, type SvelteKitSessionServerOptions } from './sveltekitsession';
|
|
2
|
+
import { SvelteKitApiKeyServer, type SvelteKitApiKeyServerOptions } from './sveltekitapikey';
|
|
3
|
+
import { SvelteKitAuthorizationServer, type SvelteKitAuthorizationServerOptions } from './sveltekitoauthserver';
|
|
4
4
|
import { UserStorage, KeyStorage, OAuthClientStorage } from '@crossauth/backend';
|
|
5
|
-
import { User } from '@crossauth/common';
|
|
6
|
-
import { Handle, RequestEvent, ResolveOptions
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
5
|
+
import { type User } from '@crossauth/common';
|
|
6
|
+
import { type Handle, type RequestEvent, type ResolveOptions } from '@sveltejs/kit';
|
|
7
|
+
import { type MaybePromise } from './tests/sveltemocks';
|
|
8
|
+
import { SvelteKitOAuthClient } from './sveltekitoauthclient';
|
|
9
|
+
import type { SvelteKitOAuthClientOptions } from './sveltekitoauthclient';
|
|
10
|
+
import { SvelteKitOAuthResourceServer, type SvelteKitOAuthResourceServerOptions } from './sveltekitresserver';
|
|
9
11
|
import { SvelteKitSessionAdapter } from './sveltekitsessionadapter';
|
|
10
|
-
|
|
11
12
|
export interface SvelteKitServerOptions extends SvelteKitSessionServerOptions, SvelteKitApiKeyServerOptions, SvelteKitAuthorizationServerOptions, SvelteKitOAuthClientOptions, SvelteKitOAuthResourceServerOptions {
|
|
12
13
|
/** User can set this to check if the user is an administrator.
|
|
13
14
|
* By default, the admin booloean field in the user object is checked
|
|
@@ -93,7 +94,7 @@ export type Resolver = (event: RequestEvent, opts?: ResolveOptions) => MaybeProm
|
|
|
93
94
|
*
|
|
94
95
|
* ```
|
|
95
96
|
* import { type Handle } from '@sveltejs/kit';
|
|
96
|
-
* import { crossauth } from '
|
|
97
|
+
* import { crossauth } from './server/crossauthsession';
|
|
97
98
|
* import { CrossauthLogger } from '@crossauth/common';
|
|
98
99
|
* export const handle: Handle = crossauth.hooks;
|
|
99
100
|
* ```
|
|
@@ -246,6 +247,7 @@ export declare class SvelteKitServer {
|
|
|
246
247
|
dummyLoad: (event: RequestEvent) => Promise<{
|
|
247
248
|
[key: string]: any;
|
|
248
249
|
}>;
|
|
250
|
+
emptyLoad: (event: RequestEvent) => Promise<{}>;
|
|
249
251
|
/**
|
|
250
252
|
* See class documentation for {@link SvelteKitUserEndpoints}.
|
|
251
253
|
*
|