The Ultimate Guide to handling JWTs on frontend clients (GraphQL)
转自: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 to JWT
- Login
- GraphQL client setup with JWT
- Logout
- Silent refresh
- Persisting sessions
- Force logout
- Server side rendering (SSR)
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: header, payload 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 }) | |
} |
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 }) |
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') | |
} | |
} |
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 |
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(), | |
}); |
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(), | |
}); |
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?
A 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') | |
} | |
} |
These are the 2 things we now need to do on logout:
- Nullify the token
- 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()) | |
} |
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:
- 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.
- 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:
- It can be used to make an API call (say,
/refresh_token
) to fetch a new JWT token before the previous JWT expires. - 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:
Magento 2 Theme Ultimate Guide - 如何创建Magento 2主题基础指南 在Magento 2中管理和设置主题的方式有很多改进.Magento 1.9中引入的theme ... By Iaroslav Kudritskiy (Source: https://rocketbots.io/blog/the-ultimate-guide-to-whatsapp-business-a ... Ultimate Guide to WeChat for Business 2019 By Iaroslav Kudritskiy (source :https://rocketbots.io/blo ... Ultimate Guide to Line For Business (May 2019) By Iaroslav Kudritskiy February 4, 2019 No Comments I ... Android Build System Ultimate Guide April 8,2013 Lately, Android Open Source Project has gone throug ... What is AngularJS? Angular is a client-side MVC/MVVM framework built in JavaScript, essential for mo ... [photographyconcentrate] 六级/考研单词: embark, thrill, excite, intimidate, accessory, comprehensive, timi ... 备忘:http://www.paintcodeapp.com/news/ultimate-guide-to-iphone-resolutionshttp://appicontemplate.com/ w http://blog.jobbole.com/25576/?utm_source=blog.jobbole.com&utm_medium=relatedPosts https://www ... 0. 引言 0x1:载荷是什么?在整个入侵过程中起到什么作用? 载荷的作用在整个入侵链路的作用起到纽带的作用,它借助于目标系统提供的某些功能:组件:执行环境,将攻击者的传递的恶意payload包裹起来 ... 我们在读取txt文件时,如果文件格式不是utf8,则获取的中文会乱码,所以要么另存文件为utf8格式,要么使用和文件相同的编码来读取. 如果文件为utf8,则: //一种 StreamReader s ... public void Configuration(IAppBuilder app) { ConfigureAuth(app); //指定使用Sql ... 特性 特性(Attribute)是用于在运行时传递程序中各种元素(比如类.方法.结构.枚举.组件等)的行为信息的声明性标签. 特性可以当成一个特殊的类看待 列举特性语法: [attribute(pos ... 一.SuperBench.sh VPS/服务器一键检测带宽.CPU.内存.负载.IO读写等的脚本: wget -qO- https://raw.githubusercontent.com/oooldk ... Outline spc 可以帮你实现: Linux Server 之间互传数据: Linux Server 和 Windows Server 之间互传数据: 参考: https://www.cnblo ... 不同于我们普通爬虫获取xpath,scrapy获得xpath对象获取他的值语法 一.xpath对象获取值 xpath对象..extract() 二.Scrapy框架独有的xpath取值方式 利用hre ... 最近在安装window server 2012 R2的时候,输入好密钥可以继续安装,但在后面又提示我“无人参与应答文件包含的产品密钥无效.删除无效的密钥或在无人参与应答文件中提供有效的产品密钥继续进行 ... 5.频率控制 5.1频率监控代码实现 import time VISIT_RECORD = {} class VisitThrottle(object): def __init__(self): #定 ... strong元素表示内容的重要性,而em则表示内容的着重点. 根据内容需要,这两个元素既可以单独使用,也可以一起使用. 例如: ... <body> <p><strong ...The Ultimate Guide to handling JWTs on frontend clients (GraphQL)的更多相关文章
随机推荐