Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discussion: Use cases for useAuthenticator Hook #1497

Closed
2 tasks done
vymao opened this issue Mar 9, 2022 · 56 comments
Closed
2 tasks done

Discussion: Use cases for useAuthenticator Hook #1497

vymao opened this issue Mar 9, 2022 · 56 comments
Labels
Authenticator An issue or a feature-request for an Authenticator UI Component Documentation An issue or a feature-request for our Amplify UI Doc site or AWS Amplify docs question General question React An issue or a feature-request for React platform

Comments

@vymao
Copy link

vymao commented Mar 9, 2022

The original issue has since been resolved, however, leaving this open due to good discussion around use cases for the useAuthenticator hook

Original ticket below:


Before creating a new issue, please confirm:

On which framework/platform are you having an issue?

React

Which UI component?

Authenticator

How is your app built?

Create React App

Please describe your bug.

I am trying to use the useAuthenticator hook to set a global authentication state.

However, several problems have arisen:

  1. When I use const { user, signOut } = useAuthenticator((context) => [context.user]);, as is mentioned here, there is no app-level re-rendering. So several other components that are also using the hook and are dependent on authentication state don't change.
  2. If I refresh the page, React renders everything as signed out. When I go to sign in, it then signs me in automatically without me having to manually sign in. It should ideally render everything as signed in from the start, since I am already signed in.

What's the expected behaviour?

When I sign in or out, React should trigger an app-level re-render with the new authentication state. Furthermore, if I refresh the page, the authentication state should not change.

Help us reproduce the bug!

Following the instructions, I added Authenticator.Provider at the app level:

ReactDOM.render(
  <Authenticator.Provider>
    <HelmetProvider>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </HelmetProvider>
  </Authenticator.Provider>,
  document.getElementById('root')
);

I use useAuthenticator to Login:

const { route } = useAuthenticator(context => [context.route]);
    return (
        route === 'authenticated' ? <Navigate to="/dashboard/app" />: (
            <Authenticator
                // Default to Sign Up screen
                initialState="signUp"
                // Customize `Authenticator.SignUp.FormFields`
                signUpAttributes={['preferred_username', 'birthdate']}
                components={components}
                services={{
                    async validateCustomSignUp(formData) {
                        if (!formData.acknowledgement) {
                            return {
                                acknowledgement: 'You must agree to the Terms & Conditions',
                            };
                        }
                    },
                }}
            />
        )
            );

I define const { user, signOut } = useAuthenticator((context) => [context.user]); in components, not at the app level, where I want to obtain the authentication state. Following the guide, I use the conditional typeof user === 'undefined' to see if the user is authenticated.

Code Snippet

// Put your code below this line.

Additional information and screenshots

No response

@wlee221 wlee221 added Authenticator An issue or a feature-request for an Authenticator UI Component bug Something isn't working React An issue or a feature-request for React platform and removed bug Something isn't working labels Mar 9, 2022
@ErikCH
Copy link
Contributor

ErikCH commented Mar 9, 2022

Hi @vymao !

Yes, we have seen the issue with the useAuthenticator hook not updating on refresh. We are tracking that issue here as well.

I believe the issue you are having with it not tracking on signIn/signOut are related. We'll be looking at this problem soon.

@ErikCH ErikCH added bug Something isn't working ready-for-planning labels Mar 9, 2022
@vymao
Copy link
Author

vymao commented Mar 9, 2022

Is there a recommended workaround?

@wlee221
Copy link
Contributor

wlee221 commented Mar 9, 2022

Yep, this is a bug -- I believe the root cause is the same as #1332. as of now needs to be in the component tree for it to properly transition to the route 'authenticated' .

Meaning whenever you're on /dashboard/app, there's no Authenticator on that DOM tree, and useAuthenticator fails to load the current auth state.

As per workaround, can you try putting an "invisible" authenticator inside your /dashboard/app and let us know if that works? We'll prioritize a fix meanwhile.

@vymao
Copy link
Author

vymao commented Mar 10, 2022

I'm not sure I understand. Is adding Authenticator.Provider not enough? From my understanding of context in React, using a proper hook should only look for a provider up the DOM tree, and since I place the provider at the App level, it in theory should re-render everything when context changes, no? dashboard/app is not what I mean by "App level"; it is simply a route I define. When I mean App level, I mean like so:

ReactDOM.render(
  <HelmetProvider>
    <BrowserRouter>
      <Authenticator.Provider>
        <App />
      </Authenticator.Provider>
    </BrowserRouter>
  </HelmetProvider>,
  document.getElementById('root')
);

Also I'm not sure what you mean by "invisible" authenticator. I have the actual Authenticator as a separate component because I want to still enable users to use part of the app while not signed in.

@wlee221
Copy link
Contributor

wlee221 commented Mar 10, 2022

Yep your comment on React Context is right. And this is only a workaround, we're fixing it holistically soon.

The root cause is that Authenticator, when rendered, is sending some data to Authenticator.Provider to initialize the auth state management. So whenever there's an <Authenticator.Provider> but not an <Authenticator />, <Authenticator.Provider /> does not have the auth information ready and will not provide the up-to-date context. In other words the flow is like this:

  1. <Authenticator.Provider /> renders, but have not started auth state management yet
  2. <Authenticator /> renders, and Authenticator starts the auth flow inside Authenticator.Provider

which is an anti-pattern in React and what we'll prioritize fixing.

That's why I mentioned an invisible Authenticator: Assume you have signed in and refresh in dashboard/app page. In dashboard/app, (I assume) that signed in pages does not have an Authenticator in the tree, and so it'll think that the user is not authenticated. So this should address your problem (2).


In terms of your first problem, that is app-level re-rendering, do you have the same problem if you have useAuthenticator(context => [context.route])? It should cause a re-render whenever there's a valid user in the tree. Without that, you won't be even able to automatically traverse to dashboard/app, right? The authenticator state should change when you sign in with the Authenticator.

@vymao
Copy link
Author

vymao commented Mar 10, 2022

I guess I mean to say: how does one incorporate an invisible Authenticator? I'm probably misunderstanding, but wouldn't adding Authenticator to the dashboard/app component render a new sign in page instead? That isn't my intent.

@ErikCH
Copy link
Contributor

ErikCH commented Mar 10, 2022

@vymao Could you try adding an <Authenticator> to the route and add css to hide it.

[data-amplify-authenticator] {
display:none;
}

@vymao
Copy link
Author

vymao commented Mar 10, 2022

Seems to work that way, thanks. But I think this still isn't ideal; my app has a separate login page that actually uses Authenticator; placing this hidden Authenticator above that also hides the actual Authenticator. So it seems one can only use this in the DOM tree which doesn't have another Authenticator in use below it, which can be a hassle to manage.

@wlee221
Copy link
Contributor

wlee221 commented Mar 10, 2022

Yep, +1 on it not being ideal at all, and It's an anti-pattern. We'll have a holistic fix soon.

@davegravy
Copy link

davegravy commented Mar 11, 2022

Is this issue underlying the behavior I'm seeing, described in this SO?

@wlee221
Copy link
Contributor

wlee221 commented Mar 22, 2022

@davegravy yep, correct. Taking a look at this now.

@vymao
Copy link
Author

vymao commented Apr 7, 2022

Wanted to come back to this and mention that @wlee221 and @ErikCH the solution involving hiding Authenticator seems to only work if the user is signed in. If the user is signed out, the Authenticator still appears.

To provide more context for how I used this:
In my routes, I use layouts for different pages:


export default function Router() {
  return useRoutes([
    {
      path: '/dashboard',
      element: <DashboardLayout />,
      children: [
        { path: '', element: <Navigate to="/dashboard/app" /> },
        { path: 'app', element: <DashboardApp /> },
        { path: 'user', element: <User /> },
        { path: 'products', element: <Products /> },
        { path: 'blog', element: <Blog /> },
      ]
    },
    { path: '*', element: <Navigate to="/404" replace /> }
  ]);
}

Such a layout is like:

export default function DashboardLayout() {
  const [open, setOpen] = useState(false);

  return (
    <RootStyle>
      <Authenticator />
      <DashboardNavbar onOpenSidebar={() => setOpen(true)} />
      <DashboardSidebar isOpenSidebar={open} onCloseSidebar={() => setOpen(false)} />
      <MainStyle>
        <Outlet />
      </MainStyle>
    </RootStyle>
  );
}

which imports the CSS file containing:

[data-amplify-authenticator] {
    display: none;
}

@ErikCH
Copy link
Contributor

ErikCH commented Apr 18, 2022

Hi @vymao !

Does this problem still occur?

@vymao
Copy link
Author

vymao commented Apr 23, 2022

