okta / okta-react

Okta OIDC SDK for React
https://github.com/okta/okta-react
Other
113 stars 78 forks source link

Support react-router v6 #178

Closed baumandm closed 1 year ago

baumandm commented 4 years ago

I'm submitting this issue for the package(s):

I'm submitting a:

Current behavior

The upcoming version of React-router, v6, makes a number of breaking changes, specifically breaking <SecureRoute /> due to the deprecation of useHistory() and render props. That is the only breaking feature I've encountered, but there may be others.

Expected behavior

This library should support react-router v6.

Minimal reproduction of the problem with instructions

Use the following react-router-dom dependency:

  "react-router-dom": "^6.0.0-beta.0",

Extra information about the use case/user story you are trying to implement

I've implemented a custom <SecureRoute /> component that is sufficient for my use cases. It's slightly simplified so it might not be a drop-in-replacement for all cases, but I wanted to share in case anyone else needs an interim solution.

import { useOktaAuth } from '@okta/okta-react';
import React from 'react';
import { Route } from 'react-router-dom';

const RequireAuth = ({ children }) => {
  const { authService, authState } = useOktaAuth();

  if (!authState.isAuthenticated) {
    if (!authState.isPending) {
      const fromUri = window.location.href;
      authService.login(fromUri);
    }
    return null;
  }

  return <React.Fragment>{children}</React.Fragment>;
};

const SecureRoute = ({ element, ...props }) => {
  const WrappedComponent = (wrappedProps) => <RequireAuth>{element}</RequireAuth>;
  return <Route {...props} element={<WrappedComponent />} />;
};

export default SecureRoute;

Environment

ZinkNotTheMetal commented 2 years ago

I got my coworker to install the app and his works okay. His version of yarn is 1.22.17

Mine does not work with yarn version 3.2.0

kellenmather commented 2 years ago

react-router v6 also breaks this key piece of functionality (for okta hosted logins):

<Route path="/login/callback" element={LoginCallback} />

The LoginCallback method will not be executed so you wont ever fetch your token upon successful login.

jaredperreault-okta commented 2 years ago

@kellenmather okta-react <SecureRoute /> currently only supports react-router v5. If you want to use v6 I suggest taking a look at the snippets/samples posted in this thread.

anjalichawlaa commented 2 years ago

If the browser cache is cleared, the local storage is removed. Okta react doesn’t handle this scenario and user is not logged out. Old access token is getting used for api calls. How can we handle this scenario?

zhoulytwinyu commented 2 years ago

@kellenmather LoginCallback is still compatible with react-router 6. You need to update the syntax.

<Route path="/login/callback" element={<LoginCallback/>} />
                                       ^             ^^
kenyee commented 2 years ago

any ETA on react-router v6 support?

TD-DO commented 2 years ago

@jaredperreault-okta This seems like a lot of work for us. Is it going to be upgraded soon on your end or should we just start trying out implementations?

jaredperreault-okta commented 2 years ago

To provide an update on this:

Currently we do not plan on adding a SecureRoute component for react-router v6 to this sdk. We have created a series of samples (found here) illustrating how to implement Okta with a few popular react routing libraries (react-router@6.x being one of them)

Main reason for this decision: SecureRoute forces react-router to be in our dependency tree

First, we do not want to position this sdk so it forces the use of other dependencies (like react-router) by downstream applications. We are currently in violation of this tenet.

Second, react-router is listed as a peer dependency of okta-react. This means if we release a new okta-react version that bumps react-router to v6, ALL downstream applications would need to migrate to v6 in order to upgrade okta-react. We place a high priority on making migrations to the latest versions of sdks to be as seamless as possible because our sdk updates sometimes contain vulnerability fixes and security updates

kenyee commented 2 years ago

First...thanks for the examples...that was enough to get things working w/ React Router V6.

Is there any current guidance/example on using the JWT access token for Redux Toolkit's RTK Query? There's a much older sample (not using the useAuthState or the current okta-react SDK) that seems to handle this sample by having the AuthHandler call a slice to set the auth info into the store: https://developer.okta.com/blog/2019/08/12/build-secure-react-application-redux-jwt.

e.g. something like this I'd expect to work but doesn't (authState is always undefined):

