@opensas/oidc-sveltekit-adapter 0.0.0-stage → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COPYING +674 -0
- package/COPYING.LESSER +165 -0
- package/README.md +222 -2
- package/dist/index.d.ts +7 -0
- package/dist/index.js +10 -0
- package/dist/oidc/domain-user.d.ts +7 -0
- package/dist/oidc/domain-user.js +35 -0
- package/dist/oidc/errors.d.ts +14 -0
- package/dist/oidc/errors.js +27 -0
- package/dist/oidc/permissions.d.ts +3 -0
- package/dist/oidc/permissions.js +32 -0
- package/dist/oidc/provider.d.ts +33 -0
- package/dist/oidc/provider.js +134 -0
- package/dist/oidc/request.d.ts +4 -0
- package/dist/oidc/request.js +23 -0
- package/dist/oidc/types.d.ts +1 -0
- package/dist/oidc/types.js +1 -0
- package/dist/oidc/user.d.ts +10 -0
- package/dist/oidc/user.js +27 -0
- package/dist/sveltekit-handlers.d.ts +18 -0
- package/dist/sveltekit-handlers.js +122 -0
- package/package.json +118 -3
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import * as client from 'openid-client';
|
|
2
|
+
import { OidcError } from "./errors.js";
|
|
3
|
+
import { safeReturnTo } from "./request.js";
|
|
4
|
+
// Keycloak rechaza los scopes que no conoce (invalid_scope): allatclaims, el de ADFS, no va por defecto
|
|
5
|
+
export const DEFAULT_SCOPE = 'openid profile email';
|
|
6
|
+
// /login?returnTo=/pagina: a dónde volver después del login
|
|
7
|
+
export const RETURN_TO_PARAM = 'returnTo';
|
|
8
|
+
const JWT_PARTS = 3;
|
|
9
|
+
// Errores de openid-client (oauth4webapi) que dicen que el id_token no es válido: firma (llega
|
|
10
|
+
// como respuesta inválida), iss, aud, exp, nonce, o la clave del JWKS.
|
|
11
|
+
const INVALID_ID_TOKEN_ERROR_CODES = new Set([
|
|
12
|
+
'OAUTH_INVALID_RESPONSE',
|
|
13
|
+
'OAUTH_JWT_CLAIM_COMPARISON_FAILED',
|
|
14
|
+
'OAUTH_JWT_TIMESTAMP_CHECK_FAILED',
|
|
15
|
+
'OAUTH_KEY_SELECTION_FAILED',
|
|
16
|
+
]);
|
|
17
|
+
// Discovery la primera vez que se usa; si falla, no se cachea el error y el próximo pedido reintenta.
|
|
18
|
+
// Así la app levanta aunque el proveedor esté caído.
|
|
19
|
+
export function createOidcProvider(options) {
|
|
20
|
+
const scope = options.scope ?? DEFAULT_SCOPE;
|
|
21
|
+
let configuration;
|
|
22
|
+
// Una sola promesa compartida: los pedidos que llegan mientras se resuelve esperan la misma
|
|
23
|
+
function discover() {
|
|
24
|
+
configuration ??= runDiscovery();
|
|
25
|
+
return configuration;
|
|
26
|
+
}
|
|
27
|
+
async function runDiscovery() {
|
|
28
|
+
try {
|
|
29
|
+
return await client.discovery(new URL(options.issuer), options.clientId, options.clientSecret, options.clientSecret ? undefined : client.None(), options.allowInsecureRequests ? { execute: [client.allowInsecureRequests] } : undefined);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
configuration = undefined;
|
|
33
|
+
throw new OidcError('DISCOVERY', `no se pudo leer el discovery de ${options.issuer}`, {
|
|
34
|
+
cause: err,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
async buildLoginUrl(loginUrl) {
|
|
40
|
+
const config = await discover();
|
|
41
|
+
const codeVerifier = client.randomPKCECodeVerifier();
|
|
42
|
+
const state = client.randomState();
|
|
43
|
+
const nonce = client.randomNonce();
|
|
44
|
+
const providerUrl = client.buildAuthorizationUrl(config, {
|
|
45
|
+
redirect_uri: new URL(options.callbackPath, loginUrl.origin).href,
|
|
46
|
+
scope,
|
|
47
|
+
code_challenge: await client.calculatePKCECodeChallenge(codeVerifier),
|
|
48
|
+
code_challenge_method: 'S256',
|
|
49
|
+
state,
|
|
50
|
+
nonce,
|
|
51
|
+
});
|
|
52
|
+
const returnTo = safeReturnTo(loginUrl.searchParams.get(RETURN_TO_PARAM));
|
|
53
|
+
return { providerUrl, pendingLogin: { codeVerifier, state, nonce, returnTo } };
|
|
54
|
+
},
|
|
55
|
+
async exchangeCode(callbackUrl, pendingLogin) {
|
|
56
|
+
// Antes de openid-client: sus errores no distinguen estos casos de un id_token inválido
|
|
57
|
+
const providerError = callbackUrl.searchParams.get('error');
|
|
58
|
+
if (providerError)
|
|
59
|
+
throw new OidcError('PROVIDER_ERROR', 'el proveedor devolvió un error en el callback', {
|
|
60
|
+
providerError,
|
|
61
|
+
providerErrorDescription: callbackUrl.searchParams.get('error_description') ?? undefined,
|
|
62
|
+
});
|
|
63
|
+
if (!pendingLogin)
|
|
64
|
+
throw new OidcError('NO_LOGIN_IN_PROGRESS', 'no hay un login en curso');
|
|
65
|
+
if (callbackUrl.searchParams.get('state') !== pendingLogin.state)
|
|
66
|
+
throw new OidcError('STATE_MISMATCH', 'el state del callback no coincide');
|
|
67
|
+
const config = await discover();
|
|
68
|
+
let response;
|
|
69
|
+
try {
|
|
70
|
+
response = await client.authorizationCodeGrant(config, callbackUrl, {
|
|
71
|
+
pkceCodeVerifier: pendingLogin.codeVerifier,
|
|
72
|
+
expectedState: pendingLogin.state,
|
|
73
|
+
expectedNonce: pendingLogin.nonce,
|
|
74
|
+
idTokenExpected: true,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
throw toOidcError(err);
|
|
79
|
+
}
|
|
80
|
+
const idTokenClaims = response.claims();
|
|
81
|
+
if (!response.id_token || !idTokenClaims)
|
|
82
|
+
throw new OidcError('INVALID_ID_TOKEN', 'el proveedor no devolvió un id_token');
|
|
83
|
+
return {
|
|
84
|
+
idToken: response.id_token,
|
|
85
|
+
accessToken: response.access_token,
|
|
86
|
+
refreshToken: response.refresh_token,
|
|
87
|
+
idTokenClaims: { ...idTokenClaims },
|
|
88
|
+
accessTokenClaims: decodeJwtPayload(response.access_token),
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
async buildLogoutUrl(idToken, postLogoutRedirectUri) {
|
|
92
|
+
const config = await discover();
|
|
93
|
+
return config.serverMetadata().end_session_endpoint
|
|
94
|
+
? client.buildEndSessionUrl(config, {
|
|
95
|
+
...(idToken && { id_token_hint: idToken }),
|
|
96
|
+
post_logout_redirect_uri: postLogoutRedirectUri,
|
|
97
|
+
})
|
|
98
|
+
: undefined;
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function toOidcError(err) {
|
|
103
|
+
if (err instanceof client.AuthorizationResponseError)
|
|
104
|
+
return new OidcError('PROVIDER_ERROR', 'el proveedor devolvió un error en el callback', {
|
|
105
|
+
providerError: err.error,
|
|
106
|
+
providerErrorDescription: err.error_description,
|
|
107
|
+
cause: err,
|
|
108
|
+
});
|
|
109
|
+
if (err instanceof client.ResponseBodyError)
|
|
110
|
+
return new OidcError('TOKEN_EXCHANGE', 'el proveedor rechazó el canje del code', {
|
|
111
|
+
providerError: err.error,
|
|
112
|
+
providerErrorDescription: err.error_description,
|
|
113
|
+
cause: err,
|
|
114
|
+
});
|
|
115
|
+
const code = err instanceof Error && 'code' in err ? err.code : undefined;
|
|
116
|
+
if (typeof code === 'string' && INVALID_ID_TOKEN_ERROR_CODES.has(code))
|
|
117
|
+
return new OidcError('INVALID_ID_TOKEN', 'el id_token no es válido', { cause: err });
|
|
118
|
+
return new OidcError('TOKEN_EXCHANGE', 'falló el canje del code', { cause: err });
|
|
119
|
+
}
|
|
120
|
+
// Payload de un JWT sin verificar la firma, o undefined si no es un JWT
|
|
121
|
+
function decodeJwtPayload(jwt) {
|
|
122
|
+
const parts = jwt.split('.');
|
|
123
|
+
if (parts.length !== JWT_PARTS)
|
|
124
|
+
return undefined;
|
|
125
|
+
try {
|
|
126
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
127
|
+
return typeof payload === 'object' && payload !== null && !Array.isArray(payload)
|
|
128
|
+
? payload
|
|
129
|
+
: undefined;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const DEFAULT_RETURN_TO = "/";
|
|
2
|
+
export declare function isSameOrigin(request: Request, origin: string): boolean;
|
|
3
|
+
export declare function isNavigation(request: Request): boolean;
|
|
4
|
+
export declare function safeReturnTo(value: string | null | undefined): string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const DEFAULT_RETURN_TO = '/';
|
|
2
|
+
const HTML_MEDIA_TYPE = 'text/html';
|
|
3
|
+
// La cabecera Origin del pedido es la de la app. Sin Origin, no: el logout lo hace un form del
|
|
4
|
+
// propio sitio, y los navegadores mandan Origin en todo POST.
|
|
5
|
+
export function isSameOrigin(request, origin) {
|
|
6
|
+
return request.headers.get('origin') === origin;
|
|
7
|
+
}
|
|
8
|
+
// Una navegación del navegador (GET que acepta text/html), no un fetch de la app. Las navegaciones
|
|
9
|
+
// del lado del cliente de un framework (SvelteKit: event.isDataRequest) llegan como fetch: las
|
|
10
|
+
// tiene que sumar la app.
|
|
11
|
+
export function isNavigation(request) {
|
|
12
|
+
return (request.method === 'GET' && Boolean(request.headers.get('accept')?.includes(HTML_MEDIA_TYPE)));
|
|
13
|
+
}
|
|
14
|
+
// Solo rutas propias: empieza con una sola '/'. '//otro-sitio' y '/\otro-sitio' los navegadores los
|
|
15
|
+
// toman como una URL de otro origen (open redirect), igual que 'https://otro-sitio'.
|
|
16
|
+
export function safeReturnTo(value) {
|
|
17
|
+
if (!value?.startsWith('/'))
|
|
18
|
+
return DEFAULT_RETURN_TO;
|
|
19
|
+
const second = value.charAt(1);
|
|
20
|
+
if (second === '/' || second === '\\')
|
|
21
|
+
return DEFAULT_RETURN_TO;
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type Claims = Record<string, unknown>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type DomainUserOptions } from './domain-user.ts';
|
|
2
|
+
import { type Permissions } from './permissions.ts';
|
|
3
|
+
import type { Tokens } from './provider.ts';
|
|
4
|
+
export type OidcUser = {
|
|
5
|
+
domainUser: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
groups: string[];
|
|
8
|
+
permissions: Permissions;
|
|
9
|
+
};
|
|
10
|
+
export declare function getUser(tokens: Pick<Tokens, 'idTokenClaims' | 'accessTokenClaims'>, options?: DomainUserOptions): OidcUser | undefined;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { getDomainUser } from "./domain-user.js";
|
|
2
|
+
import { getPermissions } from "./permissions.js";
|
|
3
|
+
// El usuario a partir de los tokens de exchangeCode, o undefined si no hay usuario de red. El
|
|
4
|
+
// nombre, primero del id_token (validado). Grupos y roles, del access token: Keycloak los manda
|
|
5
|
+
// solo ahí. Los roles son los del client al que se le emitió el token (claim azp). Sin access
|
|
6
|
+
// token JWT, sin grupos ni permisos. Guardarlo (la sesión) es cosa de la app.
|
|
7
|
+
export function getUser(tokens, options) {
|
|
8
|
+
const { idTokenClaims, accessTokenClaims } = tokens;
|
|
9
|
+
const domainUser = getDomainUser(idTokenClaims, accessTokenClaims, options);
|
|
10
|
+
if (!domainUser)
|
|
11
|
+
return undefined;
|
|
12
|
+
const clientId = stringClaim(accessTokenClaims, 'azp');
|
|
13
|
+
return {
|
|
14
|
+
domainUser,
|
|
15
|
+
name: stringClaim(idTokenClaims, 'name') ?? stringClaim(accessTokenClaims, 'name'),
|
|
16
|
+
groups: stringArrayClaim(accessTokenClaims, 'groups'),
|
|
17
|
+
permissions: clientId ? getPermissions(accessTokenClaims, clientId) : {},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function stringClaim(claims, name) {
|
|
21
|
+
const value = claims?.[name];
|
|
22
|
+
return typeof value === 'string' && value !== '' ? value : undefined;
|
|
23
|
+
}
|
|
24
|
+
function stringArrayClaim(claims, name) {
|
|
25
|
+
const value = claims?.[name];
|
|
26
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
|
|
27
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type RequestEvent, type RequestHandler } from '@sveltejs/kit';
|
|
2
|
+
import { OidcError } from './oidc/errors.ts';
|
|
3
|
+
import { type OidcProvider, type OidcProviderOptions, type Tokens } from './oidc/provider.ts';
|
|
4
|
+
export type OidcSvelteKitOptions = Omit<OidcProviderOptions, 'callbackPath'> & {
|
|
5
|
+
basePath: string;
|
|
6
|
+
cookiePrefix: string;
|
|
7
|
+
sessionMaxAge?: number;
|
|
8
|
+
postLogoutPath?: string;
|
|
9
|
+
onLogin: (tokens: Tokens, event: RequestEvent) => void | false | Promise<void | false>;
|
|
10
|
+
onLogout?: (event: RequestEvent) => void | Promise<void>;
|
|
11
|
+
onLoginError?: (error: OidcError, event: RequestEvent) => Response | Promise<Response>;
|
|
12
|
+
};
|
|
13
|
+
export type OidcSvelteKitHandlers = {
|
|
14
|
+
handler: RequestHandler;
|
|
15
|
+
loginRedirect: (event: RequestEvent) => string | undefined;
|
|
16
|
+
provider: OidcProvider;
|
|
17
|
+
};
|
|
18
|
+
export declare function createOidcSvelteKitHandlers(options: OidcSvelteKitOptions): OidcSvelteKitHandlers;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// La capa SvelteKit (etapa 2): hace con el provider lo que en la etapa 1 arma a mano la app (ver la
|
|
2
|
+
// app de prueba, src/routes/api/auth/): las rutas, las cookies, los redirects y la página del login
|
|
3
|
+
// fallido. La app solo arma su sesión (onLogin) y la borra (onLogout).
|
|
4
|
+
import { error, redirect } from '@sveltejs/kit';
|
|
5
|
+
import { OidcError } from "./oidc/errors.js";
|
|
6
|
+
import { createOidcProvider, RETURN_TO_PARAM, } from "./oidc/provider.js";
|
|
7
|
+
import { isNavigation, isSameOrigin } from "./oidc/request.js";
|
|
8
|
+
const LOGIN_PATH = '/login';
|
|
9
|
+
const CALLBACK_PATH = '/callback';
|
|
10
|
+
const LOGOUT_PATH = '/logout';
|
|
11
|
+
const DEFAULT_POST_LOGOUT_PATH = '/';
|
|
12
|
+
const SECONDS_PER_MINUTE = 60;
|
|
13
|
+
const LOGIN_MAX_AGE = 10 * SECONDS_PER_MINUTE;
|
|
14
|
+
const SEE_OTHER = 303;
|
|
15
|
+
const UNAUTHORIZED = 401;
|
|
16
|
+
const FORBIDDEN = 403;
|
|
17
|
+
const NOT_FOUND = 404;
|
|
18
|
+
const METHOD_NOT_ALLOWED = 405;
|
|
19
|
+
export function createOidcSvelteKitHandlers(options) {
|
|
20
|
+
const { basePath, cookiePrefix, sessionMaxAge, postLogoutPath = DEFAULT_POST_LOGOUT_PATH, onLogin, onLogout, onLoginError = (err) => defaultLoginError(err, basePath), ...providerOptions } = options;
|
|
21
|
+
const provider = createOidcProvider({
|
|
22
|
+
...providerOptions,
|
|
23
|
+
callbackPath: `${basePath}${CALLBACK_PATH}`,
|
|
24
|
+
});
|
|
25
|
+
// Kit solo redirige a otro origen si está en la lista: login y logout van al proveedor
|
|
26
|
+
const external = [new URL(options.issuer).origin];
|
|
27
|
+
const loginCookie = cookie(`${cookiePrefix}_login`, `${basePath}${CALLBACK_PATH}`, LOGIN_MAX_AGE);
|
|
28
|
+
const logoutCookie = cookie(`${cookiePrefix}_logout`, `${basePath}${LOGOUT_PATH}`, sessionMaxAge);
|
|
29
|
+
const login = async ({ url, cookies }) => {
|
|
30
|
+
const { providerUrl, pendingLogin } = await provider.buildLoginUrl(url);
|
|
31
|
+
cookies.set(loginCookie.name, JSON.stringify(pendingLogin), loginCookie.options);
|
|
32
|
+
redirect(SEE_OTHER, providerUrl.href, { external });
|
|
33
|
+
};
|
|
34
|
+
const callback = async (event) => {
|
|
35
|
+
const { url, cookies } = event;
|
|
36
|
+
const pendingLogin = parsePendingLogin(cookies.get(loginCookie.name));
|
|
37
|
+
cookies.delete(loginCookie.name, loginCookie.options);
|
|
38
|
+
try {
|
|
39
|
+
const tokens = await provider.exchangeCode(url, pendingLogin);
|
|
40
|
+
if ((await onLogin(tokens, event)) === false)
|
|
41
|
+
throw new OidcError('LOGIN_REJECTED', 'la app no aceptó el login');
|
|
42
|
+
cookies.set(logoutCookie.name, tokens.idToken, logoutCookie.options);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
if (!(err instanceof OidcError))
|
|
46
|
+
throw err;
|
|
47
|
+
return await onLoginError(err, event);
|
|
48
|
+
}
|
|
49
|
+
// pendingLogin está: si no, exchangeCode lanzó NO_LOGIN_IN_PROGRESS
|
|
50
|
+
redirect(SEE_OTHER, pendingLogin.returnTo);
|
|
51
|
+
};
|
|
52
|
+
const logout = async (event) => {
|
|
53
|
+
const { request, url, cookies } = event;
|
|
54
|
+
// Propio aunque SvelteKit tenga su chequeo de CSRF: no corre en dev y una app lo puede desactivar
|
|
55
|
+
if (!isSameOrigin(request, url.origin))
|
|
56
|
+
error(FORBIDDEN, 'Origin distinto del de la app');
|
|
57
|
+
const idToken = cookies.get(logoutCookie.name);
|
|
58
|
+
cookies.delete(logoutCookie.name, logoutCookie.options);
|
|
59
|
+
await onLogout?.(event);
|
|
60
|
+
const home = new URL(postLogoutPath, url.origin);
|
|
61
|
+
const providerLogout = await provider.buildLogoutUrl(idToken, home.href);
|
|
62
|
+
redirect(SEE_OTHER, (providerLogout ?? home).href, { external });
|
|
63
|
+
};
|
|
64
|
+
const routes = {
|
|
65
|
+
[LOGIN_PATH]: { method: 'GET', handle: login },
|
|
66
|
+
[CALLBACK_PATH]: { method: 'GET', handle: callback },
|
|
67
|
+
[LOGOUT_PATH]: { method: 'POST', handle: logout },
|
|
68
|
+
};
|
|
69
|
+
const handler = (event) => {
|
|
70
|
+
const route = routes[event.url.pathname.slice(basePath.length)];
|
|
71
|
+
if (!route)
|
|
72
|
+
error(NOT_FOUND);
|
|
73
|
+
if (event.request.method !== route.method)
|
|
74
|
+
return new Response(undefined, {
|
|
75
|
+
status: METHOD_NOT_ALLOWED,
|
|
76
|
+
headers: { allow: route.method },
|
|
77
|
+
});
|
|
78
|
+
return route.handle(event);
|
|
79
|
+
};
|
|
80
|
+
const loginRedirect = ({ request, url, isDataRequest }) => {
|
|
81
|
+
if (!isDataRequest && !isNavigation(request))
|
|
82
|
+
return undefined;
|
|
83
|
+
if (url.pathname === basePath || url.pathname.startsWith(`${basePath}/`))
|
|
84
|
+
return undefined;
|
|
85
|
+
const params = new URLSearchParams({ [RETURN_TO_PARAM]: `${url.pathname}${url.search}` });
|
|
86
|
+
return `${basePath}${LOGIN_PATH}?${params}`;
|
|
87
|
+
};
|
|
88
|
+
return { handler, loginRedirect, provider };
|
|
89
|
+
}
|
|
90
|
+
// HttpOnly y SameSite=Lax; Secure lo pone SvelteKit (salvo en http://localhost). Las mismas opciones
|
|
91
|
+
// para ponerla y para borrarla.
|
|
92
|
+
function cookie(name, path, maxAge) {
|
|
93
|
+
return { name, options: { path, httpOnly: true, sameSite: 'lax', maxAge } };
|
|
94
|
+
}
|
|
95
|
+
// Sin cookie, o rota: undefined (exchangeCode da NO_LOGIN_IN_PROGRESS)
|
|
96
|
+
function parsePendingLogin(value) {
|
|
97
|
+
if (!value)
|
|
98
|
+
return undefined;
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(value);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Los errores de un +server.ts no pasan por +error.svelte: la página la arma el componente
|
|
107
|
+
function defaultLoginError(err, basePath) {
|
|
108
|
+
const { code, message, providerError } = err;
|
|
109
|
+
console.warn(`[oidc] ${code}: ${message}${providerError ? ` (${providerError})` : ''}`);
|
|
110
|
+
const html = `<!doctype html>
|
|
111
|
+
<html lang="es">
|
|
112
|
+
<meta charset="utf-8">
|
|
113
|
+
<title>No se pudo entrar</title>
|
|
114
|
+
<h1>No se pudo entrar</h1>
|
|
115
|
+
<p>El login falló (${code}); el detalle está en el log de la app.</p>
|
|
116
|
+
<p><a href="${basePath}${LOGIN_PATH}">Reintentar</a></p>
|
|
117
|
+
</html>`;
|
|
118
|
+
return new Response(html, {
|
|
119
|
+
status: UNAUTHORIZED,
|
|
120
|
+
headers: { 'content-type': 'text/html; charset=utf-8' },
|
|
121
|
+
});
|
|
122
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,121 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opensas/oidc-sveltekit-adapter",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"license": "LGPL-3.0-or-later",
|
|
5
|
+
"description": "Login por OpenID Connect (ADFS, Keycloak) para apps SvelteKit, con una capa genérica para cualquier app Node",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=24.0.0 <25"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"COPYING",
|
|
15
|
+
"COPYING.LESSER",
|
|
16
|
+
"!dist/**/*.test.*",
|
|
17
|
+
"!dist/**/*.spec.*",
|
|
18
|
+
"!dist/**/fixtures.*"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": [
|
|
21
|
+
"**/*.css"
|
|
22
|
+
],
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./sveltekit": {
|
|
31
|
+
"types": "./dist/sveltekit-handlers.d.ts",
|
|
32
|
+
"default": "./dist/sveltekit-handlers.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"svelte",
|
|
37
|
+
"sveltekit",
|
|
38
|
+
"oidc",
|
|
39
|
+
"openid-connect",
|
|
40
|
+
"adfs",
|
|
41
|
+
"keycloak"
|
|
42
|
+
],
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://gitlab.com/trabajo-it/comun/oidc-sveltekit-adapter.git"
|
|
46
|
+
},
|
|
47
|
+
"imports": {
|
|
48
|
+
"#lib": "./src/lib/index.js",
|
|
49
|
+
"#lib/*": "./src/lib/*",
|
|
50
|
+
"#server/*": "./src/server/*",
|
|
51
|
+
"#e2e/*": "./e2e/*.ts"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@sveltejs/kit": "^3.0.0"
|
|
55
|
+
},
|
|
56
|
+
"peerDependenciesMeta": {
|
|
57
|
+
"@sveltejs/kit": {
|
|
58
|
+
"optional": true
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@eslint/js": "^10.0.1",
|
|
63
|
+
"@playwright/test": "^1.64.1",
|
|
64
|
+
"@sveltejs/adapter-node": "^6.0.1",
|
|
65
|
+
"@sveltejs/kit": "^3.1.0",
|
|
66
|
+
"@sveltejs/package": "^3.0.0",
|
|
67
|
+
"@sveltejs/vite-plugin-svelte": "^7.3.1",
|
|
68
|
+
"@types/node": "^24.19.2",
|
|
69
|
+
"eslint": "^10.13.0",
|
|
70
|
+
"eslint-config-prettier": "^10.1.8",
|
|
71
|
+
"eslint-plugin-check-file": "^3.3.2",
|
|
72
|
+
"eslint-plugin-simple-import-sort": "^14.0.0",
|
|
73
|
+
"eslint-plugin-svelte": "^3.23.1",
|
|
74
|
+
"eslint-plugin-unicorn": "^71.1.0",
|
|
75
|
+
"globals": "^17.13.0",
|
|
76
|
+
"prettier": "^3.9.10",
|
|
77
|
+
"prettier-plugin-svelte": "^4.1.0",
|
|
78
|
+
"publint": "^0.3.25",
|
|
79
|
+
"shiki": "^4.6.0",
|
|
80
|
+
"svelte": "^5.57.2",
|
|
81
|
+
"svelte-check": "^4.7.7",
|
|
82
|
+
"typescript": "^6.0.3",
|
|
83
|
+
"typescript-eslint": "^8.72.0",
|
|
84
|
+
"vite": "^8.3.5",
|
|
85
|
+
"vitest": "^4.1.12"
|
|
86
|
+
},
|
|
87
|
+
"dependencies": {
|
|
88
|
+
"openid-client": "^6.8.9"
|
|
89
|
+
},
|
|
90
|
+
"scripts": {
|
|
91
|
+
"dev": "NODE_EXTRA_CA_CERTS=docker/keycloak/certs/localhost.crt vite dev",
|
|
92
|
+
"build": "vite build && pnpm prepack",
|
|
93
|
+
"preview": "NODE_EXTRA_CA_CERTS=docker/keycloak/certs/localhost.crt vite preview",
|
|
94
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
95
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
96
|
+
"format": "prettier --write --cache --log-level warn .",
|
|
97
|
+
"lint": "eslint . --cache",
|
|
98
|
+
"lint:clean": "node -e \"for (const p of ['.eslintcache', 'node_modules/.cache/prettier']) require('node:fs').rmSync(p, { recursive: true, force: true })\"",
|
|
99
|
+
"verify": "eslint . --fix --cache && pnpm format && pnpm check",
|
|
100
|
+
"up:safe": "pnpm --config.minimumReleaseAge=1440 up -i",
|
|
101
|
+
"sv:sync-deps": "node scripts/sv-sync-deps.ts",
|
|
102
|
+
"keycloak:cert": "sh scripts/keycloak-cert.sh",
|
|
103
|
+
"keycloak:start": "pnpm keycloak:cert && docker compose -f docker/keycloak/docker-compose.yaml up -d",
|
|
104
|
+
"keycloak:stop": "docker compose -f docker/keycloak/docker-compose.yaml stop",
|
|
105
|
+
"keycloak:status": "docker ps -a --filter name=oidc-sveltekit-adapter-keycloak --format 'table {{.Names}}\\t{{.Status}}\\t{{.Ports}}'",
|
|
106
|
+
"keycloak:logs": "docker compose -f docker/keycloak/docker-compose.yaml logs -f",
|
|
107
|
+
"keycloak:purge": "docker compose -f docker/keycloak/docker-compose.yaml down -v",
|
|
108
|
+
"keycloak:init": "node scripts/keycloak-init.ts",
|
|
109
|
+
"test": "pnpm test:vite && pnpm test:play",
|
|
110
|
+
"test:unit": "vitest run --project=unit-tests",
|
|
111
|
+
"test:vite": "vitest run",
|
|
112
|
+
"test:e2e": "playwright test --project=e2e-tests --pass-with-no-tests",
|
|
113
|
+
"test:play": "playwright test --pass-with-no-tests",
|
|
114
|
+
"test:ci": "node scripts/test-ci.ts",
|
|
115
|
+
"vm:pack": "node scripts/vm-pack.ts",
|
|
116
|
+
"gitlab-runner:start": "sudo gitlab-runner start",
|
|
117
|
+
"gitlab-runner:stop": "sudo gitlab-runner stop",
|
|
118
|
+
"gitlab-runner:status": "sudo gitlab-runner status",
|
|
119
|
+
"gitlab-runner:logs": "sudo journalctl -u gitlab-runner -f"
|
|
120
|
+
}
|
|
6
121
|
}
|