Welcome to this lesson on refreshing JWTs and signing out from APIs. Building on our previous discussion about JWT Authentication, where we focused on obtaining and utilizing JSON Web Tokens (JWTs) for stateless API authentication, we now explore more advanced aspects. In particular, we'll focus on refresh tokens, a crucial component in maintaining session continuity and providing seamless user experiences. Refresh tokens allow clients to obtain new access tokens without requiring user reauthentication, thereby ensuring uninterrupted access to protected resources. Our objectives today include learning how to refresh JWTs and securely sign out, all to ensure a robust and secure authentication workflow. Let's get started on mastering these important concepts.
As a reminder, JWTs are integral to stateless authentication, often chosen for their scalability and flexibility. Access tokens, a key part of JWT-based systems, are short-lived to minimize the risk of unauthorized access if they are ever leaked. Before diving into refreshing tokens, let's revisit the process of logging in to obtain both access and refresh tokens.
In this example, we'll send a POST
login request and extract tokens from the response. Here’s how it unfolds in Swift:
Swift1import Foundation
2#if canImport(FoundationNetworking)
3import FoundationNetworking
4#endif
5
6// Base URL for the API
7let baseURL = URL(string: "http://localhost:8000")!
8
9// User credentials
10let authDetails: [String: Any] = [
11 "username": "testuser",
12 "password": "testpass123"
13]
14
15let dispatchGroup = DispatchGroup()
16
17do {
18 // Convert user credentials to JSON data
19 let jsonData = try JSONSerialization.data(withJSONObject: authDetails, options: [])
20
21 // Create a POST request for login
22 var request = URLRequest(url: baseURL.appendingPathComponent("/auth/login"))
23 request.httpMethod = "POST"
24 request.setValue("application/json", forHTTPHeaderField: "Content-Type")
25 request.httpBody = jsonData
26
27 // Perform the request
28 dispatchGroup.enter()
29 let task = URLSession.shared.dataTask(with: request) { data, response, error in
30 defer { dispatchGroup.leave() }
31 if let error = error {
32 print("An error occurred: \(error)")
33 return
34 }
35
36 guard let data = data else {
37 print("No data received")
38 return
39 }
40
41 do {
42 // Parse the JSON response
43 if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
44 let accessToken = json["access_token"] as? String
45 let refreshToken = json["refresh_token"] as? String
46 print("Access Token: \(accessToken ?? "No access token")")
47 print("Refresh Token: \(refreshToken ?? "No refresh token")")
48 }
49 } catch {
50 print("Failed to parse JSON: \(error)")
51 }
52 }
53 task.resume()
54 dispatchGroup.wait()
55} catch {
56 print("Failed to create JSON data: \(error)")
57}
In this snippet, we've sent a login request and successfully extracted the access and refresh tokens. These tokens are vital for interacting with protected endpoints and setting the stage for the refreshing process.
To maintain continuous access without forcing users to sign in repeatedly, we employ refresh tokens. These longer-lived tokens enable applications to request a fresh access token once the previous one expires.
With our previously obtained refresh token, we can request a new access token through the refresh process using the /auth/refresh
route. It is a common practice to send the refresh token in the body of the request for security reasons, minimizing exposure in URLs or headers:
Swift1do {
2 // Prepare the refresh token data
3 let refreshData: [String: Any] = ["refresh_token": refreshToken ?? ""]
4 let jsonData = try JSONSerialization.data(withJSONObject: refreshData, options: [])
5
6 // Create a POST request for refreshing tokens
7 var request = URLRequest(url: baseURL.appendingPathComponent("/auth/refresh"))
8 request.httpMethod = "POST"
9 request.setValue("application/json", forHTTPHeaderField: "Content-Type")
10 request.httpBody = jsonData
11
12 // Perform the request
13 dispatchGroup.enter()
14 let task = URLSession.shared.dataTask(with: request) { data, response, error in
15 defer { dispatchGroup.leave() }
16 if let error = error {
17 print("An error occurred: \(error)")
18 return
19 }
20
21 guard let data = data else {
22 print("No data received")
23 return
24 }
25
26 do {
27 // Parse the JSON response
28 let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
29 let newAccessToken json["access_token"] as? String
30 let newRefreshToken = json["refresh_token"] as? String
31 print("Tokens refreshed successfully!")
32 print("New Access Token: \(newAccessToken ?? "No access token")")
33 print("New Refresh Token: \(newRefreshToken ?? "No refresh token")")
34 }
35 } catch {
36 print("Failed to parse JSON: \(error)")
37 }
38 }
39 task.resume()
40 dispatchGroup.wait()
41} catch {
42 print("Failed to create JSON data: \(error)")
43}
Upon executing the refresh request, we receive a new access token, and in our case, the refresh token is also updated by the server for enhanced security. Upon successful token refresh, you should see an output confirming that the tokens have been updated successfully.
With the new access token obtained, we continue to interact securely with protected API endpoints. This process repeats what we discussed earlier — attaching the access token in the Authorization
header of requests. This time, we're utilizing the freshly acquired token to ensure the request is authorized.
Incorporating the updated token, here's how you can access a "todos" endpoint:
Swift1// Define headers with the new JWT access token
2var headers = [
3 "Authorization": "Bearer \(accessToken ?? "")"
4]
5
6// Create a GET request for the "todos" endpoint
7var request = URLRequest(url: baseURL.appendingPathComponent("/todos"))
8request.httpMethod = "GET"
9request.allHTTPHeaderFields = headers
10
11// Perform the request
12dispatchGroup.enter()
13let task = URLSession.shared.dataTask(with: request) { data, response, error in
14 defer { dispatchGroup.leave() }
15 if let error = error {
16 print("An error occurred: \(error)")
17 return
18 }
19
20 guard let data = data else {
21 print("No data received")
22 return
23 }
24
25 do {
26 // Parse the JSON response
27 if letSerialization.jsonObject(with: data, options: []) as? [String: Any] {
28 print("Accessed todos successfully!")
29 print(json)
30 }
31 } catch {
32 print("Failed to parse JSON: \(error)")
33 }
34}
35task.resume()
36dispatchGroup.wait()
The inclusion of the access token in the authorization header ensures that your requests to resources like /todos
are properly authenticated, facilitating secure data transactions. This secure request highlights how refreshed access tokens uphold the integrity of session-based activities while maintaining a streamlined user experience.
Beyond refreshing tokens, signing out from an application is equally crucial as it invalidates current tokens, preventing unauthorized access if they fall into the wrong hands. Signing out serves as an integral part of maintaining security and ending a user session.
By issuing a POST
request to the logout endpoint, we revoke access tokens on the server, often using the refresh token to identify the session:
Swift1do {
2 // Prepare the logout data
3 let logoutData: [String: Any] = ["refresh_token": refreshToken ?? ""]
4 let jsonData = try JSONSerialization.data(withJSONObject: logoutData, options: [])
5
6 // Create a POST request for logout
7 var request = URLRequest(url: baseURL.appendingPathComponent("/auth/logout"))
8 request.httpMethod = "POST"
9 request.setValue("application/json", forHTTPHeaderField: "Content-Type")
10 request.httpBody = jsonData
11
12 // Perform the request
13 dispatchGroup.enter()
14 let task = URLSession.shared.dataTask(with: request) { data, response, error in
15 defer { dispatchGroup.leave() }
16 if let error = error {
17 print("An error occurred: \(error)")
18 return
19 }
20
21 guard let data = data else {
22 print("No data received")
23 return
24 }
25
26 do {
27 // Parse the JSON response
28 if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
29 print("Logged out successfully!")
30 print(json)
31 }
32 } catch {
33 print("Failed to parse JSON: \(error)")
34 }
35 }
36 task.resume()
37 dispatchGroup.wait()
38} catch {
39 print("Failed to create JSON data: \(error)")
40}
Upon successful logout, the application invalidates the session both client-side and server-side. This prevents any further use of revoked tokens, ensuring security. Recognizing what happens server-side during logout supports a comprehensive grasp of session management and security protocols, reinforcing the importance of this step in secure authentication practices.
After executing a logout, the access token is invalidated. Consequently, any attempt to access protected endpoints using the invalid token will fail. Let's illustrate this by attempting to access the "todos" endpoint using the access token post-logout:
Swift1// Create a GET request for the "todos" endpoint after logout
2var request = URLRequest(url: baseURL.appendingPathComponent("/todos"))
3request.httpMethod = "GET"
4request.allHTTPHeaderFields = headers
5
6// Perform the request
7dispatchGroup.enter()
8let task = URLSession.shared.dataTask(with: request) { data, response, error in
9 defer { dispatchGroup.leave() }
10 if let error = error {
11 print("An error occurred: \(error)")
12 return
13 }
14
15 guard let data = data else {
16 print("No data received")
17 return
18 }
19
20 do {
21 // Parse the JSON response
22 if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
23 print("Accessed todos successfully!")
24 print(json)
25 }
26 } catch {
27 print("Failed to parse JSON: \(error)")
28 }
29}
30task.resume()
31dispatchGroup.wait()
As expected, the server will reject the request since the token is no longer valid. This secures the application by ensuring that invalid tokens cannot be used to access resources. The output from this attempt will indicate an unauthorized error, emphasizing the necessity of re-authentication for continued access, thereby safeguarding sensitive data and maintaining session integrity.
In a similar manner, the refresh token becomes invalid after logout, effectively blocking any request to refresh tokens. Here's how the system responds when we attempt to refresh tokens using the invalid refresh token:
Swift1do {
2 // Prepare the refresh token data
3 let refreshData: [String: Any] = ["refresh_token": refreshToken ?? ""]
4 let jsonData = try JSONSerialization.data(withJSONObject: refreshData, options: [])
5
6 // Create a POST request for refreshing tokens after logout
7 var request = URLRequest(url: baseURL.appendingPathComponent("/auth/refresh"))
8 request.httpMethod = "POST"
9 request.setValue("application/json", forHTTPHeaderField: "Content-Type")
10 request.httpBody = jsonData
11
12 // Perform the request
13 dispatchGroup.enter()
14 let task = URLSession.shared.dataTask(with: request) { data, response, error in
15 defer { dispatchGroup.leave() }
16 if let error = error {
17 print("An error occurred: \(error)")
18 return
19 }
20
21 guard let data = data else {
22 print("No data received")
23 return
24 }
25
26 do {
27 // Parse the JSON response
28 if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
29 print("Tokens refreshed successfully!")
30 print(json)
31 }
32 } catch {
33 print("Failed to parse JSON: \(error)")
34 }
35 }
36 task.resume()
37 dispatchGroup.wait()
38} catch {
39 print("Failed to create JSON data: \(error)")
40}
The server detects the invalid token and denies the refresh request, providing an unauthorized error. This output confirms that once a user logs out, subsequent token refresh attempts fail, reinforcing the logging out process's role in invalidating session credentials. By doing so, it ensures that no unauthorized sessions can regain access inadvertently, cementing application security.
In this lesson, you learned about refreshing JWT tokens and effectively signing out to secure API interactions. We explored the vital role of refresh tokens in retaining session continuity and reducing frequent reauthentication burdens. From refreshing tokens and accessing secured endpoints to signing out, you have gained robust skills in managing API authentication workflows confidently.
With these lessons complete, I commend your progress through the course! The techniques covered empower you to handle API security challenges with proficiency. As you proceed to practice exercises, reinforce these concepts by experimenting with different endpoints and error scenarios. Remember, the skills honed here will serve your applications well, ensuring scalable and secure interactions within the API landscape. Best of luck!
