authn

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 4, 2024 License: Apache-2.0 Imports: 6 Imported by: 3

README

authn

Build Report Card GoDoc Slack

connectrpc.com/authn provides authentication middleware for Connect. It works with any authentication scheme (including HTTP basic authentication, cookies, bearer tokens, and mutual TLS), and it's carefully designed to minimize the resource consumption of unauthenticated RPCs. Middleware built with authn covers both unary and streaming RPCs made with the Connect, gRPC, and gRPC-Web protocols.

For more on Connect, see the announcement blog post, the documentation on connectrpc.com (especially the Getting Started guide for Go), the demo service, or the protocol specification.

A small example

Curious what all this looks like in practice? From a Protobuf schema, we generate a small RPC package. Using that package, we can build a server and wrap it with some basic authentication:

package main

import (
  "context"
  "crypto/subtle"
  "net/http"

  "connectrpc.com/authn"
  "connectrpc.com/authn/internal/gen/authn/ping/v1/pingv1connect"
)

func authenticate(_ context.Context, req authn.Request) (any, error) {
  username, password, ok := req.BasicAuth()
  if !ok {
    return nil, authn.Errorf("invalid authorization")
  }
  if !equal(password, "open-sesame") {
    return nil, authn.Errorf("invalid password")
  }
  // The request is authenticated! We can propagate the authenticated user to
  // Connect interceptors and services by returning it: the middleware we're
  // about to construct will attach it to the context automatically.
  return username, nil
}

func equal(left, right string) bool {
  // Using subtle prevents some timing attacks.
  return subtle.ConstantTimeCompare([]byte(left), []byte(right)) == 1
}

func main() {
  mux := http.NewServeMux()
  service := &pingv1connect.UnimplementedPingServiceHandler{}
  mux.Handle(pingv1connect.NewPingServiceHandler(service))

  middleware := authn.NewMiddleware(authenticate)
  handler := middleware.Wrap(mux)
  http.ListenAndServe("localhost:8080", handler)
}

Cookie and token-based authentication is similar. Mutual TLS is a bit more complex, but pkg.go.dev includes a complete example.

Ecosystem

  • connect-go: the Go implementation of Connect's RPC runtime
  • examples-go: service powering demo.connectrpc.com, including bidi streaming
  • grpchealth: gRPC-compatible health checks
  • grpcreflect: gRPC-compatible server reflection
  • cors: CORS support for Connect servers
  • connect-es: Type-safe APIs with Protobuf and TypeScript
  • conformance: Connect, gRPC, and gRPC-Web interoperability tests

Status: Unstable

This module isn't stable yet, but it's fairly small — we expect to reach a stable release quickly.

It supports the three most recent major releases of Go. Keep in mind that only the last two releases receive security patches.

Within those parameters, authn follows semantic versioning. We will not make breaking changes in the 1.x series of releases.

Offered under the Apache 2 license.

Documentation

Overview

Package authn provides authentication middleware for connect.

Example (BasicAuth)
package main

import (
	"context"
	"crypto/subtle"
	"encoding/base64"
	"fmt"
	"net/http"
	"net/http/httptest"

	"connectrpc.com/authn"
	pingv1 "connectrpc.com/authn/internal/gen/authn/ping/v1"
	"connectrpc.com/authn/internal/gen/authn/ping/v1/pingv1connect"
	"connectrpc.com/connect"
)

