Open aheissenberger opened 8 months ago
Thanks for asking. No, you don't miss anything. I've been thinking some auth example would be necessary too.
I'm not sure if middleware will be a final solution (and I don't think #329 will be for auth anyway), but yes, it's the only solution for now. Let's see how much we can go with it.
Let's say if we somehow checked credential in middleware, what would be the expected response? Would be easy for redirect for HTML, and 401 Forbidden for RSC, but too limited?
Currently there are two common approaches when going to a page which needs auth:
The middleware will check the credentials and will provide user data in the context. It is than up to the server component to check this context and decide based on the permissions of the user what to do - e.g. show alternative content, a login or redirect to a login page.
I could try a prototype - or do you think I should wait for a later release of WAKU?
No, it's a good timing to try and learn what's good and bad.
FYI, check this thread: https://twitter.com/sebmarkbage/status/1765414733820129471
FYI, check this thread: https://twitter.com/sebmarkbage/status/1765414733820129471
I do not now any framework where auth is not handled at the middleware. The tweet does not offer a solution as you cannot show parts of the layout and check only at the data layer for permission.
My current implementation ist doing this in the middleware:
He is right, that accessing a database in the middleware is a performance problem but this is only the case for sessions which are database bound. Token based session do not need any database.
The routing of the authenticated or not authenticated users should be handled by the router or in the page component. So currently redirecting and 404 are all not handled by the middleware.
@dai-shi How can I read and write to the context from a server function.
Currently I get this error when I use the getContext
function:
Error: [Bug] No render context found
at Module.getContext (/waku/packages/waku/dist/server.js:37:15)
at register (/waku/examples/xx_auth/src/func.ts:25:43)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async file:///waku/packages/waku/dist/lib/renderers/rsc-renderer.js:32:25
'use server';
import { pool } from "./lib/db.js";
import { lucia } from "./lib/auth.js";
import { generateId ,Session, User} from "lucia";
import { Argon2id } from "oslo/password";
import {getContext} from 'waku/server';
export const register = async (formData: FormData) => {
const name = formData.get('name');
const email = formData.get('email');
const password = formData.get('password');
const hashedPassword = await new Argon2id().hash(password);
const userId = generateId(15);
try {
const db = await pool.getConnection();
await db.execute(`INSERT INTO user (id, username, password,email,name) VALUES (?, ?, ?,?,?)`, [userId, email, hashedPassword, email, name]);
const session = await lucia.createSession(userId, {});
const context = getContext<{ session: Session,user: User }>();
context.session = session;
} catch (e) {
console.log(e);
}
};
How do you call the register
function? It needs to be inside React so to say.
except for the context - my code is working as expected as a server function:
/// <reference types="react/canary" />
/// <reference types="react-dom/canary" />
'use client';
import { useFormStatus } from 'react-dom';
const RegisterButton = () => {
const { pending } = useFormStatus();
return (
<>
<button disabled={pending} type="submit">
Register
</button>
{pending ? 'Pending...' : null}
</>
);
};
const RegistartionForm = ({ register }) => {
return (
<div>
<h1>Registration Form</h1>
<form action={register} >
<div>
<label htmlFor="name">Name:</label>
<input type="text" id="name" name="name" required />
</div>
<div>
<label htmlFor="email">Email:</label>
<input type="email" id="email" name="email" required />
</div>
<div>
<label htmlFor="password">Password:</label>
<input type="password" id="password" name="password" required />
</div>
<div>
<RegisterButton />
</div>
</form>
</div>
)
}
export default RegistartionForm
and here is the root server component:
import RegistrationForm from "../components/RegistrationForm.js";
import { register } from "../func.js";
const RegistrationPage = () => {
return (
<RegistrationForm register={register} />
);
}
export default RegistrationPage;
Hmm, it looks fine to me. You may need to find a difference from 05_actions example. Wait, that example uses rerender
but not getContext
. But, it should be the same situation...
There is no exiting example using getContext in a server function or a server component. How can I help to find this problem?
See #584
I have been building a Middleware-based example to facilitate cookie-based authentication. I’m not sure, however, what the best way to protect routes would be. Does any of you have a suggestion?
Checking the request pathname against a set of protected routes is not great, as this doesn’t cover server components that are fetched by client-side React.
With regards to auth in server actions, is there already a way to set a 401 header if the user is not authorized?
With regards to auth in server actions, is there already a way to set a 401 header if the user is not authorized?
I'm not sure if this is going to be a proper way, but if a thrown object has .statusCode
property, it will use it as the status code in the response.
Great, I’ll experiment with that! It’s especially useful in server actions.
The main thing I’m stuck on is properly validating route access for server components. I have tried checking access inside of a server component, but that just doesn’t feel right. Throwing an error at that point also crashes the server. But at any rate, existing frameworks don’t seem to do it this way either. It seems most natural to me to check access on a route level, through the route configuration or a middleware.
Do you have any direction you would like this to go in? If I figure out a good way to do this I’d love to contribute it, as an example or a low-level integrated API in Waku.
It seems most natural to me to check access on a route level, through the route configuration or a middleware.
https://github.com/dai-shi/waku/issues/576#issuecomment-1983495551 I'm really not sure how it should be.
But, for now, if you need a route level access check, you can try it with middleware. It's the only option.
@daanlenaerts access logic for a server function is similar to validation. What you need there is a context to the current authenticated user role which you then add to your ORM call which also handles the access rights. Converting a session token in a cookie to a user needs to happen in the middleware.
It seems most natural to me to check access on a route level, through the route configuration or a middleware.
#576 (comment) I'm really not sure how it should be.
But, for now, if you need a route level access check, you can try it with middleware. It's the only option.
Makes sense!
@daanlenaerts access logic for a server function is similar to validation. What you need there is a context to the current authenticated user role which you then add to your ORM call which also handles the access rights. Converting a session token in a cookie to a user needs to happen in the middleware.
For sure, I've got that working already. The DX for it isn't great though at the moment, I'll have to figure out a way to improve what happens when access is not granted. Possible the .statusCode
error property @dai-shi mentioned will go a long way.
For sure, I've got that working already. The DX for it isn't great though at the moment, I'll have to figure out a way to improve what happens when access is not granted. Possible the
.statusCode
error property @dai-shi mentioned will go a long way.
Have a look at graphQL or tRPC - both of them handle this great. You will need a custom Error Class you throw on the server and handle this on the client.
Hi there, I just dropped it. It almost works well but WIP. https://twitter.com/t6adev/status/1783504614370967937
This might be a little off topic, but @t6adev, I was reading through your code and saw you are using Scrypt instead of Argon2 as a hashing algorithm. In my experiments I also noticed Argon2 is not working when the project is built, neither is Bcrypt.
Do you have any ideas or pointers on how to make Vite work with Argon2 or Bcrypt? (Bcrypt doesn't seem to work as the Node crypto module cannot be resolved.)
Hi, @daanlenaerts ! I don't have any consideration about what hash algorithm I should use. Currently, I'm just following it:
Argon2id is recommended, and if it's not possible, scrypt is recommended.
https://oslo.js.org/reference/password/
Anyway, I resolved how to set it up in vite.config.ts to use Argon2,
/** @type {import('vite').UserConfig} */
export default ({ mode }: { mode: string }) => ({
...(mode === 'development'
? { optimizeDeps: { exclude: ['oslo/password'] } }
: {
ssr: {
external: ['oslo/password'],
},
}),
});
Interesting, excluding it from Vite seems like a good solution to these kinds of issues. Thanks for sharing @t6adev!
I am looking into creating a simple auth example with a home page and some protected sub routes.
Do I miss something, but:
I know that #329 was post boned, but today using an OAuth 2 authorization flow is more common than using a password database.
I still would start if I get a hint on how to deal with 1. and would use Lucia as the auth layer in the middleware. Maybe I can use the middleware to implement the callback and do not need #329.