For the front end, the login is to submit the user information, then the front end does not have to worry about the rest. However, I have done a login SDK project and found that the logic here is not so simple. Here are some of my understanding of landing to share with you

session & JWT

The HTTP protocol is stateless and cannot distinguish and manage requests and responses in terms of state. In other words, if a user is authenticated using the account and password, the user needs to be authenticated again on the next request. Because, according to the HTTP protocol, the server does not know which user made the request. To identify the current user, the server and client need to agree on an identity to represent the current user

session

Which is in order to identify the user’s request, need on the server information of a user login, the login information is stored in response to the client, the next time a request when the client will carry the login information request to the server, the server will be able to distinguish between the request which is sponsored by the user Below is the schematic diagram:sessionIn the scenario, the requesting server will carrysession_id, the server will pass the currentsession_idIf the current session is valid, subsequent requests can identify the current user.

If the current session is invalid or does not exist, the client needs to redirect to the login page or prompt that the session is not logged in.

const express = require('express');
const session = require('express-session')
const redis = require('redis')
const connect = require('connect-redis')
const bodyParser = require('body-parser')

const app = express();
app.use(bodyParser.json());

app.use(bodyParser.urlencoded({ extended: true }))

const RedisStore = connect(session);

const client = redis.createClient({
  host: '127.0.0.1'.port: 6397
})

app.use(session({
  store: new RedisStore({
    client,
  }),
  secret: 'sec_id'.resave: false.saveUninitialized: false.cookie: {
    secure: true.httpOnly: true.maxAge: 1000 * 60 * 10
  }
}))


app.get('/'.(req, res) = > {
  sec = req.session;
  if (sec.user) {
    res.json({
      user: sec.user
    })
  } else {
    res.redirect('/login')
  }
})


app.post('/login'.(req, res) = > {
  const {pwd, name } = req.body;
  // Let's write it simple for simplicity
  if (pwd === name) {
    req.session.user = req.body.name;
    res.json({
      message: 'success'})}})Copy the code

When the request/interface is made, it determines whether the current session exists. If so, the corresponding information is returned; If it does not exist, it is redirected to the /login page. Once this page is logged in successfully, the session will be set

The above code only considers the scenario of a single service, but there are often multiple services in the business with different service domain namescookieYou can’t cross domains, sosessionThere are problems with sharingFor example, in the above scenario, the user first requests the serviceAuth ServerAnd then generatesession. When the user requests the service againfeedback ServerWhen, as a result ofsessionIf not, service B cannot get the login status and needs to log in again.

The disadvantage of the session

Session is used to solve authentication problems. It has the following disadvantages:

  1. Multi-cluster support: When a website is deployed in a cluster, what can I do if multiple Web servers are deployedsessionSharing problems. becausesessionIs created by a single service, and the server that handles the request may not be createdsessionThe server will not be able to retrieve information such as login credentials that were previously put into the session
  2. Poor performance: During peak traffic periods, it is a burden on resources because user information for each request needs to be stored in the database
  3. Low scalability: When expanding the server,session storeIt also needs to be expanded. This takes up additional resources and adds complexity

JWT

In the session service, the server needs to maintain the user’s session object. Either there is a service in front of the server, or each service obtains session information from the storage tier. When the number of requests is large, I/O pressure is heavy.

Compared to thesessionService, which stores user information on the client every time it is requestedcookieorhttpThe header channel is sent to the server, making the server stateless and thus relieving the strain on the server.

Compared to the browser,Native AppSet up thecookieIt’s not that easy, so the server needs to adopt a different authentication mode. After login, the server generates one based on the login informationtokenValue that will be carried in subsequent requests by the clienttokenValue for login verification.

The JWT consists of three parts: header, payload, and signature. The header specifies the signature algorithm of the JWT

header = {
  alg: "HS256".type: "JWT"
}
Copy the code

HS256 indicates that hMAC-SHA256 is used to generate the intent that the body of the signature message contains JWT:

payload = {
  "loggedInAs": "admin"."iat": 1422779638
}
Copy the code

The unsigned token is composed of a base64URL-encoded header and a message body, and the signature is computed using a private key:

key = 'your_key'
unsignedToken = encodeBase64(header) + "." + encodeBase64(payload)
signature = HAMC-SHA256(key, unsignedToken)
Copy the code

Finally, the base64URL-encoded signature concatenated to the end of the unsigned token is JWT:

token = encodeBase64(header) + '. ' + encodeBase64(payload) + '. ' + encodeBase64(signature)
Copy the code

The specific implementation

First create app.js to get the request parameters, the listening port, and so on

// app.js
require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser');
const router = require('./router');
const app = express();

app.use(bodyParser.json())
app.use(cookieParser);

app.use(bodyParser.urlencoded({ extended: true }))

router(app);


app.listen(3001.() = > {
  console.log('server start')})Copy the code

Dotenv is used to configure environment variables and create. Env files.

ACCESS_TOKEN_SECRET=swsh23hjddnns
ACCESS_TOKEN_LIFE=1200000
Copy the code

Then the login interface is registered. This interface submits the user information to the server. The back-end will use the information to generate the corresponding token, which can be directly returned to the client or set cookies

// user.js
const jwt = require('jsonwebtoken')

function login(req, res) {
  const username = req.body.username;

  const payload = {
    username,
  }
  
  const accessToken = jwt.sign(payload, process.env.ACCESS_TOKEN_SECRET, {
    algorithm: "HS256".expiresIn: process.env.ACCESS_TOKEN_LIFE
  })

  res.cookie('jwt', accessToken, {
    secure: true.httpOnly: true,
  })
  res.send();
}
Copy the code

After a successful login, you can directly set the client’s cookie

In the next request, the server directly obtains the user’s JWT cookie and determines whether the current token is valid:

//middleware.js
const jwt = require('jsonwebtoken');

exports.verify = function(req, res, next) {
  const accessToken = req.cookies.jwt;

  try {
    jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET);
    next();
  } catch (error) {
    console.log(error);
    return res.status(401).send(); }}Copy the code

Compared to session, JWT has the following advantages:

  1. Good scalability: In distributed deployment scenarios, session requires data sharing, but JWT does not
  2. Stateless: There is no need to store any state on the server

JWT also has some disadvantages:

  1. Indelible: After it is issued, it remains valid until its expiration and cannot be revoked.
  2. Poor performance: In session schemes, the sessionId that cookies need to carry is a very short string. However, since JWT is stateless and needs to carry some necessary information, its volume will be relatively large.
  3. Security: Payload in JWT is base64 encoded and not encrypted, so sensitive data cannot be stored
  4. Renewal: The traditional cookie renewal scheme is built-in to the framework. The session validity period is 30 minutes. If there is access within 30 minutes, the validity period is refreshed to 30 minutes. If you want to change the duration of the JWT, you need to issue a new JWT. One option is to update JWT on every request, which is poor performance; The second option sets an expiration time for each JWT and refreshes the JWT expiration time for each access, thus losing the stateless benefits of JWT.

Application scenarios of session and JWT

Suitable for scenarios where JWT applies:

  • The validity of short
  • You only want to be used once

For example, when requesting service A, service A will issue A JWT with A short expiration time to the browser. The browser can request service B with the current JWT, and service B can verify the JWT to determine whether the current user has the right to operate. Single sign-on and session management are very unsuitable for JWT because of its inescapable nature.

Single Sign-on (SSO)

Sso typically deals with access and logins between different applications within a company. If an enterprise application has multiple service subsystems, you can switch between subsystems by logging in to one system without logging in to the system. Here’s an example: Subsystem A unified login to the Passport domain name, and planted A cookie under the Passport domain name, and then added the token to the URL, redirected to subsystem A, back to Subsystem A, and used the token to authenticate the Passport again. If the authentication returns the necessary information to generate the session of system A when the next request is made by system A, the current service already has A session, so it will not go to passport to verify the permission. When accessing system B, because system B does not have A session, it will redirect to the passport domain name, There are cookies under the passport domain name, so there is no need to log in. The token is directly added to the URL and redirected to subsystem B. The subsequent process is the same as that of SUBSYSTEM A

