Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": [
"ping-javascript-sdk",
"scratchpad",
"@forgerock/pingone-scripts",
"@forgerock/device-client",
Expand Down
29 changes: 0 additions & 29 deletions .github/workflows/changesets-renovate.yml

This file was deleted.

Empty file added e2e/mock-api-v2/GEMINI.md
Empty file.
14 changes: 13 additions & 1 deletion e2e/mock-api-v2/src/addStepCookie.openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const addStepCookie = (spec: any) => {
const FLOW_TAGS = new Set(['Authorization', 'Capabilities']);
const FLOW_PATH_MATCHERS = [
/\/davinci\/authorize\b/,
/\/davinci\/connections\/[^/]+\/capabilities\//,
/\/davinci\/connections\/[^/]+\/capabilities/,
];

const shouldAnnotate = (path: string, op: any) =>
Expand All @@ -32,6 +32,18 @@ export const addStepCookie = (spec: any) => {
example: 2,
});
}
const alreadyAcrValues = op.parameters.some(
(p: any) => p && p.in === 'cookie' && p.name === 'acr_values',
);
if (!alreadyAcrValues && !op.tags?.includes('Authorization')) {
op.parameters.push({
name: 'acr_values',
in: 'cookie',
required: false,
description: 'The acr_values that were used to initiate the authorization flow.',
schema: { type: 'string' },
});
}
};

