├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── generator.go ├── generator_bench_test.go └── generator_test.go /.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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.6 5 | - 1.7 6 | - tip 7 | 8 | matrix: 9 | allow_failures: 10 | - go: tip 11 | 12 | install: 13 | # get dependencies 14 | - go get -t 15 | 16 | # linting tools 17 | - go get github.com/golang/lint/golint 18 | - go get github.com/fzipp/gocyclo 19 | 20 | # code coverage 21 | - go get github.com/axw/gocov/gocov 22 | - go get github.com/mattn/goveralls 23 | - if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi 24 | 25 | script: 26 | # make sure everything actually works 27 | - go build 28 | # make sure code is properly formatted and simplified 29 | - gofmt -l -s . 30 | # check for possible uh ohs 31 | - go vet ./... 32 | # run tests checking for race conditions 33 | - $HOME/gopath/bin/goveralls -service=travis-ci -repotoken=$COVERALLS -v 34 | 35 | after_success: 36 | # dust the code a bit 37 | - golint . 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Steven Berlanga 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fireauth 2 | --- 3 | [![Build Status](https://travis-ci.org/zabawaba99/fireauth.svg?branch=master)](https://travis-ci.org/zabawaba99/fireauth) [![Coverage Status](https://coveralls.io/repos/zabawaba99/fireauth/badge.svg?branch=master)](https://coveralls.io/r/zabawaba99/fireauth?branch=master) 4 | --- 5 | 6 | A Firebase token generator written in Go 7 | 8 | ## Installation 9 | 10 | ```bash 11 | go get -u github.com/zabawaba99/fireauth 12 | ``` 13 | 14 | ## Usage 15 | 16 | Import fireauth 17 | 18 | ```go 19 | import "github.com/zabawaba99/fireauth" 20 | ``` 21 | 22 | Create a TokenGenerator 23 | 24 | ```go 25 | gen := fireauth.New("foo") 26 | ``` 27 | 28 | Generate a token 29 | 30 | ```go 31 | data := fireauth.Data{"uid": "1"} 32 | token, err := gen.CreateToken(data, nil) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | println("my token: ",token) 37 | ``` 38 | 39 | ### Options 40 | 41 | You can also create a token with options 42 | 43 | ```go 44 | data := fireauth.Data{"uid": "1"} 45 | options := &fireauth.Option{ 46 | NotBefore: 2, 47 | Expiration: 3, 48 | Admin: false, 49 | Debug: true, 50 | } 51 | token, err := gen.CreateToken(data, options) 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | println("my token: ",token) 56 | ``` 57 | 58 | Check the [GoDocs](http://godoc.org/github.com/zabawaba99/fireauth) or 59 | [Firebase Auth Documentation](https://www.firebase.com/docs/rest/guide/user-auth.html#section-overview) for more details 60 | 61 | ## Contributing 62 | 63 | 1. Fork it 64 | 2. Create your feature branch (`git checkout -b new-feature`) 65 | 3. Commit your changes (`git commit -am 'Some cool reflection'`) 66 | 4. Push to the branch (`git push origin new-feature`) 67 | 5. Create new Pull Request 68 | -------------------------------------------------------------------------------- /generator.go: -------------------------------------------------------------------------------- 1 | package fireauth 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "encoding/json" 8 | "errors" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | const ( 14 | // Version used for creating token 15 | Version = 0 16 | // TokenSep used as a delimiter for the token 17 | TokenSep = "." 18 | // MaxUIDLen is the maximum length for an UID 19 | MaxUIDLen = 256 20 | ) 21 | 22 | // Firebase specific values for header 23 | const ( 24 | TokenAlgorithm = "HS256" 25 | TokenType = "JWT" 26 | ) 27 | 28 | var encodedHeader = encode([]byte(`{"alg": "` + TokenAlgorithm + `", "typ": "` + TokenType + `"}`)) 29 | 30 | // Generic errors 31 | var ( 32 | ErrNoUIDKey = errors.New(`Data payload must contain a "uid" key`) 33 | ErrUIDNotString = errors.New(`Data payload key "uid" must be a string`) 34 | ErrUIDTooLong = errors.New(`Data payload key "uid" must not be longer than 256 characters`) 35 | ErrEmptyDataNoOptions = errors.New("Data is empty and no options are set. This token will have no effect on Firebase.") 36 | ErrTokenTooLong = errors.New("Generated token is too long. The token cannot be longer than 1024 bytes.") 37 | ) 38 | 39 | // Generator represents a token generator 40 | type Generator struct { 41 | secret string 42 | } 43 | 44 | // Option represent the claims used when creating an authentication token 45 | // https://www.firebase.com/docs/rest/guide/user-auth.html#section-rest-tokens-without-helpers 46 | type Option struct { 47 | // NotBefote is the token "not before" date as a number of seconds since the Unix epoch. 48 | // If specified, the token will not be considered valid until after this date. 49 | NotBefore int64 `json:"nbf,omitempty"` 50 | 51 | // Expiration is the token expiration date as a number of seconds since the Unix epoch. 52 | // If not specified, by default the token will expire 24 hours after the "issued at" date (iat). 53 | Expiration int64 `json:"exp,omitempty"` 54 | 55 | // Admin when set to true to make this an "admin" token, which grants full read and 56 | // write access to all data. 57 | Admin bool `json:"admin,omitempty"` 58 | 59 | // Debug when set to true to enable debug mode, which provides verbose error messages 60 | // when Security and Firebase Rules fail. 61 | Debug bool `json:"debug,omitempty"` 62 | } 63 | 64 | // Data is used to create a token. The token data can contain any data of your choosing, 65 | // however it must contain a `uid` key, which must be a string of less than 256 characters 66 | type Data map[string]interface{} 67 | 68 | // New creates a new Generator 69 | func New(secret string) *Generator { 70 | return &Generator{ 71 | secret: secret, 72 | } 73 | } 74 | 75 | func generateClaim(data Data, options *Option, issuedAt int64) ([]byte, error) { 76 | // setup the claims for the token 77 | return json.Marshal(struct { 78 | *Option 79 | Version int `json:"v"` 80 | Data Data `json:"d"` 81 | IssuedAt int64 `json:"iat"` 82 | }{ 83 | Option: options, 84 | Version: Version, 85 | Data: data, 86 | IssuedAt: issuedAt, 87 | }) 88 | } 89 | 90 | // CreateToken generates a new token with the given Data and options 91 | func (t *Generator) CreateToken(data Data, options *Option) (string, error) { 92 | if options == nil { 93 | options = new(Option) 94 | } 95 | 96 | // make sure we have valid parameters 97 | if data == nil && !options.Admin && !options.Debug { 98 | return "", ErrEmptyDataNoOptions 99 | } 100 | 101 | // validate the data 102 | if err := validate(data, options.Admin); err != nil { 103 | return "", err 104 | } 105 | 106 | claim, err := generateClaim(data, options, time.Now().UTC().Unix()) 107 | if err != nil { 108 | return "", err 109 | } 110 | 111 | // create the token 112 | secureString := encodedHeader + TokenSep + encode(claim) 113 | signature := sign(secureString, t.secret) 114 | token := secureString + TokenSep + signature 115 | 116 | if len(token) > 1024 { 117 | return "", ErrTokenTooLong 118 | } 119 | return token, nil 120 | } 121 | 122 | func validate(data Data, isAdmin bool) error { 123 | uid, containsID := data["uid"] 124 | if !containsID && !isAdmin { 125 | return ErrNoUIDKey 126 | } 127 | 128 | if _, isString := uid.(string); containsID && !isString { 129 | return ErrUIDNotString 130 | } 131 | 132 | if containsID && len(uid.(string)) > MaxUIDLen { 133 | return ErrUIDTooLong 134 | } 135 | return nil 136 | } 137 | 138 | func encode(data []byte) string { 139 | return strings.Replace(base64.URLEncoding.EncodeToString(data), "=", "", -1) 140 | } 141 | 142 | func sign(message, secret string) string { 143 | h := hmac.New(sha256.New, []byte(secret)) 144 | h.Write([]byte(message)) 145 | return encode(h.Sum(nil)) 146 | } 147 | -------------------------------------------------------------------------------- /generator_bench_test.go: -------------------------------------------------------------------------------- 1 | package fireauth 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func BenchmarkGenerateCreateToken(b *testing.B) { 9 | gen := New("some-secret") 10 | data := Data{"uid": "1"} 11 | opts := &Option{NotBefore: time.Now().Unix(), Expiration: time.Now().Add(time.Hour).Unix()} 12 | 13 | b.ResetTimer() 14 | for i := 0; i < b.N; i++ { 15 | _, err := gen.CreateToken(data, opts) 16 | if err != nil { 17 | b.Fatal(err) 18 | } 19 | } 20 | } 21 | 22 | func BenchmarkSign(b *testing.B) { 23 | for i := 0; i < b.N; i++ { 24 | _ = sign("message", "secret") 25 | } 26 | } 27 | 28 | func BenchmarkGenerateClaim(b *testing.B) { 29 | for i := 0; i < b.N; i++ { 30 | _, err := generateClaim(Data{"uid": "42"}, &Option{Admin: true}, 0) 31 | if err != nil { 32 | b.Fatal(err) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /generator_test.go: -------------------------------------------------------------------------------- 1 | package fireauth 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "encoding/json" 7 | "reflect" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestNew(t *testing.T) { 13 | if gen := New("foo"); gen == nil { 14 | t.Fatal("generator should not be nil") 15 | } 16 | } 17 | 18 | func TestCreateTokenData(t *testing.T) { 19 | gen := New("foo") 20 | data := Data{"uid": "1"} 21 | 22 | token, err := gen.CreateToken(data, nil) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | tokenParts := strings.Split(token, TokenSep) 28 | if len(tokenParts) != 3 { 29 | t.Fatal("token is not composed correctly") 30 | } 31 | 32 | bytes, err := base64.URLEncoding.DecodeString(tokenParts[1] + "==") 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | claim := struct { 38 | Version int `json:"v"` 39 | Data Data `json:"d"` 40 | IssuedAt int64 `json:"iat"` 41 | }{} 42 | if err := json.Unmarshal(bytes, &claim); err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | if claim.Version != Version { 47 | t.Fatalf("Expected: %d\nActual: %d", Version, claim.Version) 48 | } 49 | 50 | if !reflect.DeepEqual(data, claim.Data) { 51 | t.Fatalf("auth data is not the same.Expected: %s\nActual: %s", data, claim.Data) 52 | } 53 | } 54 | 55 | func TestCreateTokenFailure(t *testing.T) { 56 | if _, err := New("foo").CreateToken(nil, nil); err == nil { 57 | t.Fatal("CreateToken without data nor option should fail") 58 | } 59 | if _, err := New("foo").CreateToken(Data{}, nil); err == nil { 60 | t.Fatal("CreateToken with invalid data should fail") 61 | } 62 | ch := make(chan struct{}) 63 | defer close(ch) 64 | if _, err := New("foo").CreateToken(Data{"uid": "1234", "invalid": ch}, &Option{}); err == nil { 65 | t.Fatal("Invalid data types should make the token creation fail") 66 | } 67 | } 68 | 69 | func TestCreateTokenAdminNoData(t *testing.T) { 70 | if _, err := New("foo").CreateToken(nil, &Option{Admin: true}); err != nil { 71 | t.Fatal(err) 72 | } 73 | } 74 | 75 | func TestCreateTokenTooLong(t *testing.T) { 76 | if _, err := New("foo").CreateToken(Data{"uid": "1", "bigKey": randData(t, 1024)}, nil); err == nil { 77 | t.Fatal("Token too long should have failed") 78 | } 79 | } 80 | 81 | func randData(t *testing.T, size int) string { 82 | alphanum := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 83 | var bytes = make([]byte, size) 84 | if _, err := rand.Read(bytes); err != nil { 85 | t.Fatal(err) 86 | } 87 | for i, b := range bytes { 88 | bytes[i] = alphanum[b%byte(len(alphanum))] 89 | } 90 | return string(bytes) 91 | } 92 | 93 | func TestValidate(t *testing.T) { 94 | if err := validate(nil, false); err != ErrNoUIDKey { 95 | t.Fatalf("Unexpected error. Expected: %s, Got: %v", ErrNoUIDKey, err) 96 | } 97 | if err := validate(Data{"uid": 42}, true); err != ErrUIDNotString { 98 | t.Fatalf("Unexpected error. Expected: %s, Got: %v", ErrUIDNotString, err) 99 | } 100 | if err := validate(Data{"uid": strings.Repeat(" ", MaxUIDLen+1)}, true); err != ErrUIDTooLong { 101 | t.Fatalf("Unexpected error. Expected: %s, Got: %v", ErrUIDTooLong, err) 102 | } 103 | // No uid in admin mode should not fail. 104 | if err := validate(Data{}, true); err != nil { 105 | t.Fatal(err) 106 | } 107 | } 108 | 109 | func TestGenerateClaim(t *testing.T) { 110 | buf, err := generateClaim(Data{"uid": "42"}, &Option{Admin: true}, 0) 111 | if err != nil { 112 | t.Fatal(err) 113 | } 114 | 115 | if expect, got := `{"admin":true,"v":0,"d":{"uid":"42"},"iat":0}`, string(buf); expect != got { 116 | t.Fatalf("Unexpected claim\nExpect:\t%s\nGot:\t%s", expect, got) 117 | } 118 | } 119 | 120 | func TestSign(t *testing.T) { 121 | g := struct { 122 | key string 123 | in string 124 | out string 125 | }{ 126 | key: "you cannot see me", 127 | in: "the winter is comming", 128 | out: "ytb5HiGUKtRhJg02DXS-serVBwbxud08FFNcx6dty78", 129 | // use the following code segmentation to generate the output 130 | // h := hmac.New(sha256.New, []byte(key)) 131 | // h.Write([]byte(in)) 132 | // out := encode(h.Sum(nil)) 133 | } 134 | 135 | got := sign(g.in, g.key) 136 | if g.out != got { 137 | t.Fatalf("expect:%v, but got:%v", g.out, got) 138 | } 139 | } 140 | --------------------------------------------------------------------------------