func main() {
	// This example shows how to use this package with HTTP basic authentication.
	// Any header-based authentication (including cookies and bearer tokens)
	// works similarly.

	// First, we define our authentication logic and use it to build middleware.
	authenticate := func(_ context.Context, req authn.Request) (any, error) {
		username, password, ok := req.BasicAuth()
		if !ok {
			return nil, authn.Errorf("invalid authorization")
		}
		if !equal(password, "open-sesame") {
			return nil, authn.Errorf("invalid password")
		}
		// The request is authenticated! We can propagate the authenticated user to
		// Connect interceptors and services by returning it: the middleware we're
		// about to construct will attach it to the context automatically.
		fmt.Println("authenticated request from", username)
		return username, nil
	}
	middleware := authn.NewMiddleware(authenticate)

	// Next, we build our Connect handler.
	mux := http.NewServeMux()
	service := &pingv1connect.UnimplementedPingServiceHandler{}
	mux.Handle(pingv1connect.NewPingServiceHandler(service))

	// Finally, we wrap the handler with our middleware and start our server.
	handler := middleware.Wrap(mux)
	server := httptest.NewServer(handler)
	defer server.Close()

	// Clients authenticate by setting the standard Authorization header.
	client := pingv1connect.NewPingServiceClient(http.DefaultClient, server.URL)
	req := connect.NewRequest(&pingv1.PingRequest{})
	req.Header().Set(
		"Authorization",
		"Basic "+base64.StdEncoding.EncodeToString([]byte("Aladdin:open-sesame")),
	)
	_, err := client.Ping(context.Background(), req)

	// We're using the UnimplementedPingServiceHandler stub, so authenticated
	// clients should receive an error with CodeUnimplemented.
	if connect.CodeOf(err) == connect.CodeUnimplemented {
		fmt.Println("client received response")
	} else {
		fmt.Printf("unexpected error: %v\n", err)
	}

}

func equal(left, right string) bool {

	return subtle.ConstantTimeCompare([]byte(left), []byte(right)) == 1
}
Output:

authenticated request from Aladdin
client received response
Example (MutualTLS)
package main

import (
	"context"
	"crypto/rand"
	"crypto/rsa"
	"crypto/subtle"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"errors"
	"fmt"
	"math/big"
	"net"
	"net/http"
	"net/http/httptest"
	"time"

	"connectrpc.com/authn"
	pingv1 "connectrpc.com/authn/internal/gen/authn/ping/v1"
	"connectrpc.com/authn/internal/gen/authn/ping/v1/pingv1connect"
	"connectrpc.com/connect"
)

func main() {
	// This example shows how to use this package with mutual TLS.
	// First, we define our authentication logic and use it to build middleware.
	authenticate := func(_ context.Context, req authn.Request) (any, error) {
		tls := req.TLS()
		if tls == nil {
			return nil, authn.Errorf("TLS required")
		}
		if len(tls.VerifiedChains) == 0 || len(tls.VerifiedChains[0]) == 0 {
			return nil, authn.Errorf("could not verify peer certificate")
		}
		name := tls.VerifiedChains[0][0].Subject.CommonName
		if !equal(name, "Aladdin") { // hardcode example credentials
			return nil, authn.Errorf("invalid subject common name %q", name)
		}
		// The request is authenticated! We can propagate the authenticated user to
		// Connect interceptors and services by returning it: the middleware we're
		// about to construct will attach it to the context automatically.
		fmt.Println("authenticated request from", name)
		return name, nil
	}
	middleware := authn.NewMiddleware(authenticate)

	// Next, we build our Connect handler.
	mux := http.NewServeMux()
	service := &pingv1connect.UnimplementedPingServiceHandler{}
	mux.Handle(pingv1connect.NewPingServiceHandler(service))

	// Finally, we wrap the handler with our middleware and start the server.
	// Creating server and client TLS configurations is particularly verbose in
	// examples, where we need to set up a complete self-signed chain of trust.
	clientTLS, serverTLS, err := newTLSConfigs("Aladdin", "Cave of Wonders")
	if err != nil {
		fmt.Printf("error creating TLS configs: %v\n", err)
		return
	}
	handler := middleware.Wrap(mux)
	server := httptest.NewUnstartedServer(handler)
	server.TLS = serverTLS
	server.StartTLS()
	defer server.Close()

	// Clients must configure their underlying HTTP clients to present a valid
	// certificate.
	httpClient := &http.Client{
		Transport: &http.Transport{TLSClientConfig: clientTLS},
	}
	client := pingv1connect.NewPingServiceClient(httpClient, server.URL)
	_, err = client.Ping(
		context.Background(),
		connect.NewRequest(&pingv1.PingRequest{}),
	)

	// We're using the UnimplementedPingServiceHandler stub, so authenticated
	// clients should receive an error with CodeUnimplemented.
	if connect.CodeOf(err) == connect.CodeUnimplemented {
		fmt.Println("client received response")
	} else {
		fmt.Printf("unexpected error: %v\n", err)
	}

}

