Skip to content

Commit

Permalink
feat: web driver use token sources for authorization (#548)
Browse files Browse the repository at this point in the history
Currently, only support github app token source using a rsa private key.
  • Loading branch information
Charles546 committed Jun 15, 2023
1 parent e20add8 commit 545b736
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 28 deletions.
7 changes: 6 additions & 1 deletion drivers/cmd/web/main.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2022 PayPal Inc.
// Copyright 2023 PayPal Inc.

// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT License was not distributed with this file,
Expand Down Expand Up @@ -78,6 +78,11 @@ func prepareRequest(m *dipper.Message) *http.Request {
}
}

if tokenSource, ok := dipper.GetMapDataStr(m.Payload, "tokenSource"); ok && len(tokenSource) > 0 {
token := getToken(tokenSource)
header.Set("Authorization", "Bearer "+token)
}

method, ok := dipper.GetMapDataStr(m.Payload, "method")
if !ok {
method = "GET"
Expand Down
1 change: 1 addition & 0 deletions drivers/cmd/web/test_fixtures/testkey
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb2dJQkFBS0NBUUVBbURjeVFFZUQrTWRaSFo2Mi9pdXNUUUpHNDR0dlZvdXM5VmgvWkdkY094QTdQaE1PCmJXUXY3Y056aDRhc1lBRkNnUGc4UmtGVTdGQVg3dlBBeG91K1BiR0NGUi9nN1VMSER2amhucGtMRXpRZVp0TnMKc2lUNVRwT0ZFT05zQ3BURlJVYmpmWFBjek40cDQ4TUF6MldMSVIyeDBCWldzRktCb3BnUWxZMTJyMnprR2xHeQo1Z21DczIvdEp6cWdta3d5QUNGKzBRUklXZ1RueGt3ZzFoSjRSR0x1NFd5aFB0TzNnZWxHQUlsMDloV0pFUnY1Cnh2SDBKVFFUQ2ZBcmJmWmZiMHUwRkJod1JrU05KaU5xTlliazNkUVRkSmcwbG9nSlpEY0VMeThhcTIvY0x4VUgKb3F2c0N4UnZMam92dTNkYWUwOGMxTThSM1BGQ2RHaHhCVmVqMXdJREFRQUJBb0lCQUNzT1llNkF6RG5RMmNwaApITTRrdUdaSUlKazQxZE9iU3Q5VG15VmhmMXROcWhSUys1L0IyVFRlTm8yOWNJRHZta28wN1lmSjd5V3hPalBqClMwSmVRUC9lZURkVmZ5QmQ1VVM4N2NVWThXTUxPUlpJODlRb1ZVVCt3WU1YY1haRXd0Qm56dTJybW1kdzZGUisKMG5uWDlWVDJ1MWRyR2paaUFEMW4yamtUZk9EOThwRDBuRGI2VGEvVzV4RURYRTBSdFRxK1ROZ1ZBbGpxOVdqSwo2a1FzdHlVZGJPbXBJYXlibEszK2tQRnFNM25tcVBnaE00UVcyRGg3OHNOUCtZell2VlhDVWVYNTYxVmVSWkt2CmI2Y05jYmF6N3RkajBObW56amJEcjE5KzUvTGZFSDFDV2dnSzg3VmlHaW5jMS95Vm56L2VoeFdHbjZ2N2dzZ3MKTlMyVnRVRUNnWUVBeDVOdGlGb0gwRVhtdmo5aGJQd3MzUWlLMW1hamR3b0JzcTdjQ2t0OWY2WDBlcHdoNlBCNQpMMkJjWS9uRE9DNCtvWENYWUlQOGltRmswaEhFc1AwRWRaQVFIa3dQNHRLeUlQRXByaTdBNUdWZURtWXluSUtNCm9wR2poVXhiVFozNmpzcWxBRnJ5YXFtdHFsdTdmSVJYWDdraE1POHNCSjFZd3VLMFhKKzBsd2NDZ1lFQXcwQUIKTzVmdVRKOWFNTjRsOXRLTEE1aHludS9jMGgxUVVLR09Rc3lwRmsvRHk5enJWVkxJYi9ob2xDUXNPTTFWY1ZvUApSejQ3RjJvWjFkaUNJbmFvZU9kVWNtVzlkMWFwRHFYNmNxbnVWN0JzQUJnY0ViRUt1MkpIWjArUExQTFk0Si9jCnFkNFJqdHFZa25JZVZ2TEYvOUhldVl3RWVZL0IxVlNSck9sbENMRUNnWUJxY3d4ZFNnZ1k0dS9zVWNvWlkzaGEKZlEvd3c5WTB6RFdUcFFqZ3hOc3Zsc2tNRFBOWlY4cUxwbzRoRlRzM1lCTXY4T29OSk5reXhqZ01oRVd4VVlOcgpZV2YzZ1FLSUxYR3RlSFNPMzRrclNaWWRnQTFHeGF0Vm12RHBUSXoybldqamVOc0JrWUR6dTRWUjlKUFFHcGF3CkRBTFVJdjRMaUJHc0FWZktmN1RIU1FLQmdCbU1BMTFIeU05UHZsNU1nczBqeVRxa05NTWxBVkNnczBTSmp2S2cKa3JNdnBwL0MvU3ZCMUNZS2E2eU9leGJIanhsd3ZqVUZLSGdzMHNxUE5KL0x4TWxsQTBDZ25VVERHd1dtby9saQowS082bXJiOGNKZkVBWEo1TG55UEJWM05QS0ZQYVhEMGRIbXJrbkQrNjRkVzVwOU5WNFlSa3ZoUTNmekt2dkRQCjdQOVJBb0dBT0tDK1VYbWQ3UG1sNjZZNzd6Y2J4U0ZySmdMUnVpNjNhSUMvTjQ2dEJCeWpyb0Y1R210MFZCOG4KVEd0T1hBNGJqWENhT3NQZldjWlptTko5Z3AvYkFzeUlabXQzRkRGMlI0MHJORm9kWWdYdGxyRmdBUFNvaEpmSgpRNngzRU1uWmF3V0lJcXNIUmVQU3JHYlFFcElTb0pSS0FJU0NKUDFybXh3NkJpRzlFZEU9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==
99 changes: 99 additions & 0 deletions drivers/cmd/web/token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright 2023 PayPal Inc.

// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT License was not distributed with this file,
// you can obtain one at https://mit-license.org/.

// Package web enables Honeydipper to make outbound web requests.
package main

import (
"bytes"
"crypto/rsa"
"encoding/json"
"io"
"net/http"
"time"

"github.com/golang-jwt/jwt/v5"
"github.com/honeydipper/honeydipper/pkg/dipper"
)

const globalGitHubURL = "https://api.github.com"

func getToken(source string) string {
s := dipper.MustGetMapData(driver.Options, "token_sources."+source).(map[string]interface{})
switch s["type"].(string) {
case "github":

return getGitHubToken(s)
default:
log.Panicf("[%s] unknown token source type: %+v", driver.Service, s["type"])
}

return ""
}

func getGitHubToken(s map[string]interface{}) string {
saved, ok := s["_saved"]
if ok {
exp := dipper.MustGetMapData(s, "_expiresAt").(time.Time)
//nolint: gomnd
if time.Now().Add(2 * time.Second).Before(exp) {
return saved.(string)
}
}

//nolint: gomnd
expiresAt := time.Now().Add(time.Minute * 15)
claims := &jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(time.Now().Add(-time.Minute * 1)),
ExpiresAt: jwt.NewNumericDate(expiresAt),
Issuer: s["app_id"].(string),
}
jwtToken := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)

var pk *rsa.PrivateKey
if b, ok := s["_parsed_key"]; ok {
pk = b.(*rsa.PrivateKey)
} else {
b := dipper.MustGetMapDataStr(s, "key")
pk = dipper.Must(jwt.ParseRSAPrivateKeyFromPEM([]byte(b))).(*rsa.PrivateKey)
s["_parsed_key"] = pk
}
jwtTokenStr := dipper.Must(jwtToken.SignedString(pk)).(string)

header := http.Header{}
header.Set("accept", "application/vnd.github+json")
header.Set("authorization", "Bearer "+jwtTokenStr)

permissions := dipper.MustGetMapData(s, "permissions").(map[string]interface{})
contentBytes := dipper.Must(json.Marshal(map[string]interface{}{
"permissions": permissions,
})).([]byte)
buf := bytes.NewBuffer(contentBytes)

instID := dipper.MustGetMapDataStr(s, "installation_id")

u, ok := s["github_url"]
if !ok {
u = globalGitHubURL
}
req := dipper.Must(http.NewRequest("POST", u.(string)+"/app/installations/"+instID+"/access_token", buf)).(*http.Request)
client := http.Client{}
//nolint: bodyClose
resp := dipper.Must(client.Do(req)).(*http.Response)
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Panicf("[%s] failed to fetch github access token with status code %+v", driver.Service, resp.StatusCode)
}

bodyObj := map[string]interface{}{}
dipper.Must(json.Unmarshal(dipper.Must(io.ReadAll(resp.Body)).([]byte), &bodyObj))

token := dipper.MustGetMapDataStr(bodyObj, "token")
s["_saved"] = token
s["_expiresAt"] = expiresAt

return token
}
79 changes: 79 additions & 0 deletions drivers/cmd/web/token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright 2023 PayPal Inc.

// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT License was not distributed with this file,
// you can obtain one at https://mit-license.org/.

//go:build !integration
// +build !integration

package main

import (
"encoding/base64"
"io/ioutil"
"testing"
"time"

"github.com/honeydipper/honeydipper/pkg/dipper"
"github.com/stretchr/testify/assert"
"gopkg.in/h2non/gock.v1"
)

func TestGetToken(t *testing.T) {
defer gock.Off()

gock.New("https://api.github.com").
Post("/app/installations/123/access_token").
Reply(200).
JSON(map[string]string{"token": "foobar"})

keyb64 := dipper.Must(ioutil.ReadFile("test_fixtures/testkey")).([]byte)
keybytes := dipper.Must(base64.StdEncoding.DecodeString(string(keyb64))).([]byte)

githubSource := map[string]interface{}{
"type": "github",
"app_id": "345",
"installation_id": "123",
"key": string(keybytes),
"permissions": map[string]interface{}{
"content": "write",
},
}

driver.Options = map[string]interface{}{
"token_sources": map[string]interface{}{
"test1": githubSource,
},
}

var token string
assert.NotPanicsf(t, func() { token = getToken("test1") }, "should not panic when getting token")
assert.Equalf(t, "foobar", token, "should get the test token")
assert.Containsf(t, githubSource, "_saved", "should save the token for future use")
assert.Containsf(t, githubSource, "_expiresAt", "should save the expiresAt")
assert.Containsf(t, githubSource, "_parsed_key", "should save the expiresAt")

githubSource["_saved"] = "newtoken"
assert.NotPanicsf(t, func() { token = getToken("test1") }, "should not panic when getting token from cache")
assert.Equalf(t, "newtoken", token, "should get the saved token")

gock.New("https://api.github.com").
Post("/app/installations/123/access_token").
Reply(200).
JSON(map[string]string{"token": "foobar2"})

githubSource["_expiresAt"] = time.Now().Add(-time.Minute * 15)
assert.NotPanicsf(t, func() { token = getToken("test1") }, "should not panic when refreshing token")
assert.Equalf(t, "foobar2", token, "should get a new token if expired")

gock.New("https://api.github.com").
Post("/app/installations/123/access_token").
Reply(200).
JSON(map[string]string{"token": "foobar3"})

githubSource["_expiresAt"] = time.Now().Add(-time.Minute * 15)
githubSource["key"] = ""
assert.NotPanicsf(t, func() { token = getToken("test1") }, "should not panic when refreshing token with _parsed_key")
assert.Equalf(t, "foobar3", token, "should get a new token using _parsed_key")
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ require (
cloud.google.com/go/logging v1.7.0
github.com/Masterminds/sprig/v3 v3.2.3
github.com/go-redis/redismock/v8 v8.0.6
github.com/golang-jwt/jwt/v5 v5.0.0
)

require (
Expand Down
Loading

0 comments on commit 545b736

Please sign in to comment.