Authentication examples assumes all promises a resolved in request order
See original GitHub issueIn the FAQ the recommended approach for managing the identity of the user during the processing of the request is:
app.use(async (ctx, next) => {
// do whatever checks to determine the user ID
ctx.state.userId = userId;
await next();
delete ctx.state.userId; // cleanup
});
But according to the documentation context.state
is “A record of application state.”. This is also evident in the code in context.ts (L90) where the state in all context instances are the same object as the application state.
constructor(...) {
...
this.state = app.state;
So by default the state object is shared across all request which means if the routes or any middlewares contains asynchronous code where the promises are executed out of order you will not have the state you expect (i.e. a request specific state). You can see this if you implement a route which waits for a promise which is resolved after a second invocation completes together with a middleware which follows the authentication example from the FAQ. For example:
import { Application, Router, Context } from "https://deno.land/x/oak/mod.ts";
export type Continuation = () => Promise<void>;
let resolve: () => void;
let reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
const dump = (ctx: Context) => {
console.log(
`The id ${ctx.state.userId} should be equal to ${ctx.request.url.pathname}.`
);
};
const sayHello = async (ctx: Context) => {
dump(ctx);
// do something async, e.g. db, fs, etc.
if (ctx.request.url.pathname === "/1") {
await promise;
}
dump(ctx);
if (ctx.request.url.pathname === "/2") {
resolve();
}
ctx.response.body = `Result: ${ctx.state.userId} should be equal to ${ctx.request.url.pathname}.`;
};
// And this is from the Oak FAQ (https://oakserver.github.io/oak/FAQ).
interface MyState {
userId: string;
}
const app = new Application<MyState>();
app.use(async (ctx, next) => {
// do whatever checks to determine the user ID
// We'll take the path as the userid to prove the point
ctx.state.userId = ctx.request.url.pathname;
await next();
// delete ctx.state.userId;
ctx.state.userId = "whatever";
});
const router = new Router();
router
.get("/1", sayHello) //
.get("/2", sayHello);
app.use(router.routes());
await app.listen({ port: 1993 });
Then executing in 3 separate terminals in order:
deno run --allow-net main.ts
And
curl -X GET http://localhost:1993/1
And
curl -X GET http://localhost:1993/2
Which yields:
The id /1 should be equal to /1.
The id /2 should be equal to /2.
The id /2 should be equal to /2.
The id /2 should be equal to /1. // <--- This is not as intended.
To achieve a request specific state that is stable across the entire lifetime of the request regardless of resolution order of any promises involved, which is what you need for the authentication pattern to work, you need to introduce a middleware which does something along the lines of:
async (ctx: Context, next: Continuation) => {
ctx.state = {}; // or new whatever.
await next();
};
Am I missing something obvious here or are the authentication examples broken since the state object is shared across all requests and therefore cannot be used to store the userid / session id.
Issue Analytics
- State:
- Created 3 years ago
- Reactions:4
- Comments:7 (3 by maintainers)
I have some thoughts on these two points:
As a user, I’d really appreciate keeping “kernel space” (data provided by Oak) separate from “user space” (data provided by middleware). For example, keeping
ctx.request
as a pristine (and ideally immutable) representation of the client’s request so that there is no pollution and related unpredictability as arequest
traverses to down-stack middlewares.I see
ctx.state
as an Application-scoped “global” user-memory space. Missing is a per-request user-memory space that ought to be kept separate from thectx.request
. Thus I introducectx.locals
in #284. Otherwise, there is currently no type-safe and concurrency-safe way for middleware to store data in the stack. The benefit of storing such data in the stack is that it is naturally garbage collected w/o concern of leaking sensitive data between requests.Implemented the
registerSource
suggestion at #250. The benefits are it is a bit stricter than just assigning data in middleware. Being built on getters it will only compute properties when they are called. Currently ambivalent on it and recognise it does not really fit into the whole middleware design.On the other hand a
state
property on theRequest
object would be straightforward to implement + including types so that is probably a better suggestion at this stage. The only thing about it beingPartial<RequestState>
is that you would have to tell TypeScript that the property exists… but that should be fine.