Skip to content

Commit 41ec7bd

Browse files
committed
example: added authentication examples
1 parent 64983d5 commit 41ec7bd

8 files changed

Lines changed: 714 additions & 8 deletions

File tree

examples/complete-example/README.md

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,51 @@ curl "http://localhost:3000/api/express/search?q=test&page=2&limit=20"
7272
curl http://localhost:3000/api/health
7373
```
7474

75+
### Bearer Token Authentication (Inline, Stateless)
76+
77+
Bearer authentication validates a token on every request without maintaining session state.
78+
79+
```bash
80+
# Test protected endpoint with valid token
81+
curl -H "Authorization: Bearer demo-token" \
82+
http://localhost:3000/api/bearer/protected
83+
84+
# Test without token (should return 401)
85+
curl http://localhost:3000/api/bearer/protected
86+
87+
# Public endpoint in bearer router (no auth required)
88+
curl http://localhost:3000/api/bearer/public
89+
```
90+
91+
### OAuth2 Session-Based Authentication (Stateful)
92+
93+
Session-based authentication uses a multi-step OAuth2-like flow to obtain a session, then validates it on each request.
94+
95+
```bash
96+
# Step 1: Get a challenge
97+
CHALLENGE=$(curl -s -X POST http://localhost:3000/oauth/challenge \
98+
-H "Content-Type: application/json" -d '{}' | \
99+
grep -o '"challenge":"[^"]*"' | cut -d'"' -f4)
100+
101+
# Step 2: Authorize with credentials
102+
CODE=$(curl -s -X POST http://localhost:3000/oauth/authorize \
103+
-H "Content-Type: application/json" \
104+
-d "{\"username\": \"demo\", \"password\": \"demo\", \"challenge\": \"$CHALLENGE\"}" | \
105+
grep -o '"code":"[^"]*"' | cut -d'"' -f4)
106+
107+
# Step 3: Exchange code for session
108+
SESSIONID=$(curl -s -X POST http://localhost:3000/oauth/token \
109+
-H "Content-Type: application/json" \
110+
-d "{\"code\": \"$CODE\"}" | \
111+
grep -o '"sessionId":"[^"]*"' | cut -d'"' -f4)
112+
113+
# Step 4: Access protected resource with session
114+
curl -s -H "Cookie: sessionId=$SESSIONID" \
115+
http://localhost:3000/oauth/protected && echo ""
116+
117+
# Demo credentials: demo/demo or admin/admin
118+
```
119+
75120
## Key Points
76121

77122
### Domain-Driven Organization
@@ -120,6 +165,7 @@ complete-example/
120165
├── index.ts # Entry point
121166
├── server.ts # Server with custom logger
122167
├── router.ts # Main router (groups domain routers)
168+
├── authentication.ts # Bearer and OAuth2 auth schemes
123169
├── users/ # User management domain
124170
│ ├── users-router.ts # Groups under /api/users
125171
│ ├── list-users-endpoint.ts # GET /api/users
@@ -143,4 +189,24 @@ After completing the quick-start example, this example demonstrates:
143189
5. How to validate input and handle errors gracefully
144190
6. How to configure and use custom loggers
145191
7. How to leverage Express features within your endpoints
146-
8. Best practices for structuring a production-ready API
192+
8. How to implement Bearer token authentication (inline, stateless)
193+
9. How to implement OAuth2 session-based authentication (stateful, multi-step)
194+
10. Best practices for structuring a production-ready API with authentication
195+
196+
## Authentication Approaches
197+
198+
This example demonstrates two authentication approaches:
199+
200+
### Bearer Token (Inline) Authentication
201+
- **When to use**: Stateless APIs, JWT validation, API key authentication
202+
- **How it works**: Token is validated on every request
203+
- **Credentials**: Bearer token in Authorization header
204+
- **Session**: None - token itself is the credential
205+
- **Example**: `authentication.ts` - `BearerAuthRouter`
206+
207+
### OAuth2 Session-Based Authentication
208+
- **When to use**: Stateful flows, user login, multi-factor auth
209+
- **How it works**: Multi-step flow to obtain session, then session is validated
210+
- **Credentials**: Username/password exchanged for session
211+
- **Session**: Server-side session ID in cookies
212+
- **Example**: `authentication.ts` - `OAuth2Router` and `oauth2Scheme`
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* Inline (Bearer Token) Authentication
3+
*
4+
* This module demonstrates stateless, token-based authentication.
5+
* Token validation occurs on every request without session storage.
6+
*/
7+
8+
import {
9+
BaseApiRouter,
10+
BaseApiEndpoint,
11+
BearerAuthenticationScheme,
12+
ApiRequest,
13+
ApiResponse,
14+
} from '../../../src/index';
15+
16+
/**
17+
* Bearer authentication for simple token-based APIs
18+
* Token validation on every request
19+
*/
20+
export const bearerAuth = new BearerAuthenticationScheme({
21+
checkToken: async (token: string) => {
22+
// In production: validate JWT signature, check expiration, etc.
23+
const validTokens = ['demo-token', 'admin-token', 'user-token'];
24+
return validTokens.includes(token);
25+
},
26+
schemeName: 'BearerAuth',
27+
bearerFormat: 'JWT',
28+
description: 'Bearer token authentication',
29+
});
30+
31+
/**
32+
* Protected endpoint that requires Bearer token authentication
33+
*/
34+
export class BearerProtectedEndpoint extends BaseApiEndpoint {
35+
override path = '/protected';
36+
override description = 'Protected resource requiring Bearer token';
37+
38+
async handle(request: ApiRequest, response: ApiResponse) {
39+
return {
40+
message: 'You accessed a protected resource',
41+
timestamp: new Date().toISOString(),
42+
};
43+
}
44+
}
45+
46+
/**
47+
* Public endpoint in the Bearer router (no auth required)
48+
*/
49+
export class BearerPublicEndpoint extends BaseApiEndpoint {
50+
override path = '/public';
51+
override description = 'Public resource (no authentication)';
52+
override authentication = null;
53+
54+
async handle(request: ApiRequest, response: ApiResponse) {
55+
return {
56+
message: 'Public data',
57+
authType: 'Bearer Token',
58+
};
59+
}
60+
}
61+
62+
/**
63+
* Router for Bearer token authentication endpoints
64+
*/
65+
export class BearerAuthRouter extends BaseApiRouter {
66+
override path = '/bearer';
67+
override description = 'Bearer token authentication endpoints';
68+
override authentication = bearerAuth;
69+
70+
async routes() {
71+
return [BearerProtectedEndpoint, BearerPublicEndpoint];
72+
}
73+
}
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
/**
2+
* Session-Based (OAuth2-like) Authentication
3+
*
4+
* This module demonstrates stateful, multi-step authentication.
5+
* Implements a challenge-based flow similar to OAuth2 authorization code flow.
6+
*/
7+
8+
import {
9+
BaseApiRouter,
10+
BaseApiEndpoint,
11+
SessionAuthenticationScheme,
12+
AuthFlow,
13+
AuthStep,
14+
InMemorySessionDriver,
15+
ApiRequest,
16+
ApiResponse,
17+
UnauthorizedError,
18+
} from '../../../src/index';
19+
import { EndpointMethod } from '../../../src/router/endpoint';
20+
import { SecuritySchemeObject } from 'auto-oas/oas/v3.1';
21+
22+
// Simple in-memory storage for demonstration
23+
const storedChallenges = new Map<string, { timestamp: number }>();
24+
const storedAuthCodes = new Map<
25+
string,
26+
{ username: string; timestamp: number }
27+
>();
28+
29+
/**
30+
* Session-based OAuth2-like authentication scheme
31+
*/
32+
class OAuth2Scheme extends SessionAuthenticationScheme {
33+
public readonly schemeName = 'OAuth2';
34+
public readonly type = 'oauth2' as const;
35+
36+
constructor() {
37+
super({ sessionDriver: new InMemorySessionDriver() });
38+
}
39+
40+
public getSecurityScheme(): SecuritySchemeObject {
41+
return {
42+
type: 'oauth2' as const,
43+
flows: {
44+
authorizationCode: {
45+
authorizationUrl: 'http://localhost:3000/oauth/authorize',
46+
tokenUrl: 'http://localhost:3000/oauth/token',
47+
scopes: { 'read:data': 'Read data' },
48+
},
49+
},
50+
};
51+
}
52+
53+
public getAuthFlow(): AuthFlow {
54+
const sessionDriver = this.sessionDriver;
55+
56+
/**
57+
* Challenge step - generates a random challenge for CSRF prevention
58+
*/
59+
class ChallengeStep extends AuthStep {
60+
override path = '/challenge';
61+
override method = EndpointMethod.POST;
62+
override description = 'Generate authentication challenge';
63+
64+
async handle(request: ApiRequest, response: ApiResponse) {
65+
const challenge =
66+
Math.random().toString(36).substring(2, 15) +
67+
Math.random().toString(36).substring(2, 15);
68+
storedChallenges.set(challenge, { timestamp: Date.now() });
69+
return { challenge, expiresIn: 300 };
70+
}
71+
}
72+
73+
/**
74+
* Authorization step - validates credentials
75+
*/
76+
class AuthorizationStep extends AuthStep {
77+
override path = '/authorize';
78+
override method = EndpointMethod.POST;
79+
override description = 'Authorize with username and password';
80+
81+
async handle(request: ApiRequest, response: ApiResponse) {
82+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
83+
const { username, password, challenge } = request.body as any;
84+
85+
if (!username || !password || !challenge) {
86+
throw new UnauthorizedError('Missing required fields');
87+
}
88+
89+
const storedChallenge = storedChallenges.get(challenge);
90+
if (!storedChallenge) {
91+
throw new UnauthorizedError('Invalid or expired challenge');
92+
}
93+
94+
if (Date.now() - storedChallenge.timestamp > 5 * 60 * 1000) {
95+
storedChallenges.delete(challenge);
96+
throw new UnauthorizedError('Challenge expired');
97+
}
98+
99+
// Demo: accept demo/demo or admin/admin
100+
const validUsers = [
101+
{ username: 'demo', password: 'demo' },
102+
{ username: 'admin', password: 'admin' },
103+
];
104+
105+
const user = validUsers.find(
106+
(u) => u.username === username && u.password === password
107+
);
108+
109+
if (!user) {
110+
throw new UnauthorizedError('Invalid credentials');
111+
}
112+
113+
const code =
114+
'code-' +
115+
Math.random().toString(36).substring(2, 15) +
116+
Math.random().toString(36).substring(2, 15);
117+
118+
storedAuthCodes.set(code, {
119+
username: user.username,
120+
timestamp: Date.now(),
121+
});
122+
storedChallenges.delete(challenge);
123+
124+
return { code, expiresIn: 60 };
125+
}
126+
}
127+
128+
/**
129+
* Token exchange step - exchanges code for session
130+
*/
131+
class TokenStep extends AuthStep {
132+
override path = '/token';
133+
override method = EndpointMethod.POST;
134+
override description = 'Exchange authorization code for session';
135+
136+
async handle(request: ApiRequest, response: ApiResponse) {
137+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
138+
const { code } = request.body as any;
139+
140+
if (!code) {
141+
throw new UnauthorizedError('Missing authorization code');
142+
}
143+
144+
const storedCode = storedAuthCodes.get(code);
145+
if (!storedCode) {
146+
throw new UnauthorizedError('Invalid or expired code');
147+
}
148+
149+
if (Date.now() - storedCode.timestamp > 1 * 60 * 1000) {
150+
storedAuthCodes.delete(code);
151+
throw new UnauthorizedError('Code expired');
152+
}
153+
154+
const sessionId =
155+
'session-' +
156+
Math.random().toString(36).substring(2, 15) +
157+
Math.random().toString(36).substring(2, 15);
158+
159+
// Create session in the driver
160+
sessionDriver.createSession({
161+
id: sessionId,
162+
username: storedCode.username,
163+
createdAt: new Date(),
164+
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
165+
// TODO: Fix session type
166+
// eslint-disable-next-line max-len
167+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
168+
} as any);
169+
170+
response.cookie('sessionId', sessionId, {
171+
httpOnly: true,
172+
maxAge: 24 * 60 * 60 * 1000,
173+
});
174+
175+
storedAuthCodes.delete(code);
176+
177+
return {
178+
sessionId,
179+
username: storedCode.username,
180+
expiresIn: 86400,
181+
};
182+
}
183+
}
184+
185+
return {
186+
challenge: ChallengeStep,
187+
authorize: AuthorizationStep,
188+
token: TokenStep,
189+
};
190+
}
191+
}
192+
193+
/**
194+
* Session-based authentication scheme instance
195+
*/
196+
export const oauth2Scheme = new OAuth2Scheme();
197+
198+
/**
199+
* Protected endpoint requiring session authentication
200+
*/
201+
export class SessionProtectedEndpoint extends BaseApiEndpoint {
202+
override path = '/protected';
203+
override description = 'Protected resource requiring session';
204+
override authentication = oauth2Scheme;
205+
206+
async handle(request: ApiRequest, response: ApiResponse) {
207+
return {
208+
message: 'You accessed a session-protected resource',
209+
timestamp: new Date().toISOString(),
210+
};
211+
}
212+
}
213+
214+
/**
215+
* Router for session-based OAuth2 authentication
216+
*/
217+
export class OAuth2Router extends BaseApiRouter {
218+
override path = '/oauth';
219+
override description = 'OAuth2 session-based authentication';
220+
// Don't apply authentication to the auth steps themselves
221+
override authentication = null;
222+
223+
async routes() {
224+
// Return both auth flow steps and protected endpoint
225+
const endpoints = oauth2Scheme.getEndpoints();
226+
227+
// Protected endpoint requires oauth2 authentication
228+
class OAuth2ProtectedEndpoint extends SessionProtectedEndpoint {
229+
override authentication = oauth2Scheme;
230+
}
231+
232+
return [...endpoints, OAuth2ProtectedEndpoint];
233+
}
234+
}

0 commit comments

Comments
 (0)