@functionalcms/svelte-components 2.17.12 → 2.18.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/dist/auth/authenticationHandle.d.ts +2 -0
- package/dist/auth/authenticationHandle.js +69 -0
- package/dist/auth/authenticationProvider.d.ts +7 -0
- package/dist/auth/authenticationProvider.js +98 -0
- package/dist/auth/authorizationHandle.d.ts +3 -0
- package/dist/auth/authorizationHandle.js +18 -0
- package/dist/auth/errorHandle.d.ts +3 -0
- package/dist/auth/errorHandle.js +12 -0
- package/dist/auth/sessionStorage.js +73 -0
- package/dist/auth/sessionstorage.d.ts +5 -0
- package/dist/auth/types.d.ts +24 -0
- package/dist/auth/types.js +10 -0
- package/dist/index-server.d.ts +4 -2
- package/dist/index-server.js +4 -2
- package/package.json +3 -2
- package/dist/auth/auth.d.ts +0 -8
- package/dist/auth/auth.js +0 -92
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {} from '@sveltejs/kit';
|
|
2
|
+
import { createSession, getSession } from './sessionstorage.js';
|
|
3
|
+
import { keycloak } from './authenticationProvider.js';
|
|
4
|
+
const cookieName = `auth_state`;
|
|
5
|
+
const logout = (cookies) => {
|
|
6
|
+
const headers = new Headers();
|
|
7
|
+
const sid = cookies.get(cookieName);
|
|
8
|
+
if (sid) {
|
|
9
|
+
const deleteSessionCookieHeader = `${cookieName}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`;
|
|
10
|
+
const deleteAuthCookieHeader = `auth_session=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`;
|
|
11
|
+
headers.append('Set-Cookie', deleteSessionCookieHeader);
|
|
12
|
+
headers.append('Set-Cookie', deleteAuthCookieHeader);
|
|
13
|
+
}
|
|
14
|
+
return headers;
|
|
15
|
+
};
|
|
16
|
+
const loadUserFromSession = (cookies, locals) => {
|
|
17
|
+
const sid = cookies.get('auth_session');
|
|
18
|
+
const session = getSession(sid);
|
|
19
|
+
if (session) {
|
|
20
|
+
locals.session = session.session;
|
|
21
|
+
locals.token = session.token;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
locals.username = "";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const getHeadersWithCookie = (sessionId, expiresIn) => {
|
|
28
|
+
const cookieHeader = `auth_session=${sessionId}; HttpOnly; Secure; SameSite=strict; Max-Age=${expiresIn}; Path=/`;
|
|
29
|
+
const headers = new Headers();
|
|
30
|
+
headers.append('Set-Cookie', cookieHeader);
|
|
31
|
+
return headers;
|
|
32
|
+
};
|
|
33
|
+
export const authenticationHandle = (issuer, clientId, scope = '', redirectPath = '/') => {
|
|
34
|
+
const provider = keycloak(issuer, clientId, scope, redirectPath);
|
|
35
|
+
return async ({ event, resolve }) => {
|
|
36
|
+
//login user check
|
|
37
|
+
if (event.url.pathname.startsWith('/')) {
|
|
38
|
+
loadUserFromSession(event.cookies, event.locals);
|
|
39
|
+
}
|
|
40
|
+
//logout
|
|
41
|
+
if (event.url.pathname === '/auth/logout' && event.cookies.get(cookieName) === undefined) {
|
|
42
|
+
const headers = logout(event.cookie);
|
|
43
|
+
return new Response('Logging Out...', { status: 303, headers });
|
|
44
|
+
}
|
|
45
|
+
// login
|
|
46
|
+
else if (event.url.pathname === '/auth/login') {
|
|
47
|
+
const authProvider = await provider.getAuthIdentity(event.url.origin);
|
|
48
|
+
return new Response('Redirect', { status: 302, headers: authProvider });
|
|
49
|
+
}
|
|
50
|
+
//validate
|
|
51
|
+
else if (event.url.pathname === "/auth/validate") {
|
|
52
|
+
const token = await provider.getValidation(event);
|
|
53
|
+
const session = await provider.getUser(token);
|
|
54
|
+
if (session !== undefined) {
|
|
55
|
+
session.userId = session.sub;
|
|
56
|
+
event.locals.session = session;
|
|
57
|
+
event.locals.accessToken = token.access_token;
|
|
58
|
+
const sessionId = createSession({ session, token }, token.expires_in);
|
|
59
|
+
const headers = getHeadersWithCookie(sessionId, token.expires_in);
|
|
60
|
+
headers.append('Location', provider.redirectPath);
|
|
61
|
+
return new Response('Redirect', { status: 303, headers });
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
return new Response(`Error in authorizing user`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return await resolve(event);
|
|
68
|
+
};
|
|
69
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Token } from './types.js';
|
|
2
|
+
export declare const keycloak: (issuer: string, clientId: string, scope?: string, redirectPath?: string) => {
|
|
3
|
+
getAuthIdentity: (domain: string) => Promise<any>;
|
|
4
|
+
getValidation: (event: any) => Promise<Token>;
|
|
5
|
+
getUser: (token: Token) => Promise<any>;
|
|
6
|
+
redirectPath: string;
|
|
7
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as o from "oauth4webapi";
|
|
2
|
+
const cookieName = 'auth_state';
|
|
3
|
+
const stateIdGenerator = () => crypto.randomUUID();
|
|
4
|
+
const getKeycloakIdentity = async (issuer, client_id, scope, redirectUrl) => {
|
|
5
|
+
const state = stateIdGenerator();
|
|
6
|
+
const cookieHeader = `${cookieName}=${state}; HttpOnly; Secure; SameSite=strict; Max-Age=3600; Path=/`;
|
|
7
|
+
const code_challenge = await o.calculatePKCECodeChallenge(state);
|
|
8
|
+
const authorizationUrlSearchParams = new URLSearchParams({
|
|
9
|
+
client_id: client_id,
|
|
10
|
+
redirect_uri: redirectUrl,
|
|
11
|
+
response_type: 'code',
|
|
12
|
+
state,
|
|
13
|
+
scope,
|
|
14
|
+
code_challenge
|
|
15
|
+
});
|
|
16
|
+
const authorizationUrl = `${issuer}/protocol/openid-connect/auth?${authorizationUrlSearchParams}`;
|
|
17
|
+
const headers = new Headers();
|
|
18
|
+
headers.append('Set-Cookie', cookieHeader);
|
|
19
|
+
headers.append('Location', authorizationUrl);
|
|
20
|
+
return headers;
|
|
21
|
+
};
|
|
22
|
+
const getKeycloakValidation = async (issuer, client_id, scope, event, redirectUrl) => {
|
|
23
|
+
const storedState = event.cookies.get(cookieName);
|
|
24
|
+
const state = event.url.searchParams.get("state");
|
|
25
|
+
if (!storedState || !state || storedState !== state) {
|
|
26
|
+
throw new Error('State not valid');
|
|
27
|
+
}
|
|
28
|
+
const code = event.url.searchParams.get("code");
|
|
29
|
+
if (!code) {
|
|
30
|
+
throw new Error('Challenge code not valid');
|
|
31
|
+
}
|
|
32
|
+
const code_challenge = await o.calculatePKCECodeChallenge(state);
|
|
33
|
+
const response = await fetch(`${issuer}/protocol/openid-connect/token`, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
body: new URLSearchParams({
|
|
36
|
+
grant_type: "authorization_code",
|
|
37
|
+
client_id: client_id,
|
|
38
|
+
redirect_uri: redirectUrl,
|
|
39
|
+
code_verifier: code_challenge,
|
|
40
|
+
code,
|
|
41
|
+
scope,
|
|
42
|
+
}),
|
|
43
|
+
headers: {
|
|
44
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
45
|
+
Accept: "application/json"
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
console.log('Response was NOT okay');
|
|
50
|
+
throw new Error('Token not validated.');
|
|
51
|
+
}
|
|
52
|
+
const token = await response.json();
|
|
53
|
+
event.cookies.delete(cookieName, { path: '/' });
|
|
54
|
+
return token;
|
|
55
|
+
};
|
|
56
|
+
const getUser = async (issuer, token) => {
|
|
57
|
+
try {
|
|
58
|
+
const accessToken = token.access_token;
|
|
59
|
+
const response = await fetch(`${issuer}/protocol/openid-connect/userinfo`, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: {
|
|
62
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
63
|
+
Accept: "application/json",
|
|
64
|
+
Authorization: `Bearer ${accessToken}`
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
throw new Error('Failed to fetch user information.');
|
|
69
|
+
}
|
|
70
|
+
const data = await response.json();
|
|
71
|
+
return data;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
return new Response(null, {
|
|
75
|
+
status: 400
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
export const keycloak = (issuer, clientId, scope = '', redirectPath = '/') => {
|
|
80
|
+
const extendedScope = `openid profile offline_access ${scope}`;
|
|
81
|
+
const redirectUrl = "/auth/validate";
|
|
82
|
+
return {
|
|
83
|
+
getAuthIdentity: async (domain) => {
|
|
84
|
+
const provider = await getKeycloakIdentity(issuer, clientId, extendedScope, `${domain}${redirectUrl}`);
|
|
85
|
+
return provider;
|
|
86
|
+
},
|
|
87
|
+
getValidation: async (event) => {
|
|
88
|
+
const fullRedirectUrl = `${event.url.origin}${redirectUrl}`;
|
|
89
|
+
const token = await getKeycloakValidation(issuer, clientId, extendedScope, event, fullRedirectUrl);
|
|
90
|
+
return token;
|
|
91
|
+
},
|
|
92
|
+
getUser: async (token) => {
|
|
93
|
+
const user = await getUser(issuer, token);
|
|
94
|
+
return user;
|
|
95
|
+
},
|
|
96
|
+
redirectPath
|
|
97
|
+
};
|
|
98
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { redirect } from '@sveltejs/kit';
|
|
2
|
+
const authorizationHandle = (protectedPaths) => {
|
|
3
|
+
return async ({ event, resolve }) => {
|
|
4
|
+
const path = event.url.pathname;
|
|
5
|
+
if (path.startsWith('/auth/') === false) {
|
|
6
|
+
for (let protectedPath of protectedPaths) {
|
|
7
|
+
if (path.match(protectedPath)) {
|
|
8
|
+
const session = event?.locals?.session;
|
|
9
|
+
if (!session) {
|
|
10
|
+
return new Response('Login user', { status: 303, headers: { location: '/auth/login' } });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return resolve(event);
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export default authorizationHandle;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
//in memory session Store
|
|
2
|
+
const sessionStore = new Map();
|
|
3
|
+
let nextClean = Date.now() + 1000 * 60 * 60; // 1 hour
|
|
4
|
+
function getSid() {
|
|
5
|
+
return crypto.randomUUID();
|
|
6
|
+
}
|
|
7
|
+
//loops over all sessions and deletes the ones that are expired.
|
|
8
|
+
//We call this function every hour when a session is created
|
|
9
|
+
function clean() {
|
|
10
|
+
const now = Date.now();
|
|
11
|
+
for (const [sid, session] of sessionStore) {
|
|
12
|
+
if (session.invalidAt < now) {
|
|
13
|
+
sessionStore.delete(sid);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
// // TODO: delete session from browser storage
|
|
17
|
+
nextClean = Date.now() + 1000 * 60 * 60; // 1 hour
|
|
18
|
+
}
|
|
19
|
+
//calls clean() function if it has been over 1 hour to delete expired sessions
|
|
20
|
+
if (Date.now() > nextClean) {
|
|
21
|
+
//call in setTimeout to not block the server
|
|
22
|
+
setTimeout(() => {
|
|
23
|
+
clean();
|
|
24
|
+
}, 5000);
|
|
25
|
+
}
|
|
26
|
+
//invoked from makeCookieAndSession on login/nativeAuth.ts
|
|
27
|
+
export function createSession(session, maxAge) {
|
|
28
|
+
let sid = '';
|
|
29
|
+
do {
|
|
30
|
+
sid = getSid();
|
|
31
|
+
} while (sessionStore.has(sid));
|
|
32
|
+
sessionStore.set(sid, {
|
|
33
|
+
data: session,
|
|
34
|
+
invalidAt: Date.now() + maxAge
|
|
35
|
+
});
|
|
36
|
+
return sid;
|
|
37
|
+
}
|
|
38
|
+
//gets session from sessionStore, if not in store, adds to it
|
|
39
|
+
export function getSession(sid) {
|
|
40
|
+
//if session present in sessionStore
|
|
41
|
+
if (sessionStore.has(sid)) {
|
|
42
|
+
const cache = sessionStore.get(sid);
|
|
43
|
+
return cache?.data;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
/*
|
|
47
|
+
// TODO/ITERATION: get session from browser on frontend and store in session store
|
|
48
|
+
// const session = sessionStorage.getItem(username)
|
|
49
|
+
// if (session) {
|
|
50
|
+
// sessionStore.set(sid, session);
|
|
51
|
+
// return session;
|
|
52
|
+
// }
|
|
53
|
+
*/
|
|
54
|
+
}
|
|
55
|
+
//if no session, return undefined
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
//deletes session from sessionStore
|
|
59
|
+
export function deleteSession(sid) {
|
|
60
|
+
sessionStore.delete(sid);
|
|
61
|
+
/*
|
|
62
|
+
// // TODO/ITERATION: remove session from browser storage
|
|
63
|
+
// // sessionStorage.removeItem(username);
|
|
64
|
+
// // localStorage.removeItem(username);
|
|
65
|
+
*/
|
|
66
|
+
}
|
|
67
|
+
/*
|
|
68
|
+
// TODO/ITERATION: send back to frontend to store in browser
|
|
69
|
+
// export function setBrowserSession(sid: string, username: string) {
|
|
70
|
+
// sessionStorage.setItem(username, sid);
|
|
71
|
+
// localStorage.setItem(username, sid);
|
|
72
|
+
// }
|
|
73
|
+
*/
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type SessionInfo = {
|
|
2
|
+
username: string;
|
|
3
|
+
session: any;
|
|
4
|
+
};
|
|
5
|
+
export type SessionInfoCache = {
|
|
6
|
+
data: any;
|
|
7
|
+
invalidAt: number;
|
|
8
|
+
};
|
|
9
|
+
export declare class Token {
|
|
10
|
+
access_token: string;
|
|
11
|
+
expires_in: number;
|
|
12
|
+
id_token?: string;
|
|
13
|
+
refresh_expires_in?: number;
|
|
14
|
+
refresh_token?: string;
|
|
15
|
+
scope?: string;
|
|
16
|
+
session_state?: string;
|
|
17
|
+
token_type?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface IProvider {
|
|
20
|
+
redirectPath: string;
|
|
21
|
+
getAuthIdentity(domain: string): Promise<any>;
|
|
22
|
+
getValidation(event: any): Promise<Token>;
|
|
23
|
+
getUser(token: Token): any;
|
|
24
|
+
}
|
package/dist/index-server.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { authenticationHandle } from './auth/authenticationHandle.js';
|
|
2
|
+
import authorizationHandle from './auth/authorizationHandle.js';
|
|
3
|
+
import errorHandler from './auth/errorHandle.js';
|
|
4
|
+
export { authenticationHandle, authorizationHandle, errorHandler };
|
package/dist/index-server.js
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { authenticationHandle } from './auth/authenticationHandle.js';
|
|
2
|
+
import authorizationHandle from './auth/authorizationHandle.js';
|
|
3
|
+
import errorHandler from './auth/errorHandle.js';
|
|
4
|
+
export { authenticationHandle, authorizationHandle, errorHandler };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@functionalcms/svelte-components",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.18.0",
|
|
4
4
|
"watch": {
|
|
5
5
|
"build": {
|
|
6
6
|
"patterns": [
|
|
@@ -70,7 +70,8 @@
|
|
|
70
70
|
"rehype-autolink-headings": "^7.1.0",
|
|
71
71
|
"rehype-slug": "^6.0.0",
|
|
72
72
|
"remark-abbr": "^1.4.2",
|
|
73
|
-
"remark-github": "^12.0.0"
|
|
73
|
+
"remark-github": "^12.0.0",
|
|
74
|
+
"oauth4webapi": "^2.17.0"
|
|
74
75
|
},
|
|
75
76
|
"svelte": "./dist/index.js",
|
|
76
77
|
"types": "./dist/index.d.ts",
|
package/dist/auth/auth.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/// <reference types="@sveltejs/kit" />
|
|
2
|
-
type ProcessSession = (session: any) => any;
|
|
3
|
-
export declare const configureAuthentication: (secret: string, issuer: string, clientId: string, clientSecret: string, customClaims?: string[], sessionTransformation?: ProcessSession) => () => {
|
|
4
|
-
handle: import("@sveltejs/kit").Handle;
|
|
5
|
-
signIn: import("@sveltejs/kit").Action;
|
|
6
|
-
signOut: import("@sveltejs/kit").Action;
|
|
7
|
-
};
|
|
8
|
-
export {};
|
package/dist/auth/auth.js
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { SvelteKitAuth } from "@auth/sveltekit";
|
|
2
|
-
import Keycloak from "@auth/sveltekit/providers/keycloak";
|
|
3
|
-
const defaultScopes = ["openid", "profile", "offline_access", "functional"];
|
|
4
|
-
const defaultTokenCallback = (session) => { };
|
|
5
|
-
function isTokenRefreshRequired(issued_at) {
|
|
6
|
-
const dateNow = Math.floor(new Date().getTime() / 1000);
|
|
7
|
-
const diffrence = Math.abs(new Date(issued_at).getTime() - dateNow);
|
|
8
|
-
const minutes = Math.floor(diffrence / 60);
|
|
9
|
-
return minutes > 25;
|
|
10
|
-
}
|
|
11
|
-
async function refreshToken(issuer, clientId, clientSecret, session) {
|
|
12
|
-
const user = session?.user;
|
|
13
|
-
if (user) {
|
|
14
|
-
try {
|
|
15
|
-
const request = await fetch(`${issuer}/protocol/openid-connect/token`, {
|
|
16
|
-
method: 'POST',
|
|
17
|
-
headers: {
|
|
18
|
-
'content-type': 'application/x-www-form-urlencoded'
|
|
19
|
-
},
|
|
20
|
-
body: new URLSearchParams({
|
|
21
|
-
grant_type: 'client_credentials',
|
|
22
|
-
client_id: clientId,
|
|
23
|
-
client_secret: clientSecret // ${ISSUER}/api/v2/
|
|
24
|
-
})
|
|
25
|
-
});
|
|
26
|
-
return await request.json();
|
|
27
|
-
}
|
|
28
|
-
catch (error) {
|
|
29
|
-
console.error(`Error while updating token data: ${error?.message}. Reusing existing tokens!`);
|
|
30
|
-
return {
|
|
31
|
-
accessToken: user.access_token,
|
|
32
|
-
expires_at: user.expires_at
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
return {
|
|
38
|
-
message: 'User is not authorized to rotate access-token'
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
export const configureAuthentication = (secret, issuer, clientId, clientSecret, customClaims = [], sessionTransformation = defaultTokenCallback) => {
|
|
43
|
-
const scope = defaultScopes.concat(customClaims).join(' ');
|
|
44
|
-
return () => SvelteKitAuth({
|
|
45
|
-
providers: [Keycloak({
|
|
46
|
-
issuer: issuer,
|
|
47
|
-
clientId: clientId,
|
|
48
|
-
clientSecret: clientSecret,
|
|
49
|
-
authorization: {
|
|
50
|
-
params: {
|
|
51
|
-
scope: scope
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
})],
|
|
55
|
-
secret: secret,
|
|
56
|
-
trustHost: true,
|
|
57
|
-
callbacks: {
|
|
58
|
-
async jwt({ token, user, account, profile, session }) {
|
|
59
|
-
if (account != null) {
|
|
60
|
-
token.account = account;
|
|
61
|
-
}
|
|
62
|
-
if (user != null) {
|
|
63
|
-
token.user = user;
|
|
64
|
-
}
|
|
65
|
-
if (profile != null) {
|
|
66
|
-
token.profile = profile;
|
|
67
|
-
}
|
|
68
|
-
if (token && isTokenRefreshRequired(token.expires_at)) {
|
|
69
|
-
const updatedToken = await refreshToken(issuer, clientId, clientSecret, session);
|
|
70
|
-
if (updatedToken.access_token) {
|
|
71
|
-
token = {
|
|
72
|
-
...token,
|
|
73
|
-
...updatedToken
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return token;
|
|
78
|
-
},
|
|
79
|
-
async session({ session, token, user }) {
|
|
80
|
-
session.accessToken = token.account.access_token;
|
|
81
|
-
if (session.user != null) {
|
|
82
|
-
session = { ...session, ...user };
|
|
83
|
-
}
|
|
84
|
-
if (token.profile != null) {
|
|
85
|
-
session.profile = token.profile;
|
|
86
|
-
}
|
|
87
|
-
sessionTransformation(session);
|
|
88
|
-
return session;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
};
|