Web Developer
Currently coding Daffy.org
Previously @prmkr, @platzi (YC W15), @vercel, and @able_co.
Organizer of @techtalks_pe.
Repos are my own.
A Remix Auth strategy for working with forms.
License: MIT License
Hey Sergio!
Given this action:
export async function action({ request }: ActionArgs) {
try {
const form = await request.formData();
validateEmail(form.get("email"));
validatePasswordAndConfirmation(form.get("password"), form.get("password"));
await authenticator.authenticate("user-pass", request, {
throwOnError: true,
context: { formData: form },
successRedirect: "/profile",
});
} catch (error) {
if (error instanceof AuthorizationError) {
return json({ error: errorForToast(error.message) }, { status: 401 });
}
// Because redirects work by throwing a Response, it needs to be re-thrown.
// Any other generic errors must also be thrown too.
//
// The "instanceof Response" line is added here for clarity, but "throw error"
// would cover it already.
if (error instanceof Response) throw error;
throw error;
}
}
If any kind of error is thrown internally, such Prisma throwing that the database is down, (
Line 36 in 5e3d408
this.failure
, and then it gets wrapped in AuthorizationError
by remix-auth here: https://github.com/sergiodxa/remix-auth/blob/d4e4f85745b13b0bbbb08bdd12375a91de48fd84/src/strategy.ts#LL125C35-L125C35
So effectively a prisma Error ends up being an AuthorizationError so it's treated no differently from user errors.
How can I determine what's an "AuthenticationError" such as wrong username, or password, which I'm doing by specifically throwing that error from within my Authenticator code [1], so that it's shown to users, versus internal server errors, that must not be displayed and should instead return a 500 or be caught by the CatchBoundary?
In my remix-auth-form strategy I'm specifically throwing AuthorizationError
, but the way error handling is done it seems to cast a wider net:
export const authUserWithEmailAndPassword = async ({
inputEmail,
inputPassword,
}: loginUserArgs): Promise<User> => {
const blacklisted = await existsByDomainOrEmail(inputEmail);
if (blacklisted) {
throw new AuthorizationError(
"Sorry, your account has been suspended for breaching our Terms of Service."
);
}
const user = await getUserByEmail(inputEmail);
if (!user || !user.password) {
throw new AuthorizationError("Incorrect email or password");
}
const passwordMatches = await bcrypt.compare(inputPassword, user.password);
if (!passwordMatches) {
await incrementFailedLogins(user);
throw new AuthorizationError("Incorrect email or password");
}
await createLoginRecord({ userId: user.id, email: inputEmail });
return user;
Thanks for creating this module.
Unfortunately it is not clear how to use this.
remix-auth: 3.2.1
remix-auth-form: 1.1.1
after configuring everything and hitting the login button I get an error saying request.formData
is not a function. I tranced it back to the request.clone()
found in authenticator.js:88
return strategyObj.authenticate(request.clone(), this.sessionStorage, {...
removing the .clone()
and I get no error.
could it be that cloning removed the formData ?
I am trying to work out how to get some arbitrary data into the form strategy as a means of creating an history of sign-in but the type on the context object seems strange.
export async function action({ context, request }: ActionArgs) {
return await authenticator.authenticate("form", request, {
successRedirect: "/",
failureRedirect: "/login",
context, // optional
});
};
on this one, you are passing the remix context - which is all good.
but then you also override it with formData on the next one.
export async function action({ context, request }: ActionArgs) {
let formData = await request.formData();
return await authenticator.authenticate("form", request, {
// use formData here
successRedirect: formData.get("redirectTo"),
failureRedirect: "/login",
context: { formData }, // pass pre-read formData here
});
};
but it should still be typed as AppLoadContext, It seems like this type might want to be generic or somehow user customizable? is that a fair assumption?
(ps happy to submit a pr if worthwhile)
Would it be worth providing a small hash function to the authenticate method?
This adds a bit of opinion about the right way to store passwords, but in this case I think a bit of well educated opinion would go a long way toward encouraging good security practices with a well chosen hash, like PBKDF2 which crypto-js supports (and is Cloudflare workers compliant)
authenticate({ form, hash }) { }
Hi,
trying to pass t helper for yup validation inside FormStrategy but it fails.
What is the right way to do it?
import { useTranslation } from "react-i18next";
export let authenticator = new Authenticator<User>(sessionStorage, {
sessionErrorKey: "sessionErrorKey"
});
authenticator.use(
new FormStrategy(async ({ form }) => {
const { t } = useTranslation('common');
const email = form.get('email') as string;
const password = form.get('password') as string;
const validationResult = await userValidationSchema(t).validate(Object.fromEntries(form), {abortEarly: true}).catch((err) => err);
if(validationResult.errors){
throw new AuthorizationError(validationResult.errors[0]);
}
const user = await validateUser(email, password);
// console.log('user', user);
if (user !== null) {
return await Promise.resolve({ ...user as User});
} else {
// if problem with user throw error AuthorizationError
throw new AuthorizationError("Bad Credentials")
}
}),
);
I get request.formData is not a function
error when I submit my login form
// auth.server.ts
import { Authenticator } from "remix-auth"
import { FormStrategy } from "remix-auth-form"
import type { User } from "@prisma/client"
import { loginSchema } from "~/validation"
import { sessionStorage } from "~/services/session.server"
export const authenticator = new Authenticator<User>(sessionStorage)
authenticator.use(
new FormStrategy(async ({ form }) => {
const userInput = Object.fromEntries(form)
const { email, password } = await loginSchema.validate(userInput)
let user = await login(email, password)
return user
}),
"form"
)
// routes/login.tsx
import type { VFC } from "react"
import type { ActionFunction } from "remix"
import { Form } from "remix"
import { authenticator } from "~/services/auth.server"
export const action: ActionFunction = async ({ request }) => {
await authenticator.authenticate("form", request, {
failureRedirect: "/login",
successRedirect: "/",
})
return {}
}
const Login: VFC = () => {
return (
<Form method="post">
<label>
email
<input type="email" name="email" />
</label>
<label>
password
<input type="password" name="password" />
</label>
<button type="submit">Login</button>
</Form>
)
}
export default Login
"react": "^17.0.2",
"react-dom": "^17.0.2",
"remix": "^1.0.6",
"remix-auth": "^2.5.0-0",
"remix-auth-form": "^1.1.1",
"@remix-run/dev": "^1.0.6",
"@remix-run/react": "^1.0.6",
"@remix-run/serve": "^1.0.6",
I don't see any way to add the ability for a user to check a "remember me" box on the form to set the maxAge value of the session to something like 30 days.
In Remix-land, this would be done with a session commit like this:
await sessionStorage.commitSession(session, {
maxAge: remember
? 60 * 60 * 24 * 7 // 7 days
: undefined,
}),
But Remix-Auth doesn't seem to have a way to provide an "options" object that can be passed to the commitSession call.
With the current solution an user is authenticated as long as a valid session cookie is provided. What I miss is a checkSession
function like in remix-auth-supabase to validate the user against the database while he still has a valid session cookie. When checkSession
is called successfully we can also set the session cookie again to keep the user logged in.
How do you like the idea? I would be open to help with the implementation.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.