gRPC clients authenticate with OAuth2 bearer tokens not by passing them in metadata, but by using them to obtain an access token which is then passed in metadata.

Here’s how it looks in practice. Imagine a UserService with a GetUser RPC.

service UserService {
  rpc GetUser (GetUserRequest) returns (User);
}

message GetUserRequest {
  string user_id = 1;
}

message User {
  string id = 1;
  string name = 2;
}

Your client code, using Go, might look like this:

package main

import (
	"context"
	"fmt"
	"log"

	"golang.org/x/oauth2"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/oauth"
	pb "your_module/userpb" // Assuming your proto is in userpb
)

func main() {
	// OAuth2 configuration
	conf := &oauth2.Config{
		ClientID:     "your-client-id",
		ClientSecret: "your-client-secret",
		Endpoint: oauth2.Endpoint{
			TokenURL: "https://your-auth-server.com/token",
		},
		Scopes: []string{"read:users"},
	}

	// This is a dummy token, in a real app you'd get this from a secure store
	// or refresh it.
	// The "refresh_token" is what you use to get new access tokens.
	tokenSource := conf.TokenSource(context.Background(), &oauth2.Token{
		RefreshToken: "your-refresh-token",
		Expiry:       time.Now().Add(-24 * time.Hour), // Force refresh
	})

	// gRPC client setup
	// Replace with your server's address
	conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithPerRPCCredentials(oauth.TokenSource{TokenSource: tokenSource}))
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	client := pb.NewUserServiceClient(conn)

	// Make the gRPC call
	ctx := context.Background()
	req := &pb.GetUserRequest{UserId: "user123"}
	user, err := client.GetUser(ctx, req)
	if err != nil {
		log.Fatalf("could not get user: %v", err)
	}

	fmt.Printf("User: %s, Name: %s\n", user.Id, user.Name)
}

The magic happens with grpc.WithPerRPCCredentials(oauth.TokenSource{TokenSource: tokenSource}). This tells gRPC to use the oauth2.TokenSource to obtain credentials for each RPC. The oauth2.TokenSource handles the underlying OAuth2 flow: if the access token is expired or missing, it will use the refresh token to fetch a new one from your authorization server’s token endpoint. This new access token is then automatically added to the outgoing gRPC request headers as an Authorization: Bearer <access_token> header.

This system solves the problem of securely distributing and managing access tokens for a distributed gRPC service. Instead of embedding sensitive refresh tokens directly into client applications or passing them around insecurely, the client only ever needs to hold the refresh token. The oauth2.TokenSource abstracts away the complexity of token refresh, expiry management, and secure transmission to the gRPC server. The server, in turn, receives a standard Authorization: Bearer header, which it can then validate against its OAuth2 provider.

The most surprising mechanical detail is how grpc.WithPerRPCCredentials works. It’s not just a one-time credential injection. The oauth.TokenSource interface is called by the gRPC client framework before each RPC is sent. If the existing token is expired or invalid, the Token() method on the oauth2.TokenSource is invoked, which triggers the refresh process. This ensures that your gRPC calls are always using a valid, unexpired access token without manual intervention on the client side. The framework handles the polling and refreshing transparently.

This pattern is robust for microservices where clients might be long-lived or short-lived, and where you want to delegate authentication authority to a centralized identity provider. The gRPC server simply needs to trust the issuer of the tokens and have a mechanism to validate their signature and scope.

The next logical step is to explore how the gRPC server validates these incoming bearer tokens.

Want structured learning?

Take the full Grpc course →