Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(examples): auth pattern exploration #3406

Draft
wants to merge 39 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
d1f0fce
feat: auth pattern
Dec 25, 2024
8ba0626
chore: mod tidy
Dec 26, 2024
f0b169b
chore: add core auth test
Dec 26, 2024
095aa6a
chore: simplify test and add doc
Dec 26, 2024
ea3e5df
chore: more doc
Dec 26, 2024
39a78ae
chore: use test realm
Dec 26, 2024
f564760
chore: more explicit example in test
Dec 26, 2024
322bfa7
chore: rename arg
Dec 26, 2024
c479c9c
chore: improve integration test and expose subacc.EntityID
Dec 26, 2024
60fc391
chore: improve integration test
Dec 26, 2024
9012eec
chore: improve test
Dec 26, 2024
2903fcc
chore: better comments
Dec 26, 2024
1d0ee2b
chore: cleaner
Dec 26, 2024
9da86f1
chore: authbanker doc
Dec 26, 2024
2a70395
chore: add check
Dec 26, 2024
6195e49
chore: move integration test
n0izn0iz Dec 26, 2024
1490451
chore: fix package path
n0izn0iz Dec 26, 2024
ef0b843
chore: add nil test
n0izn0iz Dec 26, 2024
b1b68aa
chore: explicitely handle nil tokens
n0izn0iz Dec 26, 2024
9a31a60
fix: vulnerability in authreg
n0izn0iz Dec 27, 2024
ab6c3ac
chore: add authreg adversarial test
n0izn0iz Dec 27, 2024
024c5e2
chore: add authbanker txtar
n0izn0iz Dec 27, 2024
79e39d2
chore: improve txtar spacing
n0izn0iz Dec 27, 2024
6a39e9a
Merge branch 'master' into auth-patterns
n0izn0iz Dec 28, 2024
ce9ea3c
fix: prevent vuln in subacc + cleanup
n0izn0iz Dec 28, 2024
de7d7b2
chore: fix test
n0izn0iz Dec 28, 2024
607cbef
chore: rename var
n0izn0iz Dec 28, 2024
4b8daf4
feat: unregister
n0izn0iz Dec 28, 2024
129acbf
chore: improve reg
n0izn0iz Dec 28, 2024
faa0df8
chore: disable check due to linter bug
n0izn0iz Dec 28, 2024
9108640
fix: prevent nil in subacc
n0izn0iz Dec 28, 2024
1743373
chore: moar doc
n0izn0iz Dec 28, 2024
651ab8d
chore: add sessions example
n0izn0iz Dec 28, 2024
e9f39fb
chore: remove dev artifact
n0izn0iz Dec 28, 2024
68b6e6b
fix: test
n0izn0iz Dec 28, 2024
e3d5f78
chore: add sessions.EntityID helper
n0izn0iz Dec 28, 2024
7c835b4
chore: abstract account
n0izn0iz Dec 28, 2024
eae7282
chore: clearer txtar comments
n0izn0iz Dec 28, 2024
23e45a3
chore: explicit comments
n0izn0iz Dec 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions examples/gno.land/p/demo/auth/auth.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package auth

import (
"errors"
"std"
)

// Token represents an authentication token
type Token interface {
// Source is used to identify the provenance of the token.
// It is intented to be used to find the corresponding [AuthenticateFn], for example in registries.
// It can be spoofed and should not be trusted.
Source() std.Realm
}

// AuthenticateFn validates a token and returns the ID of the authenticated entity.
// It panics with [ErrInvalidToken] if the token is invalid.
//
// XXX: Should we add some kind of `Token.EntityID` method instead of returning the ID here?
type AuthenticateFn = func(autok Token) string

var (
ErrInvalidToken = errors.New("invalid token")
)
70 changes: 70 additions & 0 deletions examples/gno.land/p/demo/auth/auth_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package auth_test

import (
"std"
"testing"

"gno.land/p/demo/auth"
"gno.land/p/demo/urequire"
)

func TestToken(t *testing.T) {
urequire.NotPanics(t, func() {
authenticate(getToken())
})

urequire.PanicsWithMessage(t, auth.ErrInvalidToken.Error(), func() {
authenticate((*token)(nil))
})

urequire.PanicsWithMessage(t, auth.ErrInvalidToken.Error(), func() {
authenticate(getFakeToken())
})

urequire.PanicsWithMessage(t, auth.ErrInvalidToken.Error(), func() {
authenticate(nil)
})

urequire.PanicsWithMessage(t, auth.ErrInvalidToken.Error(), func() {
authenticate((*fakeToken)(nil))
})
}

var testRealm = std.NewCodeRealm("gno.land/r/demo/absacc")

type token struct {
}

func (t *token) Source() std.Realm {
return testRealm
}

var _ auth.Token = (*token)(nil)

func getToken() auth.Token {
return &token{}
}

func authenticate(autok auth.Token) string {
// the next line is the core of the auth pattern, this ensures we created this token
if val, ok := autok.(*token); !ok || val == nil {
panic(auth.ErrInvalidToken)
}

return "alice"
}

var _ auth.AuthenticateFn = authenticate

type fakeToken struct {
}

func (t *fakeToken) Source() std.Realm {
return testRealm
}

var _ auth.Token = (*fakeToken)(nil)

func getFakeToken() auth.Token {
return &fakeToken{}
}
1 change: 1 addition & 0 deletions examples/gno.land/p/demo/auth/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/p/demo/auth
78 changes: 78 additions & 0 deletions examples/gno.land/r/demo/authbanker/authbanker.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package authbanker

import (
"errors"
"std"
"strings"

"gno.land/p/demo/auth"
"gno.land/p/demo/ufmt"
"gno.land/r/demo/authreg"
)

// This example is there mostly to demonstrate auth usage, it is quite limited
// only EOAs can fund accounts

const denom = "ugnot"

var vaults = make(map[string]int64)

// GetCoins returns the amount of coins in the vault identified by `entityID`
func GetCoins(entityID string) int64 {
return vaults[entityID]
}

// SendCoins sends `amount` coins from the vault identified by `atok` to the account identified by `to`.
//
// `to` can be an entity ID or an address.
func SendCoins(atok auth.Token, to string, amount int64) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you write a txtar or share a few maketx calls that you expect to use with this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an integration test here

Copy link
Contributor Author

@n0izn0iz n0izn0iz Dec 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also added txtar here

if amount < 1 {
panic("sent amount must be >= 0")
}

from := authreg.Authenticate(atok)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What prevents me from creating my own authenticator? I don't understand where you expect to whitelist the approved authenticators.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing, the pattern is meant to be extendable.

The registry namespaces the authenticators so you can't authenticate an entity from an other authenticator.

How would you exploit this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually there was a vulnerability in namespacing if you pass paths with .., maybe you were refering to that? I added a guard against that here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also if you want to whitelist providers somewhere, you can check the token's Source or statically import the providers and call their Authenticate function

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was considering creating a new authenticator, registering it, and then using it for universal acceptance.

I see two secure approaches: one is to implement a whitelist system on the registry itself, and the other is to establish a whitelist system within the contract that checks the authentication.

However, allowing anyone to create an authenticator and contracts to simply "verify if a token is valid" is definitely insecure.


if from == to {
panic("cannot send to self")
}
if vaultAmount := vaults[from]; amount > vaultAmount {
panic("not enough in account")
}

vaults[from] -= amount

if isEntityID(to) {
vaults[to] += amount
} else {
realmBanker := std.GetBanker(std.BankerTypeRealmSend)
from := std.CurrentRealm().Addr()
coins := std.Coins{std.NewCoin(denom, amount)}
realmBanker.SendCoins(from, std.Address(to), coins)
}
}

// FundVault funds the vault identified by `entityID` with the `OrigSend` coins.
// It panics if it's not an `OriginCall`.
func FundVault(entityID string) {
// XXX: maybe replace the following with `authreg.Validate(to)`
if !isEntityID(entityID) {
panic("invalid destination")
}

std.AssertOriginCall()

sentCoins := std.GetOrigSend()
for _, coin := range sentCoins {
if coin.Denom != denom {
panic(ufmt.Errorf("only %q supported", denom))
}
if coin.Amount <= 0 {
panic(errors.New("send amount must be > 0"))
}
vaults[entityID] += coin.Amount
}
}

func isEntityID(str string) bool {
return strings.HasPrefix(str, "/")
}
1 change: 1 addition & 0 deletions examples/gno.land/r/demo/authbanker/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/r/demo/authbanker
47 changes: 47 additions & 0 deletions examples/gno.land/r/demo/authbanker/integration_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package authbanker_test

import (
"std"
"testing"

"gno.land/p/demo/testutils"
"gno.land/p/demo/urequire"
"gno.land/r/demo/authbanker"
"gno.land/r/demo/subacc"
)

