Introduction to JWTs: Authenticate and Access Protected Endpoints

Welcome to this lesson on JWT Authentication. Building on what we learned about session-based authentication, where the server retained user sessions, we now delve into a more stateless form of authentication using JSON Web Tokens (JWTs). JWT authentication is popular because it offers a decentralized and scalable approach, which is crucial for modern applications needing to handle numerous requests efficiently. By the end of this lesson, you will understand how JWTs work and how to use them to securely authenticate API requests, enhancing your capability in managing API interactions. Let's dive into JWT authentication and explore its significance in securing endpoints.

How JWT Works: A Client-Side Perspective

From a client-side perspective, using JWTs involves a series of steps to ensure secure and authenticated interactions with the API. Here’s a simplified breakdown of the JWT process:

  1. User Authentication: The first step is authenticating the user through a login request. The client sends a POST request with the user’s credentials (username and password) to the API's login endpoint.

  2. Token Issuance: Upon successful authentication, the API responds with two tokens:

    • Access Token: This token is a short-lived credential used to access protected API endpoints. It contains encoded information such as the user ID and expiration timestamp.
    • Refresh Token: This is a longer-lived token designed to obtain a new access token without requiring the user to log in again.
  3. Token Utilization: The client stores these tokens and uses the access token to make requests to protected resources. To do this, the access token is added to the HTTP request headers under the key "Authorization." It is usually included as a "Bearer" token, which simply means the token is presented as proof of authentication. So, the header looks like this: Authorization: Bearer YOUR_ACCESS_TOKEN_HERE. This tells the server that the request is authorized and should be granted access to the protected resource.

  4. Access Token Expiration: Once the access token expires, the client can use the refresh token to request a new access token, maintaining a smooth user experience without frequent logins.

In this unit, we'll focus on the access token, understanding its role in securing requests to protected endpoints. Later in this course, we will delve into managing the refresh token, which plays a critical part in sustaining sessions when access tokens expire. By grasping the client-side workflow of JWTs, you can efficiently manage authenticated API interactions in a secure, scalable manner.

Signup Recap

Before diving into JWT-based authentication, let's revisit the signup process we covered in the previous lesson on session-based authentication. Here, we're registering a user with the API using a simple POST request, adding the user to the system without engaging in authentication yet. This familiar process sets the stage for our JWT authentication exploration. Here's a quick example:

Swift
1import Foundation 2#if canImport(FoundationNetworking) 3import FoundationNetworking 4#endif 5 6// Base URL for the API 7let baseURL = URL(string: "http://localhost:8000")! 8 9let dispatchGroup = DispatchGroup() 10 11// User signup details 12let signupDetails: [String: Any] = [ 13 "username": "testuser", 14 "password": "testpass123" 15] 16 17do { 18 // Convert signup details to JSON data 19 let jsonData = try JSONSerialization.data(withJSONObject: signupDetails, options: []) 20 21 // Create a URL request for the signup endpoint 22 var request = URLRequest(url: baseURL.appendingPathComponent("/auth/signup")) 23 request.httpMethod = "POST" 24 request.setValue("application/json", forHTTPHeaderField: "Content-Type") 25 request.httpBody = jsonData 26 27 // Enter the dispatch group 28 dispatchGroup.enter() 29 30 // Create a URLSession data task 31 let task = URLSession.shared.dataTask(with: request) { data, response, error in 32 defer { dispatchGroup.leave() } // Leave the dispatch group when the task is complete 33 34 if let error = error { 35 print("An error occurred: \(error)") 36 return 37 } 38 39 guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { 40 print("Server error!") 41 return 42 } 43 44 if let data = data { 45 do { 46 if let jsonResponse = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { 47 print("Signed up successfully!") 48 print(jsonResponse) 49 } 50 } catch { 51 print("JSON error: \(error.localizedDescription)") 52 } 53 } 54 } 55 56 // Start the task 57 task.resume() 58 59 // Wait for the task to complete 60 dispatchGroup.wait() 61} catch { 62 print("JSON serialization error: \(error.localizedDescription)") 63}

In this code, we're making a POST request to our API's signup endpoint to register a new user. This step mirrors the session-based signup approach where user data is added to the system but doesn't directly engage in authentication processes. With the user now registered, we're ready to proceed to the login step and obtain JWT tokens.

Login for JWT Retrieval

With the user now registered, the next crucial step is to log in with these credentials and begin the JWT retrieval. This process involves sending another POST request to the API:

Swift
1// User login details (same as signup details) 2let loginDetails: [String: Any] = [ 3 "username": "testuser", 4 "password": "testpass123" 5] 6 7do { 8 // Convert login details to JSON data 9 let jsonData = try JSONSerialization.data(withJSONObject: loginDetails, options: []) 10 11 // Create a URL request for the login endpoint 12 var request = URLRequest(url: baseURL.appendingPathComponent("/auth/login")) 13 request.httpMethod = "POST" 14 request.setValue("application/json", forHTTPHeaderField: "Content-Type") 15 request.httpBody = jsonData 16 17 // Enter the dispatch group 18 dispatchGroup.enter() 19 20 // Create a URLSession data task 21 let task = URLSession.shared.dataTask(with: request) { data, response, error in 22 defer { dispatchGroup.leave() } // Leave the dispatch group when the task is complete 23 24 if let error = error { 25 print("An error occurred: \(error)") 26 return 27 } 28 29 guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { 30 print("Server error!") 31 return 32 } 33 34 if let data = data { 35 do { 36 if let jsonResponse = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { 37 print("Logged in successfully!") 38 print(jsonResponse) 39 } 40 } catch { 41 print("JSON error: \(error.localizedDescription)") 42 } 43 } 44 } 45 46 // Start the task 47 task.resume() 48 49 // Wait for the task to complete 50 dispatchGroup.wait() 51} catch { 52 print("JSON serialization error: \(error.localizedDescription)") 53}

In this login step, we submit a POST request that includes the user credentials. Unlike the session-based method, which used cookies to maintain sessions, this server returns JWTs that provide secure and efficient access to API resources.

Extracting Access and Refresh Tokens

Once you've successfully logged in, it's essential to extract the JWT tokens from the login response to use them in your application:

Swift
1// Assuming jsonResponse is the response dictionary from the login request 2var accessToken: String? 3var refreshToken: String? 4 5if let jsonResponse = try? JSONSerialization.jsonObject(with: Data(), options: []) as? [String: Any] { 6 if let token = jsonResponse["access_token"] as? String { 7 accessToken = token 8 print("Access Token: \(accessToken!)") 9 } 10 if let token = jsonResponse["refresh_token"] as? String { 11 refreshToken = token 12 print("Refresh Token: \(refreshToken!)") 13 } 14} else { 15 print("An error occurred during token extraction") 16}

In this code snippet, we pull out the access_token and refresh_token from the server's response. These tokens enable you to authenticate API requests and manage sessions in a stateless manner. The access token facilitates immediate request authentication, while the refresh token ensures the continuity of the session by allowing renewal of access tokens without requiring user reauthentication. By efficiently handling these tokens, we embrace a scalable and secure method for managing authenticated interactions with the API.

Some authentication systems implement refresh token rotation, meaning that each time a refresh request is made, the server provides a new refresh token while invalidating the previous one. This prevents an attacker from using an old refresh token in case it was stolen. Always ensure only one active refresh token exists per user session by revoking old refresh tokens upon issuing a new one. If the refresh token is stored insecurely (e.g., in memory instead of an HTTP-only cookie), it may be vulnerable to theft via cross-site scripting (XSS).

Making Requests to Protected Endpoints with JWT

With the access token in hand, we can now authenticate requests to protected API endpoints. This is done by including the JWT in the HTTP request headers, ensuring that the API recognizes and allows the request. Here’s how you can make a request to a secured endpoint using the access token:

Swift
1if let accessToken = accessToken { 2 // Define the headers with the JWT token for authentication 3 var request = URLRequest(url: baseURL.appendingPathComponent("/todos")) 4 request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") 5 6 // Enter the dispatch group 7 dispatchGroup.enter() 8 9 // Create a URLSession data task 10 let task = URLSession.shared.dataTask(with: request) { data, response, error in 11 defer { dispatchGroup.leave() } // Leave the dispatch group when the task is complete 12 13 if let error = error { 14 print("An error occurred: \(error)") 15 return 16 } 17 18 guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { 19 print("Server error!") 20 return 21 } 22 23 if let data = data { 24 do { 25 if let jsonResponse = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { 26 print("Accessed todos successfully!") 27 print(jsonResponse) 28 } 29 } catch { 30 print("JSON error: \(error.localizedDescription)") 31 } 32 } 33 } 34 35 // Start the task 36 task.resume() 37 38 // Wait for the task to complete 39 dispatchGroup.wait() 40}

In this code, the access token is included in the "Authorization" header as a Bearer token. This header is crucial, as it signals to the API that the request is properly authenticated, granting access to the protected resources. For example, the /todos endpoint in this snippet is accessed securely, and if successful, the protected data is printed in the output.

By making requests in this manner, you ensure that sensitive operations and data retrievals are secure, leveraging the JWT approach for stateless, scalable, and flexible interaction with the API.

Summary and Preparing for Practice

In this lesson, you have enhanced your API authentication skills by understanding and utilizing JWTs. From signing up and logging in to securing requests with tokens, you are now equipped to apply JWT authentication in real-world scenarios. The transition from session-based to token-based authentication brings flexibility, scalability, and ease to API interactions. As you proceed to practice exercises, apply these new techniques, explore different endpoints, and solidify your understanding of JWT authentication. Keep experimenting to master these concepts, preparing you for further advanced methods like refreshing tokens and signing out in subsequent lessons.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal