转自:https://blog.hasura.io/best-practices-of-using-jwt-with-graphql/ hasura 团队关于jwt 的实践

JWTs (JSON Web Token, pronounced 'jot') are becoming a popular way of handling auth. This post aims to demystify what a JWT is, discuss its pros/cons and cover best practices in implementing JWT on the client-side, keeping security in mind. Although, we’ve worked on the examples with a GraphQL clients, but the concepts apply to any frontend client.


Introduction: What is a JWT?

For a detailed, technical description of JWTs refer to this article.

For the purposes of auth, a JWT is a token that is issued by the server. The token has a JSON payload that contains information specific to the user. This token can be used by clients when talking to APIs (by sending it along as an HTTP header) so that the APIs can identify the user represented by the token, and take user specific action.

But can’t a client just create a random JSON payload an impersonate a user?

Good question! That’s why a JWT also contains a signature. This signature is created by the server that issued the token (let’s say your login endpoint) and any other server that receives this token can independently verify the signature to ensure that the JSON payload was not tampered with, and has information that was issued by a legitimate source.

But if I have a valid and signed JWT and someone steals it from the client, can’t they use my JWT forever?

Yes! If a JWT is stolen, then the thief can can keep using the JWT. An API that accepts JWTs does an independent verification without depending on the JWT source so the API server has no way of knowing if this was a stolen token! This is why JWTs have an expiry value. And these values are kept short. Common practice is to keep it around 15 minutes, so that any leaked JWTs will cease to be valid fairly quickly. But also, make sure that JWTs don’t get leaked.

These 2 facts result in almost all the peculiarities about handling JWTs! The fact that JWTs shouldn’t get stolen and that they need to have short expiry times in case they do get stolen.

That’s why it’s also really important not to store JWT on the client, say via cookies or localstorage. Doing so you make your app vulnerable to CSRF & XSS attacks, by malicious forms or scripts to use or steal your token lying around in cookies or localstorage.

So does a JWT have a specific kind of structure? What does it look like?

A JWT looks something like this, when it's serialized:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o

If you decode that base64, you'll get JSON in 3 important parts:  headerpayload and signature.

The 3 parts of a JWT (based on image taken from jwt.io)

The serialized form is in the following format:

[ base64UrlEncode(header) ] . [ base64UrlEncode(payload) ] . [signature ]

A JWT is not encrypted. It is based64 encoded and signed. So anyone can decode the token and use its data. A JWT's signature is used to verify that it is in fact from a legitimate source.

Here is the diagram of how a JWT is issued(/login) and then used to make an API call to another service( /api) in a nutshell:

A workflow of how a JWT is issued and then used

Ugh! This seems complicated. Why shouldn’t I stick to good old session tokens?

This is a painful discussion on the Internet. Our short (and opinionated answer) is that backend developers like using JWTs because a) microservices b) not needing a centralized token database.

In a microservices setup, each microservice can independently verify that a token received from a client is valid. The microservice can further decode the token and extract relevant information without needing to have access to a centralized token database.

This is why API developers like JWTs, and we (on the client-side) need to figure out how to use it. However, if you can get away with a session token issued by your favourite monolithic framework, you’re totally good to go and probably don’t need JWTs!


Basics: Login

Now that we have a basic understanding what a JWT is, let's create a simple login flow and extract the JWT. This is what we want to achieve:

A login flow for getting a JWT

So how do we start?

The login process doesn’t really change from what you'd usually do. For example, here’s a login form that submits a username/password to an auth endpoint and grabs the JWT token from the response. This could be login with an external provider, an OAuth or OAuth2 step. It really doesn't matter, as long as the client finally gets a JWT token in the response of the final login success step.

First, we'll build a simple login form to send the username and password to our login server. The server will issue JWT token and we will store it in memory. In this tutorial we won’t focus on auth server backend, but you're welcome to check it out in example repo for this blogpost.

This is what the handleSubmit handler for a login button might look like:

   
  async function handleSubmit () {
  //...
  // Make the login API call
  const response = await fetch(`/auth/login`, {
  method: 'POST',
  body: JSON.stringify({ username, password })
  })
  //...
  // Extract the JWT from the response
  const { jwt_token } = await response.json()
  //...
  // Do something the token in the login method
  await login({ jwt_token })
  }
view rawhandleSubmit.js hosted with ❤ by GitHub

