Skip to content

Commit e0ee528

Browse files
committed
feat: add support for session-based auth
1 parent 695208b commit e0ee528

14 files changed

Lines changed: 1101 additions & 94 deletions

src/authentication/authentication-scheme.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,23 @@ import {
33
SecuritySchemeObject,
44
SecurityRequirementObject,
55
} from 'auto-oas/oas/v3.1';
6+
import { ApiNextFunction, ApiRequest, ApiResponse } from '../router';
7+
import { SessionDriver } from '../session/session-driver';
8+
import { Session } from '../session/session';
9+
import { AuthenticatedRequest } from './authenticated-request';
10+
11+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
12+
export interface AuthenticationSchemeOptions {}
613

714
/**
815
* Abstract base class for authentication schemes
916
* Extend this class to create custom authentication schemes that integrate
1017
* with both Express middleware and OpenAPI specification generation
1118
*/
1219
export abstract class AuthenticationScheme {
20+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
21+
public constructor(options: AuthenticationSchemeOptions = {}) {}
22+
1323
/**
1424
* Unique name for this authentication scheme
1525
* This will be used as the key in OpenAPI securitySchemes
@@ -42,11 +52,55 @@ export abstract class AuthenticationScheme {
4252
return { [this.schemeName]: [] };
4353
}
4454

55+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
56+
public abstract authenticate(request: any): Promise<void>;
57+
public abstract getMiddleware(): RequestHandler;
58+
}
59+
60+
export abstract class InlineAuthenticationScheme extends AuthenticationScheme {
4561
/**
4662
* Generate the Express middleware that enforces this authentication
4763
* The middleware should validate the authentication and throw appropriate
4864
* HTTPError instances if authentication fails
4965
* @returns Express RequestHandler middleware
5066
*/
51-
public abstract getMiddleware(): RequestHandler;
67+
public getMiddleware(): RequestHandler {
68+
return async (
69+
request: AuthenticatedRequest,
70+
response: ApiResponse,
71+
next: ApiNextFunction
72+
) => {
73+
await this.authenticate(request);
74+
request.authenticated = true;
75+
76+
return next();
77+
};
78+
}
79+
}
80+
81+
export abstract class SessionAuthenticationScheme extends AuthenticationScheme {
82+
protected sessionDriver: SessionDriver;
83+
84+
public async getSession(request: ApiRequest): Promise<Session> {
85+
await this.authenticate(request);
86+
return this.sessionDriver.getSession({
87+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
88+
sessionId: (request as any).sessionId,
89+
});
90+
}
91+
92+
public getMiddleware(): RequestHandler {
93+
return async (
94+
request: ApiRequest,
95+
response: ApiResponse,
96+
next: ApiNextFunction
97+
) => {
98+
await this.sessionDriver.checkSession({
99+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
100+
sessionId: (request as any).sessionId,
101+
});
102+
103+
return next();
104+
};
105+
}
52106
}

src/authentication/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
export { AuthenticatedRequest } from './authenticated-request';
2-
export { AuthenticationScheme } from './authentication-scheme';
3-
export * from './middleware';
2+
export {
3+
AuthenticationScheme,
4+
InlineAuthenticationScheme,
5+
SessionAuthenticationScheme,
6+
} from './authentication-scheme';
47
export * from './schemes';
Lines changed: 11 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,27 @@
1-
import { Response, NextFunction } from 'express';
2-
import { UnauthorizedError } from '../../error';
3-
import { AuthenticatedRequest } from '../authenticated-request';
4-
import { BearerTokenValSan } from 'valsan';
51
import { LogInterface } from '../../log';
2+
import { BearerAuthenticationScheme } from '../..';
63

4+
/**
5+
* @deprecated Use BearerAuthenticationScheme instead
6+
*/
77
export interface BearerAuthenticationMiddlewareOptions {
88
checkToken: (token: string) => Promise<boolean>;
99
log?: LogInterface;
1010
}
1111

