Skip to content

Commit b2315eb

Browse files
committed
Add Apple JWT Token based authentication
1 parent 7588e8f commit b2315eb

13 files changed

+439
-6
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,5 +1,7 @@
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)
@@ -9,6 +11,7 @@ APNS/2 is a go package designed for simple, flexible and fast Apple Push Notific
911
- Uses new Apple APNs HTTP/2 connection
1012
- Fast - See [notes on speed](https://github.com/sideshow/apns2/wiki/APNS-HTTP-2-Push-Speed)
1113
- Works with go 1.6 and later
14+
- Supports new Apple Token Based Authentication (JWT)
1215
- Supports new iOS 10 features such as Collapse IDs, Subtitles and Mutable Notifications
1316
- Supports persistent connections to APNs
1417
- Supports VoIP/PushKit notifications (iOS 8 and later)

_example/simple.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
7+
apns "github.com/sideshow/apns2"
8+
"github.com/sideshow/apns2/certificate"
9+
)
10+
11+
func main() {
12+
13+
cert, pemErr := certificate.FromP12File("../cert.p12", "")
14+
if pemErr != nil {
15+
log.Println("Cert Error:", pemErr)
16+
}
17+
18+
notification := &apns.Notification{}
19+
notification.DeviceToken = "cb7262544176c3f15efdcdcf9dd03418dfca82ba710c54ab6b1352350d442cb4"
20+
notification.Topic = "com.Apns2"
21+
notification.Payload = []byte(`{
22+
"aps" : {
23+
"alert" : "Hello!"
24+
}
25+
}
26+
`)
27+
28+
client := apns.NewClient(cert).Production()
29+
res, err := client.Push(notification)
30+
31+
if err != nil {
32+
log.Fatal("Error: ", err)
33+
} else {
34+
fmt.Printf("%v %v %v\n", res.StatusCode, res.ApnsID, res.Reason)
35+
}
36+
}

_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: 43 additions & 5 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

@@ -35,11 +36,18 @@ var (
3536
HTTPClientTimeout = 60 * time.Second
3637
)
3738

39+
// DialTLS is the default dial function for creating TLS connections for
40+
// non-proxied HTTPS requests.
41+
var DialTLS = func(network, addr string, cfg *tls.Config) (net.Conn, error) {
42+
return tls.DialWithDialer(&net.Dialer{Timeout: TLSDialTimeout}, network, addr, cfg)
43+
}
44+
3845
// Client represents a connection with the APNs
3946
type Client struct {
40-
HTTPClient *http.Client
41-
Certificate tls.Certificate
4247
Host string
48+
Certificate tls.Certificate
49+
Token *token.Token
50+
HTTPClient *http.Client
4351
}
4452

