Skip to content

Commit

Permalink
[WIP] Add Apple JWT Token based authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
sideshow committed Sep 28, 2016
1 parent 0ef66aa commit 892c8a4
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 11 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# 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)

## Features

- Uses new Apple APNs HTTP/2 connection
- Supports new Apple Token Based Authentication (JWT)
- Works with older versions of go (1.5.x) not just 1.6
- Supports new iOS 10 features such as Collapse IDs, Subtitles and Mutable Notifications
- Supports persistent connections to APNs
Expand Down
16 changes: 8 additions & 8 deletions _example/main.go → _example/simple.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"fmt"
"log"

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

func main() {

cert, pemErr := certificate.FromPemFile("../cert.pem", "")
cert, pemErr := certificate.FromP12File("../cert.p12", "")
if pemErr != nil {
log.Println("Cert Error:", pemErr)
}

notification := &apns.Notification{}
notification.DeviceToken = "11aa01229f15f0f0c52029d8cf8cd0aeaf2365fe4cebc4af26cd6d76b7919ef7"
notification.Topic = "com.sideshow.Apns2"
notification.DeviceToken = "cb7262544176c3f15efdcdcf9dd03418dfca82ba710c54ab6b1352350d442cb4"
notification.Topic = "com.Apns2"
notification.Payload = []byte(`{
"aps" : {
"alert" : "Hello!"
"alert" : "Hello!"
}
}
`)
Expand All @@ -28,9 +29,8 @@ func main() {
res, err := client.Push(notification)

if err != nil {
log.Println("Error:", err)
return
log.Fatal("Error: ", err)
} else {
fmt.Printf("%v %v %v\n", res.StatusCode, res.ApnsID, res.Reason)
}

log.Println("APNs ID:", res.ApnsID)
}
42 changes: 42 additions & 0 deletions _example/token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package main

import (
"fmt"
"log"

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

func main() {

authKey, err := token.AuthKeyFromFile("../APNSAuthKey_T64N7W47U9.p8")
if err != nil {
log.Fatal("token error:", err)
}

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

notification := &apns.Notification{}
notification.DeviceToken = "cb7262544176c3f15efdcdcf9dd03418dfca82ba710c54ab6b1352350d442cb4"
notification.Topic = "com.Apns2"
notification.Payload = []byte(`{
"aps" : {
"alert" : "Hello!"
}
}
`)

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

if err != nil {
log.Fatal("error: ", err)
} else {
fmt.Printf("%v %v %v\n", res.StatusCode, res.ApnsID, res.Reason)
}
}
33 changes: 30 additions & 3 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"net/http"
"time"

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

Expand All @@ -37,9 +38,10 @@ var (

// 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
}

// NewClient returns a new Client with an underlying http.Client configured with
Expand Down Expand Up @@ -73,6 +75,22 @@ func NewClient(certificate tls.Certificate) *Client {
}
}

func NewTokenClient(token *token.Token) *Client {
transport := &http2.Transport{
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
return tls.DialWithDialer(&net.Dialer{Timeout: TLSDialTimeout}, network, addr, cfg)
},
}
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 All @@ -92,13 +110,17 @@ func (c *Client) Production() *Client {
// gateway, or an error if something goes wrong.
func (c *Client) Push(n *Notification) (*Response, error) {
payload, err := json.Marshal(n)

if err != nil {
return nil, err
}

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.HTTPClient.Do(req)
if err != nil {
Expand All @@ -117,6 +139,11 @@ func (c *Client) Push(n *Notification) (*Response, error) {
return response, nil
}

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
82 changes: 82 additions & 0 deletions token/token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package token

import (
"crypto/ecdsa"
"crypto/x509"
"encoding/pem"
"errors"
"io/ioutil"
"sync"
"time"

jwt "github.com/dgrijalva/jwt-go"
)

const (
TokenTimeout = 3000 // 50 minutes
)

type Token struct {
AuthKey *ecdsa.PrivateKey
KeyID string
TeamID string
IssuedAt int64
Bearer string
m sync.Mutex
}

func AuthKeyFromFile(filename string) (*ecdsa.PrivateKey, error) {
bytes, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return AuthKeyFromBytes(bytes)
}

func AuthKeyFromBytes(bytes []byte) (*ecdsa.PrivateKey, error) {
block, _ := pem.Decode(bytes)
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, errors.New("token: AuthKey must be of type ecdsa.PrivateKey")
}
}

func (t *Token) GenerateIfExpired() {
t.m.Lock()
defer t.m.Unlock()
if t.Expired() {
t.Generate()
}
}

func (t *Token) Expired() bool {
return time.Now().Unix() >= (t.IssuedAt + TokenTimeout)
}

func (t *Token) Generate() (bool, error) {
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
}

0 comments on commit 892c8a4

Please sign in to comment.