Realize the principle of

Take Tencent as an example. Tencent has a number of domain names, such as: In cd.qq.com and music.qq.com, we can set the cookie domian for qq.com to achieve cookie sharing. However, such as cd.QQ.com, tencent.com secondary domain names are inconsistent, so that all domain names can share a cookie. So you want a generic service to host this login service. For example, Tencent has such a domain name: passport.tencent.com for hosting specialized login services. At this time, cd.qq.com and tencent.com login and logout are implemented by SSO (passport.baidu.com)

The specific implementation

After SSO is successfully logged in, a token is generated and the login page is displayed. In this case, SSO is logged in, but the subsystem is not logged in. The subsystem needs to use the token to set the login state of the current subsystem and use the token to request the Passport service to obtain basic user information. The name of the passport is passport.com. The name of the passport is 3001. The name of the passport is 3002

Passport services

The passport has the following functions:

  1. Unified Login Service
  2. Obtaining user Information
  3. Verify currenttokenIs it valid

First implement some logic for the login page:

// passport.js
import express from 'express';
import session from 'express-session';
import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';
import connect from 'connect-redis';
import redis from '.. /redis';

const app = express();
app.use(bodyParser.json());

app.use(bodyParser.urlencoded({ extended: true }));
app.use(cookieParser());

app.set('view engine'.'ejs');
app.set('views'.`${__dirname}/views`);

const RedisStore = connect(session);


app.use(
  session({
    store: new RedisStore({
      client: redis,
    }),
    secret: 'token'.resave: false.saveUninitialized: false.cookie: {
      secure: true.httpOnly: true.maxAge: 1000 * 60 * 10,}})); app.get('/'.(req, res) = > {
  const { token } = req.cookies;
  if (token) {
    const { from } = req.query;
    const has_access = await redis.get(token);
    if (has_access && from) {
      return res.redirect(`https://The ${from}? token=${token}`);
    }
    // If not, boot to the login page and log in again
    return res.render('index', {
      query: req.query,
    });
  }
  return res.render('index', {
    query: req.query,
  });
})
app.port('/login'.(req, res) = > {
  const { name, pwd, from } = req.body;

  if (name === pwd) {
    const token = `The ${new Date().getTime()}_${ name}`;
    redis.set(token, name);
    res.cookie('token', token);
    if (from) {
      return res.redirect(`https://The ${from}? token=${token}`); }}else {
    console.log('Login failed'); }})Copy the code

The/interface first checks whether the passport already has a login token. If so, it checks whether the current token is valid in the storage. If it is valid and carries the from parameter, it jumps to the original page and brings the generated token value back to the original page.

The following ispassportPage style:The login interface needs to be set after successful loginpassportThe domain nametoken, and redirect to the previous page

Subsystem implementation

import express from 'express';
import axios from 'axios';
import session from 'express-session';
import bodyParser from 'body-parser';
import connect from 'connect-redis';
import cookieParser from 'cookie-parser';
import redisClient from ".. /redis";
import { argv } from 'yargs';
const app = express();

const RedisStore = connect(session);
app.use(bodyParser.json());

app.use(bodyParser.urlencoded({ extended: true }));
app.use(cookieParser('system'));

app.use(session({
  store: new RedisStore({
    client: redisClient,
  }),
  secret: 'system'.resave: false.name: 'system_id'.saveUninitialized: false.cookie: {
    httpOnly: true.maxAge: 1000 * 60 * 10
  }
}))


app.get('/'.async (req, res) => {
  const { token } = req.query;
  const { host } = req.headers;
  // If this site already has credentials, you don't need to go to passport
  if (req.session.user) {
    return res.send('user success')}// If you don't have the login information and have the token, you can go to the Passport for login authentication
  if(! token) {return res.redirect(`http://passport.com?from=${host}`)}const {data} = await axios.post('http://127.0.0.1:3000/check',{
    token,
  })
  // Validation succeeded
  if(data? .code ===0) {
    constuser = data? .user; req.session.user = user; }else {
    // Validation failed
    return res.redirect(`http://passport.com?from=${host}`)}return res.send('page has token')
})
app.listen(argv.port, () = > {
  console.log(argv.port);
})
Copy the code

If the current system session already exists, return user Success. If you are not logged in and the URL carries the token parameter, you need to jump to passport.com login.

If the token exists and the current subsystem is not logged in, you need to use the token on the current page to request the Passport service to determine whether the token is valid. If it is valid, you will return the corresponding information and set the session.

In this case, system A and system B are only listening on different interfaces, so add A variable to the boot parameter to get the boot port

Passport authentication service

app.get('/check'.(req, res) = > {
  const { token } = req.query;
  if(! token) {return res.json({
      code: 1})}const user = await redis.getAsync(token);
  if (user) {
    return res.json({
      code: 0,
      user,
    })
  } else {
    return res.redirect('passport.com')}})Copy the code

The check interface determines whether the token is valid. If the token is valid, the user information will be returned. If not, the user will be redirected to passport.com to log in again

OAuth

OAuth protocol is widely used in third-party login, which allows users to avoid the problem of login again.

Take Github authorization as an example to explain the OAuth authorization process:

  1. Access the serviceA, the serviceANo login, you can go throughgithubThird party login
  2. Click on thegithubTo switch to the authentication server. Then ask for authorization
  3. Once authorization is complete, it is redirected to A path for service A with parameterscode
  4. serviceAthroughcodeTo requestgithubTo obtain thetokenvalue
  5. throughtokenValue, and then requestgithubThe resource server gets the data you want

First of all togithub-authTo apply for aauthApplications such as the following:

The corresponding client_id and client_secret are obtained. The following is the specific authorization code (not to start the service) :

import { AuthorizationCode } from 'simple-oauth2';
const config = {
  client: {
    id: 'client_id'.secret: 'client_secret'
  },
  auth: {
    tokenHost: 'https://github.com'.tokenPath: '/login/oauth/access_token'.authorizePath: '/login/oauth/authorize'}}const client = new AuthorizationCode(config);
const authorizationUri = client.authorizeURL({
  redirect_uri: 'http://localhost:3000/callback'.scope: 'notifications'.state: '3 (# 0 /! ~ '
});

app.set('view engine'.'ejs');
app.set('views'.`${__dirname}/views`);

app.get('/auth'.(_, res) = > {
  res.redirect(authorizationUri)
})
Copy the code

When localhost:3000/auth is accessed, the service is automatically redirected to github where the authentication address is located

https://github.com/login/oauth/authorize?response_type=code&client_id=86f4138f17d0c3033ca4&redirect_uri=http%3A%2F%2Floc alhost%3A3000%2Fcallback&scope=notifications&state=3(%230%2F! ~Copy the code

When authorization is clicked, it is redirected to localhost:3000/callback with the parameter code on the URL. The following are the handlers on the server side

async function getUserInfo(token) {
  const res = await axios({
    method: 'GET'.url: 'https://api.github.com/user'.headers: {
      Authorization: `token ${token}`}})return res.data;
}

app.get('/callback'.async (req, res) => {
  const { code } = req.query;
  console.log(code);
  / / access token

  const options = {
    code,
  }

  try {
    const access = await client.getToken(options);
    const resp = await getUserInfo(access.token.access_token);
    return res.status(200).json({
      token: access.token,
      user: resp,
    });
  } catch (error) {
    
  }
})
Copy the code

According to theurlOn the parameterscodeAccess to thetokenAnd then based on thistokenTo requestgithub apiService to obtain user information, usually the website will complete a series of operations such as registration, adding session and so on according to the currently obtained user information. In the above code, the user request data is simply returned to the front end. Here is the format of the data returned to the front end:Finally, the third party login authorization is implemented

Reference documentation

medium.com/@siddhartha… Livecodestream. Dev/post/a – prac… Medium.com/myplanet-mu…