Skip to content

Commit 3be7af2

Browse files
committed
feat(lib): Create Auth tokens using inst.Access()
1 parent 12397c4 commit 3be7af2

File tree

11 files changed

+285
-23
lines changed

11 files changed

+285
-23
lines changed

auth/key/key.go

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
logger "github.com/ipfs/go-log"
77
"github.com/libp2p/go-libp2p-core/crypto"
88
peer "github.com/libp2p/go-libp2p-core/peer"
9-
"github.com/multiformats/go-multihash"
109
)
1110

1211
var log = logger.Logger("key")
@@ -38,15 +37,9 @@ func IDFromPubKey(pubKey crypto.PubKey) (string, error) {
3837
return "", fmt.Errorf("identity: public key is required")
3938
}
4039

41-
pubkeybytes, err := pubKey.Bytes()
40+
id, err := peer.IDFromPublicKey(pubKey)
4241
if err != nil {
43-
return "", fmt.Errorf("getting pubkey bytes: %s", err.Error())
44-
}
45-
46-
mh, err := multihash.Sum(pubkeybytes, multihash.SHA2_256, 32)
47-
if err != nil {
48-
return "", fmt.Errorf("summing pubkey: %s", err.Error())
42+
return "", err
4943
}
50-
51-
return mh.B58String(), nil
44+
return id.Pretty(), err
5245
}

auth/token/token.go

Lines changed: 93 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import (
1313

1414
jwt "github.com/dgrijalva/jwt-go"
1515
"github.com/libp2p/go-libp2p-core/crypto"
16+
"github.com/libp2p/go-libp2p-core/peer"
1617
"github.com/qri-io/qfs"
18+
"github.com/qri-io/qri/auth/key"
1719
"github.com/qri-io/qri/profile"
1820
)
1921

@@ -36,6 +38,7 @@ type Token = jwt.Token
3638
// Claims is a JWT Claims object
3739
type Claims struct {
3840
*jwt.StandardClaims
41+
// TODO(b5): this needs to be replaced with a profileID
3942
Username string `json:"username"`
4043
}
4144

@@ -44,6 +47,83 @@ func Parse(tokenString string, tokens Source) (*Token, error) {
4447
return jwt.Parse(tokenString, tokens.VerificationKey)
4548
}
4649

50+
// NewPrivKeyAuthToken creates a JWT token string suitable for making requests
51+
// authenticated as the given private key
52+
func NewPrivKeyAuthToken(pk crypto.PrivKey, ttl time.Duration) (string, error) {
53+
signingMethod, err := jwtSigningMethod(pk)
54+
if err != nil {
55+
return "", err
56+
}
57+
58+
t := jwt.New(signingMethod)
59+
60+
id, err := key.IDFromPrivKey(pk)
61+
if err != nil {
62+
return "", err
63+
}
64+
65+
rawPrivBytes, err := pk.Raw()
66+
if err != nil {
67+
return "", err
68+
}
69+
signKey, err := x509.ParsePKCS1PrivateKey(rawPrivBytes)
70+
if err != nil {
71+
return "", err
72+
}
73+
74+
var exp int64
75+
if ttl != time.Duration(0) {
76+
exp = Timestamp().Add(ttl).In(time.UTC).Unix()
77+
}
78+
79+
// set our claims
80+
t.Claims = &Claims{
81+
StandardClaims: &jwt.StandardClaims{
82+
Issuer: id,
83+
Subject: id,
84+
// set the expire time
85+
// see http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-20#section-4.1.4
86+
ExpiresAt: exp,
87+
},
88+
}
89+
90+
// Creat token string
91+
return t.SignedString(signKey)
92+
}
93+
94+
// ParseAuthToken will parse, validate and return a token
95+
func ParseAuthToken(tokenString string, keystore key.Store) (*Token, error) {
96+
claims := &Claims{}
97+
return jwt.ParseWithClaims(tokenString, claims, func(t *Token) (interface{}, error) {
98+
pid, err := peer.Decode(claims.Issuer)
99+
if err != nil {
100+
return nil, err
101+
}
102+
pubKey := keystore.PubKey(pid)
103+
if pubKey == nil {
104+
for _, pid := range keystore.IDsWithKeys() {
105+
fmt.Printf("key %s has a pid\n", pid)
106+
}
107+
return nil, fmt.Errorf("cannot verify key. missing public key for id %s", claims.Issuer)
108+
}
109+
rawPubBytes, err := pubKey.Raw()
110+
if err != nil {
111+
return nil, err
112+
}
113+
114+
verifyKeyiface, err := x509.ParsePKIXPublicKey(rawPubBytes)
115+
if err != nil {
116+
return nil, err
117+
}
118+
119+
verifyKey, ok := verifyKeyiface.(*rsa.PublicKey)
120+
if !ok {
121+
return nil, fmt.Errorf("public key is not an RSA key. got type: %T", verifyKeyiface)
122+
}
123+
return verifyKey, nil
124+
})
125+
}
126+
47127
// Source creates tokens, and provides a verification key for all tokens
48128
// it creates
49129
//
@@ -69,17 +149,11 @@ var _ Source = (*pkSource)(nil)
69149
// NewPrivKeySource creates an authentication interface backed by a single
70150
// private key. Intended for a node running as remote, or providing a public API
71151
func NewPrivKeySource(privKey crypto.PrivKey) (Source, error) {
72-
methodStr := ""
73-
keyType := privKey.Type().String()
74-
switch keyType {
75-
case "RSA":
76-
methodStr = "RS256"
77-
default:
78-
return nil, fmt.Errorf("unsupported key type for token creation: %q", keyType)
152+
signingMethod, err := jwtSigningMethod(privKey)
153+
if err != nil {
154+
return nil, err
79155
}
80156

81-
signingMethod := jwt.GetSigningMethod(methodStr)
82-
83157
rawPrivBytes, err := privKey.Raw()
84158
if err != nil {
85159
return nil, err
@@ -304,3 +378,13 @@ func (st *qfsStore) save(ctx context.Context) error {
304378
st.path = path
305379
return nil
306380
}
381+
382+
func jwtSigningMethod(pk crypto.PrivKey) (jwt.SigningMethod, error) {
383+
keyType := pk.Type().String()
384+
switch keyType {
385+
case "RSA":
386+
return jwt.GetSigningMethod("RS256"), nil
387+
default:
388+
return nil, fmt.Errorf("unsupported key type for token creation: %q", keyType)
389+
}
390+
}

auth/token/token_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"time"
77

88
"github.com/qri-io/qfs"
9+
"github.com/qri-io/qri/auth/key"
910
testkeys "github.com/qri-io/qri/auth/key/test"
1011
"github.com/qri-io/qri/auth/token"
1112
token_spec "github.com/qri-io/qri/auth/token/spec"
@@ -64,3 +65,26 @@ func TestTokenStore(t *testing.T) {
6465
return ts
6566
})
6667
}
68+
69+
func TestNewPrivKeyAuthToken(t *testing.T) {
70+
// create a token from a private key
71+
kd := testkeys.GetKeyData(0)
72+
str, err := token.NewPrivKeyAuthToken(kd.PrivKey, 0)
73+
if err != nil {
74+
t.Fatal(err)
75+
}
76+
77+
// prove we can parse a token with a store that only has a public key
78+
ks, err := key.NewMemStore()
79+
if err != nil {
80+
t.Fatal(err)
81+
}
82+
if err := ks.AddPubKey(kd.KeyID, kd.PrivKey.GetPublic()); err != nil {
83+
t.Fatal(err)
84+
}
85+
86+
_, err = token.ParseAuthToken(str, ks)
87+
if err != nil {
88+
t.Fatal(err)
89+
}
90+
}

lib/access.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package lib
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"github.com/qri-io/qri/auth/token"
9+
"github.com/qri-io/qri/profile"
10+
)
11+
12+
// AccessMethods is a group of methods for access control & user authentication
13+
type AccessMethods struct {
14+
d dispatcher
15+
}
16+
17+
// Name returns the name of this method group
18+
func (m AccessMethods) Name() string {
19+
return "access"
20+
}
21+
22+
// Access returns the authentication that Instance has registered
23+
func (inst *Instance) Access() AccessMethods {
24+
return AccessMethods{d: inst}
25+
}
26+
27+
// CreateAuthTokenParams are input parameters for Access().CreateAuthToken
28+
type CreateAuthTokenParams struct {
29+
GranteeUsername string
30+
GranteeProfileID string
31+
TTL time.Duration
32+
}
33+
34+
// SetNonZeroDefaults uses default token time-to-live if one isn't set
35+
func (p *CreateAuthTokenParams) SetNonZeroDefaults() {
36+
if p.TTL == 0 {
37+
p.TTL = token.DefaultTokenTTL
38+
}
39+
}
40+
41+
// Valid checks if the profile in question is valid
42+
func (p *CreateAuthTokenParams) Valid() error {
43+
if p.GranteeUsername == "" && p.GranteeProfileID == "" {
44+
return fmt.Errorf("either grantee username or profile is required")
45+
}
46+
return nil
47+
}
48+
49+
// CreateAuthToken constructs a JWT string token suitable for making OAuth
50+
// requests as the grantee user. Creating an access token requires a stored
51+
// private key for the grantee.
52+
// Callers can provide either GranteeUsername OR GranteeProfileID
53+
func (m AccessMethods) CreateAuthToken(ctx context.Context, p *CreateAuthTokenParams) (string, error) {
54+
res, err := m.d.Dispatch(ctx, dispatchMethodName(m, "createauthtoken"), p)
55+
if s, ok := res.(string); ok {
56+
return s, err
57+
}
58+
return "", err
59+
}
60+
61+
// accessImpl is the backing implementation for AccessMethods
62+
type accessImpl struct{}
63+
64+
func (accessImpl) CreateAuthToken(scp scope, p *CreateAuthTokenParams) (string, error) {
65+
var (
66+
grantee *profile.Profile
67+
err error
68+
)
69+
70+
if p.GranteeProfileID != "" {
71+
id, err := profile.IDB58Decode(p.GranteeProfileID)
72+
if err != nil {
73+
return "", err
74+
}
75+
if grantee, err = scp.Profiles().GetProfile(id); err != nil {
76+
return "", err
77+
}
78+
} else if p.GranteeUsername != "" {
79+
if grantee, err = profile.ResolveUsername(scp.Profiles(), p.GranteeUsername); err != nil {
80+
return "", err
81+
}
82+
}
83+
84+
pk := grantee.PrivKey
85+
if pk == nil {
86+
return "", fmt.Errorf("cannot create token for %q (id: %s), private key is required", grantee.Peername, grantee.ID.String())
87+
}
88+
89+
return token.NewPrivKeyAuthToken(pk, p.TTL)
90+
}

