Skip to content

Commit cd5fa0b

Browse files
authored
Merge pull request #21 from luizfonseca/feat/add-support-for-github-teams
feat: add support for Github Teams in the whitelist
2 parents b303683 + 7093787 commit cd5fa0b

10 files changed

Lines changed: 95 additions & 33 deletions

File tree

.golangci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
run:
22
modules-download-mode: vendor
3-
go: '1.21.4'
3+
go: '1.22.1'
44

55
linters:
66
enable:

.traefik.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,5 @@ testData:
1919
- 996
2020
logins:
2121
- luizfonseca
22+
teams:
23+
- 876255

Dockerfile.local

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM golang:1.21.4-alpine3.17 AS builder
1+
FROM golang:1.22.2-alpine3.19 AS builder
22

33
RUN apk add --no-cache ca-certificates && update-ca-certificates
44

@@ -17,4 +17,4 @@ WORKDIR /app
1717

1818
EXPOSE 80
1919

20-
ENTRYPOINT ["/app/server"]
20+
ENTRYPOINT ["/app/server"]

README.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ providing a more secure way for users to access protected routes.
1313
## Quick Start (Docker)
1414

1515
1. Create a GitHub OAuth App
16-
16+
1717
- See: https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app
1818
- Set the Authorization callback URL to `http://<traefik-github-oauth-server-host>/oauth/redirect`
1919

2020
2. Run the Traefik GitHub OAuth server
21-
21+
2222
```sh
2323
docker run -d --name traefik-github-oauth-server \
2424
--network <traefik-proxy-network> \
@@ -31,9 +31,9 @@ providing a more secure way for users to access protected routes.
3131
```
3232

3333
3. Install the Traefik GitHub OAuth plugin
34-
34+
3535
Add this snippet in the Traefik Static configuration
36-
36+
3737
```yaml
3838
experimental:
3939
plugins:
@@ -43,12 +43,13 @@ providing a more secure way for users to access protected routes.
4343
```
4444
4545
4. Run your App
46-
46+
4747
```sh
4848
docker run -d --whoami test \
4949
--network <traefik-proxy-network> \
5050
--label 'traefik.http.middlewares.whoami-github-oauth.plugin.github-oauth.apiBaseUrl=http://traefik-github-oauth-server' \
5151
--label 'traefik.http.middlewares.whoami-github-oauth.plugin.github-oauth.whitelist.logins[0]=luizfonseca' \
52+
--label 'traefik.http.middlewares.whoami-github-oauth.plugin.github-oauth.whitelist.teams[0]=827726' \
5253
--label 'traefik.http.routers.whoami.rule=Host(`whoami.example.com`)' \
5354
--label 'traefik.http.routers.whoami.middlewares=whoami-github-oauth' \
5455
traefik/whoami
@@ -85,12 +86,16 @@ jwtSecretKey: optional_secret_key
8586
logLevel: info
8687
# whitelist
8788
whitelist:
88-
# The list of GitHub user ids that in the whitelist
89+
# The list of GitHub user ids that are whitelisted to access the resources
8990
ids:
9091
- 996
91-
# The list of GitHub user logins that in the whitelist
92+
# The list of GitHub user logins that are whitelisted to access the resources
9293
logins:
9394
- luizfonseca
95+
96+
# The list of Github Teams that are whitelisted to access the resources
97+
teams:
98+
- 988772
9499
```
95100
96101
## License

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/luizfonseca/traefik-github-oauth-plugin
22

3-
go 1.21.4
3+
go 1.22.1
44

55
require (
66
github.com/go-chi/render v1.0.3

internal/app/traefik-github-oauth-server/model/model.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,20 @@ type ResponseGenerateOAuthPageURL struct {
1919
}
2020

2121
type ResponseGetAuthResult struct {
22-
RedirectURI string `json:"redirect_uri"`
23-
GitHubUserID string `json:"github_user_id"`
24-
GitHubUserLogin string `json:"github_user_login"`
22+
RedirectURI string `json:"redirect_uri"`
23+
GitHubUserID string `json:"github_user_id"`
24+
GitHubUserLogin string `json:"github_user_login"`
25+
GithubTeamIDs []string `json:"github_team_ids"`
2526
}
2627

2728
type ResponseError struct {
2829
Message string `json:"msg"`
2930
}
3031

3132
type AuthRequest struct {
32-
RedirectURI string `json:"redirect_uri"`
33-
AuthURL string `json:"auth_url"`
34-
GitHubUserID string `json:"github_user_id"`
35-
GitHubUserLogin string `json:"github_user_login"`
33+
RedirectURI string `json:"redirect_uri"`
34+
AuthURL string `json:"auth_url"`
35+
GitHubUserID string `json:"github_user_id"`
36+
GitHubUserLogin string `json:"github_user_login"`
37+
GithubTeamIDs []string `json:"github_team_ids"`
3638
}

internal/app/traefik-github-oauth-server/router/oauth.go

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ func OauthRedirectHandler(app *server.App) http.HandlerFunc {
9292
return
9393
}
9494

95-
user, err := oAuthCodeToUser(r.Context(), app.GitHubOAuthConfig, query.Code)
95+
githubData, err := oAuthCodeToUser(r.Context(), app.GitHubOAuthConfig, query.Code)
9696
if err != nil {
9797
app.Logger.Error().
9898
Caller().
@@ -107,8 +107,17 @@ func OauthRedirectHandler(app *server.App) http.HandlerFunc {
107107
return
108108
}
109109

110-
authRequest.GitHubUserID = cast.ToString(user.GetID())
111-
authRequest.GitHubUserLogin = user.GetLogin()
110+
authRequest.GitHubUserID = cast.ToString(githubData.User.GetID())
111+
authRequest.GitHubUserLogin = githubData.User.GetLogin()
112+
113+
if authRequest.GithubTeamIDs != nil {
114+
var teamIDs []string
115+
for _, team := range githubData.Teams {
116+
teamIDs = append(teamIDs, cast.ToString(team.GetID()))
117+
}
118+
119+
authRequest.GithubTeamIDs = teamIDs
120+
}
112121

113122
authURL, err := url.Parse(authRequest.AuthURL)
114123
if err != nil {
@@ -163,12 +172,18 @@ func OauthAuthResultHandler(app *server.App) http.HandlerFunc {
163172
RedirectURI: authRequest.RedirectURI,
164173
GitHubUserID: authRequest.GitHubUserID,
165174
GitHubUserLogin: authRequest.GitHubUserLogin,
175+
GithubTeamIDs: authRequest.GithubTeamIDs,
166176
},
167177
)
168178
}
169179
}
170180

171-
func oAuthCodeToUser(ctx context.Context, oAuthConfig *oauth2.Config, code string) (*github.User, error) {
181+
type oauthCodeToUserResponse struct {
182+
User *github.User
183+
Teams []*github.Team
184+
}
185+
186+
func oAuthCodeToUser(ctx context.Context, oAuthConfig *oauth2.Config, code string) (*oauthCodeToUserResponse, error) {
172187
ctxExchange, cancelExchange := context.WithCancel(ctx)
173188
defer cancelExchange()
174189
token, err := oAuthConfig.Exchange(ctxExchange, code)
@@ -181,14 +196,29 @@ func oAuthCodeToUser(ctx context.Context, oAuthConfig *oauth2.Config, code strin
181196
gitHubApiHttpClient := oAuthConfig.Client(ctxClient, token)
182197
gitHubApiClient := github.NewClient(gitHubApiHttpClient)
183198

199+
// Get user information, login and ID
184200
ctxGetUser, cancelGetUser := context.WithCancel(ctx)
185201
defer cancelGetUser()
186-
187202
user, _, err := gitHubApiClient.Users.Get(ctxGetUser, "")
188203
if err != nil {
189204
return nil, err
190205
}
191-
return user, nil
206+
207+
// Optionally, check if the user is a member of any teams and retrieve them
208+
// This won't cancel the main request
209+
ctxTeams, cancelListTeams := context.WithCancel(ctx)
210+
teams, _, err := gitHubApiClient.Teams.ListUserTeams(ctxTeams, &github.ListOptions{PerPage: 100})
211+
defer cancelListTeams()
212+
if err != nil {
213+
// If the user is not a member of any teams, the API will return a 404
214+
// We can ignore this error since this is not a mandatory request
215+
teams = nil
216+
}
217+
218+
return &oauthCodeToUserResponse{
219+
User: user,
220+
Teams: teams,
221+
}, nil
192222
}
193223

194224
func buildRedirectURI(apiBaseUrl, rid string) (string, error) {

internal/pkg/jwt/jwt.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@ import (
77
)
88

99
type PayloadUser struct {
10-
Id string `json:"id"`
11-
Login string `json:"login"`
10+
Id string `json:"id"`
11+
Login string `json:"login"`
12+
Teams []string `json:"teams"`
1213
}
1314

14-
func GenerateJwtTokenString(id, login, key string) (string, error) {
15+
func GenerateJwtTokenString(id string, login string, teamIds []string, key string) (string, error) {
1516
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
1617
"id": id,
1718
"login": login,
19+
"teams": teamIds,
1820
})
1921
return token.SignedString([]byte(key))
2022
}
@@ -30,9 +32,17 @@ func ParseTokenString(tokenString, key string) (*PayloadUser, error) {
3032
return nil, err
3133
}
3234
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
35+
teamFromClaims := claims["teams"].([]interface{})
36+
teams := make([]string, len(teamFromClaims))
37+
38+
for i, v := range teamFromClaims {
39+
teams[i] = v.(string)
40+
}
41+
3342
return &PayloadUser{
3443
Id: claims["id"].(string),
3544
Login: claims["login"].(string),
45+
Teams: teams,
3646
}, nil
3747
} else {
3848
return nil, fmt.Errorf("invalid token")

internal/pkg/jwt/jwt_test.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"github.com/stretchr/testify/assert"
77
)
88

9+
var testTeams = []string{"team1", "team2"}
10+
911
const (
1012
id = "12345"
1113
login = "testuser"
@@ -14,7 +16,7 @@ const (
1416

1517
func TestGenerateJwtTokenString(t *testing.T) {
1618
// execution
17-
tokenString, err := GenerateJwtTokenString(id, login, key)
19+
tokenString, err := GenerateJwtTokenString(id, login, testTeams, key)
1820

1921
// assertion
2022
assert.NoError(t, err)
@@ -23,7 +25,7 @@ func TestGenerateJwtTokenString(t *testing.T) {
2325

2426
func TestParseTokenString(t *testing.T) {
2527
// setup
26-
tokenString, _ := GenerateJwtTokenString(id, login, key)
28+
tokenString, _ := GenerateJwtTokenString(id, login, testTeams, key)
2729

2830
// execution
2931
payload, err := ParseTokenString(tokenString, key)
@@ -32,6 +34,7 @@ func TestParseTokenString(t *testing.T) {
3234
assert.NoError(t, err)
3335
assert.Equal(t, id, payload.Id)
3436
assert.Equal(t, login, payload.Login)
37+
assert.Equal(t, testTeams, payload.Teams)
3538
}
3639

3740
func TestParseTokenString_InvalidToken(t *testing.T) {
@@ -48,7 +51,7 @@ func TestParseTokenString_InvalidToken(t *testing.T) {
4851

4952
func TestParseTokenString_InvalidKey(t *testing.T) {
5053
// setup
51-
tokenString, _ := GenerateJwtTokenString(id, login, key)
54+
tokenString, _ := GenerateJwtTokenString(id, login, testTeams, key)
5255
invalidKey := "invalidkey"
5356

5457
// execution

middleware_plugin.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ type ConfigWhitelist struct {
4040
Ids []string `json:"ids,omitempty"`
4141
// Logins the GitHub user login list.
4242
Logins []string `json:"logins,omitempty"`
43+
44+
// Team IDs that the user must be a member of
45+
Teams []string `json:"teams,omitempty"`
4346
}
4447

4548
// CreateConfig creates the default middleware configuration.
@@ -52,6 +55,7 @@ func CreateConfig() *Config {
5255
Whitelist: ConfigWhitelist{
5356
Ids: []string{},
5457
Logins: []string{},
58+
Teams: []string{},
5559
},
5660
}
5761
}
@@ -68,6 +72,7 @@ type TraefikGithubOauthMiddleware struct {
6872
jwtSecretKey string
6973
whitelistIdSet *strset.Set
7074
whitelistLoginSet *strset.Set
75+
whitelistTeamSet *strset.Set
7176

7277
logger *log.Logger
7378
}
@@ -97,6 +102,7 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
97102
jwtSecretKey: config.JwtSecretKey,
98103
whitelistIdSet: strset.New(config.Whitelist.Ids...),
99104
whitelistLoginSet: strset.New(config.Whitelist.Logins...),
105+
whitelistTeamSet: strset.New(config.Whitelist.Teams...),
100106

101107
logger: logger,
102108
}, nil
@@ -110,7 +116,7 @@ func (tg *TraefikGithubOauthMiddleware) ServeHTTP(rw http.ResponseWriter, req *h
110116
return
111117
}
112118

113-
// Otherwise, handle it as oauth-start request
119+
// Otherwise, handle it as a request that has already been handled through oauth
114120
tg.handleRequest(rw, req)
115121
}
116122

@@ -128,7 +134,11 @@ func (middleware *TraefikGithubOauthMiddleware) handleRequest(rw http.ResponseWr
128134
}
129135

130136
// If cookie is present, check if user is whitelisted
131-
if !middleware.whitelistIdSet.Has(user.Id) && !middleware.whitelistLoginSet.Has(user.Login) {
137+
// If nothing can be found, returns 404 as we don't want to leak information
138+
// But we log the error internally
139+
// We are also checking for the user's teams IDs
140+
if !middleware.whitelistIdSet.Has(user.Id) &&
141+
!middleware.whitelistLoginSet.Has(user.Login) && !middleware.whitelistTeamSet.HasAny(user.Teams...) {
132142
setNoCacheHeaders(rw)
133143
http.Error(rw, "", http.StatusNotFound)
134144
return
@@ -150,7 +160,7 @@ func (p TraefikGithubOauthMiddleware) handleAuthRequest(rw http.ResponseWriter,
150160
}
151161

152162
// Generate JWTs
153-
tokenString, err := jwt.GenerateJwtTokenString(result.GitHubUserID, result.GitHubUserLogin, p.jwtSecretKey)
163+
tokenString, err := jwt.GenerateJwtTokenString(result.GitHubUserID, result.GitHubUserLogin, result.GithubTeamIDs, p.jwtSecretKey)
154164
if err != nil {
155165
p.logger.Printf("Failed to generate JWT: %s", err.Error())
156166
http.Error(rw, "", http.StatusInternalServerError)

0 commit comments

Comments
 (0)