1212
/**
13+
* @deprecated Use BearerAuthenticationScheme instead
14+
*
1315
* Bearer token authentication middleware
1416
* Validates the Authorization header against the configured bearer token
1517
*/
1618
export function bearerAuthenticationMiddleware(
1719
options: BearerAuthenticationMiddlewareOptions
1820
) {
19-
return async function (
20-
req: AuthenticatedRequest,
21-
res: Response,
22-
next: NextFunction
23-
): Promise<void> {
24-
const authHeader =
25-
req.headers['authorization'] || req.headers['Authorization'];
26-
27-
if (!authHeader) {
28-
options.log?.warn(
29-
'Unauthorized access attempt from ' +
30-
req.ip +
31-
' - No token provided'
32-
);
33-
34-
throw new UnauthorizedError('Authorization header is missing', {
35-
scheme: 'Bearer',
36-
});
37-
}
38-
39-
const validationResult = await new BearerTokenValSan().run(authHeader);
40-
let token: string;
41-
42-
if (validationResult.success) {
43-
token = validationResult.data!;
44-
}
45-
else {
46-
options.log?.warn(
47-
'Unauthorized access attempt from ' +
48-
req.ip +
49-
' - Invalid token format',
50-
JSON.stringify(validationResult.errors, null, 2)
51-
);
52-
53-
throw new UnauthorizedError('Bearer token is empty or invalid', {
54-
scheme: 'Bearer',
55-
details: validationResult.errors,
56-
});
57-
}
58-
59-
if (!(await options.checkToken(token))) {
60-
options.log?.warn(
61-
'Unauthorized access attempt from ' +
62-
req.ip +
63-
' - Check token failed'
64-
);
65-
66-
throw new UnauthorizedError('Bearer token check failed', {
67-
scheme: 'Bearer',
68-
});
69-
}
70-
71-
req.authenticated = true;
72-
next();
21+
const scheme = new BearerAuthenticationScheme({
22+
checkToken: options.checkToken,
23+
log: options.log,
24+
});
7325

74-
return;
75-
};
26+
return scheme.getMiddleware();
7627
}

src/authentication/schemes/bearer-authentication-scheme.ts

Lines changed: 123 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import { RequestHandler } from 'express';
2-
import { AuthenticationScheme } from '../authentication-scheme';
3-
import { SecuritySchemeObject } from 'auto-oas/oas/v3.1';
42
import {
5-
bearerAuthenticationMiddleware,
6-
BearerAuthenticationMiddlewareOptions,
7-
} from '../middleware/bearer-authentication';
3+
AuthenticationSchemeOptions,
4+
InlineAuthenticationScheme,
5+
} from '../authentication-scheme';
6+
import { SecuritySchemeObject } from 'auto-oas/oas/v3.1';
7+
import { UnauthorizedError } from '../../error';
8+
import { BearerTokenValSan } from 'valsan/primitives';
9+
import { LogInterface } from '../../log';
10+
import { AuthenticatedRequest } from '../authenticated-request';
11+
import { ApiNextFunction, ApiResponse } from '../../router';
812

