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

Add login / authentication example #153

Closed
rauchg opened this Issue Oct 29, 2016 · 195 comments

Comments

@rauchg
Copy link
Contributor

rauchg commented Oct 29, 2016

With:

  • re-usable authentication helper across pages
  • session synchronization among tabs
  • simple passwordless email backend hosted on now.sh

I think this will be hugely helpful to a lot of newcomers.

@nodegin

This comment has been minimized.

Copy link
Contributor

nodegin commented Oct 29, 2016

Suggestion: Use Redux and JWT to accomplish the example

@nsantini

This comment has been minimized.

Copy link

nsantini commented Oct 31, 2016

Im working on an example for this. Currently having issues getting componentWillReceiveProps to fire on my high level component (where Im planning to check if user is authenticated and redirect to login page if not)

@jaredpalmer

This comment has been minimized.

Copy link
Contributor

jaredpalmer commented Nov 2, 2016

So I have auth working swimmingly. As mentioned elsewhere, it's client-side only, which is ultimately just half the battle.

"Pretty-secure"

Like php, the atomic unit of Next is the page. One of the coolest features is that it lazy loads each page only when it's requested. With client-side only auth but with server-rendering, the js for that protected page is in fact downloaded by the browser. In the future when Next adds server workflows, you'll hopefully be able to block render and redirect on the server to prevent this entirely. This will require cookies, sessions, and AFAIK session stores, but that's just the cost of doing hybrid apps like these.

Auth Example

Assume you have a JWT-secured API with two endpoints of interest: /token and /me. /token accepts email/password credentials and returns a signed JWT (id_token) while /me returns profile information related to the JWT-authenticated user. I adapted the following AuthService.js from Auth0's lock (removing event emitter, although that's not the worst idea). It extracts almost all of the JWT token handling so it can be used on the login page and also in a Higher Order Component (more on that later).

// utils/AuthService.js
export default class AuthService {
  constructor(domain) {
    this.domain = domain || 'http://localhost:5000'
    this.fetch = this.fetch.bind(this)
    this.login = this.login.bind(this)
    this.getProfile = this.getProfile.bind(this)
  }

  login(email, password) {
    // Get a token
    return this.fetch(`${this.domain}/token`, {
      method: 'POST',
      body: JSON.stringify({
        email,
        password
      })
    }).then(res => {
      this.setToken(res.id_token)
      return this.fetch(`${this.domain}/user`, {
        method: 'GET'
      })
    }).then(res => {
      this.setProfile(res)
      return Promise.resolve(res)
    })
  }

  loggedIn(){
    // Checks if there is a saved token and it's still valid
    const token = this.getToken()
    return !!token && !isTokenExpired(token) // handwaiving here
  }

  setProfile(profile){
    // Saves profile data to localStorage
    localStorage.setItem('profile', JSON.stringify(profile))
  }

  getProfile(){
    // Retrieves the profile data from localStorage
    const profile = localStorage.getItem('profile')
    return profile ? JSON.parse(localStorage.profile) : {}
  }

  setToken(idToken){
    // Saves user token to localStorage
    localStorage.setItem('id_token', idToken)
  }

  getToken(){
    // Retrieves the user token from localStorage
    return localStorage.getItem('id_token')
  }

  logout(){
    // Clear user token and profile data from localStorage
    localStorage.removeItem('id_token');
    localStorage.removeItem('profile');
  }

  _checkStatus(response) {
    // raises an error in case response status is not a success
    if (response.status >= 200 && response.status < 300) {
      return response
    } else {
      var error = new Error(response.statusText)
      error.response = response
      throw error
    }
  }

  fetch(url, options){
    // performs api calls sending the required authentication headers
    const headers = {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    }

    if (this.loggedIn()){
      headers['Authorization'] = 'Bearer ' + this.getToken()
    }

    return fetch(url, {
      headers,
      ...options
    })
    .then(this._checkStatus)
    .then(response => response.json())
  }
}

Next up is a HOC to make protecting pages simpler. To prevent an unwanted flash of sensitive info, the page will server-render Loading... on first render while react boots up / reads the token from localStorage. This means that protected pages will not SEO, which is probably okay as of now, but definitely not optimal.

// utils/withAuth.js - a HOC for protected pages
import React, {Component} from 'react'
import AuthService from './auth'

export default function withAuth(AuthComponent) {
    const Auth = new AuthService('http://localhost:5000')
    return class Authenticated extends Component {
      constructor(props) {
        super(props)
        this.state = {
          isLoading: true
        };
      }

      componentDidMount () {
        if (!Auth.loggedIn()) {
          this.props.url.replaceTo('/')
        }
        this.setState({ isLoading: false })
      }

      render() {
        return (
          <div>
          {this.state.isLoading ? (
              <div>LOADING....</div>
            ) : (
              <AuthComponent {...this.props}  auth={Auth} />
            )}
          </div>
        )
      }
    }
}
// ./pages/dashboard.js
// example of a protected page
import React from 'react'
import withAuth from  '../utils/withAuth'

class Dashboard extends Component {
   render() {
     const user = this.props.auth.getProfile()
     return (   
         <div>Current user: {user.email}</div>
     )
   }
}

export default withAuth(Dashboard) 

The login page can't use the HOC as it stands now, because Login needs be public. So it just makes an instance of AuthService directly. You would do something similar for a Signup page too.

// ./pages/login.js
import React, {Component} from 'react'
import AuthService from '../utils/AuthService'

const auth = new AuthService('http://localhost:5000')

class Login extends Component {
  constructor(props) {
    super(props)
    this.handleSubmit = this.handleSubmit.bind(this)
  }

  componentDidMount () {
    if (auth.loggedIn()) {
      this.props.url.replaceTo('/admin')   // redirect if you're already logged in
    }
  }

  handleSubmit (e) {
    e.preventDefault()
    // yay uncontrolled forms!
    auth.login(this.refs.email.value, this.refs.password.value)
      .then(res => {
        console.log(res)
        this.props.url.replaceTo('/admin')
      })
      .catch(e => console.log(e))  // you would show/hide error messages with component state here 
  }

  render () {
    return (
      <div>
         Login
          <form onSubmit={this.handleSubmit} >
            <input type="text" ref="email"/>
            <input type="password" ref="password"/>
            <input type="submit" value="Submit"/>
          </form>
      </div>
    )
  }
}

export default Login

Inspired by Airbnb's react-with-styles, I also started working on a next-with-auth lib which would be a function returns a HOC to be used on pages. I also played with merging AuthService and this HOC. One solution might be to make this HOC accept a permission level function as an argument in addition to the component, like redux connect. Regardless, in my mind, you would use next-with-auth like this:

// ./utils/withAuth.js
import nextAuth from 'next/auth'
import parseScopes from './parseScopes'

const Loading = () => <div>Loading...</div>

export default nextAuth({
  url: 'http://localhost:5000',
  tokenEndpoint: '/api/token',
  profileEndpoint: '/api/me',
  getTokenFromResponse: (res) => res.id_token,
  getProfileFromResponse: (res) => res,
  parseScopes,
})

Doing this all with Redux seemed unnecessarily complicated, but basically you can follow the wiki example, but move AuthService into Actions (login and logout) and have a User Reducer. You could only call these actions on the client though, since there isn't localStorage on the server, so you need to check for that in your Actions. Ultimately, redux store is put on the window anyways. So you could just a well cache the user on window on your own instead of using context. If you don't want redux, you can also try out react-broadcast.

Lastly, assuming next/server ships according to #25. next-with-auth could abstract complicated localStorage vs. cookie stuff away from the developer with middleware + a HOC. It could also handle token refreshing too.

@ugiacoman

This comment has been minimized.

Copy link

ugiacoman commented Nov 2, 2016

Excited to try this out! Thanks for the barebones implementation :)

@amccloud

This comment has been minimized.

Copy link
Contributor

amccloud commented Nov 2, 2016

@jaredpalmer I'm working on something similar. How does your AuthService work when a component is rendered server side? The server would need access to the JWT but can't read it from local storage.

@jaredpalmer

This comment has been minimized.

Copy link
Contributor

jaredpalmer commented Nov 2, 2016

@amccloud It doesn't. That's the whole issue. The HOC renders <div>Loading..</div> on protected routes and must read the token and decide whether or not to redirect in componentDidMount. For it to work the way you want it to and render server-side, Next needs #25, or at least the ability to set a cookie with the value of the JWT AFAIK.

@luisrudge

This comment has been minimized.

Copy link

luisrudge commented Nov 3, 2016

I used cookie-js to set a cookie, but it's a bit of a hack..
the thing is: if you don't send a cookie, you lose all the benefits of nextjs and server side rendering in authenticated routes

@impronunciable

This comment has been minimized.

Copy link
Contributor

impronunciable commented Nov 3, 2016

@jaredpalmer this is great! thanks for the effort. I'll try to finish implementing your example (or help you doing it if you want) in the following days

@luisrudge

This comment has been minimized.

Copy link

luisrudge commented Nov 5, 2016

Yo! I published an example with nextjs and auth0 here: https://github.com/luisrudge/next.js-auth0
It has the concept of a main layout and also "secure pages" that load only when the user is authenticated.
Let me know what you think 🎉

@impronunciable

This comment has been minimized.

Copy link
Contributor

impronunciable commented Nov 6, 2016

@luisrudge amazing. I'm cloning and doing some changes but looks great

@luisrudge

This comment has been minimized.

Copy link

luisrudge commented Nov 6, 2016

Cool! What do you think it's missing? What changes are you thinking?

On Sun, Nov 6, 2016 at 1:12 PM -0200, "Dan Zajdband" <notifications@github.commailto:notifications@github.com> wrote:

@luisrudgehttps://github.com/luisrudge amazing. I'm cloning and doing some changes but looks great

You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHubhttps://github.com//issues/153#issuecomment-258687108, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AA5cE8NIsvQ_ITjc1gArTFgNXzEda4TSks5q7e5NgaJpZM4KkJmi.

@impronunciable

This comment has been minimized.

Copy link
Contributor

impronunciable commented Nov 6, 2016

  1. Using standard for linting (so it's consistent with everything we are building with next)
  2. Adding multi-tab support requested by @rauchg
  3. The css part can be simplified

I'll send you a pr :)

@luisrudge

This comment has been minimized.

Copy link

luisrudge commented Nov 6, 2016

What do you mean by multi tab support?

On Sun, Nov 6, 2016 at 1:16 PM -0200, "Dan Zajdband" <notifications@github.commailto:notifications@github.com> wrote:

  1. Using standard for linting (so it's consistent with everything we are building with next)
  2. Adding multi-tab support requested by @rauchghttps://github.com/rauchg
  3. The css part can be simplified

I'll send you a pr :)

You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHubhttps://github.com//issues/153#issuecomment-258687373, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AA5cE1A6jq4KZc9_ynukTCI4mU-rdsNaks5q7e81gaJpZM4KkJmi.

@impronunciable

This comment has been minimized.

Copy link
Contributor

impronunciable commented Nov 6, 2016

You have 2 open tabs, logout on 1, automatically logs out on the other

@luisrudge

This comment has been minimized.

Copy link

luisrudge commented Nov 6, 2016

Ahh. That's super cool!

On Sun, Nov 6, 2016 at 1:21 PM -0200, "Dan Zajdband" <notifications@github.commailto:notifications@github.com> wrote:

You have 2 open tabs, logout on 1, automatically logs out on the others

You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHubhttps://github.com//issues/153#issuecomment-258687707, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AA5cE9e2DA4_GgNQIVTMp0hx74G-6RmUks5q7fBfgaJpZM4KkJmi.

@impronunciable

This comment has been minimized.

Copy link
Contributor

impronunciable commented Nov 6, 2016

Hi @luisrudge I sent you a PR with the changes luisrudge/next.js-auth0#2

thank you so much for doing this <3

@impronunciable

This comment has been minimized.

Copy link
Contributor

impronunciable commented Nov 6, 2016

btw this is the result:

2016-11-06 11 14 31

@ugiacoman

This comment has been minimized.

Copy link

ugiacoman commented Nov 6, 2016

@impronunciable @luisrudge Fantastic implementation! If you want to use it without Auth0, it looks you'd only need to change the files in the ./utils dir, maybe even just lock.js. I'll be trying this out soon. Btw the multi-tab looks awesome 💯

@impronunciable

This comment has been minimized.

Copy link
Contributor

impronunciable commented Nov 6, 2016

@ugiacoman I've started implementing a small server with passwordless.net, let me know if you want to get my code as a starting point

@ugiacoman

This comment has been minimized.

Copy link

ugiacoman commented Nov 6, 2016

@impronunciable That'd be awesome! I was actually going to do something similar with Twitter Fabric's Digits.

@jaredpalmer

This comment has been minimized.

Copy link
Contributor

jaredpalmer commented Nov 6, 2016

@impronuncible i suggest not using password less.net , instead you can just use passport-local, and just send users a link with their email and token in query string.

@luisrudge

This comment has been minimized.

Copy link

luisrudge commented Nov 6, 2016

Thanks @impronunciable ❤️

@ugiacoman yeah, it's pretty easy to remove the auth0 dependency. I used it because I didn't want to have a separate api to handle auth

@sedubois

This comment has been minimized.

Copy link
Contributor

sedubois commented Nov 10, 2016

@jaredpalmer as far as I know, having #25 would be great but isn't blocking? I mean we have access to the server-side req in getInitialProps so nothing prevents applying cookie-parser to it? Server-side auth and session management is all new stuff to me 😬

BTW considering localStorage can't be used server-side, are cookies the only way to have server-side sessions? I have a vague remembrance it might not be the safest? But is there any other option?

@eezing

This comment has been minimized.

Copy link
Contributor

eezing commented Dec 1, 2016

@sedubois

The cookie approach can be very safe if done properly. Doing the following is fairly trivial:

  • use httpOnly flag (prevents JavaScript access to cookie)
  • use secure flag (only set cookie for https requests)
  • Signed cookies (verify source of cookie)
@rauchg

This comment has been minimized.

Copy link
Contributor Author

rauchg commented Dec 3, 2016

There's also a very significant latency advantage when you can access authentication information directly on the server.

@curran

This comment has been minimized.

Copy link
Contributor

curran commented Jun 21, 2018

I documented all the code changes I made for gutting the extraneous aspects of nextjs-starter in this Pull Request iaincollins/nextjs-starter#86

This may be getting closer to a solid bare-bones authentication example for the Next.js repository.

@tml123

This comment has been minimized.

Copy link

tml123 commented Jun 22, 2018

I had a requirement recently to implement OAuth with Office 365, so I thought I'd share a very simple example I threw together here. It needs work (and is nowhere near as developed as some of the above examples) but I think it could be generalized eventually to use various OAuth clients as well. It utilizes several of the examples already shown in the Next.js examples repo as well as the protected routes thread here. In any case, just figured I'd share in case someone wanted a quick example of how to (maybe) do this with Microsoft.

@bgold0

This comment has been minimized.

Copy link

bgold0 commented Jul 21, 2018

For anyone interested in a simple JWT tokenized authentication that works client and server side, I got some help over in the Spectrum chat and figured I'd share it with you all. Any feedback is always appreciated.

https://github.com/bgold0/nextjs-auth-skeleton

@alan2207

This comment has been minimized.

Copy link

alan2207 commented Aug 14, 2018

Hi guys!
Here is another example of authentication with next.js I've built a couple of months ago, maybe someone will find it useful:

https://github.com/alan2207/nextjs-jwt-authentication

@nmaro

This comment has been minimized.

Copy link

nmaro commented Aug 29, 2018

Ooth 2.0 is out with new and better docs

Ooth is a user identity management system built for node.js (with next.js in mind).

Currently supported strategies:

  • Primary: username/password (incl. verify email, forgot pw etc.), guest, facebook, google
  • Secondary: cookie-based sessions, JWTs

Check out this live example that puts it all together with next.js (source code).

@skitterm

This comment has been minimized.

Copy link

skitterm commented Oct 10, 2018

Many of the auth samples here (and in the examples folder) return the user session/token/etc from getInitialProps. As far as I understand, when the page is rendered server-side, this means that the user session info will get sent as part of the HTML page response (as part of NEXT_DATA) to the browser.

I see two security issues with this pattern when getInitialProps is run server-side:

  1. The user session gets transmitted over the network from server to browser. If any such requests are using http:// and not https://, the user's token, etc would be exposed over the network in plain-text.

  2. The user session is returned from server to browser as part of the HTML page (in a NEXT_DATA script tag). Having the user's token/etc sitting directly on the HTML page seems risky, especially once the page is parsed, rendered by the browser, and other 3rd-party scripts may now be running.

Are these issues that have already been addressed? Are there any mitigations for these threats?

@hugotox

This comment has been minimized.

Copy link

hugotox commented Oct 10, 2018

That's why I use cookies. See my example here https://github.com/hugotox/nextjs-starter/blob/master/pages/_app.js

@malixsys

This comment has been minimized.

Copy link
Contributor

malixsys commented Oct 11, 2018

@nmaro

This comment has been minimized.

Copy link

nmaro commented Oct 18, 2018

Finally! Here you can find a fully dockerized next.js auth example with the following containers:

  • next.js
  • auth microservice (ooth)
  • api (graphql)
  • session storage (redis)
  • small reverse proxy

Everything is pulled together with docker-compose.

Quick note on JWTs. JWTs have the advantages of being stateless, good for mobile and good if you need to pass credentials from one domain to the other. The main disadvantage is that they expose the browser to XSS. For this example I opted for a pure cookie-sessions based solution. I still managed to split things up in microservices thanks to the reverse proxy (all microservices run under the same domain) and to the shared session storage.

https://github.com/nmaro/staart/tree/master/examples/staart
The live example is, as usual: https://staart.nmr.io

@bjunc

This comment has been minimized.

Copy link

bjunc commented Oct 19, 2018

I think it is prudent to clarify, using JWT does not intrinsically "expose" you to XSS. Rather, if your site has an XSS vulnerability (not due to JWT itself), a JWT could be compromised (along with any other script accessible information); whereas httpOnly cookies will not be accessible. Nevermind that you can use a JWT as the value of an httpOnly cookie!

Cookie-only solutions can work well for same-domain communication, but if you have a headless API (eg. example.com calling api.example.com), then that's not really a solution unless you want to proxy browser requests from example.com to api.example.com by making your API calls to example.com and forward them on with the cookie appended to the request (which comes with its own set of pros/cons).

I personally feel the cons of JWT are vastly overblown, and are pretty easily mitigated via a slew of commonly implemented protections. Not least of which, a token blacklist referencing the jti claim (eg. Version4 UUID) in the event that the token has been compromised prior to expiration.

@nmaro

This comment has been minimized.

Copy link

nmaro commented Oct 19, 2018

Hey @bjunc yes thanks for clarifying. Correct, JWT doesn't itself expose you to XSS. Regarding your other observations, I'd like to add that they each have their pitfalls.

Nevermind that you can use a JWT as the value of an httpOnly cookie!

Yes, would just like to add that that removes the one advantage of JWT of transferring credentials between domains.

a token blacklist

That, and the other common practice of a refresh token remove the other advantage of JWT of being really stateless.

This is my understanding of the situation:

  • If you need cross location auth use JWT <- but if possible avoid because of security
  • If you need to auth from any client that is not a browser and doesn't have the XSS issue (e.g. a desktop or mobile app) use JWT
  • Otherwise use cookie-based sessions, because a JWT based solution will either be less secure (XSS) or not be really stateless (blacklists), or require you to use a proxy to keep everything under the same domain anyway.
@nmaro

This comment has been minimized.

Copy link

nmaro commented Oct 19, 2018

I wrote a Medium article on Next.js Authentication/User accounts.
It's an extensive tutorial and my braindump from almost two years of spare-time development and thinking (my first comment on this issue is from February 2017).

https://medium.com/the-ideal-system/user-accounts-with-next-js-an-extensive-tutorial-6831cdaed16b

@bjunc

This comment has been minimized.

Copy link

bjunc commented Oct 20, 2018

You keep correlating JWT with being less secure. JWT does not make your site any less secure. It is not in and of itself a XSS vulnerability. If you have an XSS exploit, you are in equal trouble regardless. Even with an httpOnly cookie, the attacker may not be able to read the cookie value, but it doesn't matter because they can run arbitrary code – like XHR requests that would automatically pass the session cookies (essentially acting as a CSRF attack). If your API is cross-domain, and you are making browser-to-server requests, then the JWT is somewhere in the code anyway (be it initial state, or localStorage). If you use Axios, you might have even set global defaults, where the attacker need only make an Axios request and not even worry about auth.

Also, you can't really talk about XSS without also talking about CSRF; where auth cookies are specifically targeted. Even with a reverse proxy setup, a CSRF attack would allow the attacker to execute authenticated requests against your API.

Btw, just because you put the JWT in a cookie (for say, knowing if the user is logged in), doesn't mean you are precluded from using the cookie value (eg. a JWT) for cross-domain server-to-API-server access on page load (which also works for the reverse proxy scenario). Nor are you or precluded from passing the JWT in initial state as well for browser-to-API-server requests. They're not mutually exclusive.

As an aside, I find the idea of "stateless" JWT both overrated and limited in application for the majority of use-cases. For instance:

  • Resource based / dynamic permissions (eg. not just "can edit Post", but rather "can edit Post:1634").
  • What if the user's account has been blocked / deleted?
  • Haven't paid their monthly subscription; which throttles functionality?
  • Blacklisted the token (per above)?

You're not baking all that into the JWT, which means you have to dip into the domain layer (ie. database) to find out. You just loaded the resource, so might as well put it where the rest of the app can get to it, and now you have state. I find it really silly to think everything you need to know about the subject would be baked into the token; while simultaneously keeping it mostly anonymous and lightweight. Without digressing too much, there's an argument for "stateless" requests in inter-service communication, but even that I find impractical (at least as it pertains to the concept of baking what you need to know about the subject into the JWT)...

@nmaro

This comment has been minimized.

Copy link

nmaro commented Nov 30, 2018

Ooth authentication strategies available meanwhile (new in bold):

  • Local (Username/email/password)
  • Facebook
  • Google
  • Guest
  • Patreon
  • Twitter
  • Authy (Twilio) - passwordless via SMS

timneutkens added a commit that referenced this issue Dec 14, 2018

Example with cookie auth (#5821)
Fixes #153

This is my attempt at #153

Following @rauchg instructions:

- it uses an authentication helper across pages which returns a token if there's one
- it has session synchronization across tabs
- <strike>I deployed a passwordless backend on `now.sh` (https://with-cookie-api.now.sh, [src](https://github.com/j0lv3r4/next.js-with-cookies-api))</strike> The backend is included in the repository and you can deploy everything together by running `now`

Also, from reviewing other PRs, I made sure to:

- use [isomorphic-unfetch](https://www.npmjs.com/package/isomorphic-unfetch).
- use [next-cookies](https://www.npmjs.com/package/next-cookies).

Here's a little demo:

![GIF](https://i.imgur.com/067Ph56.gif)
@lishine

This comment has been minimized.

Copy link

lishine commented Dec 26, 2018

@jaredpalmer you wrote
Like php, the atomic unit of Next is the page. One of the coolest features is that it lazy loads each page only when it's requested. With client-side only auth but with server-rendering, the js for that protected page is in fact downloaded by the browser. In the future when Next adds server workflows, you'll hopefully be able to block render and redirect on the server to prevent this entirely. This will require cookies, sessions, and AFAIK session stores, but that's just the cost of doing hybrid apps like these.

We are 2 years later. Is there a server workflow to prevent loading js for protected pages?
@timneutkens maybe puting protected content in other zone?
How would I entirely prevent access to protected content ?

@sarneeh

This comment has been minimized.

Copy link

sarneeh commented Dec 28, 2018

@lishine You have a ServerResponse in the getInitialProps of your page - you can easily redirect someone unprivileged.

@affanshahid

This comment has been minimized.

Copy link

affanshahid commented Dec 29, 2018

Is there an example of auth with redux?

@alan2207

This comment has been minimized.

Copy link

alan2207 commented Dec 29, 2018

Is there an example of auth with redux?

You can try this example which is using redux, and check out if it works for you...
You can find it somewhere in this topic, but in case you can't find it, here it is:
https://github.com/alan2207/nextjs-jwt-authentication

@letsspeak

This comment has been minimized.

Copy link

letsspeak commented Feb 3, 2019

I think this is more complicated problem when using Server Side API call results getInitialProps, because Virtual DOM uses old results after LOGOUT-LOGIN action. I'm thinking about solution

@letsspeak

This comment has been minimized.

Copy link

letsspeak commented Feb 3, 2019

EDITED
and here's my answer with redux-observable

Side Auth TODO
Server true Fetch initial data (with request cookie proxy).
Server false Show login page, and fetch data after logined.
Client true Fetch initial data.
Client false Show login page, and fetch data after logined. (This happens only when session expired in page to page moving)
  static async getInitialProps({ store, query: { rowsPerPage, pageIndex }, req, auth }) {
    store.dispatch(TemporaryStoryActions.initPageState());

    const isAuthenticated = () => req ? req.isAuthenticated()
      : store.getState().auth.isAuthenticated;

    if (isAuthenticated()) {
      // fetch initial data
      const TemporaryStoryApiProxy = withCookieProxy(req, TemporaryStoryApi);
      await TemporaryStoryApiProxy.fetchTemporaryStories({
        rowsPerPage: rowsPerPage || 15,
        pageIndex: pageIndex || 0,                                                                                                                                  }).then(json => {
        store.dispatch(TemporaryStoryActions.loadTemporaryStories(
          json.rowsPerPage, json.pageIndex, json.count, json.rows));
      }).catch(error => {
        if (error.response && error.response.status === 403) {
          store.dispatch(AuthActions.initState(false, null));
          return;
        }
        throw error;
      });
    }

    if (!isAuthenticated()) {
      // => if client side fetch failed with 403, isAuthenticated() turns off to false
      // register logined action for client side login succeeded
      const reloadAction = TemporaryStoryActions.fetchTemporaryStories({
        rowsPerPage: rowsPerPage || 15,
        pageIndex: pageIndex || 0,
      });
      store.dispatch(AuthActions.addLoginedAction(reloadAction));
    }

    return {
      ...store.getState(),
    }                                                                                                                                                           }                                                                                             }
export const withLogin = Page => class SecurePage extends React.Component {
  static async getInitialProps (ctx) {
    if (ctx.req && ctx.store) {
      // server side
      const isAuthenticated = ctx.req.isAuthenticated();
      const { user } = ctx.req;
      ctx.store.dispatch(AuthActions.initState(isAuthenticated, user));
    }
    return Page.getInitialProps && await Page.getInitialProps(ctx)
  }

  render () {
    const { auth } = this.props;
    return auth.isAuthenticated ? <Page {...this.props} /> : <LoginPage />
  }                                                                                                                                                           }
// when [front-end server] => [api server]
// on Server Side Rendering,
// needs to proxy Cookies which sent to Next.js page request
export const withCookieProxy = (req, targetApi) => {
  if (!req) {
    return targetApi;
  }
  targetApi.client.interceptors.request.use(config => {
    const cookieString = Object.keys(req.cookies).map(key => `${key}=${req.cookies[key]}`).join('; ');
    const headers = {
      ...(config.headers || {}),
      Cookie: cookieString,
    };
    return {
      ...config,
      headers: headers,
    };
  }, error => {
    return Promise.reject(error);
  });
  return targetApi;
};
const loginEpic = (action$, state$) => action$.pipe(
  ofType(AuthActionTypes.login),
  mergeMap(action => {
    const email = action.payload.email;
    const password = action.payload.password;
    return from(AuthApi.login(email, password))
      .mergeMap(json => {
        const user = json.user;
        const loginedActions = state$.value.auth.loginedActions;
        const successActions = [
          AuthActions.removeAllLoginedActions(),
          ...loginedActions,
          AuthActions.loginSuccess(user.id, user.name, user.last_login_date),
        ];
        return from(successActions);
      }).pipe(catchError(error => {
        return of$(AuthActions.loginFail(error));
      }));
  }));
@malixsys

This comment has been minimized.

Copy link
Contributor

malixsys commented Feb 3, 2019

Seems complicated when something simple would do, along the lines of :


export const withAuthSync = WrappedComponent => class extends Component {
  componentDidMount() {
    window.addEventListener('storage', this.syncLogout);
  }

  componentWillUnmount() {
    window.removeEventListener('storage', this.syncLogout);
    window.localStorage.removeItem('logout');
  }

    syncLogout = (event) => {
      if (event.key === 'logout') {
        console.log('logged out from storage!');
        window.location.reload();
      }
    };

    render() {
      return <WrappedComponent {...this.props} />;
    }
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment