DenisFrezzato / hyper-ts

Type safe middleware architecture for HTTP servers
https://denisfrezzato.github.io/hyper-ts/
MIT License
391 stars 18 forks source link

Feature request: Authentication middleware example #35

Open kylegoetz opened 3 years ago

kylegoetz commented 3 years ago

I suggest there should be an auth middleware example. I'm developing my own but wondering about thoughts on best practices. Assume we have

interface AuthenticationOpen { readonly AuthenticationOpen: unique symbol }
interface AuthorizationOpen { readonly AuthorizationOpen: unique symbol }
declare const authenticateFromToken: (token:Token) => Either<Error,AuthenticatedUser>
declare const authorizeUserForAction: (action:Action) => (user:AuthenciatedUser) => Either<Error,AuthorizedUser>

const authenticate: H.Middleware<AuthenticationOpen,H.StatusOpen,Error,void> = H.fromConnection(c => pipe(
  c.getHeader('authentication'),
  authenticateFromToken
))

Technically this creates a middleware with type A=AuthenticatedUser. So you could flow(authenticate, user => middlewareRequiring(user) but the "express way" is to register the authenticate middleware in a single place and never reference it again.

This suggests to me we need to write data to the connection and then from another middleware, have a middleware like

declare const getAuthenticatedUser: H.Middleware<AuthenticationOpen,AuthorizationOpen,NotAuthenticatedException,AuthenticatedUser>

and then we'd have some other route controller that requires authentication:

const controller: H.Middleware<AuthenticationOpen,H.ResponseEnded,Error,void> = pipe(
  getAuthenticatedUser,
  H.ichain(user => pipe(
    H.status(H.Status.OK),
    H.ichain(() => H.closeHeaders()),
    H.ichain(() => H.send(user.username)))

The idea is have a flow of [AuthenticationOpen -> [AuthorizationOpen ->]] StatusOpen -> ... so some controllers can enforce authentication (and authorization) in a typesafe manner.

HOWEVER, the issue is that you apparently cannot do something like

user => H.fromConnection(c => {
  c.authenticatedUser = some(user)
  return E.right(user)
})

because authenticatedUser does not exist on type Connection. So I was thinking how to persist state for another middleware to optionally access it down the middleware chain. setCookie and setHeader both present that option, but I'd need to serialize authenticatedUser since they each take a string.

Sounds good. But feels a bit weird, to set a cookie/header and before sending status, remove the cookie/header since they aren't actually supposed to be sent back in the request but are just kludges to pass state from one middleware to another.

Thoughts?

kylegoetz commented 3 years ago

Actually I apparently cannot write a cookie since that requires me to be in HeadersOpen, but I'm still in AuthenticationOpen phase.

kylegoetz commented 3 years ago

OK what I've settled on is the authentication middleware uses fromConnection to getRequest().rawHeaders and add a custom header that is the authenticated user.

Then I have a getAuthenticatedUser middleware that retrieves it, and is of type Middleware<AuthenticationOpen,AuthorizationOpen,Error,AuthenticatedUser>.

I can then describe a route as

const routeController: Middleware<AuthenticationOpen,ResponseEnded,Error,void> = pipe(
  getAuthenticatedUser(), // index AuthenticationOpen -> AuthorizationOpen
  user => pipe(
    noAuthorizationRequired(), // index AuthorizationOpen -> StatusOpen
    H.ichain(() => H.status(H.Status.OK)), // proceeds as normal
    H.ichain(() => H.closeHeaders()),
    H.ichain(() => H.send(user.name))
) 

Now the auth middleware is decoupled, and if I try to write a status without trying to get the authenticated user (which will do the left case if no custom auth user header is set), and I also have to specify there is no authorization required. So the indexed monad provides type safety still!

Is there any interest in a PR to update with an example like this?

thewilkybarkid commented 2 years ago

I've spent the day playing with how cookie-based sessions would work. https://github.com/PREreview/prereview-display-prototype/commit/8f51ab348950289d6ac569d7e91058654f81f5d8 is large commit that adds it to a prototype app, but https://github.com/PREreview/prereview-display-prototype/blob/8f51ab348950289d6ac569d7e91058654f81f5d8/packages/hyper-ts-session/src/index.ts is the interesting bit: I've been looking at how Hyper's sessions work and trying to replicate it. Definitely a work in progress still.

thewilkybarkid commented 2 years ago

I've released them as https://www.npmjs.com/package/hyper-ts-session and https://www.npmjs.com/package/hyper-ts-oauth. Still pretty basic.