9-
export interface BearerAuthenticationSchemeOptions
10-
extends BearerAuthenticationMiddlewareOptions {
13+
export type CheckTokenFunction = (token: string) => Promise<boolean>;
14+
15+
// eslint-disable-next-line max-len
16+
export interface BearerAuthenticationSchemeOptions extends AuthenticationSchemeOptions {
1117
/**
1218
* The name of the security scheme in OpenAPI
1319
* @default 'BearerAuth'
@@ -26,30 +32,40 @@ export interface BearerAuthenticationSchemeOptions
2632
* Appears in the OpenAPI documentation
2733
*/
2834
description?: string;
35+
36+
/**
37+
* Function to validate the bearer token
38+
* Should return true if the token is valid, false otherwise
39+
*/
40+
checkToken: CheckTokenFunction;
41+
42+
/**
43+
* Optional logger for security events
44+
*/
45+
log?: LogInterface;
2946
}
3047

3148
/**
3249
* Bearer token authentication scheme
3350
* Validates the Authorization header with Bearer token
3451
* Automatically generates OpenAPI security scheme documentation
3552
*/
36-
export class BearerAuthenticationScheme extends AuthenticationScheme {
53+
export class BearerAuthenticationScheme extends InlineAuthenticationScheme {
3754
public readonly type = 'http' as const;
3855
public readonly schemeName: string;
3956

40-
private readonly bearerFormat: string;
41-
private readonly description?: string;
42-
private readonly middlewareOptions: BearerAuthenticationMiddlewareOptions;
57+
protected readonly bearerFormat: string;
58+
protected readonly description?: string;
59+
protected readonly checkToken: CheckTokenFunction;
60+
protected readonly log?: LogInterface;
4361

4462
constructor(options: BearerAuthenticationSchemeOptions) {
45-
super();
63+
super(options);
4664
this.schemeName = options.schemeName || 'BearerAuth';
4765
this.bearerFormat = options.bearerFormat || 'JWT';
4866
this.description = options.description;
49-
this.middlewareOptions = {
50-
checkToken: options.checkToken,
51-
log: options.log,
52-
};
67+
this.checkToken = options.checkToken;
68+
this.log = options.log;
5369
}
5470

5571
public getSecurityScheme(): SecuritySchemeObject {
@@ -66,7 +82,96 @@ export class BearerAuthenticationScheme extends AuthenticationScheme {
6682
return scheme;
6783
}
6884

69-
public getMiddleware(): RequestHandler {
70-
return bearerAuthenticationMiddleware(this.middlewareOptions);
85+
public override getMiddleware(): RequestHandler {
86+
return async (
87+
request: AuthenticatedRequest,
88+
response: ApiResponse,
89+
next: ApiNextFunction
90+
) => {
91+
const authHeader =
92+
request.headers['authorization'] ||
93+
request.headers['Authorization'];
94+
95+
if (!authHeader) {
96+
const msg =
97+
'Unauthorized access attempt from ' +
98+
request.ip +
99+
' - No token provided';
100+
this.log?.warn(msg);
101+
102+
throw new UnauthorizedError('Authorization header is missing', {
103+
scheme: 'Bearer',
104+
});
105+
}
106+
107+
const validationResult = await new BearerTokenValSan().run(
108+
authHeader
109+
);
110+
let token: string;
111+
112+
if (validationResult.success) {
113+
token = validationResult.data!;
114+
}
115+
else {
116+
// eslint-disable-next-line max-len
117+
const msg =
118+
'Unauthorized access attempt from ' +
119+
request.ip +
120+
' - Invalid token format';
121+
const details = JSON.stringify(
122+
validationResult.errors,
123+
null,
124+
2
125+
);
126+
this.log?.warn(msg, details);
127+
128+
throw new UnauthorizedError(
129+
'Bearer token is empty or invalid',
130+
{
131+
scheme: 'Bearer',
132+
details: validationResult.errors,
133+
}
134+
);
135+
}
136+
137+
if (!(await this.checkToken(token))) {
138+
// eslint-disable-next-line max-len
139+
const msg =
140+
'Unauthorized access attempt from ' +
141+
request.ip +
142+
' - Check token failed';
143+
this.log?.warn(msg);
144+
145+
throw new UnauthorizedError('Bearer token check failed', {
146+
scheme: 'Bearer',
147+
});
148+
}
149+
150+
request.authenticated = true;
151+
return next();
152+
};
153+
}
154+
155+
public async authenticate(credentials: string): Promise<void> {
156+
if (!credentials) {
157+
throw new UnauthorizedError('Auth token is missing', {
158+
scheme: 'Bearer',
159+
});
160+
}
161+
162+
const validationResult = await new BearerTokenValSan().run(credentials);
163+
164+
if (!validationResult.success) {
165+
throw new UnauthorizedError('Bearer token is empty or invalid', {
166+
scheme: 'Bearer',
167+
details: validationResult.errors,
168+
});
169+
}
170+
171+
if (!(await this.checkToken(credentials))) {
172+
throw new UnauthorizedError('Bearer token check failed', {
173+
scheme: 'Bearer',
174+
});
175+
}
71176
}
72177
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './router';
22
export * from './server';
33
export * from './error';
44
export * from './authentication';
5+
export * from './session';

0 commit comments

Comments
 (0)