Skip to content

Commit 83772e6

Browse files
committed
chore: end-session and revoke
1 parent df6f85b commit 83772e6

13 files changed

Lines changed: 336 additions & 32 deletions

File tree

e2e/mock-api-v2/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ A mock API server for simulating Ping Identity and OpenID Connect flows, built u
33
## Features
44

55
- Implements endpoints for:
6-
76
- Healthcheck
87
- OpenID Connect Discovery
98
- Davinci Authorization
@@ -77,6 +76,13 @@ pnpm test
7776
- Requires a valid Bearer token.
7877
- Returns mock user information.
7978

79+
### End Session
80+
81+
- `GET /:envid/as/endSession`
82+
- Ends the user's session.
83+
- Accepts `post_logout_redirect_uri` and `state` query parameters for redirects.
84+
- Returns a confirmation message.
85+
8086
## Notes
8187

8288
- The `customHtmlRoutes` and related endpoints are not currently implemented.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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 { Effect, Console } from 'effect';
8+
import { HttpApiBuilder, HttpServerRequest } from '@effect/platform';
9+
import { MockApi } from '../spec.js';
10+
import { SessionStorage } from '../services/session.service.js';
11+
12+
export const EndSessionHandlerMock = HttpApiBuilder.group(
13+
MockApi,
14+
'SessionManagement',
15+
(handlers) =>
16+
handlers.handle('EndSession', () =>
17+
Effect.gen(function* () {
18+
const sessionStorage = yield* SessionStorage;
19+
20+
const request = yield* HttpServerRequest.HttpServerRequest;
21+
22+
const sessionId = request.cookies.sessionId;
23+
24+
if (sessionId) {
25+
yield* sessionStorage.deleteSession(sessionId);
26+
} else {
27+
yield* Console.log('No active session');
28+
}
29+
30+
const urlParams = request.url.includes('?')
31+
? new URLSearchParams(request.url.split('?')[1])
32+
: new URLSearchParams();
33+
34+
const redirectUri = urlParams.get('post_logout_redirect_uri');
35+
const state = urlParams.get('state');
36+
37+
if (redirectUri) {
38+
// For a full OIDC-compliant implementation, we would validate:
39+
// 1. If id_token_hint is provided, validate it
40+
// 2. Verify that redirectUri is registered for this client
41+
42+
// Create a proper HTTP redirect (302 Found) response
43+
const targetUrl = state
44+
? `${redirectUri}?state=${encodeURIComponent(state)}`
45+
: redirectUri;
46+
47+
return {
48+
status: 302,
49+
headers: {
50+
Location: targetUrl,
51+
'Cache-Control': 'no-store',
52+
},
53+
body: '',
54+
};
55+
}
56+
57+
// Default response if no redirect
58+
return 'Logged out successfully';
59+
}).pipe(Effect.withSpan('EndSessionHandler')),
60+
),
61+
);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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 { MockApi } from '../spec.js';
8+
import { Tokens } from '../services/tokens.service.js';
9+
import { HttpApiBuilder } from '@effect/platform';
10+
import { Effect } from 'effect';
11+
12+
const RevokeTokenHandler = HttpApiBuilder.group(MockApi, 'TokenRevocation', (handlers) =>
13+
handlers.handle('RevokeToken', () =>
14+
Effect.gen(function* () {
15+
const { revokeToken } = yield* Tokens;
16+
17+
// For simplicity in the mock API, we'll use a default token
18+
// A real implementation would parse the x-www-form-urlencoded body
19+
const token = 'example-token';
20+
const tokenTypeHint = undefined;
21+
22+
yield* Effect.log('Revoking token', { token, tokenTypeHint });
23+
24+
const result = yield* revokeToken(token, tokenTypeHint);
25+
26+
return result;
27+
}).pipe(Effect.withSpan('RevokeTokenHandler')),
28+
),
29+
);
30+
31+
export { RevokeTokenHandler };

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@ import { UserInfo } from '../services/userinfo.service.js';
1010
import { HttpApiBuilder } from '@effect/platform';
1111
import { BearerToken } from '../middleware/Authorization.js';
1212

