How to use with external oauth authentication? #46

Closed
emzero opened this Issue Jun 16, 2016 · 11 comments

Comments

Projects
None yet
5 participants
@emzero

emzero commented Jun 16, 2016

I'm trying to set up redux-auth-wrapper to authenticate with Yammer client-side flow (which is normal oauth 2 token authentication).

I don't do any authentication on my own, I just leverage Yammer's. So, basically, all I have to do is redirect the browser to https://www.yammer.com/dialog/oauth?client_id=[:client_id]&redirect_uri=[:redirect_uri]&response_type=tokenand after authentication and authorization, the browser redirects to a redirect uri I've specified in the Yammer app settings which is in the form of:

http://mydomain.com/yammer/callback#token=THETOKEN

From there, I just grab the token from the hash and save it to the redux store.

The route that needs authentication is /stream. I want to avoid unnecessary location changes. So, ideally, the flow should be something like.

  1. Non-authenticated user goes to /stream.
  2. redux-auth-wrapper redirects to the external Yammer authentication page.
  3. After authentication/authorization, Yammer redirects to my callback uri which grabs and saves the token to the user store, hence authenticating the user to the app.

Is this approach possible with redux-auth-wrapper?

@mjrussell

This comment has been minimized.

Show comment
Hide comment
@mjrussell

mjrussell Jun 17, 2016

Owner

@emzero you should definitely be able to accomplish this with redux-auth-wrapper. I think the easiest approach is to add an additional query parameter to your redirect URI so that the redirect uri becomes:

http://mydomain.com/yammer/callback?redirect=%2fstream#token=THETOKEN

Then in the component that is rendered at the callback route, the logic would be similar to the LoginComponent in the example:

import React, { Component, PropTypes } from 'react'
import { routerActions } from 'react-router-redux'
import { connect } from 'react-redux'

import { login } from '../actions/user'

function select(state, ownProps) {
  const isAuthenticated = state.user.token || false
  const redirect = ownProps.location.query.redirect || '/'
  return {
    isAuthenticated,
    redirect
  }
}

@connect(select, replace: routerActions.replace })
class LoginContainer extends Component {

    static propTypes = {
      login: PropTypes.func.isRequired,
      replace: PropTypes.func.isRequired
    };

    componentWillMount() {
      const { isAuthenticated, replace, redirect } = this.props
      if (isAuthenticated) {
        replace(redirect)
      }
    }

    componentWillReceiveProps(nextProps) {
      const { isAuthenticated, replace, redirect } = nextProps
      const { isAuthenticated: wasAuthenticated } = this.props

      if (!wasAuthenticated && isAuthenticated) {
        replace(redirect)
      }
    }

   ...
}

You'd also want to add the saving the token logic to the willMount/receiveProps or listen for the location update redux action from react-router-redux in your user reducer.

Owner

mjrussell commented Jun 17, 2016

@emzero you should definitely be able to accomplish this with redux-auth-wrapper. I think the easiest approach is to add an additional query parameter to your redirect URI so that the redirect uri becomes:

http://mydomain.com/yammer/callback?redirect=%2fstream#token=THETOKEN

Then in the component that is rendered at the callback route, the logic would be similar to the LoginComponent in the example:

import React, { Component, PropTypes } from 'react'
import { routerActions } from 'react-router-redux'
import { connect } from 'react-redux'

import { login } from '../actions/user'

function select(state, ownProps) {
  const isAuthenticated = state.user.token || false
  const redirect = ownProps.location.query.redirect || '/'
  return {
    isAuthenticated,
    redirect
  }
}

@connect(select, replace: routerActions.replace })
class LoginContainer extends Component {

    static propTypes = {
      login: PropTypes.func.isRequired,
      replace: PropTypes.func.isRequired
    };

    componentWillMount() {
      const { isAuthenticated, replace, redirect } = this.props
      if (isAuthenticated) {
        replace(redirect)
      }
    }