func TestAuthIntegration(t *testing.T) {
authbankerAddr := std.DerivePkgAddr("gno.land/r/demo/authbanker")

alice := testutils.TestAddress("alice")
aliceAccountID := subacc.EntityID(alice, "savings")

bob := testutils.TestAddress("bob")
bobAccountID := subacc.EntityID(bob, "savings")

std.TestSetOrigSend(std.Coins{{"ugnot", 42}}, nil)
authbanker.FundVault(aliceAccountID)
urequire.Equal(t, int64(42), authbanker.GetCoins(aliceAccountID))
// XXX: uncoment next line when tx send in tests is fixed
// urequire.Equal(t, int64(42), std.GetBanker(std.BankerTypeReadonly).GetCoins(authbankerAddr)[0].Amount)

std.TestSetRealm(std.NewUserRealm(alice))
aliceToken := subacc.AuthToken("savings")

authbanker.SendCoins(aliceToken, bobAccountID, 14)
urequire.Equal(t, int64(28), authbanker.GetCoins(aliceAccountID))
urequire.Equal(t, int64(14), authbanker.GetCoins(bobAccountID))

std.TestSetRealm(std.NewUserRealm(bob))
bobToken := subacc.AuthToken("savings")

urequire.PanicsWithMessage(t, "not enough in account", func() { authbanker.SendCoins(bobToken, alice.String(), 15) })

std.TestIssueCoins(authbankerAddr, std.Coins{{"ugnot", 7}}) // XXX: we need this line because in tests, tx send does not work, remove when tx send in test is fixed
authbanker.SendCoins(bobToken, alice.String(), 7)
urequire.Equal(t, int64(28), authbanker.GetCoins(aliceAccountID))
urequire.Equal(t, int64(7), authbanker.GetCoins(bobAccountID))
urequire.Equal(t, int64(7), std.GetBanker(std.BankerTypeReadonly).GetCoins(alice)[0].Amount)
urequire.Equal(t, 0, len(std.GetBanker(std.BankerTypeReadonly).GetCoins(authbankerAddr))) // XXX: replace with next line when tx send in tests is fixed
// urequire.Equal(t, int64(35), std.GetBanker(std.BankerTypeReadonly).GetCoins(authbankerAddr)[0].Amount)
}
26 changes: 26 additions & 0 deletions examples/gno.land/r/demo/authreg/authreg.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package authreg

import (
"errors"
"path"
"std"

"gno.land/p/demo/auth"
)

var fns = make(map[string]auth.AuthenticateFn)

// XXX: we could add a slug there
func Register(authenticate auth.AuthenticateFn) {
caller := std.PrevRealm().Addr()
fns[caller.String()] = authenticate
}

func Authenticate(autok auth.Token) string {
provider := autok.Source().Addr().String()
authFn, ok := fns[provider]
if !ok {
panic(errors.New("unknown auth provider"))
}
return path.Join("/", provider, authFn(autok))
}
1 change: 1 addition & 0 deletions examples/gno.land/r/demo/authreg/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/r/demo/authreg
1 change: 1 addition & 0 deletions examples/gno.land/r/demo/subacc/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/r/demo/subacc
51 changes: 51 additions & 0 deletions examples/gno.land/r/demo/subacc/subacc.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package subacc

import (
"path"
"std"

"gno.land/p/demo/auth"
"gno.land/r/demo/authreg"
)

var source std.Realm

func init() {
source = std.CurrentRealm()
authreg.Register(authenticate)
}

func AuthToken(slug string) auth.Token {
caller := std.PrevRealm().Addr()
return &token{accountKey: accountID(caller, slug)}
}

type token struct {
accountKey string
}

func (a *token) Source() std.Realm {
return source
}

var _ auth.Token = (*token)(nil)

func authenticate(autho auth.Token) string {
// this check should ensure we created this object
cauth, ok := autho.(*token)
if !ok {
panic(auth.ErrInvalidToken)
}

return cauth.accountKey
}

var _ auth.AuthenticateFn = authenticate

func EntityID(creator std.Address, slug string) string {
return path.Join("/", source.Addr().String(), accountID(creator, slug))
}

func accountID(creator std.Address, slug string) string {
return creator.String() + "/" + slug
}
15 changes: 15 additions & 0 deletions examples/gno.land/r/demo/subacc/subacc_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package subacc_test

import (
"std"
"testing"

"gno.land/p/demo/urequire"
"gno.land/r/demo/subacc"
)

func TestEntityID(t *testing.T) {
test1 := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // test1
expected := "/g1u5hdzuqfln65xyx5dvz9ldl9e45pmew5exg302/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5/savings"
urequire.Equal(t, expected, subacc.EntityID(test1, "savings"))
}
Loading