-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathauth.go
More file actions
265 lines (227 loc) · 9.12 KB
/
auth.go
File metadata and controls
265 lines (227 loc) · 9.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
// Package githubauth provides utilities for GitHub authentication,
// including generating and using GitHub App tokens and installation tokens.
//
// This package implements oauth2.TokenSource interfaces for GitHub App
// authentication and GitHub App installation token generation. It is built
// on top of the golang.org/x/oauth2 library.
package githubauth
import (
"context"
"crypto/rsa"
"errors"
"net/http"
"strconv"
"time"
jwt "github.com/golang-jwt/jwt/v5"
"golang.org/x/oauth2"
)
const (
// DefaultApplicationTokenExpiration is the default expiration time for GitHub App tokens.
// The maximum allowed expiration is 10 minutes.
DefaultApplicationTokenExpiration = 10 * time.Minute
// bearerTokenType is the token type used for OAuth2 Bearer tokens.
bearerTokenType = "Bearer"
)
// Identifier constrains GitHub App identifiers to int64 (App ID) or string (Client ID).
type Identifier interface {
~int64 | ~string
}
// applicationTokenSource generates GitHub App JWTs for authentication.
// JWTs are signed with RS256 and include iat, exp, and iss claims per GitHub's requirements.
// See https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-json-web-token-jwt-for-a-github-app
type applicationTokenSource struct {
issuer string // App ID (numeric) or Client ID (alphanumeric)
privateKey *rsa.PrivateKey
expiration time.Duration
}
// ApplicationTokenOpt is a functional option for configuring an applicationTokenSource.
type ApplicationTokenOpt func(*applicationTokenSource)
// WithApplicationTokenExpiration sets the JWT expiration duration.
// Must be between 0 and 10 minutes per GitHub's JWT requirements. Invalid values default to 10 minutes.
// See https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-json-web-token-jwt-for-a-github-app#about-json-web-tokens-jwts
func WithApplicationTokenExpiration(exp time.Duration) ApplicationTokenOpt {
return func(a *applicationTokenSource) {
if exp > DefaultApplicationTokenExpiration || exp <= 0 {
exp = DefaultApplicationTokenExpiration
}
a.expiration = exp
}
}
// NewApplicationTokenSource creates a GitHub App JWT token source.
// Accepts either int64 App ID or string Client ID. GitHub recommends Client IDs for new apps.
// Private key must be in PEM format. Generated JWTs are RS256-signed with iat, exp, and iss claims.
// JWTs expire in max 10 minutes and include clock drift protection (iat set 60s in past).
//
// The returned token source is wrapped in oauth2.ReuseTokenSource to prevent unnecessary
// token regeneration. Don't worry about wrapping the result again since ReuseTokenSource
// prevents re-wrapping automatically.
//
// See https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-json-web-token-jwt-for-a-github-app
func NewApplicationTokenSource[T Identifier](id T, privateKey []byte, opts ...ApplicationTokenOpt) (oauth2.TokenSource, error) {
var issuer string
var isZeroValue bool
// Convert the identifier to string and check for zero values
switch v := any(id).(type) {
case int64:
isZeroValue = v == 0
issuer = strconv.FormatInt(v, 10)
case string:
isZeroValue = v == ""
issuer = v
default:
return nil, errors.New("unsupported identifier type")
}
if isZeroValue {
return nil, errors.New("application identifier is required")
}
privKey, err := jwt.ParseRSAPrivateKeyFromPEM(privateKey)
if err != nil {
return nil, err
}
t := &applicationTokenSource{
issuer: issuer,
privateKey: privKey,
expiration: DefaultApplicationTokenExpiration,
}
for _, opt := range opts {
opt(t)
}
return oauth2.ReuseTokenSource(nil, t), nil
}
// Token generates a GitHub App JWT with required claims: iat, exp, iss, and alg.
// The iat claim is set 60 seconds in the past to account for clock drift.
// Generated JWTs can be used with "Authorization: Bearer" header for GitHub API requests.
func (t *applicationTokenSource) Token() (*oauth2.Token, error) {
// To protect against clock drift, set the issuance time 60 seconds in the past.
now := time.Now().Add(-60 * time.Second)
expiresAt := now.Add(t.expiration)
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(expiresAt),
Issuer: t.issuer,
})
accessToken, err := token.SignedString(t.privateKey)
if err != nil {
return nil, err
}
return &oauth2.Token{
AccessToken: accessToken,
TokenType: bearerTokenType,
Expiry: expiresAt,
}, nil
}
// InstallationTokenSourceOpt is a functional option for InstallationTokenSource.
type InstallationTokenSourceOpt func(*installationTokenSource)
// WithInstallationTokenOptions sets the options for the GitHub App installation token.
func WithInstallationTokenOptions(opts *InstallationTokenOptions) InstallationTokenSourceOpt {
return func(i *installationTokenSource) {
i.opts = opts
}
}
// WithHTTPClient sets the HTTP client for the GitHub App installation token source.
func WithHTTPClient(client *http.Client) InstallationTokenSourceOpt {
return func(i *installationTokenSource) {
client.Transport = &oauth2.Transport{
Source: i.src,
Base: client.Transport,
}
i.client = newGitHubClient(client)
}
}
// WithEnterpriseURL sets the base URL for GitHub Enterprise Server.
// This option should be used after WithHTTPClient to ensure the HTTP client is properly configured.
// If the provided base URL is invalid, the option is ignored and default GitHub base URL is used.
func WithEnterpriseURL(baseURL string) InstallationTokenSourceOpt {
return func(i *installationTokenSource) {
enterpriseClient, err := i.client.withEnterpriseURL(baseURL)
if err != nil {
return
}
i.client = enterpriseClient
}
}
// WithContext sets the context for the GitHub App installation token source.
func WithContext(ctx context.Context) InstallationTokenSourceOpt {
return func(i *installationTokenSource) {
i.ctx = ctx
}
}
// installationTokenSource represents a GitHub App installation token source
// that generates access tokens for authenticating as a specific GitHub App installation.
//
// See: https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#create-an-installation-access-token-for-an-app
type installationTokenSource struct {
id int64
ctx context.Context
src oauth2.TokenSource
client *githubClient
opts *InstallationTokenOptions
}
// NewInstallationTokenSource creates a GitHub App installation token source.
// Requires installation ID and a GitHub App JWT token source for authentication.
//
// The returned token source is wrapped in oauth2.ReuseTokenSource to prevent unnecessary
// token regeneration. Don't worry about wrapping the result again since ReuseTokenSource
// prevents re-wrapping automatically.
//
// See https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app
func NewInstallationTokenSource(id int64, src oauth2.TokenSource, opts ...InstallationTokenSourceOpt) oauth2.TokenSource {
ctx := context.Background()
httpClient := cleanHTTPClient()
httpClient.Transport = &oauth2.Transport{
Source: oauth2.ReuseTokenSource(nil, src),
Base: httpClient.Transport,
}
i := &installationTokenSource{
id: id,
ctx: ctx,
src: src,
client: newGitHubClient(httpClient),
}
for _, opt := range opts {
opt(i)
}
return oauth2.ReuseTokenSource(nil, i)
}
// Token generates a new GitHub App installation token for authenticating as a GitHub App installation.
func (t *installationTokenSource) Token() (*oauth2.Token, error) {
token, err := t.client.createInstallationToken(t.ctx, t.id, t.opts)
if err != nil {
return nil, err
}
return &oauth2.Token{
AccessToken: token.Token,
TokenType: bearerTokenType,
Expiry: token.ExpiresAt,
}, nil
}
// personalAccessTokenSource represents a static GitHub personal access token source
// that provides OAuth2 authentication using a pre-generated token.
// Personal access tokens can be classic or fine-grained and provide access to repositories
// based on the token's configured permissions and scope.
//
// See: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens
type personalAccessTokenSource struct {
token string
}
// NewPersonalAccessTokenSource creates a token source for GitHub personal access tokens.
// The provided token should be a valid GitHub personal access token (classic or fine-grained).
// This token source returns the same token value for all Token() calls without expiration,
// making it suitable for long-lived authentication scenarios.
func NewPersonalAccessTokenSource(token string) oauth2.TokenSource {
return &personalAccessTokenSource{
token: token,
}
}
// Token returns the configured personal access token as an OAuth2 token.
// The returned token has no expiry time since personal access tokens
// remain valid until manually revoked or expired by GitHub.
func (t *personalAccessTokenSource) Token() (*oauth2.Token, error) {
if t.token == "" {
return nil, errors.New("token not provided")
}
return &oauth2.Token{
AccessToken: t.token,
TokenType: bearerTokenType,
}, nil
}