wip: add token & session forwarding

Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
This commit is contained in:
Adphi 2024-10-10 15:54:20 +02:00
parent abe69f1c80
commit e5d84975a2
Signed by: adphi
GPG Key ID: 46BE4062DB2397FF
4 changed files with 149 additions and 1 deletions

24
auth/context.go Normal file
View File

@ -0,0 +1,24 @@
package auth
import (
"context"
)
type authKey struct{}
func Context[T any](ctx context.Context, auth T) context.Context {
return context.WithValue(ctx, authKey{}, auth)
}
func FromContext[T any](ctx context.Context) (T, bool) {
auth, ok := ctx.Value(authKey{}).(T)
return auth, ok
}
func MustTokenFromContext[T any](ctx context.Context) T {
auth, ok := FromContext[T](ctx)
if !ok {
panic("no auth in context")
}
return auth
}

116
auth/session.go Normal file
View File

@ -0,0 +1,116 @@
package auth
import (
"context"
"fmt"
"net/http"
"github.com/gorilla/securecookie"
"github.com/gorilla/sessions"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/proto"
)
// TokenResponse is a proto message that contains a token.
// It typically is a response from a login service.
type TokenResponse interface {
proto.Message
GetToken() string
}
// Handler is an interface that can be used to handle token forwarding.
// It can be used to forward tokens from grpc messages to cookies and vice versa.
// T is the token response type that creates the session and contains the token to be forwarded as a cookie.
// U is the message that closes the session.
type Handler interface {
CookieToAuth(ctx context.Context) context.Context
ForwardTokenAsCookie(ctx context.Context, w http.ResponseWriter, p proto.Message) error
ForwardCookieAsToken(ctx context.Context, request *http.Request) metadata.MD
SendAuthCookie(ctx context.Context, token string) error
}
func NewHandler(sessionName string, sessions *sessions.CookieStore) Handler {
return &handler{sessionName: sessionName, sessions: sessions}
}
type handler struct {
sessionName string
sessions *sessions.CookieStore
}
// ForwardTokenAsCookie forwards the token from the message to the cookie store.
func (h *handler) ForwardTokenAsCookie(_ context.Context, w http.ResponseWriter, p proto.Message) error {
switch m := any(p).(type) {
case TokenResponse:
sess := h.newSession(h.sessionName)
sess.Values["token"] = m.GetToken()
// request is not available in the context, but session.Save does not actually use it
return h.sessions.Save(nil, w, sess)
default:
sess := h.newSession(h.sessionName)
sess.Options.MaxAge = -1
return h.sessions.Save(nil, w, sess)
}
}
// ForwardCookieAsToken forwards the token from the cookie store to the grpc metadata.
func (h *handler) ForwardCookieAsToken(_ context.Context, request *http.Request) metadata.MD {
sess, err := h.sessions.Get(request, h.sessionName)
if err != nil {
return nil
}
tk, ok := sess.Values["token"]
if !ok {
return nil
}
return metadata.Pairs("authorization", fmt.Sprintf("Bearer %v", tk))
}
// SendAuthCookie sets the cookie in the response.
func (h *handler) SendAuthCookie(ctx context.Context, token string) error {
sess := h.newSession(h.sessionName)
sess.Values["token"] = token
if token == "" {
sess.Options.MaxAge = -1
}
encoded, err := securecookie.EncodeMulti(sess.Name(), sess.Values, h.sessions.Codecs...)
if err != nil {
return err
}
return grpc.SetHeader(ctx, metadata.Pairs("set-cookie", sessions.NewCookie(sess.Name(), encoded, sess.Options).String()))
}
func (h *handler) newSession(name string) *sessions.Session {
session := sessions.NewSession(h.sessions, name)
opts := *h.sessions.Options
session.Options = &opts
session.IsNew = true
return session
}
// CookieToAuth forwards the cookie authorization to the grpc metadata.
func (h *handler) CookieToAuth(ctx context.Context) context.Context {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return ctx
}
// if we have an authorization header, we don't need to check for cookies here
if len(md.Get("authorization")) > 0 {
return ctx
}
// no other way to easily parse cookies
header := http.Header{}
for _, v := range md.Get("cookie") {
header.Add("Cookie", v)
}
md.Delete("cookie")
sess, err := h.sessions.Get(&http.Request{Header: header}, h.sessionName)
if err != nil {
return ctx
}
if v, ok := sess.Values["token"]; ok {
md["authorization"] = []string{fmt.Sprintf("Bearer %s", v)}
}
return metadata.NewIncomingContext(ctx, md)
}

4
go.mod
View File

@ -1,6 +1,6 @@
module go.linka.cloud/grpc-toolkit module go.linka.cloud/grpc-toolkit
go 1.22.0 go 1.23
toolchain go1.23.2 toolchain go1.23.2
@ -15,6 +15,8 @@ require (
github.com/go-logr/logr v1.4.2 github.com/go-logr/logr v1.4.2
github.com/golang/protobuf v1.5.4 github.com/golang/protobuf v1.5.4
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/securecookie v1.1.2
github.com/gorilla/sessions v1.4.0
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0

6
go.sum
View File

@ -278,6 +278,8 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@ -309,6 +311,10 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=