4553
// NewClient returns a new Client with an underlying http.Client configured with
@@ -62,9 +70,7 @@ func NewClient(certificate tls.Certificate) *Client {
6270
}
6371
transport := &http2.Transport{
6472
TLSClientConfig: tlsConfig,
65-
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
66-
return tls.DialWithDialer(&net.Dialer{Timeout: TLSDialTimeout}, network, addr, cfg)
67-
},
73+
DialTLS: DialTLS,
6874
}
6975
return &Client{
7076
HTTPClient: &http.Client{
@@ -76,6 +82,28 @@ func NewClient(certificate tls.Certificate) *Client {
7682
}
7783
}
7884

85+
// NewTokenClient returns a new Client with an underlying http.Client configured
86+
// with the correct APNs HTTP/2 transport settings. It does not connect to the APNs
87+
// until the first Notification is sent via the Push method.
88+
//
89+
// As per the Apple APNs Provider API, you should keep a handle on this client
90+
// so that you can keep your connections with APNs open across multiple
91+
// notifications; don’t repeatedly open and close connections. APNs treats rapid
92+
// connection and disconnection as a denial-of-service attack.
93+
func NewTokenClient(token *token.Token) *Client {
94+
transport := &http2.Transport{
95+
DialTLS: DialTLS,
96+
}
97+
return &Client{
98+
Token: token,
99+
HTTPClient: &http.Client{
100+
Transport: transport,
101+
Timeout: HTTPClientTimeout,
102+
},
103+
Host: DefaultHost,
104+
}
105+
}
106+
79107
// Development sets the Client to use the APNs development push endpoint.
80108
func (c *Client) Development() *Client {
81109
c.Host = HostDevelopment
@@ -116,6 +144,11 @@ func (c *Client) PushWithContext(ctx Context, n *Notification) (*Response, error
116144

117145
url := fmt.Sprintf("%v/3/device/%v", c.Host, n.DeviceToken)
118146
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload))
147+
148+
if c.Token != nil {
149+
c.setTokenHeader(req)
150+
}
151+
119152
setHeaders(req, n)
120153

121154
httpRes, err := c.requestWithContext(ctx, req)
@@ -135,6 +168,11 @@ func (c *Client) PushWithContext(ctx Context, n *Notification) (*Response, error
135168
return response, nil
136169
}
137170

171+
func (c *Client) setTokenHeader(r *http.Request) {
172+
c.Token.GenerateIfExpired()
173+
r.Header.Set("authorization", fmt.Sprintf("bearer %v", c.Token.Bearer))
174+
}
175+
138176
func setHeaders(r *http.Request, n *Notification) {
139177
r.Header.Set("Content-Type", "application/json; charset=utf-8")
140178
if n.Topic != "" {

client_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package apns2_test
22

33
import (
4+
"crypto/ecdsa"
5+
"crypto/elliptic"
6+
"crypto/rand"
47
"crypto/tls"
58
"fmt"
69
"io/ioutil"
@@ -15,6 +18,7 @@ import (
1518

1619
apns "github.com/sideshow/apns2"
1720
"github.com/sideshow/apns2/certificate"
21+
"github.com/sideshow/apns2/token"
1822
"github.com/stretchr/testify/assert"
1923
)
2024

@@ -27,6 +31,12 @@ func mockNotification() *apns.Notification {
2731
return n
2832
}
2933

34+
func mockToken() *token.Token {
35+
pubkeyCurve := elliptic.P256()
36+
authKey, _ := ecdsa.GenerateKey(pubkeyCurve, rand.Reader)
37+
return &token.Token{AuthKey: authKey}
38+
}
39+
3040
func mockCert() tls.Certificate {
3141
return tls.Certificate{}
3242
}
@@ -42,16 +52,31 @@ func TestClientDefaultHost(t *testing.T) {
4252
assert.Equal(t, "https://api.development.push.apple.com", client.Host)
4353
}
4454

55+
func TestTokenDefaultHost(t *testing.T) {
56+
client := apns.NewTokenClient(mockToken()).Development()
57+
assert.Equal(t, "https://api.development.push.apple.com", client.Host)
58+
}
59+
4560
func TestClientDevelopmentHost(t *testing.T) {
4661
client := apns.NewClient(mockCert()).Development()
4762
assert.Equal(t, "https://api.development.push.apple.com", client.Host)
4863
}
4964

65+
func TestTokenClientDevelopmentHost(t *testing.T) {
66+
client := apns.NewTokenClient(mockToken()).Development()
67+
assert.Equal(t, "https://api.development.push.apple.com", client.Host)
68+
}
69+
5070
func TestClientProductionHost(t *testing.T) {
5171
client := apns.NewClient(mockCert()).Production()
5272
assert.Equal(t, "https://api.push.apple.com", client.Host)
5373
}
5474

75+
func TestTokenClientProductionHost(t *testing.T) {
76+
client := apns.NewTokenClient(mockToken()).Production()
77+
assert.Equal(t, "https://api.push.apple.com", client.Host)
78+
}
79+
5580
func TestClientBadUrlError(t *testing.T) {
5681
n := mockNotification()
5782
res, err := mockClient("badurl://badurl.com").Push(n)
@@ -148,6 +173,21 @@ func TestHeaders(t *testing.T) {
148173
assert.NoError(t, err)
149174
}
150175

176+
func TestAuthorizationHeader(t *testing.T) {
177+
n := mockNotification()
178+
token := mockToken()
179+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
180+
assert.Equal(t, "application/json; charset=utf-8", r.Header.Get("Content-Type"))
181+
assert.Equal(t, fmt.Sprintf("bearer %v", token.Bearer), r.Header.Get("authorization"))
182+
}))
183+
defer server.Close()
184+
185+
client := mockClient(server.URL)
186+
client.Token = token
187+
_, err := client.Push(n)
188+
assert.NoError(t, err)
189+
}
190+
151191
func TestPayload(t *testing.T) {
152192
n := mockNotification()
153193
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDfdOqotHd55SYO
3+
0dLz2oXengw/tZ+q3ZmOPeVmMuOMIYO/Cv1wk2U0OK4pug4OBSJPhl09Zs6IwB8N
4+
wPOU7EDTgMOcQUYB/6QNCI1J7Zm2oLtuchzz4pIb+o4ZAhVprLhRyvqi8OTKQ7kf
5+
Gfs5Tuwmn1M/0fQkfzMxADpjOKNgf0uy6lN6utjdTrPKKFUQNdc6/Ty8EeTnQEwU
6+
lsT2LAXCfEKxTn5RlRljDztS7Sfgs8VL0FPy1Qi8B+dFcgRYKFrcpsVaZ1lBmXKs
7+
XDRu5QR/Rg3f9DRq4GR1sNH8RLY9uApMl2SNz+sR4zRPG85R/se5Q06Gu0BUQ3UP
8+
m67ETVZLAgMBAAECggEADjU54mYvHpICXHjc5+JiFqiH8NkUgOG8LL4kwt3DeBp9
9+
bP0+5hSJH8vmzwJkeGG9L79EWG4b/bfxgYdeNX7cFFagmWPRFrlxbd64VRYFawZH
10+
RJt+2cbzMVI6DL8EK4bu5Ux5qTiV44Jw19hoD9nDzCTfPzSTSGrKD3iLPdnREYaI
11+
GDVxcjBv3Tx6rrv3Z2lhHHKhEHb0RRjATcjAVKV9NZhMajJ4l9pqJ3A4IQrCBl95
12+
ux6Xm1oXP0i6aR78cjchsCpcMXdP3WMsvHgTlsZT0RZLFHrvkiNHlPiil4G2/eHk
13+
wvT//CrcbO6SmI/zCtMmypuHJqcr+Xb7GPJoa64WoQKBgQDwrfelf3Rdfo9kaK/b
14+
rBmbu1++qWpYVPTedQy84DK2p3GE7YfKyI+fhbnw5ol3W1jjfvZCmK/p6eZR4jgy
15+
J0KJ76z53T8HoDTF+FTkR55oM3TEM46XzI36RppWP1vgcNHdz3U4DAqkMlAh4lVm
16+
3GiKPGX5JHHe7tWz/uZ55Kk58QKBgQDtrkqdSzWlOjvYD4mq4m8jPgS7v3hiHd+1
17+
OT8S37zdoT8VVzo2T4SF+fBhI2lWYzpQp2sCjLmCwK9k/Gur55H2kTBTwzlQ6WSL
18+
Te9Zj+eoMGklIirA+8YdQHXrO+CCw9BTJAF+c3c3xeUOLXafzyW29bASGfUtA7Ax
19+
QAsR+Rr3+wKBgAwfZxrh6ZWP+17+WuVArOWIMZFj7SRX2yGdWa/lxwgmNPSSFkXj
20+
hkBttujoY8IsSrTivzqpgCrTCjPTpir4iURzWw4W08bpjd7u3C/HX7Y16Uq8ohEJ
21+
T5lslveDJ3iNljSK74eMK7kLg7fBM7YDogxccHJ1IHsvInp3e1pmZxOxAoGAO+bS
22+
TUQ4N/UuQezgkF3TDrnBraO67leDGwRbfiE/U0ghQvqh5DA0QSPVzlWDZc9KUitv
23+
j8vxsR9o1PW9GS0an17GJEYuetLnkShKK3NWOhBBX6d1yP9rVdH6JhgIJEy/g0Su
24+
z7TAFiFc8i7JF8u4QJ05C8bZAMhOLotqftQeVOMCgYAid8aaRvaM2Q8a42Jn6ZTT
25+
5ms6AvNr98sv0StnfmNQ+EYXN0bEk2huSW+w2hN34TYYBTjViQmHbhudwwu8lVjE
26+
ccDmIXsUFbHVK+kTIpWGGchy5cYPs3k9s1nMR2av0Lojtw9WRY76xRXvN8W6R7Eh
27+
wA2ax3+gEEYpGhjM/lO2Lg==
28+
-----END PRIVATE KEY-----
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
MIIEogIBAAKCAQEA33TqqLR3eeUmDtHS89qF3p4MP7Wfqt2Zjj3lZjLjjCGDvwr9
3+
cJNlNDiuKboODgUiT4ZdPWbOiMAfDcDzlOxA04DDnEFGAf+kDQiNSe2ZtqC7bnIc
4+
8+KSG/qOGQIVaay4Ucr6ovDkykO5Hxn7OU7sJp9TP9H0JH8zMQA6YzijYH9LsupT
5+
errY3U6zyihVEDXXOv08vBHk50BMFJbE9iwFwnxCsU5+UZUZYw87Uu0n4LPFS9BT
6+
8tUIvAfnRXIEWCha3KbFWmdZQZlyrFw0buUEf0YN3/Q0auBkdbDR/ES2PbgKTJdk
7+
jc/rEeM0TxvOUf7HuUNOhrtAVEN1D5uuxE1WSwIDAQABAoIBAA41OeJmLx6SAlx4
8+
3OfiYhaoh/DZFIDhvCy+JMLdw3gafWz9PuYUiR/L5s8CZHhhvS+/RFhuG/238YGH
9+
XjV+3BRWoJlj0Ra5cW3euFUWBWsGR0SbftnG8zFSOgy/BCuG7uVMeak4leOCcNfY
10+
aA/Zw8wk3z80k0hqyg94iz3Z0RGGiBg1cXIwb908eq6792dpYRxyoRB29EUYwE3I
11+
wFSlfTWYTGoyeJfaaidwOCEKwgZfebsel5taFz9Iumke/HI3IbAqXDF3T91jLLx4
12+
E5bGU9EWSxR675IjR5T4opeBtv3h5ML0//wq3GzukpiP8wrTJsqbhyanK/l2+xjy
13+
aGuuFqECgYEA8K33pX90XX6PZGiv26wZm7tfvqlqWFT03nUMvOAytqdxhO2HysiP
14+
n4W58OaJd1tY4372Qpiv6enmUeI4MidCie+s+d0/B6A0xfhU5EeeaDN0xDOOl8yN
15+
+kaaVj9b4HDR3c91OAwKpDJQIeJVZtxoijxl+SRx3u7Vs/7meeSpOfECgYEA7a5K
16+
nUs1pTo72A+JquJvIz4Eu794Yh3ftTk/Et+83aE/FVc6Nk+EhfnwYSNpVmM6UKdr
17+
Aoy5gsCvZPxrq+eR9pEwU8M5UOlki03vWY/nqDBpJSIqwPvGHUB16zvggsPQUyQB
18+
fnN3N8XlDi12n88ltvWwEhn1LQOwMUALEfka9/sCgYAMH2ca4emVj/te/lrlQKzl
19+
iDGRY+0kV9shnVmv5ccIJjT0khZF44ZAbbbo6GPCLEq04r86qYAq0woz06Yq+IlE
20+
c1sOFtPG6Y3e7twvx1+2NelKvKIRCU+ZbJb3gyd4jZY0iu+HjCu5C4O3wTO2A6IM
21+
XHBydSB7LyJ6d3taZmcTsQKBgDvm0k1EODf1LkHs4JBd0w65wa2juu5XgxsEW34h
22+
P1NIIUL6oeQwNEEj1c5Vg2XPSlIrb4/L8bEfaNT1vRktGp9exiRGLnrS55EoSitz
23+
VjoQQV+ndcj/a1XR+iYYCCRMv4NErs+0wBYhXPIuyRfLuECdOQvG2QDITi6Lan7U
24+
HlTjAoGAInfGmkb2jNkPGuNiZ+mU0+ZrOgLza/fLL9ErZ35jUPhGFzdGxJNobklv
25+
sNoTd+E2GAU41YkJh24bncMLvJVYxHHA5iF7FBWx1SvpEyKVhhnIcuXGD7N5PbNZ
26+
zEdmr9C6I7cPVkWO+sUV7zfFukexIcANmsd/oBBGKRoYzP5Tti4=
27+
-----END RSA PRIVATE KEY-----

token/_fixtures/authkey-invalid.p8

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgEbVzfPnZPxfAyxqE
2+
ZV05laAoJAl+/6Xt2O4mOB611sOhRANCAASgFTKjwJAAU95g++/vzKWHkzAVmNMI
3+
tB5vTjZOOIwnEb70MsWZFIyUFD1P9Gwstz4+akHX7vI8BH6hHmBmfZZZ

token/_fixtures/authkey-valid.p8

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgEbVzfPnZPxfAyxqE
3+
ZV05laAoJAl+/6Xt2O4mOB611sOhRANCAASgFTKjwJAAU95g++/vzKWHkzAVmNMI
4+
tB5vTjZOOIwnEb70MsWZFIyUFD1P9Gwstz4+akHX7vI8BH6hHmBmfeQl
5+
-----END PRIVATE KEY-----

0 commit comments

Comments
 (0)