Skip to content

Commit

Permalink
Add Github App authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
tenjaa committed Jul 19, 2021
1 parent 9ec47e2 commit edb7747
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 4 deletions.
65 changes: 65 additions & 0 deletions authentication.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package resource

import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"gopkg.in/square/go-jose.v2"
"gopkg.in/square/go-jose.v2/jwt"
"net/http"
"time"
)

type Response struct {
Token string
}

func GenerateAccessToken(s *Source, now time.Time) (string, error) {
if s.AccessToken != "" {
return s.AccessToken, nil
}

decode, _ := pem.Decode([]byte(s.PrivateKey))
key, _ := x509.ParsePKCS1PrivateKey(decode.Bytes)
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: key}, (&jose.SignerOptions{}).WithType("JWT"))
if err != nil {
panic(err)
}

cl := jwt.Claims{
Issuer: s.AppId,
IssuedAt: jwt.NewNumericDate(now),
Expiry: jwt.NewNumericDate(now.Add(9 * time.Minute)),
}
signedJwt, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
if err != nil {
panic(err)
}

var endpoint string
if s.V3Endpoint != "" {
endpoint = s.V3Endpoint
} else {
endpoint = "https://api.github.com"
}
request, err := http.NewRequest("POST", endpoint+"/app/installations/"+s.InstallationId+"/access_tokens", nil)
if err != nil {
panic(err)
}
request.Header.Add("Authorization", "Bearer "+signedJwt)
request.Header.Add("Accept", "application/vnd.github.machine-man-preview+json")
client := &http.Client{}
response, err := client.Do(request)

if err != nil {
panic(err)
}

var r Response
err = json.NewDecoder(response.Body).Decode(&r)
if err != nil {
panic(err)
}

return r.Token, nil
}
69 changes: 69 additions & 0 deletions authentication_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package resource_test

import (
"fmt"
"github.com/stretchr/testify/assert"
resource "github.com/telia-oss/github-pr-resource"
"net/http"
"net/http/httptest"
"testing"
"time"
)

func TestGenerateAccessToken(t *testing.T) {

validResponse := `
{
"token": "v1.b71be873ad96e64a84025ae7bee7694a99cb4ba9",
"expires_at": "2020-06-21T00:03:29Z",
"permissions": {
"checks": "write",
"contents": "read",
"metadata": "read",
"pull_requests": "write"
},
"repository_selection": "selected"
}
`

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "/app/installations/9912873/access_tokens", r.RequestURI)
assert.Equal(t, "application/vnd.github.machine-man-preview+json", r.Header.Get("Accept"))
assert.Equal(t, "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1OTI2OTE5ODIsImlhdCI6MTU5MjY5MTQ0MiwiaXNzIjoiNjk1OTIifQ.H_a6i7TpaGOsaoliH_i7AT5UMwM9LqO21lEFYiZ96_H15cEF6D_kyZrcHyinP2fSC8rX_OQ-DvPsehTNTtfOhgM-nsdgg-gTzdCSlASgc00sGhw_pjBFwHpD6V1NQojc82L8SAR9Bg75g0xVlQ_dAR_Lbtmk252X_AlabRAfdSchK_GVdc3kSEbp28lc87EF7J_lFdRCNuVi1xcLFOXPmMeu4epSBf1ZMtuts28C7iqaI4QJ9keaGFug1wpL-WLDcFbvmB2nJhBYN9tArGM0ZHZ5i4EhFyFjGpBwTyo5P7WY7P3zYtz36gwgntYRtPPivcFQ-wUWuvMpL6vKd-Pp8w", r.Header.Get("Authorization"))
_, _ = fmt.Fprintln(w, validResponse)
}))
defer ts.Close()

