vercel / next.js

The React Framework
https://nextjs.org
MIT License
120.94k stars 25.86k forks source link

Add login / authentication example #153

Closed rauchg closed 5 years ago

rauchg commented 7 years ago

With:

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

nodegin commented 7 years ago

Suggestion: Use Redux and JWT to accomplish the example

nsantini commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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

amccloud commented 7 years ago

@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 commented 7 years ago

@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 commented 7 years ago

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 commented 7 years ago

@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 commented 7 years ago

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 commented 7 years ago

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

luisrudge commented 7 years ago

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.com<mailto: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/zeit/next.js/issues/153#issuecomment-258687108, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AA5cE8NIsvQ_ITjc1gArTFgNXzEda4TSks5q7e5NgaJpZM4KkJmi.

impronunciable commented 7 years ago

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 commented 7 years ago

What do you mean by multi tab support?

On Sun, Nov 6, 2016 at 1:16 PM -0200, "Dan Zajdband" notifications@github.com<mailto: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/zeit/next.js/issues/153#issuecomment-258687373, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AA5cE1A6jq4KZc9_ynukTCI4mU-rdsNaks5q7e81gaJpZM4KkJmi.

impronunciable commented 7 years ago

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

luisrudge commented 7 years ago

Ahh. That's super cool!

On Sun, Nov 6, 2016 at 1:21 PM -0200, "Dan Zajdband" notifications@github.com<mailto: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/zeit/next.js/issues/153#issuecomment-258687707, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AA5cE9e2DA4_GgNQIVTMp0hx74G-6RmUks5q7fBfgaJpZM4KkJmi.

impronunciable commented 7 years ago

Hi @luisrudge I sent you a PR with the changes https://github.com/luisrudge/next.js-auth0/pull/2

thank you so much for doing this <3

impronunciable commented 7 years ago

btw this is the result:

2016-11-06 11 14 31

ugiacoman commented 7 years ago

@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 commented 7 years ago

@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 commented 7 years ago

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

jaredpalmer commented 7 years ago

@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 commented 7 years ago

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 commented 7 years ago

@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 commented 7 years ago

@sedubois

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

rauchg commented 7 years ago

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

impronunciable commented 7 years ago

We should move this example into examples/ I'll see what I can come up with

tmpethick commented 7 years ago

I've manage to use react-cookie for isomorphic cookies by wrapping nextjs in a custom express server the following way:

const express = require('express')
const next = require('next')
const cookie = require('react-cookie')
const cookieParser = require('cookie-parser')

const app = next({ dev: true, dir: process.cwd() })
const handle = app.getRequestHandler()

app.prepare().then(() => {
  const server = express()
  server.use(cookieParser())       // <---- this line

  server.get('*', (req, res) => {
    cookie.plugToRequest(req, res) // <---- this line
    return handle(req, res)
  })

  server.listen(3000, (err) => {
    if (err) throw err
    console.log('> Ready on http://localhost:3000')
  })
})

This allows me to make authenticated request from the server side. This does not address any of the issues original bullets but it solved the problem of sharing state between client and server.

iamjacks commented 7 years ago

From the POV of someone who is learning a lot of this stuff. I think it would be best if examples aren't relying on 3rd party services like auth0. It would be more beneficial for newcomers to see a more barebones example with login/signup forms and using Redux and JWT.

rauchg commented 7 years ago

The example we plan to bundle will be based on open-source Node.js server APIs

iaincollins commented 7 years ago

I've added an example of email based authentication to an example starter project at https://github.com/iaincollins/nextjs-starter

It has session support (with Express Sessions on the backend and the browser sessionStorage API to cache them on the front end), httpOnly cookies, CSRF projection, uses built in SMTP to send emails, an easy to change the backend that defaults to SQL Lite. No configuration is required to run it.

The project also has layout pages, custom routes and includes the clock example from the wiki. It's not the fanciest example of authentication but might be helpful for those looking for easy to get started with simple project that is easy to understand and play around with.

I agree with @iamjacks that an example with JWT seems like a good idea.

I am happy to improve error handling and add features like a simple profile page users can edit, and passport integration with examples for oAuth for Facebook, Google and Twitter if that would be useful for people. If folks have good ideas about better ways to propagate / expose session info to components I'm super interested.

Example screenshot showing what to expect
rauchg commented 7 years ago

That's pretty incredible @iaincollins. We'll feature this on the release notes for 2.0 for sure :)

iaincollins commented 7 years ago

@rauchg Thank you! :)

I guess I should add explicitly that in this example sessions are both client and server based - i.e. work with and without JavaScript (and on systems without sessionStorage), and the same session is shared across both.

Achieving this gets a little gnarly in the Session component, which delves into variables with names like req.connection._httpMessage.locals._csrf to get the CSRF token from the server headers - as the 'req' object passed to pages in getInitialProps() is curiously bit different from the req object exposed in Express as I'd normally access it through req.locals._csrf (however req.session is the same in both).

Chathula commented 7 years ago

localStorage is not secure. what is the best way to make it more secure. anyone can steal localStorage data and put it again in his browser and can be logged as the victim user!!

arunoda commented 7 years ago

@Chathula that's not true. This argument is wrong. If anyone has access to the browser physically, they can do anything.

That's true for cookies as well.

Using localStorage for auth is kind a secure because we might get rid of cookie based sec issues. But on the otherhand, it affects SSR.

Chathula commented 7 years ago

@arunoda i have created login with Laravel API and Next.js Client. i store authUser access_token inside localStorage. then check user logged or not by authenticating. but it is not secure. if someone stole the localStorage data. he/she can use it.

arunoda commented 7 years ago

if someone stole the localStorage data.

How? Basically he/she should have access to the browser physically. Then that person could do anything. So, we should not worry about that.

Chathula commented 7 years ago

can't we use any encryption to make it much more secure?

arunoda commented 7 years ago

@Chathula this is going off topic. It's not exactly relevant to Next.js and we want to go with how normally web stuff works.

arunoda commented 7 years ago

@Chathula may be you could start a new thread in the above starter-project.

Chathula commented 7 years ago

@arunoda hahha!! Thank you for info! :D

iaincollins commented 7 years ago

@Chathula I'm happy discuss it more in an issue the starter project if you have specific concerns.

I'd like to correct any misapprehension, to avoid people being unnecessarily alarmed.

The Web Storage API (i.e. localStorage and sessionStorage) is - like cookies without httpOnly set - restricted via same origin policy (protocol, hostname, port number), it is not true that "anyone can steal [it]"; but yes if someone is able to execute arbitrary JavaScript on your site via a Cross Site Scripting Vulnerability in your application then they can access the store too, so you should not store session identifiers in it.

This is why you will see that session token itself is not stored in localStorage/sessionStorage and is not readable in JavaScript, it is only transmitted via an HTTP Only cookie (which is why the session class uses XMLHttpRequest() rather than fetch() - as explained in the class documentation).

This means even if someone is able to exploit a Cross Site Scripting Vulnerability in your application and execute arbitrary JavaScript in your web app they still cannot read or export a users session token.

This is perhaps an important distinction worth labouring in the documentation.

Note: Additional encryption of user data isn't helpful here because the app always needs to be able to read the data so it can be rendered (so you'd also need to store a description key in the app, which would render encrypting the user data fairly moot).

UPDATE: In the last week or two the example was refactored to use localStorage over sessionStorage as sessionStorage is not shared between tabs and sharing non-senstive data this way reduces the number of unnecessary auth checks and keeps session status consistent between tabs.

possibilities commented 7 years ago

Maybe useful to some people I made this sample app while experimenting:

https://github.com/possibilities/next.js-with-auth

Backed by this toy backend:

https://github.com/possibilities/micro-auth

Deployed here:

https://next-with-auth.now.sh/

Backend here:

https://micro-auth.now.sh/

iaincollins commented 7 years ago

@possibilities Thanks Mike! As well as exposing a separate microservice that shows a nice way of handling secure pages that I was thinking might be good pragma and might take inspiration from. I have some ideas which will will raise in the next.js-with-auth repo.

possibilities commented 7 years ago

After giving some more thought I'd probably not hold my efforts above as a great example. I would likely change to having the signup/signin fully submit web 1.0 style so that we can establish an HTTP only cookie on the server (eliminating accessibility to the JWT via XSS) and then attach the user object to req rather than the entire token. This leaves the possibility of CSRF vulnerabilities but I think this is more straightforward to mitigate than XSS (no?). A nice side effect of this is then the client app can use a nearly identical flow for when signing in via an oath service.

I also see in hindsight I could avoid the "middleware" (and therefore need for custom server) by parsing the cookie in the Page HoC's getInitialProps.

rajaraodv commented 7 years ago

@possibilities Since the 'secret' page is just a page and is bundled by webpack, wouldn't it be served to the browser if i go to /secret?

possibilities commented 7 years ago

Yeah, I suppose that should be wired to do a server-side redirect.

possibilities commented 7 years ago

BTW, I got distracted back to my initial project of signing in with github. The flow is similar with more care about security (namely not exposing any secret on the client to avoid oauth token being exposed via XSS). It's tied up in a proper app but if there's any interest I could break it out into something that could be useful for oauth flow in general.

sedubois commented 7 years ago

@possibilities I think it would be an awesome help, if there could be a PR with a most minimal (but proper) with-auth example 🙂 I'm working on auth in my app (https://github.com/relatenow/relate) but it's currently only client-side (localStorage).

impronunciable commented 7 years ago

@sedubois I have a pr for that using passwordless.net https://github.com/zeit/next.js/pull/646 but we'll move the auth server somewhere else

espoal commented 7 years ago

what about using graphql?

Apollo gives an example of graphql authentication:

https://dev-blog.apollodata.com/a-guide-to-authentication-in-graphql-e002a4039d1

Here we are authenticating a graphql request, but it could be adapted for our case.

Plus graphql can abstract away implementation and logic. It could be used with passwordless, auth0, or anything else we might prefer.