109 lines
2.5 KiB
Go
109 lines
2.5 KiB
Go
package token
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"errors"
|
|
"io/ioutil"
|
|
"sync"
|
|
"time"
|
|
|
|
jwt "github.com/dgrijalva/jwt-go"
|
|
)
|
|
|
|
const (
|
|
// TokenTimeout is the period of time in seconds that a token is valid for.
|
|
// If the timestamp for token issue is not within the last hour, APNs
|
|
// rejects subsequent push messages. This is set to under an hour so that
|
|
// we generate a new token before the existing one expires.
|
|
TokenTimeout = 3000
|
|
)
|
|
|
|
// Possible errors when parsing a .p8 file.
|
|
var (
|
|
ErrAuthKeyNotPem = errors.New("token: AuthKey must be a valid .p8 PEM file")
|
|
ErrAuthKeyNotECDSA = errors.New("token: AuthKey must be of type ecdsa.PrivateKey")
|
|
ErrAuthKeyNil = errors.New("token: AuthKey was nil")
|
|
)
|
|
|
|
// Token represents an Apple Provider Authentication Token (JSON Web Token).
|
|
type Token struct {
|
|
sync.Mutex
|
|
AuthKey *ecdsa.PrivateKey
|
|
KeyID string
|
|
TeamID string
|
|
IssuedAt int64
|
|
Bearer string
|
|
}
|
|
|
|
// AuthKeyFromFile loads a .p8 certificate from a local file and returns a
|
|
// *ecdsa.PrivateKey.
|
|
func AuthKeyFromFile(filename string) (*ecdsa.PrivateKey, error) {
|
|
bytes, err := ioutil.ReadFile(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return AuthKeyFromBytes(bytes)
|
|
}
|
|
|
|
// AuthKeyFromBytes loads a .p8 certificate from an in memory byte array and
|
|
// returns an *ecdsa.PrivateKey.
|
|
func AuthKeyFromBytes(bytes []byte) (*ecdsa.PrivateKey, error) {
|
|
block, _ := pem.Decode(bytes)
|
|
if block == nil {
|
|
return nil, ErrAuthKeyNotPem
|
|
}
|
|
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch pk := key.(type) {
|
|
case *ecdsa.PrivateKey:
|
|
return pk, nil
|
|
default:
|
|
return nil, ErrAuthKeyNotECDSA
|
|
}
|
|
}
|
|
|
|
// GenerateIfExpired checks to see if the token is about to expire and
|
|
// generates a new token.
|
|
func (t *Token) GenerateIfExpired() {
|
|
t.Lock()
|
|
defer t.Unlock()
|
|
if t.Expired() {
|
|
t.Generate()
|
|
}
|
|
}
|
|
|
|
// Expired checks to see if the token has expired.
|
|
func (t *Token) Expired() bool {
|
|
return time.Now().Unix() >= (t.IssuedAt + TokenTimeout)
|
|
}
|
|
|
|
// Generate creates a new token.
|
|
func (t *Token) Generate() (bool, error) {
|
|
if t.AuthKey == nil {
|
|
return false, ErrAuthKeyNil
|
|
}
|
|
issuedAt := time.Now().Unix()
|
|
jwtToken := &jwt.Token{
|
|
Header: map[string]interface{}{
|
|
"alg": "ES256",
|
|
"kid": t.KeyID,
|
|
},
|
|
Claims: jwt.MapClaims{
|
|
"iss": t.TeamID,
|
|
"iat": issuedAt,
|
|
},
|
|
Method: jwt.SigningMethodES256,
|
|
}
|
|
bearer, err := jwtToken.SignedString(t.AuthKey)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
t.IssuedAt = issuedAt
|
|
t.Bearer = bearer
|
|
return true, nil
|
|
}
|