Skip to content
Closed
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
3 changes: 1 addition & 2 deletions e2e/mock-api-v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"type": "module",
"main": "./src/main.js",
"scripts": {
"build": "pnpm nx nxBuild",
"build": "pnpm nx build",
"dev": "node dist/src/main.js --watch-path=./",
"lint": "pnpm nx nxLint",
"serve": "node dist/src/main.js",
Expand All @@ -15,7 +15,6 @@
"dependencies": {
"@effect/language-service": "catalog:effect",
"@effect/opentelemetry": "catalog:effect",
"@effect/platform": "catalog:effect",
"@effect/platform-node": "catalog:effect",
"@opentelemetry/sdk-logs": "0.207.0",
"@opentelemetry/sdk-metrics": "2.2.0",
Expand Down
5 changes: 3 additions & 2 deletions e2e/mock-api-v2/src/handlers/authorize.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
*/
import { Effect, pipe } from 'effect';
import { MockApi } from '../spec.js';
import { HttpApiBuilder, HttpApiError, HttpServerResponse } from '@effect/platform';
import { HttpApiBuilder, HttpApiError } from 'effect/unstable/httpapi';
import * as HttpServerResponse from 'effect/unstable/http/HttpServerResponse';
import { getFirstElementAndRespond } from '../services/mock-env-helpers/index.js';

const AuthorizeHandlerMock = HttpApiBuilder.group(MockApi, 'Authorization', (handlers) =>
handlers.handle('authorize', ({ urlParams }) =>
handlers.handle('authorize', ({ query: urlParams }) =>
Effect.gen(function* () {
const acr_value = urlParams?.acr_values ?? '';

Expand Down
15 changes: 6 additions & 9 deletions e2e/mock-api-v2/src/handlers/capabilities.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,9 @@
*/
import { Effect, pipe } from 'effect';
import { MockApi } from '../spec.js';
import {
HttpApiBuilder,
HttpApiError,
HttpServerRequest,
HttpServerResponse,
} from '@effect/platform';
import { HttpApiBuilder, HttpApiError } from 'effect/unstable/httpapi';
import * as HttpServerRequest from 'effect/unstable/http/HttpServerRequest';
import * as HttpServerResponse from 'effect/unstable/http/HttpServerResponse';
import { responseMap } from '../responses/index.js';
import { validator } from '../helpers/match.js';
import { returnSuccessResponseRedirect } from '../responses/return-success-redirect.js';
Expand Down Expand Up @@ -105,9 +102,9 @@ const CapabilitiesHandlerMock = HttpApiBuilder.group(MockApi, 'Capabilities', (h
},
),
),
Effect.flatMap((res) => HttpServerResponse.removeCookie(res, 'stepIndex')),
Effect.flatMap((res) => HttpServerResponse.setStatus(res, 200)),
Effect.flatMap((res) =>
Effect.map((res) => HttpServerResponse.removeCookie(res, 'stepIndex')),
Effect.map((res) => HttpServerResponse.setStatus(res, 200)),
Effect.map((res) =>
HttpServerResponse.setHeader(res, 'Content-Type', 'application/json'),
),
Effect.catchTag('CookieError', () => Effect.fail(new HttpApiError.InternalServerError())),
Expand Down
3 changes: 2 additions & 1 deletion e2e/mock-api-v2/src/handlers/end-session.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
* of the MIT license. See the LICENSE file for details.
*/
import { Effect, Console } from 'effect';
import { HttpApiBuilder, HttpServerRequest } from '@effect/platform';
import { HttpApiBuilder } from 'effect/unstable/httpapi';
import * as HttpServerRequest from 'effect/unstable/http/HttpServerRequest';
import { MockApi } from '../spec.js';
import { SessionStorage } from '../services/session.service.js';

Expand Down
2 changes: 1 addition & 1 deletion e2e/mock-api-v2/src/handlers/healthcheck.handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HttpApiBuilder } from '@effect/platform';
import { HttpApiBuilder } from 'effect/unstable/httpapi';
import { MockApi } from '../spec.js';
import { Effect } from 'effect';

Expand Down
6 changes: 3 additions & 3 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,11 +6,11 @@
*/
import { Effect } from 'effect';
import { MockApi } from '../spec.js';
import { HttpApiBuilder } from '@effect/platform';
import { HttpServerRequest } from '@effect/platform/HttpServerRequest';
import { HttpApiBuilder } from 'effect/unstable/httpapi';
import { HttpServerRequest } from 'effect/unstable/http/HttpServerRequest';

const OpenidConfigMock = HttpApiBuilder.group(MockApi, 'OpenIDConfig', (handlers) =>
handlers.handle('openid', ({ path: { envid } }) =>
handlers.handle('openid', ({ params: { envid } }) =>
Effect.gen(function* () {
const request = yield* HttpServerRequest;
const url = new URL(request.url);
Expand Down
2 changes: 1 addition & 1 deletion e2e/mock-api-v2/src/handlers/revoke.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
import { MockApi } from '../spec.js';
import { Tokens } from '../services/tokens.service.js';
import { HttpApiBuilder } from '@effect/platform';
import { HttpApiBuilder } from 'effect/unstable/httpapi';
import { Effect } from 'effect';

const RevokeTokenHandler = HttpApiBuilder.group(MockApi, 'Revoke', (handlers) =>
Expand Down
2 changes: 1 addition & 1 deletion e2e/mock-api-v2/src/handlers/token.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
import { MockApi } from '../spec.js';
import { Tokens } from '../services/tokens.service.js';
import { HttpApiBuilder } from '@effect/platform';
import { HttpApiBuilder } from 'effect/unstable/httpapi';
import { Effect } from 'effect';

const TokensHandler = HttpApiBuilder.group(MockApi, 'Tokens', (handlers) =>
Expand Down
2 changes: 1 addition & 1 deletion e2e/mock-api-v2/src/handlers/userinfo.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { Effect } from 'effect';
import { MockApi } from '../spec.js';
import { UserInfo } from '../services/userinfo.service.js';
import { HttpApiBuilder, HttpApiError } from '@effect/platform';
import { HttpApiBuilder, HttpApiError } from 'effect/unstable/httpapi';
import { BearerToken } from '../middleware/Authorization.js';

const UserInfoMockHandler = HttpApiBuilder.group(MockApi, 'ProtectedRequests', (handlers) =>
Expand Down
12 changes: 5 additions & 7 deletions e2e/mock-api-v2/src/helpers/match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
import { Effect, Match, Schema } from 'effect';

import { HttpApiError } from '@effect/platform';
import { HttpApiError } from 'effect/unstable/httpapi';
import { CapabilitiesRequestBody } from '../schemas/capabilities/capabilities.request.schema.js';

type PingRequestData = Schema.Schema.Type<typeof CapabilitiesRequestBody>;
Expand All @@ -21,13 +21,11 @@ const validator = Match.type<PingRequestData>().pipe(
Match.when(
{ parameters: { data: { formData: { username: Match.string, password: Match.string } } } },
({ parameters }) =>
Effect.if(
Effect.suspend(() =>
parameters.data.formData.username == 'testuser' &&
parameters.data.formData.password === 'Password',
{
onFalse: () => Effect.fail(new HttpApiError.Unauthorized()),
onTrue: () => Effect.succeed(true),
},
parameters.data.formData.password === 'Password'
? Effect.succeed(true)
: Effect.fail(new HttpApiError.Unauthorized()),
),
),
Match.orElse(() => Effect.succeed(true)),
Expand Down
82 changes: 51 additions & 31 deletions e2e/mock-api-v2/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
import { Layer } from 'effect';
import { Effect, Layer } from 'effect';
import { NodeHttpServer, NodeRuntime } from '@effect/platform-node';
import { MockApi } from './spec.js';
import { HttpApiBuilder, HttpApiSwagger, HttpMiddleware, HttpServer } from '@effect/platform';
import { HttpApiBuilder, HttpApiSwagger } from 'effect/unstable/httpapi';
import * as HttpMiddleware from 'effect/unstable/http/HttpMiddleware';
import * as HttpRouter from 'effect/unstable/http/HttpRouter';
import * as HttpServer from 'effect/unstable/http/HttpServer';
import type { ServeError } from 'effect/unstable/http/HttpServerError';
import { createServer } from 'node:http';
import { HealthCheckLive } from './handlers/healthcheck.handler.js';
import { OpenidConfigMock } from './handlers/open-id-configuration.handler.js';
Expand All @@ -26,47 +30,63 @@ import { BatchSpanProcessor, ConsoleSpanExporter } from '@opentelemetry/sdk-trac
import { EndSessionHandlerMock } from './handlers/end-session.handler.js';
import { RevokeTokenHandler } from './handlers/revoke.handler.js';

const Services = [
Layer.provide(TokensMock),
Layer.provide(IncrementStepIndexMock),
Layer.provide(AuthorizationMock),
Layer.provide(UserInfoMockService),
Layer.provide(SessionMiddlewareMock),
Layer.provide(SessionStorage.Default),
] as const;

const NodeSdkLive = NodeSdk.layer(() => ({
resource: { serviceName: 'Mock-Api' },
spanProcessor: new BatchSpanProcessor(new ConsoleSpanExporter()),
}));

const APIMock = HttpApiBuilder.api(MockApi).pipe(
Layer.provide(HealthCheckLive),
Layer.provide(OpenidConfigMock),
Layer.provide(AuthorizeHandlerMock),
Layer.provide(TokensHandler),
Layer.provide(CapabilitiesHandlerMock),
Layer.provide(UserInfoMockHandler),
Layer.provide(EndSessionHandlerMock),
Layer.provide(RevokeTokenHandler),
...Services,
// Wire SessionStorage into SessionMiddlewareMock
const SessionLayer = Layer.provide(
SessionMiddlewareMock,
Layer.effect(SessionStorage, SessionStorage.make),
);

// Merge all group handlers
const HandlersLayer = Layer.mergeAll(
HealthCheckLive,
OpenidConfigMock,
AuthorizeHandlerMock,
TokensHandler,
CapabilitiesHandlerMock,
UserInfoMockHandler,
EndSessionHandlerMock,
RevokeTokenHandler,
);

// Merge all services
const ServicesLayer = Layer.mergeAll(
TokensMock,
IncrementStepIndexMock,
AuthorizationMock,
UserInfoMockService,
SessionLayer,
);

// Build application routes layer with all handlers and services provided in one step each
const AppLayer = HttpApiBuilder.layer(MockApi).pipe(
Layer.provide(HandlersLayer),
Layer.provide(ServicesLayer),
);

// Compose app + swagger, then provide the router service
const AppWithSwagger = Layer.merge(AppLayer, HttpApiSwagger.layer(MockApi)).pipe(
Layer.provide(HttpRouter.layer),
);

const ServerMock = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
Layer.provide(HttpApiSwagger.layer()),
Layer.provide(
HttpApiBuilder.middlewareCors({
const ServerMock = HttpRouter.serve(AppWithSwagger, {
middleware: (app) =>
HttpMiddleware.cors({
allowedMethods: ['GET', 'PUT', 'POST', 'OPTIONS'],
allowedOrigins: ['*'],
credentials: true,
maxAge: 3600,
}),
),
Layer.provide(APIMock),

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

Layer.launch(ServerMock).pipe(NodeRuntime.runMain);
// TypeScript cannot fully resolve complex Effect layer generic compositions;
// all requirements ARE satisfied at runtime — NodeHttpServer provides FileSystem, Path, HttpPlatform, Etag.
NodeRuntime.runMain(Layer.launch(ServerMock) as Effect.Effect<never, ServeError, never>);
33 changes: 21 additions & 12 deletions e2e/mock-api-v2/src/middleware/Authorization.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { Unauthorized } from '@effect/platform/HttpApiError';
import { HttpApiMiddleware, HttpApiSecurity, OpenApi } from '@effect/platform';
import { Brand, Context, Effect, Layer, Redacted } from 'effect';
import { HttpApiError, HttpApiMiddleware, HttpApiSecurity, OpenApi } from 'effect/unstable/httpapi';
import type { HttpServerResponse } from 'effect/unstable/http/HttpServerResponse';
import { Brand, Context, Effect, Layer, Redacted, Types } from 'effect';

type BearerTokenValue = string & Brand.Brand<'BearerToken'>;
const BearerTokenValue = Brand.nominal<BearerTokenValue>();

// Define a service that holds the bearer token
class BearerToken extends Context.Tag('BearerToken')<BearerToken, BearerTokenValue>() {}
class BearerToken extends Context.Service<BearerToken, BearerTokenValue>()('BearerToken') {}

class Authorization extends HttpApiMiddleware.Tag<Authorization>()('Authorization', {
failure: Unauthorized,
provides: BearerToken,
class Authorization extends HttpApiMiddleware.Service<
Authorization,
{ provides: typeof BearerToken }
>()('Authorization', {
error: HttpApiError.Unauthorized,
security: {
myBearer: HttpApiSecurity.bearer.pipe(
HttpApiSecurity.annotate(OpenApi.Description, 'Bearer token for API authentication'),
Expand All @@ -24,20 +26,27 @@ const AuthorizationMock = Layer.effect(
yield* Effect.log('creating Authorization middleware');

return {
myBearer: (bearerToken) =>
myBearer: (
httpEffect: Effect.Effect<HttpServerResponse, Types.unhandled, typeof BearerToken>,
{
credential,
}: { credential: Redacted.Redacted<string>; endpoint: unknown; group: unknown },
) =>
Effect.gen(function* () {
const tokenValue = Redacted.value(bearerToken);
const tokenValue = Redacted.value(credential);
yield* Effect.log('checking bearer token', tokenValue);
Comment on lines 35 to 37

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate file relevant section =="
sed -n '1,80p' e2e/mock-api-v2/src/middleware/Authorization.ts

echo
echo "== package / Effect dependency context =="
for f in package.json e2e/mock-api-v2/package.json e2e/mock-api-v2/package-lock.json e2e/mock-api-v2/pnpm-lock.yaml e2e/mock-api-v2/yarn.lock; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    rg -n '"effect"|effect|`@effect/platform`|effect/Effect' "$f" | head -80 || true
  fi
done

echo
echo "== search for Effect.log definition/imports in local code =="
rg -n "Effect\.log|from ['\"]effect/Effect|log:" e2e/mock-api-v2/src package.json e2e/mock-api-v2/package.json 2>/dev/null || true

Repository: ForgeRock/ping-javascript-sdk

Length of output: 4500


🌐 Web query:

Effect Effect.log effect library default logger signature Effect.log documentation

💡 Result:

In the Effect library, Effect.log is a utility function used to log messages at the default INFO level [1][2]. As of recent versions (specifically following changes in v2.0.0 and related updates), the function signature is variadic, allowing it to accept any number of arguments [3][4]. The signature is defined as follows: Effect.log(...message: ReadonlyArray): Effect<void, never, never> [3] Key details regarding Effect.log and logging in Effect: - Behavior: It logs messages using the configured logger, which defaults to the INFO level [1][2]. - Observability: Effect logs include metadata such as timestamps, log levels, fiber identifiers, and message content [1][2]. - Extensibility: You can control log levels (e.g., using Logger.withMinimumLogLevel) or implement custom loggers to handle output formats like JSON or structured data [1][5][2]. - Related Functions: Effect also provides specific variants for different log levels, such as Effect.logDebug, Effect.logInfo, Effect.logWarning, and Effect.logError, all of which share the same variadic signature structure [3][6]. For more advanced logging requirements, such as writing to files, you can utilize PlatformLogger or configure custom logging layers [1][7].

Citations:


Sensitive Data Exposure (CWE-532): Insertion of Sensitive Information into Log File

Reachability: External · Exploitability: Moderate

Reachability path
● Entry
  e2e/mock-api-v2/src/main.ts:51
  UserInfoMockHandler
│
▼
● Hop
  e2e/mock-api-v2/src/handlers/userinfo.handler.ts
│
▼
● Hop
  e2e/mock-api-v2/src/spec.ts:194
  Authorization
│
▼
● Sink
  e2e/mock-api-v2/src/middleware/Authorization.ts

Do not log the bearer credential.

Effect.log(...message: ReadonlyArray<any>) forwards every argument through the logger. Passing tokenValue = Redacted.value(credential) exposes the bearer token in logs. Keep this log entry limited to non-secret metadata, such as the log message only.

Proposed fix
-          yield* Effect.log('checking bearer token', tokenValue);
+          yield* Effect.log('checking bearer token');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Effect.gen(function* () {
const tokenValue = Redacted.value(bearerToken);
const tokenValue = Redacted.value(credential);
yield* Effect.log('checking bearer token', tokenValue);
Effect.gen(function* () {
const tokenValue = Redacted.value(credential);
yield* Effect.log('checking bearer token');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/mock-api-v2/src/middleware/Authorization.ts` around lines 35 - 37, Update
the bearer-token check in Authorization middleware to stop passing tokenValue to
Effect.log; keep the log entry limited to the non-secret message while
preserving the surrounding credential validation flow.


// Validation logic
// 1. Check if token is empty
// 2. Check if token has been revoked (has REVOKED_ prefix)
if (!tokenValue || tokenValue.trim() === '' || tokenValue.startsWith('REVOKED_')) {
return yield* Effect.fail(new Unauthorized());
return yield* Effect.fail(new HttpApiError.Unauthorized());
}

// Return the token value so routes can access it
return BearerTokenValue(tokenValue);
// Provide BearerToken and run the original effect
return yield* httpEffect.pipe(
Effect.provideService(BearerToken, BearerTokenValue(tokenValue)),
);
}),
};
}),
Expand Down
Loading
Loading