mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
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:
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user