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

pomerium / pomerium / 23869499111

01 Apr 2026 08:33PM UTC coverage: 45.453% (+0.1%) from 45.312%
23869499111

push

github

web-flow
core/config: add json docs (#6211)

## Summary
Add json docs generated from the protobuf comments. These can be used
for documentation, tooltips, code generation, etc...
 
## Related issues


## User Explanation

<!-- How would you explain this change to the user? If this
change doesn't create any user-facing changes, you can leave
this blank. If filled out, add the `docs` label -->

## Checklist

- [ ] reference any related issues
- [ ] updated unit tests
- [ ] add appropriate label (`enhancement`, `bug`, `breaking`,
`dependencies`, `ci`)
- [x] ready for review

---------

Co-authored-by: bobby <1544881+desimone@users.noreply.github.com>

35078 of 77175 relevant lines covered (45.45%)

114.83 hits per line

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

52.43
/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
        "github.com/pomerium/pomerium/pkg/identity/pkce"
28
)
29

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

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

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

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

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

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

56
        userEndpoint  string
57
        emailEndpoint string
58
}
59

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

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

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

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

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

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

108
        return oauth2Token, nil
×
109
}
110

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

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

125
        return nil
×
126
}
127

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

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

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

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

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

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

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

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

×
241
        return nil
×
242
}
243

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

249
// SignIn redirects to the OAuth 2.0 provider's consent page
250
// that asks for permissions for the required scopes explicitly.
251
func (p *Provider) SignIn(w http.ResponseWriter, r *http.Request, state string) error {
×
252
        opts := []oauth2.AuthCodeOption{oauth2.AccessTypeOffline}
×
253
        if pkceParams, ok := pkce.FromContext(r.Context()); ok {
×
254
                opts = append(opts, pkce.AuthCodeOptions(pkceParams)...)
×
255
        }
×
256
        signInURL := p.Oauth.AuthCodeURL(state, opts...)
×
257
        httputil.Redirect(w, r, signInURL, http.StatusFound)
×
258
        return nil
×
259
}
260

261
// SignOut is not implemented.
262
func (p *Provider) SignOut(_ http.ResponseWriter, _ *http.Request, _, _, _ string) error {
×
263
        return oidc.ErrSignoutNotImplemented
×
264
}
×
265

266
func (p *Provider) DeviceAuth(_ context.Context) (*oauth2.DeviceAuthResponse, error) {
×
267
        return nil, oidc.ErrDeviceAuthNotImplemented
×
268
}
×
269

270
func (p *Provider) DeviceAccessToken(_ context.Context, _ *oauth2.DeviceAuthResponse, _ identity.State) (*oauth2.Token, error) {
×
271
        return nil, oidc.ErrDeviceAuthNotImplemented
×
272
}
×
273

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

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

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

294
        return claims, nil
1✔
295
}
296

297
// VerifyIdentityToken verifies an identity token.
298
func (p *Provider) VerifyIdentityToken(_ context.Context, _ string) (claims map[string]any, err error) {
×
299
        return nil, identity.ErrVerifyIdentityTokenNotSupported
×
300
}
×
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