Vapor 4 Authentication: Getting Started [FREE]

栏目: IT技术 · 发布时间: 4年前

内容简介:How can you make sure your server confirms a user’s identity at every request while keeping high standards of performance and security?In this tutorial, you’ll learn how to leverageVapor 4’s authentication model to identify and persist users. When you’re d

Authentication is the process of identifying your users and verifying they are who they claim to be. This is separate from authorization , which sounds similar but actually handles protecting user data and making sure other users can’t access it.

How can you make sure your server confirms a user’s identity at every request while keeping high standards of performance and security?

In this tutorial, you’ll learn how to leverageVapor 4’s authentication model to identify and persist users. When you’re done, you’ll know what the most common authentication techniques are for server-side apps and how to implement two of those techniques with Vapor 4.

Getting Started

Use the Download Materials button at the top or bottom of this page to download everything you need for this tutorial.

The sample app for this tutorial is DiningIn , which lets people host dinners and invite friends to join them.

In the download materials, you’ll find two folders containing the starter and final projects. There are also two files, which contain calls to all the endpoints the server will have at the end of this tutorial. They’ll help you test the calls when you add the methods to the starter project.

DiningIn-API.paw works with Paw , a native API tool for macOS. If you’re used to Postman or you develop on a Linux machine, then the other file is for you.

Additionally, a SQLite browser will help you see the data the server saves. SQLite Browser is an open-source option that works well for this tutorial.

Setting up the Authentication Project

Open the DiningInStarter directory and either double-click Package.swift or enter, in your favorite terminal app, the following command:

open Package.swift

Xcode will open your project and automatically use the Swift Package Manager (SwiftPM) to start fetching the dependencies declared in Package.swift .

Looking at the Project

While SwiftPM does its job, take a few moments to familiarize yourself with the files located in the Sources/App directory.

Vapor 4 Authentication: Getting Started [FREE]

The DiningIn starter project’s file tree

Start by looking at the classes defined in the Models directory:

  • User : Represents a user entry in your server’s database.
  • Dinner : Represents a scheduled dinner.
  • DinnerInviteePivot : Links dinners and invited users.

When looking into these class properties, notice how Vapor 4 uses property wrappers, a new feature in Swift 5.1. The @ID , @Field and @Timestampable wrappers, among others, play an essential role for Fluent, Vapor’s database framework.

Now, open the files in the Controllers folder to get a sense of how Vapor 4 handles routing.

Next, open the Migrations directory and check how you add a new table and its columns to the database. Also, pay attention to how each property has additional attributes, like constraints or references to other tables.

Finally, open configure.swift to see the initial configuration of the server: database initialization, middleware and configuring the database migrations described in the previous paragraph.

Note : For the sake of simplicity, this tutorial uses an on-disk SQLite database. In later stages of development, and certainly in production, you should use a scalable database running on a dedicated server. Good choices include PostgreSQL and MySQL.

Running the Starter Project

By now, SwiftPM has finished downloading all the dependencies. Click the Run button in Xcode or press Command-R to make sure the project compiles successfully.

Before moving to the hands-on part of the tutorial, it’s important to go over some theory. There are fundamental differences between a client-side app and a server app when dealing with users.

Later, you’ll learn how to model the data to allow the server to accept user registrations while also identifying the current user making the request.

Why Authentication and Authorization Are Essential on the Server

When you make client-side apps, you’re usually dealing with a single logged-in user. The app needs to be aware of only one current user and execute requests for that user alone.

However, when designing a server’s architecture, you need to think differently. Your server receives requests from all your users, so it’s aware of all users, and it has access to all their data.

Therefore, it must have a way to certify, for every request, the following two things:

  • Authentication: Each user is who they claim to be.
  • Authorization: Users can only access their own data and no one else’s.

You can compare the client app to a building where each resident has a storage room inside their own apartment. They own their own unit, and they’re only aware of their own storage, because they’re physically separated. They’re on different floors and in their own apartments.

The same is valid for a client-side app — the database and related files run in different installations, and they’re not necessarily aware of other users.

Vapor 4 Authentication: Getting Started [FREE]

Each client has its own database to store user’s data

On the other hand, your server is like a shared mail room. An employee receives packages and writes down the recipient’s ID. Whenever anyone wants to get their packages, the employee checks the ID, verifies they are who they claim to be, checks where the package is stored and delivers it.

The same is true for the server’s role when dealing with users’ data.

Vapor 4 Authentication: Getting Started [FREE]

The server stores all users’ data in a larger and more scalable database. Only the server should access it.

Note : This tutorial only deals with authentication — how to verify the identity of a user.

Authentication Mechanisms

It’s important to have an overview of the available authentication options, even though this tutorial will only cover two of them.

  • Session Authentication : Upon login, the server generates a Session ID that the browser stores in a cookie. Every request the browser issues contains the Session ID; the server validates this cookie on every request. This mechanism is “stateful” — the server needs to keep track of the state of the session to validate it.
  • Basic Authentication : This consists of sending the username and the password encoded in base 64 as the Authorization header. The value of the header looks like Basic <encoded username:password> . In theory, the server could use this method for every request. However, the client shouldn’t send the password over the network, even encoded, unless necessary. A good practice is to use Basic Authentication to log the user in, then to request a token.
  • Bearer : In this format, every request has an  Authorization header where the value is Bearer <token value> . The client sends the token in every request to identify the user. Each logged-in device, even for the same user, will have a different token. The advantage of this approach is that the requests don’t contain the password. Plus, the server or client can revoke the tokens, which logs out the sessions related to that token. This is useful if someone steals a device or compromises the tokens.
  • JSON Web Tokens (JWTs) : A stateless version of the Bearer token. The server doesn’t keep a record of the token. Instead, it uses the encrypted data the JWT provides for every request.
  • OAuth : An open standard for authorization. It allows an authorization server, which might be a third party like Facebook, Google or GitHub, to provide access to another server’s resources on behalf of a user.

This tutorial will not cover sessions, JWTs or OAuth. It will focus on basic and bearer authentication.

Adding Support for Token-Based Sessions

The starter project already contains the model and routes that allow users to sign up and fetch dinner information. The sign-up routes are ready, but you still need to implement user creation and token-based sessions.

Adding the Token Model

To start, select the Models folder, press Command-N to add a new Swift file , and name it Token.swift . Then replace the boilerplate with the following code:

import Vapor
import Fluent

enum SessionSource: Int, Content {
  case signup
  case login
}

//1
final class Token: Model {
  //2
  static let schema = "tokens"
  
  @ID(key: "id")
  var id: UUID?
  
  //3
  @Parent(key: "user_id")
  var user: User
  
  //4
  @Field(key: "value")
  var value: String
  
  //5
  @Field(key: "source")
  var source: SessionSource
  
  //6
  @Field(key: "expires_at")
  var expiresAt: Date?
  
  @Timestamp(key: "created_at", on: .create)
  var createdAt: Date?
  
  init() {}
}

Here’s what you’re doing in this code:

  1. You declare the Token class, which implements Fluent’s Model protocol.
  2. You implement the static variable schema , which Fluent uses to find the correct table in the database. You also implement id , which is the object’s ID in the table.
  3. You add a field to store the user ID, with a relationship to the User table, to link every token to a user.
  4. You add a field to store the value of the token itself, which clients will store and send in later requests.
  5. To save the source of this session, you add a field for the SessionSource enum declared a few lines above when the user signs up and logs in. Eventually, this will work for other social logins as well.
  6. To allow expiring tokens, you add an expiresAt date field and another date field for the token’s creation date.

Note : The last two properties are not strictly required. However, it’s a good security practice to expire tokens and save their creation date.

Adding Initializers to Tokens

To allow users to create new tokens, this model needs an initializer. You’ll add this next.

Add the following below the empty init() :

init(id: UUID? = nil, userId: User.IDValue, token: String, 
  source: SessionSource, expiresAt: Date?) {
  self.id = id
  self.$user.id = userId
  self.value = token
  self.source = source
  self.expiresAt = expiresAt
}

This is a standard initializer, which sets all the properties defined at creation. Notice the usage of the $ sign when setting the user. By doing this, you access the wrapper itself instead of the wrapped value and set the token’s user_id based on the userId , which is a UUID .

Build and run the server. When the server starts, Vapor prints the path of the working directory, where you’ll find the .sqlite file, to the console. Using Finder or the terminal command line, open this location, then open DiningIn.sqlite in your SQLite browser:

Vapor 4 Authentication: Getting Started [FREE]

The four tables present in the starter project.

There are four tables: users , dinners , a table linking them and another table that Fluent uses to manage migrations.

However, the tokens table doesn’t exist yet. It’s time to create it.

Creating the Migration

To create the tokens table in the database, you need to create a Migration and run it. One option is to run it manually via the command line, while the other is to automatically run migrations when the server starts. The latter option is already present in configure.swift .

Add a file named CreateTokens.swift to the Migrations folder. Then replace the boilerplate with the following code:

import Fluent

// 1
struct CreateTokens: Migration {
  func prepare(on database: Database) -> EventLoopFuture<Void> {
    // 2
    database.schema(Token.schema)
       // 3
      .field("id", .uuid, .identifier(auto: true))
      .field("user_id", .uuid, .references("users", "id"))
      .field("value", .string, .required)
      .unique(on: "value")
      .field("source", .int, .required)
      .field("created_at", .datetime, .required)
      .field("expires_at", .datetime)
      // 4
      .create()
  }

  // 5
  func revert(on database: Database) -> EventLoopFuture<Void> {
    database.schema(Token.schema).delete()
  }
}

Here’s what you’re doing in this code:

  1. You declare a struct that conforms to the Migration protocol.
  2. In prepare(on:) , you create a SchemaBuilder from the database with the schema declared by the Token .
  3. You add the fields based on their key, type and constraints. Notice how the user_id field has a reference to the id field in the users table.
  4. You call create() to execute the operation of adding the table to the database. Notice the lack of the return key in this method, which is new from Swift 5.1 in single expression function bodies. create() returns a future of type Void , which is exactly what prepare(on:) expects.
  5. The second required method, revert(on:) , should do what its name says: Apply the opposite changes from those you made in the previous method. You create the same SchemaBuilder , but now call delete() and return this operation.

Note : You declare another property in the Migration protocol: name , of type String . The default implementation generates a string from the struct or class name, but having it as a property allows you to set a custom name. The name should be unique for each migration.

Running the Migration

Once you’ve defined the migration, you need to add it.

Open configure.swift and, after you call the last app.migrations.add(...) , add the freshly-created migration:

app.migrations.add(CreateTokens())

Now, build and run, then open or refresh the SQLite browser. You’ll now see the tokens table!

Vapor 4 Authentication: Getting Started [FREE]

The tokens table is now present, with all the fields defined in the migration.

Allowing Users to Sign up

Now that the server is ready for token-based sessions, you can start writing the methods that handle signup and login requests. The signup request returns a User , which needs to include the session token generated upon signup.

Worry not, you’ll add this now!

Creating a New User

The response to a signup or login request should include both the user information and the generated token. This is exactly why the NewSession struct exists.

Open Controllers/UserController.swift and scroll down to create(req:) .

The first thing to do is to change the return type. At the end of the line, replace EventLoopFuture<User.Public> with this:

EventLoopFuture<NewSession>

The first three lines of this method do a validation of the request body, decode the body into UserSignup , then create a new, not saved, user object by calling User.create(from: userSignup) .

Jump to the definition of User.create(from:) ( Command-click or Control-Command-click create ) or just open Models/User.swift and scroll to the bottom. You’ll see this method currently throws an Abort(.notImplemented) error.

Replace this line with the following code:

User(username: userSignup.username,
  passwordHash: try Bcrypt.hash(userSignup.password))

You’re creating a new user object with a username and a password.

As a basic security practice, servers must never store the passwords as plain text, but should hash them instead. This is what the second line does, using Bcrypt .

Note : This tutorial doesn’t enforce lowercase usernames so that you can keep your focus on authentication. You might consider storing and querying usernames in their lowercase variants to make it easy for your users to log in and to not confuse usernames.

Creating Tokens for a User

Next, you need to make it possible to create a new token for a given user.

Below User.create(from:) , add the following method:

// 1
func createToken(source: SessionSource) throws -> Token {
  let calendar = Calendar(identifier: .gregorian)
  // 2
  let expiryDate = calendar.date(byAdding: .year, value: 1, to: Date())
  // 3
  return try Token(userId: requireID(),
    //4
    token: [UInt8].random(count: 16).base64, source: source,
    expiresAt: expiryDate)
}