const oktaAuth = new OktaAuth(oktaAuthConfig);
const AppRoutes = () => {
    const history = useHistory();

    const userInfo = useAuthUser();
    const {authState} = useOktaAuth() || {};
    useEffect(() => {
        console.log("Authstate is " + JSON.stringify(authState))
        if (authState) {
            if (authState.isAuthenticated) {
                const user: CurrentUser = {
                    id: String(userInfo?.ldap),
                    displayName: String(userInfo?.name),
                    accessToken: String(authState.accessToken)
                }
                setAuthSuccess(user)
            } else {
                setLogOut()
            }
        }
    }, [authState])
jaredperreault-okta commented 2 years ago

@kenyee To my knowledge we do not have a sample specifically for Redux Toolkit's RTX Query api (other than the blog post you mentioned). If you need assistance with this integration, I suggest opening a new issue with more details (and code snippets).

At a quick glance however, I would ensure okta-react's <Security> component is mounted above <AppRoutes>. useOktaAuth returns a React Context, where authState initializes to undefined, but is updated via <Security>. authState should only be undefined for a brief moment during bootstrapping/initial load

alexspence commented 2 years ago

If we aren't going to get a secure routes component for react router v6 can we get the example updated to something that works? The current implementation is a bit half baked and has some really strange behavior like replaying the implicit authentication flow on refresh.

@rapaccinim's solution above does resolve some of these issues but when the token expires things stop working until you refresh. It seems like there is some functionality in the original SecureRoute component that is not implemented in the example for react-router-v6.

kenyee commented 2 years ago

we do not have a sample specifically for Redux Toolkit's RTX Query api (other than the blog post you mentioned). If you need assistance with this integration, I suggest opening a new issue with more details (and code snippets).

Wrote this up since I'm sure you're going to get more questions on RTK Query since they're pushing folks to use it for network calls in the official React docs.. https://github.com/okta/okta-react/issues/236

kellengreen commented 2 years ago

Without SecureRoute the usefulness of this library seems greatly diminished. I understand not wanting to force users into a particular routing library, so perhaps this logic can be spun off into @okta/react-router-dom?

rlazimi-dev commented 2 years ago

I'd say a decent workaround for now is the following pattern: redirect to login if not authenticated while in the function components you'd like to secure.

The logic goes as follows:

  1. check if authState ready and return arbitrary component if not
  2. otherwise if it is ready and you're not authenticated, then you get redirected to login
  3. otherwise if it is ready and you're authenticated, you run the code you want to run

For example if you have a HomePage function component:

function HomePage(props) {
  yourCode()
  return yourComponent()
}

Assuming your login page is '/login', add the following authentication code:

import { useNavigate } from 'react-router-dom'
import { useOktaAuth } from '@okta/okta-react'
import { useEffect, useState } from 'react'

function HomePage(props) {

  //<-----------------------------------------BEGIN HERE 
  const navigate = useNavigate();
  const { authState, oktaAuth } = useOktaAuth();
  const [isAuthed, setAuthed] = useState(false)

  useEffect(() => {
    if (!authState) {
      setAuthed(false)
    }
    else {
      if (!authState.isAuthenticated) {
        navigate('/login')
      }
      setAuthed(true)
    }
  }, [oktaAuth, authState])

  if (!isAuthed) {
    return <p>checking for authentication...</p>
  }
  //<-----------------------------------------END HERE 

  yourCode()
  return yourComponent()
}

Pros: copying and pasting the modification in each function component you want to be protected Cons: copying and pasting the modification in each function component you want to be protected

EDIT: don't copy and paste. gonna leave the boilerplate code here since it provides a good intuition for how the redirect works, but see @jaredperreault-okta's below for better code re-use.

jaredperreault-okta commented 2 years ago

I don't recommend copying and pasting this block into all components you want protected. Instead define a component that contains this block and mount all components you want protected as subroutes (utilizing react-router-dom's Outlet)

  return (
    <Routes>
      <Route path='/' element={<Home />} />
      <Route path='login/callback' element={<LoginCallback loadingElement={<Loading />} />} />
      <Route path='/protected' element={<RequiredAuth />}>
        <Route path='' element={<Protected />} />
        {/* place components requiring protection here */}
      </Route>
    </Routes>
  );

In this example, RequiredAuth will handle checking the authentication state and will render the subroute if the user is authenticated via <Outlet />

Full sample is here

jaredperreault-okta commented 2 years ago

@alexspence The samples have been updated and should address the browser refresh issue you mentioned

rlazimi-dev commented 2 years ago

