Skip to content

Commit 0ee76ab

Browse files
authored
feat: add hashed token to responses for correlation with GitHub audit logs (#163)
1 parent 25ed084 commit 0ee76ab

File tree

6 files changed

+63
-6
lines changed

6 files changed

+63
-6
lines changed

README.org

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,15 @@ favour =repository_ids= (GitHub repository IDs are immutable) instead of
354354
=repositories= (GitHub repository names are mutable).
355355
#+end_quote
356356

357+
#+begin_quote
358+
NOTE: All token responses (including those from permission sets) include
359+
=hashed_token=, a base64-encoded SHA-256 hash of the returned token that matches
360+
GitHub's [[https://docs.github.com/en/enterprise-cloud@latest/admin/monitoring-activity-in-your-enterprise/reviewing-audit-logs-for-your-enterprise/identifying-audit-log-events-performed-by-an-access-token#token-data-in-audit-log-events][audit log]] =hashed_token= field. This value is safe to log and enables
361+
correlation between Vault-issued tokens and GitHub audit events by searching for
362+
=hashed_token:"VALUE"=. You can verify the hash yourself with:
363+
=echo -n TOKEN | openssl dgst -sha256 -binary | base64=.
364+
#+end_quote
365+
357366
- =installation_id= (int64) — the ID of the app installation.
358367
- =org_name= (string) — the organisation name.
359368
- =repositories= ([]string) — a list of the names of the repositories within the

github/client.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package github
33
import (
44
"bytes"
55
"context"
6+
"crypto/sha256"
7+
"encoding/base64"
68
"encoding/json"
79
"fmt"
810
"io"
@@ -240,6 +242,10 @@ func (c *Client) token(ctx context.Context, tokReq *tokenRequest) (*logical.Resp
240242
}
241243
}
242244

245+
if hashedToken, ok := hashedTokenFromValue(resData["token"]); ok {
246+
resData["hashed_token"] = hashedToken
247+
}
248+
243249
// Enrich the response with what we know about the installation.
244250
tokRes := &logical.Response{Data: resData}
245251
tokRes.Data["installation_id"] = tokReq.InstallationID
@@ -274,6 +280,17 @@ func (c *Client) accessTokenURLForInstallationID(installationID int) (*url.URL,
274280
return url.ParseRequestURI(fmt.Sprintf(c.accessTokenURLTemplate, installationID))
275281
}
276282

283+
func hashedTokenFromValue(token any) (string, bool) {
284+
tokenStr, ok := token.(string)
285+
if !ok || tokenStr == "" {
286+
return "", false
287+
}
288+
289+
hash := sha256.Sum256([]byte(tokenStr))
290+
291+
return base64.StdEncoding.EncodeToString(hash[:]), true
292+
}
293+
277294
// ListInstallations retrieves a list of App installations associated with the
278295
// client. It returns a logical.Response containing a map where the keys are
279296
// account names and the values are corresponding installation IDs. In case of

github/client_test.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@ import (
1919
)
2020

2121
const (
22-
testRepo1 = "vault-plugin-secrets-github"
23-
testRepo2 = "hashitalkaunz"
24-
testRepoID1 = 223704264
25-
testRepoID2 = 360447594
26-
testToken = "ghs_1aRGyjpfMQ98l0rnji5dstEEg10rOY3lenzG"
22+
testRepo1 = "vault-plugin-secrets-github"
23+
testRepo2 = "hashitalkaunz"
24+
testRepoID1 = 223704264
25+
testRepoID2 = 360447594
26+
testToken = "ghs_1aRGyjpfMQ98l0rnji5dstEEg10rOY3lenzG"
27+
testTokenHash = "p/q8OhHzX3aiPGHBmqq6IfTS1bYVqOCj1ct2oSJekGg=" // echo -n testToken | openssl dgst -sha256 -binary | base64
2728
)
2829

2930
var (
@@ -147,6 +148,7 @@ func TestClient_Token(t *testing.T) {
147148
}),
148149
res: &logical.Response{
149150
Data: map[string]any{
151+
"hashed_token": testTokenHash,
150152
"token": testToken,
151153
"installation_id": testInsID1,
152154
"expires_at": testTokenExp,
@@ -193,6 +195,7 @@ func TestClient_Token(t *testing.T) {
193195
}),
194196
res: &logical.Response{
195197
Data: map[string]any{
198+
"hashed_token": testTokenHash,
196199
"token": testToken,
197200
"installation_id": testInsID1,
198201
"expires_at": testTokenExp,
@@ -316,6 +319,7 @@ func TestClient_Token(t *testing.T) {
316319
}),
317320
res: &logical.Response{
318321
Data: map[string]any{
322+
"hashed_token": testTokenHash,
319323
"token": testToken,
320324
"installation_id": testInsID1,
321325
"expires_at": testTokenExp,
@@ -360,6 +364,7 @@ func TestClient_Token(t *testing.T) {
360364
}),
361365
res: &logical.Response{
362366
Data: map[string]any{
367+
"hashed_token": testTokenHash,
363368
"token": testToken,
364369
"installation_id": testInsID1,
365370
"expires_at": testTokenExp,
@@ -464,6 +469,7 @@ func TestClient_Token(t *testing.T) {
464469
if tc.res != nil && tc.res.Data != nil {
465470
assert.Equal(t, res.Data["expires_at"], tc.res.Data["expires_at"])
466471
assert.Equal(t, res.Data["token"], tc.res.Data["token"])
472+
assert.Equal(t, res.Data["hashed_token"], tc.res.Data["hashed_token"])
467473

468474
if _, ok := tc.res.Data["permissions"]; ok {
469475
testPerms := tc.res.Data["permissions"].(map[string]string)
@@ -476,7 +482,6 @@ func TestClient_Token(t *testing.T) {
476482
resRepos := res.Data["repositories"].([]any)
477483
assert.Equal(t, len(resRepos), len(testRepos))
478484
}
479-
assert.Equal(t, res.Data["token"], tc.res.Data["token"])
480485
}
481486
})
482487
}

github/integration_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,8 @@ func testCreateTokenByInstallationID(t *testing.T) {
368368
assert.Assert(t, resData["token"] != "")
369369
assert.Assert(t, resData["expires_at"] != "")
370370
}
371+
372+
assertHashedTokenMatchesToken(t, resData)
371373
}
372374

373375
func testCreateTokenByOrgName(t *testing.T) {
@@ -396,6 +398,8 @@ func testCreateTokenByOrgName(t *testing.T) {
396398
assert.Assert(t, resData["token"] != "")
397399
assert.Assert(t, resData["expires_at"] != "")
398400
}
401+
402+
assertHashedTokenMatchesToken(t, resData)
399403
}
400404

401405
func testCreatePermissionSetToken(t *testing.T) {
@@ -423,6 +427,8 @@ func testCreatePermissionSetToken(t *testing.T) {
423427
assert.Assert(t, resData["token"] != "")
424428
assert.Assert(t, resData["expires_at"] != "")
425429
}
430+
431+
assertHashedTokenMatchesToken(t, resData)
426432
}
427433

428434
func testRevokeTokens(t *testing.T) {
@@ -473,6 +479,24 @@ func testCreateTokenByInstallationIDWithConstraints(t *testing.T) {
473479
assert.Assert(t, resData["token"] != "")
474480
assert.Assert(t, resData["expires_at"] != "")
475481
}
482+
483+
assertHashedTokenMatchesToken(t, resData)
484+
}
485+
486+
func assertHashedTokenMatchesToken(t *testing.T, resData map[string]any) {
487+
t.Helper()
488+
489+
token, ok := resData["token"].(string)
490+
assert.Assert(t, ok, "response missing token string: %#v", resData)
491+
assert.Assert(t, token != "")
492+
493+
hashedToken, ok := resData["hashed_token"].(string)
494+
assert.Assert(t, ok, "response missing hashed_token string: %#v", resData)
495+
assert.Assert(t, hashedToken != "")
496+
497+
expectedHash, ok := hashedTokenFromValue(token)
498+
assert.Assert(t, ok)
499+
assert.Equal(t, hashedToken, expectedHash)
476500
}
477501

478502
func vaultDo(

github/path_token_permission_set_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ func testBackendPathTokenPermissionSetWrite(t *testing.T, op logical.Operation)
7878
assert.Assert(t, r != nil)
7979
assert.Equal(t, r.Data["expires_at"].(string), testTokenExp)
8080
assert.Equal(t, r.Data["token"].(string), testToken)
81+
assert.Equal(t, r.Data["hashed_token"].(string), testTokenHash)
8182
})
8283

8384
t.Run("MissingInstallationID", func(t *testing.T) {

github/path_token_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ func testBackendPathTokenWrite(t *testing.T, op logical.Operation) {
6969
assert.Assert(t, r != nil)
7070
assert.Equal(t, r.Data["expires_at"].(string), testTokenExp)
7171
assert.Equal(t, r.Data["token"].(string), testToken)
72+
assert.Equal(t, r.Data["hashed_token"].(string), testTokenHash)
7273
})
7374

7475
t.Run("FailedClient", func(t *testing.T) {

0 commit comments

Comments
 (0)