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

Documentation does not specify how to handle return from successful hosted UI authentication #1386

Closed
tunecrew opened this issue Aug 3, 2018 · 28 comments
Assignees
Labels
Auth Related to Auth components/category documentation Related to documentation feature requests

Comments

@tunecrew
Copy link

tunecrew commented Aug 3, 2018

Given a generic npx create-react-app . ; npm install aws-amplify-react app with the following App.js:

import React, { Component } from 'react';
import Amplify, { Auth } from 'aws-amplify';
import { withOAuth } from 'aws-amplify-react';

const awsconfig = {
  Auth: {
    identityPoolId: ***redacted***,
    region: ***redacted***,
    userPoolId: ***redacted***,
    userPoolWebClientId: ***redacted***,
    oauth: {
      domain: ***redacted***,
      scope: ['email', 'profile', 'openid', 'aws.cognito.signin.user.admin'],
      redirectSignIn: 'http://localhost:3000/',
      redirectSignOut: 'http://localhost:3000/',
      responseType: 'code',
    },
  },
}

Amplify.configure(awsconfig)

class App extends Component {
  state = { isAuthenticated: false, }

  async componentDidMount () {
    try {
      const currentAuthenticatedUser = await Auth.currentAuthenticatedUser()

      if (currentAuthenticatedUser) { this.setState({isAuthenticated: true}) }
      else { this.setState({isAuthenticated: false}) }
    }
    catch (error) { this.setState({isAuthenticated: false}) }
  }

  render () {
    let response = (
      <button onClick={this.props.OAuthSignIn}>Log In</button>
    )

    if (this.state.isAuthenticated) {
      response = (
		<p>Logged In</p>
      )
    }

    return (
      <div className="App">
        {response}
      </div>
    )
  }
}

export default withOAuth(App)

Authentication is successful and the hosted UI redirects back to http://localhost:3000/?code=...

At this point, however, the Log In button is still displayed and isAuthenticated is still false, although the call to Auth.currentAuthenticatedUser() has been successful and the access, id and refresh tokens have been added to local storage.

Reloading the page changes the state of isAuthenticated to true and everything works as expected, although the ?code=... query parameter is still present in the url.

It would be of great help if the documentation provided a simple example like this illustrating the following:

  • where and how in the lifecycle to handle the redirect from a successful hosted UI authentication without manually reloading
  • how to strip the code query parameter without a manual redirect

I'm wondering also if there is some complete other aspect of this missing - like having to also listen to the auth events from Hub and then respond (btw the documentation on Hub seems extremely sparse)

@elorzafe elorzafe added the Auth Related to Auth components/category label Aug 3, 2018
@jadbox
Copy link

jadbox commented Aug 6, 2018

@elorzafe I have the exact same problem! Been trying to find a solution for days now.

@yuankunluo
Copy link

yes! same problem with my angular app.

#1395

@powerful23
Copy link
Contributor

@tunecrew Hi, I can answer your first question. You can use the Hub category to listen on the signIn event. For exxample:

import { Auth, Hub } from 'aws-amplify';

class myComp extends Component {
     componentDidMount() {
          Hub.listen('auth', this);
     }

     onHubCapsule(capsule) {
          const { channel, payload, source } = capsule;
          if (channel === 'auth' && payload.event === 'signIn') { 
            this.checkUser();
         }
     }

     checkUser() {
           Auth.currentAuthenticatedUser().then(() => {
           });
     }
}

@jadbox
Copy link

jadbox commented Aug 6, 2018

@powerful23
How do I include this Hub as a module: import Hub from '@aws-amplify/hub' doesn't work.

for reference: https://hackernoon.com/modular-imports-with-aws-amplify-daeb387b6985

@powerful23
Copy link
Contributor

@jadbox you can do import { Hub } from '@aws-amplify/core

@jadbox
Copy link

jadbox commented Aug 6, 2018

@powerful23 thanks that works... Hub doesn't have a Typescript definition on the core module, which made it non-transparent. GOOD NEWS though, your example of waiting on the Hub event first allows me to then call currentAuthenticatedUser at the right time to get the authenticated user. I've got a couple thoughts though:

  • This needs to be documented under Hosted UI for anyone to be successful
  • currentAuthenticatedUser should work correctly without having to magically wait for this Hub event
  • What is this Hub module and why is it used in this fashion for oauth/hosted-UI
  • at this point of Auth.currentAuthenticatedUser().then(...), should I be able to now call mobilehub services?
Hub.listen('auth', {
    onHubCapsule: (capsule:any) => {
        const { channel, payload } = capsule; // source
        if (channel === 'auth' && payload.event === 'signIn') { 
            Auth.currentAuthenticatedUser().then((data) => {
                console.log('---', data) // THIS WORKS for Hosted UI!
            });
       }
   }
});

