Skip to content

Commit ec7aa0a

Browse files
authored
Merge pull request #42 from luizfonseca/feat/2factor-whitelist
feat: add support to check for user MFA Status
2 parents 2991278 + 7f2eca0 commit ec7aa0a

6 files changed

Lines changed: 103 additions & 46 deletions

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ providing a more secure way for users to access protected routes.
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' \
5252
--label 'traefik.http.middlewares.whoami-github-oauth.plugin.github-oauth.whitelist.teams[0]=827726' \
53+
--label 'traefik.http.middlewares.whoami-github-oauth.plugin.github-oauth.whitelist.twoFactorAuthRequired=true' \
5354
--label 'traefik.http.routers.whoami.rule=Host(`whoami.example.com`)' \
5455
--label 'traefik.http.routers.whoami.middlewares=whoami-github-oauth' \
5556
traefik/whoami
@@ -85,8 +86,14 @@ jwtSecretKey: optional_secret_key
8586
# The log level, defaults to info
8687
# Available values: debug, info, warn, error
8788
logLevel: info
89+
8890
# whitelist
8991
whitelist:
92+
# When set to `true`, the middleware will check if the given user has 2FA
93+
# configured, otherwise they will be denied access
94+
# Default is `false`
95+
twoFactorAuthRequired: 'true'
96+
9097
# The list of GitHub user ids that are whitelisted to access the resources
9198
ids:
9299
- 996

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

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,22 @@ 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"`
25-
GithubTeamIDs []string `json:"github_team_ids"`
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"`
26+
GithubUserTwoFactorAuth bool `json:"github_user_two_factor_auth"`
2627
}
2728

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

3233
type AuthRequest struct {
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"`
34+
RedirectURI string `json:"redirect_uri"`
35+
AuthURL string `json:"auth_url"`
36+
GitHubUserID string `json:"github_user_id"`
37+
GitHubUserLogin string `json:"github_user_login"`
38+
GithubTeamIDs []string `json:"github_team_ids"`
39+
GithubUserTwoFactorAuth bool `json:"github_user_two_factor_auth"`
3840
}

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ func OauthRedirectHandler(app *server.App) http.HandlerFunc {
109109

110110
authRequest.GitHubUserID = cast.ToString(githubData.User.GetID())
111111
authRequest.GitHubUserLogin = githubData.User.GetLogin()
112+
authRequest.GithubUserTwoFactorAuth = githubData.User.GetTwoFactorAuthentication()
112113

113114
if authRequest.GithubTeamIDs != nil {
114115
var teamIDs []string
@@ -169,10 +170,11 @@ func OauthAuthResultHandler(app *server.App) http.HandlerFunc {
169170
w,
170171
r,
171172
model.ResponseGetAuthResult{
172-
RedirectURI: authRequest.RedirectURI,
173-
GitHubUserID: authRequest.GitHubUserID,
174-
GitHubUserLogin: authRequest.GitHubUserLogin,
175-
GithubTeamIDs: authRequest.GithubTeamIDs,
173+
RedirectURI: authRequest.RedirectURI,
174+
GitHubUserID: authRequest.GitHubUserID,
175+
GitHubUserLogin: authRequest.GitHubUserLogin,
176+
GithubTeamIDs: authRequest.GithubTeamIDs,
177+
GithubUserTwoFactorAuth: authRequest.GithubUserTwoFactorAuth,
176178
},
177179
)
178180
}

internal/pkg/jwt/jwt.go

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,18 @@ import (
77
)
88

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

15-
func GenerateJwtTokenString(id string, login string, teamIds []string, key string) (string, error) {
16+
func GenerateJwtTokenString(id string, login string, teamIds []string, key string, two_factor_enabled bool) (string, error) {
1617
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
17-
"id": id,
18-
"login": login,
19-
"teams": teamIds,
18+
"id": id,
19+
"login": login,
20+
"teams": teamIds,
21+
"two_factor_enabled": two_factor_enabled,
2022
})
2123
return token.SignedString([]byte(key))
2224
}
@@ -49,10 +51,18 @@ func ParseTokenString(tokenString, key string) (*PayloadUser, error) {
4951
}
5052
}
5153

54+
twoFactorEnabled := false
55+
if claims["two_factor_enabled"] != nil {
56+
if factorEnabled, ok := claims["two_factor_enabled"].(bool); ok {
57+
twoFactorEnabled = factorEnabled
58+
}
59+
}
60+
5261
return &PayloadUser{
53-
Id: claims["id"].(string),
54-
Login: claims["login"].(string),
55-
Teams: teams,
62+
Id: claims["id"].(string),
63+
Login: claims["login"].(string),
64+
Teams: teams,
65+
TwoFactorEnabled: twoFactorEnabled,
5666
}, nil
5767
} else {
5868
return nil, fmt.Errorf("invalid token")

internal/pkg/jwt/jwt_test.go

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const (
1616

1717
func TestGenerateJwtTokenString(t *testing.T) {
1818
// execution
19-
tokenString, err := GenerateJwtTokenString(id, login, testTeams, key)
19+
tokenString, err := GenerateJwtTokenString(id, login, testTeams, key, false)
2020

2121
// assertion
2222
assert.NoError(t, err)
@@ -25,7 +25,7 @@ func TestGenerateJwtTokenString(t *testing.T) {
2525

2626
func TestParseTokenString(t *testing.T) {
2727
// setup
28-
tokenString, _ := GenerateJwtTokenString(id, login, testTeams, key)
28+
tokenString, _ := GenerateJwtTokenString(id, login, testTeams, key, false)
2929

3030
// execution
3131
payload, err := ParseTokenString(tokenString, key)
@@ -35,11 +35,12 @@ func TestParseTokenString(t *testing.T) {
3535
assert.Equal(t, id, payload.Id)
3636
assert.Equal(t, login, payload.Login)
3737
assert.Equal(t, testTeams, payload.Teams)
38+
assert.False(t, payload.TwoFactorEnabled)
3839
}
3940

4041
func TestParseTokenString_EmptyTeams(t *testing.T) {
4142
// setup
42-
tokenString, _ := GenerateJwtTokenString(id, login, []string{}, key)
43+
tokenString, _ := GenerateJwtTokenString(id, login, []string{}, key, false)
4344

4445
// execution
4546
payload, err := ParseTokenString(tokenString, key)
@@ -49,11 +50,12 @@ func TestParseTokenString_EmptyTeams(t *testing.T) {
4950
assert.Equal(t, id, payload.Id)
5051
assert.Equal(t, login, payload.Login)
5152
assert.Equal(t, payload.Teams, []string{})
53+
assert.False(t, payload.TwoFactorEnabled)
5254
}
5355

5456
func TestParseTokenString_NoTeams(t *testing.T) {
5557
// setup
56-
tokenString, _ := GenerateJwtTokenString(id, login, nil, key)
58+
tokenString, _ := GenerateJwtTokenString(id, login, nil, key, false)
5759

5860
// execution
5961
payload, err := ParseTokenString(tokenString, key)
@@ -63,6 +65,22 @@ func TestParseTokenString_NoTeams(t *testing.T) {
6365
assert.Equal(t, id, payload.Id)
6466
assert.Equal(t, login, payload.Login)
6567
assert.Equal(t, payload.Teams, []string{})
68+
assert.False(t, payload.TwoFactorEnabled)
69+
}
70+
71+
func TestParseTokenString_With2FAEnabled(t *testing.T) {
72+
// setup
73+
tokenString, _ := GenerateJwtTokenString(id, login, nil, key, true)
74+
75+
// execution
76+
payload, err := ParseTokenString(tokenString, key)
77+
78+
// assertion
79+
assert.NoError(t, err)
80+
assert.Equal(t, id, payload.Id)
81+
assert.Equal(t, login, payload.Login)
82+
assert.Equal(t, payload.Teams, []string{})
83+
assert.True(t, payload.TwoFactorEnabled)
6684
}
6785

6886
func TestParseTokenString_InvalidToken(t *testing.T) {
@@ -79,7 +97,7 @@ func TestParseTokenString_InvalidToken(t *testing.T) {
7997

8098
func TestParseTokenString_InvalidKey(t *testing.T) {
8199
// setup
82-
tokenString, _ := GenerateJwtTokenString(id, login, testTeams, key)
100+
tokenString, _ := GenerateJwtTokenString(id, login, testTeams, key, false)
83101
invalidKey := "invalidkey"
84102

85103
// execution

middleware_plugin.go

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ type Config struct {
3636

3737
// ConfigWhitelist the middleware configuration whitelist.
3838
type ConfigWhitelist struct {
39+
TwoFactorAuthRequired string `json:"two_factor_auth_required,omitempty"`
40+
3941
// Ids the GitHub user id list.
4042
Ids []string `json:"ids,omitempty"`
4143
// Logins the GitHub user login list.
@@ -53,9 +55,10 @@ func CreateConfig() *Config {
5355
AuthPath: DefaultConfigAuthPath,
5456
JwtSecretKey: getRandomString32(),
5557
Whitelist: ConfigWhitelist{
56-
Ids: []string{},
57-
Logins: []string{},
58-
Teams: []string{},
58+
Ids: []string{},
59+
Logins: []string{},
60+
Teams: []string{},
61+
TwoFactorAuthRequired: "false",
5962
},
6063
}
6164
}
@@ -66,13 +69,14 @@ type TraefikGithubOauthMiddleware struct {
6669
next http.Handler
6770
name string
6871

69-
apiBaseUrl string
70-
apiSecretKey string
71-
authPath string
72-
jwtSecretKey string
73-
whitelistIdSet *strset.Set
74-
whitelistLoginSet *strset.Set
75-
whitelistTeamSet *strset.Set
72+
apiBaseUrl string
73+
apiSecretKey string
74+
authPath string
75+
jwtSecretKey string
76+
whitelistIdSet *strset.Set
77+
whitelistLoginSet *strset.Set
78+
whitelistTeamSet *strset.Set
79+
whitelistRequires2FA bool
7680

7781
logger *log.Logger
7882
}
@@ -96,13 +100,14 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
96100
next: next,
97101
name: name,
98102

99-
apiBaseUrl: baseUrl,
100-
apiSecretKey: config.ApiSecretKey,
101-
authPath: authPath,
102-
jwtSecretKey: config.JwtSecretKey,
103-
whitelistIdSet: strset.New(config.Whitelist.Ids...),
104-
whitelistLoginSet: strset.New(config.Whitelist.Logins...),
105-
whitelistTeamSet: strset.New(config.Whitelist.Teams...),
103+
apiBaseUrl: baseUrl,
104+
apiSecretKey: config.ApiSecretKey,
105+
authPath: authPath,
106+
jwtSecretKey: config.JwtSecretKey,
107+
whitelistIdSet: strset.New(config.Whitelist.Ids...),
108+
whitelistLoginSet: strset.New(config.Whitelist.Logins...),
109+
whitelistTeamSet: strset.New(config.Whitelist.Teams...),
110+
whitelistRequires2FA: config.Whitelist.TwoFactorAuthRequired == "true",
106111

107112
logger: logger,
108113
}, nil
@@ -133,6 +138,13 @@ func (middleware *TraefikGithubOauthMiddleware) handleRequest(rw http.ResponseWr
133138
return
134139
}
135140

141+
// Early check for 2FA -- if user is not whitelisted and 2FA is required, return 401
142+
if middleware.whitelistRequires2FA && !user.TwoFactorEnabled {
143+
setNoCacheHeaders(rw)
144+
http.Error(rw, "", http.StatusUnauthorized)
145+
return
146+
}
147+
136148
// If cookie is present, check if user is whitelisted
137149
// If nothing can be found, returns 404 as we don't want to leak information
138150
// But we log the error internally
@@ -160,7 +172,13 @@ func (p TraefikGithubOauthMiddleware) handleAuthRequest(rw http.ResponseWriter,
160172
}
161173

162174
// Generate JWTs
163-
tokenString, err := jwt.GenerateJwtTokenString(result.GitHubUserID, result.GitHubUserLogin, result.GithubTeamIDs, p.jwtSecretKey)
175+
tokenString, err := jwt.GenerateJwtTokenString(
176+
result.GitHubUserID,
177+
result.GitHubUserLogin,
178+
result.GithubTeamIDs,
179+
p.jwtSecretKey,
180+
p.whitelistRequires2FA,
181+
)
164182
if err != nil {
165183
p.logger.Printf("Failed to generate JWT: %s", err.Error())
166184
http.Error(rw, "", http.StatusInternalServerError)

0 commit comments

Comments
 (0)