~netlandish/gobwebs-oauth2

e20f86e96a625920b6d2137b2724de264f0b96ef — Peter Sanchez 1 year, 6 months ago ba75e76
Adding token fetch ops
7 files changed, 137 insertions(+), 9 deletions(-)

M bearer.go
M go.mod
M go.sum
A logic.go
A middleware.go
M oauth2_grants.go
M routes.go
M bearer.go => bearer.go +8 -0
@@ 8,6 8,7 @@ import (
	"time"

	"git.sr.ht/~sircmpwn/go-bare"
	"hg.code.netlandish.com/~netlandish/gobwebs"
	"hg.code.netlandish.com/~netlandish/gobwebs/crypto"
)



@@ 167,3 168,10 @@ func (g *Grants) Has(grant string, mode string) bool {
func (g *Grants) Encode() string {
	return g.encoded
}

// TokenUser wrapper for gobwebs.User and token grants
type TokenUser struct {
	User   gobwebs.User
	Token  *BearerToken
	Grants *Grants
}

M go.mod => go.mod +1 -2
@@ 7,7 7,7 @@ require (
	github.com/Masterminds/squirrel v1.5.4
	github.com/labstack/echo/v4 v4.10.2
	github.com/segmentio/ksuid v1.0.4
	hg.code.netlandish.com/~netlandish/gobwebs v0.0.0-20230425200922-0a998d338bdd
	hg.code.netlandish.com/~netlandish/gobwebs v0.0.0-20230426140251-ec705d3e3fa6
)

require (


@@ 15,7 15,6 @@ require (
	github.com/99designs/gqlgen v0.17.29 // indirect
	github.com/ProtonMail/go-crypto v0.0.0-20220113124808-70ae35bab23f // indirect
	github.com/agnivade/levenshtein v1.1.1 // indirect
	github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387 // indirect
	github.com/alexedwards/scs/postgresstore v0.0.0-20211203064041-370cc303b69f // indirect
	github.com/alexedwards/scs/v2 v2.5.1 // indirect
	github.com/beorn7/perks v1.0.1 // indirect

M go.sum => go.sum +2 -5
@@ 53,8 53,6 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387 h1:loy0fjI90vF44BPW4ZYOkE3tDkGTy7yHURusOJimt+I=
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387/go.mod h1:GuR5j/NW7AU7tDAQUDGCtpiPxWIOy/c3kiRDnlwiCHc=
github.com/alexedwards/scs/postgresstore v0.0.0-20211203064041-370cc303b69f h1:5jiSGWqKk8pJrjaN/KEANWe/4I767+d6FiKoDGpChik=
github.com/alexedwards/scs/postgresstore v0.0.0-20211203064041-370cc303b69f/go.mod h1:TDDdV/xnjj+/4zBQ9a2k+i2AbuAdY7SQjPUh5zoTZ3M=
github.com/alexedwards/scs/v2 v2.4.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=


@@ 337,7 335,6 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=


@@ 630,8 627,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
hg.code.netlandish.com/~netlandish/gobwebs v0.0.0-20230425200922-0a998d338bdd h1:l2Z28g5798LK5/jkAPozA6hBqKjHgOcNjJfU9fAoew4=
hg.code.netlandish.com/~netlandish/gobwebs v0.0.0-20230425200922-0a998d338bdd/go.mod h1:+gStIFMqfW/W36PtW25x86MFS0FtXzbhnltOcB6hNf4=
hg.code.netlandish.com/~netlandish/gobwebs v0.0.0-20230426140251-ec705d3e3fa6 h1:pd2wi4TjKWcPb13/SgTi8FSHSZjeVnPxbLC/F4k3I/4=
hg.code.netlandish.com/~netlandish/gobwebs v0.0.0-20230426140251-ec705d3e3fa6/go.mod h1:+gStIFMqfW/W36PtW25x86MFS0FtXzbhnltOcB6hNf4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

A logic.go => logic.go +52 -0
@@ 0,0 1,52 @@
package oauth2

import (
	"context"
	"crypto/sha512"
	"encoding/hex"
	"fmt"
	"log"

	sq "github.com/Masterminds/squirrel"
	"hg.code.netlandish.com/~netlandish/gobwebs"
	"hg.code.netlandish.com/~netlandish/gobwebs/database"
)

func OAuth2(ctx context.Context, token string, fetch gobwebs.UserFetch) (*TokenUser, error) {
	bt := DecodeBearerToken(ctx, token)
	if bt == nil {
		return nil, fmt.Errorf("Invalid or expired OAuth 2.0 bearer token")
	}
	user, err := fetch.FromDB(ctx, uint(bt.UserID), true)
	if err != nil {
		return nil, err
	}

	hash := sha512.Sum512([]byte(token))
	hashStr := hex.EncodeToString(hash[:])
	opts := &database.FilterOptions{
		Filter: sq.Eq{"hash": hashStr},
	}
	grants, err := GetGrants(ctx, opts)
	if err != nil {
		return nil, err
	}
	if len(grants) == 0 {
		return nil, fmt.Errorf("Invalid or expired OAuth 2.0 bearer token")
	} else if len(grants) > 1 {
		// Should never happen
		log.Printf("Token hash %s has more than one grant record", hashStr)
		return nil, fmt.Errorf("Error with provided OAuth 2.0 bearer token")
	}
	grant := grants[0]
	if grant.IsExpired() {
		return nil, fmt.Errorf("Invalid or expired OAuth 2.0 bearer token")
	}

	gt := DecodeGrants(bt.Grants)
	return &TokenUser{
		User:   user,
		Token:  bt,
		Grants: &gt,
	}, nil
}

A middleware.go => middleware.go +66 -0
@@ 0,0 1,66 @@
package oauth2

import (
	"context"
	"errors"
	"fmt"
	"strings"

	"github.com/labstack/echo/v4"
	"hg.code.netlandish.com/~netlandish/gobwebs"
	"hg.code.netlandish.com/~netlandish/gobwebs/auth"
	"hg.code.netlandish.com/~netlandish/gobwebs/server"
)

var tuserCtxKey = &contextKey{"tokenuser"}

type contextKey struct {
	name string
}

// Context adds TokenUser object to context for immediate use
func Context(ctx context.Context, tuser *TokenUser) context.Context {
	return context.WithValue(ctx, tuserCtxKey, tuser)
}

// ForContext pulls TokenUser value for context
func ForContext(ctx context.Context) *TokenUser {
	tuser, ok := ctx.Value(tuserCtxKey).(*TokenUser)
	if !ok {
		panic(errors.New("invalid user context"))
	}
	return tuser
}

// AuthorizationMiddleware ...
func AuthorizationMiddleware(fetch gobwebs.UserFetch) echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			req := c.Request()
			gctx := c.(*server.Context)
			header := req.Header.Get("Authorization")
			if header == "" {
				z := strings.SplitN(header, " ", 2)
				if len(z) != 2 {
					return fmt.Errorf("Invalid Authorization header")
				}

				switch strings.ToLower(z[0]) {
				case "bearer":
					token, err := OAuth2(req.Context(), z[1], fetch)
					if err != nil {
						return err
					}
					ctx := auth.Context(req.Context(), token.User)
					ctx = Context(ctx, token)
					c.SetRequest(c.Request().WithContext(ctx))
					gctx.User = token.User
					return next(gctx)
				default:
					return fmt.Errorf("Invalid Authorization header")
				}
			}
			return next(gctx)
		}
	}
}

M oauth2_grants.go => oauth2_grants.go +6 -0
@@ 4,6 4,7 @@ import (
	"context"
	"database/sql"
	"fmt"
	"time"

	sq "github.com/Masterminds/squirrel"
	"hg.code.netlandish.com/~netlandish/gobwebs/database"


@@ 79,6 80,11 @@ func (g *Grant) Store(ctx context.Context) error {
	return err
}

// IsExpired will check the current grants exiration status
func (g *Grant) IsExpired() bool {
	return time.Now().UTC().After(g.Expires.UTC())
}

// Delete will delete this rate
func (g *Grant) Delete(ctx context.Context) error {
	if g.ID == 0 {

M routes.go => routes.go +2 -2
@@ 7,7 7,7 @@ import (
	sq "github.com/Masterminds/squirrel"
	"github.com/labstack/echo/v4"
	"hg.code.netlandish.com/~netlandish/gobwebs"
	"hg.code.netlandish.com/~netlandish/gobwebs/accounts"
	"hg.code.netlandish.com/~netlandish/gobwebs/auth"
	"hg.code.netlandish.com/~netlandish/gobwebs/database"
	"hg.code.netlandish.com/~netlandish/gobwebs/server"
)


@@ 21,7 21,7 @@ type Service struct {

// RegisterRoutes ...
func (s *Service) RegisterRoutes() {
	s.eg.Use(accounts.AuthRequired())
	s.eg.Use(auth.AuthRequired())
	s.eg.GET("/clients", s.ListClients).Name = s.RouteName("list_clients")
	s.eg.GET("/clients/add", s.AddClient).Name = s.RouteName("add_client")
	s.eg.POST("/clients/add", s.AddClient).Name = s.RouteName("add_client_post")