func newTLSConfigs(clientName, serverName string) (client *tls.Config, server *tls.Config, _ error) {
	caCertPEM, caKeyPEM, err := createCertificateAuthority()
	if err != nil {
		return nil, nil, fmt.Errorf("create certificate authority: %w", err)
	}
	certPool := x509.NewCertPool()
	if ok := certPool.AppendCertsFromPEM(caCertPEM); !ok {
		return nil, nil, errors.New("failed to append certs to pool")
	}
	serverCertificate, err := newCertificate(caCertPEM, caKeyPEM, serverName)
	if err != nil {
		return nil, nil, fmt.Errorf("create server certificate: %w", err)
	}
	clientCertificate, err := newCertificate(caCertPEM, caKeyPEM, clientName)
	if err != nil {
		return nil, nil, fmt.Errorf("create client certificate: %w", err)
	}
	clientTLS := &tls.Config{
		Certificates: []tls.Certificate{clientCertificate},
		RootCAs:      certPool,
		MinVersion:   tls.VersionTLS12,
	}
	serverTLS := &tls.Config{
		ClientAuth:   tls.RequireAndVerifyClientCert,
		Certificates: []tls.Certificate{serverCertificate},
		ClientCAs:    certPool,
		MinVersion:   tls.VersionTLS12,
	}
	return clientTLS, serverTLS, nil
}

func createCertificateAuthority() ([]byte, []byte, error) {
	caCert := &x509.Certificate{
		SerialNumber: big.NewInt(2021),
		Subject: pkix.Name{
			Organization: []string{"Acme Co"},
		},
		NotBefore:             time.Now().AddDate(-1, 0, 0),
		NotAfter:              time.Now().AddDate(10, 0, 0),
		IsCA:                  true,
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
		BasicConstraintsValid: true,
	}
	caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
	if err != nil {
		return nil, nil, err
	}
	caBytes, err := x509.CreateCertificate(rand.Reader, caCert, caCert, &caPrivKey.PublicKey, caPrivKey)
	if err != nil {
		return nil, nil, err
	}
	caPEM := pem.EncodeToMemory(&pem.Block{
		Type:  "CERTIFICATE",
		Bytes: caBytes,
	})
	caPrivKeyPEM := pem.EncodeToMemory(&pem.Block{
		Type:  "RSA PRIVATE KEY",
		Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey),
	})
	return caPEM, caPrivKeyPEM, nil
}

func newCertificate(caCertPEM, caKeyPEM []byte, commonName string) (tls.Certificate, error) {
	keyPEMBlock, _ := pem.Decode(caKeyPEM)
	privateKey, err := x509.ParsePKCS1PrivateKey(keyPEMBlock.Bytes)
	if err != nil {
		return tls.Certificate{}, err
	}
	certPEMBlock, _ := pem.Decode(caCertPEM)
	parent, err := x509.ParseCertificate(certPEMBlock.Bytes)
	if err != nil {
		return tls.Certificate{}, err
	}
	cert := &x509.Certificate{
		SerialNumber: big.NewInt(1658),
		Subject: pkix.Name{
			Organization: []string{"Acme Co"},
			CommonName:   commonName,
		},
		IPAddresses: []net.IP{
			net.IPv4(127, 0, 0, 1),
			net.IPv6loopback,
			net.IPv4(0, 0, 0, 0),
			net.IPv6zero,
		},
		NotBefore:    time.Now().AddDate(-1, 0, 0),
		NotAfter:     time.Now().AddDate(10, 0, 0),
		SubjectKeyId: []byte{1, 2, 3, 4, 6},
		ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
		KeyUsage:     x509.KeyUsageDigitalSignature,
	}
	certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
	if err != nil {
		return tls.Certificate{}, err
	}
	certBytes, err := x509.CreateCertificate(rand.Reader, cert, parent, &certPrivKey.PublicKey, privateKey)
	if err != nil {
		return tls.Certificate{}, err
	}
	certPEM := pem.EncodeToMemory(&pem.Block{
		Type:  "CERTIFICATE",
		Bytes: certBytes,
	})
	certPrivKeyPEM := pem.EncodeToMemory(&pem.Block{
		Type:  "RSA PRIVATE KEY",
		Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey),
	})
	return tls.X509KeyPair(certPEM, certPrivKeyPEM)
}

