cleaned up the token implementation for #1175

This commit is contained in:
Brad Rydzewski 2015-09-09 14:05:52 -07:00
parent cf953f19d3
commit cdfec98cf4
46 changed files with 871 additions and 1002 deletions

4
Godeps/Godeps.json generated
View file

@ -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",

View file

@ -0,0 +1,7 @@
language: go
go:
- 1.3.3
- 1.4.2
- 1.5
- tip

View file

@ -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).

View file

@ -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.

View file

@ -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 (

View file

@ -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
}
}

View file

@ -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)
}
}
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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"
)

View file

@ -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)
}

View file

@ -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

View file

@ -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)
}
}
})
}

View file

@ -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
}
}

View file

@ -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])
}
}
}
}

View file

@ -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)
}

View file

@ -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-----

View file

@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYD54V/vp+54P9DXarYqx4MPcm+HK
RIQzNasYSoRQHQ/6S6Ps8tpMcT+KvIIC8W/e9k0W7Cm72M1P9jU7SLf/vg==
-----END PUBLIC KEY-----

View file

@ -0,0 +1,6 @@
-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDCaCvMHKhcG/qT7xsNLYnDT7sE/D+TtWIol1ROdaK1a564vx5pHbsRy
SEKcIxISi1igBwYFK4EEACKhZANiAATYa7rJaU7feLMqrAx6adZFNQOpaUH/Uylb
ZLriOLON5YFVwtVUpO1FfEXZUIQpptRPtc5ixIPY658yhBSb6irfIJUSP9aYTflJ
GKk/mDkK4t8mWBzhiD5B6jg9cEGhGgA=
-----END EC PRIVATE KEY-----

View file

@ -0,0 +1,5 @@
-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE2Gu6yWlO33izKqwMemnWRTUDqWlB/1Mp
W2S64jizjeWBVcLVVKTtRXxF2VCEKabUT7XOYsSD2OufMoQUm+oq3yCVEj/WmE35
SRipP5g5CuLfJlgc4Yg+Qeo4PXBBoRoA
-----END PUBLIC KEY-----

View file

@ -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-----

View file

@ -0,0 +1,6 @@
-----BEGIN PUBLIC KEY-----
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQACVyLsNdjFM6R4IImvTzgRWF0sWjh
ihmzIyMgyPuqu8IuyzMNx4G2jpoCKhRu9qPCQUMGDeCG1x3/n/OgkWNQANsB82x7
7eiIZAl0zcQRH32tcjFILvJ/XCihdoi4MkCnCqlt9/HxjsP590ZtmHfxAeertq5w
9vakvpzjPXhkvoMt/Tk=
-----END PUBLIC KEY-----

View file

@ -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)
}

View file

@ -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))
}

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -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")

View file

@ -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

View file

@ -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()
}
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
}
})
})
}

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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),
}
}

View file

@ -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
}

View file

@ -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);
`

View file

@ -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()
})
})
}

View file

@ -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)

View file

@ -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)

98
pkg/token/token.go Normal file
View file

@ -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
}
}

1
pkg/token/token_test.go Normal file
View file

@ -0,0 +1 @@
package token

View file

@ -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"
)

View file

@ -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))
}

View file

@ -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 {