lib/access_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package lib
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/qri-io/qri/auth/token"
8+
)
9+
10+
func TestAccessCreateAuthToken(t *testing.T) {
11+
ctx, cancel := context.WithCancel(context.Background())
12+
defer cancel()
13+
inst, cleanup := NewMemTestInstance(ctx, t)
14+
defer cleanup()
15+
16+
// create an authentication token using the owner profile
17+
p := &CreateAuthTokenParams{
18+
GranteeUsername: inst.cfg.Profile.Peername,
19+
}
20+
s, err := inst.Access().CreateAuthToken(ctx, p)
21+
if err != nil {
22+
t.Fatal(err)
23+
}
24+
25+
// prove we can parse & validate that token
26+
_, err = token.ParseAuthToken(s, inst.keystore)
27+
if err != nil {
28+
t.Fatal(err)
29+
}
30+
}

lib/dispatch.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ func (inst *Instance) RegisterMethods() {
134134
// TODO(dustmop): Change registerOne to take both the MethodSet and the Impl, validate
135135
// that their signatures agree.
136136
inst.registerOne("fsi", &FSIImpl{}, reg)
137+
inst.registerOne("access", accessImpl{}, reg)
137138
inst.regMethods = &regMethodSet{reg: reg}
138139
}
139140

lib/lib.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ func NewInstance(ctx context.Context, repoPath string, opts ...Option) (qri *Ins
361361
// If configuration does not have a path assigned, but the repo has a path and
362362
// is stored on the filesystem, add that path to the configuration.
363363
if cfg.Repo.Type == "fs" && cfg.Path() == "" {
364-
cfg.SetPath(filepath.Join(repoPath, "config.yal"))
364+
cfg.SetPath(filepath.Join(repoPath, "config.yaml"))
365365
}
366366

367367
inst := &Instance{

lib/lib_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,3 +355,34 @@ func TestNewInstanceWithAccessControlPolicy(t *testing.T) {
355355
t.Errorf("expected no policy enforce error, got: %s", err)
356356
}
357357
}
358+
359+
// NewMemTestInstance creates an in-memory instance
360+
// TODO(b5): currently "NewInstance" hard-requires a repo-path, even if we can
361+
// provide a configuration that specifies entirely in-memory stores. We should
362+
// make it possible to create fully in-memory Instances using NewInstance,
363+
// but for now I'm working around it with a temp directory & cleanup function
364+
func NewMemTestInstance(ctx context.Context, t *testing.T) (inst *Instance, cleanup func()) {
365+
t.Helper()
366+
tmpPath, err := ioutil.TempDir("", "qri_test_mem_instance")
367+
if err != nil {
368+
t.Fatal(err)
369+
}
370+
371+
cfg := testcfg.DefaultConfigForTesting()
372+
cfg.Filesystems = []qfs.Config{
373+
{Type: "mem"},
374+
{Type: "local"},
375+
}
376+
cfg.Repo.Type = "mem"
377+
if err := cfg.WriteToFile(filepath.Join(tmpPath, "config.yaml")); err != nil {
378+
t.Fatal(err)
379+
}
380+
381+
// TODO(b5): I'd like to be able to do this:
382+
// if inst, err = NewInstance(ctx, "", OptConfig(cfg)); err != nil {
383+
if inst, err = NewInstance(ctx, tmpPath); err != nil {
384+
t.Fatal(err)
385+
}
386+
387+
return inst, func() { os.RemoveAll(tmpPath) }
388+
}

lib/scope.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/qri-io/qri/dsref"
1010
"github.com/qri-io/qri/event"
1111
"github.com/qri-io/qri/fsi"
12+
"github.com/qri-io/qri/profile"
1213
"github.com/qri-io/qri/repo"
1314
)
1415

@@ -53,6 +54,11 @@ func (s *scope) Dscache() *dscache.Dscache {
5354
return s.inst.Dscache()
5455
}
5556

57+
// Profiles accesses the profile store
58+
func (s *scope) Profiles() profile.Store {
59+
return s.inst.profiles
60+
}
61+
5662
// ParseAndResolveRef parses a reference and resolves it
5763
func (s *scope) ParseAndResolveRef(ctx context.Context, refStr, source string) (dsref.Ref, string, error) {
5864
return s.inst.ParseAndResolveRef(ctx, refStr, source)

0 commit comments

Comments
 (0)