sukovanej / effect-http

Declarative HTTP API library for effect-ts
https://sukovanej.github.io/effect-http
MIT License
247 stars 20 forks source link

Lucia cookie example? #665

Open danielo515 opened 3 weeks ago

danielo515 commented 3 weeks ago

Hello again, donyou have an example of how to use Lucia with effect-http? I want to set the values Lucia returns from the createCookie method, wich has a name, value and attributes. Http platform has a function that almost marches this signature, but I don't know how to make it work with effect-http. I also saw a todo comment on the security for the cookie auth, so an example for that will also be very appreciated.

Thank you!

danielo515 commented 3 weeks ago

This is what I'm doing, and it seems to work. Not sure if it is the right approach.

This is my api-definition for the login endpoint:

  Api.addEndpoint(
    pipe(
      Api.post("login", "/api/login"),
      Api.setRequestBody(Schema.Array(ExternalExpense)),
      Api.setRequestBody(LoginPayload),
      Api.setResponseHeaders(
        Schema.Struct({
          "Set-Cookie": Schema.String,
        })
      )
    )
  )

And here is the handler:

  RouterBuilder.handle("login", ({ body: { email, password } }) =>
    Effect.gen(function* (_) {
      const user = yield* UserService.login(email, password);
      yield* Effect.logDebug("User logged in: ", user);
      const lucia = yield* CookieService;
      const session = yield* lucia.getUserSessions(user.id).pipe(
        Effect.tap((sessions) =>
          Effect.logDebug("User has sessions: ", sessions.length)
        ),
        Effect.flatMap(A.head),
        Effect.mapError(() => lucia.createSession(user.id, user))
      );

      const sessionCookie = lucia.createSessionCookie(session.id);

      return yield* Effect.succeed({
        status: 200,
        headers: { "Set-Cookie": sessionCookie.serialize() },
      } as const);
    }).pipe(
      Effect.catchTags({
        UserNotFound: () =>
          Effect.gen(function* (_) {
            yield* Effect.logWarning("User not found", { email });
            return yield* HttpError.unauthorized("Unauthorized");
          }),
        InvalidPassword: () =>
          Effect.gen(function* (_) {
            yield* Effect.logWarning("Invalid password", { email });
            return yield* HttpError.unauthorized("Unauthorized");
          }),
      }),
      Effect.withLogSpan("/api/login")
    )
  ),
  RouterBuilder.build
);

The CookieService is just a wrapper around lucia.

danielo515 commented 3 weeks ago

So, here is my approach to the cookie security:

const cookie = pipe(
  HttpServerRequest.schemaCookies(
    Schema.Struct({ auth_session: Schema.String })
  ),
  Effect.mapError(() =>
    HttpError.unauthorized('Expected valid "auth_session" header')
  ),
  Effect.flatMap((obj) =>
    Effect.gen(function* (_) {
      const x = yield* HttpServerRequest.schemaHeaders(
        Schema.Struct({ origin: Schema.String, host: Schema.String })
      );

      const cookieService = yield* CookieService;
      console.log(verifyRequestOrigin(x.origin, [x.host]));
      yield* Effect.logDebug("Handling cookie security");
      const session = yield* cookieService.validateSession(obj.auth_session);
      if (!session.session) {
        yield* Effect.logDebug("Invalid or expired session, clearing cookie");
        return yield* HttpError.unauthorized("Unauthorized", {
          headers: pipe(
            Headers.empty,
            Headers.set(
              "Set-Cookie",
              cookieService.lucia.createBlankSessionCookie().serialize()
            )
          ),
        });
      }
      if (session.session.fresh) {
        /*   
        TODO: handle fresh session
         return HttpServerResponse.empty().pipe(
          HttpServerResponse.setCookie("session", session.session.id)
        );
        */
      }
      return session.user;
    })
  )
);

export const cookieSecurity = Security.make(cookie, {
  session: {
    name: "auth_session",
    type: "cookie",
    in: "cookie",
  },
});

As you can see, I'm still missing the ability to intercept the response and set additional headers to update the session cookie in case is needed. Is this a limitation of how security is supposed to be used? This looks like a job for a middleware, but security doesn't seem to have such capability.