Here’s what you’re doing in this function:

  1. This is a throwing function (more on that in step 3) on User , which receives a SessionSource and returns a new Token .
  2. You use a Calendar to generate a date a year ahead of the current date, which you’ll use as the expiry date. Change this line if you want a shorter or longer expiry date.
  3. Using the initializer you created in the previous section, you generate the token. requireID() throws an error when the user isn’t in the database and doesn’t have an ID yet.
  4. Generate the token value itself by creating 16 random bytes and getting the token’s Base 64-encoded string representation.

Including the Token in the Response

Once you’ve implemented createToken(source:) , it’s time to activate it in the user controller.

Go back to Controllers/UserController.swift ‘s create(req:) . Add a declaration for the token below the user property:

var token: Token!

Now, replace the .flatMapThrowing closure after return user.save(on: req.db) with the following:

.flatMap {
  // 1
  guard let newToken = try? user.createToken(source: .signup) else {
    return req.eventLoop.future(error: Abort(.internalServerError))
  }
  // 2
  token = newToken
  return token.save(on: req.db)
}.flatMapThrowing {
  // 3
  NewSession(token: token.value, user: try user.asPublic())
}

This is what you’re doing in this chunk of code:

  1. You create a new token from the fresh user you just saved. If it fails, it throws an error.
  2. You keep a reference to the token and save it.
  3. Upon completion, you initialize the NewSession with the token value and the User.Public from the user . The User.Public struct is a technique used to send information to the clients without exposing fields that are internal to the server.

It’s finally time to test the first API you created with this tutorial!

Build and run, then open the Paw or Postman API file from this tutorial’s download materials. Select the (1) Sign up request, set the server URL to localhost:8080 , then send the request.

You’ll get a JSON response that includes both the token and the user:

{
  "token": "t+oHBUwU2Rv+qp7O2Ed0UQ==",
  "user": {
    "username": "NatanTheChef",
    "id": "138191B9-445D-442D-9F70-B858081A661B",
    "updated_at": "2020-03-07T19:40:54Z",
    "created_at": "2020-03-07T19:40:54Z"
  }
}

Note : Your token and id values will be different.

Congratulations! You’ve created a new user and token, and returned that token in your server response. Next, you’ll use that token to authenticate the user.

Authenticating the User With a Token

There are two ways to confirm the server saved the user: The first is to use the SQLite browser by opening the users table to see the new record — select the users table, then select the Browse Data tab. The second is to add an endpoint allowing users to fetch their own information.

Your next step will be to add that endpoint.

Supporting Basic Authentication on the User Model

In order for Vapor to know that a User instance can be used for authentication, you need to conform User to ModelAuthenticatable . Vapor uses this protocol to perform all the steps around authenticating a user with a username and a password in the request headers and to link it to tokens.

Open User.swift and add the following extension:

extension User: ModelAuthenticatable {
  // 1
  static let usernameKey = \User.$username
  static let passwordHashKey = \User.$passwordHash
  
  // 2
  func verify(password: String) throws -> Bool {
    try Bcrypt.verify(password, created: self.passwordHash)
  }
}

Fluent’s ModelAuthenticatable protocol is succinct. First, it requires two KeyPath s, where the value is a field of type String . This tells Vapor which fields to look for when querying the user upon authentication.

Secondly, it requires your user model to implement verify(password:) , which determines whether the received password matches the password hash and returns the result. As with the registration, this function also uses Bcrypt to perform this check.

Conforming a Token to the ModelTokenAuthenticatable Protocol

Next, Vapor needs to know that it can use the Token for authenticating users. This lets it provide the authentication middleware, which is able to find the user for a token supplied in the request authentication header.

Open Models/Token.swift and add this extension:

extension Token: ModelTokenAuthenticatable {
  //1
  static let valueKey = \Token.$value
  static let userKey = \Token.$user

  //2
  var isValid: Bool {
    guard let expiryDate = expiresAt else {
      return true
    }
    
    return expiryDate > Date()
  }
}

Fluent’s ModelTokenAuthenticatable protocol is also very concise. First, it needs two KeyPath s — one for the token value field and another for the user relationship. Next, it checks the token to see if it’s valid at a specific moment. If your tokens don’t expire, simply return true .

In this case, you take expiresAt and compare it to the current date. If the current date is later than the expiry date, it’s invalid, causing Vapor to delete the token from the database and return  401 unauthorized .

Adding the Me Endpoint

Now Vapor can provide the token authenticator middleware. Go back to UserController.swift and add the following lines to the end of boot(routes:) :

let tokenProtected = usersRoute.grouped(Token.authenticator())
tokenProtected.get("me", use: getMyOwnUser)

The first line creates a router using the /users path defined in the first line of this method, and also wraps them in an authentication middleware. This means that every request going through the middleware requires an authenticated user.

The second line makes the /users/me endpoint use getMyOwnUser(req:) . Scroll down to this function and replace the thrown error with the following line:

try req.auth.require(User.self).asPublic()

This returns the user information by accessing the request’s authentication cache, fetches the user who’s performing the request and converts it to a public user. If the request isn’t authenticated, then it throws a 401 unauthorized error.

Whenever you need to get the current user from within a request, simply use req.auth.require(User.self) , as long as the request came through an authentication middleware, as shown above.

Build and run, then send the (2) Me request in the API file. The response should contain the user object you just signed up:

{
  "username": "NatanTheChef",
  "id": "138191B9-445D-442D-9F70-B858081A661B",
  "updated_at": "2020-03-07T19:40:54Z",
  "created_at": "2020-03-07T19:40:54Z"
}

Note : If you get an Unauthorized error, run the Sign up request again, to reset the token.

Adding the Login Endpoint

So far, users can sign up but cannot log in, so the server is only doing half its job. But don’t worry: Part of the code you added in the previous section makes dealing with login requests much easier.

Implementing the Login Route

Because User conforms to ModelAuthenticatable , you can use the User basic authentication middleware that Vapor provides out of the box.

Begin by adding the route in the boot(routes:) method of UserController :

let passwordProtected = 
  usersRoute.grouped(User.authenticator().middleware())
passwordProtected.post("login", use: login)

This is similar to the /users/me endpoint, but instead of using the Token authenticator, or bearer, it uses the User basic authenticator. Now, scroll down to the login(req:) method, delete the thrown error and add the code below:

// 1
let user = try req.auth.require(User.self)
// 2
let token = try user.createToken(source: .login)

return token
  .save(on: req.db)
  // 3
  .flatMapThrowing {
    NewSession(token: token.value, user: try user.asPublic())
}

Step-by-step, this is what you’re doing:

  1. Similarly to the me function, you get the user from the request authentication cache. Although the authentication mechanism is different, the approach is the same. Vapor works behind the curtains to authenticate and provide the user.
  2. Using the same createToken(source:) on user , you generate a new token, this time passing SessionSource.login . You save the new token to the database.
  3. Once you save the token, you wrap the token’s value and the user in a NewSession and return it in the response.

You’re now ready to test the /users/login endpoint.

Build and run one last time, then open the API file and send the (3) Login request. You should see a response similar to what you got from the (1) Sign up request, but with a new token, fresh from the oven:

{
  "token": "5/2BdXtsAZaLBPOCKCDgow==",
  "user": {
    "username": "NatanTheChef",
    "id": "138191B9-445D-442D-9F70-B858081A661B",
    "updated_at": "2020-03-07T19:40:54Z",
    "created_at": "2020-03-07T19:40:54Z"
  }
}

Note : If you changed the username or password when you signed up, select the Authorization tab, then Basic Auth and insert the username and password you used to sign up.

Congratulations! You’re ready to implement user authentication for your app using both bearer tokens and basic authentication headers.

Where to Go From Here?

Download the final project using the Download Materials button at the top or bottom of the page. Here are a few challenges you could try to tackle going forward:

  • Make a logout function that invalidates, revokes or deletes a token.
  • Add methods allowing users to reset their password. One way to do this is by creating a ResetPasswordToken Model which has an expiration date, an identifier and is linked to a user. Then send this unique link via email.
  • Allow logging in with Magic Links sent via email.
  • If you want a challenge, try to implement authorization in DinnerController.swift , making sure that only a host can invite users to a Dinner , and that only the invitees and the host can fetch a dinner’s information.

If you work with Amazon Web Services, take a look at my other tutorial SMS user authentication with Vapor and AWS

We hope you enjoyed this tutorial. If you have any questions or comments, feel free to join in the forum discussion below!


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

智能优化算法及其应用

智能优化算法及其应用

王凌 / 清华大学出版社 / 2001-10 / 22.00元

智能优化算法及其应用,ISBN:9787302044994,作者:王凌著一起来看看 《智能优化算法及其应用》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具