Skip to content

Commit 0623c5c

Browse files
committed
feat: use endpoint classes for auth-steps; to take advantage of validation, open-api spec, etc
1 parent a899bf9 commit 0623c5c

4 files changed

Lines changed: 76 additions & 47 deletions

File tree

docs/authentication.md

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -251,16 +251,15 @@ class ApiKeyScheme extends InlineAuthenticationScheme {
251251

252252
```typescript
253253
import { SessionAuthenticationScheme, AuthFlow, AuthStep } from 'api-machine';
254-
import { ApiRequest, ApiResponse } from 'api-machine';
255254

256255
class OAuth2Scheme extends SessionAuthenticationScheme {
257256
getSecurityScheme() {
258257
return {
259258
type: 'oauth2' as const,
260259
flows: {
261260
authorizationCode: {
262-
authorizationUrl: 'https://provider.com/oauth/authorize',
263-
tokenUrl: 'https://provider.com/oauth/token',
261+
authorizationUrl: 'http://localhost:4001/auth/challenge',
262+
tokenUrl: 'http://localhost:4001/auth/token',
264263
scopes: { 'read:data': 'Read data' },
265264
},
266265
},
@@ -269,28 +268,37 @@ class OAuth2Scheme extends SessionAuthenticationScheme {
269268

270269
getAuthFlow(): AuthFlow {
271270
return {
272-
challenge: {
273-
description: 'Generate authorization challenge (PKCE)',
274-
async handle(request: ApiRequest, response: ApiResponse) {
271+
// Challenge step
272+
challenge: class extends AuthStep {
273+
override path = '/challenge';
274+
override description = 'Generate authorization challenge (PKCE)';
275+
276+
override async handle() {
275277
const challenge = generateChallenge();
276278
return { challenge };
277-
},
279+
}
278280
},
279-
authorization: {
280-
description: 'User authorizes application at OAuth2 provider',
281-
async handle(request: ApiRequest, response: ApiResponse) {
281+
// Authorization step
282+
authorization: class extends AuthStep {
283+
override path = '/authorize';
284+
override description = 'User authorizes application at OAuth2 provider';
285+
286+
override async handle(request) {
282287
// User is redirected to provider, returns with authorization code
283288
const code = request.query.code as string;
284289
return { code };
285-
},
290+
}
286291
},
287-
tokenExchange: {
288-
description: 'Exchange authorization code for access token and session',
289-
async handle(request: ApiRequest, response: ApiResponse) {
292+
// Token exchange step
293+
tokenExchange: class extends AuthStep {
294+
override path = '/token';
295+
override description = 'Exchange authorization code for access token and session';
296+
297+
override async handle(request) {
290298
const accessToken = await exchangeCodeForToken(request.body.code);
291299
const session = await createSession(accessToken);
292300
return { session };
293-
},
301+
}
294302
},
295303
};
296304
}
@@ -306,7 +314,41 @@ After a session is obtained through the auth flow, each request:
306314

307315
## AuthFlow & AuthStep
308316

309-
An `AuthFlow` is a named collection of `AuthStep` objects representing the complete multi-step authentication process used to obtain a session. See [src/authentication/auth-flow.ts](../src/authentication/auth-flow.ts) and [src/authentication/auth-step.ts](../src/authentication/auth-step.ts) for type definitions.
317+
An `AuthFlow` is a named collection of `AuthStep` classes representing the complete multi-step authentication process used to obtain a session.
318+
319+
**AuthStep** is an abstract class that extends `BaseApiEndpoint`, allowing each step to leverage endpoint features:
320+
- Request validation (body, query, params, headers via valsan)
321+
- Middleware support
322+
- Standardized error handling
323+
- OpenAPI integration
324+
325+
See [src/authentication/auth-step.ts](../src/authentication/auth-step.ts) and [src/authentication/auth-flow.ts](../src/authentication/auth-flow.ts) for type definitions.
326+
327+
**Typical step responsibilities:**
328+
- `challenge` - Generate a random challenge or nonce (e.g., PKCE code challenge)
329+
- `authorization` - Handle user interaction (redirect to OAuth provider, login form, etc.)
330+
- `tokenExchange` - Exchange credentials for tokens and create a session
331+
332+
**Step example with validation:**
333+
```typescript
334+
class TokenExchangeStep extends AuthStep {
335+
override path = '/token';
336+
override description = 'Exchange code for tokens';
337+
338+
// Define request validation just like endpoints
339+
override body = new ObjectValSan({
340+
schema: {
341+
code: new StringValidator(),
342+
grantType: new StringValidator(),
343+
}
344+
});
345+
346+
override async handle(request) {
347+
// request.body is validated
348+
return { accessToken: 'token123' };
349+
}
350+
}
351+
```
310352

311353
## Public Routes
312354

src/authentication/auth-flow.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ import { AuthStep } from './auth-step';
33
/**
44
* A named collection of authentication steps
55
* Each step is identified by a key
6-
* (e.g., 'challenge', 'authorization', 'tokenExchange')
6+
* (e.g., 'challenge', 'authorization', 'tokenExchange')
7+
*
8+
* Steps are classes extending AuthStep (which extends BaseApiEndpoint)
9+
* This allows each step to use endpoint features like validation and middleware
710
*
811
* The flow represents the entire authentication process
912
* Steps are executed in the order they're defined
1013
*/
11-
export type AuthFlow = Record<string, AuthStep>;
14+
export type AuthFlow = Record<string, typeof AuthStep>;

src/authentication/auth-step.ts

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,6 @@
1-
import { ApiRequest, ApiResponse } from '../router';
1+
import { BaseApiEndpoint } from '../router';
22

33
/**
4-
* Represents a single step in an authentication flow
5-
* Can be used for inline schemes (as middleware)
6-
* or session schemes (as endpoint)
4+
* Abstract base class for authentication flow steps
75
*/
8-
export interface AuthStep {
9-
/**
10-
* Human-readable description
11-
*/
12-
description?: string;
13-
14-
/**
15-
* Execute this authentication step
16-
* Called when processing requests for this step
17-
*
18-
* @param request - The incoming request
19-
* @returns Result indicating success/failure
20-
* @throws HTTPError for authentication failures
21-
*/
22-
handle(
23-
request: ApiRequest,
24-
response: ApiResponse
25-
): Promise<Record<string, unknown>>;
26-
}
6+
export abstract class AuthStep extends BaseApiEndpoint {}

test/spec/authentication/session-scheme.spec.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'jasmine';
22
import {
33
SessionAuthenticationScheme,
44
AuthFlow,
5+
AuthStep,
56
} from '../../../src/authentication';
67
import { InMemorySessionDriver } from '../../../src/session';
78
import { SessionDriver } from '../../../src/session';
@@ -17,6 +18,9 @@ type AnyResponse = any;
1718
// eslint-disable-next-line @typescript-eslint/no-unused-vars
1819
abstract class TestSessionDriverImport extends SessionDriver {}
1920

21+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
22+
abstract class TestAuthFlowImport extends AuthStep {}
23+
2024
/**
2125
* Test implementation of SessionAuthenticationScheme
2226
*/
@@ -35,11 +39,11 @@ class TestSessionScheme extends SessionAuthenticationScheme {
3539

3640
public getAuthFlow(): AuthFlow {
3741
return {
38-
testStep: {
39-
description: 'Test step',
42+
testStep: class extends AuthStep {
43+
override description = 'Test step';
4044
async handle() {
4145
return {};
42-
},
46+
}
4347
},
4448
};
4549
}
@@ -252,11 +256,11 @@ describe('SessionAuthenticationScheme', () => {
252256

253257
public getAuthFlow(): AuthFlow {
254258
return {
255-
customStep: {
256-
description: 'Custom step',
259+
customStep: class extends AuthStep {
260+
override description = 'Custom step';
257261
async handle() {
258262
return {};
259-
},
263+
}
260264
},
261265
};
262266
}

0 commit comments

Comments
 (0)