Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

last middlewareEndpoints wins #4449

Open
fredericrous opened this issue Feb 14, 2025 · 5 comments
Open

last middlewareEndpoints wins #4449

fredericrous opened this issue Feb 14, 2025 · 5 comments

Comments

@fredericrous
Copy link

fredericrous commented Feb 14, 2025

What version of Effect is running?

3.12.11

What steps can reproduce the bug?

with the following api definition

export const ExampleApi = HttpApiGroup.make('examples')
  .add(
    HttpApiEndpoint.get('first', '/first')
      .addSuccess(ExampleResponseDto)
  )
  .middlewareEndpoints(AuthenticationApiKey)
  .add(
      HttpApiEndpoint.get('second', '/second')
      .addSuccess(ExampleResponseDto)
  )
  .middlewareEndpoints(AuthenticationOauth2)
  .add(
      HttpApiEndpoint.get('third', '/third')
      .addSuccess(ExampleResponseDto)
  )

the endpoint /first is protected by .middlewareEndpoints(AuthenticationOauth2) when I am expecting it to be behind .middlewareEndpoints(AuthenticationApiKey)

What is the expected behavior?

When I visit http://localhost:4242/first I should get UnauthorizedApiKey error

What do you see instead?

I get the error UnauthorizedOauth2

Additional information

a full code example

import { createServer } from "node:http";
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
import {
  Context,
  Effect,
  Layer,
  Logger,
  LogLevel,
  pipe,
  Redacted,
  Schema,
} from "effect";
import {
  HttpApi,
  HttpApiBuilder,
  HttpApiEndpoint,
  HttpApiGroup,
  HttpApiMiddleware,
  HttpApiSchema,
  HttpApiSecurity,
  HttpMiddleware,
  HttpServer,
} from "@effect/platform";

const ExampleResponseDto = Schema.Struct({
  status: Schema.Number,
});
type CurrentType = { current: string };
class Current extends Context.Tag("Example/Current")<
  Current,
  CurrentType
>() {}

class UnauthorizedOauth2 extends Schema.TaggedError<UnauthorizedOauth2>()(
  "UnauthorizedOauth2",
  {},
  HttpApiSchema.annotations({ status: 403 })
) {}

class UnauthorizedApiKey extends Schema.TaggedError<UnauthorizedApiKey>()(
  "UnauthorizedApiKey",
  {},
  HttpApiSchema.annotations({ status: 403 })
) {}

class AuthenticationOauth2 extends HttpApiMiddleware.Tag<AuthenticationOauth2>()(
  "Authentication/Oauth2",
  {
    provides: Current,
    failure: UnauthorizedOauth2,
    security: {
      myBearer: HttpApiSecurity.bearer,
    },
  }
) {}

class AuthenticationApiKey extends HttpApiMiddleware.Tag<AuthenticationApiKey>()(
  "Authentication/ApiKey",
  {
    provides: Current,
    failure: UnauthorizedApiKey,
    security: {
      myApiKey: HttpApiSecurity.apiKey({ in: "header", key: "Authorize" }),
    },
  }
) {}

const AuthenticationOauth2Live = Layer.effect(
  AuthenticationOauth2,
  Effect.succeed(
    AuthenticationOauth2.of({
      myBearer: (token) =>
        Effect.gen(function* myBearerGen() {
          if (Redacted.value(token) === "TOKEN") {
            return { current: "oauth2" };
          }
          throw new UnauthorizedOauth2();
        }),
    })
  )
);

const AuthenticationApiKeyLive = Layer.effect(
  AuthenticationApiKey,
  Effect.succeed(
    AuthenticationApiKey.of({
      myApiKey: (token) =>
        Effect.gen(function* myApiKeyGen() {
          if (Redacted.value(token).replace("ApiKey ", "") === "TOKEN") {
            return { current: "apiKey" };
          }
          throw new UnauthorizedApiKey();
        }),
    })
  )
);

