Avoid XSS and CSRF Attacks in JWT (React + Golang): A Tutorial | HackerNoon (2024)

Cross-site scripting(XSS) and Cross-Site Request Forgery(CSRF) are likely to occur if a JSON Web Token(JWT) is not properly stored in the browser.

In this article, I will share how we can avoid those 2 attacks when using JWT in our web application.

I value your time, so I will start off with how to accomplish this in a summary.

  1. The user fills up the login form and hit the submit button from the Frontend.

  2. Once the user is authenticated from the Backend, a JWT access_token will be sent and a refresh_token will be set in an HTTP-Only cookie.

  3. Frontend stores the access_token in-memory

    var access_token = data.access_token; 
  4. When the user refreshes the page, the access_token stored in memory will be gone. But we can still retrieve the access_token by making an API call via the refresh_token stored in HTTP-Only cookie (Step 2). Then FE will have the access_token again, which is used to communicate with Backend.

I will walk you through the tutorial from the Backend and then the Frontend. I hope this will be easy to for you to understand the article.

Backend

I will use echo as my HTTP routing framework in this entire tutorial.

1. Create a routing framework

Start your backend router with AllowCredentials: true which sets Access-Control-Allow-Credentials in the response header. This configuration tells browsers whether to expose all responses to the frontend JavaScript code when the request's credentials mode (Request.credentials) is include.

e := echo.New()e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ AllowCredentials: true,}))

2. Create Login API

In this Login API, you should perform authentication based on the credentials sent by the user before you proceed.

Once the user is authenticated, the Backend should generate an access_token and a refresh_token to send back to the Frontend in the response body, along with an HTTP-Only Cookie.

type User struct { Name string `json:"name"`}func Login(c echo.Context) error { u := new(User) if err := c.Bind(u); err !=nil { log.Errorf("error in bind: %s", err) return err } accessToken, exp, err := generateAccessToken(u) if err != nil { return err } refreshToken, exp, err := generateRefreshToken(u) if err != nil { return err } setTokenCookie(refreshTokenCookieName, refreshToken, exp, c) return c.JSON(http.StatusOK, echo.Map{ "message": "login successful", "access_token": accessToken, })}

3. Create Refresh API

When the user refreshes the page in the browser or the token in the Frontend has expired, the Frontend can call this API to retrieve a new set of access_token and refresh_token.

func RefreshToken(c echo.Context) error { cookie, err := c.Cookie(refreshTokenCookieName) if err != nil { if err == http.ErrNoCookie { return c.NoContent(http.StatusNoContent) } log.Errorf("err is : %s", err) return err } token, err := jwt.ParseWithClaims(cookie.Value, &jwtCustomClaims{}, func(token *jwt.Token) (interface{}, error) { return []byte(GetRefreshJWTSecret()), nil }) if err != nil { log.Errorf("err is : %s", err) return err } if !token.Valid { return errors.New("error") } claims := token.Claims.(*jwtCustomClaims) log.Infof("claims.Name : %+v", claims.Name) u := &User{Name: claims.Name} accessToken, exp, err := generateAccessToken(u) if err != nil { return err } refreshToken, exp, err := generateRefreshToken(u) if err != nil { return err } setTokenCookie(refreshTokenCookieName, refreshToken, exp, c) return c.JSON(http.StatusOK, echo.Map{ "access_token": accessToken, })}

4. Create Get User API

The frontend can call this API to get the user information based on the access_token provided by the Login API or Refresh API.

func UserAPI(ctx echo.Context) error { accessToken := ctx.Request().Header.Get("Authorization") token, err := jwt.ParseWithClaims(accessToken, &jwtCustomClaims{}, func(token *jwt.Token) (interface{}, error) { return []byte(jwtSecretKey), nil }) if err != nil { log.Errorf("err is : %s", err) return err } claims := token.Claims.(*jwtCustomClaims) log.Infof("claims.Name : %+v", claims.Name) if !token.Valid { return ctx.JSON(http.StatusUnauthorized, echo.Map{ "message": "Unauthorized", }) } return ctx.JSON(http.StatusOK, echo.Map{ "name": claims.Name, })}

The above-mentioned APIs (Login, Refresh and Get User API) are the only APIs required for Backend.

The Frontend components will be introduced in the following parts.

Frontend

1. Refresh and Get User API

During page load, we will make an API call to the Backend refresh API with credentials: 'include'. The HTTP-Only cookie will be sent to the Backend from the browser. If a valid refresh_token exists in the HTTP-Only cookie, the Backend will respond with a new access_token to the Frontend, which is used to make an API call to retrieve the user information from the Backend.

// Use this token as Authorization token to communicate with the other API. const [token ,setToken] = React.useState<string>(''); // Retrieve the user information from the AccessToken. const [username ,setUsername] = React.useState<string>(''); useEffect(() => { async function refresh() { // You can await here const resp = await fetch('http://localhost:8080/refresh', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', }); console.log("status is " + resp.status); if (resp.status === 204) { console.log("exit") return } const d = await resp.json(); if(resp.ok) { console.log('Login Success:', d); setToken(d.access_token); user(d.access_token); } } refresh(); }, []); const user = async (token: string) => { const resp = await fetch('http://localhost:8080/user', { method: 'GET', headers: { 'Content-Type': 'application/json', 'Authorization': `${token}`, }, credentials: 'include', }); const d = await resp.json(); if(resp.ok) { console.log('User Success:', d); setUsername(d.name); } }

2. OnFinish function

This function will be triggered once the user submits the login information from the browser. If the user credential is valid, the Backend will respond with a new access_token and refresh_token in the response body and HTTP-Only cookie respectively.

 const onFinish = async (values: any) => { const resp = await fetch('http://localhost:8080/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({'name': values.username}), credentials: 'include', }); const {data, errors} = await resp.json(); if (resp.ok) { setToken(data.access_token); setUsername(values.username); } else { console.error(errors); } };

3. Render function

This UI contains only one Username input. When the user hits the submit button, onFinish will be triggered with a parameter of the value in the input.

return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Welcome { username }. </p> <Form name="basic" labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} initialValues={{ remember: true }} onFinish={onFinish} autoComplete="off" > <Form.Item label="Username" name="username" rules={[{ required: true, message: 'Please input your username!' }]} > <Input /> </Form.Item> <Form.Item wrapperCol={{ offset: 8, span: 16 }}> <Button type="primary" htmlType="submit"> Login </Button> </Form.Item> </Form> </header> </div> );

Conclusion

As you can see, to avoid the security issues mentioned in the title, the safest way is to store the JWT token in memory. There is nothing to be worried about for the refresh_token in the HTTP-Only cookie, because the attacker can’t use the refresh_token to perform any action from their website.

You can download the full source code here.

That‘s all for my sharing, I hope you will like it.

Avoid XSS and CSRF Attacks in JWT (React + Golang): A Tutorial | HackerNoon (2024)

FAQs

Can JWT prevent CSRF? ›

JWTs by themselves do not prevent CSRF attacks. Here's why: - JWTs may be sent automatically by the browser if authentication cookies or local storage tokens are set. An attacker can leverage this to send the JWT without the user knowing.

Can CSRF tokens prevent XSS attacks? ›

You can't use CSRF tokens to prevent stored XSS threats.

What is a way you can prevent CSRF attacks? ›

It's easier for an attacker to launch a CSRF attack when they know which parameters and value combinations are used in a form. Therefore, adding an additional parameter with a value that is unknown to the attacker and that may be validated by the server, will help prevent CSRF attacks.

Is JWT vulnerable to XSS? ›

On the other hand, session storage provides a more persistent storage solution for JWTs, as the data is stored within the user's session. However, session storage is still vulnerable to XSS attacks. An attacker can steal JWTs and gain unauthorized access to the application.

How do I keep my JWT token safe? ›

To keep them secure, you should always store JWTs inside an HttpOnly cookie. This is a special kind of cookie that's only sent in HTTP requests to the server. It's never accessible (both for reading and writing) from JavaScript running in the browser.

Can we bypass CSRF token? ›

This method is quite secure; however, if the application contains any other vulnerabilities (such as XSS, CRLF Injection, etc.) that allow attackers to set a cookie on the victim's browser, the CSRF protection can be bypassed by setting a new token in the cookie and using that token in the CSRF POC.

How can XSS attacks be prevented? ›

To safeguard your web applications, take proactive measures like routine vulnerability scanning, HTTP-Only cookies, escaping output, and validating user input. XSS attacks happen in a number of ways, and implementing variable validation, output encoding, and HTML sanitization can help enhance security.

What is the strongest defense against CSRF attacks? ›

The most robust way to defend against CSRF attacks is to include a CSRF token within relevant requests. The token must meet the following criteria: Unpredictable with high entropy, as for session tokens in general. Tied to the user's session.

Which headers can help prevent XSS and CSRF attacks? ›

To prevent XSS in HTTP responses that aren't intended to contain any HTML or JavaScript, you can use the Content-Type and X-Content-Type-Options headers to ensure that browsers interpret the responses in the way you intend.

Can I disable CSRF? ›

To enable or disable CSRF protection in Spring Security, you can configure it in your application's security configuration class.

How do you solve CSRF problems? ›

Approaches to fix the “CSRF token mismatch error”
  1. Check if the CSRF tokens are actually mismatched.
  2. Make sure CSRF tokens are generated and being passed correctly.
  3. Check if the session and CSRF token has expired. ...
  4. Check for any javascript errors in the console.
  5. Consider using double submit cookies as an additional check.
Jan 11, 2024

What are the safe methods for CSRF? ›

  • Use Built-In Or Existing CSRF Implementations for CSRF Protection.
  • Synchronizer Token Pattern. Transmissing CSRF Tokens in Synchronized Patterns.
  • ALTERNATIVE: Using A Double-Submit Cookie Pattern. Signed Double-Submit Cookie (RECOMMENDED) Employing HMAC CSRF Tokens. ...
  • Naive Double-Submit Cookie Pattern (DISCOURAGED)

How does JWT prevent CSRF? ›

JWT are a more secure and scalable alternative to CSRF tokens that can be used to authenticate and authorize users in API-centric applications. Unlike CSRF tokens, JWT are self-contained and encoded, which means they can be easily verified and decoded by the server without the need for a server-side session.

Does react prevent XSS attacks? ›

Escaping Strings: React automatically escapes strings inserted into the HTML to prevent XSS attacks. Any user input rendered as text onto the page is treated as a string, not as HTML or JavaScript code.

What is the safest JWT algorithm? ›

When signing is considered, elliptic curve-based algorithms are considered more secure. The option with the best security and performance is EdDSA, though ES256 (The Elliptic Curve Digital Signature Algorithm (ECDSA) using P-256 and SHA-256) is also a good choice.

Does JSON prevent CSRF? ›

The application/json MIME type is typically sent using AJAX, which is prevented from being sent in cross-site requests by the Same-Origin Policy (SOP). Thus, to perform CSRF against a JSON endpoint, we need to either use a different MIME type, exploit a weak CORS policy, or find another means of submitting the request.

How does JWT prevent tampering? ›

JWT signature

This mechanism provides a way for servers to verify that none of the data within the token has been tampered with since it was issued: As the signature is directly derived from the rest of the token, changing a single byte of the header or payload results in a mismatched signature.

What is difference between CSRF and JWT? ›

Conclusion. To conclude, JWT and CSRF tokens serve critical yet different roles in securing web applications. JWTs facilitate secure information exchange for authentication and authorization, while CSRF tokens protect against unwanted actions on behalf of authenticated users.

Can CORS prevent CSRF? ›

CSRF (Cross-Site Request Forgery) is a web attack that exploits loopholes in the SOP policy, and CORS does not block them. The attack consists of the attacker running malicious scripts in the victim's browser, and thus the victim performs unintended actions on his behalf.

Top Articles
Latest Posts
Article information

Author: Virgilio Hermann JD

Last Updated:

Views: 6193

Rating: 4 / 5 (61 voted)

Reviews: 84% of readers found this page helpful

Author information

Name: Virgilio Hermann JD

Birthday: 1997-12-21

Address: 6946 Schoen Cove, Sipesshire, MO 55944

Phone: +3763365785260

Job: Accounting Engineer

Hobby: Web surfing, Rafting, Dowsing, Stand-up comedy, Ghost hunting, Swimming, Amateur radio

Introduction: My name is Virgilio Hermann JD, I am a fine, gifted, beautiful, encouraging, kind, talented, zealous person who loves writing and wants to share my knowledge and understanding with you.