Ah sounds good @jaredperreault-okta – I was hoping someone would generalize that! (i tried but failed)

taher267 commented 2 years ago

is it possible to use in react router v6?

kellengreen commented 2 years ago

Yes see examples here.

TL;DR - You need to roll your own SecureRoute logic, but it works.

henricook commented 2 years ago

As someone who pays tens of thousands a year to Okta this is disappointing. These examples might be helpful (thanks for those kellengreen), I'm going to have to sit and trawl through them and try to apply it to my implementation using <SecureRoute>. The Okta team should really post a clear walkthrough on how to use with/migrate to react router v6

mraible commented 2 years ago

This example might be helpful:

https://github.com/okta-samples/okta-react-sample

It uses React Router v6 and you can set it up quickly if you install the Okta CLI:

okta start react
mraymond77 commented 1 year ago

Since moving to react-router 6 with the custom SecureRoute style provided by @mraible and @kellengreen it is working and I'm happy enough with that. But seems like I get completely re-authed on every refresh or when typing in a URL and hitting enter. It jumps to my callback and comes back to the page. Wondering if there is a way around that behavior? I thought useOktaAuth() would store and retrieve from local storage and not wipe out a token stored there, and thus authState.isAuthenticated wouldn't reset on refresh.

jaredperreault-okta commented 1 year ago

@mraymond77 this sample should fix your issue, it's iteration on the one @mraible provided

mraymond77 commented 1 year ago

@jaredperreault-okta that did the trick, thanks!

kellengreen commented 1 year ago

This is what I'm using at the moment. Cleaned up some bits from the sample for better clarity and reusability.

export default function Authenticated({ success, loading }) {
  const { oktaAuth, authState } = useOktaAuth()

  useEffect(() => {
    if (authState?.isAuthenticated === false) {
      const originalUri = toRelativeUrl(
        globalThis.location.href,
        globalThis.location.origin,
      )
      oktaAuth.setOriginalUri(originalUri)
      oktaAuth.signInWithRedirect()
    }
  }, [oktaAuth, authState?.isAuthenticated])

  return authState?.isAuthenticated === true ? success : loading
}

Something along these lines should really be included in the package. This way OKTA can remove the react-router-dom dependency, while keeping developer experience high.

<Authenticated
  success={<Outlet />}
  loading={<FullPageLoading />}
/>
jchabotamica commented 1 year ago

Any update here? Are we still not able to use react-router 6 without creating our own SecureRoute?

kellengreen commented 1 year ago

okta-react appears to be decoupling the react-router-dom dependency from future releases, so I'd assume support for this is not coming. Personally I actually agree that removing SecureRoute from this lib makes sense, and router specific components should reside elsewhere.

In the meantime the Authenticated example should help fill in the gaps:

<Route
  element={
    <Authenticated success={<Outlet />} />
  }
>
  <Route
    path="/secret"
    element={<Secret />}
  />
</Route>
jchabotamica commented 1 year ago

@kellengreen that does make sense in regards to decoupling

jaredperreault-okta commented 1 year ago

We currently do not have an official SecureRoute component for react-router@6 on our roadmap. We have created this samples directory, illustrating how to integrate okta-react with popular routing libraries.

igloo12 commented 1 year ago

It would be awesome if the README was more explicit about what version of the react-router is supported. It gives the install command below which installs react-router 6. But the example code provided in the readme doesn't work with react-router 6. I only found out about the react-router 6 examples by reading this thread.

npm install --save react
npm install --save react-dom
npm install --save react-router-dom
npm install --save @okta/okta-auth-js   # requires at least version 5.3.1
arunmp25 commented 1 year ago

Hi Is there any sample on how to implement the same with react RouterProvider() especially regarding where to place the <SecureRoute /> as all the routes are directly passed to the route provider

export default function App() {
  return <RouterProvider router={router} />;
}
Spartan-Hex-Shadow commented 7 months ago

Hi everyone

We are upgrading an old application to the latest version okta-react and are encountering the same issue of SecureRoute no longer working after going to version Version 6. Will this update be released soon?

kellengreen commented 7 months ago

This won't be fixed, router-dom is now a peer dependency. See above solutions for a resolution.

bandichandu commented 1 month ago

Hi Is there any sample on how to implement the same with react RouterProvider() especially regarding where to place the <SecureRoute /> as all the routes are directly passed to the route provider

export default function App() {
  return <RouterProvider router={router} />;
}

Is there any suggestions on this?