From c0d82b0729005f65e1b0ce840201130cdfa136e6 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Mon, 3 Jun 2024 14:26:26 -0400 Subject: [PATCH] refactor: Add auth token handling in SonrContext & wallet package --- app/app.go | 2 +- internal/local/cache.go | 27 ++++ internal/local/context.go | 64 ++++++--- internal/local/encrypt.go | 2 +- internal/local/local.go | 6 + internal/local/session.go | 135 ++++++++++++++++++ .../session/{credentials.go => claims.go} | 0 internal/session/context.go | 43 ------ internal/session/session.go | 6 +- pkg/auth/authenticator.go | 22 +-- pkg/auth/credential.go | 26 +--- pkg/vault/wallet/claims.go | 34 +++++ pkg/vfs/vfs.go | 4 +- 13 files changed, 265 insertions(+), 106 deletions(-) create mode 100644 internal/local/cache.go create mode 100644 internal/local/local.go create mode 100644 internal/local/session.go rename internal/session/{credentials.go => claims.go} (100%) delete mode 100644 internal/session/context.go create mode 100644 pkg/vault/wallet/claims.go diff --git a/app/app.go b/app/app.go index 704982f72..8825a1b8f 100644 --- a/app/app.go +++ b/app/app.go @@ -154,7 +154,7 @@ import ( oracletypes "github.com/di-dao/sonr/x/oracle/types" ) -const appName = "core" +const appName = "sonr" var ( NodeDir = ".sonr" diff --git a/internal/local/cache.go b/internal/local/cache.go new file mode 100644 index 000000000..d825040c6 --- /dev/null +++ b/internal/local/cache.go @@ -0,0 +1,27 @@ +package local + +import ( + "time" + + "github.com/bool64/cache" +) + +var ( + baseSessionCache *cache.FailoverOf[session] + authorizedSessionCache *cache.FailoverOf[authorizedSession] +) + +// setupCache configures cache and inital settings for proxy. +func setupCache() { + // Setup cache for session. + baseSessionCache = cache.NewFailoverOf(func(cfg *cache.FailoverConfigOf[session]) { + // Using last 30 seconds of 5m TTL for background update. + cfg.MaxStaleness = 1 * time.Hour + cfg.BackendConfig.TimeToLive = 2*time.Hour - cfg.MaxStaleness + }) + authorizedSessionCache = cache.NewFailoverOf(func(cfg *cache.FailoverConfigOf[authorizedSession]) { + // Using last 30 seconds of 5m TTL for background update. + cfg.MaxStaleness = 30 * time.Minute + cfg.BackendConfig.TimeToLive = 1*time.Hour - cfg.MaxStaleness + }) +} diff --git a/internal/local/context.go b/internal/local/context.go index 0c2d62223..208153df1 100644 --- a/internal/local/context.go +++ b/internal/local/context.go @@ -4,6 +4,7 @@ import ( "context" "errors" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/segmentio/ksuid" "google.golang.org/grpc" "google.golang.org/grpc/metadata" @@ -12,6 +13,9 @@ import ( // Default Key in gRPC Metadata for the Session ID const kMetadataSessionIDKey = "sonr-session-id" +// Default Key in gRPC Metadata for the Session Authentication JWT Token +const kMetadataAuthTokenKey = "sonr-auth-token" + var ( chainID = "testnet" valAddr = "val1" @@ -19,9 +23,12 @@ var ( // SonrContext is the context for the Sonr API type SonrContext struct { + Context context.Context SessionID string ValidatorAddress string ChainID string + Token string + SDKContext sdk.Context } // SetLocalContextSessionID sets the session ID for the local context @@ -36,39 +43,60 @@ func SetContextChainID(id string) { // UnwrapContext uses context.Context to retreive grpc.Metadata func UnwrapContext(ctx context.Context) SonrContext { - sessionID, err := firstValueForKey(ctx, kMetadataSessionIDKey) - if err != nil { - return WrapContext(ctx) - } - return SonrContext{ - SessionID: sessionID, + sctx := SonrContext{ + SDKContext: sdk.UnwrapSDKContext(ctx), + Context: ctx, ValidatorAddress: valAddr, ChainID: chainID, } + sctx.SessionID = findOrSetSessionID(ctx) + if token, err := fetchSessionAuthToken(ctx); err == nil { + sctx.Token = token + } + return sctx } // WrapContext wraps a context with a session ID -func WrapContext(ctx context.Context) SonrContext { - sessionId := ksuid.New().String() - // create a header that the gateway will watch for - header := metadata.Pairs(kMetadataSessionIDKey, sessionId) - // send the header back to the gateway - grpc.SendHeader(ctx, header) - return SonrContext{ - SessionID: sessionId, - ValidatorAddress: valAddr, - ChainID: chainID, +func WrapContext(ctx SonrContext) context.Context { + refreshGrpcHeaders(ctx) + return ctx.Context +} + +// findOrSetSessionID finds the session ID in the metadata or sets a new one +func findOrSetSessionID(ctx context.Context) string { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return ksuid.New().String() + } + vals := md.Get(kMetadataSessionIDKey) + if len(vals) == 0 { + return ksuid.New().String() } + return vals[0] } -func firstValueForKey(ctx context.Context, key string) (string, error) { +// fetchSessionAuthToken fetches the auth token from the context +func fetchSessionAuthToken(ctx context.Context) (string, error) { md, ok := metadata.FromIncomingContext(ctx) if !ok { return "", errors.New("no metadata found") } - vals := md.Get(key) + vals := md.Get(kMetadataAuthTokenKey) if len(vals) == 0 { return "", errors.New("no values found") } return vals[0], nil } + +// refreshGrpcHeaders refreshes the grpc headers for the Context +func refreshGrpcHeaders(ctx SonrContext) { + // function to send a header to the gateway + sendHeader := func(key, value string) error { + header := metadata.Pairs(key, value) + return grpc.SendHeader(ctx.Context, header) + } + sendHeader(kMetadataSessionIDKey, ctx.SessionID) + if ctx.Token != "" { + sendHeader(kMetadataAuthTokenKey, ctx.Token) + } +} diff --git a/internal/local/encrypt.go b/internal/local/encrypt.go index e5b42bc8b..00c1b4b76 100644 --- a/internal/local/encrypt.go +++ b/internal/local/encrypt.go @@ -23,7 +23,7 @@ func keysetFile() string { return path.Join(defaultNodeHome, "daead_keyset.json") } -func init() { +func setupKeyHandle() { if _, err := os.Stat(keysetFile()); os.IsNotExist(err) { // If the keyset file doesn't exist, generate a new key handle kh, _ = NewKeyHandle() diff --git a/internal/local/local.go b/internal/local/local.go new file mode 100644 index 000000000..029573101 --- /dev/null +++ b/internal/local/local.go @@ -0,0 +1,6 @@ +package local + +func Initialize() { + setupCache() + setupKeyHandle() +} diff --git a/internal/local/session.go b/internal/local/session.go new file mode 100644 index 000000000..92948cee8 --- /dev/null +++ b/internal/local/session.go @@ -0,0 +1,135 @@ +package local + +import ( + "errors" + "time" + + "github.com/go-webauthn/webauthn/protocol" +) + +// Session is the reference to the clients current session over gRPC/HTTP in the local cache. +type Session interface { + // GetAddress returns the currently authenticated Users Sonr Address for the Session. + GetAddress() (string, error) + + // GetChallenge returns the existing challenge or a new challenge to use for validation + GetChallenge() []byte + + // IsAuthorized returns true if the Session has an attached JWT Token + IsAuthorized() bool + + // SessionID returns the ksuid for the current session + SessionID() string +} + +// session is a proxy session. +type session struct { + // ID is the ksuid of the Session + ID string `json:"id"` + + // Validator is the address of the associated validator node address for the session. + Validator string `json:"validator"` + + // ChainID is the current sonr blockchain network chain ID for the session. + ChainID string `json:"chain_id"` + + // Challenge is used for authenticating credentials for the Session + Challenge []byte `json:"challenge"` +} + +// GetAddress returns the session address +func (s session) GetAddress() (string, error) { + return "", errors.New("session does not have attached address") +} + +// GetValidator returns the associated validator address +func (s session) GetValidator() (string, error) { + if s.Validator == "" { + return "", errors.New("session does not have attached address") + } + return s.Validator, nil +} + +// GetChallenge returns the URL Encoded byte challenge +func (s session) GetChallenge() []byte { + if s.Challenge == nil { + bz, err := protocol.CreateChallenge() + if err != nil { + panic(err) + } + s.Challenge = bz + } + return s.Challenge +} + +// IsAuthorized returns true or false for if it is authorized +func (s session) IsAuthorized() bool { + return false +} + +// SessionID returns the string ksuid for the session +func (s session) SessionID() string { + return s.ID +} + +// session is a proxy session. +type authorizedSession struct { + // ID is the ksuid of the Session + ID string `json:"id"` + + // Address is the address of the session. + Address string `json:"address"` + + // Validator is the address of the associated validator node address for the session. + Validator string `json:"validator"` + + // ChainID is the current sonr blockchain network chain ID for the session. + ChainID string `json:"chain_id"` + + // Token is the token of the session. + Token string `json:"token"` + + // Expires is the expiration time of the session. + Expires time.Time `json:"expires"` + + // Challenge is used for authenticating credentials for the Session + Challenge []byte `json:"challenge"` +} + +// GetAddress returns the session address +func (s authorizedSession) GetAddress() (string, error) { + if s.Address == "" { + return "", errors.New("session does not have attached address") + } + return s.Address, nil +} + +// GetValidator returns the associated validator address +func (s authorizedSession) GetValidator() (string, error) { + if s.Address == "" { + return "", errors.New("session does not have attached address") + } + return s.Address, nil +} + +// GetChallenge returns the URL Encoded byte challenge +func (s authorizedSession) GetChallenge() []byte { + if s.Challenge == nil { + bz, err := protocol.CreateChallenge() + if err != nil { + panic(err) + } + s.Challenge = bz + } + return s.Challenge +} + +// IsAuthorized returns true or false for if it is authorized +func (s authorizedSession) IsAuthorized() bool { + return true +} + +// SessionID returns the string ksuid for the session +func (s authorizedSession) SessionID() string { + return s.ID +} diff --git a/internal/session/credentials.go b/internal/session/claims.go similarity index 100% rename from internal/session/credentials.go rename to internal/session/claims.go diff --git a/internal/session/context.go b/internal/session/context.go deleted file mode 100644 index 6b4b3877a..000000000 --- a/internal/session/context.go +++ /dev/null @@ -1,43 +0,0 @@ -package session - -import ( - "context" - - "github.com/segmentio/ksuid" - "google.golang.org/grpc" - "google.golang.org/grpc/metadata" -) - -const ( - // Default Key in gRPC Metadata for the Session ID - kMetadataSessionIDKey = "sonr-session-id" - - // Default Key in gRPC Metadata for the Session Validator Address - kMetadataValAddressKey = "sonr-validator-address" - - // Default Key in gRPC Metadata for the Session Chain ID - kMetadataChainIDKey = "sonr-chain-id" -) - -// unwrapFromContext uses context.Context to retreive grpc.Metadata -func unwrapFromContext(ctx context.Context) string { - md, ok := metadata.FromIncomingContext(ctx) - if !ok { - return wrapIntoContext(ctx) - } - vals := md.Get(kMetadataSessionIDKey) - if len(vals) == 0 { - return "" - } - return vals[0] -} - -// setSessionIDToCtx uses context.Context and set a new Session ID for grpc.Metadata -func wrapIntoContext(ctx context.Context) string { - sessionId := ksuid.New().String() - // create a header that the gateway will watch for - header := metadata.Pairs(kMetadataSessionIDKey, sessionId) - // send the header back to the gateway - grpc.SendHeader(ctx, header) - return sessionId -} diff --git a/internal/session/session.go b/internal/session/session.go index 2d554e301..88aec03ad 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -45,16 +45,16 @@ func Initialize() { // Get returns a session from cache given a key. func Get(ctx context.Context) (Session, error) { - id := unwrapFromContext(ctx) snrCtx := local.UnwrapContext(ctx) - return baseSessionCache.Get( context.Background(), []byte(snrCtx.SessionID), func(ctx context.Context) (session, error) { // Build value or return error on failure. return session{ - ID: id, + ID: snrCtx.SessionID, + Validator: snrCtx.ValidatorAddress, + ChainID: snrCtx.ChainID, }, nil }, ) diff --git a/pkg/auth/authenticator.go b/pkg/auth/authenticator.go index 0190c7fad..2ff9d940d 100644 --- a/pkg/auth/authenticator.go +++ b/pkg/auth/authenticator.go @@ -4,24 +4,10 @@ import "github.com/go-webauthn/webauthn/protocol" // Authenticator contains all needed information about an authenticator for storage. type Authenticator struct { - // The AAGUID of the authenticator. An AAGUID is defined as an array containing the globally unique - // identifier of the authenticator model being sought. - AAGUID []byte `json:"AAGUID"` - - // SignCount -Upon a new login operation, the Relying Party compares the stored signature counter value - // with the new signCount value returned in the assertion’s authenticator data. If this new - // signCount value is less than or equal to the stored value, a cloned authenticator may - // exist, or the authenticator may be malfunctioning. - SignCount uint32 `json:"signCount"` - - // CloneWarning - This is a signal that the authenticator may be cloned, i.e. at least two copies of the - // credential private key may exist and are being used in parallel. Relying Parties should incorporate - // this information into their risk scoring. Whether the Relying Party updates the stored signature - // counter value in this case, or not, or fails the authentication ceremony or not, is Relying Party-specific. - CloneWarning bool `json:"cloneWarning"` - - // Attachment is the authenticatorAttachment value returned by the request. - Attachment protocol.AuthenticatorAttachment `json:"attachment"` + Attachment protocol.AuthenticatorAttachment `json:"attachment"` + AAGUID []byte `json:"AAGUID"` + SignCount uint32 `json:"signCount"` + CloneWarning bool `json:"cloneWarning"` } // SelectAuthenticator allow for easy marshaling of authenticator options that are provided to the user. diff --git a/pkg/auth/credential.go b/pkg/auth/credential.go index 2308ac4f7..4bfaf2746 100644 --- a/pkg/auth/credential.go +++ b/pkg/auth/credential.go @@ -19,26 +19,12 @@ type CredentialFlags struct { // Credential contains all needed information about a WebAuthn credential for storage. type Credential struct { - // A probabilistically-unique byte sequence identifying a public key credential source and its authentication assertions. - ID []byte `json:"id"` - - // The public key portion of a Relying Party-specific credential key pair, generated by an authenticator and returned to - // a Relying Party at registration time (see also public key credential). The private key portion of the credential key - // pair is known as the credential private key. Note that in the case of self attestation, the credential key pair is also - // used as the attestation key pair, see self attestation for details. - PublicKey []byte `json:"publicKey"` - - // The attestation format used (if any) by the authenticator when creating the credential. - AttestationType string `json:"attestationType"` - - // The transport types the authenticator supports. - Transport []protocol.AuthenticatorTransport `json:"transport"` - - // The commonly stored flags. - Flags CredentialFlags `json:"flags"` - - // The Authenticator information for a given certificate. - Authenticator Authenticator `json:"authenticator"` + AttestationType string `json:"attestationType"` + ID []byte `json:"id"` + PublicKey []byte `json:"publicKey"` + Transport []protocol.AuthenticatorTransport `json:"transport"` + Authenticator Authenticator `json:"authenticator"` + Flags CredentialFlags `json:"flags"` } // MakeNewCredential will return a credential pointer on successful validation of a registration response. diff --git a/pkg/vault/wallet/claims.go b/pkg/vault/wallet/claims.go new file mode 100644 index 000000000..ded795890 --- /dev/null +++ b/pkg/vault/wallet/claims.go @@ -0,0 +1,34 @@ +package wallet + +import ( + "context" + "time" + + "github.com/go-webauthn/webauthn/protocol" + "github.com/golang-jwt/jwt/v5" +) + +// CredentialClaims is the claims for a credential. +type CredentialClaims struct { + jwt.RegisteredClaims + Credentials []protocol.CredentialDescriptor `json:"credentials"` +} + +// NewCredentialClaims returns the CredentialClaims for the JWS to sign +func NewCredentialClaims(ctx context.Context) CredentialClaims { + // Create claims with multiple fields populated + claims := CredentialClaims{ + Credentials: make([]protocol.CredentialDescriptor, 0), + RegisteredClaims: jwt.RegisteredClaims{ + // A usual scenario is to set the expiration time relative to the current time + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + Issuer: "test", + Subject: "somebody", + ID: "1", + Audience: []string{"somebody_else"}, + }, + } + return claims +} diff --git a/pkg/vfs/vfs.go b/pkg/vfs/vfs.go index 690eb11cd..9b350cb62 100644 --- a/pkg/vfs/vfs.go +++ b/pkg/vfs/vfs.go @@ -53,9 +53,9 @@ func loadDirectory(dir files.Directory, vfs *vfs) error { it := dir.Entries() for it.Next() { name, node := it.Name(), it.Node() - switch node.(type) { + switch node := node.(type) { case files.File: - data, err := io.ReadAll(node.(files.File)) + data, err := io.ReadAll(node) if err != nil { return err }