Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 38

API Authentication & Custom Headers

Recommended resources

Hello! Welcome to your next lesson in the "Interacting with Web Services and APIs" module.

In our last session, we successfully parsed JSON API responses into native Go structs, turning raw data into a structured, usable format. You're now able to make requests and understand the data you get back.

Today, we'll address a critical piece of the puzzle: authentication. Most APIs you'll use in a DevOps context aren't open to the public; they require you to prove your identity. We will learn how to pass authentication tokens and other custom headers in your API requests. This skill is fundamental for automating interactions with platforms like GitHub, GitLab, cloud providers, and other secured services.

1. A Quick Refresher on HTTP Headers

Before we dive into the Go implementation, let's briefly review the role of HTTP headers. They are key-value pairs sent with every HTTP request and response, carrying metadata that tells the client and server how to handle the communication.

Supercharge Your REST API with These Essential HTTP Headers

To start, let's watch a quick overview of what HTTP headers are and why they are important. This video from ByteMonk gives a clear, high-level summary.

Watch from the beginning to 01:08. Focus on understanding the basic structure and purpose of headers as metadata for an HTTP message.

While many headers exist, our primary focus today is on the Authorization header, which is the standard way to carry credentials for API access.

Supercharge Your REST API with These Essential HTTP Headers

The same video provides a concise explanation of the Authorization header.

Now, watch from 06:13 to 07:00. This section explains the header's role and introduces the Bearer token scheme, a common authentication method you'll frequently encounter when working with REST APIs.

The Bearer scheme is simple: you present a token, and the server "bears" (accepts) it as proof of your identity and permissions, without needing complex cryptographic handshakes for every request.


2. Building a Request with Custom Headers in Go

So far, we've used convenience functions like http.Get(). To add headers, we need more control. This involves creating the request object ourselves before sending it. The process is:

  1. Create a request object using http.NewRequest().
  2. Add headers to the request's Header field.
  3. Send the request using an http.Client.

The req.Header field on an *http.Request object has a Set(key, value) method that makes adding headers straightforward.

How To Make HTTP Requests in Go

The tutorial "How To Make HTTP Requests in Go" from DigitalOcean has a great FAQ section that provides a clear, practical code snippet for adding multiple headers.

Please read the answer to question #3, "How do I add headers to an HTTP request in Go?". Notice how req.Header.Set() is used multiple times to add different kinds of headers: Authorization for security, User-Agent for client identification, Accept for content negotiation, and a custom X-Custom-Header for application-specific information.

As you can see, the pattern is simple and repeatable for any header you need to add.


3. A DevOps Best Practice: Managing API Tokens Securely

Your experience with scripting and automation has surely taught you that hardcoding secrets like API keys or tokens into your code is a significant security risk. A more secure and flexible approach is to load them from the environment.

Go's standard library os package provides the Getenv() function for this purpose. This is the idiomatic way to handle secrets in Go applications, allowing your tool to be configured for different environments (dev, staging, prod) without changing the code.

Configuring the Go HTTP client

Let's look at a resource that combines the act of setting the Authorization header with the best practice of loading the token from the environment. The article "Configuring the Go HTTP client" from LogRocket demonstrates this perfectly.

Read the section titled "Authorizing your requests". The key takeaway here is the use of os.Getenv("TOKEN") to retrieve the secret. This decouples your code from the sensitive value, which is crucial for building secure and maintainable automation tools.


4. Putting It All Together: A Practical Example

Let's combine what we've learned into a practical program. We will write a small Go tool that fetches your user profile from the GitHub API. This requires authentication, so it's a perfect use case.

Here's the plan for our program:

  1. Define a struct to hold the GitHub user data (connecting to our last lesson).
  2. Read a GitHub Personal Access Token from an environment variable named GITHUB_TOKEN.
  3. Create an http.Client with a reasonable timeout.
  4. Construct a new GET request to the GitHub API.
  5. Set the Authorization header with the token.
  6. Set a custom User-Agent header, which is a good practice (and required by the GitHub API).
  7. Execute the request, check for errors and non-200 status codes.
  8. Parse the JSON response into our struct and print the result.

Here is the complete code. You can save this as main.go.

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"time"
)

// GitHubUser represents a subset of the fields from the GitHub /user API response.
type GitHubUser struct {
	Login     string `json:"login"`
	Name      string `json:"name"`
	PublicRepos int    `json:"public_repos"`
}

func main() {
	// 1. Read the token from an environment variable.
	token := os.Getenv("GITHUB_TOKEN")
	if token == "" {
		fmt.Println("Error: GITHUB_TOKEN environment variable not set.")
		os.Exit(1)
	}

	// 2. Create a custom HTTP client.
	client := &http.Client{
		Timeout: 10 * time.Second,
	}

	// 3. Create the request object.
	req, err := http.NewRequest("GET", "https://api.github.com/user", nil)
	if err != nil {
		fmt.Printf("Error creating request: %v\n", err)
		os.Exit(1)
	}

	// 4. Set the required headers.
	req.Header.Set("Authorization", "Bearer "+token)
	req.Header.Set("User-Agent", "Go-DevOps-Tool/1.0")
	// GitHub API recommends this header for API versioning.
	req.Header.Set("Accept", "application/vnd.github.v3+json")

	// 5. Execute the request.
	resp, err := client.Do(req)
	if err != nil {
		fmt.Printf("Error making request: %v\n", err)
		os.Exit(1)
	}
	defer resp.Body.Close()

	// 6. Check for a non-successful status code.
	if resp.StatusCode != http.StatusOK {
		fmt.Printf("Error: received non-200 status code: %d\n", resp.StatusCode)
		os.Exit(1)
	}

	// 7. Decode the JSON response.
	var user GitHubUser
	if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
		fmt.Printf("Error decoding JSON: %v\n", err)
		os.Exit(1)
	}

	// 8. Print the result.
	fmt.Printf("Successfully fetched data for user: %s (%s)\n", user.Name, user.Login)
	fmt.Printf("Public Repositories: %d\n", user.PublicRepos)
}

To run this code:

  1. Generate a GitHub Personal Access Token with the read:user scope.
  2. Set the environment variable and run the program from your terminal:
    export GITHUB_TOKEN="your_github_token_here"
    go run main.go
    

You should see your GitHub profile information printed to the console.

Test your understanding!

Imagine you are working with an internal company API that uses a custom header X-Request-ID for tracing requests through different services. How would you modify the main function in the example above to include a header X-Request-ID with the value devops-tool-12345?

Show answer

You would add another req.Header.Set() call after setting the other headers. The new line would look like this:

// Add this line inside the main function, for example, after setting the 'Accept' header.
req.Header.Set("X-Request-ID", "devops-tool-12345")

This demonstrates that adding any custom header follows the exact same pattern.


Conclusion

You have now mastered a critical skill for building powerful DevOps tools: authenticating with and customizing requests to modern APIs. By constructing requests manually and setting headers, you can interact with virtually any secured REST API.

Key Takeaways:

  • Use http.NewRequest() to create a request object that you can customize before sending.
  • Modify request headers using the req.Header.Set("Key", "Value") method.
  • The Authorization: Bearer <token> header is the standard for token-based authentication.
  • Always load secrets like API tokens from the environment using os.Getenv() rather than hardcoding them.
  • Setting a descriptive User-Agent header is a good practice for identifying your tool's API traffic.

Next Up

Our requests are becoming more robust, but what happens when a network is slow or an API is temporarily unavailable? A simple timeout isn't always enough. In our next lesson, we will explore Go's context package to manage request lifecycles and implement timeouts and cancellation, giving you fine-grained control over your network operations.