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 +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 
 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 +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 +2 -5
@@ 53,8 53,6 @@ github.com/alecthomas/template v0.0.0-20
 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-2019060512303
 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-9
 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 +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 +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 +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.Contex
 	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 +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")