转自: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:

最新文章

  1. 在不损坏C盘的情况下为C盘扩容,适用于Win
  2. Linux 第04天
  3. jQuery插件解析
  4. Python开源框架
  5. Codeforces 676C Vasya and String(尺取法)
  6. Java虚拟机内存管理机制
  7. grunt压缩多个js文件和css文件
  8. facelets标签
  9. Java求和
  10. C#调用WORD处理的实例代码(包含excel)
  11. FileOutputStream
  12. /bin/sh^M: bad interpreter:解决办法
  13. Docker for windows10 配置镜像加速
  14. html加载时事件触发顺序
  15. Warning: Path must be a string . Received null Use --force to continue
  16. 细谈最近上线的Vue2.0项目(一)
  17. dojo实现省份地市级联报错(二)
  18. SQL大全基本语法
  19. Day046--JavaScript-- DOM操作, js中的面向对象, 定时
  20. 按值传递 vs. 按指针传递

热门文章

  1. 使用位运算实现int32位 整数的加减乘除
  2. ML学习笔记之Anaconda中命令形式安装XGBoost(pip install)
  3. SQL Server返回DATETIME类型,年、月、日、时、分、秒、毫秒
  4. DotnetSpider爬虫简单示例 net core
  5. linuxmint安装Tools找不到Tools的压缩包问题
  6. MySQL之SQL演练(四)
  7. 2019 满帮java面试笔试题 (含面试题解析)
  8. 什么是B+树
  9. uni-app学习
  10. vue.js 实现粒子特效之插件( vue-particles )