tests := []struct {
description string
source resource.Source
expectedAccessToken string
}{
{
description: "return given access token",
source: resource.Source{
Repository: "itsdalmo/test-repository",
AccessToken: "oauthtoken",
},
expectedAccessToken: "oauthtoken",
},
{
description: "create access token",
source: resource.Source{
Repository: "itsdalmo/test-repository",
AppId: "69592",
PrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAv6oJaa+0BCOT0dITtJK6oPfrE2R0Iynofj/a/vq4rfAw2MJp\nXA5l6WbwmQhevSm8sIYNb2T32qLyRsVXwBo1J8ovIoTXiPdsV41D19tytjctnf+u\n0LncTo2JJR3ik7/Ynu5Id4zjnsn1pYqwLX4EFxgCuEKwdZutPyiY2J7wATfsTtOJ\nsLF8idQijG23i5Obs6AWCZcHOhvdgfYAUOxLv2WRkCG5O1aXYa6nqVn0AgRgKjaJ\nnAENEoG7O9OEWIcUiG30riQouxMHfj0bATCvYoj7a2tvn4CUqk+SBODyh3Fvi0oC\na/NCBYFeK/5uUNyXQHqWy7xEVRef5lC0XbrUVwIDAQABAoIBAFDdqhkIQ/iXFjAp\n5ZyDZ/CwiWNmN8X6UZiq0nhQSolA1SsvY4qunHsMrqiyql4/dNg5xwNf419A7t3D\nN5HavOCr4pU63UFxuyl5dc1mTpDo2PtXvGdec8BE4T9iy40xHXF48eRW8la1uUn+\nKPUYvRsNS2B46sDETSVfuJV1AahRN6aD2WnzQ7wB+S/mqsPXqy+S2zobnU70Wzmt\nhYQzsuIY+BkfOCS565guYvJt66wRGi/NsnybC6z3iRZxZygtKorMKXPjmtdoyCZ3\njkOHhXV5XH8Ldut+1mycg+6c+dTZ9RTrAzo4ouofptm9+ZNlv1aK7HrDDKYQ/x/z\n70hhIfkCgYEA7aNt7db3S6sTEA+yC5DLxxkFIK7K8qxdP6vE087fdHO+ik1LvyI+\ni5Wqj/fT2d/lYR+cbH/zy2JBy5RWfdVJ9HBW/eZ9RqQx8ry7lE5MqbwU6y+d2bCF\nAjffx3yJK1aljuVLdGeu8abYsusUQUhbslZJNg6Ar7z+FUHaChJI0KsCgYEAznk4\nx+5PtvIWj6aXTJiqtofnOtHXXxrq5alzBErvDBHHzP9/ZCucHxJZ4zqHWduj2+9b\nJUgk2BH3+wFMVKpcdTld2iTFWGaFsArTJkE4SBQGx3zcdsoESOnu/DrOTFNu+LiV\nhLzb9CHKM91fIet8MYYxKiyH09+Mi7xBtw8WQwUCgYAH/tCrCOmHHTll9/E4nGWO\nzFO01syzP4NfqgrUSYiRJXfKtXEP/Dn4fk+fymnRUcwo6WRc7i0osaSfEd2bHDsB\nw2nZ3xBl+Q5JKXpyMfQ4XcCibRa1hU/kVDbuQk1nLOIjHandP8POE5wE4Q3saF/V\nbzvFWtWPlB9EXdPVNOpIQwKBgHkJEsIQ72XdUGBxVewu6pQJ4wDWFhzIWL68sJHp\no2w92BRSCkmcTu7gARV1L/b7DHlXPOUD/6UyE15vCmHvZDfLozrHp3AE2YWzMsgQ\nH4ARTVAP3+U603wytkfh6SFRH5JqEiw30fCxBimVMbleo/UcJyID7LPFLkyT1SoM\njA5JAoGACEhaHTXWFYXv+eTJUXFcwhDZ5sRvQYRCTLGSv746lr+SpZ02bEcRvaTH\nv0K1Hph6OzhcCO27VdimngnzsoXoa2OXicWpdjX3hqhXMvwspe9a1y9u7LPZwqJH\n9Kc7VaZP1iS4EIxEc+qx38HqdeiUBcqTMRam6k3mSwactBKCKDI=\n-----END RSA PRIVATE KEY-----\n",
InstallationId: "9912873",
V3Endpoint: ts.URL,
},
expectedAccessToken: "v1.b71be873ad96e64a84025ae7bee7694a99cb4ba9",
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
got, _ := resource.GenerateAccessToken(&tt.source, time.Unix(1592691442, 0))
assert.Equal(t, tt.expectedAccessToken, got)
})
}
}
7 changes: 6 additions & 1 deletion git.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"path/filepath"
"strconv"
"strings"
"time"
)

// Git interface for testing purposes.
Expand All @@ -34,8 +35,12 @@ func NewGitClient(source *Source, dir string, output io.Writer) (*GitClient, err
if source.DisableGitLFS {
os.Setenv("GIT_LFS_SKIP_SMUDGE", "true")
}
accessToken, err := GenerateAccessToken(source, time.Now())
if err != nil {
return nil, err
}
return &GitClient{
AccessToken: source.AccessToken,
AccessToken: accessToken,
Directory: dir,
Output: output,
}, nil
Expand Down
7 changes: 6 additions & 1 deletion github.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"path"
"strconv"
"strings"
"time"

"github.com/google/go-github/v28/github"
"github.com/shurcooL/githubv4"
Expand Down Expand Up @@ -57,8 +58,12 @@ func NewGithubClient(s *Source) (*GithubClient, error) {
ctx = context.TODO()
}

accessToken, err := GenerateAccessToken(s, time.Now())
if err != nil {
return nil, err
}
client := oauth2.NewClient(ctx, oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: s.AccessToken},
&oauth2.Token{AccessToken: accessToken},
))

var v3 *github.Client
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/tools v0.0.0-20200423205358-59e73619c742 // indirect
google.golang.org/appengine v1.6.6 // indirect
gopkg.in/square/go-jose.v2 v2.5.1
)

go 1.14
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
Expand Down
7 changes: 5 additions & 2 deletions models.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import (
type Source struct {
Repository string `json:"repository"`
AccessToken string `json:"access_token"`
AppId string `json:"app_id"`
PrivateKey string `json:"private_key"`
InstallationId string `json:"installation_id"`
V3Endpoint string `json:"v3_endpoint"`
V4Endpoint string `json:"v4_endpoint"`
Paths []string `json:"paths"`
Expand All @@ -31,8 +34,8 @@ type Source struct {

// Validate the source configuration.
func (s *Source) Validate() error {
if s.AccessToken == "" {
return errors.New("access_token must be set")
if s.AccessToken == "" && (s.AppId == "" || s.PrivateKey == "" || s.InstallationId == "") {
return errors.New("access_token or app_id and private_key and installation_id must be set")
}
if s.Repository == "" {
return errors.New("repository must be set")
Expand Down

0 comments on commit edb7747

Please sign in to comment.