# HG changeset patch # User Peter Sanchez # Date 1683758477 21600 # Wed May 10 16:41:17 2023 -0600 # Node ID 2f465c1685f62e139bc8e442051556acccefe8e5 # Parent e437e67f5eaf0010bc3c3a5ac9dbbe17c46d045d Adding client token management handlers diff --git a/clients.go b/clients.go --- a/clients.go +++ b/clients.go @@ -145,6 +145,35 @@ } +// Revoke will revoke all grants for a given client +func (c *Client) Revoke(ctx context.Context) error { + if c.ID == 0 { + return fmt.Errorf("Client object is not populated") + } + err := database.WithTx(ctx, nil, func(tx *sql.Tx) error { + c.Revoked = true + err := sq. + Update("oauth2_clients"). + Set("revoked", c.Revoked). + Where("id = ?", c.ID). + Suffix(`RETURNING (updated_on)`). + PlaceholderFormat(sq.Dollar). + RunWith(tx). + ScanContext(ctx, &c.UpdatedOn) + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, ` + UPDATE oauth2_grants + SET expires = NOW() AT TIME ZONE 'UTC' + WHERE client_id = $1; + `, c.ID) + return err + }) + return err +} + // Delete will delete this client func (c *Client) Delete(ctx context.Context) error { if c.ID == 0 { diff --git a/routes.go b/routes.go --- a/routes.go +++ b/routes.go @@ -10,6 +10,7 @@ "fmt" "net/http" "net/url" + "strconv" "strings" "time" @@ -44,9 +45,14 @@ s.eg.GET("/personal", s.ListPersonal).Name = s.RouteName("list_personal") s.eg.GET("/personal/add", s.AddPersonal).Name = s.RouteName("add_personal") s.eg.POST("/personal/add", s.AddPersonal).Name = s.RouteName("add_personal_post") + s.eg.GET("/personal/:id/revoke", s.RevokePersonal).Name = s.RouteName("revoke_personal") + s.eg.POST("/personal/:id/revoke", s.RevokePersonal).Name = s.RouteName("revoke_personal_post") 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") + s.eg.GET("/clients/:id", s.DetailClient).Name = s.RouteName("detail_client") + s.eg.POST("/clients/:id/unregister", s.UnregisterClient).Name = s.RouteName("unregister_client_post") + s.eg.POST("/clients/:id/reissue", s.ReissueClient).Name = s.RouteName("reissue_client_post") s.eg.GET("/authorize", s.Authorize).Name = s.RouteName("authorize") s.eg.POST("/authorize", s.AuthorizePOST).Name = s.RouteName("authorize_post") } @@ -118,11 +124,53 @@ return gctx.Render(http.StatusOK, "oauth2_add_personal.html", gmap) } +// RevokePersonal ... +func (s *Service) RevokePersonal(c echo.Context) error { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + return echo.NotFoundHandler(c) + } + gctx := c.(*server.Context) + opts := &database.FilterOptions{ + Filter: sq.And{ + sq.Eq{"id": id}, + sq.Eq{"user_id": gctx.User.GetID()}, + sq.Expr("client_id IS NULL"), + sq.Expr("expires > NOW() at time zone 'UTC'"), + }, + } + tokens, err := GetGrants(c.Request().Context(), opts) + if err != nil { + return err + } + if len(tokens) == 0 { + return echo.NotFoundHandler(c) + } + token := tokens[0] + + req := c.Request() + if req.Method == "POST" { + if err := token.Revoke(req.Context()); err != nil { + return err + } + return c.Redirect(http.StatusMovedPermanently, + c.Echo().Reverse(s.RouteName("list_personal"))) + } + + return gctx.Render(http.StatusOK, "oauth2_revoke_confirm.html", gobwebs.Map{ + "token": token, + }) + +} + // ListClients ... func (s *Service) ListClients(c echo.Context) error { gctx := c.(*server.Context) opts := &database.FilterOptions{ - Filter: sq.Eq{"owner_id": gctx.User.GetID()}, + Filter: sq.And{ + sq.Eq{"owner_id": gctx.User.GetID()}, + sq.Eq{"revoked": false}, + }, } clients, err := GetClients(c.Request().Context(), opts) if err != nil { @@ -171,6 +219,77 @@ return gctx.Render(http.StatusOK, "oauth2_add_client.html", gmap) } +// DetailClient ... +func (s *Service) DetailClient(c echo.Context) error { + id := c.Param("id") + gctx := c.(*server.Context) + client, err := GetClientByID(c.Request().Context(), id) + if err != nil { + return err + } + if client == nil || client.OwnerID != int(gctx.User.GetID()) { + return echo.NotFoundHandler(c) + } + return gctx.Render(http.StatusOK, "oauth2_client_detail.html", gobwebs.Map{ + "client": client, + }) +} + +// UnregisterClient ... +func (s *Service) UnregisterClient(c echo.Context) error { + id := c.Param("id") + gctx := c.(*server.Context) + client, err := GetClientByID(c.Request().Context(), id) + if err != nil { + return err + } + if client == nil || client.OwnerID != int(gctx.User.GetID()) { + return echo.NotFoundHandler(c) + } + + err = client.Revoke(c.Request().Context()) + if err != nil { + return err + } + + return c.Redirect(http.StatusMovedPermanently, + c.Echo().Reverse(s.RouteName("list_clients"))) +} + +// ReissueClient ... +func (s *Service) ReissueClient(c echo.Context) error { + id := c.Param("id") + gctx := c.(*server.Context) + client, err := GetClientByID(c.Request().Context(), id) + if err != nil { + return err + } + if client == nil || client.OwnerID != int(gctx.User.GetID()) { + return echo.NotFoundHandler(c) + } + + err = client.Revoke(c.Request().Context()) + if err != nil { + return err + } + + newClient := &Client{ + OwnerID: int(gctx.User.GetID()), + Name: client.Name, + Description: client.Description, + RedirectURL: client.RedirectURL, + ClientURL: client.ClientURL, + } + token := newClient.GenerateKeys() + if err := newClient.Store(c.Request().Context()); err != nil { + return err + } + return gctx.Render(http.StatusOK, "oauth2_add_client_done.html", gobwebs.Map{ + "client": newClient, + "token": token, + }) +} + func oauth2Redirect(c echo.Context, redirectURI string, params gobwebs.Map) error { parts, err := url.Parse(redirectURI) if err != nil {