Lesson 4
Testing Authenticated API Endpoints with Go
Testing Authenticated Endpoints

Welcome to the final lesson in our journey through automated API testing with Go. In this lesson, we'll focus on testing authenticated API endpoints. While you've already learned to organize tests and handle CRUD operations using Go's testing package, today we'll delve into how APIs manage secure access through different authentication methods — API Keys, Sessions, and JWT (JSON Web Tokens).

Authentication is crucial for securing API endpoints against unauthorized access. By understanding how to test these mechanisms, you'll ensure that your API maintains its integrity and protects sensitive data. We'll explore practical examples for each authentication method, giving you the tools to verify that only authorized users can interact with protected resources.

API Key Authentication

API Key authentication is one of the simplest ways to secure an API. It involves sending a unique key as part of the request headers, allowing access to protected endpoints. Let's look at an example of how you can set up and test API Key authentication using Go's HTTP client and testing packages:

Go
1package main_test 2 3import ( 4 "net/http" 5 "testing" 6) 7 8const ( 9 baseURL = "http://localhost:8000" 10 apiKey = "123e4567-e89b-12d3-a456-426614174000" 11) 12 13func TestAPIKeyAuthentication(t *testing.T) { 14 // Arrange 15 req, err := http.NewRequest("GET", baseURL+"/todos", nil) 16 if err != nil { 17 t.Fatal(err) 18 } 19 req.Header.Set("X-API-Key", apiKey) 20 21 client := &http.Client{} 22 23 // Act 24 resp, err := client.Do(req) 25 if err != nil { 26 t.Fatal(err) 27 } 28 defer resp.Body.Close() 29 30 // Assert 31 if resp.StatusCode != http.StatusOK { 32 t.Errorf("Expected status 200, got %v", resp.StatusCode) 33 } 34}

Here, we define a test function TestAPIKeyAuthentication. We create a new HTTP request and set the X-API-Key header. We then use an http.Client{} to send the request and verify that the response status code is 200, indicating successful authentication and access to the endpoint.

Session Authentication: Setup

In Go, managing sessions involves maintaining state across requests. We'll set up a helper function to handle login and maintain session state, enabling each test to authenticate independently:

Go
1package main_test 2 3import ( 4 "bytes" 5 "encoding/json" 6 "net/http" 7 "net/http/cookiejar" 8 "testing" 9) 10 11type AuthDetails struct { 12 Username string `json:"username"` 13 Password string `json:"password"` 14} 15 16func login(t *testing.T, authDetails AuthDetails) *http.Client { 17 jar, _ := cookiejar.New(nil) 18 client := &http.Client{Jar: jar} 19 data, _ := json.Marshal(authDetails) 20 req, err := http.NewRequest("POST", baseURL+"/auth/login", bytes.NewBuffer(data)) 21 if err != nil { 22 t.Fatal(err) 23 } 24 req.Header.Set("Content-Type", "application/json") 25 26 resp, err := client.Do(req) 27 if err != nil { 28 t.Fatal(err) 29 } 30 defer resp.Body.Close() 31 32 if resp.StatusCode != http.StatusOK { 33 t.Fatalf("Expected status 200, got %v", resp.StatusCode) 34 } 35 36 return client 37}

The login function sends a login request and returns an HTTP client that maintains session state.

Session Authentication: Testing Login

This test uses the login helper function to authenticate independently, validating that user credentials create a session successfully:

Go
1func TestLogin(t *testing.T) { 2 // Arrange 3 authDetails := AuthDetails{ 4 Username: "testuser", 5 Password: "testpass123", 6 } 7 8 // Act 9 client := login(t, authDetails) 10 11 // Assert 12 if client == nil { 13 t.Fatal("failed to create session") 14 } 15}

The test ensures that the login process is successful and a session is established.

Session Authentication: Testing Protected Endpoint

In this test, we authenticate using the helper function before confirming that an established session grants access to a protected endpoint:

Go
1func TestAccessWithSession(t *testing.T) { 2 // Arrange 3 authDetails := AuthDetails{ 4 Username: "testuser", 5 Password: "testpass123", 6 } 7 client := login(t, authDetails) 8 9 // Act 10 req, err := http.NewRequest("GET", baseURL+"/todos", nil) 11 if err != nil { 12 t.Fatal(err) 13 } 14 resp, err := client.Do(req) 15 if err != nil { 16 t.Fatal(err) 17 } 18 defer resp.Body.Close() 19 20 // Assert 21 if resp.StatusCode != http.StatusOK { 22 t.Errorf("expected status 200, got %v", resp.StatusCode) 23 } 24}

The test verifies that the session allows access to the protected resource.

Session Authentication: Testing Logout

This test independently authenticates and then verifies the ability to terminate a session, ensuring logout functionality is effective:

Go
1func TestLogoutWithSession(t *testing.T) { 2 // Arrange 3 authDetails := AuthDetails{ 4 Username: "testuser", 5 Password: "testpass123", 6 } 7 client := login(t, authDetails) 8 9 // Act 10 req, err := http.NewRequest("POST", baseURL+"/auth/logout", nil) 11 if err != nil { 12 t.Fatal(err) 13 } 14 resp, err := client.Do(req) 15 if err != nil { 16 t.Fatal(err) 17 } 18 defer resp.Body.Close() 19 20 // Assert 21 if resp.StatusCode != http.StatusOK { 22 t.Errorf("Expected status 200, got %v", resp.StatusCode) 23 } 24}

This approach confirms that each session is independently managed, ensuring logout integrity.

JWT Authentication: Setup

For JWT-based authentication, we replicate this strategy with a helper function to obtain and manage JWTs:

Go
1func loginAndGetTokens(t *testing.T, authDetails AuthDetails) (string, string) { 2 client := &http.Client{} 3 data, _ := json.Marshal(authDetails) 4 req, err := http.NewRequest("POST", baseURL+"/auth/login", bytes.NewBuffer(data)) 5 if err != nil { 6 t.Fatal(err) 7 } 8 req.Header.Set("Content-Type", "application/json") 9 10 resp, err := client.Do(req) 11 if err != nil { 12 t.Fatal(err) 13 } 14 defer resp.Body.Close() 15 16 if resp.StatusCode != http.StatusOK { 17 t.Fatalf("Expected status 200, got %v", resp.StatusCode) 18 } 19 20 var tokens map[string]string 21 json.NewDecoder(resp.Body).Decode(&tokens) 22 23 return tokens["access_token"], tokens["refresh_token"] 24}

This setup empowers each test to authenticate independently, retrieving tokens without relying on the order or success of other tests.

JWT Authentication: Testing Login

Using the loginAndGetTokens helper function, this test independently verifies that successful authentication results in token issuance:

Go
1func TestJWTLogin(t *testing.T) { 2 // Arrange 3 authDetails := AuthDetails{ 4 Username: "testuser", 5 Password: "testpass123", 6 } 7 8 // Act 9 accessToken, refreshToken := loginAndGetTokens(t, authDetails) 10 11 // Assert 12 if accessToken == "" || refreshToken == "" { 13 t.Fatal("failed to obtain tokens") 14 } 15}

The test ensures token validity regardless of prior tests.

JWT Authentication: Testing Protected Endpoint

This test independently authenticates using the helper function and verifies that a JWT access token provides entry to a protected endpoint:

Go
1func TestAccessWithJWT(t *testing.T) { 2 // Arrange 3 authDetails := AuthDetails{ 4 Username: "testuser", 5 Password: "testpass123", 6 } 7 accessToken, _ := loginAndGetTokens(t, authDetails) 8 9 // Act 10 req, err := http.NewRequest("GET", baseURL+"/todos", nil) 11 if err != nil { 12 t.Fatal(err) 13 } 14 req.Header.Set("Authorization", "Bearer "+accessToken) 15 16 client := &http.Client{} 17 resp, err := client.Do(req) 18 if err != nil { 19 t.Fatal(err) 20 } 21 defer resp.Body.Close() 22 23 // Assert 24 if resp.StatusCode != http.StatusOK { 25 t.Errorf("expected status 200, got %v", resp.StatusCode) 26 } 27}

The test validates the token's effectiveness in protecting resources.

JWT Authentication: Testing Logout

Finally, this test independently authenticates to verify that access and refresh tokens can be invalidated:

Go
1func TestJWTLogout(t *testing.T) { 2 // Arrange 3 authDetails := AuthDetails{ 4 Username: "testuser", 5 Password: "testpass123", 6 } 7 accessToken, refreshToken := loginAndGetTokens(t, authDetails) 8 9 // Act 10 req, err := http.NewRequest("POST", baseURL+"/auth/logout", nil) 11 if err != nil { 12 t.Fatal(err) 13 } 14 req.Header.Set("Authorization", "Bearer "+accessToken) 15 req.Header.Set("Content-Type", "application/json") 16 req.Body = ioutil.NopCloser(bytes.NewBufferString(`{"refresh_token":"` + refreshToken + `"}`)) 17 18 client := &http.Client{} 19 resp, err := client.Do(req) 20 if err != nil { 21 t.Fatal(err) 22 } 23 defer resp.Body.Close() 24 25 // Assert 26 if resp.StatusCode != http.StatusOK { 27 t.Errorf("Expected status 200, got %v", resp.StatusCode) 28 } 29}

This test confirms the secure handling and invalidation of tokens, ensuring each test independently checks the integrity of the logout process.

Summary and Practices

Throughout this lesson, we've explored different methods of API authentication: API Keys, Sessions, and JWTs. Each method has distinct ways to test and secure access, and you've seen how to implement tests for them effectively using Go's net/http package.

When testing any form of authentication, it’s crucial to handle API credentials with care. Avoid hardcoding sensitive information in your codebase, and consider using environment variables or secure vaults in production environments.

As we conclude this lesson and the course, take pride in the journey you've completed. You've gained a comprehensive understanding of how to automate API tests using Go, mastering basic requests through to secure, authenticated endpoints. Continue to practice and explore, applying these skills to ensure robust, reliable APIs in your projects. Congratulations on reaching the end of this course!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.