13-
/**
14-
* TODO: Need to implement an Authorization middleware
15-
*/
1613
const UserInfoMockHandler = HttpApiBuilder.group(MockApi, 'Protected Requests', (handlers) =>
1714
handlers.handle('UserInfo', () =>
1815
Effect.gen(function* () {

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import { SessionMiddlewareMock } from './middleware/Session.js';
2323
import { SessionStorage } from './services/session.service.js';
2424
import { NodeSdk } from '@effect/opentelemetry';
2525
import { BatchSpanProcessor, ConsoleSpanExporter } from '@opentelemetry/sdk-trace-base';
26+
import { EndSessionHandlerMock } from './handlers/end-session.handler.js';
27+
import { RevokeTokenHandler } from './handlers/revoke.handler.js';
2628

2729
const NodeSdkLive = NodeSdk.layer(() => ({
2830
resource: { serviceName: 'Mock-Api' },
@@ -35,6 +37,8 @@ const APIMock = HttpApiBuilder.api(MockApi).pipe(
3537
Layer.provide(AuthorizeHandlerMock),
3638
Layer.provide(TokensHandler),
3739
Layer.provide(UserInfoMockHandler),
40+
Layer.provide(EndSessionHandlerMock),
41+
Layer.provide(RevokeTokenHandler),
3842
);
3943

4044
const ServerMock = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
@@ -51,7 +55,7 @@ const ServerMock = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
5155
Layer.provide(
5256
HttpApiBuilder.middlewareCors({
5357
allowedMethods: ['GET', 'PUT', 'POST', 'OPTIONS'],
54-
allowedOrigins: ['http://localhost:5173', 'http://localhost:8443'],
58+
allowedOrigins: ['*'],
5559
credentials: true,
5660
maxAge: 3600,
5761
}),

e2e/mock-api-v2/src/middleware/Authorization.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ const AuthorizationMock = Layer.effect(
2929
const tokenValue = Redacted.value(bearerToken);
3030
yield* Effect.log('checking bearer token', tokenValue);
3131

32-
// Here you could add validation logic if needed
33-
// For now, we just pass through any token
34-
if (!tokenValue || tokenValue.trim() === '') {
32+
// Validation logic
33+
// 1. Check if token is empty
34+
// 2. Check if token has been revoked (has REVOKED_ prefix)
35+
if (!tokenValue || tokenValue.trim() === '' || tokenValue.startsWith('REVOKED_')) {
3536
return yield* Effect.fail(new Unauthorized());
3637
}
3738

e2e/mock-api-v2/src/middleware/CookieMiddleware.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,35 @@ const IncrementStepIndexMock = Layer.effect(
2222
// Parse existing stepIndex cookie or default to 0
2323
const cookies = request.cookies;
2424
const currentStepIndex = cookies.stepIndex ? parseInt(cookies.stepIndex) : 0;
25-
const newStepIndex = currentStepIndex + 1;
2625

27-
yield* Console.log(`Current stepIndex: ${currentStepIndex}, setting to: ${newStepIndex}`);
26+
// Get the request URL path
27+
const urlPath = request.url.split('?')[0];
28+
// Check if this is an end-session request
29+
const isEndSessionRequest = urlPath.includes('/end_session');
30+
// Determine the new stepIndex based on the request type
31+
let newStepIndex = currentStepIndex;
32+
if (isEndSessionRequest) {
33+
// Reset the stepIndex for end_session requests
34+
newStepIndex = 0;
35+
yield* Console.log('End session request detected, resetting stepIndex to: ' + newStepIndex);
36+
} else if (urlPath.includes('/authorize') || urlPath.includes('/authenticate')) {
37+
// Increment the stepIndex for authorization flow requests
38+
newStepIndex = currentStepIndex + 1;
39+
yield* Console.log(
40+
'Current stepIndex: ' + currentStepIndex + ', incrementing to: ' + newStepIndex,
41+
);
42+
} else {
43+
// For other requests, keep the stepIndex the same
44+
yield* Console.log('Request to ' + urlPath + ', keeping stepIndex at: ' + currentStepIndex);
45+
}
2846

29-
// Set the incremented stepIndex cookie in the response
47+
// Set the appropriate stepIndex cookie in the response
3048
yield* HttpApp.appendPreResponseHandler((request, response) =>
3149
HttpServerResponse.setCookie(response, 'stepIndex', String(newStepIndex), {
3250
// Optional cookie options
3351
httpOnly: false,
3452
secure: false,
35-
sameSite: 'lax',
53+
sameSite: 'strict',
3654
}).pipe(
3755
Effect.catchTag(
3856
'CookieError',
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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+
8+
const revokeResponseBody = {
9+
status: 'success' as const,
10+
};
11+
12+
export { revokeResponseBody };
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
// Path parameters schema for the endSession endpoint
10+
const EndSessionPath = Schema.Struct({
11+
envid: Schema.String,
12+
});
13+
14+
// URL query parameters for endSession
15+
const EndSessionQuery = Schema.Struct({
16+
id_token_hint: Schema.optional(Schema.String),
17+
post_logout_redirect_uri: Schema.optional(Schema.String),
18+
state: Schema.optional(Schema.String),
19+
});
20+
21+
// Request headers schema
22+
const EndSessionHeaders = Schema.Struct({
23+
cookie: Schema.optional(Schema.String),
24+
});
25+
26+
export { EndSessionPath, EndSessionQuery, EndSessionHeaders };
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
// The environment ID path parameter
10+
const RevokePath = Schema.Struct({
11+
envid: Schema.String,
12+
});
13+
14+
// Request body for token revocation according to RFC 7009
15+
const RevokeRequestBody = Schema.Struct({
16+
token: Schema.String, // The token to be revoked
17+
token_type_hint: Schema.optional(
18+
Schema.Union(Schema.Literal('access_token'), Schema.Literal('refresh_token')),
19+
), // Hint about token type (access_token or refresh_token)
20+
client_id: Schema.optional(Schema.String), // OAuth 2.0 client identifier
21+
client_secret: Schema.optional(Schema.String), // OAuth 2.0 client secret
22+
});
23+
24+
// Simple success response
25+
const RevokeResponseBody = Schema.Struct({
26+
status: Schema.Literal('success'), // Indicates successful token revocation
27+
});
28+
29+
export { RevokePath, RevokeRequestBody, RevokeResponseBody };

0 commit comments

Comments
 (0)