Skip to content

Commit 2cf793a

Browse files
committed
Add Apple JWT Token based authentication
1 parent d8025ed commit 2cf793a

31 files changed

+1933
-10
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/token/token.go

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

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: 47 additions & 9 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

@@ -38,11 +39,22 @@ var (
3839
TCPKeepAlive = 60 * time.Second
3940
)
4041

42+
// DialTLS is the default dial function for creating TLS connections for
43+
// non-proxied HTTPS requests.
44+
var DialTLS = func(network, addr string, cfg *tls.Config) (net.Conn, error) {
45+
dialer := &net.Dialer{
46+
Timeout: TLSDialTimeout,
47+
KeepAlive: TCPKeepAlive,
48+
}
49+
return tls.DialWithDialer(dialer, network, addr, cfg)
50+
}
51+
4152
// Client represents a connection with the APNs
4253
type Client struct {
43-
HTTPClient *http.Client
44-
Certificate tls.Certificate
4554
Host string
55+
Certificate tls.Certificate
56+
Token *token.Token
57+
HTTPClient *http.Client
4658
}
4759

4860
type connectionCloser interface {
@@ -69,13 +81,7 @@ func NewClient(certificate tls.Certificate) *Client {
6981
}
7082
transport := &http2.Transport{
7183
TLSClientConfig: tlsConfig,
72-
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
73-
dialer := &net.Dialer{
74-
Timeout: TLSDialTimeout,
75-
KeepAlive: TCPKeepAlive,
76-
}
77-
return tls.DialWithDialer(dialer, network, addr, cfg)
78-
},
84+
DialTLS: DialTLS,
7985
}
8086
return &Client{
8187
HTTPClient: &http.Client{
@@ -87,6 +93,28 @@ func NewClient(certificate tls.Certificate) *Client {
8793
}
8894
}
8995

96+
// NewTokenClient returns a new Client with an underlying http.Client configured
97+
// with the correct APNs HTTP/2 transport settings. It does not connect to the APNs
98+
// until the first Notification is sent via the Push method.
99+
//
100+
// As per the Apple APNs Provider API, you should keep a handle on this client
101+
// so that you can keep your connections with APNs open across multiple
102+
// notifications; don’t repeatedly open and close connections. APNs treats rapid
103+
// connection and disconnection as a denial-of-service attack.
104+
func NewTokenClient(token *token.Token) *Client {
105+
transport := &http2.Transport{
106+
DialTLS: DialTLS,
107+
}
108+
return &Client{
109+
Token: token,
110+
HTTPClient: &http.Client{
111+
Transport: transport,
112+
Timeout: HTTPClientTimeout,
113+
},
114+
Host: DefaultHost,
115+
}
116+
}
117+
90118
// Development sets the Client to use the APNs development push endpoint.
91119
func (c *Client) Development() *Client {
92120
c.Host = HostDevelopment
@@ -127,6 +155,11 @@ func (c *Client) PushWithContext(ctx Context, n *Notification) (*Response, error
127155

128156
url := fmt.Sprintf("%v/3/device/%v", c.Host, n.DeviceToken)
129157
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload))
158+
159+
if c.Token != nil {
160+
c.setTokenHeader(req)
161+
}
162+
130163
setHeaders(req, n)
131164

132165
httpRes, err := c.requestWithContext(ctx, req)
@@ -153,6 +186,11 @@ func (c *Client) CloseIdleConnections() {
153186
c.HTTPClient.Transport.(connectionCloser).CloseIdleConnections()
154187
}
155188

189+
func (c *Client) setTokenHeader(r *http.Request) {
190+
c.Token.GenerateIfExpired()
191+
r.Header.Set("authorization", fmt.Sprintf("bearer %v", c.Token.Bearer))
192+
}
193+
156194
func setHeaders(r *http.Request, n *Notification) {
157195
r.Header.Set("Content-Type", "application/json; charset=utf-8")
158196
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
}
@@ -51,16 +61,31 @@ func TestClientDefaultHost(t *testing.T) {
5161
assert.Equal(t, "https://api.development.push.apple.com", client.Host)
5262
}
5363

64+
func TestTokenDefaultHost(t *testing.T) {
65+
client := apns.NewTokenClient(mockToken()).Development()
66+
assert.Equal(t, "https://api.development.push.apple.com", client.Host)
67+
}
68+
5469
func TestClientDevelopmentHost(t *testing.T) {
5570
client := apns.NewClient(mockCert()).Development()
5671
assert.Equal(t, "https://api.development.push.apple.com", client.Host)
5772
}
5873

74+
func TestTokenClientDevelopmentHost(t *testing.T) {
75+
client := apns.NewTokenClient(mockToken()).Development()
76+
assert.Equal(t, "https://api.development.push.apple.com", client.Host)
77+
}
78+
5979
func TestClientProductionHost(t *testing.T) {
6080
client := apns.NewClient(mockCert()).Production()
6181
assert.Equal(t, "https://api.push.apple.com", client.Host)
6282
}
6383

84+
func TestTokenClientProductionHost(t *testing.T) {
85+
client := apns.NewTokenClient(mockToken()).Production()
86+
assert.Equal(t, "https://api.push.apple.com", client.Host)
87+
}
88+
6489
func TestClientBadUrlError(t *testing.T) {
6590
n := mockNotification()
6691
res, err := mockClient("badurl://badurl.com").Push(n)
@@ -157,6 +182,21 @@ func TestHeaders(t *testing.T) {
157182
assert.NoError(t, err)
158183
}
159184

185+
func TestAuthorizationHeader(t *testing.T) {
186+
n := mockNotification()
187+
token := mockToken()
188+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
189+
assert.Equal(t, "application/json; charset=utf-8", r.Header.Get("Content-Type"))
190+
assert.Equal(t, fmt.Sprintf("bearer %v", token.Bearer), r.Header.Get("authorization"))
191+
}))
192+
defer server.Close()
193+
194+
client := mockClient(server.URL)
195+
client.Token = token
196+
_, err := client.Push(n)
197+
assert.NoError(t, err)
198+
}
199+
160200
func TestPayload(t *testing.T) {
161201
n := mockNotification()
162202
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)