@tunecrew
Copy link
Author

tunecrew commented Aug 7, 2018

@powerful23 thanks for this - it does work - a few comments:

  • to remove the ?code... from the url I added a this.props.history.push('/') in checkUser() although this seems like a hack
  • there is a noticeable pause sometimes between the redirect and the auth event firing - this means that when a user is redirected back from a successful authentication, they will briefly see the "Log In" button. Because there is no state preserved from when the app originals redirects the user to the hosted UI, there is no way to set some kind of state like isAuthenticating so that we can load a spinner or something instead.

I'm fairly new to React so it is quite possible I'm missing some obvious ways of doing things, but I would think if the Amplify React components were a bit more complete in dealing with these kind of things people could get up and running w/ Cognito much quicker (not insulting the team's efforts at all, just an opinion on which way to go with things!).

@jadbox
Copy link

jadbox commented Aug 8, 2018

I'm also having problems with the authentication persisting. Once being logged in successfully in my app with the Code token, if I redirect to remove the code... Amplify no longer recognizes that I was authenticated. I have tried both cookies and localstorage options without success. Maybe I'm missing something?

@tunecrew
Copy link
Author

tunecrew commented Aug 8, 2018

@jadbox I'm not having that problem - I am not using cookies but I'm not sure what you mean by "localhost options"

@powerful23
Copy link
Contributor

@tunecrew I can answer the second question. I agree to add a state like isAuthenticating. For example, we can dispatch an isAuthenticating event by the Hub module so you can check it every time the page is reloaded. We are also working on to add a loading page for aws-amplify-react.

@jadbox How are you redirecting your app? The code returned by Cognito will be used to revoke another call to get you the tokens for Auth so if you redirect your app before this process gets completed then you won't be signed in successfully.

@jadbox
Copy link

jadbox commented Aug 9, 2018

I meant "localstorage", lol. Corrected the mistake above.
@tunecrew do you do anything special to persist authentication after the code/token is removed from the url?

@tunecrew
Copy link
Author

tunecrew commented Aug 9, 2018

@jadbox calling Auth.currentAuthenticatedUser() seems to be what reads the ?code=xxx part of the url and then gets the tokens from Cognito backend (and stores them in localstorage). So once this happens you can redirect as the tokens will be in localstorage already - so I am doing the redirect after making this call.

@jadbox
Copy link

jadbox commented Aug 9, 2018

@tunecrew is there a different promise other than currentAuthenticatedUser for when the user should be authenticated what the data is in localstorage rather than coming from uri query code= param? I actually see that "CognitoIdentityServiceProvider" keys get saved to local storage, but as soon as I remove the "/callback?code=xxx" from the url, the reloaded page loses all those keys in localstorage. Note that the domain or subdomain hasn't changed at all.

@tunecrew
Copy link
Author

tunecrew commented Aug 9, 2018

@jadbox I'll try and go a gist w/ my code.

@mlabieniec mlabieniec added the documentation Related to documentation feature requests label Aug 9, 2018
@jadbox
Copy link

jadbox commented Aug 9, 2018

@tunecrew any help would be super appreciated... I've been literally stuck on this for the last few days. I'm so close too, but I just can't seem to get authentication "to stick" when moving to another page.

Edit: created a ticket for the sticky authentication issue #1426

@uclaeagit
Copy link

uclaeagit commented Oct 12, 2018

Can someone point me to the documentation and examples for implementing Cognito Hosted UI on the client side? React, angular, vanilla is all fine. I've been googling for a long time and this is the closest thing I've been able to find.

Is it correct that amazon-cognito-identity-js is purely for implementing the Cognito Signin/Signup APIs directly (not Hosted UI)?

@JustFly1984
Copy link

If your app is not SSR, and you do not need to serialize your redux store, you can simply store your Cognito object in redux. Don't forget to reset your reducers to initialState on logout.

This is my example of signIn redux-thunk action with immutable.js, but it general you should get an idea. history object I get from withRouter() HOC from 'react-router v4` in login button component.

This is not the best example thou it does work. If somebody have better ideas, please share.

In my case mutable Cognito object with methods is not serializable in context of otherwise completely immutable state. There should be a better way to handle Cognito object outside of redux store.

export const signIn = ({ history }) => async (dispatch, getState) => {
  const state = getState()

  const email = sanitizeEmail(state.getIn(['authSignin', 'email', 'value']))
  const password = state.getIn(['authSignin', 'password', 'value'])
  const rememberMe = state.getIn(['authSignin', 'rememberMe'])

  const valid = (
    (
      email.length > 5 &&
      email.length < 321
    ) &&
    emailRegexp(email)
  ) && password.length > 7

  if (!valid) {
    return
  }

  dispatch(
    userRequest()
  )

  try {
    const cognito = await Auth.signIn(email, password)

    // console.log('signin cognito:', cognito)

    if (
      cognito.challengeName === 'SMS_MFA' ||
      cognito.challengeName === 'SOFTWARE_TOKEN_MFA'
    ) {
      dispatch(
        userCognito({
          cognito
        })
      )

      return history.push('/signin-confirm')
    } else if (cognito.challengeName === 'NEW_PASSWORD_REQUIRED') {
      dispatch(
        userCognito({
          cognito
        })
      )

      return history.push('/password')
    } else if (cognito.challengeName === 'MFA_SETUP') {
      dispatch(
        userCognito({
          cognito
        })
      )

      return history.push('/signin-totp-setup')
    }

    const calls = {
      onSuccess: async data => {
        await getUserData({
          jwtToken: cognito.signInUserSession.idToken.jwtToken,
          history,
          dispatch
        })
      },
      onFailure: err => {
        console.log('remember failure:', err)

        const errorMessage = err instanceof Object ? err.message : err

        toast.showError(`SignIn Error: ${errorMessage}`)

        dispatch(
          userFailure({
            errorMessage
          })
        )
      }
    }

    rememberMe
      ? cognito.setDeviceStatusRemembered(calls)
      : cognito.setDeviceStatusNotRemembered(calls)
  } catch (err) {
    console.log('signin error:', err)

    const errorMessage = err instanceof Object ? err.message : err

    toast.showError(`Authentication Error: ${errorMessage}`)

    dispatch(
      userFailure({
        errorMessage
      })
    )

    if (err.name === 'UserNotConfirmedException') {
      history.push('/signin-confirm')
    }
  }
}

@uclaeamsavino
Copy link

uclaeamsavino commented Oct 18, 2018

@powerful23 thanks that works... Hub doesn't have a Typescript definition on the core module, which made it non-transparent. GOOD NEWS though, your example of waiting on the Hub event first allows me to then call currentAuthenticatedUser at the right time to get the authenticated user. I've got a couple thoughts though:

  • This needs to be documented under Hosted UI for anyone to be successful
  • currentAuthenticatedUser should work correctly without having to magically wait for this Hub event
  • What is this Hub module and why is it used in this fashion for oauth/hosted-UI
  • at this point of Auth.currentAuthenticatedUser().then(...), should I be able to now call mobilehub services?
Hub.listen('auth', {
    onHubCapsule: (capsule:any) => {
        const { channel, payload } = capsule; // source
        if (channel === 'auth' && payload.event === 'signIn') { 
            Auth.currentAuthenticatedUser().then((data) => {
                console.log('---', data) // THIS WORKS for Hosted UI!
            });
       }
   }
});

I have one issue with this method. If I use it on my home page, then a user signs in later through a different page. The home page listener still fires and I get the warning about component not mounted:

Warning: Can't call setState (or forceUpdate) on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
    in Home (created by _class)
    in _class (created by Route)
    in Route (at src/index.js:52)

Does anyone know how I can unsubscribe to this when my Home component unmounts? Thanks a lot.

@powerful23
Copy link
Contributor

@uclaeamsavino thanks for you feedback. The reason why you need to listen on Hub events before calling Auth.currentAuthenticatedUser() is that there is some async process happens when redirecting back from the Hosted UI Page. We want to make sure all those async processes get done before people asking whether they are logged in or not.

The Hub module is just a very simple implementation for the local events subscription. We will improve that category in the future for more generic use. Unfortunately for now it doesn't provide the method to unsubscribe the listener. Will create a story for that.

@hp0210
Copy link

hp0210 commented Oct 31, 2018

We are having the same problem where our users see our login UI for a few seconds when they are redirected back from Cognito Hosted UI.
@powerful23 Do you have any timeline about when the isAuthenticating event will be implemented?

@karlmosenbacher
Copy link

So, did you end up with a full example? This is still very interesting!

@jitsunen
Copy link

jitsunen commented Dec 6, 2018

@tunecrew I can answer the second question. I agree to add a state like isAuthenticating. For example, we can dispatch an isAuthenticating event by the Hub module so you can check it every time the page is reloaded. We are also working on to add a loading page for aws-amplify-react.

For those of you who want a quick solution, I just implemented my own mechanism to dispatch and listen to 'isAuthenticating' event. Use it at your own risk.

import ReactDOM from 'react-dom';
import './index.css';
import 'react-notifications/lib/notifications.css';
import App from './App';
import Amplify, {Hub} from 'aws-amplify';
import aws_exports from './aws-exports';
import {SignIn} from 'aws-amplify-react';
import {BarLoader} from 'react-spinners';

Amplify.configure(aws_exports);

const decorate = function(fn){
    return function(){
        Hub.dispatch('auth', {event:'isAuthenticating'}, 'Auth');
        fn.apply(this, arguments);
    }
}

class AuthListener extends React.Component{
    constructor(props){
        super(props);
        this.state = {isAuthenticating : false};
        Hub.listen("auth", this, 'auth_listener');
    }

    onHubCapsule(capsule){
        const {channel, payload} = capsule;
        if(channel === 'auth'){
            this.setState({isAuthenticating : payload.event === 'isAuthenticating'});
        }
    }

    render(){
        return <BarLoader loading={this.state.isAuthenticating}></BarLoader>;
    }
}

SignIn.prototype.signIn = decorate(SignIn.prototype.signIn);
ReactDOM.render(<Fragment><App /><AuthListener /></Fragment>, document.getElementById('root'));```

@undefobj
Copy link
Contributor

Hello everyone, we have created an RFC for feature work that should make the challenges found in this issue easier in the future. If you have a moment please read through the details and add any comments: #2716

Your feedback in the RFC will help us ensure that we are delivering the best experience possible. Thank you.

@jamstooks
Copy link

Hi, @undefobj - the proposed changes in #2716 look promising. I'm not quite sure how they will affect the lag after successful authentication where we see the sign in component displayed briefly. Will you be implementing @powerful23's suggestion of adding the isAuthenticating state?

@neil-rubens
Copy link

neil-rubens commented Mar 10, 2019

a good example is provided in https://aws-amplify.github.io/docs/js/authentication#make-it-work-in-your-app

partial code snippet:

class App extends Component {
  constructor(props) {
    super(props);
    this.onHubCapsule = this.onHubCapsule.bind(this);
    this.signOut = this.signOut.bind(this);
    // let the Hub module listen on Auth events
    Hub.listen('auth', this);
    this.state = {
      authState: 'loading'
    }
  }

  componentDidMount() {
    console.log('on component mount');
    // check the current user when the App component is loaded
    Auth.currentAuthenticatedUser().then(user => {
      console.log(user);
      this.setState({authState: 'signedIn'});
    }).catch(e => {
      console.log(e);
      this.setState({authState: 'signIn'});
    });
  }

  onHubCapsule(capsule) {
    // The Auth module will emit events when user signs in, signs out, etc
    const { channel, payload, source } = capsule;
    if (channel === 'auth') {
      switch (payload.event) {
        case 'signIn':
          console.log('signed in');
          this.setState({authState: 'signedIn'});
          break;
        case 'signIn_failure':
          console.log('not signed in');
          this.setState({authState: 'signIn'});
          break;
        default:
          break;
      }
    }
  }

  signOut() {
    Auth.signOut().then(() => {
      this.setState({authState: 'signIn'});
    }).catch(e => {
      console.log(e);
    });
  }

  render() {
    const { authState } = this.state;
    return (
      <div className="App">
        {authState === 'loading' && (<div>loading...</div>)}
        {authState === 'signIn' && <OAuthButton/>}
        {authState === 'signedIn' && <button onClick={this.signOut}>Sign out</button>}
      </div>
    );
  }
}

export default App;

// OAuthButton.js
import { withOAuth } from 'aws-amplify-react';
import React, { Component } from 'react';

class OAuthButton extends React.Component {
  render() {
    return (
      <button onClick={this.props.OAuthSignIn}>
        Sign in with AWS
      </button>
    )
  }
}

@powerful23
Copy link
Contributor

@tunecrew we recently did an update about how to use hosted UI in the doc: https://aws-amplify.github.io/docs/js/authentication#oauth-and-hosted-ui

I am going to close this issue. Please let me know if you want to reopen it.

@mdsadique2
Copy link

@powerful23 thanks for this - it does work - a few comments:

  • to remove the ?code... from the url I added a this.props.history.push('/') in checkUser() although this seems like a hack
  • there is a noticeable pause sometimes between the redirect and the auth event firing - this means that when a user is redirected back from a successful authentication, they will briefly see the "Log In" button. Because there is no state preserved from when the app originals redirects the user to the hosted UI, there is no way to set some kind of state like isAuthenticating so that we can load a spinner or something instead.

I'm fairly new to React so it is quite possible I'm missing some obvious ways of doing things, but I would think if the Amplify React components were a bit more complete in dealing with these kind of things people could get up and running w/ Cognito much quicker (not insulting the team's efforts at all, just an opinion on which way to go with things!).

I am facing this exact same problem. Is there a way around it ?

@github-actions
Copy link

This issue has been automatically locked since there hasn't been any recent activity after it was closed. Please open a new issue for related bugs.

Looking for a help forum? We recommend joining the Amplify Community Discord server *-help channels or Discussions for those types of questions.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jun 12, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Auth Related to Auth components/category documentation Related to documentation feature requests
Projects
None yet
Development

No branches or pull requests