Coder Social home page Coder Social logo

remix-auth's Introduction

Remix Auth

Simple Authentication for Remix

Features

  • Full Server-Side Authentication
  • Complete TypeScript Support
  • Strategy-based Authentication
  • Easily handle success and failure
  • Implement custom strategies
  • Supports persistent sessions

Overview

Remix Auth is a complete open-source authentication solution for Remix.run applications.

Heavily inspired by Passport.js, but completely rewrote it from scratch to work on top of the Web Fetch API. Remix Auth can be dropped in to any Remix-based application with minimal setup.

As with Passport.js, it uses the strategy pattern to support the different authentication flows. Each strategy is published individually as a separate npm package.

Installation

To use it, install it from npm (or yarn):

npm install remix-auth

Also, install one of the strategies. A list of strategies is available in the Community Strategies discussion.

Usage

Remix Auth needs a session storage object to store the user session. It can be any object that implements the SessionStorage interface from Remix.

In this example I'm using the createCookieSessionStorage function.

// app/services/session.server.ts
import { createCookieSessionStorage } from "@remix-run/node";

// export the whole sessionStorage object
export let sessionStorage = createCookieSessionStorage({
  cookie: {
    name: "_session", // use any name you want here
    sameSite: "lax", // this helps with CSRF
    path: "/", // remember to add this so the cookie will work in all routes
    httpOnly: true, // for security reasons, make this cookie http only
    secrets: ["s3cr3t"], // replace this with an actual secret
    secure: process.env.NODE_ENV === "production", // enable this in prod only
  },
});

// you can also export the methods individually for your own usage
export let { getSession, commitSession, destroySession } = sessionStorage;

Now, create a file for the Remix Auth configuration. Here import the Authenticator class and your sessionStorage object.

// app/services/auth.server.ts
import { Authenticator } from "remix-auth";
import { sessionStorage } from "~/services/session.server";

// Create an instance of the authenticator, pass a generic with what
// strategies will return and will store in the session
export let authenticator = new Authenticator<User>(sessionStorage);

The User type is whatever you will store in the session storage to identify the authenticated user. It can be the complete user data or a string with a token. It is completely configurable.

After that, register the strategies. In this example, we will use the FormStrategy to check the documentation of the strategy you want to use to see any configuration you may need.

import { FormStrategy } from "remix-auth-form";

// Tell the Authenticator to use the form strategy
authenticator.use(
  new FormStrategy(async ({ form }) => {
    let email = form.get("email");
    let password = form.get("password");
    let user = await login(email, password);
    // the type of this user must match the type you pass to the Authenticator
    // the strategy will automatically inherit the type if you instantiate
    // directly inside the `use` method
    return user;
  }),
  // each strategy has a name and can be changed to use another one
  // same strategy multiple times, especially useful for the OAuth2 strategy.
  "user-pass"
);

Now that at least one strategy is registered, it is time to set up the routes.

First, create a /login page. Here we will render a form to get the email and password of the user and use Remix Auth to authenticate the user.

// app/routes/login.tsx
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
import { Form } from "@remix-run/react";
import { authenticator } from "~/services/auth.server";

// First we create our UI with the form doing a POST and the inputs with the
// names we are going to use in the strategy
export default function Screen() {
  return (
    <Form method="post">
      <input type="email" name="email" required />
      <input
        type="password"
        name="password"
        autoComplete="current-password"
        required
      />
      <button>Sign In</button>
    </Form>
  );
}

// Second, we need to export an action function, here we will use the
// `authenticator.authenticate method`
export async function action({ request }: ActionFunctionArgs) {
  // we call the method with the name of the strategy we want to use and the
  // request object, optionally we pass an object with the URLs we want the user
  // to be redirected to after a success or a failure
  return await authenticator.authenticate("user-pass", request, {
    successRedirect: "/dashboard",
    failureRedirect: "/login",
  });
};

// Finally, we can export a loader function where we check if the user is
// authenticated with `authenticator.isAuthenticated` and redirect to the
// dashboard if it is or return null if it's not
export async function loader({ request }: LoaderFunctionArgs) {
  // If the user is already authenticated redirect to /dashboard directly
  return await authenticator.isAuthenticated(request, {
    successRedirect: "/dashboard",
  });
};

With this, we have our login page. If we need to get the user data in another route of the application, we can use the authenticator.isAuthenticated method passing the request this way:

// get the user data or redirect to /login if it failed
let user = await authenticator.isAuthenticated(request, {
  failureRedirect: "/login",
});

// if the user is authenticated, redirect to /dashboard
await authenticator.isAuthenticated(request, {
  successRedirect: "/dashboard",
});

// get the user or null, and do different things in your loader/action based on
// the result
let user = await authenticator.isAuthenticated(request);
if (user) {
  // here the user is authenticated
} else {
  // here the user is not authenticated
}

Once the user is ready to leave the application, we can call the logout method inside an action.

export async function action({ request }: ActionFunctionArgs) {
  await authenticator.logout(request, { redirectTo: "/login" });
};

Advanced Usage

Custom redirect URL based on the user

Say we have /dashboard and /onboarding routes, and after the user authenticates, you need to check some value in their data to know if they are onboarded or not.

If we do not pass the successRedirect option to the authenticator.authenticate method, it will return the user data.

Note that we will need to store the user data in the session this way. To ensure we use the correct session key, the authenticator has a sessionKey property.

export async function action({ request }: ActionFunctionArgs) {
  let user = await authenticator.authenticate("user-pass", request, {
    failureRedirect: "/login",
  });

  // manually get the session
  let session = await getSession(request.headers.get("cookie"));
  // and store the user data
  session.set(authenticator.sessionKey, user);

  // commit the session
  let headers = new Headers({ "Set-Cookie": await commitSession(session) });

  // and do your validation to know where to redirect the user
  if (isOnboarded(user)) return redirect("/dashboard", { headers });
  return redirect("/onboarding", { headers });
};

Changing the session key

If we want to change the session key used by Remix Auth to store the user data, we can customize it when creating the Authenticator instance.

export let authenticator = new Authenticator<AccessToken>(sessionStorage, {
  sessionKey: "accessToken",
});

With this, both authenticate and isAuthenticated will use that key to read or write the user data (in this case, the access token).

If we need to read or write from the session manually, remember always to use the authenticator.sessionKey property. If we change the key in the Authenticator instance, we will not need to change it in the code.

Reading authentication errors

When the user cannot authenticate, the error will be set in the session using the authenticator.sessionErrorKey property.

We can customize the name of the key when creating the Authenticator instance.

export let authenticator = new Authenticator<User>(sessionStorage, {
  sessionErrorKey: "my-error-key",
});

Furthermore, we can read the error using that key after a failed authentication.

// in the loader of the login route
export async function loader({ request }: LoaderFunctionArgs) {
  await authenticator.isAuthenticated(request, {
    successRedirect: "/dashboard",
  });
  let session = await getSession(request.headers.get("cookie"));
  let error = session.get(authenticator.sessionErrorKey);
  return json({ error }, {
    headers:{
      'Set-Cookie': await commitSession(session) // You must commit the session whenever you read a flash
    }
  });
};

Remember always to use the authenticator.sessionErrorKey property. If we change the key in the Authenticator instance, we will not need to change it in the code.

Errors Handling

By default, any error in the authentication process will throw a Response object. If failureRedirect is specified, this will always be a redirect response with the error message on the sessionErrorKey.

If a failureRedirect is not defined, Remix Auth will throw a 401 Unauthorized response with a JSON body containing the error message. This way, we can use the CatchBoundary component of the route to render any error message.

If we want to get an error object inside the action instead of throwing a Response, we can configure the throwOnError option to true. We can do this when instantiating the Authenticator or calling authenticate.

If we do it in the Authenticator, it will be the default behavior for all the authenticate calls.

export let authenticator = new Authenticator<User>(sessionStorage, {
  throwOnError: true,
});

Alternatively, we can do it on the action itself.

import { AuthorizationError } from "remix-auth";

export async function action({ request }: ActionFunctionArgs) {
  try {
    return await authenticator.authenticate("user-pass", request, {
      successRedirect: "/dashboard",
      throwOnError: true,
    });
  } catch (error) {
    // Because redirects work by throwing a Response, you need to check if the
    // caught error is a response and return it or throw it again
    if (error instanceof Response) return error;
    if (error instanceof AuthorizationError) {
      // here the error is related to the authentication process
    }
    // here the error is a generic error that another reason may throw
  }
};

If we define both failureRedirect and throwOnError, the redirect will happen instead of throwing an error.

remix-auth's People

Contributors

aaronadamsca avatar acairns avatar antanasmisiunas avatar aphofstede avatar aydrian avatar cliffordfajardo avatar coreyleelarson avatar dependabot[bot] avatar dokeet avatar edgesoft avatar edmundhung avatar faheempatel avatar gsawers1 avatar hyphenized avatar jacobparis avatar jfsiii avatar jnicklas avatar jonnybnator avatar juhanakristian avatar kyh avatar lgastler avatar lifeiscontent avatar machour avatar michaeldeboey avatar myleslinder avatar pbteja1998 avatar penx avatar sergiodxa avatar tessellator avatar welldan28 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

remix-auth's Issues

Consider shipping each strategy separately

While I am working on #49, it feels like there will always be case that works only on some environments. It might requires node package that cannot polyfill or depends on package specific to certain platforms.