func equal(left, right string) bool {

	return subtle.ConstantTimeCompare([]byte(left), []byte(right)) == 1
}
Output:

authenticated request from Aladdin
client received response

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Errorf

func Errorf(template string, args ...any) *connect.Error

Errorf is a convenience function that returns an error coded with connect.CodeUnauthenticated.

func GetInfo

func GetInfo(ctx context.Context) any

GetInfo retrieves authentication information, if any, from the request context.

func SetInfo

func SetInfo(ctx context.Context, info any) context.Context

SetInfo attaches authentication information to the context. It's often useful in tests.

AuthFunc implementations do not need to call SetInfo explicitly. Any returned authentication information is automatically added to the context by Middleware.

func WithoutInfo

func WithoutInfo(ctx context.Context) context.Context

WithoutInfo strips the authentication information, if any, from the provided context.

Types

type AuthFunc

type AuthFunc func(ctx context.Context, req Request) (any, error)

An AuthFunc authenticates an RPC. The function must return an error if the request cannot be authenticated. The error is typically produced with Errorf, but any error will do.

If requests are successfully authenticated, the authentication function may return some information about the authenticated caller (or nil). If non-nil, the information is automatically attached to the context using SetInfo.

Implementations must be safe to call concurrently.

type Middleware

type Middleware struct {
	// contains filtered or unexported fields
}

Middleware is server-side HTTP middleware that authenticates RPC requests. In addition to rejecting unauthenticated requests, it can optionally attach arbitrary information about the authenticated identity to the context.

Middleware operates at a lower level than Connect interceptors, so the server doesn't decompress and unmarshal the request until the caller has been authenticated.

func NewMiddleware

func NewMiddleware(auth AuthFunc, opts ...connect.HandlerOption) *Middleware

NewMiddleware constructs HTTP middleware using the supplied authentication function. If authentication succeeds, the authentication information (if any) will be attached to the context. Subsequent HTTP middleware, all RPC interceptors, and application code may access it with GetInfo.

In order to properly marshal errors, applications must pass NewMiddleware the same handler options used when constructing Connect handlers.

func (*Middleware) Wrap

func (m *Middleware) Wrap(handler http.Handler) http.Handler

Wrap returns an HTTP handler that authenticates requests before forwarding them to handler.

type Request

type Request struct {
	// contains filtered or unexported fields
}

Request describes a single RPC invocation.

func (Request) BasicAuth

func (r Request) BasicAuth() (username string, password string, ok bool)

BasicAuth returns the username and password provided in the request's Authorization header, if any.

func (Request) ClientAddr

func (r Request) ClientAddr() string

ClientAddr returns the client address, in IP:port format.

func (Request) Cookie

func (r Request) Cookie(name string) (*http.Cookie, error)

Cookie returns the named cookie provided in the request or http.ErrNoCookie if not found. If multiple cookies match the given name, only one cookie will be returned.

func (Request) Cookies

func (r Request) Cookies() []*http.Cookie

Cookies parses and returns the HTTP cookies sent with the request, if any.

func (Request) Header

func (r Request) Header() http.Header

Header returns the HTTP request headers.

func (Request) Procedure

func (r Request) Procedure() string

Procedure returns the RPC procedure name, in the form "/service/method". If the request path does not contain a procedure name, the entire path is returned.

func (Request) Protocol

func (r Request) Protocol() string

Protocol returns the RPC protocol. It is one of connect.ProtocolConnect, connect.ProtocolGRPC, or connect.ProtocolGRPCWeb.

func (Request) TLS

func (r Request) TLS() *tls.ConnectionState

TLS returns the TLS connection state, if any. It may be nil if the connection is not using TLS.

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL