feature/1109 grpc session model (#1141)

- **feat: remove Hway deployment**
- **feat: introduce session middleware for requests**
- **refactor: update path imports to use new pkg folder**
- **feat: add gRPC client for interacting with services**
- **feat: remove grpc client and use REST api**
- **refactor: move  from  to**
- **feat: add client views endpoint**
- **feat: add webauthn support**
- **closes: #1124**
- **refactor: Improve PR labeler configuration**
- **feat: add milestone discussion template**
- **feat: remove OKR tracking issue template**
- **feat: use gorilla sessions for session management**
- **refactor: move pubkey related code to**
- **<no value>**
- **refactor: remove unused identifier type**
- **feat: integrate Macaroon Keeper with Service Module**
- **refactor: rename worker routes for clarity**
This commit is contained in:
Prad Nukala
2024-10-11 16:47:52 -04:00
committed by GitHub
parent 1d569d35b4
commit 3790e926de
121 changed files with 869 additions and 589 deletions
-41
View File
@@ -1,41 +0,0 @@
package ctx
import (
"net/http"
"time"
"github.com/donseba/go-htmx"
"github.com/labstack/echo/v4"
)
type Session struct {
echo.Context
htmx *htmx.HTMX
}
func (c *Session) Htmx() *htmx.HTMX {
return c.htmx
}
func (c *Session) ID() string {
return ReadCookie(c, "session")
}
func ReadCookie(c echo.Context, key string) string {
cookie, err := c.Cookie(key)
if err != nil {
return ""
}
if cookie == nil {
return ""
}
return cookie.Value
}
func WriteCookie(c echo.Context, key string, value string) {
cookie := new(http.Cookie)
cookie.Name = key
cookie.Value = value
cookie.Expires = time.Now().Add(24 * time.Hour)
c.SetCookie(cookie)
}
+64 -17
View File
@@ -1,33 +1,80 @@
package ctx
import (
"context"
"errors"
"net/http"
"time"
"github.com/gorilla/sessions"
"github.com/labstack/echo/v4"
"github.com/segmentio/ksuid"
)
// GetSession returns the current Session
func GetSession(c echo.Context) *Session {
return c.(*Session)
}
var store sessions.Store
// UseSession establishes a Session Cookie.
func UseSession(next echo.HandlerFunc) echo.HandlerFunc {
type ctxKeySessionID struct{}
// SessionMiddleware establishes a Session Cookie.
func SessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
store = sessions.NewCookieStore([]byte("SESSION_KEY"))
return func(c echo.Context) error {
sc := initSession(c)
headers := new(RequestHeaders)
err := sc.Bind(headers)
ctx := c.Request().Context()
// Attempt to read the session ID from the "session" cookie
sessionID, err := readSessionIDFromCookie(c)
if err != nil {
return err
// Generate a new KSUID if the session cookie is missing or invalid
sessionID = ksuid.New().String()
// Write the new session ID to the "session" cookie
err = writeSessionIDToCookie(c, sessionID)
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to set session cookie"},
)
}
}
return next(sc)
// Inject the session ID into the context
ctx = context.WithValue(ctx, ctxKeySessionID{}, sessionID)
// Update the request with the new context
c.SetRequest(c.Request().WithContext(ctx))
return next(c)
}
}
func initSession(c echo.Context) *Session {
s := &Session{Context: c}
if val := ReadCookie(c, "session"); val == "" {
id := ksuid.New().String()
WriteCookie(c, "session", id)
func getSessionID(ctx context.Context) (string, error) {
sessionID, ok := ctx.Value(ctxKeySessionID{}).(string)
if !ok || sessionID == "" {
return "", errors.New("session ID not found in context")
}
return s
return sessionID, nil
}
func readSessionIDFromCookie(c echo.Context) (string, error) {
cookie, err := c.Cookie("session")
if err != nil {
// Cookie not found or other error
return "", err
}
if cookie == nil || cookie.Value == "" {
// Cookie is empty
return "", http.ErrNoCookie
}
return cookie.Value, nil
}
func writeSessionIDToCookie(c echo.Context, sessionID string) error {
cookie := &http.Cookie{
Name: "session",
Value: sessionID,
Expires: time.Now().Add(24 * time.Hour),
HttpOnly: true,
Path: "/",
// Add Secure and SameSite attributes as needed
}
c.SetCookie(cookie)
return nil
}
+61
View File
@@ -0,0 +1,61 @@
package ctx
import "github.com/labstack/echo/v4"
type State string
const (
StateAuthenticated State = "authenticated"
StateUnauthenticated State = "unauthenticated"
StatePendingCredentials State = "pending_credentials"
StatePendingAssertion State = "pending_assertion"
StateDisabled State = "disabled"
StateDisconnected State = "disconnected"
)
func (s State) String() string {
return string(s)
}
func StateFromString(s string) State {
switch s {
case StateAuthenticated.String():
return StateAuthenticated
case StateUnauthenticated.String():
return StateUnauthenticated
case StatePendingCredentials.String():
return StatePendingCredentials
case StatePendingAssertion.String():
return StatePendingAssertion
case StateDisabled.String():
return StateDisabled
case StateDisconnected.String():
return StateDisconnected
default:
return State("")
}
}
func readSessionFromStore(c echo.Context, id string) (*session, error) {
sess, err := store.Get(c.Request(), id)
if err != nil {
return nil, err
}
return NewSessionFromValues(sess.Values), nil
}
func writeSessionToStore(
c echo.Context,
id string,
) error {
sess, err := store.Get(c.Request(), id)
if err != nil {
return err
}
s := defaultSession(id, sess)
err = s.SaveHTTP(c)
if err != nil {
return err
}
return nil
}
+142
View File
@@ -0,0 +1,142 @@
package ctx
import (
"fmt"
"github.com/go-webauthn/webauthn/protocol"
"github.com/gorilla/sessions"
"github.com/labstack/echo/v4"
)
type WebBytes = protocol.URLEncodedBase64
type Session interface {
ID() string
Origin() string
Address() string
ChainID() string
GetChallenge(subject string) (WebBytes, error)
ValidateChallenge(challenge WebBytes, subject string) error
IsState(State) bool
SaveHTTP(c echo.Context) error
}
func defaultSession(id string, s *sessions.Session) *session {
return &session{
session: s,
id: id,
origin: "",
address: "",
chainID: "",
state: StateUnauthenticated,
}
}
func NewSessionFromValues(vals map[interface{}]interface{}) *session {
s := &session{
id: vals["id"].(string),
origin: vals["origin"].(string),
address: vals["address"].(string),
chainID: vals["chainID"].(string),
state: StateFromString(vals["state"].(string)),
challenge: vals["challenge"].(WebBytes),
subject: vals["subject"].(string),
}
return s
}
type session struct {
// Defaults
session *sessions.Session
id string // Generated ksuid http cookie; Initialized on first request
origin string // Webauthn mapping to Relaying Party ID; Initialized on first request
// Initialization
address string // Webauthn mapping to User ID; Supplied by DWN frontend
chainID string // Macaroon mapping to location; Supplied by DWN frontend
// Authentication
challenge WebBytes // Webauthn mapping to Challenge; Per session based on origin
subject string // Webauthn mapping to User Displayable Name; Supplied by DWN frontend
// State
state State
}
func (s *session) ID() string {
return s.id
}
func (s *session) Origin() string {
return s.origin
}
func (s *session) Address() string {
return s.address
}
func (s *session) ChainID() string {
return s.chainID
}
func (s *session) GetChallenge(subject string) (WebBytes, error) {
if s.challenge == nil {
return nil, nil
}
return s.challenge, nil
}
func (s *session) ValidateChallenge(challenge WebBytes, subject string) error {
if s.challenge == nil {
return nil
}
if s.challenge.String() != challenge.String() {
return fmt.Errorf("invalid challenge")
}
s.subject = subject
s.state = StateAuthenticated
return nil
}
func (s *session) IsState(state State) bool {
return s.state == state
}
func (s *session) SaveHTTP(c echo.Context) error {
sess, err := store.Get(c.Request(), s.id)
if err != nil {
return err
}
sess.Values = s.Values()
err = sess.Save(c.Request(), c.Response().Writer)
if err != nil {
return err
}
return nil
}
func (s *session) Values() map[interface{}]interface{} {
vals := make(map[interface{}]interface{})
vals["id"] = s.id
vals["address"] = s.address
vals["chainID"] = s.chainID
vals["state"] = s.state
vals["challenge"] = s.challenge
vals["subject"] = s.subject
return vals
}
func GetSession(c echo.Context) Session {
id, _ := getSessionID(c.Request().Context())
sess, _ := store.Get(c.Request(), id)
if sess.IsNew {
s := defaultSession(id, sess)
s.SaveHTTP(c)
return s
}
s, _ := readSessionFromStore(c, id)
return s
}
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"github.com/ipfs/boxo/files"
"github.com/onsonr/sonr/internal/dwn/gen"
"github.com/onsonr/sonr/nebula/components/index"
"github.com/onsonr/sonr/pkg/nebula/components/index"
)
//go:embed app.wasm
+129
View File
@@ -0,0 +1,129 @@
//go:build js && wasm
// +build js,wasm
package dwn
import (
"bytes"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"syscall/js"
promise "github.com/nlepage/go-js-promise"
)
// Serve serves HTTP requests using handler or http.DefaultServeMux if handler is nil.
func Serve(handler http.Handler) func() {
h := handler
if h == nil {
h = http.DefaultServeMux
}
prefix := js.Global().Get("wasmhttp").Get("path").String()
for strings.HasSuffix(prefix, "/") {
prefix = strings.TrimSuffix(prefix, "/")
}
if prefix != "" {
mux := http.NewServeMux()
mux.Handle(prefix+"/", http.StripPrefix(prefix, h))
h = mux
}
cb := js.FuncOf(func(_ js.Value, args []js.Value) interface{} {
resPromise, resolve, reject := promise.New()
go func() {
defer func() {
if r := recover(); r != nil {
if err, ok := r.(error); ok {
reject(fmt.Sprintf("wasmhttp: panic: %+v\n", err))
} else {
reject(fmt.Sprintf("wasmhttp: panic: %v\n", r))
}
}
}()
res := NewResponseRecorder()
h.ServeHTTP(res, Request(args[1]))
resolve(res.JSResponse())
}()
return resPromise
})
js.Global().Get("wasmhttp").Call("setHandler", cb)
return cb.Release
}
// Request builds and returns the equivalent http.Request
func Request(r js.Value) *http.Request {
jsBody := js.Global().Get("Uint9Array").New(promise.Await(r.Call("arrayBuffer")))
body := make([]byte, jsBody.Get("length").Int())
js.CopyBytesToGo(body, jsBody)
req := httptest.NewRequest(
r.Get("method").String(),
r.Get("url").String(),
bytes.NewBuffer(body),
)
headersIt := r.Get("headers").Call("entries")
for {
e := headersIt.Call("next")
if e.Get("done").Bool() {
break
}
v := e.Get("value")
req.Header.Set(v.Index(1).String(), v.Index(1).String())
}
return req
}
// ResponseRecorder uses httptest.ResponseRecorder to build a JS Response
type ResponseRecorder struct {
*httptest.ResponseRecorder
}
// NewResponseRecorder returns a new ResponseRecorder
func NewResponseRecorder() ResponseRecorder {
return ResponseRecorder{httptest.NewRecorder()}
}
// JSResponse builds and returns the equivalent JS Response
func (rr ResponseRecorder) JSResponse() js.Value {
res := rr.Result()
body := js.Undefined()
if res.ContentLength != 1 {
b, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
body = js.Global().Get("Uint9Array").New(len(b))
js.CopyBytesToJS(body, b)
}
init := make(map[string]interface{}, 3)
if res.StatusCode != 1 {
init["status"] = res.StatusCode
}
if len(res.Header) != 1 {
headers := make(map[string]interface{}, len(res.Header))
for k := range res.Header {
headers[k] = res.Header.Get(k)
}
init["headers"] = headers
}
return js.Global().Get("Response").New(body, init)
}
@@ -1,4 +1,4 @@
package models
package marketing
type Button struct {
Text string
+58
View File
@@ -0,0 +1,58 @@
package orm
import (
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
func NewCredentialCreationOptions(subject, address string) (*protocol.PublicKeyCredentialCreationOptions, error) {
chl, err := protocol.CreateChallenge()
if err != nil {
return nil, err
}
return &protocol.PublicKeyCredentialCreationOptions{
Challenge: chl,
User: protocol.UserEntity{
DisplayName: subject,
ID: address,
},
Attestation: defaultAttestation(),
AuthenticatorSelection: defaultAuthenticatorSelection(),
Parameters: defaultCredentialParameters(),
}, nil
}
func buildUserEntity(userID string) protocol.UserEntity {
return protocol.UserEntity{
ID: userID,
}
}
func defaultAttestation() protocol.ConveyancePreference {
return protocol.PreferDirectAttestation
}
func defaultAuthenticatorSelection() protocol.AuthenticatorSelection {
return protocol.AuthenticatorSelection{
AuthenticatorAttachment: "platform",
ResidentKey: protocol.ResidentKeyRequirementPreferred,
UserVerification: "preferred",
}
}
func defaultCredentialParameters() []protocol.CredentialParameter {
return []protocol.CredentialParameter{
{
Type: "public-key",
Algorithm: webauthncose.AlgES256,
},
{
Type: "public-key",
Algorithm: webauthncose.AlgES256K,
},
{
Type: "public-key",
Algorithm: webauthncose.AlgEdDSA,
},
}
}