├── .github ├── dependabot.yml ├── release.yml └── workflows │ └── ci.yaml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── auth.go ├── auth_test.go ├── example_test.go ├── go.mod ├── go.sum └── staticcheck.conf /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | reviewers: [akshayjshah] 8 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | authors: 6 | - dependabot 7 | categories: 8 | - title: Enhancements 9 | labels: 10 | - enhancement 11 | - title: Bugfixes 12 | labels: 13 | - bug 14 | - title: Other Changes 15 | labels: 16 | - "*" 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: [main] 5 | tags: ['v*'] 6 | pull_request: 7 | branches: [main] 8 | schedule: 9 | - cron: '15 22 * * *' 10 | workflow_dispatch: {} # support manual runs 11 | permissions: read-all 12 | jobs: 13 | ci: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | go-version: [oldstable, stable] 18 | steps: 19 | - name: Checkout Code 20 | uses: actions/checkout@v4 21 | - name: Install Go 22 | uses: actions/setup-go@v5 23 | with: 24 | go-version: ${{ matrix.go-version }} 25 | - name: Test 26 | run: make test 27 | - name: Lint 28 | # Often, lint & gofmt guidelines depend on the Go version. To prevent 29 | # conflicting guidance, run only on the most recent supported version. 30 | if: matrix.go-version == 'stable' 31 | run: make lint 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .tmp/ 2 | cover*.out 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Akshay Shah 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # See https://tech.davis-hansson.com/p/make/ 2 | SHELL := bash 3 | .DELETE_ON_ERROR: 4 | .SHELLFLAGS := -eu -o pipefail -c 5 | .DEFAULT_GOAL := all 6 | MAKEFLAGS += --warn-undefined-variables 7 | MAKEFLAGS += --no-builtin-rules 8 | MAKEFLAGS += --no-print-directory 9 | GO ?= go 10 | BIN := .tmp/bin 11 | 12 | .PHONY: help 13 | help: ## Describe useful make targets 14 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "%-30s %s\n", $$1, $$2}' 15 | 16 | .PHONY: all 17 | all: ## Build, test, and lint (default) 18 | $(MAKE) test 19 | $(MAKE) lint 20 | 21 | .PHONY: test 22 | test: build ## Run unit tests 23 | $(GO) test -vet=off -race -cover ./... 24 | 25 | .PHONY: build 26 | build: ## Build all packages 27 | $(GO) build ./... 28 | 29 | .PHONY: lint 30 | lint: $(BIN)/gofmt $(BIN)/staticcheck ## Lint Go 31 | test -z "$$($(BIN)/gofmt -s -l . | tee /dev/stderr)" 32 | $(GO) vet ./... 33 | $(BIN)/staticcheck ./... 34 | 35 | .PHONY: lintfix 36 | lintfix: $(BIN)/gofmt ## Automatically fix some lint errors 37 | $(BIN)/gofmt -s -w . 38 | 39 | .PHONY: upgrade 40 | upgrade: ## Upgrade dependencies 41 | go get -u -t ./... && go mod tidy -v 42 | 43 | .PHONY: clean 44 | clean: ## Remove intermediate artifacts 45 | rm -rf .tmp 46 | 47 | $(BIN)/gofmt: 48 | @mkdir -p $(@D) 49 | $(GO) build -o $(@) cmd/gofmt 50 | 51 | $(BIN)/staticcheck: 52 | @mkdir -p $(@D) 53 | GOBIN=$(abspath $(@D)) $(GO) install honnef.co/go/tools/cmd/staticcheck@latest 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | connectauth (deprecated) 2 | ======================== 3 | 4 | [![Build](https://github.com/akshayjshah/connectauth/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/akshayjshah/connectauth/actions/workflows/ci.yaml) 5 | [![Report Card](https://goreportcard.com/badge/go.akshayshah.org/connectauth)](https://goreportcard.com/report/go.akshayshah.org/connectauth) 6 | [![GoDoc](https://pkg.go.dev/badge/go.akshayshah.org/connectauth.svg)](https://pkg.go.dev/go.akshayshah.org/connectauth) 7 | 8 | > [!CAUTION] 9 | > A variant of this package is now officially part of the Connect project! **Use 10 | > [connectrpc.com/authn][authn] instead.** 11 | 12 | `connectauth` provides flexible authentication for [Connect][connect] 13 | servers written in Go. It works with any authentication function, covers both 14 | unary and streaming RPCs, and runs efficiently. 15 | 16 | ## Installation 17 | 18 | ``` 19 | go get go.akshayshah.org/connectauth 20 | ``` 21 | 22 | ## Usage 23 | 24 | ```go 25 | package main 26 | 27 | import ( 28 | "context" 29 | "fmt" 30 | "net/http" 31 | 32 | "connectrpc.com/connect" 33 | "go.akshayshah.org/connectauth" 34 | ) 35 | 36 | // Our authentication logic is just a function. 37 | func authenticate(ctx context.Context, req *connectauth.Request) (any, error) { 38 | const passphrase = "open-sesame" 39 | if req.Header.Get("Authorization") != "Bearer "+passphrase { 40 | // If authentication fails, we return an error. connectauth.Errorf is a 41 | // convenient shortcut to produce an error coded with 42 | // connect.CodeUnauthenticated. 43 | return nil, connectauth.Errorf("try %q as a bearer token instead", passphrase) 44 | } 45 | // Once we've authenticated the request, we can return some information about 46 | // the client. That information gets attached to the context passed to 47 | // subsequent interceptors and our service implementation. 48 | return "Ali Baba", nil 49 | } 50 | 51 | // This constructor would normally be generated by protoc-gen-connect-go. For 52 | // this example, we'll use a small stub. 53 | func NewHelloServiceHandler(svc any, opts ...connect.HandlerOption) (string, http.Handler) { 54 | return "/hello.v1/Hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 55 | // Service implementations can retrieve information about the authenticated 56 | // caller from the context. 57 | identity := connectauth.GetInfo(r.Context()) 58 | fmt.Fprintf(w, "Hello, %v!", identity) 59 | }) 60 | } 61 | 62 | func main() { 63 | mux := http.NewServeMux() 64 | mux.Handle(NewHelloServiceHandler(struct{}{})) 65 | // Before starting the HTTP server, wrap the whole mux in our authenticating 66 | // middleware. 67 | middleware := connectauth.NewMiddleware(authenticate) 68 | http.ListenAndServe("localhost:8080", middleware.Wrap(mux)) 69 | } 70 | ``` 71 | 72 | ## Status: Deprecated 73 | 74 | This module is currently _deprecated_ in favor of 75 | [connectrpc.com/authn][authn]. This package isn't going anywhere, but users 76 | should migrate — the APIs are very similar! 77 | 78 | ## Legal 79 | 80 | Offered under the [MIT license][license]. 81 | 82 | [authn]: https://github.com/connectrpc/authn-go 83 | [connect]: https://github.com/connectrpc/connect-go 84 | [go-support-policy]: https://golang.org/doc/devel/release#policy 85 | [license]: https://github.com/akshayjshah/connectauth/blob/main/LICENSE 86 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | // Package connectauth provides flexible authentication middleware for 2 | // [connect]. 3 | package connectauth 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "net/http" 9 | "strings" 10 | 11 | "connectrpc.com/connect" 12 | ) 13 | 14 | type key int 15 | 16 | const infoKey key = iota 17 | 18 | // An AuthFunc authenticates an RPC. The function must return an error if the 19 | // request cannot be authenticated. The error is typically produced with 20 | // [Errorf], but any error will do. 21 | // 22 | // If requests are successfully authenticated, the authentication function may 23 | // return some information about the authenticated caller (or nil). 24 | // Authentication functions must be safe to call concurrently. 25 | type AuthFunc = func(context.Context, *Request) (any, error) 26 | 27 | // SetInfo attaches authentication information to the context. It's often 28 | // useful in tests. 29 | func SetInfo(ctx context.Context, info any) context.Context { 30 | if info == nil { 31 | return ctx 32 | } 33 | return context.WithValue(ctx, infoKey, info) 34 | } 35 | 36 | // GetInfo retrieves authentication information, if any, from the request 37 | // context. 38 | func GetInfo(ctx context.Context) any { 39 | return ctx.Value(infoKey) 40 | } 41 | 42 | // WithoutInfo strips the authentication information, if any, from the provided 43 | // context. 44 | func WithoutInfo(ctx context.Context) context.Context { 45 | return context.WithValue(ctx, infoKey, nil) 46 | } 47 | 48 | // Errorf is a convenience function that returns an error coded with 49 | // [connect.CodeUnauthenticated]. 50 | func Errorf(template string, args ...any) *connect.Error { 51 | return connect.NewError(connect.CodeUnauthenticated, fmt.Errorf(template, args...)) 52 | } 53 | 54 | // Request describes a single RPC invocation. 55 | type Request struct { 56 | Procedure string // for example, "/acme.foo.v1.FooService/Bar" 57 | ClientAddr string // client address, in IP:port format 58 | Protocol string // connect.ProtocolConnect, connect.ProtocolGRPC, or connect.ProtocolGRPCWeb 59 | Header http.Header 60 | } 61 | 62 | // Middleware is server-side HTTP middleware that authenticates RPC requests. 63 | // In addition to rejecting unauthenticated requests, it can optionally attach 64 | // arbitrary information to the context of authenticated requests. Any non-RPC 65 | // requests (as determined by their Content-Type) are forwarded directly to the 66 | // wrapped handler without authentication. 67 | // 68 | // Middleware operates at a lower level than [Interceptor]. For most 69 | // applications, Middleware is preferable because it defers decompressing and 70 | // unmarshaling the request until after the caller has been authenticated. 71 | type Middleware struct { 72 | auth AuthFunc 73 | errW *connect.ErrorWriter 74 | } 75 | 76 | // NewMiddleware constructs HTTP middleware using the supplied authentication 77 | // function. If authentication succeeds, the authentication information (if 78 | // any) will be attached to the context. Subsequent HTTP middleware, all RPC 79 | // interceptors, and application code may access it with [GetInfo]. 80 | // 81 | // In order to properly identify RPC requests and marshal errors, applications 82 | // must pass NewMiddleware the same handler options used when constructing 83 | // Connect handlers. 84 | func NewMiddleware(auth AuthFunc, opts ...connect.HandlerOption) *Middleware { 85 | return &Middleware{ 86 | auth: auth, 87 | errW: connect.NewErrorWriter(opts...), 88 | } 89 | } 90 | 91 | // Wrap decorates an HTTP handler with authentication logic. 92 | func (m *Middleware) Wrap(next http.Handler) http.Handler { 93 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 94 | if !m.errW.IsSupported(r) { 95 | next.ServeHTTP(w, r) 96 | return 97 | } 98 | ctx := r.Context() 99 | info, err := m.auth(ctx, &Request{ 100 | Procedure: procedureFromHTTP(r), 101 | ClientAddr: r.RemoteAddr, 102 | Protocol: protocolFromHTTP(r), 103 | Header: r.Header, 104 | }) 105 | if err != nil { 106 | m.errW.Write(w, r, err) 107 | return 108 | } 109 | if info != nil { 110 | r = r.WithContext(SetInfo(ctx, info)) 111 | } 112 | next.ServeHTTP(w, r) 113 | }) 114 | } 115 | 116 | // Interceptor is a server-side authentication interceptor. In addition to 117 | // rejecting unauthenticated requests, it can optionally attach arbitrary 118 | // information to the context of authenticated requests. 119 | // 120 | // Because RPC interceptors run after the request has already been decompressed 121 | // and unmarshaled, it's inefficient (and potentially dangerous) to rely on 122 | // interceptors for authentication. Most applications should use [Middleware] 123 | // instead, unless they: 124 | // - Mount Connect HTTP handlers on routes that don't end in the procedure 125 | // name (e.g., "/user.v1/GetUser"). This is unusual, since it breaks 126 | // generated Connect and gRPC clients. 127 | // - Use authentication logic that relies on other interceptors (e.g., 128 | // authenticating requests relies on a struct attached to the context by a 129 | // previous interceptor). 130 | // 131 | // Attach interceptors to your RPC handlers using [connect.WithInterceptors]. 132 | type Interceptor struct { 133 | auth func(context.Context, *Request) (any, error) 134 | } 135 | 136 | // NewInterceptor constructs a Connect interceptor using the supplied 137 | // authentication function. If authentication succeeds, the authentication 138 | // information (if any) will be attached to the context. Subsequent 139 | // interceptors and application code may access it with [GetInfo]. 140 | // 141 | // Most applications should use [Middleware] instead. 142 | func NewInterceptor(auth AuthFunc) *Interceptor { 143 | return &Interceptor{auth} 144 | } 145 | 146 | // WrapUnary implements connect.Interceptor. 147 | func (i *Interceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { 148 | return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { 149 | spec := req.Spec() 150 | peer := req.Peer() 151 | info, err := i.auth(ctx, &Request{ 152 | Procedure: spec.Procedure, 153 | ClientAddr: peer.Addr, 154 | Protocol: peer.Protocol, 155 | Header: req.Header(), 156 | }) 157 | if err != nil { 158 | return nil, err 159 | } 160 | return next(SetInfo(ctx, info), req) 161 | } 162 | } 163 | 164 | // WrapStreamingClient implements connect.Interceptor with a no-op. 165 | func (i *Interceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { 166 | return next 167 | } 168 | 169 | // WrapStreamingHandler implements connect.Interceptor. 170 | func (i *Interceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { 171 | return func(ctx context.Context, conn connect.StreamingHandlerConn) error { 172 | spec := conn.Spec() 173 | peer := conn.Peer() 174 | info, err := i.auth(ctx, &Request{ 175 | Procedure: spec.Procedure, 176 | ClientAddr: peer.Addr, 177 | Protocol: peer.Protocol, 178 | Header: conn.RequestHeader(), 179 | }) 180 | if err != nil { 181 | return err 182 | } 183 | return next(SetInfo(ctx, info), conn) 184 | } 185 | } 186 | 187 | func procedureFromHTTP(r *http.Request) string { 188 | path := strings.TrimSuffix(r.URL.Path, "/") 189 | ultimate := strings.LastIndex(path, "/") 190 | if ultimate < 0 { 191 | return "" 192 | } 193 | penultimate := strings.LastIndex(path[:ultimate], "/") 194 | if penultimate < 0 { 195 | return "" 196 | } 197 | procedure := path[penultimate:] 198 | if len(procedure) < 4 { // two slashes + service + method 199 | return "" 200 | } 201 | return procedure 202 | } 203 | 204 | func protocolFromHTTP(r *http.Request) string { 205 | ct := r.Header.Get("Content-Type") 206 | switch { 207 | case strings.HasPrefix(ct, "application/grpc-web"): 208 | return connect.ProtocolGRPCWeb 209 | case strings.HasPrefix(ct, "application/grpc"): 210 | return connect.ProtocolGRPC 211 | default: 212 | return connect.ProtocolConnect 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /auth_test.go: -------------------------------------------------------------------------------- 1 | package connectauth 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/http" 7 | "strings" 8 | "testing" 9 | 10 | "connectrpc.com/connect" 11 | "go.akshayshah.org/attest" 12 | "go.akshayshah.org/memhttp/memhttptest" 13 | "google.golang.org/protobuf/types/known/emptypb" 14 | ) 15 | 16 | const ( 17 | hero = "Ali Baba" 18 | passphrase = "opensesame" 19 | ) 20 | 21 | func assertInfo(tb testing.TB, ctx context.Context) { 22 | tb.Helper() 23 | info := GetInfo(ctx) 24 | if info == nil { 25 | tb.Fatal("no authentication info") 26 | } 27 | name, ok := info.(string) 28 | attest.True(tb, ok, attest.Sprintf("got info of type %T, expected string", info)) 29 | attest.Equal(tb, name, hero) 30 | if id := GetInfo(WithoutInfo(ctx)); id != nil { 31 | tb.Fatalf("got info %v after WithoutInfo", id) 32 | } 33 | } 34 | 35 | func authenticate(ctx context.Context, r *Request) (any, error) { 36 | parts := strings.SplitN(r.Header.Get("Authorization"), " ", 2) 37 | if len(parts) < 2 || parts[0] != "Bearer" { 38 | err := Errorf("expected Bearer authentication scheme") 39 | err.Meta().Set("WWW-Authenticate", "Bearer") 40 | return nil, err 41 | } 42 | if tok := parts[1]; tok != passphrase { 43 | return nil, Errorf("%q is not the magic passphrase", tok) 44 | } 45 | return hero, nil 46 | } 47 | 48 | func TestInterceptor(t *testing.T) { 49 | auth := NewInterceptor(authenticate) 50 | mux := http.NewServeMux() 51 | mux.Handle("/unary", connect.NewUnaryHandler( 52 | "unary", 53 | func(ctx context.Context, _ *connect.Request[emptypb.Empty]) (*connect.Response[emptypb.Empty], error) { 54 | assertInfo(t, ctx) 55 | return connect.NewResponse(&emptypb.Empty{}), nil 56 | }, 57 | connect.WithInterceptors(auth), 58 | )) 59 | mux.Handle("/clientstream", connect.NewClientStreamHandler( 60 | "clientstream", 61 | func(ctx context.Context, _ *connect.ClientStream[emptypb.Empty]) (*connect.Response[emptypb.Empty], error) { 62 | assertInfo(t, ctx) 63 | return connect.NewResponse(&emptypb.Empty{}), nil 64 | }, 65 | connect.WithInterceptors(auth), 66 | )) 67 | srv := memhttptest.New(t, mux) 68 | 69 | t.Run("unary", func(t *testing.T) { 70 | client := connect.NewClient[emptypb.Empty, emptypb.Empty]( 71 | srv.Client(), 72 | srv.URL()+"/unary", 73 | ) 74 | req := connect.NewRequest(&emptypb.Empty{}) 75 | _, err := client.CallUnary(context.Background(), req) 76 | attest.Error(t, err) 77 | attest.Equal(t, connect.CodeOf(err), connect.CodeUnauthenticated) 78 | req.Header().Set("Authorization", "Bearer "+passphrase) 79 | _, err = client.CallUnary(context.Background(), req) 80 | attest.Ok(t, err) 81 | }) 82 | 83 | t.Run("streaming", func(t *testing.T) { 84 | client := connect.NewClient[emptypb.Empty, emptypb.Empty]( 85 | srv.Client(), 86 | srv.URL()+"/clientstream", 87 | ) 88 | stream := client.CallClientStream(context.Background()) 89 | stream.Send(nil) 90 | _, err := stream.CloseAndReceive() 91 | attest.Error(t, err) 92 | attest.Equal(t, connect.CodeOf(err), connect.CodeUnauthenticated) 93 | 94 | stream = client.CallClientStream(context.Background()) 95 | stream.RequestHeader().Set("Authorization", "Bearer "+passphrase) 96 | stream.Send(nil) 97 | _, err = stream.CloseAndReceive() 98 | attest.Ok(t, err) 99 | }) 100 | } 101 | 102 | func TestMiddleware(t *testing.T) { 103 | mux := http.NewServeMux() 104 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 105 | if r.Header.Get("Check-Info") != "" { 106 | assertInfo(t, r.Context()) 107 | } 108 | io.WriteString(w, "ok") 109 | }) 110 | srv := memhttptest.New(t, NewMiddleware(authenticate).Wrap(mux)) 111 | 112 | assertResponse := func(headers http.Header, expectCode int) { 113 | req, err := http.NewRequest( 114 | http.MethodPost, 115 | srv.URL()+"/empty.v1/GetEmpty", 116 | strings.NewReader("{}"), 117 | ) 118 | attest.Ok(t, err) 119 | for k, vals := range headers { 120 | for _, v := range vals { 121 | req.Header.Add(k, v) 122 | } 123 | } 124 | res, err := srv.Client().Do(req) 125 | attest.Ok(t, err) 126 | attest.Equal(t, res.StatusCode, expectCode) 127 | } 128 | // Middleware should ignore non-RPC requests. 129 | assertResponse(http.Header{}, 200) 130 | // RPCs without the right bearer token should be rejected. 131 | assertResponse( 132 | http.Header{"Content-Type": []string{"application/json"}}, 133 | http.StatusUnauthorized, 134 | ) 135 | // RPCs with the right token should be allowed. 136 | assertResponse( 137 | http.Header{ 138 | "Content-Type": []string{"application/json"}, 139 | "Authorization": []string{"Bearer " + passphrase}, 140 | "Check-Info": []string{"1"}, // verify that auth info is attached to context 141 | }, 142 | http.StatusOK, 143 | ) 144 | } 145 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package connectauth_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "connectrpc.com/connect" 9 | "go.akshayshah.org/connectauth" 10 | ) 11 | 12 | // Our authentication logic is just a function. 13 | func authenticate(ctx context.Context, req *connectauth.Request) (any, error) { 14 | const passphrase = "open-sesame" 15 | if req.Header.Get("Authorization") != "Bearer "+passphrase { 16 | // If authentication fails, we return an error. connectauth.Errorf is a 17 | // convenient shortcut to produce an error coded with 18 | // connect.CodeUnauthenticated. 19 | return nil, connectauth.Errorf("try %q as a bearer token instead", passphrase) 20 | } 21 | // Once we've authenticated the request, we can return some information about 22 | // the client. That information gets attached to the context passed to 23 | // subsequent interceptors and our service implementation. 24 | return "Ali Baba", nil 25 | } 26 | 27 | // This constructor would normally be generated by protoc-gen-connect-go. For 28 | // this example, we'll use a small stub. 29 | func NewHelloServiceHandler(svc any, opts ...connect.HandlerOption) (string, http.Handler) { 30 | return "/hello.v1/Hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 31 | // Service implementations can retrieve information about the authenticated 32 | // caller from the context. 33 | identity := connectauth.GetInfo(r.Context()) 34 | fmt.Fprintf(w, "Hello, %v!", identity) 35 | }) 36 | } 37 | 38 | func Example() { 39 | mux := http.NewServeMux() 40 | mux.Handle(NewHelloServiceHandler(struct{}{})) 41 | // Before starting the HTTP server, wrap the whole mux in our authenticating 42 | // middleware. 43 | middleware := connectauth.NewMiddleware(authenticate) 44 | http.ListenAndServe("localhost:8080", middleware.Wrap(mux)) 45 | } 46 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go.akshayshah.org/connectauth 2 | 3 | go 1.19 4 | 5 | require ( 6 | connectrpc.com/connect v1.11.0 7 | go.akshayshah.org/attest v1.0.2 8 | go.akshayshah.org/memhttp v0.1.0 9 | google.golang.org/protobuf v1.31.0 10 | ) 11 | 12 | require github.com/google/go-cmp v0.5.9 // indirect 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | connectrpc.com/connect v1.11.0 h1:Av2KQXxSaX4vjqhf5Cl01SX4dqYADQ38eBtr84JSUBk= 2 | connectrpc.com/connect v1.11.0/go.mod h1:3AGaO6RRGMx5IKFfqbe3hvK1NqLosFNP2BxDYTPmNPo= 3 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 4 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 5 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 6 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 7 | go.akshayshah.org/attest v1.0.2 h1:qOv9PXCG2mwnph3g0I3yZj0rLAwLyUITs8nhxP+wS44= 8 | go.akshayshah.org/attest v1.0.2/go.mod h1:PnWzcW5j9dkyGwTlBmUsYpPnHG0AUPrs1RQ+HrldWO0= 9 | go.akshayshah.org/memhttp v0.1.0 h1:Enf7JeZnm+A8iRur0FYvs4ZjWa1VVMc2gG4EirG+aNE= 10 | go.akshayshah.org/memhttp v0.1.0/go.mod h1:Q1A5oqQfj2tZFRzpw0HRmmZAMzw8f3AxqOe55Afn1d8= 11 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 12 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 13 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= 14 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 15 | -------------------------------------------------------------------------------- /staticcheck.conf: -------------------------------------------------------------------------------- 1 | checks = ["all"] 2 | --------------------------------------------------------------------------------