markdown 挑战16:本地认证

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了markdown 挑战16:本地认证相关的知识,希望对你有一定的参考价值。

# Challenge - Local Authentication

In this challenge you'll add authentication using Passport Local Strategy to the Noteful app.

## Requirements

## Save a new User to the DB

The first challenge is to add the ability to save a user to the database. This means you need to create a User schema and User model, and you need to add a `POST /api/users` endpoint to create the new user.

### Create `User` schema and model

Create a `/models/user.js` file and add a `userSchema` and `User` model. The schema should have the following:

- `fullname` as a String
- `username` as a String that is both required and unique
- `password` as a String that is required

Like the previous challenges, we'll use the mongoose `transform` function to modify the results from the database and create a representation. But this time you also need to prevent the password from being returned, so add a `delete ret.password;` statement to `userSchema.set('toObject'...)` like the following.

```js
userSchema.set('toObject', {
  virtuals: true,     // include built-in virtual `id`
  versionKey: false,  // remove `__v` version key
  transform: (doc, ret) => {
    delete ret._id; // delete `_id`
    delete ret.password;
  }
});
```

> Note, the password needs to be selectable in order to be compared with the incoming password during authentication, so don't use `select: false` in the schema. It will not work in this scenario.

Remember to export the model so it can be used on in other files.

### Create a POST `/api/users` endpoint

Next you need to create an endpoint to insert users.

Create a `/routes/users.js` file with a single POST endpoint. The endpoint creates a new user in the database and responds with a 201 status, a location header and a JSON representation of the user **without the password**.

> Note, you will add input validation later in the challenge, after adding Bcryptjs. For now, focus on creating the ability to insert a new user.

### Update server.js

Finally, you need to mount the new `/users` route on `/api` base path.

Update `server.js` to require the `/users` route.

```js
const usersRouter = require('./routes/users');
```

And mount the route on the `/api` path.

```js
app.use('/api', usersRouter);
```

Test the new `User` model and `/api/users` route using Postman. Verify new users are saved to the database

## Create a Local Strategy and `/login` endpoint

Now that you have users we can create a `/login` endpoint which check the incoming username and password against the database and conditionally allows the user access or returns an unauthorized error.

First, you will need to create a Passport Local Strategy, then you can create a `/login` endpoint which is protected by the strategy.

### Create a Passport Local Strategy

Create a `/passport/local.js` directory and file. In the file, create Passport Local Strategy that finds the user and validates the password. For now you will compare the **plain-text** passwords, later you will had bcryptjs to hash and compare the password.

Below are the basic steps followed by example code to get you started.

- First, `npm install` both `passport` and `passport-local` in the project
- In `/passport/local.js`
  - Require `passport-local` in the file and set the `Strategy` property to a local variable named `LocalStrategy` using object destructuring.
  - Require the `User` model
  - Define a new local strategy using `new LocalStrategy` using the skeleton code below.
- And in `/models/users.js`
  - Add a `validatePassword` method which compare the incoming password to the plain-text password in the database.

Example code for `/passport/local.js`

```js
const { Strategy: LocalStrategy } = require('passport-local');
const User = require('../models/user');

// ===== Define and create basicStrategy =====
const localStrategy = new LocalStrategy((username, password, done) => {
  let user;
  User.findOne({ username })
    .then(results => {
      user = results;
      if (!user) {
        return Promise.reject({
          reason: 'LoginError',
          message: 'Incorrect username',
          location: 'username'
        });
      }
      const isValid = user.validatePassword(password);
      if (!isValid) {
        return Promise.reject({
          reason: 'LoginError',
          message: 'Incorrect password',
          location: 'password'
        });
      }
      return done(null, user);
    })
    .catch(err => {
      if (err.reason === 'LoginError') {
        return done(null, false);
      }
      return done(err);
    });
});
```

Add the following to `/models/users.js`

```js
userSchema.methods.validatePassword = function (password) {
  return password === this.password;
};
```

> Again, this code assumes a plain-text password and `.validatePassword` is synchronous. You will update the `.validatePassword` method to an async hashing process in a later step.

### Create protected login route

Create a `/routes/auth.js` file. In the file create a POST `/login` route which is protected by the local strategy you just created. Below is skeleton code but you will need to setup the correct require and export statements.