The login API returns a token and then we pass this token to a login function from /utils/auth where we can decide what to do with the token once we have it.

   
  import { login } from '../utils/auth'
  await login({ jwt_token })
view rawauth.js hosted with ❤ by GitHub

So we’ve got the token, now where do we store this token?

We need to save our JWT token somewhere, so that we can forward it to our API as a header. You might be tempted to persist it in localstorage; don’t do it! This is prone to XSS attacks.

What about saving it in a cookie?

Creating cookies on the client to save the JWT will also be prone to XSS. If it can be read on the client from Javascript outside of your app - it can be stolen. You might think an HttpOnly cookie (created by the server instead of the client) will help, but cookies are vulnerable to CSRF attacks. It is important to note that HttpOnly and sensible CORS policies cannot prevent CSRF form-submit attacks and using cookies require a proper CSRF mitigation strategy.

Note that the new SameSite cookie spec which is getting increased support in most browsers will make Cookie based approaches safe from CSRF attacks. It might not be a solution if your Auth and API servers are hosted on different domains, but it should work really well otherwise!

Where do we save it then?

Well for now, we will store it in memory (and we’ll come to persisting sessions in the following section).

   
  let inMemoryToken;
   
  function login ({ jwt_token, jwt_token_expiry }, noRedirect) {
  inMemoryToken = {
  token: jwt_token,
  expiry: jwt_token_expiry
  };
  if (!noRedirect) {
  Router.push('/app')
  }
  }
view rawinmemory.js hosted with ❤ by GitHub

As you can see here we store token in memory. Yes, the token will be nullified when the user switches between tabs, but we will deal with that later. I will also explain why I have noRedirect flag as well as jwt_token_expiry

Ok! Now that we have the token what can we do with it?

  • Using in our API client to pass it as a header to every API call
  • Check if a user is logged in by seeing if the JWT variable is set.
  • Optionally, we can even decode the JWT on the client to access data in the payload. Let's say we need the user-id or the username on the client, which we can extract from the JWT.

How do we check if our user is logged in?

We check in our utils/auth if the token variable is set and if it isn't - redirect to login page.

   
  const jwt_token = inMemoryToken;
  if (!jwt_token) {
  Router.push('/login')
  }
  return jwt_token
view rawisloggedin.js hosted with ❤ by GitHub

Basics: Client setup

Now it’s time to set up our GraphQL client. The idea is to get the token from the variable we set, and if it’s there, we pass it to our GraphQL client.

Using the JWT in a GraphQL client

Assuming your GraphQL API accepts a JWT auth token as an Authorization header, all you need to do is setup your client to set an HTTP header by using the JWT token from the variable.

Here's what a setup with the Apollo GraphQL client using an ApolloLink middleware.

   
  let appJWTToken
  const httpLink = new HttpLink({uri: 'https://graphql-jwt-tutorial.herokuapp.com/v1/graphql'})
  const authMiddleware = new ApolloLink((operation, forward)=> {
  if (appJWTToken) {
  operation.setContext({
  headers: {
  Authorization: `Bearer ${appJWTToken}`
  }
  });
  }
  return forward(operation);
  })
   
  const apolloClient = new ApolloClient({
  link: concat(authMiddleware, httpLink),
  cache: new InMemoryCache(),
  });
view rawapollomiddleware.js hosted with ❤ by GitHub

As you can see from the code, whenever there is a token, it’s passed as a header to every request.

But what will happen if there is no token?

It depends on the flow in your application. Let's say you redirect the user back to the login page:

else {
Router.push('/login')
}

What happens if a token expires as we're using it?

Let's say our token is only valid for 15 minutes. In this case we'll probably get an  error from our API denying our request (let's say a 401: Unauthorized error). Remember that every service that knows how to use a JWT can independently verify it and check whether it has expired or not.

Let’s add error handling to our app to handle this case. We'll write code that will run for every API response and check the error. When we receive the token expired/invalid error from our API, we trigger the logout or the redirect to login workflow.

Here's what the code looks like if we're using the Apollo client:

   
  import { onError } from 'apollo-link-error';
   
  const logoutLink = onError(({ networkError }) => {
  if (networkError.statusCode === 401) logout();
  })
   
  const apolloClient = new ApolloClient({
  link: logoutLink.concat(concat(authMiddleware, httpLink)),
  cache: new InMemoryCache(),
  });
view rawonError.js hosted with ❤ by GitHub

You may notice that this will result in a fairly sucky user experience. The user will keep getting asked to re-authenticate every time the token expires. This is why apps implement a silent refresh workflow that keeps refreshing the JWT token in the background. More on this in the next sections below!


Basics: Logout

With JWTs, a "logout" is simply deleting the token on the client side so that it can't be used for subsequent API calls.

So...is there no /logout API call at all?

logout endpoint is not really required, because any microservice that accepts your JWTs will keep accepting it. If your auth server deletes the JWT, it won't matter because the other services will keep accepting it anyway (since the whole point of JWTs was to not require centralised coordination).

The token is still valid and can be used. What if I need to ensure that the token cannot be used ever again?

This is why keeping JWT expiry values to a small value is important. And this is why ensuring that your JWTs don't get stolen is even more important. The token is valid (even after you delete it on the client), but only for short period to reduce the probability of it being used maliciously.

In addition, you can add a blacklisting workflow to your JWTs. In this case, you can have a /logout API call and your auth server puts the tokens in a "invalid list". However, all the API services that consume the JWT now need to add an additional step to their JWT verification to check with the centralised "blacklist". This introduces central state again, and brings us back to what we had before using JWTs at all.

Doesn’t blacklisting negate the benefit of JWT not needing any central storage?

In a way it does. It’s an optional precaution that you can take if you are worried that your token can get stolen and misused, but it also increases the amount of verification that has to be done. As you can imagine, this had led to much gnashing of teeth on the internet.

What will happen if I am logged in on different tabs?

One way of solving this is by introducing a global event listener on localstorage. Whenever we update this logout key in localstorage on one tab, the listener will fire on the other tabs and trigger a "logout" too and redirect users to the login screen.

  window.addEventListener('storage', this.syncLogout)
   
  //....
   
   
  syncLogout (event) {
  if (event.key === 'logout') {
  console.log('logged out from storage!')
  Router.push('/login')
  }
  }
view rawlogout.js hosted with ❤ by GitHub

These are the 2 things we now need to do on logout:

  1. Nullify the token
  2. Set logout item in local storage
   
  async function logout () {
  inMemoryToken = null;
  const url = 'http://localhost:3010/auth/logout'
  const response = await fetch(url, {
  method: 'POST',
  credentials: 'include',
  })
  // to support logging out from all windows
  window.localStorage.setItem('logout', Date.now())
  }
view rawlogout2.js hosted with ❤ by GitHub

In that case whenever you log out from one tab, event listener will fire in all other tabs and redirect them to login screen.

This works across tabs. But how do I "force logout" of all sessions on different devices?!

We cover this topic in a little more detail in a section later on: Force logout.


Silent refresh

There are 2 major problems that users of our JWT based app will still face:

  1. Given our short expiry times on the JWTs, the user will be logged out every 15 minutes. This would be a fairly terrible experience. Ideally, we'd probably want our user to be logged in for a long time.
  2. If a user closes their app and opens it again, they'll need to login again. Their session is not persisted because we're not saving the JWT token on the client anywhere.

To solve this problem, most JWT providers, provide a refresh token. A refresh token has 2 properties:

  1. It can be used to make an API call (say, /refresh_token) to fetch a new JWT token before the previous JWT expires.
  2. It can be safely persisted across sessions on the client!

How does a refresh token work?

This token is issued as part of authentication process along with the JWT. The auth server should saves this refresh token and associates it to a particular user in its own database, so that it can handle the renewing JWT logic.

On the client, before the previous JWT token expires, we wire up our app to make a /refresh_token endpoint and grab a new JWT.

How is a refresh token safely persisted on the client?!

The refresh token is sent by the auth server to the client as an HttpOnly cookie and is automatically sent by the browser in a /refresh_token API call.

Because client side Javascript can't read or steal an HttpOnly cookie, this is a little better at mitigating XSS than persisting it as a normal cookie or in localstorage.

This is safe from CSRF attacks, because even though a form submit attack can make a /refresh_token API call, the attacker cannot get the new JWT token value that is returned.

To recap, this is how we're thinking about what would be the best way of persisting a JWT based session:

The Ultimate Guide to handling JWTs on frontend clients (GraphQL)的更多相关文章

  1. Magento 2 Theme Ultimate Guide - 如何创建Magento 2主题终极指南

    Magento 2 Theme Ultimate Guide - 如何创建Magento 2主题基础指南 在Magento 2中管理和设置主题的方式有很多改进.Magento 1.9中引入的theme ...

  2. Ultimate Guide to WhatsApp for Business 2019

    By Iaroslav Kudritskiy (Source: https://rocketbots.io/blog/the-ultimate-guide-to-whatsapp-business-a ...

  3. Ultimate Guide to WeChat for Business 2019

    Ultimate Guide to WeChat for Business 2019 By Iaroslav Kudritskiy (source :https://rocketbots.io/blo ...

  4. Ultimate Guide to Line For Business (May 2019)

    Ultimate Guide to Line For Business (May 2019) By Iaroslav Kudritskiy February 4, 2019 No Comments I ...

  5. Android Build System Ultimate Guide

    Android Build System Ultimate Guide April 8,2013 Lately, Android Open Source Project has gone throug ...

  6. Ultimate guide to learning AngularJS in one day

    What is AngularJS? Angular is a client-side MVC/MVVM framework built in JavaScript, essential for mo ...

  7. The Ultimate Guide to Buying A New Camera

    [photographyconcentrate] 六级/考研单词: embark, thrill, excite, intimidate, accessory, comprehensive, timi ...

  8. The Ultimate Guide To iPhone Resolutions

    备忘:http://www.paintcodeapp.com/news/ultimate-guide-to-iphone-resolutionshttp://appicontemplate.com/

  9. The Ultimate Guide To A/B Testing

    w http://blog.jobbole.com/25576/?utm_source=blog.jobbole.com&utm_medium=relatedPosts https://www ...

随机推荐

  1. prometheus搜索指标显示No datapoints found.

    在指标能够在下拉框可以选择到的情况下,还有No datapoints found. 则考虑是时区的问题,详见官方issue https://github.com/prometheus/promethe ...

  2. SpringBoot构建RESTful API

    1.RESTful介绍 RESTful是一种软件架构风格! RESTful架构风格规定,数据的元操作,即CRUD(create, read, update和delete,即数据的增删查改)操作,分别对 ...

  3. English--不完全及物动词与授予动词

    English|不完全及物动词与授予动词 动词在整个语法中都是占据着十分重要的角色.在五大句型中将动词分为五种:系动词.不及物动词.及物动词.不完全及物动词.授予动词.这篇主讲不完全及物动词和授予动词 ...

  4. 单词cymophanite猫眼石cymophanite英语

    金绿石的猫眼石(Cymophanite)是所谓正宗的猫眼石,非常罕有,尤其是5卡以上而质优的,其售价可以高达七万多港元一卡. 相传这类猫眼石是宝石学家的宝石,从此可知其地位在珠宝玉石之中的重要性.颜色 ...

  5. React 性能优化之组件动态加载(react-loadable)

    React 项目打包时,如果不进行异步组件的处理,那么所有页面所需要的 js 都在同一文件中(bundle.js),整个js文件很大,从而导致首屏加载时间过长. 所有,可以对组件进行异步加载处理,通常 ...

  6. Solr基础理论【排名检索、查准率、查全率】

    一.排名检索 搜索引擎代表了基于查询,返回优先文档的一种方法.在关系型数据库的SQL查询中,表的一行要么匹配一个查询,要么不匹配,查询结果基于一列或多列排序.搜索引擎根据文档与查询匹配的程度为文档打分 ...

  7. MySQL安装和使用

    1.MySQL安装: 1).到MySQL官网https://dev.mysql.com/downloads/installer/,下载MySQL 5.7版本:(注:现在官网上最新版本已经变成8.0.1 ...

  8. xshell链接ubuntu16

    用xshell 链接 ubuntu16 失败 ,是因为没有装 ssh 服务 sudo apt-get install openssh-server         //安装ssh服务 ps -ef | ...

  9. Prometheus(五):Prometheus+Alertmanager 配置企业微信报警

    此处默认已安装Prometheus服务,服务地址:192.168.56.200  一.设置企业微信 1.1.企业微信注册(已有企业微信账号请跳过) 企业微信注册地址:https://work.weix ...

  10. Linux运维技术之LVM(逻辑卷管理)

    DM :逻辑设备(模块)--->LVM 优点: 个分区) 2.1.partprobe  /dev/sdb : 使用partprobe可以不用重启系统即可配合fdisk工具创建新的分区 2.2.c ...