├── .gitignore ├── errors.go ├── utils.go ├── utils_test.go ├── default.go ├── token.go ├── objectid_test.go ├── default_test.go ├── store.go ├── README.md ├── tokenauth_test.go ├── objectid.go ├── tokenauth.go ├── store_boltdbfile.go ├── store_boltdbfile_test.go └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Author YuShuangqi. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tokenauth 6 | 7 | import ( 8 | "fmt" 9 | ) 10 | 11 | //Customer error. 12 | type ValidationError struct { 13 | Code string `json:"errcode"` 14 | Msg string `json:"errmsg"` 15 | } 16 | 17 | func (v ValidationError) Error() string { 18 | return fmt.Sprintf("%s:%s", v.Code, v.Msg) 19 | } 20 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Author YuShuangqi. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tokenauth 6 | 7 | import ( 8 | "crypto/rand" 9 | "encoding/base32" 10 | ) 11 | 12 | const ( 13 | alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 14 | ) 15 | 16 | // Returns s random string 17 | func GenerateRandomString(size int, encodeToBase32 bool) string { 18 | var bytes = make([]byte, size) 19 | rand.Read(bytes) 20 | for i, b := range bytes { 21 | bytes[i] = alphanum[b%byte(len(alphanum))] 22 | } 23 | if encodeToBase32 { 24 | return base32.StdEncoding.EncodeToString(bytes) 25 | } 26 | return string(bytes) 27 | } 28 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Author YuShuangqi. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tokenauth_test 6 | 7 | import ( 8 | "github.com/ysqi/tokenauth" 9 | . "gopkg.in/check.v1" 10 | ) 11 | 12 | func (s *S) TestUtls_RandomString(c *C) { 13 | 14 | strs := make([]string, 20) 15 | for i := 0; i < 10; i++ { 16 | strs[i] = tokenauth.GenerateRandomString(10, false) 17 | strs[20-i-1] = tokenauth.GenerateRandomString(10, true) 18 | } 19 | for i := 0; i < 20; i++ { 20 | str := strs[i] 21 | for n, nstr := range strs { 22 | if n != i { 23 | c.Assert(str, Not(Equals), nstr, Commentf("Generated RandomString is not unique")) 24 | } 25 | } 26 | } 27 | } 28 | 29 | func (s *S) TestUtls_RandomStringLength(c *C) { 30 | 31 | for i := 0; i < 20; i++ { 32 | str := tokenauth.GenerateRandomString(i, false) 33 | c.Check(len(str), Equals, i) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /default.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Author YuShuangqi. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tokenauth 6 | 7 | import ( 8 | "crypto/hmac" 9 | "crypto/sha256" 10 | "encoding/base64" 11 | "fmt" 12 | "time" 13 | ) 14 | 15 | const ( 16 | // default secret length. 17 | SecretLength = 32 18 | ) 19 | 20 | type DefaultProvider struct { 21 | Name string 22 | } 23 | 24 | func (d *DefaultProvider) GenerateSecretString(clientID string) (secretString string) { 25 | 26 | return GenerateRandomString(SecretLength, false) 27 | } 28 | 29 | func (d *DefaultProvider) GenerateTokenString(audience *Audience) string { 30 | 31 | if audience == nil { 32 | panic("audience is nil") 33 | } 34 | 35 | hash := hmac.New(sha256.New, []byte(audience.Secret)) 36 | 37 | info := fmt.Sprintf("%s:%s:%d", audience.ID, GenerateRandomString(6, false), time.Now().Unix()) 38 | hash.Write([]byte(info)) 39 | 40 | return base64.StdEncoding.EncodeToString(hash.Sum(nil)) 41 | 42 | } 43 | -------------------------------------------------------------------------------- /token.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Author YuShuangqi. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tokenauth 6 | 7 | import ( 8 | "time" 9 | ) 10 | 11 | // Audience Info, token rely on audience. 12 | // Contains secret string , tokenPeriod for generatating token string. 13 | type Audience struct { 14 | Name string 15 | ID string // Unique key for audience 16 | Secret string //audience secret string,can update. 17 | TokenPeriod uint64 //token period ,unit: seconds. 18 | } 19 | 20 | // Token Info 21 | type Token struct { 22 | ClientID string // Audience.ID 23 | SingleID string // Single Token ID 24 | Value string // Token string 25 | DeadLine int64 // Token Expiration date, time unix. 26 | } 27 | 28 | // Returns this token is expried. 29 | // Note: never exprires if token's deadLine =0 30 | func (t *Token) Expired() bool { 31 | if t.DeadLine == 0 { 32 | return false 33 | } 34 | return time.Now().Unix() >= t.DeadLine 35 | } 36 | 37 | // Returns true if token clientID is empty and signleID is not empty. 38 | func (t *Token) IsSingle() bool { 39 | return len(t.ClientID) == 0 && len(t.SingleID) > 0 40 | } 41 | -------------------------------------------------------------------------------- /objectid_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Author YuShuangqi. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tokenauth_test 6 | 7 | import ( 8 | "github.com/ysqi/tokenauth" 9 | . "gopkg.in/check.v1" 10 | ) 11 | 12 | func (s *S) TestObjectId_New(c *C) { 13 | // Generate 10 ids 14 | ids := make([]tokenauth.ObjectId, 10) 15 | for i := 0; i < 10; i++ { 16 | ids[i] = tokenauth.NewObjectId() 17 | } 18 | for i := 1; i < 10; i++ { 19 | prevId := ids[i-1] 20 | id := ids[i] 21 | // Test for uniqueness among all other 9 generated ids 22 | for j, tid := range ids { 23 | if j != i { 24 | c.Assert(id, Not(Equals), tid, Commentf("Generated ObjectId is not unique")) 25 | } 26 | } 27 | // Check that timestamp was incremented and is within 30 seconds of the previous one 28 | secs := id.Time().Sub(prevId.Time()).Seconds() 29 | c.Assert((secs >= 0 && secs <= 30), Equals, true, Commentf("Wrong timestamp in generated ObjectId")) 30 | // Check that machine ids are the same 31 | c.Assert(id.Machine(), DeepEquals, prevId.Machine()) 32 | // Check that pids are the same 33 | c.Assert(id.Pid(), Equals, prevId.Pid()) 34 | // Test for proper increment 35 | delta := int(id.Counter() - prevId.Counter()) 36 | c.Assert(delta, Equals, 1, Commentf("Wrong increment in generated ObjectId")) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /default_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Author YuShuangqi. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tokenauth_test 6 | 7 | import ( 8 | "github.com/ysqi/tokenauth" 9 | . "gopkg.in/check.v1" 10 | ) 11 | 12 | func (s *S) TestProvider_NewSecret(c *C) { 13 | 14 | provider := &tokenauth.DefaultProvider{} 15 | audience, _ := tokenauth.NewAudience("test", provider.GenerateSecretString) 16 | 17 | var secret string 18 | for i := 0; i < 10; i++ { 19 | newSecret := provider.GenerateSecretString(audience.ID) 20 | 21 | c.Check(len(newSecret), Equals, 32) 22 | c.Assert(newSecret, Not(Equals), secret, Commentf("New Secret is not unique")) 23 | secret = newSecret 24 | } 25 | 26 | } 27 | 28 | func (s *S) TestProvider_Tokne_New(c *C) { 29 | 30 | provider := &tokenauth.DefaultProvider{} 31 | audience, _ := tokenauth.NewAudience("test", provider.GenerateSecretString) 32 | 33 | tokens := make([]string, 20) 34 | for i := 0; i < 10; i++ { 35 | token, _ := tokenauth.NewToken(audience, provider.GenerateTokenString) 36 | tokens[i] = token.Value 37 | } 38 | 39 | //update secret 40 | audience.Secret = provider.GenerateSecretString(audience.ID) 41 | for i := 10; i < 20; i++ { 42 | token, _ := tokenauth.NewToken(audience, provider.GenerateTokenString) 43 | tokens[i] = token.Value 44 | } 45 | 46 | for i := 0; i < 20; i++ { 47 | token := tokens[i] 48 | for j, token2 := range tokens { 49 | if j != i { 50 | c.Assert(token, Not(Equals), token2, Commentf("Generated Token String is not unique")) 51 | } 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /store.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Author YuShuangqi. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tokenauth 6 | 7 | import ( 8 | "fmt" 9 | "runtime" 10 | "time" 11 | ) 12 | 13 | //Token store interface. 14 | type TokenStore interface { 15 | 16 | // Init store 17 | // Returns error if init fail. 18 | Open(config string) error 19 | 20 | // Close store 21 | Close() error 22 | 23 | // Save audience into store. 24 | // Returns error if error occured during execution. 25 | SaveAudience(audience *Audience) error 26 | 27 | // Delete audience and all tokens of audience. 28 | DeleteAudience(clientID string) error 29 | 30 | // Get audience info or returns error. 31 | GetAudience(clientID string) (*Audience, error) 32 | 33 | // Save token to token. 34 | // Returns error if save token fail. 35 | SaveToken(token *Token) error 36 | 37 | // Delete token info from store. 38 | // Returns error if error occured during execution 39 | DeleteToken(tokenString string) error 40 | 41 | // Get token info from store. 42 | // Returns nil if not found token. 43 | // Returns error if get token fail. 44 | GetToken(tokenString string) (*Token, error) 45 | 46 | DeleteExpired() 47 | } 48 | 49 | // Janitor contains TokenStore and Janitor instance. 50 | type janitorTaget struct { 51 | store TokenStore 52 | janitor *janitor 53 | } 54 | 55 | //Janitor implement method to period exec store method. 56 | type janitor struct { 57 | Interval time.Duration 58 | stop chan bool 59 | } 60 | 61 | // Period Run 62 | func (j *janitor) Run(taget *janitorTaget) { 63 | j.stop = make(chan bool) 64 | tick := time.Tick(j.Interval) 65 | for { 66 | select { 67 | case <-tick: 68 | taget.store.DeleteExpired() 69 | case <-j.stop: 70 | return 71 | } 72 | } 73 | } 74 | 75 | func stopJanitor(taget *janitorTaget) { 76 | taget.janitor.stop <- true 77 | } 78 | 79 | func runJanitor(taget *janitorTaget, ci time.Duration) { 80 | j := &janitor{ 81 | Interval: ci, 82 | } 83 | taget.janitor = j 84 | go j.Run(taget) 85 | } 86 | 87 | var adapters = make(map[string]TokenStore) 88 | 89 | // Resister one store provider. 90 | // If name is empty,will panic. 91 | // If same name has registerd ,will panic. 92 | func RegStore(name string, adapter TokenStore) { 93 | 94 | if adapter == nil { 95 | panic("tokenStore: Register adapter is nil") 96 | } 97 | if _, ok := adapters[name]; ok { 98 | panic("tokenStore: Register called twice for adapter " + name) 99 | } 100 | adapters[name] = adapter 101 | } 102 | 103 | // New regiesterd store 104 | func NewStore(adapterName, config string) (TokenStore, error) { 105 | 106 | adapter, ok := adapters[adapterName] 107 | if !ok { 108 | return nil, fmt.Errorf("tokenStore: unknown adapter name %q (forgot registration ?)", adapterName) 109 | } 110 | if err := adapter.Open(config); err != nil { 111 | return nil, err 112 | } else { 113 | s := &janitorTaget{store: adapter} 114 | runJanitor(s, time.Minute*5) 115 | runtime.SetFinalizer(s, stopJanitor) 116 | return adapter, nil 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tokenauth 2 | tokenauth 是一个 token 维护验证存储包,支持单点和多点模式,默认使用 boltdbf 存储 token 数据。 3 | 4 | # 安装 5 | 6 | `go get github.com/ysqi/tokenauth` 7 | 8 | 也可以通过`-u`参数来更新tokenauth 和所依赖的第三方包 9 | 10 | `go get -u github.com/ysqi/tokenauth` 11 | 12 | # 功能 13 | 14 | + 支持自定义存储 15 | + 默认使用boltdbf存储token到本地 16 | + 随机生成客户令牌 17 | + 自定义算法生成 Token 18 | + 支持对一个客户维护N个Token 19 | + 支持对一个客户仅维护一个 Token 20 | + 支持Token有效性验证 21 | 22 | # 使用场景 23 | 24 | __为第三方客户端颁发Token__ 25 | 26 | 作为使用平台,第三方可注册使用平台服务,当客户端登录成功后单一客户端拉取一个全新Token,后续客户端可直接携带 Token访问平台资源,而不需要提供账号和密码信息,同时可在Token到期后请求拉取新 Token。 27 | 28 | __单点登录__ 29 | 30 | 用户在第一次成功登录后,可给用户拉取一个全新Token,同时该用户相关的旧Token立即失效。用户可以使用该 Token 访问其他子系统,如 App 登录后,可 URL 携带 Token 访问 Web 站点资源。 31 | 32 | # 简单使用 33 | 34 | ```Go 35 | import ( 36 | "fmt" 37 | "github.com/ysqi/tokenauth" 38 | ) 39 | func main() { 40 | 41 | if err := tokenauth.UseDeaultStore(); err != nil { 42 | panic(err) 43 | } 44 | defer tokenauth.Store.Close() 45 | 46 | // Ready. 47 | d := &tokenauth.DefaultProvider{} 48 | globalClient := tokenauth.NewAudienceNotStore("globalClient", d.GenerateSecretString) 49 | 50 | //New token 51 | token, err := tokenauth.NewSingleToken("singleID", globalClient, d.GenerateTokenString) 52 | if err != nil { 53 | fmt.Println("generate token fail,", err.Error()) 54 | return 55 | } 56 | Check token 57 | if checkToken, err := tokenauth.ValidateToken(token.Value); err != nil { 58 | fmt.Println("token check did not pass,", err.Error()) 59 | } else { 60 | fmt.Println("token check pass,token Expiration date:", checkToken.DeadLine) 61 | } 62 | 63 | } 64 | ``` 65 | 66 | 1.程序初始化时,需手动选择Store方案 67 | 68 | + 选择默认方案: 69 | ```go 70 | tokenauth.UseDeaultStore(); 71 | ``` 72 | + 选择自定义Store 73 | ```go 74 | if store, err := tokenauth.NewStore(newStoreName, storeConf); err != nil { 75 | panic(err) 76 | }else if err = tokenauth.ChangeTokenStore(store); err != nil { 77 | panic(err) 78 | } 79 | ``` 80 | 81 | 2.定义生成密钥 Secret 和 Token 算法 82 | 83 | + 选择默认算法: 84 | ```go 85 | d := &tokenauth.DefaultProvider{} 86 | secretFunc := d.GenerateSecretString 87 | tokenFunc := d.GenerateTokenString 88 | ``` 89 | 90 | + 选择自定义算法 91 | ```go 92 | secretFunc := func(clientID string) (secretString string) { return "myself secret for all client" } 93 | tokenFunc := func(audience *Audience) string { return "same token string" } 94 | ``` 95 | 96 | 3.使用算法在 Store 中创建存储一个听众(相当于用户) 97 | ```go 98 | client := tokenauth.NewAudienceNotStore("client name", secretFunc) 99 | ``` 100 | 101 | 4.使用算法给用户颁发一个或多个Token 102 | ```go 103 | token, err := tokenauth.NewToken(client, tokenFunc) 104 | if err != nil { 105 | fmt.Println("generate token fail,", err.Error()) 106 | } 107 | 108 | // more ... 109 | t2 ,err := tokenauth.NewToken(client, tokenFunc) 110 | ``` 111 | > 每个Token 都要自己的生命周期,`Store`自动定期清除过期`Token`,默认有效时常为:`tokenauth.TokenPeriod` 112 | 113 | 5.验证 Token String 的有效性 114 | ```go 115 | if checkToken, err := tokenauth.ValidateToken(tokenString); err != nil { 116 | fmt.Println("token check did not pass,", err.Error()) 117 | } else { 118 | fmt.Println("token check pass,token Expiration date:", checkToken.DeadLine) 119 | } 120 | ``` 121 | 6.当然可以主动删除 Token 122 | ```go 123 | err:=tokenauth.Store.DeleteToken(tokenString) 124 | ``` 125 | -------------------------------------------------------------------------------- /tokenauth_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Author YuShuangqi. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tokenauth_test 6 | 7 | import ( 8 | "fmt" 9 | "github.com/ysqi/tokenauth" 10 | . "gopkg.in/check.v1" 11 | "io/ioutil" 12 | "os" 13 | "strings" 14 | "testing" 15 | "time" 16 | ) 17 | 18 | // Hook up gocheck into the "go test" runner. 19 | func Test(t *testing.T) { TestingT(t) } 20 | 21 | type S struct{} 22 | 23 | var _ = Suite(&S{}) 24 | 25 | func (s *S) SetUpSuite(c *C) { 26 | tokenauth.ChangeTokenStore(openBoltStore()) 27 | } 28 | 29 | func NewSecret(clientID string) (Secret string) { 30 | return "TestForSecretString" 31 | } 32 | 33 | func GenerateTokenString(audience *tokenauth.Audience) string { 34 | return "TestForNewTokenString" 35 | } 36 | 37 | func (s *S) TestAudience_New(c *C) { 38 | 39 | audience, err := tokenauth.NewAudience("forTest", NewSecret) 40 | c.Assert(err, IsNil) 41 | c.Assert(audience, NotNil) 42 | c.Assert(audience.Name, Equals, "forTest") 43 | c.Assert(audience.Secret, Equals, "TestForSecretString") 44 | c.Assert(audience.TokenPeriod, Equals, tokenauth.TokenPeriod) 45 | 46 | newAudience, err := tokenauth.Store.GetAudience(audience.ID) 47 | c.Assert(err, IsNil) 48 | c.Assert(newAudience, NotNil) 49 | c.Assert(newAudience, DeepEquals, audience) 50 | } 51 | 52 | func (s *S) TestToken_New(c *C) { 53 | 54 | tokenauth.TokenPeriod = 10 //10s 55 | 56 | audience, _ := tokenauth.NewAudience("forTest", NewSecret) 57 | token, err := tokenauth.NewToken(audience, GenerateTokenString) 58 | c.Assert(err, IsNil) 59 | c.Assert(token, NotNil) 60 | c.Assert(token.ClientID, Equals, audience.ID) 61 | c.Assert(token.Value, Equals, "TestForNewTokenString") 62 | // c.Assert((token.DeadLine - time.Now().Unix()), Equals, int64(tokenauth.TokenPeriod)) 63 | 64 | newToken, err := tokenauth.Store.GetToken(token.Value) 65 | c.Assert(err, IsNil) 66 | c.Assert(newToken, NotNil) 67 | c.Assert(newToken, DeepEquals, token) 68 | } 69 | 70 | func (s *S) TestToken_Valid(c *C) { 71 | 72 | token, err := tokenauth.ValidateToken("") 73 | c.Assert(err, NotNil) 74 | c.Assert(token, IsNil) 75 | 76 | token, err = tokenauth.ValidateToken(" ") 77 | c.Assert(err, NotNil) 78 | c.Assert(token, IsNil) 79 | 80 | token, err = tokenauth.ValidateToken("empty") 81 | c.Assert(err, NotNil) 82 | c.Assert(token, IsNil) 83 | 84 | tokenauth.TokenPeriod = 2 //2s 85 | audience, _ := tokenauth.NewAudience("forTest", NewSecret) 86 | token, _ = tokenauth.NewToken(audience, GenerateTokenString) 87 | 88 | newToken, err := tokenauth.ValidateToken(token.Value) 89 | c.Assert(err, IsNil) 90 | c.Assert(newToken, DeepEquals, token) 91 | 92 | time.Sleep(3 * time.Second) 93 | newToken, err = tokenauth.ValidateToken(token.Value) 94 | c.Assert(err, NotNil) 95 | c.Assert(newToken, NotNil) 96 | c.Assert(newToken.Expired(), Equals, true) 97 | } 98 | 99 | // tempfile returns a temporary file path. 100 | func tempfile() string { 101 | f, _ := ioutil.TempFile("", "bolt-store-") 102 | f.Close() 103 | os.Remove(f.Name()) 104 | // safe string 105 | return strings.Replace(f.Name(), `\`, `/`, -1) 106 | } 107 | 108 | func openBoltStore() *tokenauth.BoltDBFileStore { 109 | st := tokenauth.NewBoltDBFileStore() 110 | file := tempfile() 111 | if err := st.Open(fmt.Sprintf(`{"path":"%s"}`, file)); err != nil { 112 | panic(fmt.Sprintf("cannot init boltdb file %q :%s", file, err.Error())) 113 | } 114 | return st 115 | } 116 | -------------------------------------------------------------------------------- /objectid.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Author YuShuangqi. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tokenauth 6 | 7 | import ( 8 | "crypto/md5" 9 | "crypto/rand" 10 | "encoding/binary" 11 | "encoding/hex" 12 | "fmt" 13 | "io" 14 | "os" 15 | "sync/atomic" 16 | "time" 17 | ) 18 | 19 | // ObjectId is a unique ID identifying a BSON value. It must be exactly 12 bytes long. 20 | // clone from https://github.com/go-mgo/mgo/blob/v2-unstable/bson/bson.go. 21 | // http://www.mongodb.org/display/DOCS/Object+IDs. 22 | type ObjectId string 23 | 24 | // machineId stores machine id generated once and used in subsequent calls 25 | // to NewObjectId function. 26 | var machineId = readMachineId() 27 | 28 | var objectIdCounter uint32 = 0 29 | 30 | // readMachineId generates machine id and puts it into the machineId global 31 | // variable. If this function fails to get the hostname, it will cause 32 | // a runtime error. 33 | func readMachineId() []byte { 34 | var sum [3]byte 35 | id := sum[:] 36 | hostname, err1 := os.Hostname() 37 | if err1 != nil { 38 | _, err2 := io.ReadFull(rand.Reader, id) 39 | if err2 != nil { 40 | panic(fmt.Errorf("cannot get hostname: %v; %v", err1, err2)) 41 | } 42 | return id 43 | } 44 | hw := md5.New() 45 | hw.Write([]byte(hostname)) 46 | copy(id, hw.Sum(nil)) 47 | return id 48 | } 49 | 50 | // NewObjectId returns a new unique ObjectId. 51 | func NewObjectId() ObjectId { 52 | 53 | var b [12]byte 54 | // Timestamp, 4 bytes, big endian 55 | binary.BigEndian.PutUint32(b[:], uint32(time.Now().Unix())) 56 | // Machine, first 3 bytes of md5(hostname) 57 | b[4] = machineId[0] 58 | b[5] = machineId[1] 59 | b[6] = machineId[2] 60 | // Pid, 2 bytes, specs don't specify endianness, but we use big endian. 61 | pid := os.Getpid() 62 | b[7] = byte(pid >> 8) 63 | b[8] = byte(pid) 64 | // Increment, 3 bytes, big endian 65 | i := atomic.AddUint32(&objectIdCounter, 1) 66 | b[9] = byte(i >> 16) 67 | b[10] = byte(i >> 8) 68 | b[11] = byte(i) 69 | return ObjectId(b[:]) 70 | } 71 | 72 | // byteSlice returns byte slice of id from start to end. 73 | // Calling this function with an invalid id will cause a runtime panic. 74 | func (id ObjectId) byteSlice(start, end int) []byte { 75 | if len(id) != 12 { 76 | panic(fmt.Sprintf("Invalid ObjectId: %q", string(id))) 77 | } 78 | return []byte(string(id)[start:end]) 79 | } 80 | 81 | // Time returns the timestamp part of the id. 82 | // It's a runtime error to call this method with an invalid id. 83 | func (id ObjectId) Time() time.Time { 84 | // First 4 bytes of ObjectId is 32-bit big-endian seconds from epoch. 85 | secs := int64(binary.BigEndian.Uint32(id.byteSlice(0, 4))) 86 | return time.Unix(secs, 0) 87 | } 88 | 89 | // Machine returns the 3-byte machine id part of the id. 90 | // It's a runtime error to call this method with an invalid id. 91 | func (id ObjectId) Machine() []byte { 92 | return id.byteSlice(4, 7) 93 | } 94 | 95 | // Pid returns the process id part of the id. 96 | // It's a runtime error to call this method with an invalid id. 97 | func (id ObjectId) Pid() uint16 { 98 | return binary.BigEndian.Uint16(id.byteSlice(7, 9)) 99 | } 100 | 101 | // Counter returns the incrementing value part of the id. 102 | // It's a runtime error to call this method with an invalid id. 103 | func (id ObjectId) Counter() int32 { 104 | b := id.byteSlice(9, 12) 105 | // Counter is stored as big-endian 3-byte value 106 | return int32(uint32(b[0])<<16 | uint32(b[1])<<8 | uint32(b[2])) 107 | } 108 | 109 | // Hex returns a hex representation of the ObjectId. 110 | func (id ObjectId) Hex() string { 111 | return hex.EncodeToString([]byte(id)) 112 | } 113 | -------------------------------------------------------------------------------- /tokenauth.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Author YuShuangqi. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package Token generation and storage management . 6 | // Simple Usage. 7 | // import ( 8 | // "fmt" 9 | // "github.com/ysqi/tokenauth" 10 | // ) 11 | // func main() { 12 | // 13 | // if err := tokenauth.UseDeaultStore(); err != nil { 14 | // panic(err) 15 | // } 16 | // defer tokenauth.Store.Close() 17 | // 18 | // // Ready. 19 | // d := &tokenauth.DefaultProvider{} 20 | // globalClient := tokenauth.NewAudienceNotStore("globalClient", d.GenerateSecretString) 21 | // 22 | // // New token 23 | // token, err := tokenauth.NewSingleToken("singleID", globalClient, d.GenerateTokenString) 24 | // if err != nil { 25 | // fmt.Println("generate token fail,", err.Error()) 26 | // return 27 | // } 28 | // // Check token 29 | // if checkToken, err := tokenauth.ValidateToken(token.Value); err != nil { 30 | // fmt.Println("token check did not pass,", err.Error()) 31 | // } else { 32 | // fmt.Println("token check pass,token Expiration date:", checkToken.DeadLine) 33 | // } 34 | // 35 | // } 36 | // Advanced Usage: 37 | // 38 | // secretFunc := func(clientID string) (secretString string) { 39 | // return "myself secret for all client" 40 | // } 41 | // tokenFunc := func(audience *Audience) string { 42 | // return "same token string" 43 | // } 44 | // globalClient := tokenauth.NewAudienceNotStore("globalClient", secretFunc) 45 | // // New token 46 | // t1, err := tokenauth.NewToken(globalClient, tokenFunc) 47 | // t2, err := tokenauth.NewToken(globalClient, tokenFunc) 48 | package tokenauth 49 | 50 | import ( 51 | "errors" 52 | "time" 53 | ) 54 | 55 | // Token effective time,unti: seconds. 56 | // Defult is 2 Hour. 57 | var TokenPeriod uint64 = 7200 //2hour 58 | 59 | //Global Token Store . 60 | // Default use 61 | var Store TokenStore 62 | 63 | // Change token store and close old store. 64 | // New token and New Audience whill be saved to new store,after use new store. 65 | func ChangeTokenStore(newStore TokenStore) error { 66 | if newStore == nil { 67 | return errors.New("tokenauth: new store is nil.") 68 | } 69 | if Store != nil { 70 | if err := Store.Close(); err != nil { 71 | return err 72 | } 73 | } 74 | Store = newStore 75 | return nil 76 | } 77 | 78 | // Use default store. 79 | // Default use bolt db file, "./data/tokendb.bolt" file open or create 80 | func UseDeaultStore() error { 81 | 82 | s, err := NewStore("default", `{"path":"./data/tokendb.bolt"}`) 83 | if err != nil { 84 | return err 85 | } 86 | return ChangeTokenStore(s) 87 | } 88 | 89 | //Create Secret provider interface 90 | type GenerateSecretString func(clientID string) (secretString string) //returns new secret string. 91 | 92 | //Create token string provider interface 93 | type GenerateTokenString func(audience *Audience) string //returns new token string 94 | 95 | // New audience and this audience will be saved to store. 96 | func NewAudience(name string, secretFunc GenerateSecretString) (*Audience, error) { 97 | 98 | audience := NewAudienceNotStore(name, secretFunc) 99 | 100 | //save to store 101 | if err := Store.SaveAudience(audience); err != nil { 102 | return nil, err 103 | } else { 104 | return audience, nil 105 | } 106 | } 107 | 108 | // Returns a new audience info,not save to store. 109 | func NewAudienceNotStore(name string, secretFunc GenerateSecretString) *Audience { 110 | 111 | audience := &Audience{ 112 | Name: name, 113 | ID: NewObjectId().Hex(), 114 | TokenPeriod: TokenPeriod, 115 | } 116 | audience.Secret = secretFunc(audience.ID) 117 | return audience 118 | } 119 | 120 | // New Token and this new token will be saved to store. 121 | func NewToken(a *Audience, tokenFunc GenerateTokenString) (*Token, error) { 122 | token := &Token{ 123 | ClientID: a.ID, 124 | Value: tokenFunc(a), 125 | } 126 | if a.TokenPeriod == 0 { 127 | token.DeadLine = 0 128 | } else { 129 | token.DeadLine = time.Now().Unix() + int64(a.TokenPeriod) 130 | } 131 | 132 | if err := Store.SaveToken(token); err != nil { 133 | return nil, err 134 | } else { 135 | return token, nil 136 | } 137 | } 138 | 139 | // New Sign Token and this new token will be saved to store. 140 | func NewSingleToken(singleID string, a *Audience, tokenFunc GenerateTokenString) (*Token, error) { 141 | token := &Token{ 142 | SingleID: singleID, 143 | Value: tokenFunc(a), 144 | DeadLine: time.Now().Unix() + int64(a.TokenPeriod), 145 | } 146 | if err := Store.SaveToken(token); err != nil { 147 | return nil, err 148 | } else { 149 | return token, nil 150 | } 151 | } 152 | 153 | // Returns Exist tokenstring or error. 154 | // If token is exist but expired, then delete token and return TokenExpired error. 155 | func ValidateToken(tokenString string) (*Token, error) { 156 | 157 | if len(tokenString) == 0 { 158 | return nil, ERR_TokenEmpty 159 | } 160 | 161 | var token *Token 162 | var err error 163 | 164 | // Get token info 165 | if token, err = Store.GetToken(tokenString); err != nil { 166 | return nil, err 167 | } 168 | 169 | // Check token 170 | if token == nil || len(token.Value) == 0 { 171 | return nil, ERR_InvalidateToken 172 | } 173 | 174 | // Need delete token if token lose effectiveness 175 | if token.Expired() { 176 | if err = Store.DeleteToken(token.Value); err != nil { 177 | return nil, err 178 | } 179 | return token, ERR_TokenExpired 180 | } 181 | 182 | return token, nil 183 | } 184 | 185 | var ( 186 | ERR_InvalidateToken = ValidationError{Code: "40001", Msg: "Invalid token"} 187 | ERR_TokenEmpty = ValidationError{Code: "41001", Msg: "Token is empty"} 188 | ERR_TokenExpired = ValidationError{Code: "42001", Msg: "Token is expired"} 189 | ) 190 | -------------------------------------------------------------------------------- /store_boltdbfile.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Author YuShuangqi. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tokenauth 6 | 7 | import ( 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "github.com/boltdb/bolt" 12 | "os" 13 | "path/filepath" 14 | ) 15 | 16 | // Store implement by boltdb,see:https://github.com/boltdb/bolt 17 | type BoltDBFileStore struct { 18 | Alias string 19 | db *bolt.DB 20 | dbPath string 21 | } 22 | 23 | var ( 24 | // all tokens save in this buckert 25 | buckert_alltokens = []byte("bk_all_tokeninfo") 26 | // a one audience tokens save relation in audience's buckert child bukert. 27 | buckert_oneAudienceTokens = []byte("bk_one_audience_tokens") 28 | // one audience info key 29 | audienceInfoKey = []byte("one_audience") 30 | buckert_singletokens_singledids = []byte("bk_token_singleIDs") 31 | ) 32 | 33 | func (store *BoltDBFileStore) DBPath() string { 34 | return store.dbPath 35 | } 36 | 37 | //delete audience and all tokens of this audience 38 | func (store *BoltDBFileStore) deleteAudience(id string, tx *bolt.Tx) error { 39 | bk := tx.Bucket([]byte(id)) 40 | if bk == nil { 41 | return nil 42 | } 43 | 44 | tokensBk := tx.Bucket(buckert_alltokens) 45 | if tokensBk != nil { 46 | audienceTokensBk := bk.Bucket(buckert_oneAudienceTokens) 47 | err := audienceTokensBk.ForEach(func(k, v []byte) error { 48 | return tokensBk.Delete(k) 49 | }) 50 | if err != nil { 51 | return err 52 | } 53 | } 54 | // Last need delete client bucket 55 | return tx.DeleteBucket([]byte(id)) 56 | } 57 | 58 | // Save audience into store. 59 | // Returns error if error occured during execution. 60 | func (store *BoltDBFileStore) SaveAudience(audience *Audience) error { 61 | 62 | if audience == nil || len(audience.ID) == 0 { 63 | return errors.New("audience id is empty.") 64 | } 65 | 66 | bytes, err := json.Marshal(audience) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | return store.db.Update(func(tx *bolt.Tx) error { 72 | // need delete old audience info before save 73 | if err := store.deleteAudience(audience.ID, tx); err != nil { 74 | return err 75 | } 76 | 77 | bk, err := tx.CreateBucket([]byte(audience.ID)) 78 | if err != nil { 79 | return err 80 | } 81 | if _, err = bk.CreateBucket(buckert_oneAudienceTokens); err != nil { 82 | return err 83 | } 84 | if err = bk.Put(audienceInfoKey, bytes); err != nil { 85 | return err 86 | } 87 | return nil 88 | }) 89 | 90 | } 91 | 92 | // Delete audience and all tokens of audience. 93 | func (store *BoltDBFileStore) DeleteAudience(audienceID string) error { 94 | if len(audienceID) == 0 { 95 | return errors.New("audienceID is emtpty.") 96 | } 97 | 98 | return store.db.Update(func(tx *bolt.Tx) error { 99 | return store.deleteAudience(audienceID, tx) 100 | }) 101 | } 102 | 103 | // Get audience info or returns error. 104 | func (store *BoltDBFileStore) GetAudience(audienceID string) (audience *Audience, err error) { 105 | 106 | if len(audienceID) == 0 { 107 | return nil, errors.New("audienceID is emtpty.") 108 | } 109 | 110 | err = store.db.View(func(tx *bolt.Tx) error { 111 | bk := tx.Bucket([]byte(audienceID)) 112 | // not found 113 | if bk == nil { 114 | return nil 115 | } 116 | bytes := bk.Get(audienceInfoKey) 117 | if bytes == nil { 118 | return nil 119 | } 120 | audience = &Audience{} 121 | if err := json.Unmarshal(bytes, audience); err != nil { 122 | audience = nil 123 | return err 124 | } else { 125 | return nil 126 | } 127 | }) 128 | 129 | return 130 | 131 | } 132 | 133 | // Save token to store. return error when save fail. 134 | // Save token json to store and save the relation of token with client if not single model. 135 | // The first , token must not empty and effectiveness. 136 | // Does not consider concurrency. 137 | func (store *BoltDBFileStore) SaveToken(token *Token) error { 138 | if token == nil || len(token.Value) == 0 { 139 | return errors.New("token tokenString is empty.") 140 | } 141 | if len(token.ClientID) == 0 && len(token.SingleID) == 0 { 142 | return errors.New("token clientid and singleid,It can't be empty") 143 | } 144 | if token.Expired() { 145 | return errors.New("token is expired,not need save.") 146 | } 147 | 148 | //first to get token byte data 149 | tokenBytes, err := json.Marshal(token) 150 | if err != nil { 151 | return err 152 | } 153 | 154 | return store.db.Update(func(tx *bolt.Tx) error { 155 | 156 | bk, err := tx.CreateBucketIfNotExists(buckert_alltokens) 157 | if err != nil { 158 | return err 159 | } 160 | 161 | // Singlge token has no client. But need delete old token 162 | if token.IsSingle() { 163 | if idsBK, err := tx.CreateBucketIfNotExists(buckert_singletokens_singledids); err != nil { 164 | return err 165 | } else { 166 | key := []byte(token.SingleID) 167 | 168 | // Find and delete old token. 169 | oldTokenValueData := idsBK.Get(key) 170 | if oldTokenValueData != nil { 171 | if err = store.deleteToken(string(oldTokenValueData), tx); err != nil { 172 | return err 173 | } 174 | } 175 | 176 | // Save new token relation 177 | if err = idsBK.Put(key, []byte(token.Value)); err != nil { 178 | return err 179 | } 180 | } 181 | 182 | } else { 183 | // Need add token key to client blucket. 184 | // Only save the relation of token with client. 185 | if au := tx.Bucket([]byte(token.ClientID)); au == nil { 186 | return errors.New("can not found audience, not save audience before save token ?") 187 | } else if err = au.Bucket(buckert_oneAudienceTokens).Put([]byte(token.Value), []byte("")); err != nil { 188 | return err 189 | } 190 | } 191 | // Safe check. 192 | if err != nil { 193 | return err 194 | } 195 | err = bk.Put([]byte(token.Value), tokenBytes) 196 | return err 197 | 198 | }) 199 | } 200 | 201 | //Get token info if find in store,or return error 202 | func (store *BoltDBFileStore) GetToken(tokenString string) (token *Token, err error) { 203 | if len(tokenString) == 0 { 204 | return nil, errors.New("tokenString is empty.") 205 | } 206 | 207 | err = store.db.View(func(tx *bolt.Tx) error { 208 | 209 | bk := tx.Bucket(buckert_alltokens) 210 | if bk == nil { 211 | return nil 212 | } 213 | tokenBytes := bk.Get([]byte(tokenString)) 214 | if tokenBytes == nil { 215 | return nil 216 | } 217 | 218 | token = &Token{} 219 | if err := json.Unmarshal(tokenBytes, token); err != nil { 220 | token = nil 221 | return err 222 | } else { 223 | return nil 224 | } 225 | }) 226 | 227 | return 228 | } 229 | 230 | // Delete token 231 | // Returns error if delete token fail. 232 | func (store *BoltDBFileStore) DeleteToken(tokenString string) error { 233 | 234 | if len(tokenString) == 0 { 235 | return errors.New("incompatible tokenString") 236 | } 237 | 238 | return store.db.Update(func(tx *bolt.Tx) error { 239 | return store.deleteToken(tokenString, tx) 240 | }) 241 | } 242 | 243 | // Delete token 244 | // Returns error if delete token fail. 245 | func (store *BoltDBFileStore) deleteToken(tokenString string, tx *bolt.Tx) error { 246 | bk := tx.Bucket(buckert_alltokens) 247 | if bk == nil { 248 | return errors.New("incompatible tokenString") 249 | } 250 | 251 | key := []byte(tokenString) 252 | tokenBytes := bk.Get(key) 253 | // Not found 254 | if tokenBytes == nil { 255 | return errors.New("incompatible tokenString") 256 | } 257 | 258 | err := bk.Delete(key) 259 | if err != nil { 260 | return err 261 | } 262 | 263 | //clear the relation token with client 264 | token := &Token{} 265 | err = json.Unmarshal(tokenBytes, token) 266 | if err == nil && token.IsSingle() == false { 267 | err = tx.Bucket([]byte(token.ClientID)).Bucket(buckert_oneAudienceTokens).Delete(key) 268 | } 269 | return err 270 | } 271 | 272 | // Open db if db is not opened. 273 | // Returns error if open new db fail or close old db fail if exist 274 | func (store *BoltDBFileStore) open(dbPath string) error { 275 | 276 | // Do not open same db again 277 | if store.db != nil && dbPath == store.db.Path() { 278 | return nil 279 | } 280 | 281 | //check file dir path or create dir. 282 | dir := filepath.Dir(dbPath) 283 | if _, err := os.Stat(dir); err != nil { 284 | if os.IsNotExist(err) { 285 | err = os.Mkdir(dir, 0766) 286 | } 287 | if err != nil { 288 | return err 289 | } 290 | } 291 | 292 | if db, err := bolt.Open(dbPath, 0666, nil); err != nil { 293 | return err 294 | } else { 295 | //close old db before use new db 296 | if store.db != nil { 297 | err = store.db.Close() 298 | if err != nil { 299 | db.Close() //need close new db 300 | return errors.New("store: close old db fail," + err.Error()) 301 | } 302 | } 303 | store.db = db 304 | store.dbPath = db.Path() 305 | } 306 | 307 | return nil 308 | } 309 | 310 | // Close bolt db 311 | func (store *BoltDBFileStore) Close() error { 312 | if store.db != nil { 313 | return store.db.Close() 314 | } 315 | return nil 316 | } 317 | 318 | // Delete token if token expired 319 | func (store *BoltDBFileStore) DeleteExpired() { 320 | 321 | if store.db == nil { 322 | return 323 | } 324 | 325 | store.db.View(func(tx *bolt.Tx) error { 326 | // Get all tokens bucket. 327 | bk := tx.Bucket(buckert_alltokens) 328 | if bk == nil { 329 | return nil 330 | } 331 | // Foreach all tokens. 332 | bk.ForEach(func(k, v []byte) error { 333 | token := &Token{} 334 | if err := json.Unmarshal(v, token); err == nil { 335 | // Will delete token when expired 336 | if token.Expired() { 337 | store.DeleteToken(token.Value) 338 | } 339 | } 340 | return nil 341 | }) 342 | return nil 343 | }) 344 | return 345 | 346 | } 347 | 348 | // Init and Open BoltDBF. 349 | // config is json string. 350 | // e.g: 351 | // {"path":"./data/tokenbolt.db"} 352 | func (store *BoltDBFileStore) Open(config string) error { 353 | 354 | if len(config) == 0 { 355 | return errors.New("boltdbStore: bolt db store config is empty") 356 | } 357 | 358 | var cf map[string]string 359 | 360 | if err := json.Unmarshal([]byte(config), &cf); err != nil { 361 | return fmt.Errorf("boltdbStore: unmarshal %p fail:%s", config, err.Error()) 362 | } 363 | 364 | if path, ok := cf["path"]; !ok { 365 | return errors.New("boltdbStore: bolt db store config has no path key.") 366 | } else { 367 | return store.open(path) 368 | } 369 | 370 | } 371 | 372 | // new Bolt DB file store instance. 373 | func NewBoltDBFileStore() *BoltDBFileStore { 374 | 375 | return &BoltDBFileStore{Alias: "BoltDBFileStore"} 376 | } 377 | 378 | func init() { 379 | RegStore("default", NewBoltDBFileStore()) 380 | } 381 | -------------------------------------------------------------------------------- /store_boltdbfile_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Author YuShuangqi. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tokenauth_test 6 | 7 | import ( 8 | "fmt" 9 | "github.com/ysqi/tokenauth" 10 | . "gopkg.in/check.v1" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | func (s *S) TestStore_Bolt_Init(c *C) { 16 | 17 | st := tokenauth.NewBoltDBFileStore() 18 | defer st.Close() 19 | 20 | err := st.Open("") 21 | c.Assert(err, NotNil) 22 | 23 | err = st.Open("path") 24 | c.Assert(err, NotNil) 25 | 26 | err = st.Open("{}") 27 | c.Assert(err, NotNil) 28 | 29 | err = st.Open(`{"goodpath":""}`) 30 | c.Assert(err, NotNil) 31 | 32 | err = st.Open(`{"path":""}`) 33 | c.Assert(err, NotNil) 34 | 35 | file := tempfile() 36 | err = st.Open(fmt.Sprintf(`{"path":"%s"}`, file)) 37 | c.Assert(err, IsNil) 38 | } 39 | 40 | func (s *S) TestStore_Bolt_DBPath(c *C) { 41 | 42 | st := tokenauth.NewBoltDBFileStore() 43 | defer st.Close() 44 | 45 | c.Assert(st.DBPath(), Equals, "") 46 | 47 | file := tempfile() 48 | st.Open(fmt.Sprintf(`{"path":"%s"}`, file)) 49 | c.Assert(st.DBPath(), Equals, file) 50 | } 51 | 52 | func (s *S) TestStore_Bolt_Audience_Save(c *C) { 53 | 54 | st := openBoltStore() 55 | defer st.Close() 56 | 57 | err := st.SaveAudience(nil) 58 | c.Assert(err, NotNil) 59 | 60 | err = st.SaveAudience(&tokenauth.Audience{}) 61 | c.Assert(err, NotNil) 62 | 63 | item := newAudience() 64 | err = st.SaveAudience(item) 65 | c.Assert(err, IsNil) 66 | 67 | newItem, err := st.GetAudience(item.ID) 68 | c.Assert(err, IsNil) 69 | c.Assert(newItem, DeepEquals, item) 70 | } 71 | 72 | func (s *S) TestStore_Bolt_Audience_Save_Repeat(c *C) { 73 | 74 | st := openBoltStore() 75 | defer st.Close() 76 | 77 | item := newAudience() 78 | item.Name = "oldAudience" 79 | item.Secret = "oldSecret" 80 | item.TokenPeriod = 1 81 | st.SaveAudience(item) 82 | 83 | item.Name = "newAudience" 84 | item.Secret = "newSecret" 85 | item.TokenPeriod = 2 86 | st.SaveAudience(item) 87 | 88 | newItem, err := st.GetAudience(item.ID) 89 | c.Assert(err, IsNil) 90 | c.Assert(newItem, DeepEquals, item) 91 | } 92 | 93 | func (s *S) TestStore_Bolt_Audience_Save_RepeatWithToken(c *C) { 94 | 95 | st := openBoltStore() 96 | defer st.Close() 97 | 98 | item := newAudience() 99 | st.SaveAudience(item) 100 | tokens := make([]*tokenauth.Token, 10) 101 | for i := 0; i < 10; i++ { 102 | tokens[i], _ = tokenauth.NewToken(item, keyPorvider.GenerateTokenString) 103 | st.SaveToken(tokens[i]) 104 | } 105 | 106 | //save again 107 | st.SaveAudience(item) 108 | 109 | for i := 0; i < 10; i++ { 110 | newToken, err := st.GetToken(tokens[i].Value) 111 | c.Assert(err, IsNil) 112 | c.Assert(newToken, IsNil) 113 | } 114 | } 115 | 116 | func (s *S) TestStore_Bolt_Audience_Delete(c *C) { 117 | 118 | st := openBoltStore() 119 | defer st.Close() 120 | 121 | err := st.DeleteAudience("") 122 | c.Assert(err, NotNil) 123 | 124 | err = st.DeleteAudience(" ") 125 | c.Assert(err, IsNil) 126 | 127 | err = st.DeleteAudience("empty") 128 | c.Assert(err, IsNil) 129 | 130 | item := newAudience() 131 | st.SaveAudience(item) 132 | err = st.DeleteAudience(item.ID) 133 | c.Assert(err, IsNil) 134 | 135 | newItem, err := st.GetAudience(item.ID) 136 | c.Assert(err, IsNil) 137 | c.Assert(newItem, IsNil) 138 | } 139 | 140 | func (s *S) TestStore_Bolt_Audience_DeleteWithToken(c *C) { 141 | 142 | st := openBoltStore() 143 | defer st.Close() 144 | 145 | item := newAudience() 146 | st.SaveAudience(item) 147 | 148 | tokens := make([]*tokenauth.Token, 10) 149 | for i := 0; i < 10; i++ { 150 | tokens[i], _ = tokenauth.NewToken(item, keyPorvider.GenerateTokenString) 151 | st.SaveToken(tokens[i]) 152 | } 153 | 154 | err := st.DeleteAudience(item.ID) 155 | c.Assert(err, IsNil) 156 | 157 | newItem, err := st.GetAudience(item.ID) 158 | c.Assert(err, IsNil) 159 | c.Assert(newItem, IsNil) 160 | 161 | for i := 0; i < 10; i++ { 162 | newToken, err := st.GetToken(tokens[i].Value) 163 | c.Assert(err, IsNil) 164 | c.Assert(newToken, IsNil) 165 | } 166 | } 167 | 168 | func (s *S) TestStore_Bolt_Audience_Get(c *C) { 169 | 170 | st := openBoltStore() 171 | defer st.Close() 172 | 173 | item, err := st.GetAudience("") 174 | c.Assert(err, NotNil) 175 | 176 | item, err = st.GetAudience(" ") 177 | c.Assert(err, IsNil) 178 | c.Assert(item, IsNil) 179 | 180 | item, err = st.GetAudience("empty") 181 | c.Assert(err, IsNil) 182 | c.Assert(item, IsNil) 183 | 184 | items := make([]*tokenauth.Audience, 10) 185 | for i := 0; i < 10; i++ { 186 | items[i] = newAudience() 187 | st.SaveAudience(items[i]) 188 | } 189 | for i := 0; i < 10; i++ { 190 | newItem, err := st.GetAudience(items[i].ID) 191 | c.Assert(err, IsNil) 192 | c.Assert(newItem, DeepEquals, items[i]) 193 | } 194 | 195 | } 196 | 197 | func (s *S) TestStore_Bolt_Token_Save_Empty(c *C) { 198 | st := openBoltStore() 199 | defer st.Close() 200 | 201 | err := st.SaveToken(nil) 202 | c.Assert(err, NotNil) 203 | 204 | err = st.SaveToken(&tokenauth.Token{}) 205 | c.Assert(err, NotNil) 206 | 207 | err = st.SaveToken(&tokenauth.Token{ClientID: "id"}) 208 | c.Assert(err, NotNil) 209 | 210 | err = st.SaveToken(&tokenauth.Token{Value: "value"}) 211 | c.Assert(err, NotNil) 212 | 213 | err = st.SaveToken(&tokenauth.Token{ClientID: "id", Value: "value"}) 214 | c.Assert(err, NotNil) 215 | 216 | err = st.SaveToken(&tokenauth.Token{ClientID: "id", SingleID: "SingleID", Value: "value"}) 217 | c.Assert(err, NotNil) 218 | 219 | err = st.SaveToken(&tokenauth.Token{SingleID: "SingleID", Value: "value"}) 220 | c.Assert(err, IsNil) 221 | 222 | } 223 | 224 | func (s *S) TestStore_Bolt_Token_Save(c *C) { 225 | st := openBoltStore() 226 | defer st.Close() 227 | 228 | for ii := 0; ii < 10; ii++ { 229 | 230 | item := newAudience() 231 | st.SaveAudience(item) 232 | 233 | tokens := make([]*tokenauth.Token, 10) 234 | for i := 0; i < 10; i++ { 235 | tokens[i], _ = tokenauth.NewToken(item, keyPorvider.GenerateTokenString) 236 | st.SaveToken(tokens[i]) 237 | } 238 | for i := 0; i < 10; i++ { 239 | newToken, err := st.GetToken(tokens[i].Value) 240 | c.Assert(err, IsNil) 241 | c.Assert(newToken, DeepEquals, tokens[i]) 242 | } 243 | } 244 | 245 | } 246 | 247 | func (s *S) TestStore_Bolt_SinglnToken_Save(c *C) { 248 | st := openBoltStore() 249 | defer st.Close() 250 | 251 | item := newAudience() 252 | var err error 253 | for ii := 0; ii < 10; ii++ { 254 | 255 | tokens := make([]*tokenauth.Token, 10) 256 | for i := 0; i < 10; i++ { 257 | tokens[i], err = tokenauth.NewSingleToken(fmt.Sprintf("singleID%d", ii), item, keyPorvider.GenerateTokenString) 258 | c.Assert(err, IsNil) 259 | c.Assert(tokens[i], NotNil) 260 | st.SaveToken(tokens[i]) 261 | } 262 | for i := 0; i < 10; i++ { 263 | newToken, err := st.GetToken(tokens[i].Value) 264 | c.Assert(err, IsNil) 265 | if i != 9 { 266 | c.Assert(newToken, IsNil) 267 | } else { 268 | c.Assert(newToken, DeepEquals, tokens[i]) 269 | } 270 | } 271 | } 272 | 273 | } 274 | 275 | func (s *S) TestStore_Bolt_Token_Save_Effective(c *C) { 276 | st := openBoltStore() 277 | defer st.Close() 278 | 279 | item := newAudience() 280 | st.SaveAudience(item) 281 | 282 | token, _ := tokenauth.NewToken(item, keyPorvider.GenerateTokenString) 283 | 284 | token.DeadLine = 0 285 | err := st.SaveToken(token) 286 | c.Assert(err, IsNil) 287 | 288 | token.DeadLine = time.Now().Unix() - 1 289 | err = st.SaveToken(token) 290 | c.Assert(err, NotNil) 291 | 292 | token.DeadLine = time.Now().Unix() + 1 293 | err = st.SaveToken(token) 294 | c.Assert(err, IsNil) 295 | 296 | } 297 | 298 | func (s *S) TestStore_Bolt_Token_Get_Empty(c *C) { 299 | st := openBoltStore() 300 | defer st.Close() 301 | 302 | token, err := st.GetToken("") 303 | c.Assert(err, NotNil) 304 | c.Assert(token, IsNil) 305 | 306 | token, err = st.GetToken(" ") 307 | c.Assert(err, IsNil) 308 | c.Assert(token, IsNil) 309 | 310 | token, err = st.GetToken("empty") 311 | c.Assert(err, IsNil) 312 | c.Assert(token, IsNil) 313 | 314 | } 315 | 316 | func (s *S) TestStore_Bolt_Token_Effective(c *C) { 317 | st := openBoltStore() 318 | defer st.Close() 319 | 320 | item := newAudience() 321 | st.SaveAudience(item) 322 | 323 | token, _ := tokenauth.NewToken(item, keyPorvider.GenerateTokenString) 324 | token.DeadLine = time.Now().Unix() + 2 325 | st.SaveToken(token) 326 | 327 | newToken, err := st.GetToken(token.Value) 328 | c.Assert(err, IsNil) 329 | c.Assert(newToken, DeepEquals, token) 330 | 331 | time.Sleep(time.Second * 3) 332 | c.Log("current time:", time.Now().Unix(), ",tokenInfo:", token) 333 | 334 | newToken, err = st.GetToken(token.Value) 335 | c.Assert(err, IsNil) 336 | c.Assert(newToken, NotNil) 337 | c.Assert(newToken.Expired(), Equals, true) 338 | } 339 | 340 | func (s *S) TestStore_Bolt_Token_Delete(c *C) { 341 | 342 | st := openBoltStore() 343 | defer st.Close() 344 | 345 | err := st.DeleteToken("") 346 | c.Assert(err, NotNil) 347 | 348 | err = st.DeleteToken(" ") 349 | c.Assert(err, NotNil) 350 | 351 | err = st.DeleteToken("empty") 352 | c.Assert(err, NotNil) 353 | 354 | item := newAudience() 355 | st.SaveAudience(item) 356 | token, _ := tokenauth.NewToken(item, keyPorvider.GenerateTokenString) 357 | st.SaveToken(token) 358 | 359 | err = st.DeleteToken(token.Value) 360 | c.Assert(err, IsNil) 361 | 362 | newToken, err := st.GetToken(token.Value) 363 | c.Assert(err, IsNil) 364 | c.Assert(newToken, IsNil) 365 | } 366 | 367 | func (s *S) T2estStore_Bolt_Token_Get_Complicating(c *C) { 368 | st := openBoltStore() 369 | defer st.Close() 370 | 371 | count := 10 372 | oneJobItemCount := 100000 373 | wg := sync.WaitGroup{} 374 | wg.Add(count * oneJobItemCount) 375 | for job := 0; job < count; job++ { 376 | 377 | item := newAudience() 378 | st.SaveAudience(item) 379 | 380 | tokensChannel := make(chan *tokenauth.Token, oneJobItemCount) 381 | 382 | go func() { 383 | for i := 0; i < oneJobItemCount; i++ { 384 | token, _ := tokenauth.NewToken(item, keyPorvider.GenerateTokenString) 385 | err := st.SaveToken(token) 386 | c.Assert(err, IsNil) 387 | tokensChannel <- token 388 | } 389 | }() 390 | 391 | go func() { 392 | sum := 0 393 | select { 394 | case token := <-tokensChannel: 395 | wg.Done() 396 | newToken, err := st.GetToken(token.Value) 397 | c.Assert(err, IsNil) 398 | c.Assert(newToken, DeepEquals, token) 399 | 400 | err = st.DeleteToken(token.Value) 401 | c.Assert(err, IsNil) 402 | 403 | sum += 1 404 | 405 | if sum == oneJobItemCount { 406 | break 407 | } 408 | } 409 | }() 410 | } 411 | 412 | wg.Wait() 413 | 414 | } 415 | 416 | var keyPorvider = tokenauth.DefaultProvider{} 417 | 418 | func newAudience() *tokenauth.Audience { 419 | item, _ := tokenauth.NewAudience("test", keyPorvider.GenerateSecretString) 420 | return item 421 | } 422 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | --------------------------------------------------------------------------------