    componentWillReceiveProps(nextProps) {
      const { isAuthenticated, replace, redirect } = nextProps
      const { isAuthenticated: wasAuthenticated } = this.props

      if (!wasAuthenticated && isAuthenticated) {
        replace(redirect)
      }
    }

   ...
}

You'd also want to add the saving the token logic to the willMount/receiveProps or listen for the location update redux action from react-router-redux in your user reducer.

@mjrussell mjrussell added the question label Jun 17, 2016

@emzero

This comment has been minimized.

Show comment
Hide comment
@emzero

emzero Jun 17, 2016

@mjrussell But how do I actually redirect to the external yammer authentication page?

redux-auth-wrapper doesn't have a way to redirect to an external uri or does it?

// Redirects to /login by default
const UserIsAuthenticated = UserAuthWrapper({
  authSelector: state => state.user, // how to get the user state
  redirectAction: routerActions.replace, // the redux action to dispatch for redirect
  wrapperDisplayName: 'UserIsAuthenticated' // a nice name for this auth check
})

That code redirects to /login by default but even using failureRedirectPath doesn't provide a way to do external redirection.

emzero commented Jun 17, 2016

@mjrussell But how do I actually redirect to the external yammer authentication page?

redux-auth-wrapper doesn't have a way to redirect to an external uri or does it?

// Redirects to /login by default
const UserIsAuthenticated = UserAuthWrapper({
  authSelector: state => state.user, // how to get the user state
  redirectAction: routerActions.replace, // the redux action to dispatch for redirect
  wrapperDisplayName: 'UserIsAuthenticated' // a nice name for this auth check
})

That code redirects to /login by default but even using failureRedirectPath doesn't provide a way to do external redirection.

@emzero

This comment has been minimized.

Show comment
Hide comment
@emzero

emzero Jun 17, 2016

Also, in the callback component is where I need to parse the token from the hash and dispatch an action that sends a request to Yammer API requesting the user data. Only after I get the user data back (action: RECEIVE_USER_DATA) is when I have the user store populated (hence the user is authenticated).

This is what I'm currently doing in the /yammer/callback route and it's causing the infinite loop mentioned in the other issue (I'll close that one).

import React, {Component, PropTypes} from 'react';
import { requestCurrentUserData } from '../../reducers/currentUser';
import { connect } from 'react-redux';
import { browserHistory } from 'react-router';


class YammerCallback extends Component {
  componentWillMount() {
    if (this.props.location.hash) {
      let token = this.props.location.hash.substr(14); // TODO Improve parsing

      this.props.requestCurrentUserData(token);
    }
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.currentUser.id) {
      browserHistory.push('/stream');
    }
  }

  render() {
    return <div>Logging in...</div>
  }
}

const mapStateToProps = (state) => ({
  currentUser: state.currentUser
});

const mapDispatchToProps = (dispatch) => ({
  requestCurrentUserData: (token) => {
    dispatch(requestCurrentUserData(token));
  }
});

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(YammerCallback);

emzero commented Jun 17, 2016

Also, in the callback component is where I need to parse the token from the hash and dispatch an action that sends a request to Yammer API requesting the user data. Only after I get the user data back (action: RECEIVE_USER_DATA) is when I have the user store populated (hence the user is authenticated).

This is what I'm currently doing in the /yammer/callback route and it's causing the infinite loop mentioned in the other issue (I'll close that one).

import React, {Component, PropTypes} from 'react';
import { requestCurrentUserData } from '../../reducers/currentUser';
import { connect } from 'react-redux';
import { browserHistory } from 'react-router';


class YammerCallback extends Component {
  componentWillMount() {
    if (this.props.location.hash) {
      let token = this.props.location.hash.substr(14); // TODO Improve parsing

      this.props.requestCurrentUserData(token);
    }
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.currentUser.id) {
      browserHistory.push('/stream');
    }
  }

  render() {
    return <div>Logging in...</div>
  }
}

const mapStateToProps = (state) => ({
  currentUser: state.currentUser
});

const mapDispatchToProps = (dispatch) => ({
  requestCurrentUserData: (token) => {
    dispatch(requestCurrentUserData(token));
  }
});

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(YammerCallback);
@mjrussell

This comment has been minimized.

Show comment
Hide comment
@mjrussell

mjrussell Jun 19, 2016

Owner

how do I actually redirect to the external yammer authentication page?

react-router-redux doesn't allow you to dispatch history actions that navigate to external urls. You are going to have to either write a custom middleware that will change window.location. Or you can write your wrapper as:

const UserIsAuthenticated = UserAuthWrapper({
  authSelector: state => state.user, // how to get the user state
  redirectAction: ({ pathname, query }) => window.location.href = '...' // build the yammer URL
  wrapperDisplayName: 'UserIsAuthenticated' // a nice name for this auth check
})

Also you should be careful about your willReceiveProps, you can't guarantee how many times it will be called so you should only do the push on a change for the currentUser. This can be a source of infinite looping (see #45). So:

  componentWillReceiveProps(nextProps) {
    if (nextProps.currentUser.id) {
      browserHistory.push('/stream');
    }
  }

should be:

  componentWillReceiveProps(nextProps) {
    if (this.props.currentUser.id !== nextProps.currentUser.id) {
      browserHistory.push('/stream');
    }
  }
Owner

mjrussell commented Jun 19, 2016

how do I actually redirect to the external yammer authentication page?

react-router-redux doesn't allow you to dispatch history actions that navigate to external urls. You are going to have to either write a custom middleware that will change window.location. Or you can write your wrapper as:

const UserIsAuthenticated = UserAuthWrapper({
  authSelector: state => state.user, // how to get the user state
  redirectAction: ({ pathname, query }) => window.location.href = '...' // build the yammer URL
  wrapperDisplayName: 'UserIsAuthenticated' // a nice name for this auth check
})

Also you should be careful about your willReceiveProps, you can't guarantee how many times it will be called so you should only do the push on a change for the currentUser. This can be a source of infinite looping (see #45). So:

  componentWillReceiveProps(nextProps) {
    if (nextProps.currentUser.id) {
      browserHistory.push('/stream');
    }
  }

should be:

  componentWillReceiveProps(nextProps) {
    if (this.props.currentUser.id !== nextProps.currentUser.id) {
      browserHistory.push('/stream');
    }
  }
@emzero

This comment has been minimized.

Show comment
Hide comment
@emzero

emzero Jun 22, 2016

@mjrussell Thanks for your reply.

Unfortunately I'm still having the infinite loop. But now I know why.

The problem is that the above componentWillReceiveProps gets executed before the reducer takes care of the proper action (the one that sets the state.user with the logged in user data). So when I do that browserHistory.push('/stream'), your library checks for state.user.id which is still null, hence, redirecting to the login page again, and so on.

I'm new to the React world so probably I'm doing things the wrong way. I don't expect you to help me with this since it's not related to your library, but any advice would be greatly appreciated.

emzero commented Jun 22, 2016

@mjrussell Thanks for your reply.

Unfortunately I'm still having the infinite loop. But now I know why.

The problem is that the above componentWillReceiveProps gets executed before the reducer takes care of the proper action (the one that sets the state.user with the logged in user data). So when I do that browserHistory.push('/stream'), your library checks for state.user.id which is still null, hence, redirecting to the login page again, and so on.

I'm new to the React world so probably I'm doing things the wrong way. I don't expect you to help me with this since it's not related to your library, but any advice would be greatly appreciated.

@emzero

This comment has been minimized.

Show comment
Hide comment
@emzero

emzero Jun 22, 2016

Nevermind. I fixed it.

The problem was that I was using state => state.user.id as the authSelector (since state.user is always existent in my case) and your library expects that to be an object, not a number.
Added the predicate: user => user.id fixed the problem.

emzero commented Jun 22, 2016

Nevermind. I fixed it.

The problem was that I was using state => state.user.id as the authSelector (since state.user is always existent in my case) and your library expects that to be an object, not a number.
Added the predicate: user => user.id fixed the problem.

@emzero emzero closed this Jun 22, 2016

@mjrussell

This comment has been minimized.

Show comment
Hide comment
@mjrussell

mjrussell Jun 22, 2016

Owner

@emzero glad you worked it out, sorry about the default predicate. Maybe adding a warning if the default _.isEmpty will not not make sense in development would be a good idea

Owner

mjrussell commented Jun 22, 2016

@emzero glad you worked it out, sorry about the default predicate. Maybe adding a warning if the default _.isEmpty will not not make sense in development would be a good idea

@dafttt

This comment has been minimized.

Show comment
Hide comment
@dafttt

dafttt Aug 19, 2016

Hi @emzero
Would you share an example of your work ? Thanks

dafttt commented Aug 19, 2016

Hi @emzero
Would you share an example of your work ? Thanks

@emzero

This comment has been minimized.

Show comment
Hide comment
@emzero

emzero Aug 19, 2016

@dafttt

// Redirects to /login by default
const userIsAuthenticated = userAuthWrapper.UserAuthWrapper({
  authSelector: state => state.currentUser, // how to get the user state
  predicate: user => user.id,
  redirectAction: push,
  wrapperDisplayName: "UserIsAuthenticated", // a nice name for this auth check
});

const redirectToYammerAuth = () => {
  const windowIfDefined = typeof window === "undefined" ? null : window as any;
  if (windowIfDefined) {
    windowIfDefined.location.href = Config.getAuthenticationUri();
  }
};

// and the relevant routes
<Route path="/login" onEnter={redirectToYammerAuth} />
<Route path="/stream" component={userIsAuthenticated(Stream) } />

emzero commented Aug 19, 2016

@dafttt

// Redirects to /login by default
const userIsAuthenticated = userAuthWrapper.UserAuthWrapper({
  authSelector: state => state.currentUser, // how to get the user state
  predicate: user => user.id,
  redirectAction: push,
  wrapperDisplayName: "UserIsAuthenticated", // a nice name for this auth check
});

const redirectToYammerAuth = () => {
  const windowIfDefined = typeof window === "undefined" ? null : window as any;
  if (windowIfDefined) {
    windowIfDefined.location.href = Config.getAuthenticationUri();
  }
};

// and the relevant routes
<Route path="/login" onEnter={redirectToYammerAuth} />
<Route path="/stream" component={userIsAuthenticated(Stream) } />
@commandtab

This comment has been minimized.

Show comment
Hide comment
@commandtab

commandtab Sep 1, 2017

Hi all,

Sorry to resurrect an old issue! Does redirectAction still work in v2? I'm providing it to connectedRouterRedirect, but it doesn't seem to be called. As well, redirectPath seems to be required to be a string or function, but I hope to provide my own complete URI via redirectAction. Any suggestions?

e.g.

const ssoRequired = connectedRouterRedirect({
  redirectAction: ({ pathname, query }) => `https://sso.example.com?origin=${encodeURIComponent(window.location.href)}`,
  authenticatedSelector: (state) => state.session.user_id !== null,
});

Thanks!

Hi all,

Sorry to resurrect an old issue! Does redirectAction still work in v2? I'm providing it to connectedRouterRedirect, but it doesn't seem to be called. As well, redirectPath seems to be required to be a string or function, but I hope to provide my own complete URI via redirectAction. Any suggestions?

e.g.

const ssoRequired = connectedRouterRedirect({
  redirectAction: ({ pathname, query }) => `https://sso.example.com?origin=${encodeURIComponent(window.location.href)}`,
  authenticatedSelector: (state) => state.session.user_id !== null,
});

Thanks!

@TeaBough

This comment has been minimized.

Show comment
Hide comment
@TeaBough

TeaBough Oct 17, 2017

You can also redirect to a local component of yours that will then redirect to the desired external url.

You can also redirect to a local component of yours that will then redirect to the desired external url.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment