Skip to content

Commit

Permalink
Add Apple JWT Token based authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
sideshow committed Sep 26, 2017
1 parent d8025ed commit 2cf793a
Show file tree
Hide file tree
Showing 31 changed files with 1,933 additions and 10 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Expand Up @@ -22,3 +22,8 @@ _testmain.go
*.exe
*.test
*.prof

/*.p12
/*.pem
/*.cer
/*.p8
3 changes: 3 additions & 0 deletions README.md
@@ -1,5 +1,7 @@
# APNS/2

NOTE: This is an experimental branch for the purpose of testing the new token based authentication

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.

[![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)
Expand All @@ -9,6 +11,7 @@ APNS/2 is a go package designed for simple, flexible and fast Apple Push Notific
- Uses new Apple APNs HTTP/2 connection
- Fast - See [notes on speed](https://github.com/sideshow/apns2/wiki/APNS-HTTP-2-Push-Speed)
- Works with go 1.6 and later
- Supports new Apple Token Based Authentication (JWT)
- Supports new iOS 10 features such as Collapse IDs, Subtitles and Mutable Notifications
- Supports persistent connections to APNs
- Supports VoIP/PushKit notifications (iOS 8 and later)
Expand Down
47 changes: 47 additions & 0 deletions _example/token/token.go
@@ -0,0 +1,47 @@
package main

import (
"flag"
"fmt"
"log"

"github.com/sideshow/apns2"
"github.com/sideshow/apns2/token"
)

func main() {
authKeyPath := flag.String("cert", "", "Path to .p8 APNSAuthKey file (Required)")
deviceToken := flag.String("token", "", "Push token (Required)")
topic := flag.String("topic", "", "Topic (Required)")
flag.Parse()

authKey, err := token.AuthKeyFromFile(*authKeyPath)
if err != nil {
log.Fatal("token error:", err)
}

token := &token.Token{
AuthKey: authKey,
KeyID: "T64N7W47U9",
TeamID: "264H7447N5",
}

notification := &apns2.Notification{}
notification.DeviceToken = *deviceToken
notification.Topic = *topic
notification.Payload = []byte(`{
"aps" : {
"alert" : "Hello!"
}
}
`)

client := apns2.NewTokenClient(token)
res, err := client.Push(notification)

if err != nil {
log.Fatal("Error:", err)
}

fmt.Printf("%v %v %v\n", res.StatusCode, res.ApnsID, res.Reason)
}
2 changes: 1 addition & 1 deletion certificate/certificate.go
Expand Up @@ -26,7 +26,7 @@ var (
// FromP12File loads a PKCS#12 certificate from a local file and returns a
// tls.Certificate.
//
// Use "" as the password argument if the pem certificate is not password
// Use "" as the password argument if the PKCS#12 certificate is not password
// protected.
func FromP12File(filename string, password string) (tls.Certificate, error) {
p12bytes, err := ioutil.ReadFile(filename)
Expand Down
56 changes: 47 additions & 9 deletions client.go
Expand Up @@ -13,6 +13,7 @@ import (
"net/http"
"time"

"github.com/sideshow/apns2/token"
"golang.org/x/net/http2"
)

Expand All @@ -38,11 +39,22 @@ var (
TCPKeepAlive = 60 * time.Second
)

// DialTLS is the default dial function for creating TLS connections for
// non-proxied HTTPS requests.
var DialTLS = func(network, addr string, cfg *tls.Config) (net.Conn, error) {
dialer := &net.Dialer{
Timeout: TLSDialTimeout,
KeepAlive: TCPKeepAlive,
}
return tls.DialWithDialer(dialer, network, addr, cfg)
}

// Client represents a connection with the APNs
type Client struct {
HTTPClient *http.Client
Certificate tls.Certificate
Host string
Certificate tls.Certificate
Token *token.Token
HTTPClient *http.Client
}

type connectionCloser interface {
Expand All @@ -69,13 +81,7 @@ func NewClient(certificate tls.Certificate) *Client {
}
transport := &http2.Transport{
TLSClientConfig: tlsConfig,
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
dialer := &net.Dialer{
Timeout: TLSDialTimeout,
KeepAlive: TCPKeepAlive,
}
return tls.DialWithDialer(dialer, network, addr, cfg)
},
DialTLS: DialTLS,
}
return &Client{
HTTPClient: &http.Client{
Expand All @@ -87,6 +93,28 @@ func NewClient(certificate tls.Certificate) *Client {
}
}

// NewTokenClient returns a new Client with an underlying http.Client configured
// with the correct APNs HTTP/2 transport settings. It does not connect to the APNs
// until the first Notification is sent via the Push method.
//
// As per the Apple APNs Provider API, you should keep a handle on this client
// so that you can keep your connections with APNs open across multiple
// notifications; don’t repeatedly open and close connections. APNs treats rapid
// connection and disconnection as a denial-of-service attack.
func NewTokenClient(token *token.Token) *Client {
transport := &http2.Transport{
DialTLS: DialTLS,
}
return &Client{
Token: token,
HTTPClient: &http.Client{
Transport: transport,
Timeout: HTTPClientTimeout,
},
Host: DefaultHost,
}
}

// Development sets the Client to use the APNs development push endpoint.
func (c *Client) Development() *Client {
c.Host = HostDevelopment
Expand Down Expand Up @@ -127,6 +155,11 @@ func (c *Client) PushWithContext(ctx Context, n *Notification) (*Response, error

url := fmt.Sprintf("%v/3/device/%v", c.Host, n.DeviceToken)
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload))

if c.Token != nil {
c.setTokenHeader(req)
}

setHeaders(req, n)

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

func (c *Client) setTokenHeader(r *http.Request) {
c.Token.GenerateIfExpired()
r.Header.Set("authorization", fmt.Sprintf("bearer %v", c.Token.Bearer))
}

func setHeaders(r *http.Request, n *Notification) {
r.Header.Set("Content-Type", "application/json; charset=utf-8")
if n.Topic != "" {
Expand Down
40 changes: 40 additions & 0 deletions client_test.go
@@ -1,6 +1,9 @@
package apns2_test

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"fmt"
"io/ioutil"
Expand All @@ -15,6 +18,7 @@ import (

apns "github.com/sideshow/apns2"
"github.com/sideshow/apns2/certificate"
"github.com/sideshow/apns2/token"
"github.com/stretchr/testify/assert"
)

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

func mockToken() *token.Token {
pubkeyCurve := elliptic.P256()
authKey, _ := ecdsa.GenerateKey(pubkeyCurve, rand.Reader)
return &token.Token{AuthKey: authKey}
}

func mockCert() tls.Certificate {
return tls.Certificate{}
}
Expand All @@ -51,16 +61,31 @@ func TestClientDefaultHost(t *testing.T) {
assert.Equal(t, "https://api.development.push.apple.com", client.Host)
}

func TestTokenDefaultHost(t *testing.T) {
client := apns.NewTokenClient(mockToken()).Development()
assert.Equal(t, "https://api.development.push.apple.com", client.Host)
}

func TestClientDevelopmentHost(t *testing.T) {
client := apns.NewClient(mockCert()).Development()
assert.Equal(t, "https://api.development.push.apple.com", client.Host)
}

func TestTokenClientDevelopmentHost(t *testing.T) {
client := apns.NewTokenClient(mockToken()).Development()
assert.Equal(t, "https://api.development.push.apple.com", client.Host)
}

func TestClientProductionHost(t *testing.T) {
client := apns.NewClient(mockCert()).Production()
assert.Equal(t, "https://api.push.apple.com", client.Host)
}

func TestTokenClientProductionHost(t *testing.T) {
client := apns.NewTokenClient(mockToken()).Production()
assert.Equal(t, "https://api.push.apple.com", client.Host)
}

func TestClientBadUrlError(t *testing.T) {
n := mockNotification()
res, err := mockClient("badurl://badurl.com").Push(n)
Expand Down Expand Up @@ -157,6 +182,21 @@ func TestHeaders(t *testing.T) {
assert.NoError(t, err)
}

func TestAuthorizationHeader(t *testing.T) {
n := mockNotification()
token := mockToken()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "application/json; charset=utf-8", r.Header.Get("Content-Type"))
assert.Equal(t, fmt.Sprintf("bearer %v", token.Bearer), r.Header.Get("authorization"))
}))
defer server.Close()

client := mockClient(server.URL)
client.Token = token
_, err := client.Push(n)
assert.NoError(t, err)
}

func TestPayload(t *testing.T) {
n := mockNotification()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down
28 changes: 28 additions & 0 deletions token/_fixtures/authkey-invalid-ecdsa.p8
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDfdOqotHd55SYO
0dLz2oXengw/tZ+q3ZmOPeVmMuOMIYO/Cv1wk2U0OK4pug4OBSJPhl09Zs6IwB8N
wPOU7EDTgMOcQUYB/6QNCI1J7Zm2oLtuchzz4pIb+o4ZAhVprLhRyvqi8OTKQ7kf
Gfs5Tuwmn1M/0fQkfzMxADpjOKNgf0uy6lN6utjdTrPKKFUQNdc6/Ty8EeTnQEwU
lsT2LAXCfEKxTn5RlRljDztS7Sfgs8VL0FPy1Qi8B+dFcgRYKFrcpsVaZ1lBmXKs
XDRu5QR/Rg3f9DRq4GR1sNH8RLY9uApMl2SNz+sR4zRPG85R/se5Q06Gu0BUQ3UP
m67ETVZLAgMBAAECggEADjU54mYvHpICXHjc5+JiFqiH8NkUgOG8LL4kwt3DeBp9
bP0+5hSJH8vmzwJkeGG9L79EWG4b/bfxgYdeNX7cFFagmWPRFrlxbd64VRYFawZH
RJt+2cbzMVI6DL8EK4bu5Ux5qTiV44Jw19hoD9nDzCTfPzSTSGrKD3iLPdnREYaI
GDVxcjBv3Tx6rrv3Z2lhHHKhEHb0RRjATcjAVKV9NZhMajJ4l9pqJ3A4IQrCBl95
ux6Xm1oXP0i6aR78cjchsCpcMXdP3WMsvHgTlsZT0RZLFHrvkiNHlPiil4G2/eHk
wvT//CrcbO6SmI/zCtMmypuHJqcr+Xb7GPJoa64WoQKBgQDwrfelf3Rdfo9kaK/b
rBmbu1++qWpYVPTedQy84DK2p3GE7YfKyI+fhbnw5ol3W1jjfvZCmK/p6eZR4jgy
J0KJ76z53T8HoDTF+FTkR55oM3TEM46XzI36RppWP1vgcNHdz3U4DAqkMlAh4lVm
3GiKPGX5JHHe7tWz/uZ55Kk58QKBgQDtrkqdSzWlOjvYD4mq4m8jPgS7v3hiHd+1
OT8S37zdoT8VVzo2T4SF+fBhI2lWYzpQp2sCjLmCwK9k/Gur55H2kTBTwzlQ6WSL
Te9Zj+eoMGklIirA+8YdQHXrO+CCw9BTJAF+c3c3xeUOLXafzyW29bASGfUtA7Ax
QAsR+Rr3+wKBgAwfZxrh6ZWP+17+WuVArOWIMZFj7SRX2yGdWa/lxwgmNPSSFkXj
hkBttujoY8IsSrTivzqpgCrTCjPTpir4iURzWw4W08bpjd7u3C/HX7Y16Uq8ohEJ
T5lslveDJ3iNljSK74eMK7kLg7fBM7YDogxccHJ1IHsvInp3e1pmZxOxAoGAO+bS
TUQ4N/UuQezgkF3TDrnBraO67leDGwRbfiE/U0ghQvqh5DA0QSPVzlWDZc9KUitv
j8vxsR9o1PW9GS0an17GJEYuetLnkShKK3NWOhBBX6d1yP9rVdH6JhgIJEy/g0Su
z7TAFiFc8i7JF8u4QJ05C8bZAMhOLotqftQeVOMCgYAid8aaRvaM2Q8a42Jn6ZTT
5ms6AvNr98sv0StnfmNQ+EYXN0bEk2huSW+w2hN34TYYBTjViQmHbhudwwu8lVjE
ccDmIXsUFbHVK+kTIpWGGchy5cYPs3k9s1nMR2av0Lojtw9WRY76xRXvN8W6R7Eh
wA2ax3+gEEYpGhjM/lO2Lg==
-----END PRIVATE KEY-----
27 changes: 27 additions & 0 deletions token/_fixtures/authkey-invalid-pkcs8.p8
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEA33TqqLR3eeUmDtHS89qF3p4MP7Wfqt2Zjj3lZjLjjCGDvwr9
cJNlNDiuKboODgUiT4ZdPWbOiMAfDcDzlOxA04DDnEFGAf+kDQiNSe2ZtqC7bnIc
8+KSG/qOGQIVaay4Ucr6ovDkykO5Hxn7OU7sJp9TP9H0JH8zMQA6YzijYH9LsupT
errY3U6zyihVEDXXOv08vBHk50BMFJbE9iwFwnxCsU5+UZUZYw87Uu0n4LPFS9BT
8tUIvAfnRXIEWCha3KbFWmdZQZlyrFw0buUEf0YN3/Q0auBkdbDR/ES2PbgKTJdk
jc/rEeM0TxvOUf7HuUNOhrtAVEN1D5uuxE1WSwIDAQABAoIBAA41OeJmLx6SAlx4
3OfiYhaoh/DZFIDhvCy+JMLdw3gafWz9PuYUiR/L5s8CZHhhvS+/RFhuG/238YGH
XjV+3BRWoJlj0Ra5cW3euFUWBWsGR0SbftnG8zFSOgy/BCuG7uVMeak4leOCcNfY
aA/Zw8wk3z80k0hqyg94iz3Z0RGGiBg1cXIwb908eq6792dpYRxyoRB29EUYwE3I
wFSlfTWYTGoyeJfaaidwOCEKwgZfebsel5taFz9Iumke/HI3IbAqXDF3T91jLLx4
E5bGU9EWSxR675IjR5T4opeBtv3h5ML0//wq3GzukpiP8wrTJsqbhyanK/l2+xjy
aGuuFqECgYEA8K33pX90XX6PZGiv26wZm7tfvqlqWFT03nUMvOAytqdxhO2HysiP
n4W58OaJd1tY4372Qpiv6enmUeI4MidCie+s+d0/B6A0xfhU5EeeaDN0xDOOl8yN
+kaaVj9b4HDR3c91OAwKpDJQIeJVZtxoijxl+SRx3u7Vs/7meeSpOfECgYEA7a5K
nUs1pTo72A+JquJvIz4Eu794Yh3ftTk/Et+83aE/FVc6Nk+EhfnwYSNpVmM6UKdr
Aoy5gsCvZPxrq+eR9pEwU8M5UOlki03vWY/nqDBpJSIqwPvGHUB16zvggsPQUyQB
fnN3N8XlDi12n88ltvWwEhn1LQOwMUALEfka9/sCgYAMH2ca4emVj/te/lrlQKzl
iDGRY+0kV9shnVmv5ccIJjT0khZF44ZAbbbo6GPCLEq04r86qYAq0woz06Yq+IlE
c1sOFtPG6Y3e7twvx1+2NelKvKIRCU+ZbJb3gyd4jZY0iu+HjCu5C4O3wTO2A6IM
XHBydSB7LyJ6d3taZmcTsQKBgDvm0k1EODf1LkHs4JBd0w65wa2juu5XgxsEW34h
P1NIIUL6oeQwNEEj1c5Vg2XPSlIrb4/L8bEfaNT1vRktGp9exiRGLnrS55EoSitz
VjoQQV+ndcj/a1XR+iYYCCRMv4NErs+0wBYhXPIuyRfLuECdOQvG2QDITi6Lan7U
HlTjAoGAInfGmkb2jNkPGuNiZ+mU0+ZrOgLza/fLL9ErZ35jUPhGFzdGxJNobklv
sNoTd+E2GAU41YkJh24bncMLvJVYxHHA5iF7FBWx1SvpEyKVhhnIcuXGD7N5PbNZ
zEdmr9C6I7cPVkWO+sUV7zfFukexIcANmsd/oBBGKRoYzP5Tti4=
-----END RSA PRIVATE KEY-----
3 changes: 3 additions & 0 deletions token/_fixtures/authkey-invalid.p8
@@ -0,0 +1,3 @@
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgEbVzfPnZPxfAyxqE
ZV05laAoJAl+/6Xt2O4mOB611sOhRANCAASgFTKjwJAAU95g++/vzKWHkzAVmNMI
tB5vTjZOOIwnEb70MsWZFIyUFD1P9Gwstz4+akHX7vI8BH6hHmBmfZZZ
5 changes: 5 additions & 0 deletions token/_fixtures/authkey-valid.p8
@@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgEbVzfPnZPxfAyxqE
ZV05laAoJAl+/6Xt2O4mOB611sOhRANCAASgFTKjwJAAU95g++/vzKWHkzAVmNMI
tB5vTjZOOIwnEb70MsWZFIyUFD1P9Gwstz4+akHX7vI8BH6hHmBmfeQl
-----END PRIVATE KEY-----

0 comments on commit 2cf793a

Please sign in to comment.