> Note: the following example uses `failWithError: true` which is unique to the Noteful app. The `failWithError` option configures the middleware to throw an error instead of automatically returning a 401 response. The error is then caught by the error handling middleware on `server.js` and returned as JSON.

```js
const options = {session: false, failWithError: true};

const localAuth = passport.authenticate('local', options);

router.post('/login', localAuth, function (req, res) {
  return res.json(req.user);
});
```

### Update `server.js`

Update `server.js` to require the new strategy and `login` router. Configure passport to utilize the strategy and mount the router.

Towards the top of the file, add the require statements

```js
const passport = require('passport');
const localStrategy = require('./passport/local');

// Other statements removed for brevity
const authRouter = require('./routes/auth');
```

Configure Passport to utilize the strategy

```js
passport.use(localStrategy);
```

Then mount the new router along with the existing routers

```js
app.use('/api', authRouter);
```

Test the new `/login` route using Postman. Verify you can create a new user and then login.

## Add Bcrypt

Currently, the passwords are stored in plain-text, let's fix that!

### Install `bcryptjs` and add `.hash()` and `.compare()` methods

First, `npm install bcryptjs`. Please use `bcryptjs` and **not** `bcrypt`.

Add the `.hashPassword` and update `.validatePassword` methods to the User Schema in the `/models/user.js` file. Remember to require `bcrypt` in the file.

```js
userSchema.methods.validatePassword = function (password) {
  return bcrypt.compare(password, this.password);
};

userSchema.statics.hashPassword = function (password) {
  return bcrypt.hash(password, 10);
};
```

### Update POST `/users` endpoint to use `.hashPassword`

Next, update the POST `/users` endpoint to use `.hashPassword` method. You must first call `.hashPassword()` to create the digest (aka hash). Since the hashing process returns a promise, you can chain the calls using `.then()`.

```js
return User.hashPassword(password)
  .then(digest => {
    const newUser = {
      username,
      password: digest,
      fullname
    };
    return User.create(newUser);
  })
  .then(result => {
    return res.status(201).location(`/api/users/${result.id}`).json(result);
  })
  .catch(err => {
    if (err.code === 11000) {
      err = new Error('The username already exists');
      err.status = 400;
    }
    next(err);
  });
```

### Update the Local Strategy to use `.validatePassword()`

Update the Local Strategy to use `.validatePassword()`.

```js
  let user;
  User.findOne({ username })
    .then(results => {
      user = results;
      if (!user) {
        // Removed for brevity
      }
      return user.validatePassword(password);
    })
    .then(isValid => {
      if (!isValid) {
        // Removed for brevity
      }
      return done(null, user);
    })
    .catch(err => {
      // Removed for brevity
    });
```

> Note: Remember to drop the `users` collection or delete users that have plain-text password.

Test the process using Postman:

- Register: Create a new user by posting to the `/api/users` endpoint
- Inspect the database to ensure the password was hashed before saving
- Login: Post the username and password to `/api/login`, you should receive a representation of the user **without the password**.

## Add validation to the POST `/users` endpoint

The API currently works but users can enter simple passwords with only two or three characters. Add validation before persisting the user to the DB which performs the following checks.

- The `username` and `password` fields are required
- The fields are type string
- The `username` and `password` should not have leading or trailing whitespace. And the endpoint should **not** automatically trim the values
- The `username` is a minimum of 1 character
- The `password` is a minimum of 8 and max of 72 characters

Here is an example for the first validation is provided to help get you started.

```js
  const requiredFields = ['username', 'password'];
  const missingField = requiredFields.find(field => !(field in req.body));

  if (missingField) {
    const err = new Error(`Missing '${missingField}' in request body`);
    err.status = 422;
    return next(err);
  }
```

以上是关于markdown 挑战16:本地认证的主要内容,如果未能解决你的问题,请参考以下文章

什么是Windows挑战和响应认证流程

物联网之MQTT3.1.1和MQTT5协议 (16) AUTH 报文

基于挑战/应答的认证

markdown 值得关注的挑战指数

markdown Certbot与DNS挑战

markdown 挑战11:Mongo基础知识