Implementing User Login and JWT Token Management(With Refresh Tokens) in React with TypeScript and ASP.NET Web API

Madhawa Polkotuwa
4 min readJun 11, 2024

Online TicTacToe Game Development Series! Microsoft SignalR and React with Typescript (EP04)

In this tutorial, we’ll walk through implementing user login and token management in a React application using TypeScript and an ASP.NET Web API backend. We’ll cover creating interfaces, managing state with a global context and reducer, handling token refresh and expiration, and validating tokens on the server side.

Table of Contents

  1. Setting Up Interfaces
  2. Global Context and Reducer
  3. Token Management Functions
  4. Backend Token Validation
  5. Demo and Testing
  6. Conclusion
  7. What’s Next

Setting Up Interfaces

UserState Interface

First, we’ll define the user state interface to manage the login status and username.

export interface UserState {
isLogin: boolean;
username: string;
accessToken: string;
refreshToken: string;
}

Action Interface

Next, we’ll define the action interface for handling state changes.

export interface Action {
type: string;
payload: any;
}

Global Context and Reducer

Initial User State

We’ll create an initial state for our user.

const initialUserState: UserState = {
isLogin: false,
username: '',
accessToken: '',
refreshToken: ''
};

Creating the Global Context

We need a context to provide global state across our application.

export const GlobalContext = createContext<{
userState: UserState;
userDispatch: React.Dispatch<Action>;
}>({
userState: initialUserState,
userDispatch: () => undefined
});

Reducer Function

The reducer function handles state updates based on actions.

export const userControlReducer = (state: UserState, action: Action): UserState => {
switch (action.type) {
case 'LOGIN':
return { ...state, isLogin: true, username: action.payload.username, accessToken: action.payload.accessToken, refreshToken: action.payload.refreshToken };
case 'LOGOUT':
return { ...state, isLogin: false, username: '', accessToken: '', refreshToken: '' };
case 'REFRESH_TOKEN':
return { ...state, accessToken: action.payload.accessToken, refreshToken: action.payload.refreshToken };
default:
return state;
}
};

Global State Provider

The GlobalState component provides the context to its children

interface GlobalStateProps {
children: ReactNode;
}
const GlobalState: React.FC<GlobalStateProps> = ({ children }) => {
const [userState, userDispatch] = useReducer(userControlReducer, initialUserState);

return (
<GlobalContext.Provider value={{ userState, userDispatch }}>
{children}
</GlobalContext.Provider>
);
};

Token Management Functions

Checking Token Expiration

We need a function to check if the token has expired and refresh it if necessary.

const checkTokenExpiration = () => {
if (userState.accessToken) {
const refreshInterval = setInterval(() => {
const jwtToken = localStorage.getItem('jwtToken');
if (jwtToken) {
const decoded = decodeJwt(jwtToken);
if (decoded.exp * 1000 > Date.now()) {
fetchRefreshToken();
} else {
userDispatch({ type: 'LOGOUT', payload: null });
clearInterval(refreshInterval);
}
}
}, 9000);
}
};

Fetching Refresh Token

A function to fetch a new token from the server.

const fetchRefreshToken = async () => {
const jwtToken = localStorage.getItem('jwtToken');
const refreshToken = localStorage.getItem('refreshToken');
try {
const result = await fetch(process.env.REACT_APP_API_URL + "api/User/refresh", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ accessToken: jwtToken, refreshToken: refreshToken })
}).then(res => res.json());
if (result.accessToken) {
localStorage.setItem('jwtToken', result.accessToken);
localStorage.setItem('refreshToken', result.refreshToken);
userDispatch({ type: 'REFRESH_TOKEN', payload: { accessToken: result.accessToken, refreshToken: result.refreshToken } });
}
} catch (error) {
console.log(error);
}
};

if (result.accessToken) {
localStorage.setItem('jwtToken', result.accessToken);
localStorage.setItem('refreshToken', result.refreshToken);
userDispatch({ type: 'REFRESH_TOKEN', payload: { accessToken: result.accessToken, refreshToken: result.refreshToken } });
}
} catch (error) {
console.log(error);
}
};

Decode JWT

A helper function to decode JWT tokens.

const decodeJwt = (token: string) => {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace('-', '+').replace('_', '/');
return JSON.parse(atob(base64));
};

Backend Token Validation

Refresh Endpoint

In the ASP.NET Web API, we need to implement an endpoint to handle token refresh.

[HttpPost("refresh")]
public async Task<IActionResult> Refresh([FromBody] TokenDto tokenDto) {

if (tokenDto == null) return BadRequest(new { message = "Invalid Client Request" });

var principal = GetPrincipalFromExpiredToken(tokenDto.AccessToken);
var username = principal.Identity.Name;
var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Username == username);

if (user == null || user.RefreshToken != tokenDto.RefreshToken || user.RefreshTokenExpiryTime <= DateTime.Now)
return BadRequest("Invalid Request");
var newAccessToken = CreateJwt(user);
var newRefreshToken = CreateRefreshToken();
user.Token = newAccessToken;
user.RefreshToken = newRefreshToken;
user.RefreshTokenExpiryTime = DateTime.Now.AddDays(5);

await _dbContext.SaveChangesAsync();

return Ok(new TokenDto { AccessToken = newAccessToken, RefreshToken = newRefreshToken });
}

Validate Expired Token

A private method to validate the expired token and extract the principal.

private ClaimsPrincipal GetPrincipalFromExpiredToken(string token) {
var tokenValidationParameters = new TokenValidationParameters {
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("veryverysecretkey")),
ValidateAudience = false,
ValidateIssuer = false,
ValidateLifetime = false,
ClockSkew = TimeSpan.Zero
};
var tokenHandler = new JwtSecurityTokenHandler();
var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out SecurityToken securityToken);
var jwtSecurityToken = securityToken as JwtSecurityToken;
if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCulture))
throw new SecurityTokenException("Invalid token");
return principal;
}

Demo and Testing

  1. Login: Attempt to login and verify that the token is correctly stored and refreshed.
  2. JWT Decoding: Use a JWT decoding tool to check the token expiration time.
  3. Logout: Verify that logging out clears the tokens from local storage.
  4. Adjust Intervals: Ensure the refresh interval and token expiration times are set appropriately.

Conclusion

In this tutorial, we’ve covered:

  • Setting up user state and action interfaces.
  • Managing global state with context and reducer.
  • Implementing token refresh and expiration checks.
  • Validating tokens in an ASP.NET Web API.

What’s Next

In the next video, I will explain how to create a Tic Tac Toe game user interface with React and TypeScript. Additionally, in upcoming videos of this series, I will cover handling online users with a database using Microsoft SignalR.

Thank you for following along! If you have any questions or feedback, feel free to leave a comment.

--

--