const ensureSetCookie = (op: any, status: string) => {
Expand Down
22 changes: 10 additions & 12 deletions e2e/mock-api-v2/src/handlers/authorize.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,26 @@
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
import { Effect } from 'effect';

import { Effect, pipe } from 'effect';
import { MockApi } from '../spec.js';
import { HttpApiBuilder, HttpApiError } from '@effect/platform';
import { HttpApiBuilder, HttpApiError, HttpServerResponse } from '@effect/platform';
import { getFirstElementAndRespond } from '../services/mock-env-helpers/index.js';

const AuthorizeHandlerMock = HttpApiBuilder.group(MockApi, 'Authorization', (handlers) =>
handlers.handle('authorize', ({ urlParams }) =>
Effect.gen(function* () {
/**
* We expect an acr_value query parameter to be present in the request.
* If it is not present, we return a 404 Not Found error.
*/
const acr_value = urlParams?.acr_values ?? '';

if (!acr_value) {
return yield* Effect.fail(new HttpApiError.NotFound());
}
const body = yield* getFirstElementAndRespond(urlParams);

const response = yield* getFirstElementAndRespond(urlParams);
const res = yield* pipe(
HttpServerResponse.json(body),
Effect.flatMap(HttpServerResponse.setCookie('acr_values', acr_value, { path: '/' })),
Effect.catchTag('CookieError', () => Effect.fail(new HttpApiError.InternalServerError())),
Effect.catchTag('HttpBodyError', () => Effect.fail(new HttpApiError.InternalServerError())),
);

return response;
return res;
}).pipe(Effect.withSpan('DavinciAuthorize')),
),
);
Expand Down
99 changes: 42 additions & 57 deletions e2e/mock-api-v2/src/handlers/capabilities.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
import { Console, Effect, pipe } from 'effect';
import { Effect, pipe } from 'effect';
import { MockApi } from '../spec.js';
import {
HttpApiBuilder,
HttpApiError,
HttpBody,
HttpServerRequest,
HttpServerResponse,
} from '@effect/platform';
Expand All @@ -18,15 +17,13 @@ import { validator } from '../helpers/match.js';
import { returnSuccessResponseRedirect } from '../responses/return-success-redirect.js';

const CapabilitiesHandlerMock = HttpApiBuilder.group(MockApi, 'Capabilities', (handlers) =>
handlers.handle('capabilities', ({ urlParams, payload }) =>
handlers.handle('capabilities', ({ payload }) =>
Effect.gen(function* () {
/**
* We expect an acr_value query parameter to be present in the request.
* If it is not present, we return a 404 Not Found error.
*/
const acr_value = urlParams?.acr_values ?? '';
console.log('acr_value', acr_value);
const req = yield* HttpServerRequest.HttpServerRequest;
console.log('request cookies', req.cookies);
const acr_value = req.cookies.acr_values ?? '';

console.log('acr_value', acr_value);
if (!acr_value) {
return yield* Effect.fail(new HttpApiError.NotFound());
}
Expand All @@ -36,15 +33,13 @@ const CapabilitiesHandlerMock = HttpApiBuilder.group(MockApi, 'Capabilities', (h
* If the cookie is not present, we return a 404 Not Found error.
*/

const req = yield* HttpServerRequest.HttpServerRequest;

const stepIndexCookie = req.cookies['stepIndex'];
console.log(req.cookies);

/**
* If we are here with no step index that means we can't continue through a flow.
* We should error
*/
console.log('step index cookie', stepIndexCookie);
if (!stepIndexCookie) {
console.log('no step index');
return yield* Effect.fail(new HttpApiError.NotFound());
Expand Down Expand Up @@ -76,59 +71,49 @@ const CapabilitiesHandlerMock = HttpApiBuilder.group(MockApi, 'Capabilities', (h
*/
const steps = responseMap[acr_value];

/**
* This may not be the best way to write this.
* An alternative option would be for us to include the success response we want to return,
* in the response map.
*
* then we can check if we are at the last step. if we are we write the cookie
* and then we return the success response (last item in array)
*
* for now, this returns a default success response and writes cookies.
*/
if (stepIndex + 1 >= steps.length) {
if (stepIndex + 1 === steps.length - 1) {
/**
* we need to return a success because we have not failed yet,
* and we have no more steps to process.
*/
const body = yield* HttpBody.json(returnSuccessResponseRedirect).pipe(
Effect.tap(Console.log(`here stepIndex: ${stepIndex}`)),
/**
* Decide on a better way to handle this error possibiltiy
*/
Effect.catchTag('HttpBodyError', () =>
Effect.fail(
new HttpApiError.HttpApiDecodeError({
message: 'Failed to encode body',
issues: [],
}),
const body = responseMap[stepIndex];

return yield* pipe(
HttpServerResponse.json(body),
Effect.flatMap((res) => HttpServerResponse.setCookie(res, 'ST', 'MockApiCookie123')),
Effect.flatMap((res) =>
HttpServerResponse.setCookie(
res,
'interactionId',
returnSuccessResponseRedirect.interactionId,
{
httpOnly: true,
secure: true,
sameSite: 'strict',
},
),
),
);
return pipe(
HttpServerResponse.json(body),
HttpServerResponse.setCookie('ST', 'MockApiCookie123'),
HttpServerResponse.setCookie(
'interactionId',
returnSuccessResponseRedirect.interactionId,
{
httpOnly: true,
secure: true,
sameSite: 'strict',
},
Effect.flatMap((res) =>
HttpServerResponse.setCookie(
res,
'interactionToken',
returnSuccessResponseRedirect.interactionToken,
{
httpOnly: true,
secure: true,
sameSite: 'strict',
},
),
),
HttpServerResponse.setCookie(
'interactionToken',
returnSuccessResponseRedirect.interactionToken,
{
httpOnly: true,
secure: true,
sameSite: 'strict',
},
Effect.flatMap((res) => HttpServerResponse.removeCookie(res, 'stepIndex')),
Effect.flatMap((res) => HttpServerResponse.setStatus(res, 200)),
Effect.flatMap((res) =>
HttpServerResponse.setHeader(res, 'Content-Type', 'application/json'),
),
Effect.catchTag('CookieError', () => Effect.fail(new HttpApiError.InternalServerError())),
Effect.catchTag('HttpBodyError', () =>
Effect.fail(new HttpApiError.InternalServerError()),
),
HttpServerResponse.removeCookie('stepIndex'),
HttpServerResponse.setStatus(200),
HttpServerResponse.setHeader('Content-Type', 'application/json'),
);
}

Expand Down
100 changes: 87 additions & 13 deletions e2e/mock-api-v2/src/handlers/open-id-configuration.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,96 @@
*/
import { Effect } from 'effect';
import { MockApi } from '../spec.js';
import { openidConfigurationResponse } from '../responses/open-id-configuration.js';
import { HttpApiBuilder } from '@effect/platform';
import { HttpServerRequest } from '@effect/platform/HttpServerRequest';

/**
* TODO: This needs to make a request for an openid configuration in a LIVE environment
* The proper way is to probably create a LIVE (effect convention) route, that handles this
* then the LIVE app is provided the HttpClient needed
*
*
*/
const OpenidConfigMock = HttpApiBuilder.group(MockApi, 'OpenIDConfig', (handlers) =>
handlers.handle(
'openid',
Effect.fn('OpenId')(function* () {
const value = yield* Effect.succeed(openidConfigurationResponse);
return value;
handlers.handle('openid', ({ path: { envid } }) =>
Effect.gen(function* () {
const request = yield* HttpServerRequest;
const url = new URL(request.url);
const issuer = `${url.protocol}//${url.host}/${envid}/as`;
return {
issuer,
authorization_endpoint: `${issuer}/authorize`,
pushed_authorization_request_endpoint: `${issuer}/par`,
token_endpoint: `${issuer}/token`,
userinfo_endpoint: `${issuer}/userinfo`,
jwks_uri: `${issuer}/jwks`,
end_session_endpoint: `${issuer}/signoff`,
check_session_iframe: `${issuer}/checksession`,
introspection_endpoint: `${issuer}/introspect`,
revocation_endpoint: `${issuer}/revoke`,
device_authorization_endpoint: `${issuer}/device_authorization`,
claims_parameter_supported: false,
request_parameter_supported: true,
request_uri_parameter_supported: false,
require_pushed_authorization_requests: false,
scopes_supported: ['openid', 'profile', 'email', 'address', 'phone'],
response_types_supported: [
'code',
'id_token',
'token id_token',
'code id_token',
'code token',
'code token id_token',
],
response_modes_supported: ['pi.flow', 'query', 'fragment', 'form_post'],
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should ensure 'pi.flow' is only included as a supported mode if it's PingOne, but absent in PingAM.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be a different route than this, wouldn't it?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do I mentally connect this to the route it's serving? Is this PingOne only wellknown endpoint? If so, I can't immediately identify it as such.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every handler has a name, I named this openid. I can be more explicit.

All these routes are PingOne specific right now. I haven't touched anything AIC.

the spec.ts file defines the route, name, and schemas for the route.

The handler is the implementation of it.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, okay. Since OIDC is a spec, and not server-specific, we'll want to discuss how we will differentiate the subtle differences and explicitly label them.

grant_types_supported: [
'authorization_code',
'implicit',
'client_credentials',
'refresh_token',
'urn:ietf:params:oauth:grant-type:device_code',
],
subject_types_supported: ['public'],
id_token_signing_alg_values_supported: ['RS256'],
userinfo_signing_alg_values_supported: ['none'],
request_object_signing_alg_values_supported: [
'none',
'HS256',
'HS384',
'HS512',
'RS256',
'RS384',
'RS512',
],
token_endpoint_auth_methods_supported: [
'client_secret_basic',
'client_secret_post',
'client_secret_jwt',
'private_key_jwt',
],
token_endpoint_auth_signing_alg_values_supported: [
'HS256',
'HS384',
'HS512',
'RS256',
'RS384',
'RS512',
],
claim_types_supported: ['normal'],
claims_supported: [
'sub',
'iss',
'auth_time',
'acr',
'name',
'given_name',
'family_name',
'middle_name',
'preferred_username',
'profile',
'picture',
'zoneinfo',
'phone_number',
'updated_at',
'address',
'email',
'locale',
],
code_challenge_methods_supported: ['plain', 'S256'],
};
}),
),
);
Expand Down
7 changes: 6 additions & 1 deletion e2e/mock-api-v2/src/handlers/userinfo.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@
import { Effect } from 'effect';
import { MockApi } from '../spec.js';
import { UserInfo } from '../services/userinfo.service.js';
import { HttpApiBuilder } from '@effect/platform';
import { HttpApiBuilder, HttpApiError } from '@effect/platform';
import { BearerToken } from '../middleware/Authorization.js';

const UserInfoMockHandler = HttpApiBuilder.group(MockApi, 'ProtectedRequests', (handlers) =>
handlers.handle('UserInfo', () =>
Effect.gen(function* () {
const authToken = yield* BearerToken;

if (!authToken) {
return yield* Effect.fail(new HttpApiError.Unauthorized());
}

const { getUserInfo } = yield* UserInfo;

const response = yield* getUserInfo(authToken);
Expand Down
2 changes: 1 addition & 1 deletion e2e/mock-api-v2/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const ServerMock = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(

Layer.provide(NodeSdkLive),
HttpServer.withLogAddress,
Layer.provide(NodeHttpServer.layer(createServer, { port: 9443 })),
Layer.provide(NodeHttpServer.layer(createServer, { port: 9443, host: 'localhost' })),
);

Layer.launch(ServerMock).pipe(NodeRuntime.runMain);
Loading
Loading