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

XX_auth example #576

Open
aheissenberger opened this issue Mar 7, 2024 · 23 comments
Open

XX_auth example #576

aheissenberger opened this issue Mar 7, 2024 · 23 comments

Comments

@aheissenberger
Copy link
Contributor

I am looking into creating a simple auth example with a home page and some protected sub routes.

Do I miss something, but:

  1. I could not find any docs on how to define a protected (conditional) route which covers all sub routes with waku route or a guard hook like https://vike.dev/guard .
  2. will need to define an API endpoint to catch the callbacks in the OAuth process (Open ID)

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.

@dai-shi
Copy link
Owner

dai-shi commented Mar 7, 2024

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?

@aheissenberger
Copy link
Contributor Author

Currently there are two common approaches when going to a page which needs auth:

  1. User will be redirected to a login page url - this will require to store the original request in the context to allow a recall / redirect after the login
  2. Login will replace the content of the target page and a simple reload after the login will provide the content of the target url

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?

@dai-shi
Copy link
Owner

dai-shi commented Mar 7, 2024

No, it's a good timing to try and learn what's good and bad.

@dai-shi
Copy link
Owner

dai-shi commented Mar 7, 2024

FYI, check this thread: https://twitter.com/sebmarkbage/status/1765414733820129471

@aheissenberger
Copy link
Contributor Author

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:

  1. get the session ID from the cookie
  2. get the User by the session from the database

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.

@aheissenberger
Copy link
Contributor Author

aheissenberger commented Mar 7, 2024

@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);
    }
};

@dai-shi
Copy link
Owner

dai-shi commented Mar 8, 2024

How do you call the register function? It needs to be inside React so to say.

@aheissenberger
Copy link
Contributor Author

aheissenberger commented Mar 8, 2024

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;

@dai-shi
Copy link
Owner

dai-shi commented Mar 8, 2024

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...

@aheissenberger
Copy link
Contributor Author

There is no exiting example using getContext in a server function or a server component. How can I help to find this problem?

@dai-shi
Copy link
Owner

dai-shi commented Mar 8, 2024

See #584

@dai-shi
Copy link
Owner

dai-shi commented Mar 10, 2024

#587 will fix.

@daanlenaerts
Copy link
Contributor

daanlenaerts commented Apr 14, 2024

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?

@dai-shi
Copy link
Owner

dai-shi commented Apr 16, 2024

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.

@daanlenaerts
Copy link
Contributor

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.

@dai-shi
Copy link
Owner

dai-shi commented Apr 16, 2024

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.

@aheissenberger
Copy link
Contributor Author

@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.

@daanlenaerts
Copy link
Contributor

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.

@aheissenberger
Copy link
Contributor Author

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.

@t6adev
Copy link
Contributor

t6adev commented Apr 25, 2024

Hi there, I just dropped it. It almost works well but WIP.
https://twitter.com/t6adev/status/1783504614370967937

@daanlenaerts
Copy link
Contributor

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.)

@t6adev
Copy link
Contributor

t6adev commented May 1, 2024

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'],
        },
      }),
});

@daanlenaerts
Copy link
Contributor

Interesting, excluding it from Vite seems like a good solution to these kinds of issues. Thanks for sharing @t6adev!

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

No branches or pull requests

4 participants