JWT Lesson
Notebook Link wget ___
Backend Repository Link git clone
Why do you need JWT
Certain features in your application need to be restricted or require some sort of authentication mechanism. For example, a user information database should only be accessed by administrators as it can contain sensitive information. Certain actions also need to be restricted such as deleting, updating, and creating new records. Additionally, different actions may be attributed to different roles.
What is JWT (AKA “JOT”)
★ JWT stands for **_JSON Web Token_**.
JWT allows information, in our case authentication roles, to be securely shared between an applications frontend and backend server as a JSON object .
Compact = Because of its size, it can be sent through an URL, POST parameter, or inside an HTTP header. Additionally, due to its size its transmission is fast. [will discuss payload later]
Self-contained = The payload contains all the required information about the user, to avoid querying the database more than once.
What is a JWT Token
JWT is represented as JSON objects, these objects contain information about the user. JWT are supposed to be compact (should be easy to send between 2 parties). This is useful in the context of web development. When data needs to be moved efficiently.
[by looking in the JwtTokenUtil.java]
JWT mainly has 3 different parts:
- Header
- usually contains the type of token and signing algo being used
- determined by ‘SignitureAlgorithm’ which is used for signing the key it is using HMAC SHA algorithm and the ‘getSecertKey’ method to provide key for signing
//header example
{
"alg": "HS256",
"typ": "JWT"
}
- Payload
- contains claims (statements about user) and other data, 3 types of claims: **___register__, ___public__, ___private__.** It is good to have predetermined claims: iss (issuer), exp (expiration time), sub (subject), aud (audience), ect. Here are some more examples of predetermined claims.
- payload is made from the ‘doGenerateToken’ method here claims like orles are added ‘roles’ info is taken from ‘GrantedAuthority’ objects from ‘UserDetails
//payload example
{
//subject
"sub": "1234567890",
"name": "Tanisha Patel",
"admin": true
}
- Signature
- to create signature part head and payload are combined and secret is used to sign it. Signature is used to verify sender of JWT to ensure message wasn’t changed along the way
- the signature is made from the **‘__doGererateToken__’** method where the JWT is signed with **‘__SecretKey__’** from the ‘getSecretKey’ method
JWT are commonly used in authentication, when the user logs in and gets a JWT, sent to the request to authenticate said user. Servers can verify a token’s authenticity by checking the signature, if valid then the server can trust the information in the token.
//example using the HMAC SHA 256 algorithm
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
Web Tokens
When a user logs in a **_Json _** is returned. Tokens are basically credentials so they need to be protected
★ Should not keep tokens longer than required
★ Should not store data in local storage
Whenever the user wants to access a protected route or resource, the user agent should send the JWT, typically in the Authorization header using the Bearer schema. The content of the header should look like the following:
Authorization: Bearer <token>
★ if you send JWT tokens through HTTP headers, you should try to prevent them from getting too big. (some servers don’t accept more than 8KB in headers)
Alternative solution » Auth0 Fine-Grained Authorization.
Instead of adding all the details into a large JWT you can keep the tokens smaller and use Auth0 to check more detailed rules and premissions. Instead of having all the acess information in each request, check Autho0 to see if the user has the right permissions, only fetch the necessary details when required, reducing the data transfer overhead.
1.★ The application or client requests authorization to the authorization server. This is performed through one of the different authorization flows. For example, a typical OpenID Connect compliant web application will go through the /oauth/authorize endpoint using the authorization code flow.
2.★ When the authorization is granted, the authorization server returns an access token to the application.
3.★ The application uses the access token to access a protected resource (like an API).
Don’t put secret information within token > with signed tokens all info contained within token is exposed to userer/other parties but they can’ change it.
Benefits of JWT
Some benefits of JSON Web Tokens (JWT) over Simple Web Tokens (SWT) and Security Assertion Markup Language Tokens (SAML):
★ Asymmetric Signing:
JWT: Supports asymmetric signing using a public/private key pair, providing a more robust security model.
SWT: Limited to symmetric signing with HMAC, which may have implications for key management and security.
★ Versatility in Key Usage:
JWT: Allows the use of various key types, including X.509 certificates, enhancing flexibility in key management strategies.
SWT: Primarily relies on shared secrets for signing, limiting the range of available key types.
★ Ease of Implementation:
JWT: Typically considered easier to implement due to its simpler structure and support for common web development libraries.
SWT and SAML: May involve more complex implementations, especially in the case of XML-based SAML tokens.
★ Compatibility with Modern Web Standards:
JWT: Aligns well with modern web development practices, RESTful APIs, and JSON-based communication, making it a natural fit for contemporary applications.
SWT and SAML: May be perceived as more traditional or heavyweight, with SAML using XML, which could be less favorable in certain contexts.
★ Token Size and Efficiency:
JWT: Typically has a more compact size due to its JSON format, leading to more efficient transmission and reduced overhead.
SWT and SAML: May have larger token sizes, especially in the case of XML-based SAML, potentially impacting network performance.
★ Community Adoption:
JWT: Enjoying widespread adoption in the developer community, with extensive support in libraries, frameworks, and platforms.
SWT and SAML: While still used in various scenarios, they may not be as widely adopted or preferred in modern web development.
★ Standardization and Interoperability:
JWT: Benefits from standardized specifications, promoting interoperability between different systems and services.
SWT and SAML: While standardized, may face challenges in terms of interoperability and compatibility in diverse environments.
However, between these three (JWT, SWT, and SAML) depends on requirments of application, security concerns, and overall structure of system. [each one has special uses can suitable depending on the case]
Anatomy of JWT Folder
Open the cloned backend »
src/main > spring_portfolio > mvc > jwt
JwtApiController.java
Maps the authentication token creation method to the “/create” endpoint. Validating email and password and if valid then generates the JWT token for credentials.
JwtAuthenticationEntryPoint.java
Implementing AuthenticationEntryPoint and overriding the commence function to specify what to do when a user is not authenticated, return unauthorized error.
JwtRequestFilter.java
Extend Spring Web Filter using OncePerRequestFiler class and overrides doFilterInternal function so requests sent to server are processed through function. Function then checks if JWT token is valid and sets Authentication to specify current user is authenticated.
JwtTokenUtil.java
Contains utilities/functions that are needed to generate JWT tokens and get information like email from JWT tokens to make sure JWT token is valid
Unauthenticated User Redirect
In the context of JWT (JSON Web Tokens) in Java, the concept of unauthenticated user redirect typically involves handling situations where a user tries to access a protected resource without proper authentication. JWT is commonly used for authentication and authorization purposes in web applications.
Here’s an explanation of how unauthenticated user redirect might be implemented in Java with JWT:
- Authentication Process:
- When a user tries to access a _____protected_____ resource, they need to include a valid JWT token in their request.
- The server verifies the JWT token’s signature and checks its claims to ensure the user is authenticated.
- Handling Unauthenticated Users:
- If the user is not authenticated (i.e., they don’t provide a valid JWT token or the token is expired), the server identifies them as an _______unauthenticated________ user.
- Redirect Mechanism:
- Instead of directly denying access, a common approach is to redirect the unauthenticated user to a login page or an authentication ______endpoint______.
- Java Implementation:
- In Java, you might use a web framework like Spring Boot for building your application.
- Spring Security is commonly used for handling authentication and authorization.
Here’s a simplified example using Spring Security:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/protected-resource/**").authenticated() // authentication for "protected resources"
.anyRequest().permitAll() // any request NOT to protected resources is allowed
.and()
.formLogin()
.loginPage("/login") // Redirect to the login page for unauthenticated users
.permitAll()
.and()
.logout()
.permitAll();
}
// Other configurations, userDetailsService, etc.
}
- In this example, `______protected-resource______` is a placeholder for the path of your protected resource.
- If an unauthenticated user tries to access this resource, they will be redirected to the login page (“/login” in this case).
- Login Controller:
- You would need to implement a controller for handling the login process and authentication.
@Controller
public class LoginController {
@GetMapping("/login")
public String showLoginForm() {
return "login"; // Render the login page
}
// Handle form submission and authentication here
}
- The
showLoginForm
method renders the login page when an unauthenticated user is redirected.
What Happens When you aren’t Logged In
When a user is not logged in and tries to access a protected resource (e.g., /person/authenticate
or /human/authenticate
), they will be redirected to the login page (/login
). This redirection is typically handled by the security configuration in your Spring application. In the provided code, the redirection is not explicitly shown, but it’s assumed that there is a login page or endpoint where users are redirected for authentication.
In the JwtAuthenticationEntryPoint
class, the commence
method is called when an unauthenticated user tries to access a secured resource. It sends an unauthorized response (HTTP 401) with the message “Unauthorized.”
JwtAuthenticationEntryPoint.java
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
// When an unauthenticated user tries to access a secured resource
// Send an unauthorized response (HTTP 401) with the message "Unauthorized"
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
Explanation:
- The
JwtAuthenticationEntryPoint
class implements theAuthenticationEntryPoint
interface. - The
commence
method is invoked when an unauthenticated user attempts to access a secured resource. - The method sends an HTTP 401 (Unauthorized) response to indicate that authentication is required.
This class is part of the authentication flow and handles the entry point for unauthenticated requests.
JwtRequestFilter.java
Popcorn Hack: Think back to your mini-project. What kind of pages did you have that could’ve required login? Use the JWT code given in the backend and show in the empty code cell below how you would implement it:
// Code/Code/Code here!
@Component
@RequiredArgsConstructor
public class JwtRequestFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailService userDetailService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String path = request.getRequestURI();
if (path.startsWith("/auth")) {
filterChain.doFilter(request, response);
return;
}
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String token = null;
HttpSession session = request.getSession();
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
token = authorizationHeader.substring(7);
}
username = jwtUtil.extractUsername(token);
if (username == null) {
exceptionCall(response, "invalidToken");
return;
}
UserDetails userDetails = userDetailService.loadUserByUsername(username);
if (SecurityContextHolder.getContext().getAuthentication() == null) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
= new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
session.setAttribute("userId", username);
}
filterChain.doFilter(request, response);
}
private HttpServletResponse exceptionCall(HttpServletResponse response, String errorType) throws IOException {
ResultJson resultJson = new ResultJson();
if (errorType.equals("invalidToken")) {
resultJson.setCode(ResultCode.INVALID_TOKEN.getCode());
resultJson.setMsg(ResultCode.INVALID_TOKEN.getMsg());
}
ObjectMapper objectMapper = new ObjectMapper();
response.getWriter().write(objectMapper.writeValueAsString(resultJson));
response.setCharacterEncoding("utf-8");
response.setContentType("application/json");
return response;
}
}
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
// ... (other autowired dependencies)
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// Extract JWT token from the cookie in the request
final Cookie[] cookies = request.getCookies();
String username = null;
String jwtToken = null;
// Check if cookies are present in the request
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals("jwt")) {
jwtToken = cookie.getValue();
}
}
// If a JWT token is found, attempt to validate and extract username
if (jwtToken != null) {
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException e) {
System.out.println("Unable to get JWT Token");
} catch (ExpiredJwtException e) {
System.out.println("JWT Token has expired");
} catch (Exception e) {
System.out.println("An error occurred");
}
}
}
// If no valid JWT token is found or an error occurs, the user remains unauthenticated
// Continue with the filter chain
chain.doFilter(request, response);
}
}
Explanation:
- The
JwtRequestFilter
class extendsOncePerRequestFilter
and is responsible for processing JWT tokens in each request. - In the
doFilterInternal
method, it extracts the JWT token from the cookie in the request. - If no valid JWT token is found or an error occurs during extraction/validation, the user remains unauthenticated.
- The filter then continues with the filter chain, allowing other filters to process the request.
This class is crucial for validating and processing JWT tokens in each incoming request. If a valid token is present, it sets the user’s authentication details in the security context. If not, the user remains unauthenticated.
Live Demo
Take notes in your own words during the live demo as if you’re explaining to someone else how a JWT program can be used.
Notes:
- JWTTokenUti: This file underwent significant modifications to incorporate role-based functionality. It was necessary to allocate the cookie’s authorities/roles based on their database-assigned roles. These roles were included in the claims, becoming integral to the cookie. Upon the return of the JWT token or cookie in subsequent API requests, we can examine them to identify and retrieve the respective roles and authorities.
- Security Config: Using the authorities granted and set up we can allow requests to specfic api endpoints to be accessed only by people have the specfic role
- When a user attempts to sign in, they provide their credentials (e.g., username and password) to the authentication server. –> generates JWT
Hacks
-
Finish popcorn hacks
- Using Canva, Draw.io, or any other graphics software of your choice, make a diagram that visually explains how user authentication works with JWT in Java.
-
Take a screenshot of your backend repository being successfully tested with Postman.
- Create a basic API, and implement the JWT security features mentioned above. Bonus points for redirection to a login page to create a JWT token.
Details
Jwt token
If you send POST /auth/v1/login with a valid account and password, a token generated as a JWT is created as shown below. In the token, the userId value is set in the json field called username.
If the token is missing or incorrect, the expiration date has passed, or the token is incorrect, an error is displayed as shown below. If the ROLE (authority) for the URL is different, an error is displayed as shown below.
JwtUtil
@Component
public class JwtUtil {
@Value("${jwt.secret-key}")
private String secretKey;
private String createToken(Map<String, Object> claims) {
String secretKeyEncodeBase64 = Encoders.BASE64.encode(secretKey.getBytes());
byte[] keyBytes = Decoders.BASE64.decode(secretKeyEncodeBase64);
Key key = Keys.hmacShaKeyFor(keyBytes);
return Jwts.builder()
.signWith(key)
.setClaims(claims)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24))
.compact();
}
private Claims extractAllClaims(String token) {
if (StringUtils.isEmpty(token)) return null;
String secretKeyEncodeBase64 = Encoders.BASE64.encode(secretKey.getBytes());
Claims claims = null;
try {
claims = Jwts.parserBuilder().setSigningKey(secretKeyEncodeBase64).build().parseClaimsJws(token).getBody();
} catch (JwtException e) {
claims = null;
}
return claims;
}
public String extractUsername(String token) {
final Claims claims = extractAllClaims(token);
if (claims == null) return null;
else return claims.get("username",String.class);
}
public String generateToken(Users users) {
Map<String, Object> claims = new HashMap<>();
claims.put("username", users.getUserId());
return createToken(claims);
}
}
JwtController
@RestController
@RequestMapping("/api/v1")
public class ApiController {
@GetMapping("/hello")
@ResponseBody
public ResultJson hello(HttpServletRequest request) {
ResultJson resultJson = new ResultJson();
resultJson.setCode(ResultCode.SUCCESS.getCode());
resultJson.setMsg("Hello, " + request.getSession().getAttribute("userId").toString());
return resultJson;
}
}
JwtToken
@Component
@RequiredArgsConstructor
public class JwtRequestFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailService userDetailService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String path = request.getRequestURI();
if (path.startsWith("/auth")) {
filterChain.doFilter(request, response);
return;
}
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String token = null;
HttpSession session = request.getSession();
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
token = authorizationHeader.substring(7);
}
username = jwtUtil.extractUsername(token);
if (username == null) {
exceptionCall(response, "invalidToken");
return;
}
UserDetails userDetails = userDetailService.loadUserByUsername(username);
if (SecurityContextHolder.getContext().getAuthentication() == null) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
= new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
session.setAttribute("userId", username);
}
filterChain.doFilter(request, response);
}
private HttpServletResponse exceptionCall(HttpServletResponse response, String errorType) throws IOException {
ResultJson resultJson = new ResultJson();
if (errorType.equals("invalidToken")) {
resultJson.setCode(ResultCode.INVALID_TOKEN.getCode());
resultJson.setMsg(ResultCode.INVALID_TOKEN.getMsg());
}
ObjectMapper objectMapper = new ObjectMapper();
response.getWriter().write(objectMapper.writeValueAsString(resultJson));
response.setCharacterEncoding("utf-8");
response.setContentType("application/json");
return response;
}
}