Adding introspect handler
6 files changed, 63 insertions(+), 11 deletions(-)

M bearer.go
M go.mod
M go.sum
M logic.go
M middleware.go
M routes.go
M bearer.go +5 -3
@@ 31,6 31,7 @@ func ToTimestamp(t time.Time) Timestamp 
 // BearerToken ...
 type BearerToken struct {
 	Version  uint
+	Issued   Timestamp
 	Expires  Timestamp
 	Grants   string
 	ClientID string

          
@@ 171,7 172,8 @@ func (g *Grants) Encode() string {
 
 // TokenUser wrapper for gobwebs.User and token grants
 type TokenUser struct {
-	User   gobwebs.User
-	Token  *BearerToken
-	Grants *Grants
+	User      gobwebs.User
+	Token     *BearerToken
+	Grants    *Grants
+	TokenHash [64]byte
 }

          
M go.mod +1 -1
@@ 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-20230426140251-ec705d3e3fa6
+	hg.code.netlandish.com/~netlandish/gobwebs v0.0.0-20230428122420-009ed0d86738
 )
 
 require (

          
M go.sum +2 -2
@@ 627,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-20230426140251-ec705d3e3fa6 h1:pd2wi4TjKWcPb13/SgTi8FSHSZjeVnPxbLC/F4k3I/4=
-hg.code.netlandish.com/~netlandish/gobwebs v0.0.0-20230426140251-ec705d3e3fa6/go.mod h1:+gStIFMqfW/W36PtW25x86MFS0FtXzbhnltOcB6hNf4=
+hg.code.netlandish.com/~netlandish/gobwebs v0.0.0-20230428122420-009ed0d86738 h1:y3ywBE43YyBWjV32gHyfJArK6Xj89damOXDzWUTZ8AQ=
+hg.code.netlandish.com/~netlandish/gobwebs v0.0.0-20230428122420-009ed0d86738/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=

          
M logic.go +5 -3
@@ 43,10 43,12 @@ func OAuth2(ctx context.Context, token s
 		return nil, fmt.Errorf("Invalid or expired OAuth 2.0 bearer token")
 	}
 
+	bt.Issued = ToTimestamp(grant.Issued)
 	gt := DecodeGrants(bt.Grants)
 	return &TokenUser{
-		User:   user,
-		Token:  bt,
-		Grants: &gt,
+		User:      user,
+		Token:     bt,
+		Grants:    &gt,
+		TokenHash: hash,
 	}, nil
 }

          
M middleware.go +1 -2
@@ 2,7 2,6 @@ package oauth2
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"strings"
 

          
@@ 27,7 26,7 @@ func Context(ctx context.Context, tuser 
 func ForContext(ctx context.Context) *TokenUser {
 	tuser, ok := ctx.Value(tuserCtxKey).(*TokenUser)
 	if !ok {
-		panic(errors.New("invalid user context"))
+		return nil
 	}
 	return tuser
 }

          
M routes.go +49 -0
@@ 21,6 21,8 @@ type Service struct {
 
 // RegisterRoutes ...
 func (s *Service) RegisterRoutes() {
+	s.eg.POST("/introspect", s.Introspect).Name = s.RouteName("introspect_post")
+
 	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")

          
@@ 79,6 81,53 @@ func (s *Service) AddClient(c echo.Conte
 	return gctx.Render(http.StatusOK, "oauth2_add_client.html", gmap)
 }
 
+// Introspect ...
+func (s *Service) Introspect(c echo.Context) error {
+	req := c.Request()
+	ctype := req.Header.Get("Content-Type")
+	if ctype != "application/x-www-form-urlencoded" {
+		retErr := struct {
+			err  string `json:"error"`
+			desc string `json:"error_description"`
+			uri  string `json:"error_url"` // TODO Make this customizable
+		}{
+			err:  "invalid request",
+			desc: "Content-Type must be application/x-www-form-urlencoded",
+		}
+		return c.JSON(http.StatusBadRequest, &retErr)
+	}
+
+	retFalse := struct {
+		active bool `json:"active"`
+	}{false}
+
+	token := ForContext(req.Context())
+	if token == nil {
+		return c.JSON(http.StatusOK, retFalse)
+	}
+
+	if token.Token.ClientID == "" {
+		return c.JSON(http.StatusOK, retFalse)
+	}
+
+	ret := struct {
+		active    bool   `json:"active"`
+		clientID  string `json:"client_id"`
+		username  string `json:"username"`
+		tokenType string `json:"token_type"`
+		exp       int    `json:"exp"`
+		iat       int    `json:"iat"`
+	}{
+		active:    true,
+		clientID:  token.Token.ClientID,
+		username:  token.User.GetEmail(),
+		tokenType: "bearer",
+		exp:       int(token.Token.Expires),
+		iat:       int(token.Token.Issued),
+	}
+	return c.JSON(http.StatusOK, ret)
+}
+
 // RouteName ...
 func (s *Service) RouteName(value string) string {
 	return fmt.Sprintf("%s:%s", s.name, value)