@crossauth/sveltekit 1.1.0 → 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.
Files changed (46) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +16 -6181
  3. package/dist/sveltekitadminclientendpoints.d.ts +13 -12
  4. package/dist/sveltekitadminclientendpoints.js +187 -0
  5. package/dist/sveltekitadminendpoints.d.ts +5 -4
  6. package/dist/sveltekitadminendpoints.js +766 -0
  7. package/dist/sveltekitapikey.d.ts +4 -4
  8. package/dist/sveltekitapikey.js +81 -0
  9. package/dist/sveltekitoauthclient.d.ts +6 -5
  10. package/dist/sveltekitoauthclient.js +2309 -0
  11. package/dist/sveltekitoauthserver.d.ts +4 -4
  12. package/dist/sveltekitoauthserver.js +1350 -0
  13. package/dist/sveltekitresserver.d.ts +6 -5
  14. package/dist/sveltekitresserver.js +286 -0
  15. package/dist/sveltekitserver.d.ts +11 -10
  16. package/dist/sveltekitserver.js +393 -0
  17. package/dist/sveltekitsession.d.ts +5 -5
  18. package/dist/sveltekitsession.js +1112 -0
  19. package/dist/sveltekitsessionadapter.d.ts +2 -3
  20. package/dist/sveltekitsessionadapter.js +2 -0
  21. package/dist/sveltekitsharedclientendpoints.d.ts +7 -6
  22. package/dist/sveltekitsharedclientendpoints.js +630 -0
  23. package/dist/sveltekituserclientendpoints.d.ts +13 -12
  24. package/dist/sveltekituserclientendpoints.js +270 -0
  25. package/dist/sveltekituserendpoints.d.ts +6 -5
  26. package/dist/sveltekituserendpoints.js +1813 -0
  27. package/dist/tests/sveltekitadminclientendpoints.test.js +330 -0
  28. package/dist/tests/sveltekitadminendpoints.test.js +242 -0
  29. package/dist/tests/sveltekitapikeyserver.test.js +44 -0
  30. package/dist/tests/sveltekitoauthclient.test.d.ts +5 -5
  31. package/dist/tests/sveltekitoauthclient.test.js +1016 -0
  32. package/dist/tests/sveltekitoauthresserver.test.d.ts +4 -4
  33. package/dist/tests/sveltekitoauthresserver.test.js +185 -0
  34. package/dist/tests/sveltekitoauthserver.test.js +673 -0
  35. package/dist/tests/sveltekituserclientendpoints.test.js +244 -0
  36. package/dist/tests/sveltekituserendpoints.test.js +152 -0
  37. package/dist/tests/sveltemock.test.js +36 -0
  38. package/dist/tests/sveltemocks.d.ts +2 -3
  39. package/dist/tests/sveltemocks.js +114 -0
  40. package/dist/tests/sveltesessionhooks.test.js +224 -0
  41. package/dist/tests/testshared.d.ts +8 -8
  42. package/dist/tests/testshared.js +344 -0
  43. package/dist/utils.d.ts +1 -2
  44. package/dist/utils.js +123 -0
  45. package/package.json +6 -4
  46. package/dist/index.cjs +0 -1
@@ -1,9 +1,10 @@
1
- import { RequestEvent } from '@sveltejs/kit';
2
- import { User } from '@crossauth/common';
3
- import { OAuthResourceServer, UserStorage, OAuthResourceServerOptions, OAuthTokenConsumer } from '@crossauth/backend';
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
- import { MaybePromise } from './tests/sveltemocks';
6
-
7
+ import { type MaybePromise } from './tests/sveltemocks';
7
8
  /**
8
9
  * Options for {@link SvelteKitOAuthResourceServer}
9
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,14 +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 } from '@sveltejs/kit';
7
- import { MaybePromise } from './tests/sveltemocks';
8
- import { SvelteKitOAuthClient, SvelteKitOAuthClientOptions } from './sveltekitoauthclient';
9
- import { SvelteKitOAuthResourceServer, SvelteKitOAuthResourceServerOptions } from './sveltekitresserver';
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';
10
11
  import { SvelteKitSessionAdapter } from './sveltekitsessionadapter';
11
-
12
12
  export interface SvelteKitServerOptions extends SvelteKitSessionServerOptions, SvelteKitApiKeyServerOptions, SvelteKitAuthorizationServerOptions, SvelteKitOAuthClientOptions, SvelteKitOAuthResourceServerOptions {
13
13
  /** User can set this to check if the user is an administrator.
14
14
  * By default, the admin booloean field in the user object is checked
@@ -94,7 +94,7 @@ export type Resolver = (event: RequestEvent, opts?: ResolveOptions) => MaybeProm
94
94
  *
95
95
  * ```
96
96
  * import { type Handle } from '@sveltejs/kit';
97
- * import { crossauth } from '$lib/server/crossauthsession';
97
+ * import { crossauth } from './server/crossauthsession';
98
98
  * import { CrossauthLogger } from '@crossauth/common';
99
99
  * export const handle: Handle = crossauth.hooks;
100
100
  * ```
@@ -247,6 +247,7 @@ export declare class SvelteKitServer {
247
247
  dummyLoad: (event: RequestEvent) => Promise<{
248
248
  [key: string]: any;
249
249
  }>;
250
+ emptyLoad: (event: RequestEvent) => Promise<{}>;
250
251
  /**
251
252
  * See class documentation for {@link SvelteKitUserEndpoints}.
252
253
  *