-
Notifications
You must be signed in to change notification settings - Fork 385
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
base: master
Are you sure you want to change the base?
Changes from 19 commits
d1f0fce
8ba0626
f0b169b
095aa6a
ea3e5df
39a78ae
f564760
322bfa7
c479c9c
60fc391
9012eec
2903fcc
1d0ee2b
9da86f1
2a70395
6195e49
1490451
ef0b843
b1b68aa
9a31a60
ab6c3ac
024c5e2
79e39d2
6a39e9a
ce9ea3c
de7d7b2
607cbef
4b8daf4
129acbf
faa0df8
9108640
1743373
651ab8d
e9f39fb
68b6e6b
e3d5f78
7c835b4
eae7282
23e45a3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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") | ||
) |
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{} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module gno.land/p/demo/auth |
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) { | ||
if amount < 1 { | ||
panic("sent amount must be >= 0") | ||
} | ||
|
||
from := authreg.Authenticate(atok) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually there was a vulnerability in namespacing if you pass paths with There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, "/") | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module gno.land/r/demo/authbanker |
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) | ||
} |
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)) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module gno.land/r/demo/authreg |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module gno.land/r/demo/subacc |
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 | ||
} |
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")) | ||
} |
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also added txtar here