Code Monkey home page Code Monkey logo

remix-auth-oauth2's People

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

remix-auth-oauth2's Issues

Question about OAuth2Profile typings

Hey, I had a question about OAuth2 profiles. Currently in this library, they are typed like this:

image
image

This means that any type of profile I pass in must extend from that OAuth2Profile interface, even if it doesn't include all of those properties.

How can I type this to only be what the OAuth API provides?

Redirecting the user to the callback URL results in SyntaxError: Unexpected end of JSON input

Currently I call the authenticator.authenticate function with the remix-auth-facebook strategy whenever someone posts to the following route with this action, where dataSource corresponds to Facebook:

export async function action({ request, params }: DataFunctionArgs) {
	const dataSourceName = DataSourceNameSchema.parse(params['data_source'])
	try {
		return await dataSourceAuthenticator.authenticate(dataSourceName, request)
	} catch (error: unknown) {
		console.log(error)
		if (error instanceof Response) {
			console.log(await error.json())
			const formData = await request.formData()
			const rawRedirectTo = formData.get('redirectTo')
			const redirectTo =
				typeof rawRedirectTo === 'string'
					? rawRedirectTo
					: getReferrerRoute(request)
			const redirectToCookie = getRedirectCookieHeader(redirectTo)
			if (redirectToCookie) {
				error.headers.append('set-cookie', redirectToCookie)
			}
		}
	}
}

And the authenticate function stops working whenever remix-auth-oauth2 tries to redirect the user to the provided callback URL

		if (url.pathname !== callbackURL.pathname) {
			debug('Redirecting to callback URL')
			let state = this.generateState()
			debug('State', state)
			session.set(this.sessionStateKey, state)
			let url = this.getAuthorizationURL(request, state).toString()
			debug('AuthorizationUrl', url)

			throw redirect(this.getAuthorizationURL(request, state).toString(), {
				headers: {
					'Set-Cookie': await sessionStorage.commitSession(session),
				},
			})
		}

The logged error is equal to:

