Skip to content

Commit d680462

Browse files
committed
[WIP] Add Apple JWT Token based authentication
1 parent e42f05d commit d680462

File tree

8 files changed

+198
-12
lines changed

8 files changed

+198
-12
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,8 @@ _testmain.go
2222
*.exe
2323
*.test
2424
*.prof
25+
26+
*.p12
27+
*.pem
28+
*.cer
29+
*.p8

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
# APNS/2
22

3+
NOTE: This is an experimental branch for the purpose of testing the new token based authentication
4+
35
APNS/2 is a go package designed for simple, flexible and fast Apple Push Notifications on iOS, OSX and Safari using the new HTTP/2 Push provider API.
46

57
[![Build Status](https://travis-ci.org/sideshow/apns2.svg?branch=master)](https://travis-ci.org/sideshow/apns2) [![Coverage Status](https://coveralls.io/repos/sideshow/apns2/badge.svg?branch=master&service=github)](https://coveralls.io/github/sideshow/apns2?branch=master) [![GoDoc](https://godoc.org/github.com/sideshow/apns2?status.svg)](https://godoc.org/github.com/sideshow/apns2)
68

79
## Features
810

911
- Uses new Apple APNs HTTP/2 connection
12+
- Supports new Apple Token Based Authentication (JWT)
1013
- Works with older versions of go (1.5.x) not just 1.6
1114
- Supports new iOS 10 features such as Collapse IDs, Subtitles and Mutable Notifications
1215
- Supports persistent connections to APNs
Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"fmt"
45
"log"
56

67
apns "github.com/sideshow/apns2"
@@ -9,17 +10,17 @@ import (
910

1011
func main() {
1112

12-
cert, pemErr := certificate.FromPemFile("../cert.pem", "")
13+
cert, pemErr := certificate.FromP12File("../cert.p12", "")
1314
if pemErr != nil {
1415
log.Println("Cert Error:", pemErr)
1516
}
1617

1718
notification := &apns.Notification{}
18-
notification.DeviceToken = "11aa01229f15f0f0c52029d8cf8cd0aeaf2365fe4cebc4af26cd6d76b7919ef7"
19-
notification.Topic = "com.sideshow.Apns2"
19+
notification.DeviceToken = "cb7262544176c3f15efdcdcf9dd03418dfca82ba710c54ab6b1352350d442cb4"
20+
notification.Topic = "com.Apns2"
2021
notification.Payload = []byte(`{
2122
"aps" : {
22-
"alert" : "Hello!"
23+
"alert" : "Hello!"
2324
}
2425
}
2526
`)
@@ -28,9 +29,8 @@ func main() {
2829
res, err := client.Push(notification)
2930

3031
if err != nil {
31-
log.Println("Error:", err)
32-
return
32+
log.Fatal("Error: ", err)
33+
} else {
34+
fmt.Printf("%v %v %v\n", res.StatusCode, res.ApnsID, res.Reason)
3335
}
34-
35-
log.Println("APNs ID:", res.ApnsID)
3636
}

_example/token.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
7+
apns "github.com/sideshow/apns2"
8+
"github.com/sideshow/apns2/token"
9+
)
10+
11+
func main() {
12+
13+
authKey, err := token.AuthKeyFromFile("../APNSAuthKey_T64N7W47U9.p8")
14+
if err != nil {
15+
log.Fatal("token error:", err)
16+
}
17+
18+
token := &token.Token{
19+
AuthKey: authKey,
20+
KeyID: "T64N7W47U9",
21+
TeamID: "264H7447N5",
22+
}
23+
24+
notification := &apns.Notification{}
25+
notification.DeviceToken = "cb7262544176c3f15efdcdcf9dd03418dfca82ba710c54ab6b1352350d442cb4"
26+
notification.Topic = "com.Apns2"
27+
notification.Payload = []byte(`{
28+
"aps" : {
29+
"alert" : "Hello!"
30+
}
31+
}
32+
`)
33+
34+
client := apns.NewTokenClient(token)
35+
res, err := client.Push(notification)
36+
37+
if err != nil {
38+
log.Fatal("error: ", err)
39+
} else {
40+
fmt.Printf("%v %v %v\n", res.StatusCode, res.ApnsID, res.Reason)
41+
}
42+
}

certificate/certificate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ var (
2626
// FromP12File loads a PKCS#12 certificate from a local file and returns a
2727
// tls.Certificate.
2828
//
29-
// Use "" as the password argument if the pem certificate is not password
29+
// Use "" as the password argument if the PKCS#12 certificate is not password
3030
// protected.
3131
func FromP12File(filename string, password string) (tls.Certificate, error) {
3232
p12bytes, err := ioutil.ReadFile(filename)

client.go

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"net/http"
1414
"time"
1515

16+
"github.com/sideshow/apns2/token"
1617
"golang.org/x/net/http2"
1718
)
1819

@@ -37,9 +38,10 @@ var (
3738

3839
// Client represents a connection with the APNs
3940
type Client struct {
40-
HTTPClient *http.Client
41-
Certificate tls.Certificate
4241
Host string
42+
Certificate tls.Certificate
43+
Token *token.Token
44+
HTTPClient *http.Client
4345
}
4446

4547
// NewClient returns a new Client with an underlying http.Client configured with
@@ -76,6 +78,30 @@ func NewClient(certificate tls.Certificate) *Client {
7678
}
7779
}
7880

81+
// NewTokenClient returns a new Client with an underlying http.Client configured
82+
// with the correct APNs HTTP/2 transport settings. It does not connect to the APNs
83+
// until the first Notification is sent via the Push method.
84+
//
85+
// As per the Apple APNs Provider API, you should keep a handle on this client
86+
// so that you can keep your connections with APNs open across multiple
87+
// notifications; don’t repeatedly open and close connections. APNs treats rapid
88+
// connection and disconnection as a denial-of-service attack.
89+
func NewTokenClient(token *token.Token) *Client {
90+
transport := &http2.Transport{
91+
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
92+
return tls.DialWithDialer(&net.Dialer{Timeout: TLSDialTimeout}, network, addr, cfg)
93+
},
94+
}
95+
return &Client{
96+
Token: token,
97+
HTTPClient: &http.Client{
98+
Transport: transport,
99+
Timeout: HTTPClientTimeout,
100+
},
101+
Host: DefaultHost,
102+
}
103+
}
104+
79105
// Development sets the Client to use the APNs development push endpoint.
80106
func (c *Client) Development() *Client {
81107
c.Host = HostDevelopment
@@ -95,13 +121,17 @@ func (c *Client) Production() *Client {
95121
// gateway, or an error if something goes wrong.
96122
func (c *Client) Push(n *Notification) (*Response, error) {
97123
payload, err := json.Marshal(n)
98-
99124
if err != nil {
100125
return nil, err
101126
}
102127

103128
url := fmt.Sprintf("%v/3/device/%v", c.Host, n.DeviceToken)
104129
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload))
130+
131+
if c.Token != nil {
132+
c.setTokenHeader(req)
133+
}
134+
105135
setHeaders(req, n)
106136
httpRes, err := c.HTTPClient.Do(req)
107137
if err != nil {
@@ -120,6 +150,11 @@ func (c *Client) Push(n *Notification) (*Response, error) {
120150
return response, nil
121151
}
122152

153+
func (c *Client) setTokenHeader(r *http.Request) {
154+
c.Token.GenerateIfExpired()
155+
r.Header.Set("authorization", fmt.Sprintf("bearer %v", c.Token.Bearer))
156+
}
157+
123158
func setHeaders(r *http.Request, n *Notification) {
124159
r.Header.Set("Content-Type", "application/json; charset=utf-8")
125160
if n.Topic != "" {

response.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ func (c *Response) Sent() bool {
9696
return c.StatusCode == StatusSent
9797
}
9898

99+
// Time represents a device uninstall time
99100
type Time struct {
100101
time.Time
101102
}

token/token.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package token
2+
3+
import (
4+
"crypto/ecdsa"
5+
"crypto/x509"
6+
"encoding/pem"
7+
"errors"
8+
"io/ioutil"
9+
"sync"
10+
"time"
11+
12+
jwt "github.com/dgrijalva/jwt-go"
13+
)
14+
15+
const (
16+
// TokenTimeout is the period of time in seconds that a token is valid for.
17+
// If the timestamp for token issue is not within the last hour, APNs
18+
// rejects subsequent push messages. This is set to under an hour so that
19+
// we generate a new token before the existing one expires.
20+
TokenTimeout = 3000
21+
)
22+
23+
// Possible errors when parsing a .p8 file.
24+
var (
25+
ErrAuthKeyWrongType = errors.New("token: AuthKey must be of type ecdsa.PrivateKey")
26+
)
27+
28+
// Token represents an Apple Provider Authentication Token (JSON Web Token).
29+
type Token struct {
30+
AuthKey *ecdsa.PrivateKey
31+
KeyID string
32+
TeamID string
33+
IssuedAt int64
34+
Bearer string
35+
m sync.Mutex
36+
}
37+
38+
// AuthKeyFromFile loads a .p8 certificate from a local file and returns a
39+
// *ecdsa.PrivateKey.
40+
func AuthKeyFromFile(filename string) (*ecdsa.PrivateKey, error) {
41+
bytes, err := ioutil.ReadFile(filename)
42+
if err != nil {
43+
return nil, err
44+
}
45+
return AuthKeyFromBytes(bytes)
46+
}
47+
48+
// AuthKeyFromBytes loads a .p8 certificate from an in memory byte array and
49+
// returns an *ecdsa.PrivateKey.
50+
func AuthKeyFromBytes(bytes []byte) (*ecdsa.PrivateKey, error) {
51+
block, _ := pem.Decode(bytes)
52+
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
53+
if err != nil {
54+
return nil, err
55+
}
56+
switch pk := key.(type) {
57+
case *ecdsa.PrivateKey:
58+
return pk, nil
59+
default:
60+
return nil, ErrAuthKeyWrongType
61+
}
62+
}
63+
64+
// GenerateIfExpired checks to see if the token is about to expire and
65+
// generates a new token.
66+
func (t *Token) GenerateIfExpired() {
67+
t.m.Lock()
68+
defer t.m.Unlock()
69+
if t.Expired() {
70+
t.Generate()
71+
}
72+
}
73+
74+
// Expired checks to see if the token has expired.
75+
func (t *Token) Expired() bool {
76+
return time.Now().Unix() >= (t.IssuedAt + TokenTimeout)
77+
}
78+
79+
// Generate creates a new token.
80+
func (t *Token) Generate() (bool, error) {
81+
issuedAt := time.Now().Unix()
82+
jwtToken := &jwt.Token{
83+
Header: map[string]interface{}{
84+
"alg": "ES256",
85+
"kid": t.KeyID,
86+
},
87+
Claims: jwt.MapClaims{
88+
"iss": t.TeamID,
89+
"iat": issuedAt,
90+
},
91+
Method: jwt.SigningMethodES256,
92+
}
93+
bearer, err := jwtToken.SignedString(t.AuthKey)
94+
if err != nil {
95+
return false, err
96+
}
97+
t.IssuedAt = issuedAt
98+
t.Bearer = bearer
99+
return true, nil
100+
}

0 commit comments

Comments
 (0)