From cdfec98cf4f2b2cdb6259b529c09df82a30f379b Mon Sep 17 00:00:00 2001 From: Brad Rydzewski Date: Wed, 9 Sep 2015 14:05:52 -0700 Subject: [PATCH] cleaned up the token implementation for #1175 --- Godeps/Godeps.json | 4 +- .../github.com/dgrijalva/jwt-go/.travis.yml | 7 + .../src/github.com/dgrijalva/jwt-go/README.md | 14 +- .../dgrijalva/jwt-go/VERSION_HISTORY.md | 5 + .../dgrijalva/jwt-go/cmd/jwt/app.go | 2 +- .../src/github.com/dgrijalva/jwt-go/ecdsa.go | 136 ++++++++++ .../github.com/dgrijalva/jwt-go/ecdsa_test.go | 100 +++++++ .../dgrijalva/jwt-go/ecdsa_utils.go | 67 +++++ .../src/github.com/dgrijalva/jwt-go/errors.go | 43 +++ .../dgrijalva/jwt-go/example_test.go | 2 +- .../github.com/dgrijalva/jwt-go/hmac_test.go | 14 +- .../src/github.com/dgrijalva/jwt-go/jwt.go | 39 --- .../github.com/dgrijalva/jwt-go/jwt_test.go | 15 +- .../github.com/dgrijalva/jwt-go/rsa_pss.go | 126 +++++++++ .../dgrijalva/jwt-go/rsa_pss_test.go | 96 +++++++ .../github.com/dgrijalva/jwt-go/rsa_test.go | 32 ++- .../dgrijalva/jwt-go/test/ec256-private.pem | 5 + .../dgrijalva/jwt-go/test/ec256-public.pem | 4 + .../dgrijalva/jwt-go/test/ec384-private.pem | 6 + .../dgrijalva/jwt-go/test/ec384-public.pem | 5 + .../dgrijalva/jwt-go/test/ec512-private.pem | 7 + .../dgrijalva/jwt-go/test/ec512-public.pem | 6 + cmd/drone-server/drone.go | 17 +- pkg/hash/hash.go | 12 - pkg/remote/builtin/gitlab/gitlab.go | 9 +- pkg/server/gitlab.go | 22 +- pkg/server/hooks.go | 14 +- pkg/server/login.go | 17 +- pkg/server/repos.go | 12 +- pkg/server/server.go | 52 +--- pkg/server/session/session.go | 107 -------- pkg/server/token.go | 68 ----- pkg/server/token_test.go | 120 --------- pkg/server/user.go | 23 +- pkg/server/users.go | 10 +- pkg/store/builtin/store.go | 4 +- pkg/store/builtin/token.go | 43 --- pkg/store/builtin/token_sql.go | 255 ------------------ pkg/store/builtin/token_test.go | 149 ---------- pkg/store/mock/mock.go | 47 ---- pkg/store/store.go | 19 -- pkg/token/token.go | 98 +++++++ pkg/token/token_test.go | 1 + pkg/types/token.go | 18 -- pkg/types/util.go | 14 +- pkg/types/util_test.go | 7 - 46 files changed, 871 insertions(+), 1002 deletions(-) create mode 100644 Godeps/_workspace/src/github.com/dgrijalva/jwt-go/.travis.yml create mode 100644 Godeps/_workspace/src/github.com/dgrijalva/jwt-go/ecdsa.go create mode 100644 Godeps/_workspace/src/github.com/dgrijalva/jwt-go/ecdsa_test.go create mode 100644 Godeps/_workspace/src/github.com/dgrijalva/jwt-go/ecdsa_utils.go create mode 100644 Godeps/_workspace/src/github.com/dgrijalva/jwt-go/errors.go create mode 100644 Godeps/_workspace/src/github.com/dgrijalva/jwt-go/rsa_pss.go create mode 100644 Godeps/_workspace/src/github.com/dgrijalva/jwt-go/rsa_pss_test.go create mode 100644 Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/ec256-private.pem create mode 100644 Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/ec256-public.pem create mode 100644 Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/ec384-private.pem create mode 100644 Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/ec384-public.pem create mode 100644 Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/ec512-private.pem create mode 100644 Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/ec512-public.pem delete mode 100644 pkg/hash/hash.go delete mode 100644 pkg/server/session/session.go delete mode 100644 pkg/server/token.go delete mode 100644 pkg/server/token_test.go delete mode 100644 pkg/store/builtin/token.go delete mode 100644 pkg/store/builtin/token_sql.go delete mode 100644 pkg/store/builtin/token_test.go create mode 100644 pkg/token/token.go create mode 100644 pkg/token/token_test.go delete mode 100644 pkg/types/token.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index e47a2b549..11e1ef85a 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -25,8 +25,8 @@ }, { "ImportPath": "github.com/dgrijalva/jwt-go", - "Comment": "v2.2.0-16-gc48cfd5", - "Rev": "c48cfd5d9711c75acb6036d2698ef3aef7bb655a" + "Comment": "v2.3.0-4-gc1da563", + "Rev": "c1da56349675b292d3200463e2c88b9aa5e02391" }, { "ImportPath": "github.com/elazarl/go-bindata-assetfs", diff --git a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/.travis.yml b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/.travis.yml new file mode 100644 index 000000000..d6089146c --- /dev/null +++ b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/.travis.yml @@ -0,0 +1,7 @@ +language: go + +go: + - 1.3.3 + - 1.4.2 + - 1.5 + - tip diff --git a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/README.md b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/README.md index f8321b094..001c0a338 100644 --- a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/README.md +++ b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/README.md @@ -1,5 +1,7 @@ A [go](http://www.golang.org) (or 'golang' for search engine friendliness) implementation of [JSON Web Tokens](http://self-issued.info/docs/draft-jones-json-web-token.html) +[![Build Status](https://travis-ci.org/dgrijalva/jwt-go.svg?branch=master)](https://travis-ci.org/dgrijalva/jwt-go) + **NOTICE:** A vulnerability in JWT was [recently published](https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/). As this library doesn't force users to validate the `alg` is what they expected, it's possible your usage is effected. There will be an update soon to remedy this, and it will likey require backwards-incompatible changes to the API. In the short term, please make sure your implementation verifies the `alg` is what you expect. ## What the heck is a JWT? @@ -21,8 +23,8 @@ Parsing and verifying tokens is pretty straight forward. You pass in the token ```go token, err := jwt.Parse(myToken, func(token *jwt.Token) (interface{}, error) { // Don't forget to validate the alg is what you expect: - if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok { - return nil, fmt.Errorf("Unexpected signing method: %v", t.Header["alg"]) + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } return myLookupKey(token.Header["kid"]) }) @@ -46,12 +48,20 @@ Parsing and verifying tokens is pretty straight forward. You pass in the token tokenString, err := token.SignedString(mySigningKey) ``` +## Extensions + +This library publishes all the necessary components for adding your own signing methods. Simply implement the `SigningMethod` interface and register a factory method using `RegisterSigningMethod`. + +Here's an example of an extension that integrates with the Google App Engine signing tools: https://github.com/someone1/gcp-jwt-go + ## Project Status & Versioning This library is considered production ready. Feedback and feature requests are appreciated. The API should be considered stable. There should be very few backwards-incompatible changes outside of major version updates (and only with good reason). This project uses [Semantic Versioning 2.0.0](http://semver.org). Accepted pull requests will land on `master`. Periodically, versions will be tagged from `master`. You can find all the releases on [the project releases page](https://github.com/dgrijalva/jwt-go/releases). +While we try to make it obvious when we make breaking changes, there isn't a great mechanism for pushing announcements out to users. You may want to use this alternative package include: `gopkg.in/dgrijalva/jwt-go.v2`. It will do the right thing WRT semantic versioning. + ## More Documentation can be found [on godoc.org](http://godoc.org/github.com/dgrijalva/jwt-go). diff --git a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/VERSION_HISTORY.md b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/VERSION_HISTORY.md index 972722ee1..5aa3b139f 100644 --- a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/VERSION_HISTORY.md +++ b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/VERSION_HISTORY.md @@ -1,5 +1,10 @@ ## `jwt-go` Version History +#### 2.3.0 + +* Added support for ECDSA signing methods +* Added support for RSA PSS signing methods (requires go v1.4) + #### 2.2.0 * Gracefully handle a `nil` `Keyfunc` being passed to `Parse`. Result will now be the parsed token and an error, instead of a panic. diff --git a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/cmd/jwt/app.go b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/cmd/jwt/app.go index e9a12c3c2..62cb9a46e 100644 --- a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/cmd/jwt/app.go +++ b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/cmd/jwt/app.go @@ -15,7 +15,7 @@ import ( "os" "regexp" - "github.com/drone/drone/Godeps/_workspace/src/github.com/dgrijalva/jwt-go" + "github.com/dgrijalva/jwt-go" ) var ( diff --git a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/ecdsa.go b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/ecdsa.go new file mode 100644 index 000000000..9cd538ff7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/ecdsa.go @@ -0,0 +1,136 @@ +package jwt + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "encoding/asn1" + "errors" + "math/big" +) + +var ( + // Sadly this is missing from crypto/ecdsa compared to crypto/rsa + ErrECDSAVerification = errors.New("crypto/ecdsa: verification error") +) + +// Implements the ECDSA family of signing methods signing methods +type SigningMethodECDSA struct { + Name string + Hash crypto.Hash +} + +// Marshalling structure for r, s EC point +type ECPoint struct { + R *big.Int + S *big.Int +} + +// Specific instances for EC256 and company +var ( + SigningMethodES256 *SigningMethodECDSA + SigningMethodES384 *SigningMethodECDSA + SigningMethodES512 *SigningMethodECDSA +) + +func init() { + // ES256 + SigningMethodES256 = &SigningMethodECDSA{"ES256", crypto.SHA256} + RegisterSigningMethod(SigningMethodES256.Alg(), func() SigningMethod { + return SigningMethodES256 + }) + + // ES384 + SigningMethodES384 = &SigningMethodECDSA{"ES384", crypto.SHA384} + RegisterSigningMethod(SigningMethodES384.Alg(), func() SigningMethod { + return SigningMethodES384 + }) + + // ES512 + SigningMethodES512 = &SigningMethodECDSA{"ES512", crypto.SHA512} + RegisterSigningMethod(SigningMethodES512.Alg(), func() SigningMethod { + return SigningMethodES512 + }) +} + +func (m *SigningMethodECDSA) Alg() string { + return m.Name +} + +// Implements the Verify method from SigningMethod +// For this verify method, key must be an ecdsa.PublicKey struct +func (m *SigningMethodECDSA) Verify(signingString, signature string, key interface{}) error { + var err error + + // Decode the signature + var sig []byte + if sig, err = DecodeSegment(signature); err != nil { + return err + } + + // Get the key + var ecdsaKey *ecdsa.PublicKey + switch k := key.(type) { + case *ecdsa.PublicKey: + ecdsaKey = k + default: + return ErrInvalidKey + } + + // Unmarshal asn1 ECPoint + var ecpoint = new(ECPoint) + if _, err := asn1.Unmarshal(sig, ecpoint); err != nil { + return err + } + + // Create hasher + if !m.Hash.Available() { + return ErrHashUnavailable + } + hasher := m.Hash.New() + hasher.Write([]byte(signingString)) + + // Verify the signature + if verifystatus := ecdsa.Verify(ecdsaKey, hasher.Sum(nil), ecpoint.R, ecpoint.S); verifystatus == true { + return nil + } else { + return ErrECDSAVerification + } +} + +// Implements the Sign method from SigningMethod +// For this signing method, key must be an ecdsa.PrivateKey struct +func (m *SigningMethodECDSA) Sign(signingString string, key interface{}) (string, error) { + // Get the key + var ecdsaKey *ecdsa.PrivateKey + switch k := key.(type) { + case *ecdsa.PrivateKey: + ecdsaKey = k + default: + return "", ErrInvalidKey + } + + // Create the hasher + if !m.Hash.Available() { + return "", ErrHashUnavailable + } + + hasher := m.Hash.New() + hasher.Write([]byte(signingString)) + + // Sign the string and return r, s + if r, s, err := ecdsa.Sign(rand.Reader, ecdsaKey, hasher.Sum(nil)); err == nil { + // asn1 marhsal r, s using ecPoint as the structure + var ecpoint = new(ECPoint) + ecpoint.R = r + ecpoint.S = s + + if signature, err := asn1.Marshal(*ecpoint); err != nil { + return "", err + } else { + return EncodeSegment(signature), nil + } + } else { + return "", err + } +} diff --git a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/ecdsa_test.go b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/ecdsa_test.go new file mode 100644 index 000000000..98e3e5edb --- /dev/null +++ b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/ecdsa_test.go @@ -0,0 +1,100 @@ +package jwt_test + +import ( + "crypto/ecdsa" + "io/ioutil" + "strings" + "testing" + + "github.com/dgrijalva/jwt-go" +) + +var ecdsaTestData = []struct { + name string + keys map[string]string + tokenString string + alg string + claims map[string]interface{} + valid bool +}{ + { + "Basic ES256", + map[string]string{"private": "test/ec256-private.pem", "public": "test/ec256-public.pem"}, + "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.MEQCIHoSJnmGlPaVQDqacx_2XlXEhhqtWceVopjomc2PJLtdAiAUTeGPoNYxZw0z8mgOnnIcjoxRuNDVZvybRZF3wR1l8w", + "ES256", + map[string]interface{}{"foo": "bar"}, + true, + }, + { + "Basic ES384", + map[string]string{"private": "test/ec384-private.pem", "public": "test/ec384-public.pem"}, + "eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.MGUCMQCHBr61FXDuFY9xUhyp8iWQAuBIaSgaf1z2j_8XrKcCfzTPzoSa3SZKq-m3L492xe8CMG3kafRMeuaN5Aw8ZJxmOLhkTo4D3-LaGzcaUWINvWvkwFMl7dMC863s0gov6xvXuA", + "ES384", + map[string]interface{}{"foo": "bar"}, + true, + }, + { + "Basic ES512", + map[string]string{"private": "test/ec512-private.pem", "public": "test/ec512-public.pem"}, + "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.MIGIAkIAmVKjdJE5lG1byOFgZZVTeNDRp6E7SNvUj0UrvpzoBH6nrleWVTcwfHzbwWuooNpPADDSFR_Ql3ze-Vwwi8hBqQsCQgHn-ZooL8zegkOVeEEsqd7WHWdhb8UekFCYw3X8JnNP-D3wvZQ1-tkkHakt5gZ2-xO29TxfSPun4ViGkMYa7Q4N-Q", + "ES512", + map[string]interface{}{"foo": "bar"}, + true, + }, + { + "basic ES256 invalid: foo => bar", + map[string]string{"private": "test/ec256-private.pem", "public": "test/ec256-public.pem"}, + "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.MEQCIHoSJnmGlPaVQDqacx_2XlXEhhqtWceVopjomc2PJLtdAiAUTeGPoNYxZw0z8mgOnnIcjoxRuNDVZvybRZF3wR1l8W", + "ES256", + map[string]interface{}{"foo": "bar"}, + false, + }, +} + +func TestECDSAVerify(t *testing.T) { + for _, data := range ecdsaTestData { + var err error + + key, _ := ioutil.ReadFile(data.keys["public"]) + + var ecdsaKey *ecdsa.PublicKey + if ecdsaKey, err = jwt.ParseECPublicKeyFromPEM(key); err != nil { + t.Errorf("Unable to parse ECDSA public key: %v", err) + } + + parts := strings.Split(data.tokenString, ".") + + method := jwt.GetSigningMethod(data.alg) + err = method.Verify(strings.Join(parts[0:2], "."), parts[2], ecdsaKey) + if data.valid && err != nil { + t.Errorf("[%v] Error while verifying key: %v", data.name, err) + } + if !data.valid && err == nil { + t.Errorf("[%v] Invalid key passed validation", data.name) + } + } +} + +func TestECDSASign(t *testing.T) { + for _, data := range ecdsaTestData { + var err error + key, _ := ioutil.ReadFile(data.keys["private"]) + + var ecdsaKey *ecdsa.PrivateKey + if ecdsaKey, err = jwt.ParseECPrivateKeyFromPEM(key); err != nil { + t.Errorf("Unable to parse ECDSA private key: %v", err) + } + + if data.valid { + parts := strings.Split(data.tokenString, ".") + method := jwt.GetSigningMethod(data.alg) + sig, err := method.Sign(strings.Join(parts[0:2], "."), ecdsaKey) + if err != nil { + t.Errorf("[%v] Error signing token: %v", data.name, err) + } + if sig == parts[2] { + t.Errorf("[%v] Identical signatures\nbefore:\n%v\nafter:\n%v", data.name, parts[2], sig) + } + } + } +} diff --git a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/ecdsa_utils.go b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/ecdsa_utils.go new file mode 100644 index 000000000..d19624b72 --- /dev/null +++ b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/ecdsa_utils.go @@ -0,0 +1,67 @@ +package jwt + +import ( + "crypto/ecdsa" + "crypto/x509" + "encoding/pem" + "errors" +) + +var ( + ErrNotECPublicKey = errors.New("Key is not a valid ECDSA public key") + ErrNotECPrivateKey = errors.New("Key is not a valid ECDSA private key") +) + +// Parse PEM encoded Elliptic Curve Private Key Structure +func ParseECPrivateKeyFromPEM(key []byte) (*ecdsa.PrivateKey, error) { + var err error + + // Parse PEM block + var block *pem.Block + if block, _ = pem.Decode(key); block == nil { + return nil, ErrKeyMustBePEMEncoded + } + + // Parse the key + var parsedKey interface{} + if parsedKey, err = x509.ParseECPrivateKey(block.Bytes); err != nil { + return nil, err + } + + var pkey *ecdsa.PrivateKey + var ok bool + if pkey, ok = parsedKey.(*ecdsa.PrivateKey); !ok { + return nil, ErrNotECPrivateKey + } + + return pkey, nil +} + +// Parse PEM encoded PKCS1 or PKCS8 public key +func ParseECPublicKeyFromPEM(key []byte) (*ecdsa.PublicKey, error) { + var err error + + // Parse PEM block + var block *pem.Block + if block, _ = pem.Decode(key); block == nil { + return nil, ErrKeyMustBePEMEncoded + } + + // Parse the key + var parsedKey interface{} + if parsedKey, err = x509.ParsePKIXPublicKey(block.Bytes); err != nil { + if cert, err := x509.ParseCertificate(block.Bytes); err == nil { + parsedKey = cert.PublicKey + } else { + return nil, err + } + } + + var pkey *ecdsa.PublicKey + var ok bool + if pkey, ok = parsedKey.(*ecdsa.PublicKey); !ok { + return nil, ErrNotECPublicKey + } + + return pkey, nil +} diff --git a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/errors.go b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/errors.go new file mode 100644 index 000000000..e9e788ff9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/errors.go @@ -0,0 +1,43 @@ +package jwt + +import ( + "errors" +) + +// Error constants +var ( + ErrInvalidKey = errors.New("key is invalid or of invalid type") + ErrHashUnavailable = errors.New("the requested hash function is unavailable") + ErrNoTokenInRequest = errors.New("no token present in request") +) + +// The errors that might occur when parsing and validating a token +const ( + ValidationErrorMalformed uint32 = 1 << iota // Token is malformed + ValidationErrorUnverifiable // Token could not be verified because of signing problems + ValidationErrorSignatureInvalid // Signature validation failed + ValidationErrorExpired // Exp validation failed + ValidationErrorNotValidYet // NBF validation failed +) + +// The error from Parse if token is not valid +type ValidationError struct { + err string + Errors uint32 // bitfield. see ValidationError... constants +} + +// Validation error is an error type +func (e ValidationError) Error() string { + if e.err == "" { + return "token is invalid" + } + return e.err +} + +// No errors +func (e *ValidationError) valid() bool { + if e.Errors > 0 { + return false + } + return true +} diff --git a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/example_test.go b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/example_test.go index 3b2271593..edb48e4db 100644 --- a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/example_test.go +++ b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/example_test.go @@ -2,7 +2,7 @@ package jwt_test import ( "fmt" - "github.com/drone/drone/Godeps/_workspace/src/github.com/dgrijalva/jwt-go" + "github.com/dgrijalva/jwt-go" "time" ) diff --git a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/hmac_test.go b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/hmac_test.go index 3eb672950..c7e114f4f 100644 --- a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/hmac_test.go +++ b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/hmac_test.go @@ -1,7 +1,7 @@ package jwt_test import ( - "github.com/drone/drone/Godeps/_workspace/src/github.com/dgrijalva/jwt-go" + "github.com/dgrijalva/jwt-go" "io/ioutil" "strings" "testing" @@ -77,3 +77,15 @@ func TestHMACSign(t *testing.T) { } } } + +func BenchmarkHS256Signing(b *testing.B) { + benchmarkSigning(b, jwt.SigningMethodHS256, hmacTestKey) +} + +func BenchmarkHS384Signing(b *testing.B) { + benchmarkSigning(b, jwt.SigningMethodHS384, hmacTestKey) +} + +func BenchmarkHS512Signing(b *testing.B) { + benchmarkSigning(b, jwt.SigningMethodHS512, hmacTestKey) +} diff --git a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/jwt.go b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/jwt.go index f7b4322b2..06995aa65 100644 --- a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/jwt.go +++ b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/jwt.go @@ -3,7 +3,6 @@ package jwt import ( "encoding/base64" "encoding/json" - "errors" "net/http" "strings" "time" @@ -20,13 +19,6 @@ var TimeFunc = time.Now // Header of the token (such as `kid`) to identify which key to use. type Keyfunc func(*Token) (interface{}, error) -// Error constants -var ( - ErrInvalidKey = errors.New("key is invalid or of invalid type") - ErrHashUnavailable = errors.New("the requested hash function is unavailable") - ErrNoTokenInRequest = errors.New("no token present in request") -) - // A JWT Token. Different fields will be used depending on whether you're // creating or parsing/verifying a token. type Token struct { @@ -167,37 +159,6 @@ func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { return token, vErr } -// The errors that might occur when parsing and validating a token -const ( - ValidationErrorMalformed uint32 = 1 << iota // Token is malformed - ValidationErrorUnverifiable // Token could not be verified because of signing problems - ValidationErrorSignatureInvalid // Signature validation failed - ValidationErrorExpired // Exp validation failed - ValidationErrorNotValidYet // NBF validation failed -) - -// The error from Parse if token is not valid -type ValidationError struct { - err string - Errors uint32 // bitfield. see ValidationError... constants -} - -// Validation error is an error type -func (e ValidationError) Error() string { - if e.err == "" { - return "token is invalid" - } - return e.err -} - -// No errors -func (e *ValidationError) valid() bool { - if e.Errors > 0 { - return false - } - return true -} - // Try to find the token in an http.Request. // This method will call ParseMultipartForm if there's no token in the header. // Currently, it looks in the Authorization header as well as diff --git a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/jwt_test.go b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/jwt_test.go index 0a1fbaf28..9108dedb4 100644 --- a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/jwt_test.go +++ b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/jwt_test.go @@ -2,7 +2,7 @@ package jwt_test import ( "fmt" - "github.com/drone/drone/Godeps/_workspace/src/github.com/dgrijalva/jwt-go" + "github.com/dgrijalva/jwt-go" "io/ioutil" "net/http" "reflect" @@ -172,3 +172,16 @@ func TestParseRequest(t *testing.T) { } } } + +// Helper method for benchmarking various methods +func benchmarkSigning(b *testing.B, method jwt.SigningMethod, key interface{}) { + t := jwt.New(method) + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if _, err := t.SignedString(key); err != nil { + b.Fatal(err) + } + } + }) + +} diff --git a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/rsa_pss.go b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/rsa_pss.go new file mode 100644 index 000000000..0cedd5ee0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/rsa_pss.go @@ -0,0 +1,126 @@ +// +build go1.4 + +package jwt + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" +) + +// Implements the RSAPSS family of signing methods signing methods +type SigningMethodRSAPSS struct { + *SigningMethodRSA + Options *rsa.PSSOptions +} + +// Specific instances for RS/PS and company +var ( + SigningMethodPS256 *SigningMethodRSAPSS + SigningMethodPS384 *SigningMethodRSAPSS + SigningMethodPS512 *SigningMethodRSAPSS +) + +func init() { + // PS256 + SigningMethodPS256 = &SigningMethodRSAPSS{ + &SigningMethodRSA{ + Name: "PS256", + Hash: crypto.SHA256, + }, + &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, + Hash: crypto.SHA256, + }, + } + RegisterSigningMethod(SigningMethodPS256.Alg(), func() SigningMethod { + return SigningMethodPS256 + }) + + // PS384 + SigningMethodPS384 = &SigningMethodRSAPSS{ + &SigningMethodRSA{ + Name: "PS384", + Hash: crypto.SHA384, + }, + &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, + Hash: crypto.SHA384, + }, + } + RegisterSigningMethod(SigningMethodPS384.Alg(), func() SigningMethod { + return SigningMethodPS384 + }) + + // PS512 + SigningMethodPS512 = &SigningMethodRSAPSS{ + &SigningMethodRSA{ + Name: "PS512", + Hash: crypto.SHA512, + }, + &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, + Hash: crypto.SHA512, + }, + } + RegisterSigningMethod(SigningMethodPS512.Alg(), func() SigningMethod { + return SigningMethodPS512 + }) +} + +// Implements the Verify method from SigningMethod +// For this verify method, key must be an rsa.PrivateKey struct +func (m *SigningMethodRSAPSS) Verify(signingString, signature string, key interface{}) error { + var err error + + // Decode the signature + var sig []byte + if sig, err = DecodeSegment(signature); err != nil { + return err + } + + var rsaKey *rsa.PublicKey + switch k := key.(type) { + case *rsa.PublicKey: + rsaKey = k + default: + return ErrInvalidKey + } + + // Create hasher + if !m.Hash.Available() { + return ErrHashUnavailable + } + hasher := m.Hash.New() + hasher.Write([]byte(signingString)) + + return rsa.VerifyPSS(rsaKey, m.Hash, hasher.Sum(nil), sig, m.Options) +} + +// Implements the Sign method from SigningMethod +// For this signing method, key must be an rsa.PublicKey struct +func (m *SigningMethodRSAPSS) Sign(signingString string, key interface{}) (string, error) { + var rsaKey *rsa.PrivateKey + + switch k := key.(type) { + case *rsa.PrivateKey: + rsaKey = k + default: + return "", ErrInvalidKey + } + + // Create the hasher + if !m.Hash.Available() { + return "", ErrHashUnavailable + } + + hasher := m.Hash.New() + hasher.Write([]byte(signingString)) + + // Sign the string and return the encoded bytes + if sigBytes, err := rsa.SignPSS(rand.Reader, rsaKey, m.Hash, hasher.Sum(nil), m.Options); err == nil { + return EncodeSegment(sigBytes), nil + } else { + return "", err + } +} diff --git a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/rsa_pss_test.go b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/rsa_pss_test.go new file mode 100644 index 000000000..9045aaf34 --- /dev/null +++ b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/rsa_pss_test.go @@ -0,0 +1,96 @@ +// +build go1.4 + +package jwt_test + +import ( + "crypto/rsa" + "io/ioutil" + "strings" + "testing" + + "github.com/dgrijalva/jwt-go" +) + +var rsaPSSTestData = []struct { + name string + tokenString string + alg string + claims map[string]interface{} + valid bool +}{ + { + "Basic PS256", + "eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.PPG4xyDVY8ffp4CcxofNmsTDXsrVG2npdQuibLhJbv4ClyPTUtR5giNSvuxo03kB6I8VXVr0Y9X7UxhJVEoJOmULAwRWaUsDnIewQa101cVhMa6iR8X37kfFoiZ6NkS-c7henVkkQWu2HtotkEtQvN5hFlk8IevXXPmvZlhQhwzB1sGzGYnoi1zOfuL98d3BIjUjtlwii5w6gYG2AEEzp7HnHCsb3jIwUPdq86Oe6hIFjtBwduIK90ca4UqzARpcfwxHwVLMpatKask00AgGVI0ysdk0BLMjmLutquD03XbThHScC2C2_Pp4cHWgMzvbgLU2RYYZcZRKr46QeNgz9w", + "PS256", + map[string]interface{}{"foo": "bar"}, + true, + }, + { + "Basic PS384", + "eyJhbGciOiJQUzM4NCIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.w7-qqgj97gK4fJsq_DCqdYQiylJjzWONvD0qWWWhqEOFk2P1eDULPnqHRnjgTXoO4HAw4YIWCsZPet7nR3Xxq4ZhMqvKW8b7KlfRTb9cH8zqFvzMmybQ4jv2hKc3bXYqVow3AoR7hN_CWXI3Dv6Kd2X5xhtxRHI6IL39oTVDUQ74LACe-9t4c3QRPuj6Pq1H4FAT2E2kW_0KOc6EQhCLWEhm2Z2__OZskDC8AiPpP8Kv4k2vB7l0IKQu8Pr4RcNBlqJdq8dA5D3hk5TLxP8V5nG1Ib80MOMMqoS3FQvSLyolFX-R_jZ3-zfq6Ebsqr0yEb0AH2CfsECF7935Pa0FKQ", + "PS384", + map[string]interface{}{"foo": "bar"}, + true, + }, + { + "Basic PS512", + "eyJhbGciOiJQUzUxMiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.GX1HWGzFaJevuSLavqqFYaW8_TpvcjQ8KfC5fXiSDzSiT9UD9nB_ikSmDNyDILNdtjZLSvVKfXxZJqCfefxAtiozEDDdJthZ-F0uO4SPFHlGiXszvKeodh7BuTWRI2wL9-ZO4mFa8nq3GMeQAfo9cx11i7nfN8n2YNQ9SHGovG7_T_AvaMZB_jT6jkDHpwGR9mz7x1sycckEo6teLdHRnH_ZdlHlxqknmyTu8Odr5Xh0sJFOL8BepWbbvIIn-P161rRHHiDWFv6nhlHwZnVzjx7HQrWSGb6-s2cdLie9QL_8XaMcUpjLkfOMKkDOfHo6AvpL7Jbwi83Z2ZTHjJWB-A", + "PS512", + map[string]interface{}{"foo": "bar"}, + true, + }, + { + "basic PS256 invalid: foo => bar", + "eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.PPG4xyDVY8ffp4CcxofNmsTDXsrVG2npdQuibLhJbv4ClyPTUtR5giNSvuxo03kB6I8VXVr0Y9X7UxhJVEoJOmULAwRWaUsDnIewQa101cVhMa6iR8X37kfFoiZ6NkS-c7henVkkQWu2HtotkEtQvN5hFlk8IevXXPmvZlhQhwzB1sGzGYnoi1zOfuL98d3BIjUjtlwii5w6gYG2AEEzp7HnHCsb3jIwUPdq86Oe6hIFjtBwduIK90ca4UqzARpcfwxHwVLMpatKask00AgGVI0ysdk0BLMjmLutquD03XbThHScC2C2_Pp4cHWgMzvbgLU2RYYZcZRKr46QeNgz9W", + "PS256", + map[string]interface{}{"foo": "bar"}, + false, + }, +} + +func TestRSAPSSVerify(t *testing.T) { + var err error + + key, _ := ioutil.ReadFile("test/sample_key.pub") + var rsaPSSKey *rsa.PublicKey + if rsaPSSKey, err = jwt.ParseRSAPublicKeyFromPEM(key); err != nil { + t.Errorf("Unable to parse RSA public key: %v", err) + } + + for _, data := range rsaPSSTestData { + parts := strings.Split(data.tokenString, ".") + + method := jwt.GetSigningMethod(data.alg) + err := method.Verify(strings.Join(parts[0:2], "."), parts[2], rsaPSSKey) + if data.valid && err != nil { + t.Errorf("[%v] Error while verifying key: %v", data.name, err) + } + if !data.valid && err == nil { + t.Errorf("[%v] Invalid key passed validation", data.name) + } + } +} + +func TestRSAPSSSign(t *testing.T) { + var err error + + key, _ := ioutil.ReadFile("test/sample_key") + var rsaPSSKey *rsa.PrivateKey + if rsaPSSKey, err = jwt.ParseRSAPrivateKeyFromPEM(key); err != nil { + t.Errorf("Unable to parse RSA private key: %v", err) + } + + for _, data := range rsaPSSTestData { + if data.valid { + parts := strings.Split(data.tokenString, ".") + method := jwt.GetSigningMethod(data.alg) + sig, err := method.Sign(strings.Join(parts[0:2], "."), rsaPSSKey) + if err != nil { + t.Errorf("[%v] Error signing token: %v", data.name, err) + } + if sig == parts[2] { + t.Errorf("[%v] Signatures shouldn't match\nnew:\n%v\noriginal:\n%v", data.name, sig, parts[2]) + } + } + } +} diff --git a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/rsa_test.go b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/rsa_test.go index 4560b6be0..13ba1fcdc 100644 --- a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/rsa_test.go +++ b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/rsa_test.go @@ -1,7 +1,7 @@ package jwt_test import ( - "github.com/drone/drone/Godeps/_workspace/src/github.com/dgrijalva/jwt-go" + "github.com/dgrijalva/jwt-go" "io/ioutil" "strings" "testing" @@ -142,3 +142,33 @@ func TestRSAKeyParsing(t *testing.T) { } } + +func BenchmarkRS256Signing(b *testing.B) { + key, _ := ioutil.ReadFile("test/sample_key") + parsedKey, err := jwt.ParseRSAPrivateKeyFromPEM(key) + if err != nil { + b.Fatal(err) + } + + benchmarkSigning(b, jwt.SigningMethodRS256, parsedKey) +} + +func BenchmarkRS384Signing(b *testing.B) { + key, _ := ioutil.ReadFile("test/sample_key") + parsedKey, err := jwt.ParseRSAPrivateKeyFromPEM(key) + if err != nil { + b.Fatal(err) + } + + benchmarkSigning(b, jwt.SigningMethodRS384, parsedKey) +} + +func BenchmarkRS512Signing(b *testing.B) { + key, _ := ioutil.ReadFile("test/sample_key") + parsedKey, err := jwt.ParseRSAPrivateKeyFromPEM(key) + if err != nil { + b.Fatal(err) + } + + benchmarkSigning(b, jwt.SigningMethodRS512, parsedKey) +} diff --git a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/ec256-private.pem b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/ec256-private.pem new file mode 100644 index 000000000..a6882b3e5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/ec256-private.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIAh5qA3rmqQQuu0vbKV/+zouz/y/Iy2pLpIcWUSyImSwoAoGCCqGSM49 +AwEHoUQDQgAEYD54V/vp+54P9DXarYqx4MPcm+HKRIQzNasYSoRQHQ/6S6Ps8tpM +cT+KvIIC8W/e9k0W7Cm72M1P9jU7SLf/vg== +-----END EC PRIVATE KEY----- diff --git a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/ec256-public.pem b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/ec256-public.pem new file mode 100644 index 000000000..7191361e7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/ec256-public.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYD54V/vp+54P9DXarYqx4MPcm+HK +RIQzNasYSoRQHQ/6S6Ps8tpMcT+KvIIC8W/e9k0W7Cm72M1P9jU7SLf/vg== +-----END PUBLIC KEY----- diff --git a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/ec384-private.pem b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/ec384-private.pem new file mode 100644 index 000000000..a86c823e5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/ec384-private.pem @@ -0,0 +1,6 @@ +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDCaCvMHKhcG/qT7xsNLYnDT7sE/D+TtWIol1ROdaK1a564vx5pHbsRy +SEKcIxISi1igBwYFK4EEACKhZANiAATYa7rJaU7feLMqrAx6adZFNQOpaUH/Uylb +ZLriOLON5YFVwtVUpO1FfEXZUIQpptRPtc5ixIPY658yhBSb6irfIJUSP9aYTflJ +GKk/mDkK4t8mWBzhiD5B6jg9cEGhGgA= +-----END EC PRIVATE KEY----- diff --git a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/ec384-public.pem b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/ec384-public.pem new file mode 100644 index 000000000..e80d00564 --- /dev/null +++ b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/ec384-public.pem @@ -0,0 +1,5 @@ +-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE2Gu6yWlO33izKqwMemnWRTUDqWlB/1Mp +W2S64jizjeWBVcLVVKTtRXxF2VCEKabUT7XOYsSD2OufMoQUm+oq3yCVEj/WmE35 +SRipP5g5CuLfJlgc4Yg+Qeo4PXBBoRoA +-----END PUBLIC KEY----- diff --git a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/ec512-private.pem b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/ec512-private.pem new file mode 100644 index 000000000..213afaf13 --- /dev/null +++ b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/ec512-private.pem @@ -0,0 +1,7 @@ +-----BEGIN EC PRIVATE KEY----- +MIHcAgEBBEIB0pE4uFaWRx7t03BsYlYvF1YvKaBGyvoakxnodm9ou0R9wC+sJAjH +QZZJikOg4SwNqgQ/hyrOuDK2oAVHhgVGcYmgBwYFK4EEACOhgYkDgYYABAAJXIuw +12MUzpHggia9POBFYXSxaOGKGbMjIyDI+6q7wi7LMw3HgbaOmgIqFG72o8JBQwYN +4IbXHf+f86CRY1AA2wHzbHvt6IhkCXTNxBEffa1yMUgu8n9cKKF2iLgyQKcKqW33 +8fGOw/n3Rm2Yd/EB56u2rnD29qS+nOM9eGS+gy39OQ== +-----END EC PRIVATE KEY----- diff --git a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/ec512-public.pem b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/ec512-public.pem new file mode 100644 index 000000000..02ea02203 --- /dev/null +++ b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/test/ec512-public.pem @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQACVyLsNdjFM6R4IImvTzgRWF0sWjh +ihmzIyMgyPuqu8IuyzMNx4G2jpoCKhRu9qPCQUMGDeCG1x3/n/OgkWNQANsB82x7 +7eiIZAl0zcQRH32tcjFILvJ/XCihdoi4MkCnCqlt9/HxjsP590ZtmHfxAeertq5w +9vakvpzjPXhkvoMt/Tk= +-----END PUBLIC KEY----- diff --git a/cmd/drone-server/drone.go b/cmd/drone-server/drone.go index 34fd57e52..f22cd9eaf 100644 --- a/cmd/drone-server/drone.go +++ b/cmd/drone-server/drone.go @@ -11,7 +11,6 @@ import ( "github.com/drone/drone/Godeps/_workspace/src/github.com/elazarl/go-bindata-assetfs" "github.com/drone/drone/pkg/remote" "github.com/drone/drone/pkg/server" - "github.com/drone/drone/pkg/server/session" log "github.com/drone/drone/Godeps/_workspace/src/github.com/Sirupsen/logrus" eventbus "github.com/drone/drone/pkg/bus/builtin" @@ -40,11 +39,6 @@ var conf = struct { key string } - session struct { - expiry string - secret string - } - docker struct { host string cert string @@ -76,8 +70,6 @@ func main() { flag.StringVar(&conf.server.addr, "server-addr", ":8080", "") flag.StringVar(&conf.server.cert, "server-cert", "", "") flag.StringVar(&conf.server.key, "server-key", "", "") - flag.StringVar(&conf.session.expiry, "session-expiry", "", "") - flag.StringVar(&conf.session.secret, "session-secret", "", "") flag.StringVar(&conf.remote.driver, "remote-driver", "github", "") flag.StringVar(&conf.remote.config, "remote-config", "https://github.com", "") flag.StringVar(&conf.database.driver, "database-driver", "sqlite3", "") @@ -98,7 +90,6 @@ func main() { panic(err) } - session := session.New(conf.remote.config) eventbus_ := eventbus.New() queue_ := queue.New() updater := runner.NewUpdater(eventbus_, store, remote) @@ -116,8 +107,7 @@ func main() { api.Use(server.SetDatastore(store)) api.Use(server.SetRemote(remote)) api.Use(server.SetQueue(queue_)) - api.Use(server.SetSession(session)) - api.Use(server.SetUser(session)) + api.Use(server.SetUser()) api.Use(server.SetRunner(&runner_)) api.OPTIONS("/*path", func(c *gin.Context) {}) @@ -129,9 +119,7 @@ func main() { user.PATCH("", server.PutUserCurr) user.GET("/feed", server.GetUserFeed) user.GET("/repos", server.GetUserRepos) - user.GET("/tokens", server.GetUserTokens) - user.POST("/tokens", server.PostToken) - user.DELETE("/tokens/:label", server.DelToken) + user.POST("/token", server.PostUserToken) } users := api.Group("/users") @@ -199,7 +187,6 @@ func main() { auth.Use(server.SetHeaders()) auth.Use(server.SetDatastore(store)) auth.Use(server.SetRemote(remote)) - auth.Use(server.SetSession(session)) auth.GET("", server.GetLogin) auth.POST("", server.GetLogin) } diff --git a/pkg/hash/hash.go b/pkg/hash/hash.go deleted file mode 100644 index 3950244cb..000000000 --- a/pkg/hash/hash.go +++ /dev/null @@ -1,12 +0,0 @@ -package hash - -import ( - "crypto/sha256" - "encoding/hex" -) - -func New(text, salt string) string { - hasher := sha256.New() - hasher.Write([]byte(text + salt)) - return hex.EncodeToString(hasher.Sum(nil)) -} diff --git a/pkg/remote/builtin/gitlab/gitlab.go b/pkg/remote/builtin/gitlab/gitlab.go index 79a162c8f..54268ce2f 100644 --- a/pkg/remote/builtin/gitlab/gitlab.go +++ b/pkg/remote/builtin/gitlab/gitlab.go @@ -11,9 +11,9 @@ import ( "github.com/drone/drone/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client" "github.com/drone/drone/Godeps/_workspace/src/github.com/hashicorp/golang-lru" - "github.com/drone/drone/pkg/hash" "github.com/drone/drone/pkg/oauth2" "github.com/drone/drone/pkg/remote" + "github.com/drone/drone/pkg/token" common "github.com/drone/drone/pkg/types" "github.com/drone/drone/pkg/utils/httputil" ) @@ -186,17 +186,18 @@ func (g *Gitlab) Netrc(u *common.User, r *common.Repo) (*common.Netrc, error) { return nil, err } netrc := &common.Netrc{} + netrc.Machine = url_.Host switch g.CloneMode { case "oauth": netrc.Login = "oauth2" netrc.Password = u.Token case "token": + t := token.New(token.HookToken, r.FullName) netrc.Login = "drone-ci-token" - netrc.Password = hash.New(r.FullName, r.Hash) + netrc.Password, err = t.Sign(r.Hash) } - netrc.Machine = url_.Host - return netrc, nil + return netrc, err } // Activate activates a repository by adding a Post-commit hook and diff --git a/pkg/server/gitlab.go b/pkg/server/gitlab.go index adbf53b98..5f965e861 100644 --- a/pkg/server/gitlab.go +++ b/pkg/server/gitlab.go @@ -6,7 +6,7 @@ import ( "github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin" - "github.com/drone/drone/pkg/hash" + "github.com/drone/drone/pkg/token" ) // RedirectSha accepts a request to retvie a redirect @@ -79,8 +79,14 @@ func GetPullRequest(c *gin.Context) { store := ToDatastore(c) repo := ToRepo(c) - // get the token and verify the hook is authorized - if c.Request.FormValue("access_token") != hash.New(repo.FullName, repo.Hash) { + parsed, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) { + return repo.Hash, nil + }) + if err != nil { + c.Fail(400, err) + return + } + if parsed.Text != repo.FullName { c.AbortWithStatus(403) return } @@ -118,8 +124,14 @@ func GetCommit(c *gin.Context) { repo := ToRepo(c) sha := c.Params.ByName("sha") - // get the token and verify the hook is authorized - if c.Request.FormValue("access_token") != hash.New(repo.FullName, repo.Hash) { + parsed, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) { + return repo.Hash, nil + }) + if err != nil { + c.Fail(400, err) + return + } + if parsed.Text != repo.FullName { c.AbortWithStatus(403) return } diff --git a/pkg/server/hooks.go b/pkg/server/hooks.go index f533378e0..16a1b7cd2 100644 --- a/pkg/server/hooks.go +++ b/pkg/server/hooks.go @@ -6,8 +6,8 @@ import ( log "github.com/drone/drone/Godeps/_workspace/src/github.com/Sirupsen/logrus" "github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin" - "github.com/drone/drone/pkg/hash" "github.com/drone/drone/pkg/queue" + "github.com/drone/drone/pkg/token" common "github.com/drone/drone/pkg/types" "github.com/drone/drone/pkg/utils/httputil" "github.com/drone/drone/pkg/yaml" @@ -56,8 +56,16 @@ func PostHook(c *gin.Context) { } // get the token and verify the hook is authorized - if c.Request.FormValue("access_token") != hash.New(repo.FullName, repo.Hash) { - log.Errorf("invalid token sent with hook.") + parsed, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) { + return repo.Hash, nil + }) + if err != nil { + log.Errorf("failure to parse token from hook for %s. %s", repo.FullName, err) + c.Fail(400, err) + return + } + if parsed.Text != repo.FullName { + log.Errorf("failure to verify token from hook. Expected %s, got %s", repo.FullName, parsed.Text) c.AbortWithStatus(403) return } diff --git a/pkg/server/login.go b/pkg/server/login.go index 948127804..bffe5f645 100644 --- a/pkg/server/login.go +++ b/pkg/server/login.go @@ -7,7 +7,8 @@ import ( "github.com/drone/drone/Godeps/_workspace/src/github.com/ungerik/go-gravatar" log "github.com/drone/drone/Godeps/_workspace/src/github.com/Sirupsen/logrus" - common "github.com/drone/drone/pkg/types" + "github.com/drone/drone/pkg/token" + "github.com/drone/drone/pkg/types" ) // GetLogin accepts a request to authorize the user and to @@ -17,7 +18,6 @@ import ( // GET /authorize // func GetLogin(c *gin.Context) { - session := ToSession(c) remote := ToRemote(c) store := ToDatastore(c) @@ -65,13 +65,13 @@ func GetLogin(c *gin.Context) { } // create the user account - u = &common.User{} + u = &types.User{} u.Login = login.Login u.Token = login.Token u.Secret = login.Secret u.Email = login.Email u.Avatar = login.Avatar - u.Hash = common.GenerateToken() + u.Hash = types.GenerateToken() // insert the user into the database if err := store.AddUser(u); err != nil { @@ -106,12 +106,9 @@ func GetLogin(c *gin.Context) { return } - token := &common.Token{ - Kind: common.TokenSess, - Login: u.Login, - Issued: time.Now().UTC().Unix(), - } - tokenstr, err := session.GenerateToken(token) + exp := time.Now().Add(time.Hour * 72).Unix() + token := token.New(token.SessToken, u.Login) + tokenstr, err := token.SignExpires(u.Hash, exp) if err != nil { log.Errorf("cannot create token for %s. %s", u.Login, err) c.Redirect(303, "/login#error=internal_error") diff --git a/pkg/server/repos.go b/pkg/server/repos.go index b72b71b32..efe5f1bbc 100644 --- a/pkg/server/repos.go +++ b/pkg/server/repos.go @@ -10,8 +10,8 @@ import ( "github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin/binding" "github.com/drone/drone/Godeps/_workspace/src/gopkg.in/yaml.v2" - "github.com/drone/drone/pkg/hash" "github.com/drone/drone/pkg/remote" + "github.com/drone/drone/pkg/token" common "github.com/drone/drone/pkg/types" "github.com/drone/drone/pkg/utils/httputil" "github.com/drone/drone/pkg/utils/sshutil" @@ -208,10 +208,18 @@ func PostRepo(c *gin.Context) { r.FullName, ) + // crates the jwt token used to verify the repository + t := token.New(token.HookToken, r.FullName) + sig, err := t.Sign(r.Hash) + if err != nil { + c.Fail(500, err) + return + } + link := fmt.Sprintf( "%s/api/hook?access_token=%s", httputil.GetURL(c.Request), - hash.New(r.FullName, r.Hash), + sig, ) // generate an RSA key and add to the repo diff --git a/pkg/server/server.go b/pkg/server/server.go index d5d7bd594..79519f4e0 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -10,8 +10,8 @@ import ( "github.com/drone/drone/pkg/queue" "github.com/drone/drone/pkg/remote" "github.com/drone/drone/pkg/runner" - "github.com/drone/drone/pkg/server/session" "github.com/drone/drone/pkg/store" + "github.com/drone/drone/pkg/token" common "github.com/drone/drone/pkg/types" ) @@ -103,10 +103,6 @@ func ToDatastore(c *gin.Context) store.Store { return c.MustGet("datastore").(store.Store) } -func ToSession(c *gin.Context) session.Session { - return c.MustGet("session").(session.Session) -} - func SetDatastore(ds store.Store) gin.HandlerFunc { return func(c *gin.Context) { c.Set("datastore", ds) @@ -114,44 +110,24 @@ func SetDatastore(ds store.Store) gin.HandlerFunc { } } -func SetSession(s session.Session) gin.HandlerFunc { +func SetUser() gin.HandlerFunc { return func(c *gin.Context) { - c.Set("session", s) - c.Next() - } -} -func SetUser(s session.Session) gin.HandlerFunc { - return func(c *gin.Context) { - ds := ToDatastore(c) - token := s.GetLogin(c.Request) - if token == nil || len(token.Login) == 0 { - c.Next() - return - } + var store = ToDatastore(c) + var user *common.User - user, err := ds.UserLogin(token.Login) - if err == nil { + _, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) { + var err error + user, err = store.UserLogin(t.Text) + if err != nil { + return "", err + } + return user.Hash, nil + }) + + if err == nil && user != nil && user.ID != 0 { c.Set("user", user) } - - // if session token we can proceed, otherwise - // we should validate the token hasn't been revoked - switch token.Kind { - case common.TokenSess: - c.Next() - return - } - - // to verify the token we fetch from the datastore - // and check to see if the token issued date matches - // what we found in the jwt (in case the label is re-used) - t, err := ds.TokenLabel(user, token.Label) - if err != nil || t.Issued != token.Issued { - c.AbortWithStatus(403) - return - } - c.Next() } } diff --git a/pkg/server/session/session.go b/pkg/server/session/session.go deleted file mode 100644 index b85616cf3..000000000 --- a/pkg/server/session/session.go +++ /dev/null @@ -1,107 +0,0 @@ -package session - -import ( - "fmt" - "net/http" - "time" - - "github.com/drone/drone/Godeps/_workspace/src/github.com/dgrijalva/jwt-go" - common "github.com/drone/drone/pkg/types" -) - -type Session interface { - GenerateToken(*common.Token) (string, error) - GetLogin(*http.Request) *common.Token -} - -type session struct { - secret []byte - expire time.Duration -} - -func New(rand string) Session { - secret := []byte(rand) - expire := time.Hour * 72 - return &session{ - secret: secret, - expire: expire, - } -} - -// GenerateToken generates a JWT token for the user session -// that can be appended to the #access_token segment to -// facilitate client-based OAuth2. -func (s *session) GenerateToken(t *common.Token) (string, error) { - token := jwt.New(jwt.GetSigningMethod("HS256")) - token.Claims["user"] = t.Login - token.Claims["kind"] = t.Kind - token.Claims["date"] = t.Issued - token.Claims["label"] = t.Label - return token.SignedString(s.secret) -} - -// GetLogin gets the currently authenticated user for the -// http.Request. The user details will be stored as either -// a simple API token or JWT bearer token. -func (s *session) GetLogin(r *http.Request) *common.Token { - t := getToken(r) - if len(t) == 0 { - return nil - } - - claims := getClaims(t, s.secret) - if claims == nil || claims["user"] == nil || claims["date"] == nil || claims["label"] == nil || claims["kind"] == nil { - return nil - } - - token := &common.Token{ - Kind: claims["kind"].(string), - Login: claims["user"].(string), - Label: claims["label"].(string), - Issued: int64(claims["date"].(float64)), - } - if token.Kind != common.TokenSess { - return token - } - - if time.Unix(token.Issued, 0).Add(s.expire).Before(time.Now()) { - return nil - } - return token -} - -// getToken is a helper function that extracts the token -// from the http.Request. -func getToken(r *http.Request) string { - token := getTokenHeader(r) - if len(token) == 0 { - token = getTokenParam(r) - } - return token -} - -// getTokenHeader parses the JWT token value from -// the http Authorization header. -func getTokenHeader(r *http.Request) string { - var tokenstr = r.Header.Get("Authorization") - fmt.Sscanf(tokenstr, "Bearer %s", &tokenstr) - return tokenstr -} - -// getTokenParam parses the JWT token value from -// the http Request's query parameter. -func getTokenParam(r *http.Request) string { - return r.FormValue("access_token") -} - -// getClaims is a helper function that extracts the token -// claims from the JWT token string. -func getClaims(token string, secret []byte) map[string]interface{} { - t, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { - return secret, nil - }) - if err != nil || !t.Valid { - return nil - } - return t.Claims -} diff --git a/pkg/server/token.go b/pkg/server/token.go deleted file mode 100644 index 48f067a4e..000000000 --- a/pkg/server/token.go +++ /dev/null @@ -1,68 +0,0 @@ -package server - -import ( - "time" - - "github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin" - "github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin/binding" - - common "github.com/drone/drone/pkg/types" -) - -// POST /api/user/tokens -func PostToken(c *gin.Context) { - sess := ToSession(c) - store := ToDatastore(c) - user := ToUser(c) - - in := &common.Token{} - if !c.BindWith(in, binding.JSON) { - return - } - - token := &common.Token{} - token.Label = in.Label - token.UserID = user.ID - // token.Repos = in.Repos - // token.Scopes = in.Scopes - token.Login = user.Login - token.Kind = common.TokenUser - token.Issued = time.Now().UTC().Unix() - - err := store.AddToken(token) - if err != nil { - c.Fail(500, err) - return - } - - jwt, err := sess.GenerateToken(token) - if err != nil { - c.Fail(400, err) - return - } - - c.JSON(200, struct { - *common.Token - Hash string `json:"hash"` - }{token, jwt}) -} - -// DELETE /api/user/tokens/:label -func DelToken(c *gin.Context) { - store := ToDatastore(c) - user := ToUser(c) - label := c.Params.ByName("label") - - token, err := store.TokenLabel(user, label) - if err != nil { - c.Fail(404, err) - return - } - err = store.DelToken(token) - if err != nil { - c.Fail(400, err) - return - } - - c.Writer.WriteHeader(200) -} diff --git a/pkg/server/token_test.go b/pkg/server/token_test.go deleted file mode 100644 index f90dc3211..000000000 --- a/pkg/server/token_test.go +++ /dev/null @@ -1,120 +0,0 @@ -package server - -import ( - "bytes" - "database/sql" - "encoding/json" - "fmt" - "net/http" - "testing" - - "github.com/drone/drone/Godeps/_workspace/src/github.com/dgrijalva/jwt-go" - . "github.com/drone/drone/Godeps/_workspace/src/github.com/franela/goblin" - "github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin" - "github.com/drone/drone/Godeps/_workspace/src/github.com/stretchr/testify/mock" - "github.com/drone/drone/pkg/server/recorder" - "github.com/drone/drone/pkg/server/session" - "github.com/drone/drone/pkg/store/mock" - "github.com/drone/drone/pkg/types" -) - -var createTests = []struct { - inLabel string - inBody string - storeErr error - outCode int - outKind string -}{ - {"", `{}`, sql.ErrNoRows, 500, ""}, - {"app1", `{"label": "app1"}`, nil, 200, types.TokenUser}, - {"app2", `{"label": "app2"}`, nil, 200, types.TokenUser}, -} - -var deleteTests = []struct { - inLabel string - errTokenLabel error - errDelToken error - outCode int - outToken *types.Token -}{ - {"app1", sql.ErrNoRows, nil, 404, &types.Token{}}, - {"app2", nil, sql.ErrNoRows, 400, &types.Token{Label: "app2"}}, - {"app3", nil, nil, 200, &types.Token{Label: "app2"}}, -} - -func TestToken(t *testing.T) { - store := new(mocks.Store) - - g := Goblin(t) - g.Describe("Token", func() { - - // POST /api/user/tokens - g.It("should create tokens", func() { - for _, test := range createTests { - rw := recorder.New() - ctx := gin.Context{Engine: gin.Default(), Writer: rw} - body := bytes.NewBufferString(test.inBody) - ctx.Request, _ = http.NewRequest("POST", "/api/user/tokens", body) - - ctx.Set("datastore", store) - ctx.Set("user", &types.User{Login: "Freya"}) - - ctx.Set("session", session.New("Otto")) - - // prepare the mock - store.On("AddToken", mock.AnythingOfType("*types.Token")).Return(test.storeErr).Once() - PostToken(&ctx) - - g.Assert(rw.Code).Equal(test.outCode) - if test.outCode != 200 { - continue - } - - var respjson map[string]interface{} - json.Unmarshal(rw.Body.Bytes(), &respjson) - g.Assert(respjson["kind"]).Equal(types.TokenUser) - g.Assert(respjson["label"]).Equal(test.inLabel) - - // this is probably going too far... maybe just validate hash is not empty? - jwt.Parse(respjson["hash"].(string), func(token *jwt.Token) (interface{}, error) { - _, ok := token.Method.(*jwt.SigningMethodHMAC) - g.Assert(ok).IsTrue() - g.Assert(token.Claims["label"]).Equal(test.inLabel) - return nil, nil - }) - } - }) - - // DELETE /api/user/tokens/:label - g.It("should delete tokens", func() { - for _, test := range deleteTests { - rw := recorder.New() - ctx := gin.Context{Engine: gin.Default(), Writer: rw} - ctx.Params = append(ctx.Params, gin.Param{Key: "label", Value: test.inLabel}) - - ctx.Set("datastore", store) - ctx.Set("user", &types.User{Login: "Freya"}) - ctx.Set("session", session.New("Otto")) - - // prepare the mock - store.On("TokenLabel", mock.AnythingOfType("*types.User"), test.inLabel).Return(test.outToken, test.errTokenLabel).Once() - - if test.errTokenLabel == nil { - // we don't need this expectation if we error on our first - store.On("DelToken", mock.AnythingOfType("*types.Token")).Return(test.errDelToken).Once() - } - fmt.Println(test) - DelToken(&ctx) - - g.Assert(rw.Code).Equal(test.outCode) - if test.outCode != 200 { - continue - } - - var respjson map[string]interface{} - json.Unmarshal(rw.Body.Bytes(), &respjson) - fmt.Println(rw.Code, respjson) - } - }) - }) -} diff --git a/pkg/server/user.go b/pkg/server/user.go index 6347a1979..4fc3cb56f 100644 --- a/pkg/server/user.go +++ b/pkg/server/user.go @@ -5,7 +5,8 @@ import ( "github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin/binding" "github.com/drone/drone/Godeps/_workspace/src/github.com/ungerik/go-gravatar" - common "github.com/drone/drone/pkg/types" + "github.com/drone/drone/pkg/token" + "github.com/drone/drone/pkg/types" ) // GetUserCurr accepts a request to retrieve the @@ -27,7 +28,7 @@ func PutUserCurr(c *gin.Context) { store := ToDatastore(c) user := ToUser(c) - in := &common.User{} + in := &types.User{} if !c.BindWith(in, binding.JSON) { return } @@ -76,19 +77,15 @@ func GetUserFeed(c *gin.Context) { } } -// GetUserTokens accepts a request to get the currently -// authenticated user's token list from the datastore, -// encoded and returned in JSON format. -// -// GET /api/user/tokens -// -func GetUserTokens(c *gin.Context) { - store := ToDatastore(c) +// POST /api/user/token +func PostUserToken(c *gin.Context) { user := ToUser(c) - tokens, err := store.TokenList(user) + + t := token.New(token.UserToken, user.Login) + s, err := t.Sign(user.Hash) if err != nil { - c.Fail(400, err) + c.Fail(500, err) } else { - c.JSON(200, &tokens) + c.String(200, "application/jwt", s) } } diff --git a/pkg/server/users.go b/pkg/server/users.go index a88ee69d0..c27361867 100644 --- a/pkg/server/users.go +++ b/pkg/server/users.go @@ -5,7 +5,7 @@ import ( "github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin/binding" "github.com/drone/drone/Godeps/_workspace/src/github.com/ungerik/go-gravatar" - common "github.com/drone/drone/pkg/types" + "github.com/drone/drone/pkg/types" ) // GetUsers accepts a request to retrieve all users @@ -32,9 +32,13 @@ func GetUsers(c *gin.Context) { func PostUser(c *gin.Context) { store := ToDatastore(c) name := c.Params.ByName("name") - user := &common.User{Login: name} + user := &types.User{Login: name} user.Token = c.Request.FormValue("token") user.Secret = c.Request.FormValue("secret") + user.Hash = c.Request.FormValue("hash") + if len(user.Hash) == 0 { + user.Hash = types.GenerateToken() + } if err := store.AddUser(user); err != nil { c.Fail(400, err) } else { @@ -75,7 +79,7 @@ func PutUser(c *gin.Context) { return } - in := &common.User{} + in := &types.User{} if !c.BindWith(in, binding.JSON) { return } diff --git a/pkg/store/builtin/store.go b/pkg/store/builtin/store.go index 91a002400..4e6b43f89 100644 --- a/pkg/store/builtin/store.go +++ b/pkg/store/builtin/store.go @@ -8,10 +8,10 @@ import ( "github.com/drone/drone/pkg/store/builtin/migrate" "github.com/drone/drone/Godeps/_workspace/src/github.com/BurntSushi/migration" + _ "github.com/drone/drone/Godeps/_workspace/src/github.com/go-sql-driver/mysql" _ "github.com/drone/drone/Godeps/_workspace/src/github.com/lib/pq" _ "github.com/drone/drone/Godeps/_workspace/src/github.com/mattn/go-sqlite3" "github.com/drone/drone/Godeps/_workspace/src/github.com/russross/meddler" - _ "github.com/drone/drone/Godeps/_workspace/src/github.com/go-sql-driver/mysql" ) const ( @@ -94,7 +94,6 @@ func New(db *sql.DB) store.Store { *Jobstore *Blobstore *Starstore - *Tokenstore *Agentstore }{ NewUserstore(db), @@ -103,7 +102,6 @@ func New(db *sql.DB) store.Store { NewJobstore(db), NewBlobstore(db), NewStarstore(db), - NewTokenstore(db), NewAgentstore(db), } } diff --git a/pkg/store/builtin/token.go b/pkg/store/builtin/token.go deleted file mode 100644 index c6939359f..000000000 --- a/pkg/store/builtin/token.go +++ /dev/null @@ -1,43 +0,0 @@ -package builtin - -import ( - "database/sql" - - "github.com/drone/drone/pkg/types" -) - -type Tokenstore struct { - *sql.DB -} - -func NewTokenstore(db *sql.DB) *Tokenstore { - return &Tokenstore{db} -} - -// Token returns a token by ID. -func (db *Tokenstore) Token(id int64) (*types.Token, error) { - return getToken(db, rebind(stmtTokenSelect), id) -} - -// TokenLabel returns a token by label -func (db *Tokenstore) TokenLabel(user *types.User, label string) (*types.Token, error) { - return getToken(db, rebind(stmtTokenSelectTokenUserLabel), user.ID, label) -} - -// TokenList returns a list of all user tokens. -func (db *Tokenstore) TokenList(user *types.User) ([]*types.Token, error) { - return getTokens(db, rebind(stmtTokenSelectTokenUserId), user.ID) -} - -// AddToken inserts a new token into the datastore. -// If the token label already exists for the user -// an error is returned. -func (db *Tokenstore) AddToken(token *types.Token) error { - return createToken(db, rebind(stmtTokenInsert), token) -} - -// DelToken removes the DelToken from the datastore. -func (db *Tokenstore) DelToken(token *types.Token) error { - var _, err = db.Exec(rebind(stmtTokenDelete), token.ID) - return err -} diff --git a/pkg/store/builtin/token_sql.go b/pkg/store/builtin/token_sql.go deleted file mode 100644 index 2554bede2..000000000 --- a/pkg/store/builtin/token_sql.go +++ /dev/null @@ -1,255 +0,0 @@ -package builtin - -// DO NOT EDIT -// code generated by go:generate - -import ( - "database/sql" - "encoding/json" - - . "github.com/drone/drone/pkg/types" -) - -var _ = json.Marshal - -// generic database interface, matching both *sql.Db and *sql.Tx -type tokenDB interface { - Exec(query string, args ...interface{}) (sql.Result, error) - Query(query string, args ...interface{}) (*sql.Rows, error) - QueryRow(query string, args ...interface{}) *sql.Row -} - -func getToken(db tokenDB, query string, args ...interface{}) (*Token, error) { - row := db.QueryRow(query, args...) - return scanToken(row) -} - -func getTokens(db tokenDB, query string, args ...interface{}) ([]*Token, error) { - rows, err := db.Query(query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - return scanTokens(rows) -} - -func createToken(db tokenDB, query string, v *Token) error { - var v0 int64 - var v1 string - var v2 string - var v3 int64 - var v4 int64 - v0 = v.UserID - v1 = v.Kind - v2 = v.Label - v3 = v.Expiry - v4 = v.Issued - - res, err := db.Exec(query, - &v0, - &v1, - &v2, - &v3, - &v4, - ) - if err != nil { - return err - } - - v.ID, err = res.LastInsertId() - return err -} - -func updateToken(db tokenDB, query string, v *Token) error { - var v0 int64 - var v1 int64 - var v2 string - var v3 string - var v4 int64 - var v5 int64 - v0 = v.ID - v1 = v.UserID - v2 = v.Kind - v3 = v.Label - v4 = v.Expiry - v5 = v.Issued - - _, err := db.Exec(query, - &v1, - &v2, - &v3, - &v4, - &v5, - &v0, - ) - return err -} - -func scanToken(row *sql.Row) (*Token, error) { - var v0 int64 - var v1 int64 - var v2 string - var v3 string - var v4 int64 - var v5 int64 - - err := row.Scan( - &v0, - &v1, - &v2, - &v3, - &v4, - &v5, - ) - if err != nil { - return nil, err - } - - v := &Token{} - v.ID = v0 - v.UserID = v1 - v.Kind = v2 - v.Label = v3 - v.Expiry = v4 - v.Issued = v5 - - return v, nil -} - -func scanTokens(rows *sql.Rows) ([]*Token, error) { - var err error - var vv []*Token - for rows.Next() { - var v0 int64 - var v1 int64 - var v2 string - var v3 string - var v4 int64 - var v5 int64 - err = rows.Scan( - &v0, - &v1, - &v2, - &v3, - &v4, - &v5, - ) - if err != nil { - return vv, err - } - - v := &Token{} - v.ID = v0 - v.UserID = v1 - v.Kind = v2 - v.Label = v3 - v.Expiry = v4 - v.Issued = v5 - vv = append(vv, v) - } - return vv, rows.Err() -} - -const stmtTokenSelectList = ` -SELECT - token_id -,token_user_id -,token_kind -,token_label -,token_expiry -,token_issued -FROM tokens -` - -const stmtTokenSelectRange = ` -SELECT - token_id -,token_user_id -,token_kind -,token_label -,token_expiry -,token_issued -FROM tokens -LIMIT ? OFFSET ? -` - -const stmtTokenSelect = ` -SELECT - token_id -,token_user_id -,token_kind -,token_label -,token_expiry -,token_issued -FROM tokens -WHERE token_id = ? -` - -const stmtTokenSelectTokenUserId = ` -SELECT - token_id -,token_user_id -,token_kind -,token_label -,token_expiry -,token_issued -FROM tokens -WHERE token_user_id = ? -` - -const stmtTokenSelectTokenUserLabel = ` -SELECT - token_id -,token_user_id -,token_kind -,token_label -,token_expiry -,token_issued -FROM tokens -WHERE token_user_id = ? -AND token_label = ? -` - -const stmtTokenInsert = ` -INSERT INTO tokens ( - token_user_id -,token_kind -,token_label -,token_expiry -,token_issued -) VALUES (?,?,?,?,?); -` - -const stmtTokenUpdate = ` -UPDATE tokens SET - token_user_id = ? -,token_kind = ? -,token_label = ? -,token_expiry = ? -,token_issued = ? -WHERE token_id = ? -` - -const stmtTokenDelete = ` -DELETE FROM tokens -WHERE token_id = ? -` - -const stmtTokenTable = ` -CREATE TABLE IF NOT EXISTS tokens ( - token_id INTEGER PRIMARY KEY AUTOINCREMENT -,token_user_id INTEGER -,token_kind VARCHAR -,token_label VARCHAR -,token_expiry INTEGER -,token_issued INTEGER -); -` - -const stmtTokenTokenUserIdIndex = ` -CREATE INDEX IF NOT EXISTS ix_token_user_id ON tokens (token_user_id); -` - -const stmtTokenTokenUserLabelIndex = ` -CREATE UNIQUE INDEX IF NOT EXISTS ux_token_user_label ON tokens (token_user_id,token_label); -` diff --git a/pkg/store/builtin/token_test.go b/pkg/store/builtin/token_test.go deleted file mode 100644 index b244f01e8..000000000 --- a/pkg/store/builtin/token_test.go +++ /dev/null @@ -1,149 +0,0 @@ -package builtin - -import ( - "testing" - "time" - - "github.com/drone/drone/Godeps/_workspace/src/github.com/franela/goblin" - "github.com/drone/drone/pkg/types" -) - -func TestTokenstore(t *testing.T) { - db := mustConnectTest() - ts := NewTokenstore(db) - defer db.Close() - - g := goblin.Goblin(t) - g.Describe("Tokenstore", func() { - - // before each test be sure to purge the package - // table data from the database. - g.BeforeEach(func() { - db.Exec("DELETE FROM tokens") - }) - - g.It("Should Add a new Token", func() { - token := types.Token{ - UserID: 1, - Label: "foo", - Kind: types.TokenUser, - Issued: time.Now().Unix(), - Expiry: time.Now().Unix() + 1000, - } - err := ts.AddToken(&token) - g.Assert(err == nil).IsTrue() - g.Assert(token.ID != 0).IsTrue() - }) - - g.It("Should get a Token", func() { - token := types.Token{ - UserID: 1, - Label: "foo", - Kind: types.TokenUser, - Issued: time.Now().Unix(), - Expiry: time.Now().Unix() + 1000, - } - err1 := ts.AddToken(&token) - gettoken, err2 := ts.Token(token.ID) - g.Assert(err1 == nil).IsTrue() - g.Assert(err2 == nil).IsTrue() - g.Assert(token.ID).Equal(gettoken.ID) - g.Assert(token.Label).Equal(gettoken.Label) - g.Assert(token.Kind).Equal(gettoken.Kind) - g.Assert(token.Issued).Equal(gettoken.Issued) - g.Assert(token.Expiry).Equal(gettoken.Expiry) - }) - - g.It("Should Get a Token By Label", func() { - token := types.Token{ - UserID: 1, - Label: "foo", - Kind: types.TokenUser, - Issued: time.Now().Unix(), - Expiry: time.Now().Unix() + 1000, - } - err1 := ts.AddToken(&token) - gettoken, err2 := ts.TokenLabel(&types.User{ID: 1}, "foo") - g.Assert(err1 == nil).IsTrue() - g.Assert(err2 == nil).IsTrue() - g.Assert(token.ID).Equal(gettoken.ID) - g.Assert(token.Label).Equal(gettoken.Label) - g.Assert(token.Kind).Equal(gettoken.Kind) - g.Assert(token.Issued).Equal(gettoken.Issued) - g.Assert(token.Expiry).Equal(gettoken.Expiry) - }) - - g.It("Should Enforce Unique Token Label", func() { - token1 := types.Token{ - UserID: 1, - Label: "foo", - Kind: types.TokenUser, - Issued: time.Now().Unix(), - Expiry: time.Now().Unix() + 1000, - } - token2 := types.Token{ - UserID: 1, - Label: "foo", - Kind: types.TokenUser, - Issued: time.Now().Unix(), - Expiry: time.Now().Unix() + 1000, - } - err1 := ts.AddToken(&token1) - err2 := ts.AddToken(&token2) - g.Assert(err1 == nil).IsTrue() - g.Assert(err2 == nil).IsFalse() - }) - - g.It("Should Get a User Token List", func() { - token1 := types.Token{ - UserID: 1, - Label: "bar", - Kind: types.TokenUser, - Issued: time.Now().Unix(), - Expiry: time.Now().Unix() + 1000, - } - token2 := types.Token{ - UserID: 1, - Label: "foo", - Kind: types.TokenUser, - Issued: time.Now().Unix(), - Expiry: time.Now().Unix() + 1000, - } - token3 := types.Token{ - UserID: 2, - Label: "foo", - Kind: types.TokenUser, - Issued: time.Now().Unix(), - Expiry: time.Now().Unix() + 1000, - } - ts.AddToken(&token1) - ts.AddToken(&token2) - ts.AddToken(&token3) - tokens, err := ts.TokenList(&types.User{ID: 1}) - g.Assert(err == nil).IsTrue() - g.Assert(len(tokens)).Equal(2) - g.Assert(tokens[0].ID).Equal(token1.ID) - g.Assert(tokens[0].Label).Equal(token1.Label) - g.Assert(tokens[0].Kind).Equal(token1.Kind) - g.Assert(tokens[0].Issued).Equal(token1.Issued) - g.Assert(tokens[0].Expiry).Equal(token1.Expiry) - }) - - g.It("Should Del a Token", func() { - token := types.Token{ - UserID: 1, - Label: "foo", - Kind: types.TokenUser, - Issued: time.Now().Unix(), - Expiry: time.Now().Unix() + 1000, - } - ts.AddToken(&token) - _, err1 := ts.Token(token.ID) - err2 := ts.DelToken(&token) - _, err3 := ts.Token(token.ID) - g.Assert(err1 == nil).IsTrue() - g.Assert(err2 == nil).IsTrue() - g.Assert(err3 == nil).IsFalse() - }) - }) -} diff --git a/pkg/store/mock/mock.go b/pkg/store/mock/mock.go index 603d46d5f..b2952dcf3 100644 --- a/pkg/store/mock/mock.go +++ b/pkg/store/mock/mock.go @@ -84,53 +84,6 @@ func (m *Store) DelUser(_a0 *types.User) error { return r0 } -func (m *Store) Token(_a0 int64) (*types.Token, error) { - ret := m.Called(_a0) - - var r0 *types.Token - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Token) - } - r1 := ret.Error(1) - - return r0, r1 -} -func (m *Store) TokenLabel(_a0 *types.User, _a1 string) (*types.Token, error) { - ret := m.Called(_a0, _a1) - - var r0 *types.Token - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Token) - } - r1 := ret.Error(1) - - return r0, r1 -} -func (m *Store) TokenList(_a0 *types.User) ([]*types.Token, error) { - ret := m.Called(_a0) - - var r0 []*types.Token - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*types.Token) - } - r1 := ret.Error(1) - - return r0, r1 -} -func (m *Store) AddToken(_a0 *types.Token) error { - ret := m.Called(_a0) - - r0 := ret.Error(0) - - return r0 -} -func (m *Store) DelToken(_a0 *types.Token) error { - ret := m.Called(_a0) - - r0 := ret.Error(0) - - return r0 -} func (m *Store) Starred(_a0 *types.User, _a1 *types.Repo) (bool, error) { ret := m.Called(_a0, _a1) diff --git a/pkg/store/store.go b/pkg/store/store.go index 8b2db4ee2..1b3c545e0 100644 --- a/pkg/store/store.go +++ b/pkg/store/store.go @@ -70,25 +70,6 @@ type Store interface { // - // Token returns a token by ID. - Token(int64) (*types.Token, error) - - // TokenLabel returns a token by label - TokenLabel(*types.User, string) (*types.Token, error) - - // TokenList returns a list of all user tokens. - TokenList(*types.User) ([]*types.Token, error) - - // AddToken inserts a new token into the datastore. - // If the token label already exists for the user - // an error is returned. - AddToken(*types.Token) error - - // DelToken removes the DelToken from the datastore. - DelToken(*types.Token) error - - // - // Starred returns true if the user starred // the given repository. Starred(*types.User, *types.Repo) (bool, error) diff --git a/pkg/token/token.go b/pkg/token/token.go new file mode 100644 index 000000000..b371c9783 --- /dev/null +++ b/pkg/token/token.go @@ -0,0 +1,98 @@ +package token + +import ( + "net/http" + + "github.com/drone/drone/Godeps/_workspace/src/github.com/dgrijalva/jwt-go" +) + +type SecretFunc func(*Token) (string, error) + +const ( + UserToken = "user" + SessToken = "sess" + HookToken = "hook" +) + +// Default algorithm used to sign JWT tokens. +const SignerAlgo = "HS256" + +type Token struct { + Kind string + Text string +} + +// Parse parses +func Parse(raw string, fn SecretFunc) (*Token, error) { + token := &Token{} + parsed, err := jwt.Parse(raw, keyFunc(token, fn)) + if err != nil { + return nil, err + } else if !parsed.Valid { + return nil, jwt.ValidationError{} + } + return token, nil +} + +func ParseRequest(req *http.Request, fn SecretFunc) (*Token, error) { + token := &Token{} + parsed, err := jwt.ParseFromRequest(req, keyFunc(token, fn)) + if err != nil { + return nil, err + } else if !parsed.Valid { + return nil, jwt.ValidationError{} + } + return token, nil +} + +func New(kind, text string) *Token { + return &Token{Kind: kind, Text: text} +} + +// Sign signs the token using the given secret hash +// and returns the string value. +func (t *Token) Sign(secret string) (string, error) { + return t.SignExpires(secret, 0) +} + +// Sign signs the token using the given secret hash +// with an expiration date. +func (t *Token) SignExpires(secret string, exp int64) (string, error) { + token := jwt.New(jwt.SigningMethodHS256) + token.Claims["type"] = t.Kind + token.Claims["text"] = t.Text + if exp > 0 { + token.Claims["exp"] = float64(exp) + } + return token.SignedString([]byte(secret)) +} + +func keyFunc(token *Token, fn SecretFunc) jwt.Keyfunc { + return func(t *jwt.Token) (interface{}, error) { + // validate the correct algorithm is being used + if t.Method.Alg() != SignerAlgo { + return nil, jwt.ErrSignatureInvalid + } + + // extract the token kind and cast to + // the expected type. + kindv, ok := t.Claims["type"] + if !ok { + return nil, jwt.ValidationError{} + } + token.Kind, _ = kindv.(string) + + // extract the token value and cast to + // exepected type. + textv, ok := t.Claims["text"] + if !ok { + return nil, jwt.ValidationError{} + } + token.Text, _ = textv.(string) + + // invoke the callback function to retrieve + // the secret key used to verify + secret, err := fn(token) + return []byte(secret), err + } +} diff --git a/pkg/token/token_test.go b/pkg/token/token_test.go new file mode 100644 index 000000000..1765cc067 --- /dev/null +++ b/pkg/token/token_test.go @@ -0,0 +1 @@ +package token diff --git a/pkg/types/token.go b/pkg/types/token.go deleted file mode 100644 index c0d5e93d6..000000000 --- a/pkg/types/token.go +++ /dev/null @@ -1,18 +0,0 @@ -package types - -type Token struct { - ID int64 `meddler:"token_id,pk" json:"-"` - UserID int64 `meddler:"token_user_id" json:"-" sql:"index:ix_token_user_id,unique:ux_token_user_label"` - Login string `meddler:"-" json:"-" sql:"-"` - Kind string `meddler:"token_kind" json:"kind,omitempty"` - Label string `meddler:"token_label" json:"label,omitempty" sql:"unique:ux_token_user_label"` - Expiry int64 `meddler:"token_expiry" json:"expiry,omitempty"` - Issued int64 `meddler:"token_issued" json:"issued_at,omitempty"` -} - -const ( - TokenUser = "u" - TokenSess = "s" - TokenHook = "h" - TokenAgent = "a" -) diff --git a/pkg/types/util.go b/pkg/types/util.go index 894a17f10..951e8b407 100644 --- a/pkg/types/util.go +++ b/pkg/types/util.go @@ -1,18 +1,15 @@ package types import ( - "crypto/md5" "crypto/rand" - "fmt" "io" - "strings" ) // standard characters allowed in token string. var chars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") // default token length -var length = 40 +var length = 32 // GenerateToken generates random strings good for use in URIs to // identify unique objects. @@ -37,12 +34,3 @@ func GenerateToken() string { } } } - -// helper function to create a Gravatar Hash -// for the given Email address. -func CreateGravatar(email string) string { - email = strings.ToLower(strings.TrimSpace(email)) - hash := md5.New() - hash.Write([]byte(email)) - return fmt.Sprintf("%x", hash.Sum(nil)) -} diff --git a/pkg/types/util_test.go b/pkg/types/util_test.go index 2a2f3e352..36f849b05 100644 --- a/pkg/types/util_test.go +++ b/pkg/types/util_test.go @@ -4,13 +4,6 @@ import ( "testing" ) -func Test_CreateGravatar(t *testing.T) { - var got, want = CreateGravatar("dr_cooper@caltech.edu"), "2b77ba83e2216ddcd11fe8c24b70c2a3" - if got != want { - t.Errorf("Got gravatar hash %s, want %s", got, want) - } -} - func Test_GenerateToken(t *testing.T) { token := GenerateToken() if len(token) != length {