Yes. Was there an update that fixed this?

@ErikCH
Copy link
Contributor

ErikCH commented Apr 28, 2022

Hi @vymao !

Yes, so we made a change with #1580 that improves the experiences for users that are on multiple routes. So now as long as you have your application surrounded by Authenticator.Provider the useAuthenticator will work on any route you're on. Before, if you didn't have an Authenticator on your page it wouldn't work.

There is still one more outstanding issue with this solution. On refresh the route will temporarily be in a setup or idle state before it transitions to an authenticated state. If you check route as soon as the page loads, and redirect somewhere while it's in the idle or setup state, that could be an issue.

I created a guide on authenticated routes here. In it I describe this scenario, and work around for it.

In this scenario if someone goes to an authenticated route, and it's an idle or setup state then we redirect back to /login. Which will then by that time see the user is authenticated and will re-route back to the authenticated page.

Let me know if that's what you're experiencing and if this work around helps in the mean time.

@vymao
Copy link
Author

vymao commented Apr 29, 2022

It seems to work for now, will report back if any issues. Thanks!

@ErikCH ErikCH mentioned this issue Apr 29, 2022
4 tasks
@calebpollman
Copy link
Member

@adriaanbalt That was a mistake on my end. It's available as a prop passed in to the SignIn component:

components={ SignIn: ({ handleSubmit, ...props }) => {
  // ...do custom UI stuff
  }
}

@adriaanbalt
Copy link

@calebpollman
this works. I have removed my previous fetchAuthSession code.

Screenshot 2024-01-12 at 8 53 08 PM

Question: Why is this information so difficult to find and not an obvious alternative?

@calebpollman
Copy link
Member

@adriaanbalt Glad that you got things working!

Question: Why is this information so difficult to find and not an obvious alternative?

The documentation could be improved here, think the addition of a concrete example for this use case would be a good starting point but curious if there is anything else that you would have found helpful?

@adriaanbalt
Copy link

adriaanbalt commented Jan 13, 2024

@calebpollman

Generally, the documentation is good, but since time has changed the capabilities it seems like it is a bit dated.

I'm using React Native but this approach also works with React or JavaScript.

Firstly, the documentation relies on examples. There isn't the classic documentation style that outlines each property, class or method that is available on each hook or module. This would already be helpful and probably easier to maintain. When something is deprecated the page that the information is displayed can also say it has been deprecated. This is relatively common when reviewing documentation for other APIs.

Secondly, currently the AWS Amplify customization docs suggest this approach but this is not quite right because the NBM "module itself seems to handle the revealing of the children (see my earlier post's "Other finds" section).

Lastly, if you search for "handlesubmit" on the authenticator documentation page the word doesn't exist
Screenshot 2024-01-13 at 2 54 27 PM
It would at the bare minimum be helpful to know that this exists as a prop when instantiating a custom "Sign In" view.

For an example, you can go with the a basic approach, which satisfies almost all use-cases.

Use case: I want to add custom authentication to my app.
Result:

Provider

<Authenticator.Provider>
  <Authenticator
    components={{
      SignIn: props => {
        return <MySignIn {...props} />;
      },
      SignUp: props => {
        return <MySignUp {...props} />;
      },
    }}
    Header={MyHeader}
    Footer={Footer}
  >
    {children}
  </Authenticator>
</Authenticator.Provider>

MySignIn component

const MySignIn: React.FC<any> = ({ toSignUp, handleSubmit }) => {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  const handleSignIn = async () => {
    try {
      await handleSubmit({ username, password });
    } catch (error) {
      console.error('Error signing in:', error);
    }
  };
  return (
    <View
      style={{
        marginTop: 24,
        padding: 24,
      }}
    >
      <TextInterBold style={[Styles.h2, { marginBottom: 30 }]}>
        Sign in
      </TextInterBold>
      <View
        style={{
          marginBottom: 30,
        }}
      >
        <Input
          style={Styles.input}
          placeholder="Username"
          onChangeText={text => setUsername(text)}
          value={username}
          autoComplete="email"
        />
        <Input
          style={Styles.input}
          placeholder="Password"
          secureTextEntry={true}
          onChangeText={text => setPassword(text)}
          value={password}
          autoComplete="password"
        />
      </View>
      <View
        style={{
          flexDirection: 'row',
          justifyContent: 'space-between',
          marginTop: 10,
          marginBottom: 30,
        }}
      >
        <Button
          style={[Styles.button, { width: '25%' }]}
          onPress={handleSignIn}
        >
          Sign In
        </Button>
        <Button style={[Styles.button, { width: '25%' }]} onPress={toSignUp}>
          Create an account
        </Button>
      </View>
    </View>
  );
};