const ExampleApi = HttpApiGroup.make("examples")
  .add(HttpApiEndpoint.get("first", "/first").addSuccess(ExampleResponseDto))
  .middlewareEndpoints(AuthenticationApiKey)
  .add(HttpApiEndpoint.get("second", "/second").addSuccess(ExampleResponseDto))
  .middlewareEndpoints(AuthenticationOauth2)
  .add(HttpApiEndpoint.get("third", "/third").addSuccess(ExampleResponseDto));

const api = HttpApi.make("Api").add(ExampleApi);

const ExamplesLive = HttpApiBuilder.group(api, "examples", (handlers) =>
  handlers
    .handle("first", () => Effect.succeed({ status: 1 }))
    .handle("second", () => Effect.succeed({ status: 2 }))
    .handle("third", () => Effect.succeed({ status: 3 }))
);

const MyApiLive = Layer.provide(HttpApiBuilder.api(api), [
  ExamplesLive.pipe(
    Layer.provide(AuthenticationOauth2Live),
    Layer.provide(AuthenticationApiKeyLive)
  ),
]);

const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
  // Provide the Swagger layer so clients can access auto-generated docs
  Layer.provide(MyApiLive),
  HttpServer.withLogAddress,
  Layer.provide(NodeHttpServer.layer(createServer, { port: 4242 }))
);

HttpLive.pipe(Layer.launch, NodeRuntime.runMain);

I'm using

    "@effect/platform": "^0.76.0",
    "@effect/platform-node": "^0.72.0",
    "effect": "^3.12.11"

and the code to call the api:

// node example-bin.mts
const result = await fetch('http://localhost:4242/first', { headers: { Authorize: 'ApiKey TOKEN' }})
console.info(result)
@fredericrous fredericrous added the bug Something isn't working label Feb 14, 2025
@fredericrous
Copy link
Author

if it's not a bug but an expected behavior, this should be documented

@tim-smart
Copy link
Contributor

Apis in effect that "wrap" things, generally flow from the bottom up.

If you want to apply specific middleware to specific endpoints then you can use .middleware on the endpoint itself.

@tim-smart tim-smart added working as intended and removed bug Something isn't working labels Feb 15, 2025
@fredericrous
Copy link
Author

fredericrous commented Feb 15, 2025

I had to take some time to understand your message because I think there is a misunderstanding.
The solution of using .middleware looks good enough for me, and is documented.
I just like to try to explain again the issue report because I am not sure it was understood.
Indeed the flow goes from the bottom up but one overwrite the other

what I see

const ExampleApi = HttpApiGroup.make("examples")
  .add(HttpApiEndpoint.get("first", "/first").addSuccess(ExampleResponseDto)) // *protected by oauth2*
  .middlewareEndpoints(AuthenticationApiKey)
  .add(HttpApiEndpoint.get("second", "/second").addSuccess(ExampleResponseDto)) // protected by oauth2
  .middlewareEndpoints(AuthenticationOauth2)
  .add(HttpApiEndpoint.get("third", "/third").addSuccess(ExampleResponseDto)); // not protected

what I expect

const ExampleApi = HttpApiGroup.make("examples")
  .add(HttpApiEndpoint.get("first", "/first").addSuccess(ExampleResponseDto)) // *protected by api key*
  .middlewareEndpoints(AuthenticationApiKey)
  .add(HttpApiEndpoint.get("second", "/second").addSuccess(ExampleResponseDto)) // protected by oauth2
  .middlewareEndpoints(AuthenticationOauth2)
  .add(HttpApiEndpoint.get("third", "/third").addSuccess(ExampleResponseDto)); // not protected

I'm fine with closing the issue, however a line in the doc regarding this behavior could help others

@tim-smart
Copy link
Contributor

The first endpoint will be protected by both. It will hit oauth first, and then the API key middleware.

@fredericrous
Copy link
Author

okayy that makes sense, thank you for the explanation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants