vercel / next.js

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

How to run passport-local with next.js #1777

Closed sunnixx closed 7 years ago

sunnixx commented 7 years ago

Hi Sorry I am new to Next.js, so I am basically stuck at a place where I don't know how to authenticate a route with passport.js Normally we would just do this:

router.get('/',function(req,res){
    res.render('index', {
        isAuthenticated: req.isAuthenticated(),
        user: req.user
    });
});

and we can check whether the user is authenticated to access the route. but with passport.js and custom express server, the above strategy doesn't seem to be working.

How can I edit my current code to fit the above description.

server.get('/', (req, res) => {
    return app.render(req, res, '/index', req.query)
  })

Thank You.

njj commented 7 years ago

There is some discussion of this on #153

kolpav commented 7 years ago

@sunnixx I am no expert on this but after some trial and error I have managed to authenticate routes by storing JWT inside cookie that way I can even authenticate against my external apis without much trouble I just need to share secret key. I found using sessions much more awkward/complicated.

sunnixx commented 7 years ago

@kolpav can you share some code / rep ?

njj commented 7 years ago

@sunnixx There is also this, https://github.com/iaincollins/nextjs-starter

sunnixx commented 7 years ago

@njj thanks, I've looked into this. I've come across a simpler solution but it's not optimal.

server.get('/', (req, res) => {
    if(req.user){
      return app.render(req, res, '/index', req.query)
    }else{
      res.redirect('/login');
    }
  })

I don't recommend this, but for the time being this does the trick.

njj commented 7 years ago

@sunnixx Share more of your example if possible, the community will be thankful :)

sunnixx commented 7 years ago

@njj definitely, I'll share some more examples for the authentication part.

//PASSPORT STRATEGY
passport.use(new Strategy(function(username, password, done){
    User.findOne({username:username},function(err,user){
        if(err) return done(err);
        if(!user){
            return done(null,false);
        }
        if(!user.comparePassword(password)){
            return done(null,false);
        }
        return done(null,user);
    });
}));

//passport Serialization
passport.serializeUser(function(user,done){
    done(null,user._id);
});

//passport Deserialize
passport.deserializeUser(function(id,done){
    User.findById(id,function(err,user){
        if(err) return done(err);
        done(null,user);
    })
});
//MIDDLEWARE
server.use(bodyParser.json())
  server.use(bodyParser.urlencoded({extended: false}))
  server.use(cookieParser())
  server.use(session({
    secret: process.env.SESSION_SECRET || secret.key,
    resave: true,
    saveUninitialized: false,
    store: new MongoStore({url:secret.database})
  }))
  server.use(passport.initialize())
  server.use(passport.session())

//Handling Authentication on routes
server.get('/', (req,res) =>{
    if(req.user){
        app.render(req,res, '/index',req.query);
    }else{
        res.redirect('/login');
    }
})
server.post('/login', passport.authenticate('local',{failureRedirect: '/login'}), (req,res) => {
    res.redirect('/');
}

Hope this helps !! 👍

njj commented 7 years ago

@sunnixx Looks good, I'd even recommend that isUserAuthenticated (or whatever they named it) middleware method that they do in most of the Passport examples. Just so you can tack it on to your routes w/o having to always add that logic for checking the user.

bresson commented 7 years ago

@sunnixx looks good. Did the solution work? I've seen others solutions( https://nextjs-starter.now.sh and https://github.com/luisrudge/next.js-auth0 ) but as far as I can tell, neither leverage the backend ...?

Also does the server authentication refresh the entire page? I'm afraid any state on the client will be lost ( unless saved somewhere ) in a browser refresh

mpowell90 commented 6 years ago

I'm also having issues with passport local auth strategy - serverside code works as expected, logging in user when correct username and password is supplied, until attempting to render route after successful login. The redirect hangs and doesn't render '/profile' page. After a few seconds I get the following:

GET /profile 200 52.725 ms - - GET /_next/on-demand-entries-ping?page=/login 200 2.336 ms - - GET /_next/on-demand-entries-ping?page=/login 200 4.323 ms - - GET /_next/on-demand-entries-ping?page=/login 200 4.777 ms - - Disposing inactive page(s): /

Example Server Code (cut down for brevity)
// Initialise Passport
  server.use(passport.initialize())
  server.use(passport.session())

  server.get('/login',
    (req, res) => {
      if (req.user === undefined) {
        return app.render(req, res, '/login', req.params)
      }
      else {
        return app.render(req, res, '/profile', req.params);
      }
    }
  );

server.post('/login', (req, res, next) => {
    // Provided by connect-ensure-login function
    let returnURL = req.session.returnTo
    console.log("ReturnURL:", returnURL);
    console.log("Req Method:", req.method);
    console.log("Req URL:", req.url);
    passport.authenticate('local', (err, user, info) => {
      if (err) {
        // Authentication failed - Error 500 - Server Error
        return next(err);
      }
      if (!user) {
        // Authentication failed - Error 401 Missing Credentials
        return res.status(401).json(info)
      }
      req.login(user, (err) => {
        if (err) { return next(err); }
        // Trigger User Login - not working!
        return res.redirect('/profile');
      });
    })(req, res, next)
  });

  server.get('/logout', (req, res) => {
      req.logout()
      res.redirect('/')
      // return app.render(req, res, '/')
    }
  )

Login Component:

import React, { Component } from 'react'
import { Button, Form, Grid, Header, Image, Loader, Message, Segment } from 'semantic-ui-react'

// Services
import Session from '../services/session'

export default class extends Component {
  static async getInitialProps({req}) {
    // On the sign in page we always force get the latest session data from the
    // server by passing 'true' to getSession. This page is the destination
    // page after logging or linking/unlinking accounts so avoids any weird
    // edge cases.
    const session = new Session({req})
    const sess = await session.getSession(true)
    console.log(sess);
    return {session: sess}
  }

  async componentDidMount() {
    // Get latest session data after rendering on client
    // Any page that is specified as the oauth callback should do this
    const session = new Session()
    this.state = {
      email: this.state.email,
      session: await session.getSession(true)
    }
  }

  constructor(props) {
    super(props)
    this.state = {
      email: '',
      password: '',
      csrfToken: this.props.session.csrfToken,
      xhrMessage: null,
      formError: false,
      isLoading: false
    }
  }

  handleChange = (e, { name, value }) => this.setState({ [name]: value })

  handleSubmit = (e) => {
    // e.preventDefault()
    const { email, password } = this.state
    let message

    let xhr = new XMLHttpRequest()

    xhr.open("POST", "/login")

    xhr.setRequestHeader('Content-Type', 'application/json')

    xhr.onreadystatechange = () => {
      if (xhr.readyState < 4) {
        this.setState({ isLoading: true });
      }
      else if (xhr.readyState === 4) {
        if (xhr.status === 401) {
          let response = JSON.parse(xhr.responseText);
          return this.setState({ formError: true, xhrMessage: response.message, isLoading: false });
        }
        if (xhr.status === 500) {
          return this.setState({ formError: true, xhrMessage: "Something went wrong, check your internet connection and try again", isLoading: false });
        }
      }
    }
    xhr.onerror = () => {
      Error('XMLHttpRequest error: Unable to get confirmation')
    }
    xhr.send(
      JSON.stringify({
        email: email,
        password: password,
        _csrf: this.state.csrfToken
      })
    )
  }

  render() {
    const { email, password, xhrMessage, formError } = this.state

    return (
      <div className='login-form'>
        <style>
          {`
            div.login-form {
              height: 100%;
            }
          `}
        </style>
        <Grid
          textAlign='center'
          style={{ height: '700px' }}
          verticalAlign='middle'
        >
          <Grid.Column style={{ maxWidth: 450 }}>
            <Header as='h2' color='green' textAlign='center'>
              Log-in to your account
            </Header>
            <Form size='large' onSubmit={this.handleSubmit} error={formError}>
              <Segment stacked>
                <Loader active={this.state.isLoading} />
                <Message
                  error
                  header="Login Unsuccessful"
                  content={xhrMessage}
                />
                <input type="hidden" name="_csrf" value={this.props.session.csrfToken}/>
                <Form.Input
                  fluid
                  icon='user'
                  iconPosition='left'
                  name='email'
                  value={email}
                  placeholder='E-mail address'
                  onChange={this.handleChange}
                />
                <Form.Input
                  fluid
                  icon='lock'
                  iconPosition='left'
                  placeholder='Password'
                  type='password'
                  name='password' value={password}
                  onChange={this.handleChange}
                />
                <Form.Button content='Submit' color='green' fluid size='large'/>
              </Segment>
            </Form>
            <Message>
              New to us? <a href='#'>Sign Up</a>
            </Message>
          </Grid.Column>
        </Grid>
      </div>
    )
  }
}

The session object is derived from Iain Collins example - https://nextjs-starter.now.sh. I'm pulling my hair out here, any help appreciated!

iaincollins commented 6 years ago

I apprecaite this is late, but it in case it helps someone else, all you need to do to add login support is something like this:

express.post(`login`, (req, res) => {
  const email = req.body.email || null
  const password = req.body.password || null
  find({email: email, password: hashPassword(password)})
  .then(user => {
    if (user === true) {    
      req.logIn(user, (err) => {
        if (err) throw err
        return res.redirect('/login?success=true')
      })
    return res.redirect('/login?success=false')
  })
  .catch(err => {
    return res.redirect('/error')
  })
})

If you are using the render function - e.g. return app.render(req, res, '/login', req.params) - to render pages on sign in routes, you might want to swap them out for redirects - e.g. return res.redirect('/login').

kaym0 commented 6 years ago

Ended up getting it working. Axios post requests weren't, for some reason, allowing the res.redirect to work. So I had to implement a solution which involved sending the route via json to the client side, where I used conditionals to route to the appropriate pages using

window.location = '/index'
kaym0 commented 6 years ago

Have any of you been able to implement flash messages using express-flash or flash (npm package, not Adobe)? I have no idea how to do it with React and I cannot find any sort of documentation around. Even if you could point me in the direction of someones Github who has implemented it in one of their public projects, that would be great!

jimmylee commented 6 years ago

Sorry to post on a dead thread... but I just stumbled upon this.

In case anyone is having trouble with implementing passport and NextJS, I have a working example of passport-local 1.0.0 && next 6.1.1 here: https://github.com/jimmylee/next-postgres. The example is just a boilerplate example so you'll have to do some more work to get it production ready and secure.