export default MySignIn;

Note the use of handleSubmit versus what the documentation suggests with is signIn()

You can see the documentation suggests using signIn() here. and in several other places for JavaScript or React. This is misleading as we both know that this doesn't work as expected.

I would suggest including information at this point in the documentation that explains the use of handleSubmit, even if this is a temporary alternative and a fix is coming in the future. Because for people like myself I almost had to forego using AWS Amplify due to the lack of ability to log a user in with a custom form. That is probably not ideal for AWS Amplify.

I hope this helps and wish you luck improving the docs. Thanks!

@reesscot reesscot added the Documentation An issue or a feature-request for our Amplify UI Doc site or AWS Amplify docs label Jan 22, 2024
@annjawn
Copy link

annjawn commented Feb 7, 2024

I am a little confused by this thread. I tried to implement this with Javascript and "@aws-amplify/ui-react": "^6.1.3" but this doesn't seem to work. <MyAuthentication/> is my custom login page. What am I missing here?

<Authenticator components={{
        SignIn: props => {
          return <MyAuthentication {...props}/>
        },
        SignUp: props => {
          return <MyAuthentication {...props}/>
        }
      }}>
  <BrowserRouter>
   <App />
  </BrowserRouter>
</Authenticator>

@reesscot
Copy link
Contributor

reesscot commented Feb 7, 2024

Hi @annjawn,
I think you may be getting confused because completely overriding the SignIn screen is only available in the React Native version of the Authenticator. There is ongoing work to bring this capability to the React Web Authenticator.

@annjawn
Copy link

annjawn commented Feb 7, 2024

Ah ok, I read the previous comment which indicated that it may work. Thanks. so the only option for a fully custom Auth screen experience is to fall back to aws-amplify, not sure if I can still use <Authenticator.Provider>. I've previously posted in this repo how difficult it is to build a fully custom login experience. There also used to be an example in the @aws-amplify/ui-react docs about a full custom login experience, but that page is now gone.

@annjawn
Copy link

annjawn commented Feb 8, 2024

Well no, i take that back. I don't think that's even possible now that Auth doesn't exist in aws-amplify. @reesscot what are the options for a fully custom auth experience here?

https://docs.amplify.aws/javascript/build-a-backend/troubleshooting/migrate-from-javascript-v5-to-v6/

so

import { signIn } from 'aws-amplify/auth';

🤦

@reesscot
Copy link
Contributor

reesscot commented Feb 8, 2024

There is a new version of the Authenticator in the works which will allow full UI customization without having to recreate all the auth flow logic, but it's not available yet.

Yes, you can definitely create your own auth experience using the Amplify Auth JS apis here:
https://docs.amplify.aws/react/build-a-backend/auth/enable-sign-up/

@timheilman
Copy link

I had much better luck with the Authenticator component and provider when I was using amplify-js version 5. On version 6, not as much luck. I found a workaround that may help others.

I only use authStatus from useAuthenticator. This seems to update to "authenticated" upon login using the Authenticator component, but I could not get it to update to "unauthenticated" upon logout.

I found a workaround for that issue.

I'm also using redux, and at signOut, I do not use the signOut method from useAuthenticator. Instead, I use:

import { signOut } from "aws-amplify/auth";

And my sign-out button handler then does this:

    void signOut();
    dispatch(setLogoutInt(randomLargeInt()));

setLogoutInt is a redux action, and dispatch is the redux Dispatch.

Then, this was the trick that got things working for me: forcing a rerender of the Provider upon logout via redux:

  const logoutInt = useAppSelector(selectLogoutInt);
  return (
    <Authenticator.Provider key={logoutInt}>
      <AppBeneathAuthenticatorProvider />
    </Authenticator.Provider>
  );

With this workaround, I am able to use amplify v6 with the Authenticator component and logout successfully. Hope it helps! Good luck.

@adriaanbalt
Copy link

@annjawn implementation is using React Native in a web build configuration as I'm building a project that must work across all platforms. The other comments should also help you though :)

@annjawn
Copy link

annjawn commented Feb 8, 2024

Hi @adriaanbalt, so fully overriding the components in <Authenticator> in web/React isn't possible right now, as confirmed by @reesscot yesterday. It's a work in progress. As a workaround, I am working with aws-amplify/auth to build the custom Authentication experience, but its yet to be seen if it plays well with <Authenticator.Provider>. I will probably find out either today/tomorrow.

@annjawn
Copy link

annjawn commented Feb 9, 2024

Ok, I can confirm that using only <Authenticator.Provider> with aws-amplify/auth methods works pretty well with useAuthenticator for a fully custom UI experience. Here's the simple implementation that works with no force window reload to re-render etc. for login or logout needed and useAuthenticator hook works beautifully

React+Vite

App.jsx

const Login = React.lazy(() => import('./pages/Authentication/Login'))
const SignUp = React.lazy(() => import('./pages/Authentication/Signup'))
const Reset = React.lazy(() => import('./pages/Authentication/Reset'))

function App() {
  const { authStatus } = useAuthenticator((context) => [context.user]);
  
  return (
    <>
    {
      (authStatus === "authenticated")?
      <Routes>
        <Route path="/" element={<AppLayout />}>
          .....//all the app authenticated routes
        </Route>
      </Routes>
      :<Routes>       
          <Route path="/" element={<Authenticate />}>
              <Route index element={<React.Suspense fallback={<Spin/>}>
                                      <Login/> //--> fully custom UI using aws-amplify/auth
                                    </React.Suspense>} />
              <Route path="signup" element={<React.Suspense fallback={<Spin/>}>
                                    <SignUp/>  //--> fully custom UI using aws-amplify/auth
                                  </React.Suspense>} />   
              <Route path="reset" element={<React.Suspense fallback={<Spin/>}>
                                    <Reset/>  //--> fully custom UI using aws-amplify/auth
                                  </React.Suspense>} /> 
          </Route>   
      </Routes>
    }
    </>
  )
}

main.jsx

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <Authenticator.Provider>
        <BrowserRouter>
          <App />
        </BrowserRouter>
    </Authenticator.Provider>
  </React.StrictMode>,
)

AppLayout.jsx (has SignOut button)

const { signOut } = useAuthenticator((context) => [context.user]);

const onSignOut = () => {
        signOut()      
}

return (<button onClick={onSignOut}>Log out</button>)

Actually quite shocked to see how well this works.

@jordanvn jordanvn added pending-maintainer-response Issue is pending response from an Amplify UI maintainer and removed pending-maintainer-response Issue is pending response from an Amplify UI maintainer labels Sep 19, 2024
@reesscot reesscot added question General question and removed bug Something isn't working labels Sep 26, 2024
@reesscot reesscot changed the title useAuthenticator doesn't trigger re-render when using <Authenticator.Provider> at the app level Discussion: Use cases for useAuthenticator Hook Sep 26, 2024
@williamtorc
Copy link

My user [from useAuthenticator] is undefined even tough I'm only rendering when authenticated

return authStatus === "authenticated" ? (
    <AuthenticatedRoutes />
  ) : (
    <PublicRoutes />
  );

on <AuthenticatedRoutes />

const { user } = useAuthenticator((context) => [context.user]);

I get undefined and then the user is populated

@github-actions github-actions bot added the pending-maintainer-response Issue is pending response from an Amplify UI maintainer label Oct 3, 2024
@jordanvn jordanvn removed the pending-maintainer-response Issue is pending response from an Amplify UI maintainer label Oct 3, 2024
@thaddmt
Copy link
Contributor

thaddmt commented Oct 3, 2024

@williamtorc are you using the Authenticator component with the user of useAuthenticator? Similar to this comment here useAuthenticator is only meant to be used inside of a rendered Authenticator and things like user and route will not work without it - #5861 (comment)

@github-actions github-actions bot added the pending-maintainer-response Issue is pending response from an Amplify UI maintainer label Oct 3, 2024
@thaddmt thaddmt removed the pending-maintainer-response Issue is pending response from an Amplify UI maintainer label Oct 3, 2024
@thaddmt
Copy link
Contributor

thaddmt commented Oct 4, 2024

Closing this issue as the original bug has been resolved. If you have any new issues please open a new ticket.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Authenticator An issue or a feature-request for an Authenticator UI Component Documentation An issue or a feature-request for our Amplify UI Doc site or AWS Amplify docs question General question React An issue or a feature-request for React platform
Projects
None yet
Development

No branches or pull requests