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.
A JWT consists of three parts, separated by dots (.
):
Plain text1HEADER.PAYLOAD.SIGNATURE
- Header – Contains metadata like the token type (
JWT
) and signing algorithm (HS256
). - Payload (Claims) – Holds user-related data, such as:
sub
: User IDiat
: Issued timeexp
: Expiration time
- Signature – A cryptographic hash of the header and payload, signed with a secret key to prevent tampering.
Example JWT (before decoding):
Plain text1eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjoxNzExNDYyODAwfQ.XYZ123...
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:
-
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. -
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.
-
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. -
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.
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:
Go1package main 2 3import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "net/http" 8) 9 10func main() { 11 // Base URL for the API 12 baseURL := "http://localhost:8000" 13 14 // User signup details 15 signupDetails := map[string]string{ 16 "username": "johnsmith", 17 "password": "testpass123", 18 } 19 20 // Convert signup details to JSON 21 signupDetailsJSON, err := json.Marshal(signupDetails) 22 if err != nil { 23 fmt.Println("Error marshalling signup details:", err) 24 return 25 } 26 27 // Send a POST request to the signup endpoint with user details 28 resp, err := http.Post(fmt.Sprintf("%s/auth/signup", baseURL), "application/json", bytes.NewBuffer(signupDetailsJSON)) 29 if err != nil { 30 fmt.Println("Error sending signup request:", err) 31 return 32 } 33 defer resp.Body.Close() 34 35 // Check if the request was successful 36 if resp.StatusCode != http.StatusOK { 37 fmt.Printf("Signup failed with status: %s\n", resp.Status) 38 return 39 } 40 41 // Print a success message 42 fmt.Println("Signed up successfully!") 43}
In this code, as before, 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.
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:
Go1package main 2 3import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9) 10 11func main() { 12 // Base URL for the API 13 baseURL := "http://localhost:8000" 14 15 // User login details (same as signup details) 16 loginDetails := map[string]string{ 17 "username": "johnsmith", 18 "password": "testpass123", 19 } 20 21 // Convert login details to JSON 22 loginDetailsJSON, err := json.Marshal(loginDetails) 23 if err != nil { 24 fmt.Println("Error marshalling login details:", err) 25 return 26 } 27 28 // Send a POST request to the login endpoint with user details 29 resp, err := http.Post(fmt.Sprintf("%s/auth/login", baseURL), "application/json", bytes.NewBuffer(loginDetailsJSON)) 30 if err != nil { 31 fmt.Println("Error sending login request:", err) 32 return 33 } 34 defer resp.Body.Close() 35 36 // Check if the request was successful 37 if resp.StatusCode != http.StatusOK { 38 fmt.Printf("Login failed with status: %s\n", resp.Status) 39 return 40 } 41 42 // Read the response body 43 body, err := io.ReadAll(resp.Body) 44 if err != nil { 45 fmt.Println("Error reading response body:", err) 46 return 47 } 48 49 // Print a success message and the response content 50 fmt.Println("Logged in successfully!") 51 fmt.Println(string(body)) 52}
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.
Once you've successfully logged in, it's essential to extract the JWT tokens from the login response to use them in your application:
Go1package main 2 3import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9) 10 11func main() { 12 // Base URL for the API 13 baseURL := "http://localhost:8000" 14 15 // User login details (same as signup details) 16 loginDetails := map[string]string{ 17 "username": "johnsmith", 18 "password": "testpass123", 19 } 20 21 // Convert login details to JSON 22 loginDetailsJSON, err := json.Marshal(loginDetails) 23 if err != nil { 24 fmt.Println("Error marshalling login details:", err) 25 return 26 } 27 28 // Send a POST request to the login endpoint with user details 29 resp, err := http.Post(fmt.Sprintf("%s/auth/login", baseURL), "application/json", bytes.NewBuffer(loginDetailsJSON)) 30 if err != nil { 31 fmt.Println("Error sending login request:", err) 32 return 33 } 34 defer resp.Body.Close() 35 36 // Check if the request was successful 37 if resp.StatusCode != http.StatusOK { 38 fmt.Printf("Login failed with status: %s\n", resp.Status) 39 return 40 } 41 42 // Read the response body 43 body, err := io.ReadAll(resp.Body) 44 if err != nil { 45 fmt.Println("Error reading response body:", err) 46 return 47 } 48 49 // Parse the response body to extract tokens 50 var responseData map[string]string 51 if err := json.Unmarshal(body, &responseData); err != nil { 52 fmt.Println("Error parsing response body:", err) 53 return 54 } 55 56 // Extract the access token and refresh token 57 accessToken := responseData["access_token"] 58 refreshToken := responseData["refresh_token"] 59 60 fmt.Println("Access Token:", accessToken) 61 fmt.Println("Refresh Token:", refreshToken) 62}
In this final 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.
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:
Go1package main 2 3import ( 4 "fmt" 5 "io" 6 "net/http" 7) 8 9func main() { 10 // Base URL for the API 11 baseURL := "http://localhost:8000" 12 13 // Access token obtained from login 14 accessToken := "YOUR_ACCESS_TOKEN_HERE" 15 16 // Define the headers with the JWT token for authentication 17 client := &http.Client{} 18 req, err := http.NewRequest("GET", fmt.Sprintf("%s/todos", baseURL), nil) 19 if err != nil { 20 fmt.Println("Error creating request:", err) 21 return 22 } 23 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken)) 24 25 // Access a protected endpoint with the JWT token 26 resp, err := client.Do(req) 27 if err != nil { 28 fmt.Println("Error sending request:", err) 29 return 30 } 31 defer resp.Body.Close() 32 33 // Check if the request was successful 34 if resp.StatusCode != http.StatusOK { 35 fmt.Printf("Failed to access todos with status: %s\n", resp.Status) 36 return 37 } 38 39 // Read the response body 40 body, err := io.ReadAll(resp.Body) 41 if err != nil { 42 fmt.Println("Error reading response body:", err) 43 return 44 } 45 46 // Print a success message and the response content 47 fmt.Println("Accessed todos successfully!") 48 fmt.Println(string(body)) 49}
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.
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.