SyntaxError: Unexpected end of JSON input
    at JSON.parse (<anonymous>)
    at Response.json (/home/ralph/ventures/argus/node_modules/@remix-run/web-fetch/src/body.js:162:15)
    at processTicksAndRejections (node:internal/process/task_queues:95:5)
    at action10 (file:///home/ralph/ventures/argus/build/index.js?update=1701289190908:5860:19)
    at /home/ralph/ventures/argus/node_modules/@sentry/remix/cjs/utils/instrumentServer.js:2:3500
    at Object.callRouteActionRR (/home/ralph/ventures/argus/node_modules/@remix-run/server-runtime/dist/data.js:11:190)
    at callLoaderOrAction (/home/ralph/ventures/argus/node_modules/@remix-run/router/dist/router.cjs.js:11:77600)
    at submit (/home/ralph/ventures/argus/node_modules/@remix-run/router/dist/router.cjs.js:11:64446)
    at queryImpl (/home/ralph/ventures/argus/node_modules/@remix-run/router/dist/router.cjs.js:11:63629)
    at Object.queryRoute (/home/ralph/ventures/argus/node_modules/@remix-run/router/dist/router.cjs.js:11:62814)
POST /auth/data-source/facebook?_data=routes/_auth+/auth.data-source.$data_source 500 - - 27.790 ms

I have no idea why it is giving me this error. I found a solution where I return the redirect as follows:

return redirect(this.getAuthorizationURL(request, state).toString(), {
				headers: {
					'Set-Cookie': await sessionStorage.commitSession(session),
				},
			}) as any as Promise<User>

But this is ofcourse not ideal. Do you have any suggestions on what I can do here or is anyone else experiencing this?

Missing state on session

The state is set correctly in the browser's cookies (and decoding the cookie shows me the state is correct), but the server thinks it's not there.

"State doesn't match" race condition

We started running into occasional Auth0 failures with "State doesn't match". After a day of banging my head against the wall, I think I have a pretty good idea of what's going on:

https://stackoverflow.com/questions/65493296/authorization-code-flow-concurrent-requests-from-multiple-tabs

If a user simultaneously loads multiple pages while unauthenticated, the result is a race condition:

  1. Tab 1 updates state and redirects to OAuth
  2. Tab 2 updates state and redirects to OAuth
    • Overwriting Tab 1 state
  3. Tab 1 callback fails due to state mismatch
  4. Tab 2 callback succeeds

This is pretty common when reopening a closed browser, for example.

Support query params for token request

Hi, this is a bit of an odd situation, and I'm not sure how it should best be handled. I am dealing with an OAuth provider that requires all parameters for the token request to be sent as query parameters instead of body parameters. So for example, the provider requires that the token request look like this:

curl --location --request POST 'https://some-provider.com/oauth/authorize/?grant_type=authorization_code&redirect_uri=<param>&client_id=<param>&client_secret=<param>&code=<param>'

instead of the more standard version supported by this library:

curl --location --request POST 'https://some-provider.com/oauth/authorize/' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=<param>' \
--data-urlencode 'redirect_uri=<param>' \
--data-urlencode 'client_id=<param>' \
--data-urlencode 'client_secret=<param>' \
--data-urlencode 'code=<param>'

I'm not sure if this is worth creating a setting for, or if I should just create a new strategy to handle this requirement. Creating a new strategy that extends this behavior isn't too hard:

  async fetchAccessToken(
    code: string,
    params: URLSearchParams
  ) {
    // ...

    // Add all parameters as query parameters, as required by this provider
    const url = new URL(this.tokenURL);
    url.search = params.toString();
    let response = await fetch(url.toString(), { // This is what's different
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: params, // This isn't necessary
    });

  // ...

But the fetchAccessToken (https://github.com/sergiodxa/remix-auth-oauth2/blob/main/src/index.ts#L302) method is private, so it seems like it wasn't intented to be extended. I'm curious if that method could be made protected, or if this is a common enough scenario that it might be worth adding configuration to handle it. Please let me know if I can clarify anything or provide a PR to handle this situation.

Support Basic authentication header

As per the OAuth 2.0 Standard Section 2.3.1:

The authorization server MUST support the HTTP Basic authentication scheme for authenticating clients that were issued a client password.

As far as I can tell, this is not supported in the OAuth 2.0 Strategy.

I can create a PR for you to look at!

OneLogin ERR_TOO_MANY_REDIRECTS after update to v2.0.0

Hi

After updating to v2.0.0 and modifying the "options" parameters to the new naming convention I now get a constant loop due to the Authenticate function failing:L

This page isn’t xxxxxx.onelogin.com redirected you too many times.
[Try deleting your cookies](https://support.google.com/chrome?p=rl_error&hl=en-GB).
ERR_TOO_MANY_REDIRECTS

All the code is exactlyt the same as before all that was done was upgrade to v2.0.0 and change the parameter names to match the new convention.
Have other parts also changed that would cause OIDC flow to stop working?
I changed back to v1.11.2 and everything is back to working.

Also looking at the readme, it seems some of the parameters in the samples don't work and may have changed in v 2.0.0 and the readme not updated. (I am sure they used to work- not related to this issue though, more of an FYI)

Add proper logout

Current usage

Right now I have to manually do the auth0 logout by doing a double redirect in my logout function:

export const loader: LoaderFunction = async ({ request }) => {
  return authenticator.logout(request, {
    redirectTo: `https://${appConfig.auth0.issuerBaseUrl}/v2/logout?client_id=${appConfig.auth0.clientId}&returnTo=${appConfig.auth0.baseUrl}/auth/login`,
  });
};

Expected usage

export const loader: LoaderFunction = async ({ request }) => {
  return authenticator.logout(request, {
    redirectTo: `/auth/login`,
  });
};

Allow setting redirectURI based on request object

Hi!

Would it be possible to enable allowing to set the redirectURI based on the request object? In particular, I would like to be able to set the host of the redirectURI. In my current app environment, the hostname is not available as an environment variable and we automatically generate PR servers, making dynamically setting the redirectURI quite useful.

Thanks!

Grant Type customization

Hi just discovered the remix-auth package and was going to start digging deep, trying to get my grant (password based) I noticed that the grant_type is hard coded to be authorization_code while I'm using a password grant, is there any chance I can change the grant_type in the configuration?

Feature request: make `fetchAccessToken` protected instead of private

Currently the fetchAccessToken method is private, but it would be helpful for extending the Strategy if it were protected instead.

My specific use case is using the fetchAccessToken method to rotate my tokens using a refresh token. That method currently supports the ability to rotate a refresh token, but because it's private, it's not accessible from any classes that are extending this strategy.

I can currently get around the private classification by using @ts-ignore, but I'd rather not be cheating.

Thanks for the great library!

Breaking error in 1.11.0 - Asked for scope that doesn't exist on the resource

I use remix-auth, remix-auth-microsoft, and remix-auth-oauth2 to authenticate using Office 365 in my tenant.

After updating from 1.10.0 to 1.11.0, authentication fails with the following error:

GET /auth/microsoft/callback?error=invalid_client&error_description=AADSTS650053%3a+The+application+%27test-app-microsoft%27+asked+for+scope+%27openid%2cprofile%2cemail%27+that+doesn%27t+exist+on+the+resource+%2700000003-0000-0000-c000-000000000000%27.+Contact+the+app+vendor.

`uuid` dependency or dev dependency?

In the latest release uuid has been moved from dependencies to devDependencies. In environments that prune the dev dependencies this then causes an error on running the build:

Error: Cannot find module 'uuid'

So it looks like uuid should be moved back to dependencies?

Unusable behind proxy

The authentication process times out when the server is behind a proxy.

A possible fix would be to let developers override the fetch function and provide their own.

Error: Missing state on session

Has been working fine with hugging face for about a week until today, when on login I started getting errors like: Missing state on session in the authenticator.authenticate() call.

My config looks like:

new OAuth2Strategy(
    {
      authorizationURL: "https://huggingface.co/oauth/authorize",
      tokenURL: "https://huggingface.co/oauth/token",
      clientID: process.env.HF_CLIENT_ID!,
      clientSecret: process.env.HF_CLIENT_SECRET!,
      useBasicAuthenticationHeader: true,
      callbackURL: "http://localhost:8080/auth/callback",
      scope: "openid profile email read-repos manage-repos",
    },

and:

export async function loader(args: LoaderFunctionArgs) {
  const searchParams = new URL(args.request.url).searchParams
  const redirectTo = searchParams.get("redirectTo")

  try {
    return await authenticator.authenticate(
      huggingfaceAuthenticator,
      args.request,
      {
        successRedirect: "/dashboard",
        throwOnError: true,
      }
    )
  } catch (error) {
    if (error instanceof Response) {
      // Let's inject the cookie to set
      if (redirectTo) {
        error.headers.set(
          "set-cookie",
          await signinRedirectCookie.serialize(redirectTo)
        )
      }
      return error
    }

    return redirect(
      "/signin?failed=" + encodeURIComponent((error as Error).message)
    )
  }
}
export const signinRedirectCookie = createCookie("signin-redirect", {
  sameSite: "lax",
  path: "/",
  httpOnly: true,
  secrets: [process.env.COOKIE_SECRET!],
  secure: false, // process.env.NODE_ENV === "production", // enable this in prod only
})

Nothing has changed in my code, but it seems I am unable to manually provide a state? Not sure what they changed, but it seems I am unable to accommodate for it with existing configuration options?

It also causes the browser to loop:

[14:41:28.583] DEBUG (7831): loaded URL
    URL: "http://localhost:8080/signin?failed=Missing%20state%20on%20session."
[14:41:28.876] DEBUG (7831): loaded URL
    URL: "http://localhost:8080/signin?failed=Failed%20to%20connect"
[14:41:29.144] DEBUG (7831): loaded URL
    URL: "http://localhost:8080/signin?failed=Failed%20to%20connect"
[14:41:29.263] DEBUG (7831): loaded URL
    URL: "http://localhost:8080/signin?failed=Failed%20to%20connect"
[14:41:29.384] DEBUG (7831): loaded URL
    URL: "http://localhost:8080/signin?failed=Failed%20to%20connect"
[14:41:29.509] DEBUG (7831): loaded URL
    URL: "http://localhost:8080/signin?failed=Failed%20to%20connect"
[14:41:30.754] DEBUG (7831): loaded URL
    URL: "http://localhost:8080/signin?failed=Failed%20to%20connect"
[14:41:30.862] DEBUG (7831): loaded URL
    URL: "http://localhost:8080/signin?failed=Failed%20to%20connect"
[14:41:30.967] DEBUG (7831): loaded URL
    URL: "http://localhost:8080/signin?failed=Failed%20to%20connect"
[14:41:31.083] DEBUG (7831): loaded URL
    URL: "http://localhost:8080/signin?failed=Failed%20to%20connect"
[14:41:31.191] DEBUG (7831): loaded URL
    URL: "http://localhost:8080/signin?failed=Failed%20to%20connect"
[14:41:31.412] DEBUG (7831): loaded URL
    URL: "http://localhost:8080/signin?failed=Failed%20to%20connect"

Edit:
Seems to be related to this chunk of code: https://github.com/sergiodxa/remix-auth-oauth2/blob/main/src/index.ts#L190-L200

Debug looks like:

2024-01-11T19:59:40.647Z OAuth2Strategy Request URL http://localhost:8080/signin?redirectTo=%2Fdashboard
2024-01-11T19:59:40.647Z OAuth2Strategy Callback URL URL {}
2024-01-11T19:59:40.649Z OAuth2Strategy Redirecting to callback URL
2024-01-11T19:59:40.649Z OAuth2Strategy State 4ffd6f02-c2dc-4465-9e7e-ddbc87350bd7
2024-01-11T19:59:42.095Z OAuth2Strategy Request URL http://localhost:8080/auth/callback?code=bvDHoCWdtrGmCfeL&state=4ffd6f02-c2dc-4465-9e7e-ddbc87350bd7
2024-01-11T19:59:42.095Z OAuth2Strategy Callback URL URL {}
2024-01-11T19:59:42.095Z OAuth2Strategy State from URL 4ffd6f02-c2dc-4465-9e7e-ddbc87350bd7
2024-01-11T19:59:42.095Z OAuth2Strategy State from session undefined
2024-01-11T19:59:42.099Z OAuth2Strategy Request URL http://localhost:8080/signin?failed=Missing%20state%20on%20session.
2024-01-11T19:59:42.099Z OAuth2Strategy Callback URL URL {}
2024-01-11T19:59:42.099Z OAuth2Strategy Redirecting to callback URL
2024-01-11T19:59:42.099Z OAuth2Strategy State 761d5749-c61f-415e-b979-c1edc325cbed
2024-01-11T19:59:42.160Z OAuth2Strategy Request URL http://localhost:8080/auth/callback?code=zPEEeDzlgIuPLLWY&state=761d5749-c61f-415e-b979-c1edc325cbed
2024-01-11T19:59:42.161Z OAuth2Strategy Callback URL URL {}
2024-01-11T19:59:42.161Z OAuth2Strategy State from URL 761d5749-c61f-415e-b979-c1edc325cbed
2024-01-11T19:59:42.161Z OAuth2Strategy State from session 761d5749-c61f-415e-b979-c1edc325cbed
2024-01-11T19:59:42.161Z OAuth2Strategy State is valid
2024-01-11T19:59:42.372Z OAuth2Strategy Failed to verify user ECONNREFUSED: Failed to connect
    at <anonymous> (/Users/dangoodman/code/sellmyai/node_modules/pg-pool/index.js:47:11)
    at promiseReactionJob (native)
    at processTicksAndRejections (native) {
  code: 'ECONNREFUSED',
  syscall: 'connect',
  errno: 0,
  stack: 'Error: Failed to connect\n' +
    '    at <anonymous> (/Users/dangoodman/code/sellmyai/node_modules/pg-pool/index.js:47:11)\n' +
    '    at promiseReactionJob (native)\n' +
    '    at processTicksAndRejections (native)'
}

Verify function does not work

I'm trying to use this lib to connect to a Keycloak auth server. I'm not trying to expand this lib, just trying to use it as the read me shows. When clicking to authenticate, I'm redirected to the Keycloak service to authenticate and when I succeed and redirected back to my /callback route the authenticator.isAuthenticated always returns null:

// app/routes/callback.tsx
import type { LoaderFunctionArgs } from "@remix-run/node"
import { json } from '@remix-run/node'
import { authenticator } from "~/services/auth.server"

export const loader = async ({ request }: LoaderFunctionArgs) => {
  const data = await authenticator.isAuthenticated(request)

  console.log("data:", data)

  return json({ data })
}

image
image

Which is weird, because the cookies that the Keycloak service sets and the _session cookie that I configured with the remix-auth all have content. The _session cookie is defined as:

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

export let sessionStorage = createCookieSessionStorage({
  cookie: {
    name: '_session',
    sameSite: "lax",
    path: "/",
    httpOnly: true,
    secrets: ["secret"],
    secure: process.env.NODE_ENV === 'production'
  }
})

export let { commitSession, destroySession, getSession } = sessionStorage

Also, the remix-auth is configured as such:

// app/services/auth.server.ts
import { Authenticator } from 'remix-auth'
import { OAuth2Strategy } from 'remix-auth-oauth2'

import { sessionStorage } from '~/services/session.server'

export let authenticator = new Authenticator(sessionStorage)

authenticator.use(
  new OAuth2Strategy(
    {
      authorizationURL: "http://localhost:8080/realms/teste/protocol/openid-connect/auth",
      tokenURL: "http://localhost:8080/realms/teste/protocol/openid-connect/token",
      clientID: process.env.OIDC_CLIENT_ID as string,
      clientSecret: process.env.OIDC_CLIENT_SECRET as string,
      callbackURL: "http://localhost:3000/callback",
      scope: "openid", // optional
      useBasicAuthenticationHeader: false // defaults to false
    },
    async ({ profile }) => {
      console.log("profile:", profile)
      return profile
    }
  ),
  "oidc-keycloak"
)

I don't think this is a problem with the Keycloak service because again, the session is created successfully.

Should I expand this lib to be able to use it with Keycloak? Or is it really something going wrong here?

Enable OAuth to work with sessions that are using sameSite: Strict

There is a way, explored by Brock Allen, to use sameSite: "Strict" for the session cookie while using OAuth2 redirects for login.

I prototyped it some, and I used the OAuth state to store the session, but that's probably not ideal. Even when POSTing back to the callback. For the final success or failure redirects, I used the redirectHtml function shown below and that worked well. It allowed the final isAuthorized page to use the strict session cookies as normal.

The final part needed is probably to do the same for the incoming callback. That is, to store the information from the callback into a cookie, then perform an HTML redirect to pick up the session cookies and continue from there.

export function redirectHtml(url: string, init: number | ResponseInit = {}) {
  let responseInit = init;

  if (typeof responseInit === "number") {
    responseInit = {
      status: responseInit,
    };
  }

  let headers = new Headers(responseInit.headers);

  if (!headers.has("Content-Type")) {
    headers.set("Content-Type", "text/html; charset=utf-8");
  }

  const body = `<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="refresh" content="0;url=${url}" />
</head>
</html>`;

  return new Response(body, { ...responseInit, headers });
}

Error when authorization redirect uses same domain as Remix app

Hey there, thank you for developing the oauth2 auth strategy!

We ran into an issue, because the oauth provider we're trying to redirect to is running on the same domain as the Remix app.

The Remix app is served at /
The oauth provider (Keycloak) is served at /login.
This is probably not a common setup, but we cannot change it at the moment.

Using the library is currently causing issues, because when remix-auth-oauth2 is throwing the redirect, Remix tries to do a client side navigation, which errors, because we don't have a /login route.

It looks like this can be avoided by throwing redirectDocument(...) instead of redirect(...), to force a hard reload when redirecting, as described in the remix docs. Do you think remix-auth-oauth2 could switch to redirectDocument per default, or does that not make sense for the project? It should be a drop-in replacement.

The only other workaround for us I can think of is to set the magic X-Remix-Reload-Document header on the response ourselves (after catching it), which I'd like to avoid, as it seems to be an implementation detail. Or to fork remix-auth-oauth2 of course.

Issue getting token from Cognito token endpoint since upgrading to V2

Hi,

I'm having issues with getting the access token from Cognito token endpoint since upgrading to V2.

This is the example code of a successful request on Cognito's doc:

POST https://mydomain.auth.us-east-1.amazoncognito.com/oauth2/token&
                            Content-Type='application/x-www-form-urlencoded'&
                            Authorization=Basic ZGpjOTh1M2ppZWRtaTI4M2V1OTI4OmFiY2RlZjAxMjM0NTY3ODkw
                            
                            grant_type=authorization_code&
                            client_id=1example23456789&
                            code=AUTHORIZATION_CODE&
                            redirect_uri=com.myclientapp://myclient/redirect

After troubleshooting, I discovered a couple of things:

  • There's an open issue on @oslojs/oauth2 that the Authorization header is missing the Basic prefix
  • remix-auth-oauth2's authenticatemethod sets either headers or body, but not both (not sure if I'm mistaken here). However Cognito's request seems to need both.

I'm not sure if it's a bug (by cognito or remix-auth-oauth2) to be fixed, or do I need to extend the strategy and override authenticate.

Any advice?

How can I configure the issuer, clientId, and clientSecret at request time while still abstracting?

Help! Been stuck on this for hours.... 😩

I have a requirement where each customer has unique credentials for issuer, clientID, and clientSecret. (Okta)

I don't know which credentials to configure on the strategy until the user tells me which tenant they are trying to log in to. At that point I can look up the tenant and grab the credentials. Therefore I can't create the strategy until after that point.

This is the problem I'm having (and it might be due to me not understanding typescript):
I import my instance of the authenticator:

export const authenticator = new Authenticator<string | undefined>(storage, {
  sessionKey: 'userEmail',
});

Then in my action function I create the strategy and register it on the authenticator:

import {authenticator} from 'auth.server.tsx';

export async function action({ request }: ActionArgs) {
  const user: User = getUserFromDB();
  if (user.company.oktaClientId && user.company.oktaClientSecret && user.company.oktaDomain) {
    let oktaStrategy = new OktaStrategy(
      {
        issuer: `https://${user.company.oktaDomain}/oauth2/default`,
        clientID: user.company.oktaClientId,
        clientSecret: user.company.oktaClientSecret,
        callbackURL: `${process.env.FRONT_URL}/auth/okta/authorization-code/callback`,
      },
      async ({ accessToken, refreshToken, extraParams, profile }) => {
        // Do stuff with profile
      }
    );
    authenticator.use(oktaStrategy);
    try {
      return authenticator.authenticate('okta', request);
    } catch (error) {
      //....
    }
  }
}

As soon as I try to call authenticator, i get the error:

strategy okta not found

which leads me to think the registration isn't happening.

I HAVE gotten this all to work by defining everything inline in the action function. (i.e. creating the authenticator, creating the strategy, registering the strategy, and calling authenticate.) Seems like something about importing it from another module breaks it. While it worked, I would much rather be able to remove all the authenticator logic from my login route, including configuring the strategy.

Do I need to have some kind of singleton for my Authenticator?

Any help would be very appreciated.

Support PKCE

Some OAuth2 providers, such as Twitter, require using PKCE to complete the authorization code flow. It could be preferable to have this extension configurable in this Strategy rather than having to override the authenticate method in extended Strategies.

P.S. I understand that this is not a standard requirement for most OAuth2 flows, but I believe an extended Strategy would have a very similar authenticate method which is why I opened this issue, however I understand if you don't want to add something so niche to the library.

state value is not removed when cookie based session storage is used

The value of the state parameter is not removed from the session after successful authentication when a cookie based session storage is used in the application. The state is unset in the implementation but the new session is not committed in the response so the value stays in the session until the end of the validity of the session.
The session size is limited by the cookie size (4096 bytes) and therefore it would be nice to get rid of session values, which are useless.

Respect url search param in callback URL`

Hi Sergio, thanks heaps for all the open source work you've done for the community!

I'd like to request a feature, so that it is possible to redirect to somewhere after successful login. The use case:

  1. User goes directly to protected url, e.g. https://example.com/my-path-somewhere/123
  2. The user is not logged in (e.g. session has expired) - they are therefore redirected to the /auth/login?redirect_uri=https://example.com/my-path-somewhere/123 which redirects them to the oauth2 provider, e.g. auth0 universal login page.
  3. User types in login credentials and after successful authentication is redirected to /auth/callback?redirect_uri=https://example.com/my-path-somewhere/123.
  4. We can now grab the redirect_uri from the request.url and set the the successRedirect to /my-path-somewhere/123 (or manually redirect).

I think to implement this all what needs to be changed is the getCallbackURL method to respect the url search params of the url argument.

I'm keen on doing this myself and contributing to the library, just let us know please if that's ok.

Cheers, Katarina

Dynamic callbackURL

First of all, thanks a lot for your work on this library!

I'm trying to incorporate it into our stack and have just hit a crucial issue: I need the callbackURL to be dynamic. Our code serves multiple domains and only when the user attempts to authenticate do we know what the callbackURL should be. Is this by any chance possible?

With passport, this was achieved by providing an overriding callbackURL during the authenticate call, like:

 app.get('/api/auth/facebook', (req, res, next) =>
    passport.authenticate('facebook', {
      callbackURL: getFacebookCallbackUrl(req)
    })(req, res, next),
  );

Thanks a lot in advance!

callbackURL response being ignored after 302 redirect from auth URL

Hey! I don't know where to post this, as it could be either an issue with this library, a remix-auth issue, or just the way my OAuth provider is handling the request and I'm completely new to Remix.

I also don't know how to provide reproduction steps, since it might be specific to this provider.

So basically what is happening, is that my callbackURL is being completely ignored, and I'm getting redirected back to the login route. BUT the first login with my provider is working, since it redirects to the provider's website and asks for consent. But once we have consent, the logins after that are "instant" (no consent asked).

But this "instant" functionality is making my callbackURL be skipped entirely.

Here's the logs when no consent is given, and the flow is normal:

GET / 200 - - 12.049 ms
  OAuth2Strategy Request URL http://localhost:4200/api/auth/login +0ms
  OAuth2Strategy Callback URL URL {
  href: 'http://localhost:4200/api/auth/callback',
  origin: 'http://localhost:4200',
  protocol: 'http:',
  username: '',
  password: '',
  host: 'localhost:4200',
  hostname: 'localhost',
  port: '4200',
  pathname: '/api/auth/callback',
  search: '',
  searchParams: URLSearchParams {},
  hash: ''
} +0ms
  OAuth2Strategy Redirecting to callback URL +2ms
  OAuth2Strategy State 9b7048dd-bbf8-4730-a8b6-d065f40759f0 +0ms
POST /api/auth/login?_data=routes%2Fapi%2Fauth%2Flogin 204 - - 9.372 ms
  OAuth2Strategy Request URL http://localhost:4200/api/auth/callback?code=def50200470... +3s
  OAuth2Strategy Callback URL URL {
  href: 'http://localhost:4200/api/auth/callback',
  origin: 'http://localhost:4200',
  protocol: 'http:',
  username: '',
  password: '',
  host: 'localhost:4200',
  hostname: 'localhost',
  port: '4200',
  pathname: '/api/auth/callback',
  search: '',
  searchParams: URLSearchParams {},
  hash: ''
} +1ms
  OAuth2Strategy State from URL 9b7048dd-bbf8-4730-a8b6-d065f40759f0 +1ms
  OAuth2Strategy State from session 9b7048dd-bbf8-4730-a8b6-d065f40759f0 +0ms
  OAuth2Strategy State is valid +0ms
Fetching user...
Fetched user hallowatcher
TODO create or fetch user { provider: 'osu', id: '1874761', username: 'hallowatcher' }
  OAuth2Strategy User authenticated +1s
GET /api/auth/callback?code=def502004... 302 - - 1291.926 ms
Homepage User { provider: 'osu', id: '1874761', username: 'hallowatcher' }
GET / 200 - - 9.630 ms

Here's the logs once consent has been given, and the OAuth provider instantly redirects back:

GET / 200 - - 1651.086 ms
  OAuth2Strategy Request URL http://localhost:4200/api/auth/login +0ms
  OAuth2Strategy Callback URL URL {
  href: 'http://localhost:4200/api/auth/callback',
  origin: 'http://localhost:4200',
  protocol: 'http:',
  username: '',
  password: '',
  host: 'localhost:4200',
  hostname: 'localhost',
  port: '4200',
  pathname: '/api/auth/callback',
  search: '',
  searchParams: URLSearchParams {},
  hash: ''
} +1ms
  OAuth2Strategy Redirecting to callback URL +2ms
  OAuth2Strategy State 49fa4df2-6721-49d3-847a-b4507d1ac04d +0ms
POST /api/auth/login?_data=routes%2Fapi%2Fauth%2Flogin 204 - - 9.126 ms
Error: You made a GET request to http://localhost:4200/api/auth/login but did not provide a default component or `loader` for route "routes/api/auth/login", so there is no way to handle the request.
GET /api/auth/login 500 - - 7.771 ms

And here's the network tab for that:
image

As you can see, the authorize and callback requests have no statuses, so my browser tries to GET the login route which doesn't exist, only POST exists for that. And adding a redirect from the login route also does not help, it simply redirects and the user is not logged in in the end. But it does allow the flow to complete, here's the logs with a redirect from the login page:

GET /?_data=root 200 - - 4.842 ms
  OAuth2Strategy Request URL http://localhost:4200/api/auth/login +9m
  OAuth2Strategy Callback URL URL {
  href: 'http://localhost:4200/api/auth/callback',
  origin: 'http://localhost:4200',
  protocol: 'http:',
  username: '',
  password: '',
  host: 'localhost:4200',
  hostname: 'localhost',
  port: '4200',
  pathname: '/api/auth/callback',
  search: '',
  searchParams: URLSearchParams {},
  hash: ''
} +1ms
  OAuth2Strategy Redirecting to callback URL +0ms
  OAuth2Strategy State 64d7c39a-7d51-40c7-98d5-9772e64e84e6 +1ms
POST /api/auth/login?_data=routes%2Fapi%2Fauth%2Flogin 204 - - 6.430 ms
  OAuth2Strategy Request URL http://localhost:4200/api/auth/callback?code=def5020027... +547ms
  OAuth2Strategy Callback URL URL {
  href: 'http://localhost:4200/api/auth/callback',
  origin: 'http://localhost:4200',
  protocol: 'http:',
  username: '',
  password: '',
  host: 'localhost:4200',
  hostname: 'localhost',
  port: '4200',
  pathname: '/api/auth/callback',
  search: '',
  searchParams: URLSearchParams {},
  hash: ''
} +1ms
  OAuth2Strategy State from URL 64d7c39a-7d51-40c7-98d5-9772e64e84e6 +0ms
  OAuth2Strategy State from session 64d7c39a-7d51-40c7-98d5-9772e64e84e6 +1ms
  OAuth2Strategy State is valid +0ms
GET /api/auth/callback?code=def5020027f3... - - - - ms
GET /api/auth/login 302 - - 4.362 ms
Homepage User null
GET / 200 - - 11.102 ms
Fetching user...
Fetched user hallowatcher
TODO create or fetch user { provider: 'osu', id: '1874761', username: 'hallowatcher' }
  OAuth2Strategy User authenticated +1s

As you can see, the OAuth strategy actually completes in this case, but it is not awaited. The user is already on the page for a second or two before OAuth2Strategy User authenticated is shown.


Here's my business logic for reference:

app/src/Layout.tsx

...
<Form action="/api/auth/login" method="post">
  <Button type="submit">
   Log in
  </Button>
</Form>
...

app/routes/api/auth/login.ts

// export let loader: LoaderFunction = () => redirect('/');

export const action: ActionFunction = ({ request }) => {
  return authenticator.authenticate('osu', request);
}

app/routes/api/auth/callback.ts

export const loader: LoaderFunction = ({ request }) => {
  return authenticator.authenticate('osu', request, {
    successRedirect: '/',
    failureRedirect: '/'
  });
}

app/services/osu.strategy.ts

export class OsuStrategy<User> extends OAuth2Strategy<User, OsuProfile, OsuExtraParams> {
  public readonly name = 'osu';

  private readonly scope: string;
  private readonly userInfoURL: string;

  // We receive our custom options and our verify callback
  constructor(
    options: OsuStrategyOptions,
    verify: StrategyVerifyCallback<User, OAuth2StrategyVerifyParams<OsuProfile, OsuExtraParams>>
  ) {
    const configuration: OAuth2StrategyOptions = {
      authorizationURL: `https://osu.ppy.sh/oauth/authorize`,
      tokenURL: `https://osu.ppy.sh/oauth/token`,
      clientID: options.clientID,
      clientSecret: options.clientSecret,
      callbackURL: options.callbackURL,
      responseType: 'code'
    };

    super(configuration, verify);

    this.userInfoURL = `https://osu.ppy.sh/api/v2/me/osu`;
    this.scope = options.scope || 'public';
  }

  protected authorizationParams() {
    const urlSearchParams: Record<string, string> = {
      scope: this.scope,
    };

    return new URLSearchParams(urlSearchParams);
  }

  protected async userProfile(accessToken: string): Promise<OsuProfile> {
    console.log('Fetching user...');
    let response = await fetch(this.userInfoURL, {
      headers: { Authorization: `Bearer ${accessToken}` },
    });

    let data: ApiUser = await response.json();

    console.log('Fetched user', data.username);
    return {
      provider: this.name,
      id: String(data.id),
      username: data.username,
    } as OsuProfile;
  }
}

app/services/osu.strategy.ts

export let authenticator = new Authenticator<unknown>(sessionStorage);

const osuStrategy = new OsuStrategy(
  {
    clientID: '...',
    clientSecret: '...',
    callbackURL: 'http://localhost:4200/api/auth/callback',
    scope: 'friends.read'
  },
  async ({ profile }) => {

    console.log('TODO create or fetch user', profile);

    return profile;
  }
);

authenticator.use(osuStrategy);

app/services/session.server.ts

export let sessionStorage = createCookieSessionStorage({
  cookie: {
    name: '_session',
    sameSite: 'lax',
    path: '/',
    httpOnly: true,
    secrets: ['s3cr3t'], // TODO replace with actual secret
    secure: process.env.NODE_ENV === 'production',
  },
});

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

I know this is a lot to look at, and I'm not expecting much. I'm currently checking out Remix and seeing if it fits my needs in order to change from Angular + NestJS. Thanks!

Missing `client_id`

I am trying to set up remix-auth-oauth2 with Google and Microsoft. In both cases, I see that I am getting an error like:

The request body must contain the following parameter: 'client_id'

I would think that I could use the simple out-of-the-box OAuth2Strategy for each but that fails:

new OAuth2Strategy(
  {
    authorizationEndpoint:
      'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
    tokenEndpoint: `https://login.microsoftonline.com/common/oauth2/v2.0/token`,
    clientId: microsoftClientId,
    clientSecret: microsoftClientSecret,
    redirectURI: `${baseUrl}/auth/microsoft/callback`,
    scopes: ['openid', 'profile', 'email'],
  },

Have pretty much the an analogous setup with Google, and I see the same error when I put debugging in the underlying @oslojs/oauth2 library (note that there is a problem there where error gets propagated error_description doesn't, the latter has the actual failing info). Am I doing something wrong or is this a bug?

Consider account linking

Hello, great work!

I was looking how an account linking strategy could be implemented and looking at a few passport examples, I saw it was possible to access the request (and knowing if a user is already signed in or not). Would you consider having this implemented?

I think it may also need something like #14 if the "already logged in then ignore" behavior is to be kept.

Sources:

Custom state object

We've had this library implemented for some time, and we continue to encounter challenges with the current recommendation to use session storage to store auth state (like returnTo) ourselves:

  • Because authenticate() calls getSession(), there is a race condition with our own getSession() calls that cause new sessions to be lost.
  • There is a race condition between tabs during simultaneous authentication that causes our returnTo values to overwrite each other.

#73 (comment) includes some messy workarounds for these problems.

Between the above and the additional work to gracefully recover from #67, our auth routes have become quite the mess. 😕

My request is to add support to authenticate for an optional state object stored in session storage, plus an optional successCallback to get it back.

As prior art, passport-oauth2 has offered basically the same thing since v1.6.0:

https://medium.com/passportjs/application-state-in-oauth-2-0-1d94379164e

I think this feature would make this strategy much more robust and easier to use.

I realize feature request #1 was declined at the time, but I'm not asking to change the value of state; I just want to store additional auth-related state alongside it in session storage. This would also line up nicely with a potential future fix to #67.

`refreshToken` type should be optional

The refreshToken type is currently defined as non-optional:

refreshToken: string;

This doesn't match the OAuth 2.0 specification (source):

Issuing a refresh token is optional at the discretion of the authorization server.

Auth0 doesn't issue refresh tokens by default:

image

Currently if we access verifyParams.refreshToken, the types assure us of a string when in fact we will always get undefined. I think the types should be widened to reflect this.

Sorry if this has been discussed before, but I couldn't find anything!

Support for custom authorization URL query parameters

Auth0 supports a few query parameters for the ability to customize the login flow. Notably login_hint for prefilling the username/email and connection for specifying the social connection (which saves the user a click).

I'm looking to add support for passing in these query parameters for the authorization URL for each call to authenticator.authenticate(). It seems like the only way to pass data to a Strategy without changing the interface would be via options.context.

I'm happy to make a PR for this but would like some input here in case I'm missing something or there's a better approach.

"Unexpected end of JSON input" error due to unimplemented response.clone in bun.

I implemented Google login using remix-auth-oauth2 and remix-auth-google, based on remix-on-bun by @sergiodxa . However, I encountered an "Unexpected end of JSON input" error when calling authenticator.authenticate in the callback.

Upon investigation, I found that the issue is due to the lack of Response.clone implementation in Bun. This causes an error in response.json() when getAccessToken tries to read from a cloned response created in fetchAccessToken.

While waiting for Bun to implement this might be an option, it seems unnecessary to clone the response in fetchAccessToken in the first place.

I've managed to get it working by applying the following patch on my end. Is cloning really necessary?

diff --git a/node_modules/remix-auth-oauth2/build/index.js b/node_modules/remix-auth-oauth2/build/index.js
index 78f2712..1597705 100644
--- a/node_modules/remix-auth-oauth2/build/index.js
+++ b/node_modules/remix-auth-oauth2/build/index.js
@@ -251,7 +251,7 @@ class OAuth2Strategy extends remix_auth_1.Strategy {
             let body = await response.text();
             throw body;
         }
-        return await this.getAccessToken(response.clone());
+        return await this.getAccessToken(response);
     }
 }
 exports.OAuth2Strategy = OAuth2Strategy;

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.