New logger, audit notifications
This commit is contained in:
parent
a86b2a346b
commit
563e0a0350
@ -1,3 +1,6 @@
|
|||||||
|
# Current environment - production or development
|
||||||
|
NODE_ENV=production
|
||||||
|
|
||||||
# Front-end public URL, without leading slash
|
# Front-end public URL, without leading slash
|
||||||
PUBLIC_URL=http://localhost:5173
|
PUBLIC_URL=http://localhost:5173
|
||||||
|
|
||||||
@ -23,6 +26,7 @@ SESSION_SECURE=true
|
|||||||
JWT_ALGORITHM=RS256
|
JWT_ALGORITHM=RS256
|
||||||
JWT_EXPIRATION=7d
|
JWT_EXPIRATION=7d
|
||||||
JWT_ISSUER=http://localhost:5173
|
JWT_ISSUER=http://localhost:5173
|
||||||
|
JWT_KEYLENGTH=2048
|
||||||
|
|
||||||
# SMTP settings
|
# SMTP settings
|
||||||
EMAIL_ENABLED=true
|
EMAIL_ENABLED=true
|
||||||
@ -32,6 +36,7 @@ EMAIL_SMTP_PORT=587
|
|||||||
EMAIL_SMTP_SECURE=false
|
EMAIL_SMTP_SECURE=false
|
||||||
EMAIL_SMTP_USER=
|
EMAIL_SMTP_USER=
|
||||||
EMAIL_SMTP_PASS=
|
EMAIL_SMTP_PASS=
|
||||||
|
EMAIL_ADMIN=
|
||||||
|
|
||||||
# Enable new account registrations
|
# Enable new account registrations
|
||||||
REGISTRATIONS=true
|
REGISTRATIONS=true
|
||||||
|
@ -12,10 +12,8 @@ This is a SvelteKit-powered authentication service.
|
|||||||
1. Clone the repository.
|
1. Clone the repository.
|
||||||
2. Install dependenices - `npm install`.
|
2. Install dependenices - `npm install`.
|
||||||
3. Configure the environment - `cp .env.example .env`.
|
3. Configure the environment - `cp .env.example .env`.
|
||||||
4. Generate secrets and stuff:
|
4. Generate secrets:
|
||||||
1. Session secret - `node -e 'console.log(require("crypto").randomBytes(16).toString("hex"))'`.
|
1. Session secret - `node -e 'console.log(require("crypto").randomBytes(16).toString("hex"))'`.
|
||||||
2. Challenge secret - `node -e 'console.log(require("crypto").randomBytes(32).toString("hex"))'`.
|
2. Challenge secret - `node -e 'console.log(require("crypto").randomBytes(32).toString("hex"))'`.
|
||||||
3. Generate JWT keys in the `private` directory - `openssl genpkey -out jwt.private.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048`.
|
|
||||||
4. Also make the public key - `openssl rsa -in jwt.private.pem -pubout -outform PEM -out jwt.public.pem`.
|
|
||||||
5. Build the application - `npm run build`.
|
5. Build the application - `npm run build`.
|
||||||
6. Run the application - `node -r dotenv/config build`.
|
6. Run the application - `node -r dotenv/config build`.
|
||||||
|
622
package-lock.json
generated
622
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
31
package.json
31
package.json
@ -12,40 +12,41 @@
|
|||||||
"format": "prettier --write ."
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/kit": "^2.8.2",
|
"@sveltejs/kit": "^2.9.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.1",
|
"@sveltejs/vite-plugin-svelte": "^5.0.1",
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/eslint": "^9.6.1",
|
"@types/eslint": "^9.6.1",
|
||||||
"@types/mime-types": "^2.1.4",
|
"@types/mime-types": "^2.1.4",
|
||||||
"@types/node": "^22.9.3",
|
"@types/node": "^22.10.1",
|
||||||
"@types/nodemailer": "^6.4.17",
|
"@types/nodemailer": "^6.4.17",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
"@typescript-eslint/eslint-plugin": "^8.17.0",
|
||||||
"@typescript-eslint/parser": "^8.15.0",
|
"@typescript-eslint/parser": "^8.17.0",
|
||||||
"drizzle-kit": "^0.28.1",
|
"drizzle-kit": "^0.30.0",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-svelte": "^2.46.0",
|
"eslint-plugin-svelte": "^2.46.1",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.4.2",
|
||||||
"prettier-plugin-svelte": "^3.3.2",
|
"prettier-plugin-svelte": "^3.3.2",
|
||||||
"svelte": "^5.2.7",
|
"svelte": "^5.9.1",
|
||||||
"svelte-check": "^4.1.0",
|
"svelte-check": "^4.1.1",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"vite": "^5.4.11"
|
"vite": "^6.0.3"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sveltejs/adapter-node": "^5.2.9",
|
"@sveltejs/adapter-node": "^5.2.9",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
"chalk": "^5.4.0",
|
||||||
"cropperjs": "^1.6.2",
|
"cropperjs": "^1.6.2",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.7",
|
||||||
"drizzle-orm": "^0.36.4",
|
"drizzle-orm": "^0.38.0",
|
||||||
"image-size": "^1.1.1",
|
"image-size": "^1.1.1",
|
||||||
"jose": "^5.9.6",
|
"jose": "^5.9.6",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"mysql2": "^3.11.4",
|
"mysql2": "^3.11.5",
|
||||||
"nodemailer": "^6.9.16",
|
"nodemailer": "^6.9.16",
|
||||||
"otplib": "^12.0.1",
|
"otplib": "^12.0.1",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
|
@ -3,25 +3,34 @@ import { csrf } from '$lib/server/csrf';
|
|||||||
import { DB } from '$lib/server/drizzle';
|
import { DB } from '$lib/server/drizzle';
|
||||||
import { runSeeds } from '$lib/server/drizzle/seeds';
|
import { runSeeds } from '$lib/server/drizzle/seeds';
|
||||||
import { JWT } from '$lib/server/jwt';
|
import { JWT } from '$lib/server/jwt';
|
||||||
|
import { Logger, LogLevel } from '$lib/server/logger';
|
||||||
import type { ThemeModeType } from '$lib/theme-mode';
|
import type { ThemeModeType } from '$lib/theme-mode';
|
||||||
import type { Handle } from '@sveltejs/kit';
|
import type { Handle } from '@sveltejs/kit';
|
||||||
import { sequence } from '@sveltejs/kit/hooks';
|
import { sequence } from '@sveltejs/kit/hooks';
|
||||||
import { migrate } from 'drizzle-orm/mysql2/migrator';
|
import { migrate } from 'drizzle-orm/mysql2/migrator';
|
||||||
import { handleSession } from 'svelte-kit-cookie-session';
|
import { handleSession } from 'svelte-kit-cookie-session';
|
||||||
|
|
||||||
const { AUTO_MIGRATE, SESSION_SECRET, SESSION_SECURE } = env;
|
const { AUTO_MIGRATE, SESSION_SECRET, SESSION_SECURE, NODE_ENV } = env;
|
||||||
|
|
||||||
|
// Set logger to debug mode
|
||||||
|
if (NODE_ENV === 'development') {
|
||||||
|
Logger.setLogLevel(LogLevel.DEBUG);
|
||||||
|
Logger.debug('Debug mode enabled');
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize the database
|
// Initialize the database
|
||||||
await DB.init();
|
await DB.init();
|
||||||
|
|
||||||
// Initialize the JWT keys
|
|
||||||
await JWT.init();
|
|
||||||
|
|
||||||
// Migrate the database when automatic migration is enabled
|
// Migrate the database when automatic migration is enabled
|
||||||
if (AUTO_MIGRATE === 'true') {
|
if (AUTO_MIGRATE === 'true') {
|
||||||
|
Logger.info('Running automatic database migrations');
|
||||||
await migrate(DB.drizzle, { migrationsFolder: './migrations' });
|
await migrate(DB.drizzle, { migrationsFolder: './migrations' });
|
||||||
|
Logger.info('Database migrations complete');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize the JWT keys
|
||||||
|
await JWT.init();
|
||||||
|
|
||||||
// Run database data seeders
|
// Run database data seeders
|
||||||
await runSeeds();
|
await runSeeds();
|
||||||
|
|
||||||
@ -50,3 +59,15 @@ export const handle = sequence(
|
|||||||
}),
|
}),
|
||||||
handleThemeHook
|
handleThemeHook
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export async function handleError({ event, error, message, status }) {
|
||||||
|
const request = `${event.request.method} ${event.request.url} ${status} - ${message}`;
|
||||||
|
if (status && status >= 200 && status < 500) {
|
||||||
|
// Don't need error trace for non-server errors
|
||||||
|
Logger.error(request);
|
||||||
|
} else {
|
||||||
|
Logger.error(request, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { message };
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { SQL, count, desc, eq, inArray, or, sql } from 'drizzle-orm';
|
import { SQL, count, desc, eq, inArray, lt, or, sql } from 'drizzle-orm';
|
||||||
import { DB, auditLog, user, type AuditLog, type NewAuditLog, type User } from '../drizzle';
|
import { DB, auditLog, user, type AuditLog, type NewAuditLog, type User } from '../drizzle';
|
||||||
import {
|
import {
|
||||||
AuditAction,
|
AuditAction,
|
||||||
@ -7,15 +7,23 @@ import {
|
|||||||
type MinimalRequestEvent
|
type MinimalRequestEvent
|
||||||
} from './types';
|
} from './types';
|
||||||
import type { Paginated, PaginationMeta } from '$lib/types';
|
import type { Paginated, PaginationMeta } from '$lib/types';
|
||||||
|
import { Logger } from '../logger';
|
||||||
|
import { AdminNotificationEmail, Emails } from '../email';
|
||||||
|
import { env } from '$env/dynamic/private';
|
||||||
|
import { env as publicEnv } from '$env/dynamic/public';
|
||||||
|
|
||||||
const AUTOFLAG = [
|
const FLAG_EMAIL_COOLDOWN = 1 * 60 * 1000;
|
||||||
AuditAction.MALICIOUS_REQUEST,
|
const FLAG_TRESHOLD_COOLDOWN = 30 * 60 * 1000;
|
||||||
AuditAction.THROTTLE,
|
const FLAG_EMAIL_TRESHOLD = 3;
|
||||||
AuditAction.DEACTIVATION_REQUEST,
|
const AUTOFLAG = [AuditAction.MALICIOUS_REQUEST, AuditAction.THROTTLE];
|
||||||
AuditAction.DATA_DOWNLOAD_REQUEST
|
|
||||||
];
|
|
||||||
|
|
||||||
export class Audit {
|
export class Audit {
|
||||||
|
protected static logger = new Logger('AUDIT');
|
||||||
|
protected static flagEmailTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
|
||||||
|
protected static flagTresholdTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
|
||||||
|
protected static flagTriggerCount = 0;
|
||||||
|
protected static flagEmailCount = 0;
|
||||||
|
|
||||||
public static async insert(
|
public static async insert(
|
||||||
action: AuditAction,
|
action: AuditAction,
|
||||||
comment?: string,
|
comment?: string,
|
||||||
@ -23,16 +31,23 @@ export class Audit {
|
|||||||
ip?: string,
|
ip?: string,
|
||||||
userAgent?: string
|
userAgent?: string
|
||||||
) {
|
) {
|
||||||
|
const flagged = AUTOFLAG.includes(action);
|
||||||
const newAuditLog: NewAuditLog = {
|
const newAuditLog: NewAuditLog = {
|
||||||
action,
|
action,
|
||||||
content: comment,
|
content: comment,
|
||||||
actorId: user?.id,
|
actorId: user?.id,
|
||||||
actor_ip: ip,
|
actor_ip: ip,
|
||||||
actor_ua: userAgent,
|
actor_ua: userAgent,
|
||||||
flagged: Number(AUTOFLAG.includes(action))
|
flagged: Number(flagged)
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: send flagged to administrator
|
Audit.logger.info(
|
||||||
|
`${action} ${user?.id || '-'} (${ip || '-'}) "${userAgent || '-'}" "${comment}" ${flagged}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (flagged) {
|
||||||
|
Audit.auditFlagTrigger();
|
||||||
|
}
|
||||||
|
|
||||||
await DB.drizzle.insert(auditLog).values(newAuditLog);
|
await DB.drizzle.insert(auditLog).values(newAuditLog);
|
||||||
}
|
}
|
||||||
@ -159,4 +174,51 @@ export class Audit {
|
|||||||
return accum;
|
return accum;
|
||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async auditFlagTrigger() {
|
||||||
|
Audit.flagTriggerCount += 1;
|
||||||
|
|
||||||
|
clearTimeout(Audit.flagTresholdTimeout);
|
||||||
|
Audit.flagTresholdTimeout = setTimeout(() => {
|
||||||
|
Audit.flagTriggerCount = 0;
|
||||||
|
Audit.flagEmailCount = 0;
|
||||||
|
}, FLAG_TRESHOLD_COOLDOWN);
|
||||||
|
|
||||||
|
if (
|
||||||
|
Audit.flagTriggerCount < FLAG_EMAIL_TRESHOLD ||
|
||||||
|
Audit.flagEmailTimeout ||
|
||||||
|
Audit.flagEmailCount > 1
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.verbose('Flag treshold reached, sending out audit email to administrator');
|
||||||
|
Audit.flagEmailTimeout = setTimeout(() => {
|
||||||
|
void Audit.sendAuditEmail(Audit.flagTriggerCount);
|
||||||
|
Audit.flagTriggerCount = 0;
|
||||||
|
Audit.flagEmailCount += 1;
|
||||||
|
Audit.flagEmailTimeout = undefined;
|
||||||
|
}, FLAG_EMAIL_COOLDOWN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an audit warning email
|
||||||
|
*/
|
||||||
|
private static async sendAuditEmail(count: number) {
|
||||||
|
if (!env.EMAIL_ADMIN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = AdminNotificationEmail(count);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Emails.getSender().sendTemplate(
|
||||||
|
env.EMAIL_ADMIN,
|
||||||
|
`Audit events on ${publicEnv.PUBLIC_SITE_NAME} may require attention`,
|
||||||
|
content
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('Failed to send audit email:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { env } from '$env/dynamic/private';
|
import { env } from '$env/dynamic/private';
|
||||||
import { drizzle } from 'drizzle-orm/mysql2';
|
import { drizzle } from 'drizzle-orm/mysql2';
|
||||||
import mysql from 'mysql2/promise';
|
import mysql from 'mysql2';
|
||||||
import * as schema from './schema';
|
import * as schema from './schema';
|
||||||
|
import { Logger } from '../logger';
|
||||||
|
|
||||||
const { DATABASE_DB, DATABASE_HOST, DATABASE_PASS, DATABASE_USER } = env;
|
const { DATABASE_DB, DATABASE_HOST, DATABASE_PASS, DATABASE_USER } = env;
|
||||||
|
|
||||||
@ -24,12 +25,14 @@ export class DB {
|
|||||||
DB.mysqlConnection.on('connection', (connection) => {
|
DB.mysqlConnection.on('connection', (connection) => {
|
||||||
connection.on('error', (error) => {
|
connection.on('error', (error) => {
|
||||||
// We log warning on connection error, this is not a critical error
|
// We log warning on connection error, this is not a critical error
|
||||||
console.warn('Error received on connection. Will destroy.', { error });
|
Logger.warn('Error received on connection. Will destroy.', { error });
|
||||||
connection.destroy();
|
connection.destroy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
DB.drizzle = drizzle(DB.mysqlConnection, { schema, mode: 'default' });
|
DB.drizzle = drizzle({ client: DB.mysqlConnection, schema, mode: 'default' });
|
||||||
|
|
||||||
|
Logger.debug('Database initialization complete');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
17
src/lib/server/email/templates/admin-notification.email.ts
Normal file
17
src/lib/server/email/templates/admin-notification.email.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { env } from '$env/dynamic/public';
|
||||||
|
import type { EmailTemplate } from '../template.interface';
|
||||||
|
|
||||||
|
export const AdminNotificationEmail = (count: number): EmailTemplate => ({
|
||||||
|
text: `
|
||||||
|
${env.PUBLIC_SITE_NAME}
|
||||||
|
|
||||||
|
There have been ${count} flagged audit messages in the past 30 minutes. Urgent attention may be required.
|
||||||
|
|
||||||
|
This email was sent to you because you are the administrator contact on ${env.PUBLIC_SITE_NAME}.`,
|
||||||
|
html: /* html */ `
|
||||||
|
<h1>${env.PUBLIC_SITE_NAME}</h1>
|
||||||
|
|
||||||
|
<p>There have been <strong>${count}</strong> flagged audit messages in the past 5 minutes. <strong>Urgent attention may be required.</strong></p>
|
||||||
|
|
||||||
|
<p>This email was sent to you because you are the administrator contact on ${env.PUBLIC_SITE_NAME}.</p>`
|
||||||
|
});
|
@ -2,3 +2,4 @@ export * from './forgot-password.email';
|
|||||||
export * from './invitation.email';
|
export * from './invitation.email';
|
||||||
export * from './oauth2-invitation.email';
|
export * from './oauth2-invitation.email';
|
||||||
export * from './registration.email';
|
export * from './registration.email';
|
||||||
|
export * from './admin-notification.email';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { mkdir, readFile, stat, unlink, writeFile } from 'fs/promises';
|
import { mkdir, readFile, rm, stat, unlink, writeFile } from 'fs/promises';
|
||||||
import { dirname, join, resolve } from 'path';
|
import { dirname, join, resolve } from 'path';
|
||||||
|
|
||||||
export class FileBackend {
|
export class FileBackend {
|
||||||
@ -32,6 +32,15 @@ export class FileBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async deleteDirectory(path: string | string[]) {
|
||||||
|
try {
|
||||||
|
await rm(FileBackend.filePath(path), { recursive: true });
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static filePath(path: string | string[]) {
|
private static filePath(path: string | string[]) {
|
||||||
return resolve(Array.isArray(path) ? join(...path) : path);
|
return resolve(Array.isArray(path) ? join(...path) : path);
|
||||||
}
|
}
|
||||||
|
@ -15,8 +15,10 @@ import { v4 as uuidv4 } from 'uuid';
|
|||||||
import { FileBackend } from './file-backend';
|
import { FileBackend } from './file-backend';
|
||||||
import { DB, jwks, type JsonKey } from './drizzle';
|
import { DB, jwks, type JsonKey } from './drizzle';
|
||||||
import { CryptoUtils } from './crypto-utils';
|
import { CryptoUtils } from './crypto-utils';
|
||||||
|
import { Logger } from './logger';
|
||||||
|
import { lt } from 'drizzle-orm';
|
||||||
|
|
||||||
const { JWT_ALGORITHM, JWT_EXPIRATION, JWT_ISSUER } = env;
|
const { JWT_ALGORITHM, JWT_EXPIRATION, JWT_ISSUER, JWT_KEYLENGTH } = env;
|
||||||
const ISSUER_EXPIRY = 365 * 24 * 60 * 60 * 1000;
|
const ISSUER_EXPIRY = 365 * 24 * 60 * 60 * 1000;
|
||||||
const ISSUER_ROTATE = ISSUER_EXPIRY / 2;
|
const ISSUER_ROTATE = ISSUER_EXPIRY / 2;
|
||||||
|
|
||||||
@ -26,11 +28,6 @@ interface AvailableWebKey extends JsonKey {
|
|||||||
publicKeyJWK: JWK;
|
publicKeyJWK: JWK;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate JWT keys using the following commands:
|
|
||||||
* Private: openssl genpkey -out jwt.private.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048
|
|
||||||
* Public: openssl rsa -in jwt.private.pem -pubout -outform PEM -out jwt.public.pem
|
|
||||||
*/
|
|
||||||
export class JWT {
|
export class JWT {
|
||||||
private static keys: AvailableWebKey[] = [];
|
private static keys: AvailableWebKey[] = [];
|
||||||
|
|
||||||
@ -44,19 +41,8 @@ export class JWT {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async init() {
|
static async init() {
|
||||||
const keys = await DB.drizzle.select().from(jwks);
|
await JWT.clearExpiredKeys();
|
||||||
JWT.keys = await JWT.loadKeys(keys);
|
await JWT.loadFromStorage();
|
||||||
|
|
||||||
// No current key or it is time to rotate
|
|
||||||
const keyPair = JWT.keys.find(({ current }) => current === 1);
|
|
||||||
if (!keyPair || keyPair.rotate_at.getTime() < Date.now()) {
|
|
||||||
const newKey = await JWT.createKeyPair();
|
|
||||||
if (keyPair) {
|
|
||||||
keyPair.current = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
JWT.keys.push(newKey);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async issue(claims: Record<string, unknown>, subject: string, audience?: string) {
|
static async issue(claims: Record<string, unknown>, subject: string, audience?: string) {
|
||||||
@ -95,6 +81,45 @@ export class JWT {
|
|||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async clearExpiredKeys() {
|
||||||
|
Logger.debug(`Loading expired JWT key information`);
|
||||||
|
|
||||||
|
const expiredQuery = lt(jwks.expires_at, new Date());
|
||||||
|
const expireList = await DB.drizzle.select().from(jwks).where(expiredQuery);
|
||||||
|
if (!expireList.length) {
|
||||||
|
Logger.debug(`There are no expired JWT keys to clean up`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const expired of expireList) {
|
||||||
|
Logger.warn(`Deleting expired JWT key pair ${expired.uuid} (${expired.fingerprint})`);
|
||||||
|
await FileBackend.deleteDirectory(['private', expired.uuid]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete expired keys
|
||||||
|
await DB.drizzle.delete(jwks).where(expiredQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async loadFromStorage() {
|
||||||
|
// Fetch existing keys
|
||||||
|
const keys = await DB.drizzle.select().from(jwks);
|
||||||
|
Logger.verbose(`Loading ${keys.length} JWT key pairs into memory`);
|
||||||
|
JWT.keys = await JWT.loadKeys(keys);
|
||||||
|
|
||||||
|
// No current key or it is time to rotate
|
||||||
|
const keyPair = JWT.keys.find(({ current }) => current === 1);
|
||||||
|
if (!keyPair || keyPair.rotate_at.getTime() < Date.now()) {
|
||||||
|
Logger.verbose(`Creating a new JWT key pair`);
|
||||||
|
|
||||||
|
const newKey = await JWT.createKeyPair();
|
||||||
|
if (keyPair) {
|
||||||
|
keyPair.current = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
JWT.keys.push(newKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static getCurrentKey() {
|
private static getCurrentKey() {
|
||||||
const current = JWT.keys.find(({ current }) => current === 1);
|
const current = JWT.keys.find(({ current }) => current === 1);
|
||||||
if (!current) {
|
if (!current) {
|
||||||
@ -104,8 +129,13 @@ export class JWT {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static async createKeyPair() {
|
private static async createKeyPair() {
|
||||||
|
/**
|
||||||
|
* Equivalent openssl commands:
|
||||||
|
* Private: openssl genpkey -out jwt.private.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048
|
||||||
|
* Public: openssl rsa -in jwt.private.pem -pubout -outform PEM -out jwt.public.pem
|
||||||
|
*/
|
||||||
const { privateKey, publicKey } = await generateKeyPair(JWT_ALGORITHM, {
|
const { privateKey, publicKey } = await generateKeyPair(JWT_ALGORITHM, {
|
||||||
modulusLength: 2048
|
modulusLength: Number(JWT_KEYLENGTH) || 2048
|
||||||
});
|
});
|
||||||
const jwk = await exportJWK(publicKey);
|
const jwk = await exportJWK(publicKey);
|
||||||
const kid = uuidv4({ random: Buffer.from(jwk.n as string).subarray(0, 16) });
|
const kid = uuidv4({ random: Buffer.from(jwk.n as string).subarray(0, 16) });
|
||||||
|
151
src/lib/server/logger.ts
Normal file
151
src/lib/server/logger.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import chalk from 'chalk';
|
||||||
|
import { format as strfmt, formatWithOptions as strfmtOptions } from 'node:util';
|
||||||
|
|
||||||
|
export enum LogLevel {
|
||||||
|
DEBUG = 0,
|
||||||
|
VERBOSE,
|
||||||
|
INFO,
|
||||||
|
WARN,
|
||||||
|
ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILogger {
|
||||||
|
log: (format: any, ...arg: any[]) => void;
|
||||||
|
debug: (format: any, ...arg: any[]) => void;
|
||||||
|
verbose: (format: any, ...arg: any[]) => void;
|
||||||
|
info: (format: any, ...arg: any[]) => void;
|
||||||
|
warn: (format: any, ...arg: any[]) => void;
|
||||||
|
error: (format: any, ...arg: any[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chalkColors: Record<LogLevel, (str: string) => string> = {
|
||||||
|
[LogLevel.DEBUG]: chalk.magenta,
|
||||||
|
[LogLevel.VERBOSE]: chalk.blueBright,
|
||||||
|
[LogLevel.INFO]: chalk.green,
|
||||||
|
[LogLevel.WARN]: chalk.yellow,
|
||||||
|
[LogLevel.ERROR]: chalk.red
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Logger implements ILogger {
|
||||||
|
protected static instance?: Logger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public context: string = '',
|
||||||
|
public logLevel: LogLevel = LogLevel.VERBOSE
|
||||||
|
) {}
|
||||||
|
|
||||||
|
static getInstance() {
|
||||||
|
if (!Logger.instance) {
|
||||||
|
Logger.instance = new Logger();
|
||||||
|
}
|
||||||
|
return Logger.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected get formatted() {
|
||||||
|
return this.logLevel === LogLevel.DEBUG;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected colorize(level: LogLevel, leading: string, trailing: string) {
|
||||||
|
if (!this.formatted) {
|
||||||
|
return leading + trailing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return chalkColors[level](leading) + trailing;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected handle(level: LogLevel, format: any, ...arg: any[]): void {
|
||||||
|
if (level < this.logLevel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = this.getTimestamp();
|
||||||
|
const logLevelKey = (Object.values(LogLevel) as string[])[level].toUpperCase().padStart(7);
|
||||||
|
|
||||||
|
let formatted = this.formatted
|
||||||
|
? strfmtOptions({ colors: true, depth: 8 }, format, ...arg)
|
||||||
|
: strfmt(format, ...arg);
|
||||||
|
|
||||||
|
// Keep non-debug logs on one line for neatness
|
||||||
|
if (!this.formatted) {
|
||||||
|
formatted = formatted.replace(/\n/g, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
const outLine = this.colorize(
|
||||||
|
level,
|
||||||
|
`[${process.pid}] ${timestamp} ${logLevelKey}${this.context ? ` [${this.context}]` : ''} `,
|
||||||
|
formatted
|
||||||
|
);
|
||||||
|
this.writeLog(LogLevel.ERROR ? 'err' : 'out', outLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getTimestamp() {
|
||||||
|
return new Intl.DateTimeFormat('en-GB', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
}).format(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected writeLog(stream: 'out' | 'err', message: string) {
|
||||||
|
(stream === 'out' ? process.stdout : process.stderr).write(`${message}\r\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLogLevel(newLevel: LogLevel) {
|
||||||
|
this.logLevel = newLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
static setLogLevel(newLevel: LogLevel) {
|
||||||
|
Logger.getInstance().logLevel = newLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(format: any, ...arg: any[]) {
|
||||||
|
this.handle(LogLevel.INFO, format, ...arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static log(format: any, ...arg: any[]) {
|
||||||
|
Logger.getInstance().handle(LogLevel.INFO, format, ...arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(format: any, ...arg: any[]) {
|
||||||
|
this.handle(LogLevel.DEBUG, format, ...arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static debug(format: any, ...arg: any[]) {
|
||||||
|
Logger.getInstance().handle(LogLevel.DEBUG, format, ...arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
verbose(format: any, ...arg: any[]) {
|
||||||
|
this.handle(LogLevel.VERBOSE, format, ...arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static verbose(format: any, ...arg: any[]) {
|
||||||
|
Logger.getInstance().handle(LogLevel.VERBOSE, format, ...arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
info(format: any, ...arg: any[]) {
|
||||||
|
this.handle(LogLevel.INFO, format, ...arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static info(format: any, ...arg: any[]) {
|
||||||
|
Logger.getInstance().handle(LogLevel.INFO, format, ...arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(format: any, ...arg: any[]) {
|
||||||
|
this.handle(LogLevel.WARN, format, ...arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static warn(format: any, ...arg: any[]) {
|
||||||
|
Logger.getInstance().handle(LogLevel.WARN, format, ...arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
error(format: any, ...arg: any[]) {
|
||||||
|
this.handle(LogLevel.ERROR, format, ...arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static error(format: any, ...arg: any[]) {
|
||||||
|
Logger.getInstance().handle(LogLevel.ERROR, format, ...arg);
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
import { Logger } from '$lib/server/logger';
|
||||||
import type { UserSession } from '../../users';
|
import type { UserSession } from '../../users';
|
||||||
import {
|
import {
|
||||||
InvalidRequest,
|
InvalidRequest,
|
||||||
@ -26,7 +27,7 @@ export class OAuth2AuthorizationController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const redirectUri = url.searchParams.get('redirect_uri') as string;
|
const redirectUri = url.searchParams.get('redirect_uri') as string;
|
||||||
// console.debug('Parameter redirect uri is', redirectUri);
|
Logger.debug('Parameter redirect uri is', redirectUri);
|
||||||
|
|
||||||
if (!url.searchParams.has('client_id')) {
|
if (!url.searchParams.has('client_id')) {
|
||||||
throw new InvalidRequest('client_id field is mandatory for authorization endpoint');
|
throw new InvalidRequest('client_id field is mandatory for authorization endpoint');
|
||||||
@ -40,14 +41,14 @@ export class OAuth2AuthorizationController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const clientId = url.searchParams.get('client_id') as string;
|
const clientId = url.searchParams.get('client_id') as string;
|
||||||
// console.debug('Parameter client_id is', clientId);
|
Logger.debug('Parameter client_id is', clientId);
|
||||||
|
|
||||||
if (!url.searchParams.has('response_type')) {
|
if (!url.searchParams.has('response_type')) {
|
||||||
throw new InvalidRequest('response_type field is mandatory for authorization endpoint');
|
throw new InvalidRequest('response_type field is mandatory for authorization endpoint');
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseType = url.searchParams.get('response_type') as string;
|
const responseType = url.searchParams.get('response_type') as string;
|
||||||
// console.debug('Parameter response_type is', responseType);
|
Logger.debug('Parameter response_type is', responseType);
|
||||||
|
|
||||||
// Support multiple types
|
// Support multiple types
|
||||||
const responseTypes = responseType.split(' ');
|
const responseTypes = responseType.split(' ');
|
||||||
@ -77,7 +78,7 @@ export class OAuth2AuthorizationController {
|
|||||||
throw new InvalidRequest('Grant type "none" cannot be combined with other grant types');
|
throw new InvalidRequest('Grant type "none" cannot be combined with other grant types');
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.debug('Parameter grant_type is', grantTypes.join(' '));
|
Logger.debug('Parameter grant_type is', grantTypes.join(' '));
|
||||||
|
|
||||||
const client = await OAuth2Clients.fetchById(clientId);
|
const client = await OAuth2Clients.fetchById(clientId);
|
||||||
if (!client || client.activated === 0) {
|
if (!client || client.activated === 0) {
|
||||||
@ -89,7 +90,7 @@ export class OAuth2AuthorizationController {
|
|||||||
} else if (!(await OAuth2Clients.checkRedirectUri(client, redirectUri))) {
|
} else if (!(await OAuth2Clients.checkRedirectUri(client, redirectUri))) {
|
||||||
throw new InvalidRequest('Wrong RedirectUri provided');
|
throw new InvalidRequest('Wrong RedirectUri provided');
|
||||||
}
|
}
|
||||||
// console.debug('redirect_uri check passed');
|
Logger.debug('redirect_uri check passed');
|
||||||
|
|
||||||
// The client needs to support all grant types
|
// The client needs to support all grant types
|
||||||
for (const grantType of grantTypes) {
|
for (const grantType of grantTypes) {
|
||||||
@ -97,13 +98,13 @@ export class OAuth2AuthorizationController {
|
|||||||
throw new UnauthorizedClient('This client does not support grant type ' + grantType);
|
throw new UnauthorizedClient('This client does not support grant type ' + grantType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// console.debug('Grant type check passed');
|
Logger.debug('Grant type check passed');
|
||||||
|
|
||||||
const scope = OAuth2Clients.transformScope(url.searchParams.get('scope') as string);
|
const scope = OAuth2Clients.transformScope(url.searchParams.get('scope') as string);
|
||||||
if (!OAuth2Clients.checkScope(client, scope)) {
|
if (!OAuth2Clients.checkScope(client, scope)) {
|
||||||
throw new InvalidScope('Client does not allow access to this scope');
|
throw new InvalidScope('Client does not allow access to this scope');
|
||||||
}
|
}
|
||||||
// console.debug('Scope check passed');
|
Logger.debug('Scope check passed');
|
||||||
|
|
||||||
const codeChallenge = url.searchParams.get('code_challenge') as string;
|
const codeChallenge = url.searchParams.get('code_challenge') as string;
|
||||||
const codeChallengeMethod =
|
const codeChallengeMethod =
|
||||||
@ -267,7 +268,7 @@ export class OAuth2AuthorizationController {
|
|||||||
throw new AccessDenied('User denied access to the resource');
|
throw new AccessDenied('User denied access to the resource');
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.debug('Decision check passed');
|
Logger.debug('Decision check passed');
|
||||||
}
|
}
|
||||||
|
|
||||||
await OAuth2Users.saveConsent(user, client, scope);
|
await OAuth2Users.saveConsent(user, client, scope);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
import { ApiUtils } from '$lib/server/api-utils';
|
import { ApiUtils } from '$lib/server/api-utils';
|
||||||
|
import { Logger } from '$lib/server/logger';
|
||||||
import { InvalidClient, InvalidRequest, InvalidScope, UnauthorizedClient } from '../error';
|
import { InvalidClient, InvalidRequest, InvalidScope, UnauthorizedClient } from '../error';
|
||||||
import { OAuth2Clients, OAuth2DeviceCodes, OAuth2Tokens } from '../model';
|
import { OAuth2Clients, OAuth2DeviceCodes, OAuth2Tokens } from '../model';
|
||||||
import { OAuth2Response } from '../response';
|
import { OAuth2Response } from '../response';
|
||||||
@ -34,7 +35,7 @@ export class OAuth2DeviceAuthorizationController {
|
|||||||
|
|
||||||
clientId = pieces[0];
|
clientId = pieces[0];
|
||||||
clientSecret = pieces[1];
|
clientSecret = pieces[1];
|
||||||
// console.debug('Client credentials parsed from basic auth header:', clientId, clientSecret);
|
Logger.debug('Client credentials parsed from basic auth header:', clientId, clientSecret);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!clientId) {
|
if (!clientId) {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ApiUtils } from '$lib/server/api-utils';
|
import { ApiUtils } from '$lib/server/api-utils';
|
||||||
|
import { Logger } from '$lib/server/logger';
|
||||||
import { InvalidClient, InvalidRequest, UnauthorizedClient } from '../error';
|
import { InvalidClient, InvalidRequest, UnauthorizedClient } from '../error';
|
||||||
import { OAuth2AccessTokens, OAuth2Clients } from '../model';
|
import { OAuth2AccessTokens, OAuth2Clients } from '../model';
|
||||||
import { OAuth2Response } from '../response';
|
import { OAuth2Response } from '../response';
|
||||||
@ -13,7 +14,7 @@ export class OAuth2IntrospectionController {
|
|||||||
if (body.client_id && body.client_secret) {
|
if (body.client_id && body.client_secret) {
|
||||||
clientId = body.client_id as string;
|
clientId = body.client_id as string;
|
||||||
clientSecret = body.client_secret as string;
|
clientSecret = body.client_secret as string;
|
||||||
// console.debug('Client credentials parsed from body parameters ', clientId, clientSecret);
|
Logger.debug('Client credentials parsed from body parameters ', clientId, clientSecret);
|
||||||
} else {
|
} else {
|
||||||
if (!request.headers?.has('authorization')) {
|
if (!request.headers?.has('authorization')) {
|
||||||
throw new InvalidRequest('No authorization header passed');
|
throw new InvalidRequest('No authorization header passed');
|
||||||
@ -35,7 +36,7 @@ export class OAuth2IntrospectionController {
|
|||||||
|
|
||||||
clientId = pieces[0];
|
clientId = pieces[0];
|
||||||
clientSecret = pieces[1];
|
clientSecret = pieces[1];
|
||||||
// console.debug('Client credentials parsed from basic auth header: ', clientId, clientSecret);
|
Logger.debug('Client credentials parsed from basic auth header: ', clientId, clientSecret);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!body.token) {
|
if (!body.token) {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ApiUtils } from '$lib/server/api-utils';
|
import { ApiUtils } from '$lib/server/api-utils';
|
||||||
|
import { Logger } from '$lib/server/logger';
|
||||||
import {
|
import {
|
||||||
InvalidRequest,
|
InvalidRequest,
|
||||||
InvalidClient,
|
InvalidClient,
|
||||||
@ -22,7 +23,7 @@ export class OAuth2TokenController {
|
|||||||
if (body.client_id) {
|
if (body.client_id) {
|
||||||
clientId = body.client_id as string;
|
clientId = body.client_id as string;
|
||||||
clientSecret = body.client_secret as string;
|
clientSecret = body.client_secret as string;
|
||||||
// console.debug('Client credentials parsed from body parameters', clientId, clientSecret);
|
Logger.debug('Client credentials parsed from body parameters', clientId, clientSecret);
|
||||||
} else {
|
} else {
|
||||||
if (!request.headers?.has('authorization')) {
|
if (!request.headers?.has('authorization')) {
|
||||||
throw new InvalidRequest('No authorization header passed');
|
throw new InvalidRequest('No authorization header passed');
|
||||||
@ -44,7 +45,7 @@ export class OAuth2TokenController {
|
|||||||
|
|
||||||
clientId = pieces[0];
|
clientId = pieces[0];
|
||||||
clientSecret = pieces[1];
|
clientSecret = pieces[1];
|
||||||
// console.debug('Client credentials parsed from basic auth header:', clientId, clientSecret);
|
Logger.debug('Client credentials parsed from basic auth header:', clientId, clientSecret);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!body.grant_type) {
|
if (!body.grant_type) {
|
||||||
@ -52,7 +53,7 @@ export class OAuth2TokenController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
grantType = body.grant_type as string;
|
grantType = body.grant_type as string;
|
||||||
// console.debug('Parameter grant_type is', grantType);
|
Logger.debug('Parameter grant_type is', grantType);
|
||||||
|
|
||||||
// The spec does not allow using this grant type directly by this name,
|
// The spec does not allow using this grant type directly by this name,
|
||||||
// but for verification purposes, we will simplify it below.
|
// but for verification purposes, we will simplify it below.
|
||||||
@ -80,7 +81,7 @@ export class OAuth2TokenController {
|
|||||||
if (!OAuth2Clients.checkGrantType(client, grantType) && grantType !== 'refresh_token') {
|
if (!OAuth2Clients.checkGrantType(client, grantType) && grantType !== 'refresh_token') {
|
||||||
throw new UnauthorizedClient('Invalid grant type for the client');
|
throw new UnauthorizedClient('Invalid grant type for the client');
|
||||||
}
|
}
|
||||||
// console.debug('Grant type check passed');
|
Logger.debug('Grant type check passed');
|
||||||
|
|
||||||
let tokenResponse: OAuth2TokenResponse = {};
|
let tokenResponse: OAuth2TokenResponse = {};
|
||||||
try {
|
try {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { CryptoUtils } from '$lib/server/crypto-utils';
|
import { CryptoUtils } from '$lib/server/crypto-utils';
|
||||||
import type { OAuth2Client, User } from '$lib/server/drizzle';
|
import type { OAuth2Client, User } from '$lib/server/drizzle';
|
||||||
|
import { Logger } from '$lib/server/logger';
|
||||||
import { Users } from '$lib/server/users';
|
import { Users } from '$lib/server/users';
|
||||||
import { InvalidRequest, ServerError, InvalidGrant } from '../../error';
|
import { InvalidRequest, ServerError, InvalidGrant } from '../../error';
|
||||||
import {
|
import {
|
||||||
@ -38,7 +39,7 @@ export async function authorizationCode(
|
|||||||
try {
|
try {
|
||||||
code = await OAuth2Codes.fetchByCode(providedCode);
|
code = await OAuth2Codes.fetchByCode(providedCode);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
Logger.error('Failed to fetch OAuth2 access code', err);
|
||||||
throw new ServerError('Failed to call code.fetchByCode function');
|
throw new ServerError('Failed to call code.fetchByCode function');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,7 +55,7 @@ export async function authorizationCode(
|
|||||||
throw new InvalidGrant('Code not found');
|
throw new InvalidGrant('Code not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.debug('Code fetched', code);
|
Logger.debug('Code fetched', code);
|
||||||
|
|
||||||
const scope = code.scope || '';
|
const scope = code.scope || '';
|
||||||
const cleanScope = OAuth2Clients.transformScope(scope);
|
const cleanScope = OAuth2Clients.transformScope(scope);
|
||||||
@ -83,15 +84,15 @@ export async function authorizationCode(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.debug('Code passed PCKE check');
|
Logger.debug('Code passed PCKE check');
|
||||||
|
|
||||||
if (!OAuth2Clients.checkGrantType(client, 'refresh_token')) {
|
if (!OAuth2Clients.checkGrantType(client, 'refresh_token')) {
|
||||||
// console.debug('Client does not allow grant type refresh_token, skip creation');
|
Logger.debug('Client does not allow grant type refresh_token, skip creation');
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
respObj.refresh_token = await OAuth2RefreshTokens.create(userId, clientId, scope);
|
respObj.refresh_token = await OAuth2RefreshTokens.create(userId, clientId, scope);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
Logger.error('Failed to issue an OAuth2 refresh token', err);
|
||||||
throw new ServerError('Failed to call refreshTokens.create function');
|
throw new ServerError('Failed to call refreshTokens.create function');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -104,17 +105,17 @@ export async function authorizationCode(
|
|||||||
OAuth2Tokens.tokenTtl
|
OAuth2Tokens.tokenTtl
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
Logger.error('Failed to issue an OAuth2 access token', err);
|
||||||
throw new ServerError('Failed to call accessTokens.create function');
|
throw new ServerError('Failed to call accessTokens.create function');
|
||||||
}
|
}
|
||||||
|
|
||||||
respObj.expires_in = OAuth2Tokens.tokenTtl;
|
respObj.expires_in = OAuth2Tokens.tokenTtl;
|
||||||
// console.debug('Access token saved:', respObj.access_token);
|
Logger.debug('Access token saved:', respObj.access_token);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await OAuth2Codes.removeByCode(providedCode);
|
await OAuth2Codes.removeByCode(providedCode);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
Logger.error('Failed to remove OAuth2 access code', err);
|
||||||
throw new ServerError('Failed to call codes.removeByCode function');
|
throw new ServerError('Failed to call codes.removeByCode function');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,7 +130,7 @@ export async function authorizationCode(
|
|||||||
code.nonce || undefined
|
code.nonce || undefined
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
Logger.error('Failed to issue a new ID token', err);
|
||||||
throw new ServerError('Failed to issue an ID token');
|
throw new ServerError('Failed to issue an ID token');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import type { OAuth2Client } from '$lib/server/drizzle';
|
import type { OAuth2Client } from '$lib/server/drizzle';
|
||||||
|
import { Logger } from '$lib/server/logger';
|
||||||
import { InvalidScope, ServerError } from '../../error';
|
import { InvalidScope, ServerError } from '../../error';
|
||||||
import { OAuth2AccessTokens, OAuth2Clients, OAuth2Tokens } from '../../model';
|
import { OAuth2AccessTokens, OAuth2Clients, OAuth2Tokens } from '../../model';
|
||||||
import type { OAuth2TokenResponse } from '../../response';
|
import type { OAuth2TokenResponse } from '../../response';
|
||||||
@ -24,7 +25,7 @@ export async function clientCredentials(
|
|||||||
throw new InvalidScope('Client does not allow access to this scope');
|
throw new InvalidScope('Client does not allow access to this scope');
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.debug('Scope check passed', scope);
|
Logger.debug('Scope check passed', scope);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
resObj.access_token = await OAuth2AccessTokens.create(
|
resObj.access_token = await OAuth2AccessTokens.create(
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import type { OAuth2Client } from '$lib/server/drizzle';
|
import type { OAuth2Client } from '$lib/server/drizzle';
|
||||||
|
import { Logger } from '$lib/server/logger';
|
||||||
import { Users } from '$lib/server/users';
|
import { Users } from '$lib/server/users';
|
||||||
import { AccessDenied, AuthorizationPending, ExpiredToken, ServerError } from '../../error';
|
import { AccessDenied, AuthorizationPending, ExpiredToken, ServerError } from '../../error';
|
||||||
import {
|
import {
|
||||||
@ -51,7 +52,7 @@ export async function device(client: OAuth2Client, deviceCode: string) {
|
|||||||
try {
|
try {
|
||||||
resObj.id_token = await OAuth2Users.issueIdToken(user, client, cleanScope);
|
resObj.id_token = await OAuth2Users.issueIdToken(user, client, cleanScope);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
Logger.error('Failed to issue a new ID token:', err);
|
||||||
throw new ServerError('Failed to issue an ID token');
|
throw new ServerError('Failed to issue an ID token');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import type { OAuth2Client, User } from '$lib/server/drizzle';
|
import type { OAuth2Client, User } from '$lib/server/drizzle';
|
||||||
|
import { Logger } from '$lib/server/logger';
|
||||||
import { Users } from '$lib/server/users';
|
import { Users } from '$lib/server/users';
|
||||||
import { InvalidRequest, ServerError, InvalidGrant, InvalidClient } from '../../error';
|
import { InvalidRequest, ServerError, InvalidGrant, InvalidClient } from '../../error';
|
||||||
import {
|
import {
|
||||||
@ -43,7 +44,7 @@ export async function refreshToken(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (refreshToken.clientId !== client.id) {
|
if (refreshToken.clientId !== client.id) {
|
||||||
console.warn(
|
Logger.warn(
|
||||||
'Client %s tried to fetch a refresh token which belongs to client %s!',
|
'Client %s tried to fetch a refresh token which belongs to client %s!',
|
||||||
client.id,
|
client.id,
|
||||||
refreshToken.clientId
|
refreshToken.clientId
|
||||||
|
@ -8,6 +8,7 @@ import { env as privateEnv } from '$env/dynamic/private';
|
|||||||
import { Emails, ForgotPasswordEmail, InvitationEmail, RegistrationEmail } from '../email';
|
import { Emails, ForgotPasswordEmail, InvitationEmail, RegistrationEmail } from '../email';
|
||||||
import { env as publicEnv } from '$env/dynamic/public';
|
import { env as publicEnv } from '$env/dynamic/public';
|
||||||
import { UserTokens } from './tokens';
|
import { UserTokens } from './tokens';
|
||||||
|
import { Logger } from '../logger';
|
||||||
|
|
||||||
export class Users {
|
export class Users {
|
||||||
/**
|
/**
|
||||||
@ -238,7 +239,6 @@ export class Users {
|
|||||||
`${publicEnv.PUBLIC_URL}/login?${params.toString()}`
|
`${publicEnv.PUBLIC_URL}/login?${params.toString()}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: logging
|
|
||||||
try {
|
try {
|
||||||
await Emails.getSender().sendTemplate(
|
await Emails.getSender().sendTemplate(
|
||||||
user.email,
|
user.email,
|
||||||
@ -246,7 +246,7 @@ export class Users {
|
|||||||
content
|
content
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
Logger.error('Failed to send activation email:', error);
|
||||||
await UserTokens.remove(token);
|
await UserTokens.remove(token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -267,14 +267,14 @@ export class Users {
|
|||||||
`${publicEnv.PUBLIC_URL}/login/password?${params.toString()}`
|
`${publicEnv.PUBLIC_URL}/login/password?${params.toString()}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: logging
|
|
||||||
try {
|
try {
|
||||||
await Emails.getSender().sendTemplate(
|
await Emails.getSender().sendTemplate(
|
||||||
user.email,
|
user.email,
|
||||||
`Reset your password on ${publicEnv.PUBLIC_SITE_NAME}`,
|
`Reset your password on ${publicEnv.PUBLIC_SITE_NAME}`,
|
||||||
content
|
content
|
||||||
);
|
);
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
Logger.error('Failed to send password email:', error);
|
||||||
await UserTokens.remove(token);
|
await UserTokens.remove(token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -294,14 +294,14 @@ export class Users {
|
|||||||
const params = new URLSearchParams({ token: token.token });
|
const params = new URLSearchParams({ token: token.token });
|
||||||
const content = InvitationEmail(`${publicEnv.PUBLIC_URL}/register?${params.toString()}`);
|
const content = InvitationEmail(`${publicEnv.PUBLIC_URL}/register?${params.toString()}`);
|
||||||
|
|
||||||
// TODO: logging
|
|
||||||
try {
|
try {
|
||||||
await Emails.getSender().sendTemplate(
|
await Emails.getSender().sendTemplate(
|
||||||
email,
|
email,
|
||||||
`You have been invited to create an account on ${publicEnv.PUBLIC_SITE_NAME}`,
|
`You have been invited to create an account on ${publicEnv.PUBLIC_SITE_NAME}`,
|
||||||
content
|
content
|
||||||
);
|
);
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
Logger.error('Failed to send invitation email:', error);
|
||||||
await UserTokens.remove(token);
|
await UserTokens.remove(token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
import ButtonRow from '$lib/components/container/ButtonRow.svelte';
|
import ButtonRow from '$lib/components/container/ButtonRow.svelte';
|
||||||
import TitleRow from '$lib/components/container/TitleRow.svelte';
|
import TitleRow from '$lib/components/container/TitleRow.svelte';
|
||||||
import ThemeButton from '$lib/components/ThemeButton.svelte';
|
import ThemeButton from '$lib/components/ThemeButton.svelte';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
|
Loading…
Reference in New Issue
Block a user