Vault CLI Authentication using OIDC

Written by Luke Arntz on November 27, 2023

tags:

Contents

The vault-client-go SDK has some great examples, but doesn’t explicitly show how to perform OIDC authentication. This may have limited use cases, but it is something I needed to do and wanted to write about it and hopefully save someone else some time figure out how to make this work.

Prerequisites

The first thing we need is a Vault server with the JWT/OIDC auth method configured. During testing I used the google oidc-provider.

Second, you will need go version >=1.21 to run the examples.

Overview

Here is the scenario. We have a Vault server that has oidc authentication configured using the Google OIDC provider. I want to write a command line application that can interact with Vault, but will need to authenticate using OIDC. Because of the way OIDC works this is a little more complicated than just calling a Login() function.

The process will work something like this.

We are going to setup a local HTTP server to receive the callback URL. We’ll then request an authorization URL from Vault (this url will point to Google servers). We’re going to open that URL in our default browser to perform the necessary authentication with Google, receive the callback URL on our local HTTP server, and send specific query parameters from the callback URL to Vault and complete the authentication with vault.

While this post is specific to Hashicorp Vault it should serve as a decent starting point for using a similar authentication flow on other services that support OIDC (e.g., authenticating to a cloud provider).

This video is a nice general overview of OAuth 2.0 and OIDC.

The Code

This code is available on my github.

main.go

The entire main.go file is very basic. We’re defining our httpCallback handler function and global variable, CallBackURL, that will hold the callback URL received from Google. The main() function will call LoginExample() which does all the work.

package main

import (
	"io"
	"net/http"
	"net/url"
)

var CallBackURL *url.URL

func handleCallback(w http.ResponseWriter, r *http.Request) {
	io.WriteString(w, "Close this page")
	CallBackURL = r.URL
}

func main() {
	LoginExample("http://localhost:8200")
}

vault.go

In LoginExample(vaultURL string) we’re going to perform the necessary steps to authenticate with Vault.

  1. Start a local HTTP server to receive the callback URL, and defer the server shutdown (in real life we’ll be doing more work after authentication and we don’t want to leave this server running).
    httpServer := &http.Server{Addr: "127.0.0.1:8250"}
    http.HandleFunc("/", handleCallback)
    go func() {
    	if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
    		fmt.Printf("failed to start callback webserver: %s", err.Error())
    	}
    }()
    defer httpServer.Shutdown(context.TODO())
    
  2. Next, we create a new Vault client and request the authorization URL from Vault. This URL will point to your configured OIDC provider.
    client, err := vault.New(
    	vault.WithAddress(vaultURL),
    	vault.WithRequestTimeout(30*time.Second),
    )
    if err != nil {
    	log.Fatal(err)
    }
    
    r, err := client.Auth.JwtOidcRequestAuthorizationUrl(ctx,
    	schema.JwtOidcRequestAuthorizationUrlRequest{
    		RedirectUri: "http://localhost:8250/oidc/callback",
    	},
    	vault.WithMountPath("oidc"),
    )
    if err != nil {
    	fmt.Println("error JwtOidcRequestAuthorizationUrl:", err.Error())
    	os.Exit(42)
    }
    
  3. Now that we have the authorization URL we want to open in the default browser so we can authenticate with the OIDC provider. We also need to wait until we’ve received the callback URL before we proceed.
    if u, ok := r.Data["auth_url"].(string); ok {
    	fmt.Printf("response: %s\n\n", u)
    	open.Run(u)
    }
    
    for {
    	if CallBackURL != nil {
    		break
    	}
    	time.Sleep(100 * time.Millisecond)
    }
    
  4. Once we have the callback URL we need to send a couple key bits of information back to Vault to complete our authentication with Vault. These bits are information are query parameters in the callback URL. They are specifically code and state. It is possible these query parameters will have different names depending on which OIDC provider you will be using.
    r, err = client.Auth.JwtOidcCallback(ctx,
    	"", // client nonce is not needed in this case
    	CallBackURL.Query().Get("code"),
    	CallBackURL.Query().Get("state"),
    	vault.WithMountPath("oidc"),
    )
    if err != nil {
    	fmt.Printf("error JwtOidcCallback: %s\n\n", err.Error())
    	os.Exit(42)
    }
    
  5. Now we are authenticated with Vault, but there’s still one more thing we must before we can use Vault. We have to tell our vault.Client to use the token we just received. The response from client.Auth.JwtOidcCallback will contain a ResponseAuth struct that holds our token.
    if err := client.SetToken(r.Auth.ClientToken); err != nil {
    	fmt.Printf("client.SetToken error: %s\n", err.Error())
    	os.Exit(84)
    }
    

Now you can continue to follow the examples in the vault-client-go readme.

References

Related Articles

Top