Skip to content

Commit

Permalink
Merge dc6191c into 70ae8f6
Browse files Browse the repository at this point in the history
  • Loading branch information
jparound30 committed Feb 28, 2021
2 parents 70ae8f6 + dc6191c commit 8969bba
Show file tree
Hide file tree
Showing 7 changed files with 323 additions and 17 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/apiconnstate.json
/cert/server.key
/cert/server.crt

config.json

# Created by .ignore support plugin (hsz.mobi)
### macOS template
Expand Down
232 changes: 232 additions & 0 deletions apiconn.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,34 @@ package goboxer

import (
"encoding/json"
"encoding/pem"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"strings"
"sync"
"time"

"github.com/dgrijalva/jwt-go/v4"
"github.com/google/uuid"
"github.com/youmark/pkcs8"
"golang.org/x/xerrors"
)

const (
refreshMarginInSec = 60.0
)

func generateUniqueIdForJwt() string {
uuidGen, err := uuid.NewRandom()
if err != nil {
return ""
}
return uuidGen.URN()
}

// TODO Suppressing Notifications https://developer.box.com/reference#suppressing-notifications

// APIConnRefreshNotifier is the interface that notifies the refresh result AccessToken/RefreshToken
Expand Down Expand Up @@ -43,6 +57,8 @@ type APIConn struct {
notifier APIConnRefreshNotifier
accessTokenLock sync.RWMutex
RestrictedTo []*FileScope `json:"restricted_to"`
jwtConfig *jwtConfig
privateKey interface{}
}

// Common Initialization
Expand Down Expand Up @@ -283,3 +299,219 @@ func (ac *APIConn) lockAccessToken() (string, error) {
func (ac *APIConn) unlockAccessToken() {
ac.accessTokenLock.Unlock()
}

type jwtConfig struct {
BoxAppSettings struct {
ClientID string `json:"clientID"`
ClientSecret string `json:"clientSecret"`
AppAuth struct {
PublicKeyID string `json:"publicKeyID"`
PrivateKey string `json:"privateKey"`
Passphrase string `json:"passphrase"`
} `json:"appAuth"`
} `json:"boxAppSettings"`
EnterpriseID string `json:"enterpriseID"`
}

type boxJwt struct {
BoxSubType string `json:"box_sub_type"`
Audience string `json:"aud"`
ExpiresAt int64 `json:"exp"`
jwt.StandardClaims
}

// NewAPIConnWithJwtConfig allocates and returns a new Box API connection from Jwt config.
func NewAPIConnWithJwtConfig(jwtConfigPath string) (*APIConn, error) {
// 1. Read JSON configuration
jwtConfig, err := loadJwtConfig(jwtConfigPath)
if err != nil {
return nil, xerrors.Errorf("failed to load jwt config %w", err)
}

// 2. Decrypt private key
pkey, err := decryptPrivateKey(jwtConfig, err)
if err != nil {
return nil, xerrors.Errorf("failed to decrypt private key %w", err)
}

instance := &APIConn{
ClientID: jwtConfig.BoxAppSettings.ClientID,
ClientSecret: jwtConfig.BoxAppSettings.ClientSecret,
jwtConfig: jwtConfig,
privateKey: pkey,
}
instance.commonInit()

jwtAssertion, err := instance.createJwtAssertionForServiceAccount()
if err != nil {
return nil, xerrors.Errorf("failed to create jwt assertion. %w", err)
}
err = instance.authenticateWithJwt(jwtAssertion)
if err != nil {
return nil, xerrors.Errorf("failed to authenticate with jwt. %w", err)
}
return instance, nil
}

// NewAPIConnWithJwtConfigForUser allocates and returns a new Box API connection from Jwt config.
func NewAPIConnWithJwtConfigForUser(jwtConfigPath string, userId string) (*APIConn, error) {
// 1. Read JSON configuration
jwtConfig, err := loadJwtConfig(jwtConfigPath)
if err != nil {
return nil, xerrors.Errorf("failed to load jwt config %w", err)
}

// 2. Decrypt private key
pkey, err := decryptPrivateKey(jwtConfig, err)
if err != nil {
return nil, xerrors.Errorf("failed to decrypt private key %w", err)
}

instance := &APIConn{
ClientID: jwtConfig.BoxAppSettings.ClientID,
ClientSecret: jwtConfig.BoxAppSettings.ClientSecret,
jwtConfig: jwtConfig,
privateKey: pkey,
}
instance.commonInit()

jwtAssertion, err := instance.createJwtAssertionForUser(userId)
if err != nil {
return nil, xerrors.Errorf("failed to create jwt assertion. %w", err)
}
err = instance.authenticateWithJwt(jwtAssertion)
if err != nil {
return nil, xerrors.Errorf("failed to authenticate with jwt. %w", err)
}
return instance, nil
}

func decryptPrivateKey(jwtConfig *jwtConfig, err error) (interface{}, error) {
block, _ := pem.Decode([]byte(jwtConfig.BoxAppSettings.AppAuth.PrivateKey))
if block == nil {
return nil, xerrors.New("failed to decode a PEM")
}

pkey, _, err := pkcs8.ParsePrivateKey(
block.Bytes,
[]byte(jwtConfig.BoxAppSettings.AppAuth.Passphrase),
)
if err != nil {
log.Printf("failed to parse private key. %+v", err)
return nil, xerrors.Errorf("failed to parse private key. %w", err)
}
return pkey, nil
}

func loadJwtConfig(jwtConfigPath string) (*jwtConfig, error) {
configFile, err := ioutil.ReadFile(jwtConfigPath)
if err != nil {
return nil, xerrors.Errorf("failed to read Jwt Config File. %w", err)
}
jwtConfig := jwtConfig{}
err = json.Unmarshal(configFile, &jwtConfig)
if err != nil {
return nil, xerrors.Errorf("failed to parse Jwt Config File. %w", err)
}
return &jwtConfig, nil
}

func (ac *APIConn) createJwtAssertionForServiceAccount() (string, error) {
// 3. Create JWT assertion
boxJwt := boxJwt{
BoxSubType: "enterprise",
Audience: ac.TokenURL,
ExpiresAt: time.Now().Add(time.Duration(55) * time.Second).Unix(),
StandardClaims: jwt.StandardClaims{
Issuer: ac.jwtConfig.BoxAppSettings.ClientID,
Subject: ac.jwtConfig.EnterpriseID,
ID: generateUniqueIdForJwt(),
IssuedAt: nil,
NotBefore: nil,
},
}

token := jwt.NewWithClaims(jwt.SigningMethodRS256, boxJwt)
token.Header["kid"] = ac.jwtConfig.BoxAppSettings.AppAuth.PublicKeyID
signedString, err := token.SignedString(ac.privateKey)
if err != nil {
log.Printf("failed to signing token : %s", err)
return "", xerrors.New("failed to signing token")
}

return signedString, nil
}

func (ac *APIConn) createJwtAssertionForUser(userId string) (string, error) {
// 3. Create JWT assertion
boxJwt := boxJwt{
BoxSubType: "user",
Audience: ac.TokenURL,
ExpiresAt: time.Now().Add(time.Duration(55) * time.Second).Unix(),
StandardClaims: jwt.StandardClaims{
Issuer: ac.jwtConfig.BoxAppSettings.ClientID,
Subject: userId,
ID: generateUniqueIdForJwt(),
IssuedAt: nil,
NotBefore: nil,
},
}

token := jwt.NewWithClaims(jwt.SigningMethodRS256, boxJwt)
token.Header["kid"] = ac.jwtConfig.BoxAppSettings.AppAuth.PublicKeyID
signedString, err := token.SignedString(ac.privateKey)
if err != nil {
log.Printf("failed to signing token : %s", err)
return "", xerrors.New("failed to signing token")
}

return signedString, nil
}

func (ac *APIConn) authenticateWithJwt(jwt string) error {
ac.rwLock.Lock()
defer ac.rwLock.Unlock()

var params = url.Values{}
params.Add("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer")
params.Add("assertion", jwt)
params.Add("client_id", ac.ClientID)
params.Add("client_secret", ac.ClientSecret)

header := http.Header{}
header.Add(httpHeaderContentType, ContentTypeFormUrlEncoded)

request := NewRequest(ac, ac.TokenURL, POST, header, strings.NewReader(params.Encode()))
request.shouldAuthenticate = false

resp, err := request.Send()
if err != nil {
ac.notifyFail(err)
return err
}

if resp.ResponseCode != http.StatusOK {
log.Printf("error: %+v", resp)
log.Printf("body: %s", string(resp.Body))
err := xerrors.New("failed to Authenticate with Jwt")
ac.notifyFail(err)
return err
}

var tokenResp tokenResponse
if err := json.Unmarshal(resp.Body, &tokenResp); err != nil {
log.Printf("error:\n%+v", tokenResp)
err = xerrors.Errorf("failed to parse response. error = %w", err)
ac.notifyFail(err)
return err
}

ac.AccessToken = tokenResp.AccessToken
ac.RefreshToken = tokenResp.RefreshToken
ac.Expires = tokenResp.ExpiresIn
ac.LastRefresh = time.Now()

ac.notifySuccess()

return nil
}
15 changes: 5 additions & 10 deletions apiconn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,6 @@ func TestApiConn_Refresh(t *testing.T) {
if err != nil {
t.Fatalf("予期しないエラー:%v", err)
}
//if (err != nil) != tt.wantErr {
// t.Errorf("GetConfiguration() error = %v, wantErr %v", err, tt.wantErr)
// return
//}
if apiConn.AccessToken != "ACCESS_TOKEN_2" {
t.Errorf("WRONG AC")
}
Expand Down Expand Up @@ -267,10 +263,6 @@ func TestApiConn_Authenticate(t *testing.T) {
if err != nil {
t.Fatalf("予期しないエラー:%v", err)
}
//if (err != nil) != tt.wantErr {
// t.Errorf("GetConfiguration() error = %v, wantErr %v", err, tt.wantErr)
// return
//}
if apiConn.AccessToken != "ACCESS_TOKEN_2" {
t.Errorf("WRONG AC")
}
Expand Down Expand Up @@ -434,7 +426,7 @@ func TestApiConn_SaveStateAndRestore(t *testing.T) {
"TOKEN_URL", "REVOKE_URL", "BASE_URL", "BASE_UPLOAD_URL",
"AUTHORIZATION_URL", "USER_AGENT", testTime, 3600.0, 10,
sync.RWMutex{}, nil, sync.RWMutex{},
nil,
nil, nil, "",
},
false},
}
Expand All @@ -457,6 +449,9 @@ func TestApiConn_SaveStateAndRestore(t *testing.T) {
rwLock: sync.RWMutex{},
notifier: nil,
accessTokenLock: sync.RWMutex{},
RestrictedTo: nil,
jwtConfig: nil,
privateKey: "",
}
got, err := ac.SaveState()
if (err != nil) != tt.wantErr {
Expand All @@ -471,7 +466,7 @@ func TestApiConn_SaveStateAndRestore(t *testing.T) {
opt := cmp.AllowUnexported(APIConn{})
opt1 := cmpopts.IgnoreUnexported(sync.RWMutex{})
if diff := cmp.Diff(ac, tt.want, opt, opt1); diff != "" {
t.Errorf("APIConn.SaveState() = \n%v, want \n%v\n", ac, &tt.want)
t.Errorf("APIConn.SaveState() = \n%v, want \n%v\n", ac, tt.want)
}
})
}
Expand Down
63 changes: 63 additions & 0 deletions cmd/sandbox/jwt_example.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package main

import (
"flag"
"fmt"
"log"
"os"
"strings"

"github.com/jparound30/goboxer"
)

const (
rootFolderId = "0"
jwtConfigFilePath = "./config.json"
)

func main() {

var userId string
flag.StringVar(&userId, "userId", "0", "your userId")
flag.VisitAll(func(f *flag.Flag) {
if s := os.Getenv(strings.ToUpper(f.Name)); s != "" {
_ = f.Value.Set(s)
}
})
flag.Parse()

if userId == "0" {
log.Fatalf("userId must be non-zero")
}

fmt.Printf(`=====
START REQUEST JWT TOKEN for ENTERPRISE.
=====
`)
apiConn, err := goboxer.NewAPIConnWithJwtConfig(jwtConfigFilePath)
if err != nil {
log.Fatalf("Failed: ENTERPRISE")
}
folder := goboxer.NewFolder(apiConn)
getInfo, err := folder.GetInfo(rootFolderId, nil)
if err != nil {
log.Fatalf("Failed: ENTERPRISE GET FOLDER")
}
log.Printf("%v\n", getInfo)

fmt.Printf(`=====
START REQUEST JWT TOKEN for User.
=====
`)
apiConnUser, err := goboxer.NewAPIConnWithJwtConfigForUser(jwtConfigFilePath, userId)
if err != nil {
log.Fatalf("Failed: USER")
}
folder = goboxer.NewFolder(apiConnUser)
getInfo, err = folder.GetInfo(rootFolderId, nil)
if err != nil {
log.Fatalf("Failed: USER GET FOLDER")
}
log.Printf("%v\n", getInfo)

}

0 comments on commit 8969bba

Please sign in to comment.