All strategies are bundled together at the moment. But maybe it worth consider shipping them separately:
Either (1) As independent package or (2) As subfolders (i.e. import { LocalStrategy } from "remix-auth/strategies/LocalStrategy";

Authorization

Right now Remix Auth handles authentication but does nothing about authorization except giving you a way to check if the user is authenticated or not, the package could come with some built in way to do authorization in a per route basis.

User is not logged in when verify data contains UTF-8 characters

Describe the bug

When the data returns from verify function contains UTF-8, cookie is set, authenticator redirects to successRedirect but user isn't logged in.

Your Example Website or App

thangngoc89@73c6ed2

Steps to Reproduce the Bug or Issue

  1. Clone https://github.com/thangngoc89/remix-auth/
  2. git checkout utf8-bug
  3. cd examples/local
  4. npm run dev
  5. Go to http://localhost:3000
  6. Click on login
  7. Enter password abc123 and click login

Expected behavior

User should be logged in.

Screenshots or Videos

No response

Platform

  • OS: [e.g. macOS, Windows, Linux]
  • Browser: [e.g. Chrome, Safari, Firefox]
  • Version: [e.g. 91.1]

Additional context

No response


Update: I've tracked this error to this line https://github.com/remix-run/remix/blob/a69a631cb5add72d5fb24211ab2a0be367b6f2fd/packages/remix-server-runtime/cookies.ts#L156

Since atob and btoa require that data must be Latin1 encoding, putting utf-8 data inside the cookie session would throw. I'm not sure why remix has silenced this error but I think this isn't a remix-auth's error

Suggestions on how to fix this is available at https://stackoverflow.com/a/26603875

can't install with remix version 0.21.0

โฏ npm install --save remix-auth
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: remix-pesonal-cms@undefined
npm ERR! Found: @remix-run/[email protected]
npm ERR! node_modules/@remix-run/react
npm ERR!   @remix-run/react@"^0.21.0" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer @remix-run/react@"^0.20.1" from [email protected]
npm ERR! node_modules/remix-auth
npm ERR!   remix-auth@"^2.2.0" from the root project
npm ERR!
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force, or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR!
npm ERR! See C:\Users\PoshI\AppData\Local\npm-cache\eresolve-report.txt for a full report.

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\PoshI\AppData\Local\npm-cache\_logs\2021-11-18T07_54_37_799Z-debug.log

GitHub Strategy with CF Workers

Currently there is an error when you want to use the GitHub Strategy from remix-auth with Cloudflare Workers.

The error occurs when trying to retrieve profile information from the GitHub API. This request fails with an error status of 403. The error occurs because GitHub needs to force a user agent header, which is apparently not sent by the worker fetch. (https://docs.github.com/en/rest/overview/resources-in-the-rest-api#user-agent-required)

The solution is quite simple, just add a user agent header to the profile request.

However, I'm not sure how best to implement it. You could either set a fixed User-Agent (e.g. remix-auth-app) or forward the User-Agent from the incoming request. I think it makes no sense to have it configured as it probably only affects CloudFlare workers at the moment and other platforms automatically fill in the user agent.

protected async userProfile(accessToken: string): Promise<GitHubProfile> {
let response = await fetch(this.userInfoURL, {
headers: {
Accept: "application/vnd.github.v3+json",
Authorization: `token ${accessToken}`,
},
});

let response = await fetch(this.userInfoURL, {
  headers: {
     Accept: "application/vnd.github.v3+json",
     Authorization: `token ${accessToken}`,
+    User-Agent: "remix-auth-app"
  },
});

Heres a example repo with a custom github.ts to fix the error.

Identity Provider

Remix Auth could not only give you a way to authenticate users against a third party service using oAuth2, it could also give you everything to become a identity provider using oAuth2 yourself, allowing you to expose the required endpoints for someone to login with your app.

Error installing remix-auth v2.1.1

I am trying to install remix-auth into a remix app and am receiving the following error:

โžœ  my-remix-app npm i remix-auth
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR! 
npm ERR! While resolving: remix-app-template@undefined
npm ERR! Found: [email protected]
npm ERR! node_modules/remix
npm ERR!   remix@"^0.20.1" from the root project
npm ERR! 
npm ERR! Could not resolve dependency:
npm ERR! peer remix@"^0.19.2" from [email protected]
npm ERR! node_modules/remix-auth
npm ERR!   remix-auth@"*" from the root project
npm ERR! 
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force, or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.

Throw Response in case of error

With Remix 0.19 we will have a feature to do throw new Response() and use that to redirect the user or send a 4xx response.

Remix Auth could have built-in support for that in different cases:

  1. When calling auth.isAuthenticated(request) we could pass an object to ask for a redirect auth.isAuthenticated(request, { redirectTo: "/login" }) and if you do that run throw new redirect(options.redirectTo) or throw a 401 in case it's not authenticated by adding { protected: true } to mark the route as protected.
  2. When calling auth.authenticate("local", request) we throw an AuthorizationError in some cases, it could change to be a Response with a 400 or 401.

Example callback will leak user session to client

Describe the bug

The use of returning the result of authenticator.authenticate in the examples allows the client to re-submit the request once logged in and get a json response of the user session. Usually this content is protected by storage or encrypted cookies.

https://github.com/sergiodxa/remix-auth/blob/main/README.md?plain=1#L124-L132

This applies to the examples in the remix-auth-oauth2 repo even more since it's probable for access tokens to be stored in the user session.

A better approach would be simply await it and then throw an error:

  await authenticator.authenticate("user-pass", request, {
    successRedirect: "/dashboard",
    failureRedirect: "/login",
  });

  throw new Error("already logged in");

Your Example Website or App

N/A

Steps to Reproduce the Bug or Issue

N/A

Expected behavior

I expect the examples to show a more secure path.

Screenshots or Videos

No response

Platform

N/A

Additional context

No response

failureRedirect not working with multiple forms

Describe the bug

I'm having an issue receiving authentication errors while using multiple form elements. Here's a demo of my login page, the component has a login form and create an account form.

Screen.Recording.2022-07-10.at.1.42.10.AM.mov

FORM 1
If I login with a random email and password, the correct error is presented:

Screen Shot 2022-07-10 at 1 44 25 AM

If I login with correct email and password, a successful redirect happens.

====

FORM 2
If I login with correct email, confirmation email and valid password, a successful redirect happens.

If I login with mismatching email and confirmation email (this throws exception inside strategy), nothing happens, but an error should be returned. If I refresh the page, the error appears. After checking the networking tab, it seems after the POST there's no subsequent GET request to fetch the error from the Loader.

Screen.Recording.2022-07-10.at.1.46.01.AM.mov

Any idea what's happening here?

Your Example Website or App

https://github.com/sergiodxa/remix-auth

Steps to Reproduce the Bug or Issue

See > Describe The Bug

Platform

  • OS: macOS
  • Browser: Brave
  • Version: OS 12.4

Additional context

LoaderFunction of /login.tsx

Screen Shot 2022-07-10 at 2 19 39 AM

Login Page component of /login.tsx
Screen Shot 2022-07-10 at 2 20 09 AM

ActionFunction of /login.tsx
Screen Shot 2022-07-10 at 2 20 36 AM
As you can see I even made multiple strategies but that didn't fix the issue.

LocalStrategy not found

Describe the bug

Following the example code for Local Strategy, the import for LocalStrategy is missing or not found using
import { Authenticator, LocalStrategy } from "remix-auth";

import { Authenticator, LocalStrategy } from "remix-auth";

Your Example Website or App

examples/local

Steps to Reproduce the Bug or Issue

Install npm packages and try to run the app

Expected behavior

Compiler should not complain about missing import

Screenshots or Videos

Screen Shot 2022-01-22 at 3 18 55 PM

Platform

  • OS: macOS
  • Browser: Chrome, Brave (but N/A)
  • Version: N/A

Additional context

No response

Add SupabaseStrategy

Check if it's possible to use Supebase server-side or come up with a way to use it client-side and have a useful strategy server-side too.

userProfile method is protected even though it's an easy way to access user info

Describe the bug

I would love to be able to access the userProfile method in my remix app to identify the currently logged in user.
Auth0-js has this functionality built in the client and i think we should expose it here unless i'm missing something

Your Example Website or App

https://stackblitz.com/none

Steps to Reproduce the Bug or Issue

image

Expected behavior

I would love to have an easy way to get the use info from the auth0 strategy/authenticator

Screenshots or Videos

No response

Platform

  • OS: [e.g. macOS, Windows, Linux]
  • Browser: [e.g. Chrome, Safari, Firefox]
  • Version: [e.g. 91.1]

Additional context

No response

JWTClaimsError('Invalid audience')

Describe the bug

Using Auth0 and the example application. I notice the accessToken generated cannot be used for to make external API calls to a different backend service verifying tokens with Auth0. The decoded jwt token's aud doesn't match the expected aud. The error returned is JWTClaimsError('Invalid audience').

I haven't dug deep into why, but maybe when setting the strategy, you should allow setting the audience. See

authenticator.use(
  new Auth0Strategy(
    {
      callbackURL: process.env.AUTH0_CALLBACK_URL,
      clientID: process.env.AUTH0_CLIENT_ID,
      clientSecret: process.env.AUTH0_CLIENT_SECRET,
      domain: process.env.AUTH0_DOMAIN,
      scope: process.env.AUTH0_SCOPES,

      audience: process.env.AUTH0_AUDIENCE,  // this may be the fix. :)
    },
...

Your Example Website or App

NA

Steps to Reproduce the Bug or Issue

Expected behavior

Token should be valid

Screenshots or Videos

No response

Platform

  • OS: macOS
  • Browser: Chrome

Additional context

No response

Accept session in addition to request

Discussed in #142

Originally posted by ngbrown March 12, 2022
isAuthenticated and logout take a Request just to get the session. The page very well could have already gotten a session and if it is a storage based session, then it would be advantageous to not fetch from the database multiple times.

My suggestion is to change the signature like this:

  async logout(
    request: Request | Session,
    options: { redirectTo: string }
  ): Promise<void> {
    const session = isSession(request)
      ? request
      : await this._sessionStorage.getSession(request.headers.get("Cookie"));
// the rest is identical ...
}

isSession is provided by remix.

Support customizing OAuth strategy callback url dynamically

This is similar to #35, but I make use of Netlify's preview deployments which can be accessed via several different endpoints. I don't actually know which endpoint a user has hit until the request is in flight. I'd like to be able to customize the callback url of the OAuth strategy based on the contents of the request (in this specific case I'm using Auth0). I can submit a pull request if desired.

How to display failure

Hey,
I want to display the AuthenticationError that is thrown in the login function on the sign in page. Any suggestions on how I could do that or is this even possible?

Thanks

Form Data can not be accessed when calling `authenticator.authenticate`

Describe the bug

I am trying to implement some additional processing on top of authenticator.authenticate that requires me to access formData before calling authenticate. However, given that formData can only be read once it errors out during the authenticate call.

Your Example Website or App

NA

Steps to Reproduce the Bug or Issue

  1. Call await request.formData();
  2. Call await authenticator.authenticate('form-login', request, {});

Expected behavior

It works fine and logs me in (and/or allows me to pass in the already parsed formData)

Screenshots or Videos

No response

Platform

  • OS: Windows and Macos
  • Browser: All Browsers
  • Version: All Versions

Additional context

No response

Broken on Netlify Functions

Describe the bug

I upgraded to the latest versions and now I get this error message on post during authorize:

Error: cannot clone body after it is used
    at Authenticator.authenticate (/var/task/node_modules/remix-auth/build/authenticator.js:86:49)

Full call stack

Jun 15, 10:26:23 AM: 24f9d30b ERROR  Error: cannot clone body after it is used
    at clone (/var/task/node_modules/@remix-run/web-fetch/src/body.js:283:9)
    at new Request (/var/task/node_modules/@remix-run/web-fetch/src/request.js:88:6)
    at NodeRequest.clone (/var/task/node_modules/@remix-run/web-fetch/src/request.js:226:10)
    at NodeRequest.clone (/var/task/node_modules/@remix-run/node/fetch.js:28:18)
    at Authenticator.authenticate (/var/task/node_modules/remix-auth/build/authenticator.js:86:49)
    at action11 (/var/task/.netlify/functions-internal/server.js:6253:32)
    at Object.callRouteAction (/var/task/node_modules/@remix-run/server-runtime/data.js:40:14)
    at handleDataRequest (/var/task/node_modules/@remix-run/server-runtime/server.js:94:18)
    at requestHandler (/var/task/node_modules/@remix-run/server-runtime/server.js:34:18)
    at Runtime.handler (/var/task/node_modules/@remix-run/netlify/server.js:35:20)
    

Works like a charm locally

package.json extract :

    "dependencies": {
      "@netlify/functions": "^1.0.0",
      "@ramp-network/ramp-instant-sdk": "3.1.1",
      "@reduxjs/toolkit": "1.8.2",
      "@remix-run/netlify": "^1.6.0",
      "@remix-run/react": "^1.6.0",
      "remix-auth": "3.2.2",
      "remix-utils": "3.3.0",
      "remix": "1.6.0",
  },

Your Example Website or App

ping me in private

Steps to Reproduce the Bug or Issue

I wish it was that simple.

Expected behavior

Not error out.

Screenshots or Videos

No response

Platform

Netlify

Additional context

No response

[DOCS] Explicitly require @remix-run/server-runtime?

Describe the bug

Just wondering whether the docs should be including the addition of installing @remix-run/server-runtime as a dependency?

It's set as a peer and only works because @remix-run/node requires it as a dependency, but when you switch up to yarn berry it'll eventually error as the resolution is incorrect

Your Example Website or App

Steps to Reproduce the Bug or Issue

Expected behavior

Screenshots or Videos

No response

Platform

Additional context

No response

Upgrade to Remix v0.18

The v0.18 changed how to import types and it makes the types between Remix and Remix Auth incompatible.

"State doesn't match" authorization error after POST status 302(redirect) in the OAuth2 flow

Hi,
Thank you for this package, it is very useful.
I found an issue while using it with AWS Cognito.

After a POST with status 302(redirect) in the OAuth2 flow, the redirect location url has the "state" parameter already decoded and will not match anymore the session state.

I think the correct fix is to encode/decode state when set-to/get-from URLSearchParams but not encode it in the session.
This will work with in any situation, decoding an already decoded value is the same.

async authorize(sessionStorage, session) {
  let state = randomBytes(100).toString("base64");
  session.set(this.sessionStateKey, state);
  ...
  params.set("state", encodeURIComponent(state));
  ...
}

async authenticate(request, sessionStorage, options, callback) {
  ...
   let state = url.searchParams.get("state");
  if (!state)
        throw new AuthorizationError("Missing state.");
  state = decodeURIComponent(state);
  
  if (session.get(this.sessionStateKey) === state) {
  ...
}

Support passing additional information to the Strategy for use during Verify

I have a use-case where my datasource / service instance lives on the Remix AppContext object and is not available when the auth strategy gets initialized.

I would like to be able to pass in some additional parameters from the action/loader when I call auth.authenticate() and then make those parameters available on the verify function call

Better Docs

  • Documentation website
  • Document how to approach a Passport to Remix Auth migration
  • Document all current strategies
  • Create example apps for every strategy
  • Create examples using Cloudflare Workers

GitHubStrategy example crashes app

The GitHubStrategy example in docs crashes the application with error Error: Promise rejected with value when clicking login.

image

Seems like the fix is to return the value of authenticator.authenticate in app/routes/auth/github.tsx

export let action: ActionFunction = ({ request }) => {
  return authenticator.authenticate("github", request);
};

Also the loader in app/routes/auth/github/calllback.tsx needs to be changed to return the value of authenticator.authenticate

export let loader: LoaderFunction = ({ request }) => {
  return authenticator.authenticate("github", request, {
    successRedirect: "/dashboard",
    failureRedirect: "/login",
  });
};

Tested with Remix 0.20.1.

I'd be happy to create a PR for this!

Add support of Cloudflare Workers

As per discussion in the discord channel, we are looking at how to help making remix-auth works with the Cloudflare Workers runtime. Currently there are a few blockers we noticed:

  • Usage @remix-run/server-runtime as peerDependencies instead of @remix-run/node
  • #57
  • #58
  • #59

Can we get around it?

While (1) is critical to be fixed, possibly by replacing it with @remix-run/server-runtime. People can get around issues (2) and (3) by polyfilling crypto themselves:

For example, with esbuild, we can use esbuild-plugin-alias to polyfill the crypto package:

const esbuild = require("esbuild");
const alias = require("esbuild-plugin-alias");

esbuild.build({
  // ...
  plugins: [
    alias({
      crypto: require.resolve("./crypto.js"),
    }),
  ],
});
// ./crypto.js
module.exports = {
  // you can either leave it as an empty object or provides a polyfill for some methods from crypto
};

With this setup, depends on the polyfill setup, you should be able to use strategies not relying on crypto.

Customizable session key

Right now, the authenticate method is always using the session key user to store the data returned by the verify callback of the strategy (this is done on each individual strategy), then the isAuthenticated method of the Authenticator read that same key from the session to check if the user is authenticated.

The key should be configureable, at the moment you create an instance of the Authenticator.

let auth = new Authenticator(sessionStorage, {
  sessionKey: "user",
});

Then if it should be passed to the strategy and isAuthenticate should use that key.

This will also solve an issue if the developer pass a callback to authenticate and set the user data on another session key, we can add a getter to know the key without duplicating the string everywhere.

return auth.authenticate("local", request, async user => {
  let session = await getSession(request.headers.get("Cookie"));
  session.set(auth.key, user); // you don't need to set `user` here, if you later change it will keep working
  let headers = new Headers({ "Set-Cookie": await commitSession(session) });
  return redirect("/private", { headers });
});

createCookieSessionStorage not logged in

Describe the bug

I used OAuth2
After logging in, redirect (successRedirect) is processed in callback and null is returned in isAuthenticated.
But when I use this code, it seems to work normally, so there seems to be a problem with createCookieSessionStorage.

I used Google Translate, so the sentences may be weird.

Your Example Website or App

.

Steps to Reproduce the Bug or Issue

  1. Log in
  2. Redirect to successRedirect
  3. isAuthenticated returned null

Expected behavior

When i use createCookieSessionStorage, isAuthenticated returned my profile.

Screenshots or Videos

No response

Platform

  • Server OS: Linux
  • Browser: Chrome
  • Version: 97.0.4692.71

Additional context

No response

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.