Skip to content

Commit cf73f8b

Browse files
committed
chore: implement auth middleware and schema refactor
1 parent 4028a49 commit cf73f8b

11 files changed

Lines changed: 194 additions & 180 deletions

File tree

e2e/mock-api-v2/README.md

Lines changed: 55 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,87 @@
1-
### Mock Api
1+
A mock API server for simulating Ping Identity and OpenID Connect flows, built using [Effect](https://effect.website/) and [@effect/platform](https://github.com/Effect-TS/platform). This service is designed for end-to-end testing and development, providing realistic responses for authentication and authorization scenarios.
22

3-
## Docs
3+
## Features
44

5-
you can run the server and visit `http://localhost:9443/docs#` to visit the swagger docs
6-
The swagger docs are automatically created (with effect-http) by way of the schemas that are defined in the [spec file]('./src/spec.ts')
5+
- Implements endpoints for:
76

8-
## Creating an endpoint in the Api specification
7+
- Healthcheck
8+
- OpenID Connect Discovery
9+
- Davinci Authorization
10+
- Token acquisition
11+
- UserInfo (protected, requires Bearer token)
912

10-
To create an endpoint, visit the [spec]('./src/spec.ts') file and add your endpoint. For organization and cleanliness, endpoints can be abstracted into the [endpoints]('./src/endpoints/') folder.
13+
- Uses Effect and @effect/platform for functional, type-safe API definition and handling.
14+
- Includes middleware for logging, CORS, cookie management, and Bearer token authorization.
1115

12-
When an endpoint is made, you can add it to the [`spec`]('./src/spec.ts') `pipe`.
16+
## Tech Stack
1317

14-
## Handling your new endpoint
18+
- [Effect](https://effect.website/)
19+
- [@effect/platform](https://github.com/Effect-TS/platform)
20+
- Node.js (via @effect/platform-node)
21+
- TypeScript
1522

16-
When you have created an endpoint in the specification, you need to now handle the endpoint. This is the actual code implementation of your endpoint.
23+
## Getting Started
1724

18-
handlers are saved in the [handlers]('./src/handlers/') folder.
25+
### Install dependencies
1926

20-
You use RouterBuilder.handler, passing in the [api spec]('./src/spec.ts') and the name of the route, and then an Effect returning function (`Effect.gen` is the simplest form of this).
21-
22-
The request arguments, you define in your endpoint specification, (query params, path params, request bodies, etc) are the arguments passed to the callback function of `RouterBuilder.handler`.
23-
24-
Ensure that you also add your `handler` to the RouterBuilder.handle [here]('./src/main.ts');
25-
26-
## Adding a journey / flow to the response map
27-
28-
If you are adding a flow to the response map the first thing to do is open up [responseMap]('./src/responses/index.ts')
29-
30-
This file is a `map` of Names -> Array<Step> where a Step is the response you want to return (in order). Order is key here.
31-
32-
If the Response you want to return is not already defined as a schema, you will have to define a new Schema and add the response.
33-
34-
A schema is defined in the schemas folder [here]('./src/schemas/');
35-
A response is defined in the responses folder [here]('./src/responses/')
36-
37-
This is still a work in progress in terms of making it more scalable.
38-
39-
## Validating your code in Test
40-
41-
After adding a journey/flow to the response map and defining a schema, you next want to have some validation on the submitted request. You can do this by adding it to the `validator` function [here]('./src/helpers/match.ts');
42-
43-
This functions job is to `match` the type passed in, and validate based on the condition provided. If it passes, a boolean is returned, if it fails, a new Error should be returned.
44-
45-
## Services
46-
47-
To make it so types line up easier, each route, has a service dedicated to itself. The service under the hood, uses the `Requester` service. The `Requester` service is to mimic a call to the authorization server.
48-
49-
Let's look at the `Authorize` service. This service is the workhorse of the `authorize` handler.
50-
51-
`Authorize`, the service, uses `Requester` which will fetch a response from the authorization server.
52-
53-
After retrieving the response, the service will catch any errors that may be thrown, and mold them into HttpErrors to respond back to the client.
54-
55-
In a mock environment, rather than fetching from the client, authorization service will grab the next response from the `responseMap`.
27+
```sh
28+
pnpm install
29+
```
5630

57-
In a live environment, it will forward a request to the Fetch service, and return that response.
31+
### Build
5832

59-
## Creating Errors
33+
```sh
34+
pnpm build
35+
```
6036

61-
If you want to create an error, it is simple. This is the skeleton of how to create an Error in `Effect`
37+
### Run the server
6238

63-
```
64-
class MyErrorName {
65-
readonly _tag = 'MyErrorName'
66-
}
39+
```sh
40+
pnpm serve
6741
```
6842

69-
The `_tag` is important as this is the name of the error, and how we can `catchTags` in our error handling. For simplicity, you can name is the same as your error class.
43+
The server will start on port `9443`.
7044

71-
## Handling Errors
45+
### Run tests
7246

73-
We want to return our errors back to the client, but typically we need an error response body that informs the client of the issue.
47+
```sh
48+
pnpm test
49+
```
7450

75-
You should add your error responses to the response folder [here]('./src/responses');
51+
## API Overview
7652

77-
In the service where you want to handle your error, you will see a `catchTags` function.
53+
### Healthcheck
7854

79-
Let's pause here to understand the `Effect` type.
55+
- `GET /healthcheck`
56+
- Returns `"Healthy"` if the server is running.
8057

81-
```ts
82-
Effect<Success, Error, Requirements>;
83-
```
58+
### OpenID Connect Discovery
8459

85-
When reading an Effect type, the first generic, is what is returned if the effect is successful.
60+
- `GET /:envid/as/.well-known/openid-configuration`
61+
- Returns a static OpenID configuration response.
8662

87-
The second argument is what is returned if the effect is unsuccessful.
63+
### Davinci Authorization
8864

89-
The third argument is any services (or layers) that are required to run this effect.
65+
- `GET /:envid/as/authorize`
66+
- Accepts query parameters for authorization.
67+
- Returns a mock authorization response.
9068

91-
So if we have an `effect` like this `Effect<Users, HttpError.HttpError | NoSuchElementException, never>`
69+
### Token Endpoint
9270

93-
This tells us the `effect` returns `users`, and can error two ways, `NoSuchElementException`, and with an `HttpError`.
71+
- `POST /:envid/as/token`
72+
- Returns a mock access token response.
9473

95-
We would rather handle this `NoSuchElementException` and send back to the client an HttpError informing them of the error that occurred.
74+
### UserInfo (Protected)
9675

97-
We can do something like this now
76+
- `GET /:envid/as/userinfo`
77+
- Requires a valid Bearer token.
78+
- Returns mock user information.
9879

99-
```ts
100-
Effect.catchTag('NoSuchElementException', () =>
101-
HttpError.unauthorizedError('no such element found'),
102-
);
103-
```
80+
## Notes
10481

105-
This will return a 401 with that message.
82+
- The `customHtmlRoutes` and related endpoints are not currently implemented.
83+
- All endpoints return static or mock data for testing purposes.
10684

107-
When handling errors, we try to keep the handler always returning an `HttpError`, so we should handle any other errors we have deeper in the call stack, to return HttpError unless there is a valid reason to allow the error to bubble up.
85+
## License
10886

109-
If you have a shape of an error that you want to return from a handler, that does not match the current schema, you can add it to the `api spec`.
87+
MIT © Ping Identity Corporation

e2e/mock-api-v2/src/errors/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class FetchError {
1515
class InvalidProtectNode {
1616
readonly _tag = 'InvalidProtectNode';
1717
}
18+
1819
class UnableToFindNextStep {
1920
readonly _tag = 'UnableToFindNextStep';
2021
}

e2e/mock-api-v2/src/handlers/userinfo.handler.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,18 @@ import { Effect } from 'effect';
88
import { MockApi } from '../spec.js';
99
import { UserInfo } from '../services/userinfo.service.js';
1010
import { HttpApiBuilder } from '@effect/platform';
11+
import { BearerToken } from '../middleware/Authorization.js';
1112

1213
/**
1314
* TODO: Need to implement an Authorization middleware
1415
*/
1516
const UserInfoMockHandler = HttpApiBuilder.group(MockApi, 'Protected Requests', (handlers) =>
1617
handlers.handle('UserInfo', () =>
1718
Effect.gen(function* () {
19+
const authToken = yield* BearerToken;
1820
const { getUserInfo } = yield* UserInfo;
1921

20-
const response = yield* getUserInfo;
22+
const response = yield* getUserInfo(authToken);
2123

2224
return response;
2325
}),

e2e/mock-api-v2/src/main.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { TokensMock } from './services/tokens.service.js';
1818
import { TokensHandler } from './handlers/token.handler.js';
1919
import { UserInfoMockHandler } from './handlers/userinfo.handler.js';
2020
import { UserInfoMockService } from './services/userinfo.service.js';
21+
import { AuthorizationMock } from './middleware/Authorization.js';
2122

2223
const APIMock = HttpApiBuilder.api(MockApi).pipe(
2324
Layer.provide(HealthCheckLive),
@@ -34,7 +35,7 @@ const ServerMock = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
3435
Layer.provide(TokensMock),
3536
Layer.provide(UserInfoMockService),
3637
Layer.provide(IncrementStepIndexMock),
37-
// Layer.provide(AuthorizationLive),
38+
Layer.provide(AuthorizationMock),
3839
Layer.provide(HttpApiBuilder.middlewareCors()),
3940
HttpServer.withLogAddress,
4041
Layer.provide(NodeHttpServer.layer(createServer, { port: 9443 })),
Lines changed: 43 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,43 @@
1-
// import { Unauthorized } from '@effect/platform/HttpApiError';
2-
// import { UserInfoTagged } from '../schemas/userinfo/userinfo.schema.js';
3-
// import { HttpApiMiddleware, HttpApiSecurity } from '@effect/platform';
4-
// import { Effect, Layer, Redacted } from 'effect';
5-
//
6-
// class Authorization extends HttpApiMiddleware.Tag<Authorization>()('Authorization', {
7-
// // Define the error schema for unauthorized access
8-
// failure: Unauthorized,
9-
// // Specify the resource this middleware will provide
10-
// provides: UserInfoTagged,
11-
// // Add security definitions
12-
// security: {
13-
// // ┌─── Custom name for the security definition
14-
// // ▼
15-
// myBearer: HttpApiSecurity.bearer,
16-
// // Additional security definitions can be added here.
17-
// // They will attempt to be resolved in the order they are defined.
18-
// },
19-
// }) {}
20-
//
21-
// const AuthorizationLive = Layer.effect(
22-
// Authorization,
23-
// Effect.gen(function* () {
24-
// yield* Effect.log('creating Authorization middleware');
25-
//
26-
// // Return the security handlers for the middleware
27-
// return {
28-
// // Define the handler for the Bearer token
29-
// // The Bearer token is redacted for security
30-
// myBearer: (bearerToken) =>
31-
// Effect.gen(function* () {
32-
// yield* Effect.log('checking bearer token', Redacted.value(bearerToken));
33-
//
34-
// // Pass through bearer token for future requests?
35-
// return bearerToken;
36-
// }),
37-
// };
38-
// }),
39-
// );
40-
//
41-
// export { Authorization, AuthorizationLive };
1+
import { Unauthorized } from '@effect/platform/HttpApiError';
2+
import { HttpApiMiddleware, HttpApiSecurity } from '@effect/platform';
3+
import { Brand, Context, Effect, Layer, Redacted } from 'effect';
4+
5+
type BearerTokenValue = string & Brand.Brand<'BearerToken'>;
6+
const BearerTokenValue = Brand.nominal<BearerTokenValue>();
7+
8+
// Define a service that holds the bearer token
9+
class BearerToken extends Context.Tag('BearerToken')<BearerToken, BearerTokenValue>() {}
10+
11+
class Authorization extends HttpApiMiddleware.Tag<Authorization>()('Authorization', {
12+
failure: Unauthorized,
13+
provides: BearerToken, // Declare that this middleware provides the bearer token
14+
security: {
15+
myBearer: HttpApiSecurity.bearer,
16+
},
17+
}) {}
18+
19+
const AuthorizationMock = Layer.effect(
20+
Authorization,
21+
Effect.gen(function* () {
22+
yield* Effect.log('creating Authorization middleware');
23+
24+
return {
25+
myBearer: (bearerToken) =>
26+
Effect.gen(function* () {
27+
const tokenValue = Redacted.value(bearerToken);
28+
yield* Effect.log('checking bearer token', tokenValue);
29+
30+
// Here you could add validation logic if needed
31+
// For now, we just pass through any token
32+
if (!tokenValue || tokenValue.trim() === '') {
33+
return yield* Effect.fail(new Unauthorized());
34+
}
35+
36+
// Return the token value so routes can access it
37+
return BearerTokenValue(tokenValue);
38+
}),
39+
};
40+
}),
41+
);
42+
43+
export { Authorization, AuthorizationMock, BearerToken };

e2e/mock-api-v2/src/schemas/custom-html-template/custom-html-template-request.schema.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,12 @@
55
* of the MIT license. See the LICENSE file for details.
66
*/
77
import { Schema } from 'effect';
8+
import { FormDataResponseUsernamePassword, PingProtectSDKResponse } from './form-data.schema.js';
89

910
/**
1011
* Schemas of what FormData may look like in a Ping Request
1112
*
1213
*/
13-
const FormDataResponseUsernamePassword = Schema.Struct({
14-
username: Schema.String,
15-
password: Schema.String,
16-
});
17-
18-
const PingProtectSDKResponse = Schema.Struct({ pingprotectsdk: Schema.String });
19-
2014
const PossibleFormDatas = Schema.Struct({
2115
value: Schema.Union(FormDataResponseUsernamePassword, PingProtectSDKResponse),
2216
});

e2e/mock-api-v2/src/schemas/custom-html-template/custom-html-template-response.schema.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,10 @@
55
* of the MIT license. See the LICENSE file for details.
66
*/
77
import { Schema } from 'effect';
8+
import { ProtectSDKRequestFormData, UsernamePasswordFormData } from './form-data.schema.js';
89

910
const PingOnePathParams = Schema.Struct({ envid: Schema.String, connectionid: Schema.String });
1011

11-
const ProtectSDKRequestFormData = Schema.Struct({
12-
value: Schema.Struct({
13-
protectsdk: Schema.String,
14-
}),
15-
});
16-
17-
const UsernamePasswordFormData = Schema.Struct({
18-
value: Schema.Struct({
19-
username: Schema.String,
20-
password: Schema.String,
21-
}),
22-
});
23-
2412
const PossibleFormDatas = Schema.Union(ProtectSDKRequestFormData, UsernamePasswordFormData);
2513

2614
const _PingOneCustomHtmlResponseBody = Schema.Struct({
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
3+
*
4+
* This software may be modified and distributed under the terms
5+
* of the MIT license. See the LICENSE file for details.
6+
*/
7+
import { Schema } from 'effect';
8+
9+
const FormDataResponseUsernamePassword = Schema.Struct({
10+
username: Schema.String,
11+
password: Schema.String,
12+
});
13+
14+
const PingProtectSDKResponse = Schema.Struct({ pingprotectsdk: Schema.String });
15+
16+
const ProtectSDKRequestFormData = Schema.Struct({
17+
value: Schema.Struct({
18+
protectsdk: Schema.String,
19+
}),
20+
});
21+
22+
const UsernamePasswordFormData = Schema.Struct({
23+
value: Schema.Struct({
24+
username: Schema.String,
25+
password: Schema.String,
26+
}),
27+
});
28+
29+
export {
30+
FormDataResponseUsernamePassword,
31+
PingProtectSDKResponse,
32+
ProtectSDKRequestFormData,
33+
UsernamePasswordFormData,
34+
};

e2e/mock-api-v2/src/schemas/userinfo/userinfo.schema.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This software may be modified and distributed under the terms
55
* of the MIT license. See the LICENSE file for details.
66
*/
7-
import { Context, Schema } from 'effect';
7+
import { Schema } from 'effect';
88

99
class UserInfoSchema extends Schema.Class<UserInfoSchema>('UserInfo')(
1010
Schema.Struct({
@@ -20,6 +20,4 @@ class UserInfoSchema extends Schema.Class<UserInfoSchema>('UserInfo')(
2020
}),
2121
) {}
2222

23-
class UserInfoTagged extends Context.Tag('UserInfoTagged')<UserInfoTagged, UserInfoSchema>() {}
24-
25-
export { UserInfoTagged, UserInfoSchema };
23+
export { UserInfoSchema };

0 commit comments

Comments
 (0)