Skip to content

Commit

Permalink
Prep for OSS (#3)
Browse files Browse the repository at this point in the history
* Rename to connectrpc.com/authn
* Generate code as part of build
* Reword GoDoc, remove ref to deleted type
* Remove Buf-specific GitHub actions
* Add contribution guide
* Add SECURITY.md
* Focus examples on authentication
* Add cookie support
* Flesh out README
* Separate protobuf messages with empty lines
* Make auth logic more prominent in README example
* Fix broken README link
* Update examples_test.go
* Address review feedback
* Authenticate non-RPC requests
* Use crypto/subtle to mitigate timing attacks
* Upgrade to connect 1.14.0
* Rely on new ErrorWriter fallback logic
* Fix GoDoc rendering for README link

---------

Co-authored-by: Edward McFarlane <[email protected]>
  • Loading branch information
akshayjshah and emcfarlane authored Jan 4, 2024
1 parent 37355e0 commit 3817558
Show file tree
Hide file tree
Showing 15 changed files with 426 additions and 268 deletions.
73 changes: 73 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
Contributing
============

We'd love your help making this package better!

If you'd like to add new exported APIs, please [open an issue][open-issue]
describing your proposal &mdash; discussing API changes ahead of time makes
pull request review much smoother. In your issue, pull request, and any other
communications, please remember to treat your fellow contributors with
respect!

Note that you'll need to sign the [Contributor License Agreement][cla] before
we can accept any of your contributions. If necessary, a bot will remind you to
accept the CLA when you open your pull request.

## Setup

[Fork][fork], then clone the repository:

```
mkdir -p $GOPATH/src/connectrpc.com
cd $GOPATH/src/connectrpc.com
git clone [email protected]:your_github_username/authn-go.git authn
cd authn
git remote add upstream https://github.com/connectrpc/authn-go.git
git fetch upstream
```

Make sure that the tests and the linters pass (you'll need `bash` and the
latest stable Go release installed):

```
make
```

## Making Changes

Start by creating a new branch for your changes:

```
cd $GOPATH/src/connectrpc.com/authn
git checkout main
git fetch upstream
git rebase upstream/main
git checkout -b cool_new_feature
```

Make your changes, then ensure that `make` still passes. (You can use the
standard `go build ./...` and `go test ./...` while you're coding.) When you're
satisfied with your changes, push them to your fork.

```
git commit -a
git push origin cool_new_feature
```

Then use the GitHub UI to open a pull request.

At this point, you're waiting on us to review your changes. We *try* to respond
to issues and pull requests within a few business days, and we may suggest some
improvements or alternatives. Once your changes are approved, one of the
project maintainers will merge them.

We're much more likely to approve your changes if you:

* Add tests for new functionality.
* Write a [good commit message][commit-message].
* Maintain backward compatibility.

[fork]: https://github.com/connectrpc/authn-go/fork
[open-issue]: https://github.com/connectrpc/authn-go/issues/new
[cla]: https://cla-assistant.io/connectrpc/authn-go
[commit-message]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
12 changes: 0 additions & 12 deletions .github/workflows/emergency-review-bypass.yaml

This file was deleted.

13 changes: 0 additions & 13 deletions .github/workflows/notify-approval-bypass.yaml

This file was deleted.

26 changes: 22 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ build: generate ## Build all packages
go build ./...

.PHONY: generate
generate: $(BIN)/license-header ## Regenerate code and licenses
generate: $(BIN)/buf $(BIN)/protoc-gen-go $(BIN)/protoc-gen-connect-go $(BIN)/license-header ## Regenerate code and licenses
rm -rf internal/gen
PATH="$(abspath $(BIN))" buf generate
license-header \
--license-type apache \
--copyright-holder "Buf Technologies, Inc." \
Expand All @@ -45,19 +47,21 @@ generate: $(BIN)/license-header ## Regenerate code and licenses
lint: $(BIN)/golangci-lint ## Lint
go vet ./...
golangci-lint run --modules-download-mode=readonly --timeout=3m0s
buf lint
buf format -d --exit-code

.PHONY: lintfix
lintfix: $(BIN)/golangci-lint ## Automatically fix some lint errors
golangci-lint run --fix --modules-download-mode=readonly --timeout=3m0s
buf format -w

.PHONY: install
install: ## Install all binaries
go install ./...

.PHONY: upgrade
upgrade: ## Upgrade dependencies
go get -u -t ./...
go mod tidy -v
go get -u -t ./... && go mod tidy -v

.PHONY: checkgenerate
checkgenerate:
Expand All @@ -66,8 +70,22 @@ checkgenerate:

$(BIN)/license-header: Makefile
@mkdir -p $(@D)
go install github.com/bufbuild/buf/private/pkg/licenseheader/cmd/license-header@v1.26.1
go install github.com/bufbuild/buf/private/pkg/licenseheader/cmd/license-header@v1.27.2

$(BIN)/golangci-lint: Makefile
@mkdir -p $(@D)
go install github.com/golangci/golangci-lint/cmd/[email protected]

$(BIN)/buf: Makefile
@mkdir -p $(@D)
go install github.com/bufbuild/buf/cmd/[email protected]

$(BIN)/protoc-gen-go: Makefile go.mod
@mkdir -p $(@D)
@# The version of protoc-gen-go is determined by the version in go.mod
go install google.golang.org/protobuf/cmd/protoc-gen-go

$(BIN)/protoc-gen-connect-go: Makefile go.mod
@mkdir -p $(@D)
@# The version of protoc-gen-connect-go is determined by the version in go.mod
go install connectrpc.com/connect/cmd/protoc-gen-connect-go
108 changes: 102 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,110 @@
authn-go
===============
authn
=====
[![Build](https://github.com/connectrpc/authn-go/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/connectrpc/authn-go/actions/workflows/ci.yaml)
[![Report Card](https://goreportcard.com/badge/connectrpc.com/authn)](https://goreportcard.com/report/connectrpc.com/authn)
[![GoDoc](https://pkg.go.dev/badge/connectrpc.com/authn.svg)](https://pkg.go.dev/connectrpc.com/authn)
[![Slack](https://img.shields.io/badge/slack-buf-%23e01563)][slack]

Authn provides authentication middleware for [connect](https://connectrpc.com/). It is designed to work with any authentication scheme, including HTTP Basic Authentication, OAuth2, and custom schemes. It covers both Unary and Streaming RPCs and works with both gRPC and Connect protocols.
`connectrpc.com/authn` provides authentication middleware for
[Connect](https://connectrpc.com/). 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.

## Status: Alpha
For more on Connect, see the [announcement blog post][blog], the documentation
on [connectrpc.com][docs] (especially the [Getting Started] guide for Go), the
[demo service][examples-go], or the [protocol specification][protocol].

This project is currently in alpha. The API should be considered unstable and likely to change.
## A small example

Curious what all this looks like in practice? From a [Protobuf
schema](internal/proto/authn/ping/v1/ping.proto), we generate [a small RPC
package](internal/gen/authn/ping/v1/pingv1connect/ping.connect.go). Using that
package, we can build a server and wrap it with some basic authentication:

```go
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][godoc] 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 &mdash; 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][go-support-policy].

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

## Legal

Offered under the [Apache 2 license][license].

[license]: https://github.com/bufbuild/authn-go/blob/main/LICENSE
[Getting Started]: https://connectrpc.com/docs/go/getting-started
[blog]: https://buf.build/blog/connect-a-better-grpc
[conformance]: https://github.com/connectrpc/conformance
[connect-es]: https://github.com/connectrpc/connect-es
[connect-go]: https://github.com/connectrpc/connect-go
[cors]: https://github.com/connectrpc/cors-go
[docs]: https://connectrpc.com
[examples-go]: https://github.com/connectrpc/examples-go
[go-support-policy]: https://golang.org/doc/devel/release#policy
[godoc]: https://pkg.go.dev/connectrpc.com/authn
[grpchealth]: https://github.com/connectrpc/grpchealth-go
[grpcreflect]: https://github.com/connectrpc/grpcreflect-go
[license]: https://github.com/connectrpc/authn-go/blob/main/LICENSE
[protocol]: https://connectrpc.com/docs/protocol
[slack]: https://buf.build/links/slack
5 changes: 5 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Security Policy
===============

This project follows the [Connect security policy and reporting
process](https://connectrpc.com/docs/governance/security).
48 changes: 29 additions & 19 deletions authn.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,18 @@ const infoKey key = iota
// [Errorf], but any error will do.
//
// If requests are successfully authenticated, the authentication function may
// return some information about the authenticated caller (or nil).
// 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 AuthFunc func(ctx context.Context, req Request) (any, error)

// 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 SetInfo(ctx context.Context, info any) context.Context {
if info == nil {
return ctx
Expand Down Expand Up @@ -76,6 +82,18 @@ func (r Request) BasicAuth() (username string, password string, ok bool) {
return r.request.BasicAuth()
}

// Cookies parses and returns the HTTP cookies sent with the request, if any.
func (r Request) Cookies() []*http.Cookie {
return r.request.Cookies()
}

// 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 (r Request) Cookie(name string) (*http.Cookie, error) {
return r.request.Cookie(name)
}

// 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.
Expand All @@ -101,8 +119,8 @@ func (r Request) ClientAddr() string {
return r.request.RemoteAddr
}

// Protocol returns the RPC protocol. It is one of connect.ProtocolConnect,
// connect.ProtocolGRPC, or connect.ProtocolGRPCWeb.
// Protocol returns the RPC protocol. It is one of [connect.ProtocolConnect],
// [connect.ProtocolGRPC], or [connect.ProtocolGRPCWeb].
func (r Request) Protocol() string {
ct := r.request.Header.Get("Content-Type")
switch {
Expand All @@ -128,13 +146,11 @@ func (r Request) TLS() *tls.ConnectionState {

// Middleware is server-side HTTP middleware that authenticates RPC requests.
// In addition to rejecting unauthenticated requests, it can optionally attach
// arbitrary information to the context of authenticated requests. Any non-RPC
// requests (as determined by their Content-Type) are forwarded directly to the
// wrapped handler without authentication.
// arbitrary information about the authenticated identity to the context.
//
// Middleware operates at a lower level than [Interceptor]. For most
// applications, Middleware is preferable because it defers decompressing and
// unmarshaling the request until after the caller has been authenticated.
// 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.
type Middleware struct {
auth AuthFunc
errW *connect.ErrorWriter
Expand All @@ -145,25 +161,19 @@ type Middleware struct {
// 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 identify RPC requests and marshal errors, applications
// must pass NewMiddleware the same handler options used when constructing
// Connect handlers.
// In order to properly marshal errors, applications must pass NewMiddleware
// the same handler options used when constructing Connect handlers.
func NewMiddleware(auth AuthFunc, opts ...connect.HandlerOption) *Middleware {
return &Middleware{
auth: auth,
errW: connect.NewErrorWriter(opts...),
}
}

// Wrap returns an HTTP handler that authenticates RPC requests before
// forwarding them to handler. If handler is not an RPC request, it is forwarded
// directly, without authentication.
// Wrap returns an HTTP handler that authenticates requests before forwarding
// them to handler.
func (m *Middleware) Wrap(handler http.Handler) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if !m.errW.IsSupported(request) {
handler.ServeHTTP(writer, request)
return // not an RPC request
}
ctx := request.Context()
info, err := m.auth(ctx, Request{request: request})
if err != nil {
Expand Down
Loading

0 comments on commit 3817558

Please sign in to comment.