Implementing User Login and JWT Token Management(With Refresh Tokens) in React with TypeScript and ASP.NET Web API
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
- Setting Up Interfaces
- Global Context and Reducer
- Token Management Functions
- Backend Token Validation
- Demo and Testing
- Conclusion
- 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
- Login: Attempt to login and verify that the token is correctly stored and refreshed.
- JWT Decoding: Use a JWT decoding tool to check the token expiration time.
- Logout: Verify that logging out clears the tokens from local storage.
- 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.