• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pomerium / pomerium / 16526957574

25 Jul 2025 04:34PM UTC coverage: 52.172% (-6.4%) from 58.559%
16526957574

push

github

web-flow
ci: restore coverage step in test workflow (#5752)

I think the step to upload coverage to coveralls has been getting 
skipped due to a mismatch in platform name.

23627 of 45287 relevant lines covered (52.17%)

75.44 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

53.3
/pkg/identity/oauth/github/github.go
1
// Package github implements OAuth2 based authentication for github
2
//
3
// https://www.pomerium.com/docs/identity-providers/github
4
package github
5

6
import (
7
        "context"
8
        "encoding/json"
9
        "errors"
10
        "fmt"
11
        "net/http"
12
        "net/url"
13
        "strings"
14
        "time"
15

16
        "github.com/go-jose/go-jose/v3/jwt"
17
        "golang.org/x/oauth2"
18

19
        "github.com/pomerium/pomerium/internal/httputil"
20
        "github.com/pomerium/pomerium/internal/jwtutil"
21
        "github.com/pomerium/pomerium/internal/log"
22
        "github.com/pomerium/pomerium/internal/urlutil"
23
        "github.com/pomerium/pomerium/internal/version"
24
        "github.com/pomerium/pomerium/pkg/identity/identity"
25
        "github.com/pomerium/pomerium/pkg/identity/oauth"
26
        "github.com/pomerium/pomerium/pkg/identity/oidc"
27
)
28

29
// Name identifies the GitHub identity provider
30
const Name = "github"
31

32
const (
33
        defaultProviderURL = "https://github.com"
34
        githubAPIURL       = "https://api.github.com"
35
        userPath           = "/user"
36
        revokePath         = "/applications/%s/grant"
37
        emailPath          = "/user/emails"
38
        // https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps
39
        authURL  = "/login/oauth/authorize"
40
        tokenURL = "/login/oauth/access_token" //nolint:gosec
41

42
        // since github doesn't implement oidc, we need this to refresh the user session
43
        refreshDeadline = time.Minute * 60
44
)
45

46
var maxTime = time.Unix(253370793661, 0) // year 9999
47

48
// https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/
49
var defaultScopes = []string{"user:email", "read:org"}
50

51
// Provider is an implementation of the OAuth Provider.
52
type Provider struct {
53
        Oauth *oauth2.Config
54

55
        userEndpoint  string
56
        emailEndpoint string
57
}
58

59
// New instantiates an OAuth2 provider for Github.
60
func New(_ context.Context, o *oauth.Options) (*Provider, error) {
1✔
61
        p := Provider{}
1✔
62
        if o.ProviderURL == "" {
1✔
63
                o.ProviderURL = defaultProviderURL
×
64
        }
×
65

66
        // when the default provider url is used, use the Github API endpoint
67
        if o.ProviderURL == defaultProviderURL {
1✔
68
                p.userEndpoint = urlutil.Join(githubAPIURL, userPath)
×
69
                p.emailEndpoint = urlutil.Join(githubAPIURL, emailPath)
×
70
        } else {
1✔
71
                p.userEndpoint = urlutil.Join(o.ProviderURL, userPath)
1✔
72
                p.emailEndpoint = urlutil.Join(o.ProviderURL, emailPath)
1✔
73
        }
1✔
74

75
        if len(o.Scopes) == 0 {
2✔
76
                o.Scopes = defaultScopes
1✔
77
        }
1✔
78
        p.Oauth = &oauth2.Config{
1✔
79
                ClientID:     o.ClientID,
1✔
80
                ClientSecret: o.ClientSecret,
1✔
81
                Scopes:       o.Scopes,
1✔
82
                RedirectURL:  o.RedirectURL.String(),
1✔
83
                Endpoint: oauth2.Endpoint{
1✔
84
                        AuthURL:  urlutil.Join(o.ProviderURL, authURL),
1✔
85
                        TokenURL: urlutil.Join(o.ProviderURL, tokenURL),
1✔
86
                },
1✔
87
        }
1✔
88
        return &p, nil
1✔
89
}
90

91
// Authenticate creates an identity session with github from a authorization code, and follows up
92
// call to the user and user group endpoint with the
93
func (p *Provider) Authenticate(ctx context.Context, code string, v identity.State) (*oauth2.Token, error) {
×
94
        oauth2Token, err := p.Oauth.Exchange(ctx, code)
×
95
        if err != nil {
×
96
                return nil, fmt.Errorf("github: token exchange failed %w", err)
×
97
        }
×
98

99
        // github tokens never expire
100
        oauth2Token.Expiry = maxTime
×
101

×
102
        err = p.UpdateUserInfo(ctx, oauth2Token, v)
×
103
        if err != nil {
×
104
                return nil, err
×
105
        }
×
106

107
        return oauth2Token, nil
×
108
}
109

110
// UpdateUserInfo will get the user information from github and also retrieve the user's team(s)
111
//
112
// https://developer.github.com/v3/users/#get-the-authenticated-user
113
func (p *Provider) UpdateUserInfo(ctx context.Context, t *oauth2.Token, v any) error {
×
114
        err := p.userInfo(ctx, t, v)
×
115
        if err != nil {
×
116
                return fmt.Errorf("github: could not retrieve user info %w", err)
×
117
        }
×
118

119
        err = p.userEmail(ctx, t, v)
×
120
        if err != nil {
×
121
                return fmt.Errorf("github: could not retrieve user email %w", err)
×
122
        }
×
123

124
        return nil
×
125
}
126

127
// Refresh is a no-op for github, because github sessions never expire.
128
func (p *Provider) Refresh(_ context.Context, t *oauth2.Token, _ identity.State) (*oauth2.Token, error) {
×
129
        t.Expiry = time.Now().Add(refreshDeadline)
×
130
        return t, nil
×
131
}
×
132

133
// userEmail returns the primary email of the user by making
134
// a query to github API.
135
//
136
// https://developer.github.com/v3/users/emails/#list-email-addresses-for-a-user
137
// https://developer.github.com/v3/auth/
138
func (p *Provider) userEmail(ctx context.Context, t *oauth2.Token, v any) error {
1✔
139
        // response represents the github user email
1✔
140
        // https://developer.github.com/v3/users/emails/#response
1✔
141
        var response []struct {
1✔
142
                Email      string `json:"email"`
1✔
143
                Verified   bool   `json:"verified"`
1✔
144
                Primary    bool   `json:"primary"`
1✔
145
                Visibility string `json:"visibility"`
1✔
146
        }
1✔
147
        headers := map[string]string{"Authorization": fmt.Sprintf("token %s", t.AccessToken)}
1✔
148
        err := httputil.Do(ctx, http.MethodGet, p.emailEndpoint, version.UserAgent(), headers, nil, &response)
1✔
149
        if err != nil {
1✔
150
                return err
×
151
        }
×
152
        var out struct {
1✔
153
                Email    string `json:"email"`
1✔
154
                Verified bool   `json:"email_verified"`
1✔
155
        }
1✔
156
        log.Ctx(ctx).Debug().Interface("emails", response).Msg("github: user emails")
1✔
157
        for _, email := range response {
2✔
158
                if email.Primary && email.Verified {
2✔
159
                        out.Email = email.Email
1✔
160
                        out.Verified = true
1✔
161
                        break
1✔
162
                }
163
        }
164
        b, err := json.Marshal(out)
1✔
165
        if err != nil {
1✔
166
                return err
×
167
        }
×
168
        return json.Unmarshal(b, v)
1✔
169
}
170

171
func (p *Provider) userInfo(ctx context.Context, t *oauth2.Token, v any) error {
1✔
172
        var response struct {
1✔
173
                ID        int    `json:"id"`
1✔
174
                Login     string `json:"login"`
1✔
175
                Name      string `json:"name"`
1✔
176
                AvatarURL string `json:"avatar_url,omitempty"`
1✔
177
        }
1✔
178

1✔
179
        headers := map[string]string{
1✔
180
                "Authorization": fmt.Sprintf("token %s", t.AccessToken),
1✔
181
                "Accept":        "application/vnd.github.v3+json",
1✔
182
        }
1✔
183
        err := httputil.Do(ctx, http.MethodGet, p.userEndpoint, version.UserAgent(), headers, nil, &response)
1✔
184
        if err != nil {
1✔
185
                return err
×
186
        }
×
187
        var out struct {
1✔
188
                Subject string `json:"sub"`
1✔
189
                Name    string `json:"name,omitempty"`
1✔
190
                User    string `json:"user"`
1✔
191
                Picture string `json:"picture,omitempty"`
1✔
192
                // needs to be set manually
1✔
193
                Expiry    *jwt.NumericDate `json:"exp,omitempty"`
1✔
194
                NotBefore *jwt.NumericDate `json:"nbf,omitempty"`
1✔
195
                IssuedAt  *jwt.NumericDate `json:"iat,omitempty"`
1✔
196
        }
1✔
197

1✔
198
        out.Expiry = jwt.NewNumericDate(time.Now().Add(refreshDeadline))
1✔
199
        out.NotBefore = jwt.NewNumericDate(time.Now())
1✔
200
        out.IssuedAt = jwt.NewNumericDate(time.Now())
1✔
201

1✔
202
        out.User = response.Login
1✔
203
        out.Subject = response.Login
1✔
204
        out.Name = response.Name
1✔
205
        out.Picture = response.AvatarURL
1✔
206
        b, err := json.Marshal(out)
1✔
207
        if err != nil {
1✔
208
                return err
×
209
        }
×
210
        return json.Unmarshal(b, v)
1✔
211
}
212

213
// Revoke method will remove all the github grants the user
214
// gave pomerium application during authorization.
215
//
216
// https://developer.github.com/v3/apps/oauth_applications/#delete-an-app-authorization
217
func (p *Provider) Revoke(ctx context.Context, token *oauth2.Token) error {
×
218
        // build the basic authentication request
×
219
        basicAuth := url.UserPassword(p.Oauth.ClientID, p.Oauth.ClientSecret)
×
220
        revokeURL := url.URL{
×
221
                Scheme: "https",
×
222
                User:   basicAuth,
×
223
                Host:   "api.github.com",
×
224
                Path:   fmt.Sprintf(revokePath, p.Oauth.ClientID),
×
225
        }
×
226
        reqBody := strings.NewReader(fmt.Sprintf(`{"access_token": "%s"}`, token.AccessToken))
×
227
        req, err := http.NewRequestWithContext(ctx, http.MethodDelete, revokeURL.String(), reqBody)
×
228
        if err != nil {
×
229
                return errors.New("github: could not create revoke request")
×
230
        }
×
231

232
        req.Header.Set("Content-Type", "application/json")
×
233
        client := &http.Client{}
×
234
        resp, err := client.Do(req)
×
235
        if err != nil {
×
236
                return err
×
237
        }
×
238
        defer resp.Body.Close()
×
239

×
240
        return nil
×
241
}
242

243
// Name returns the provider name.
244
func (p *Provider) Name() string {
×
245
        return Name
×
246
}
×
247

248
// SignIn redirects to the OAuth 2.0 provider's consent page
249
// that asks for permissions for the required scopes explicitly.
250
func (p *Provider) SignIn(w http.ResponseWriter, r *http.Request, state string) error {
×
251
        signInURL := p.Oauth.AuthCodeURL(state, oauth2.AccessTypeOffline)
×
252
        httputil.Redirect(w, r, signInURL, http.StatusFound)
×
253
        return nil
×
254
}
×
255

256
// SignOut is not implemented.
257
func (p *Provider) SignOut(_ http.ResponseWriter, _ *http.Request, _, _, _ string) error {
×
258
        return oidc.ErrSignoutNotImplemented
×
259
}
×
260

261
func (p *Provider) DeviceAuth(_ context.Context) (*oauth2.DeviceAuthResponse, error) {
×
262
        return nil, oidc.ErrDeviceAuthNotImplemented
×
263
}
×
264

265
func (p *Provider) DeviceAccessToken(_ context.Context, _ *oauth2.DeviceAuthResponse, _ identity.State) (*oauth2.Token, error) {
×
266
        return nil, oidc.ErrDeviceAuthNotImplemented
×
267
}
×
268

269
// VerifyAccessToken verifies an access token.
270
func (p *Provider) VerifyAccessToken(ctx context.Context, rawAccessToken string) (claims map[string]any, err error) {
1✔
271
        claims = jwtutil.Claims(map[string]any{})
1✔
272

1✔
273
        err = p.userInfo(ctx, &oauth2.Token{
1✔
274
                TokenType:   "Bearer",
1✔
275
                AccessToken: rawAccessToken,
1✔
276
        }, &claims)
1✔
277
        if err != nil {
1✔
278
                return nil, fmt.Errorf("error retrieving user info with access token: %w", err)
×
279
        }
×
280

281
        err = p.userEmail(ctx, &oauth2.Token{
1✔
282
                TokenType:   "Bearer",
1✔
283
                AccessToken: rawAccessToken,
1✔
284
        }, &claims)
1✔
285
        if err != nil {
1✔
286
                return nil, fmt.Errorf("error retrieving user email with access token: %w", err)
×
287
        }
×
288

289
        return claims, nil
1✔
290
}
291

292
// VerifyIdentityToken verifies an identity token.
293
func (p *Provider) VerifyIdentityToken(_ context.Context, _ string) (claims map[string]any, err error) {
×
294
        return nil, identity.ErrVerifyIdentityTokenNotSupported
×
295
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc