Skip to content

Commit

Permalink
Merge pull request #42 from luizfonseca/feat/2factor-whitelist
Browse files Browse the repository at this point in the history
feat: add support to check for user MFA Status
  • Loading branch information
luizfonseca authored Jan 3, 2025
2 parents 2991278 + 7f2eca0 commit ec7aa0a
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 46 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ providing a more secure way for users to access protected routes.
--label 'traefik.http.middlewares.whoami-github-oauth.plugin.github-oauth.apiBaseUrl=http://traefik-github-oauth-server' \
--label 'traefik.http.middlewares.whoami-github-oauth.plugin.github-oauth.whitelist.logins[0]=luizfonseca' \
--label 'traefik.http.middlewares.whoami-github-oauth.plugin.github-oauth.whitelist.teams[0]=827726' \
--label 'traefik.http.middlewares.whoami-github-oauth.plugin.github-oauth.whitelist.twoFactorAuthRequired=true' \
--label 'traefik.http.routers.whoami.rule=Host(`whoami.example.com`)' \
--label 'traefik.http.routers.whoami.middlewares=whoami-github-oauth' \
traefik/whoami
Expand Down Expand Up @@ -85,8 +86,14 @@ jwtSecretKey: optional_secret_key
# The log level, defaults to info
# Available values: debug, info, warn, error
logLevel: info

# whitelist
whitelist:
# When set to `true`, the middleware will check if the given user has 2FA
# configured, otherwise they will be denied access
# Default is `false`
twoFactorAuthRequired: 'true'

# The list of GitHub user ids that are whitelisted to access the resources
ids:
- 996
Expand Down
20 changes: 11 additions & 9 deletions internal/app/traefik-github-oauth-server/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,22 @@ type ResponseGenerateOAuthPageURL struct {
}

type ResponseGetAuthResult struct {
RedirectURI string `json:"redirect_uri"`
GitHubUserID string `json:"github_user_id"`
GitHubUserLogin string `json:"github_user_login"`
GithubTeamIDs []string `json:"github_team_ids"`
RedirectURI string `json:"redirect_uri"`
GitHubUserID string `json:"github_user_id"`
GitHubUserLogin string `json:"github_user_login"`
GithubTeamIDs []string `json:"github_team_ids"`
GithubUserTwoFactorAuth bool `json:"github_user_two_factor_auth"`
}

type ResponseError struct {
Message string `json:"msg"`
}

type AuthRequest struct {
RedirectURI string `json:"redirect_uri"`
AuthURL string `json:"auth_url"`
GitHubUserID string `json:"github_user_id"`
GitHubUserLogin string `json:"github_user_login"`
GithubTeamIDs []string `json:"github_team_ids"`
RedirectURI string `json:"redirect_uri"`
AuthURL string `json:"auth_url"`
GitHubUserID string `json:"github_user_id"`
GitHubUserLogin string `json:"github_user_login"`
GithubTeamIDs []string `json:"github_team_ids"`
GithubUserTwoFactorAuth bool `json:"github_user_two_factor_auth"`
}
10 changes: 6 additions & 4 deletions internal/app/traefik-github-oauth-server/router/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ func OauthRedirectHandler(app *server.App) http.HandlerFunc {

authRequest.GitHubUserID = cast.ToString(githubData.User.GetID())
authRequest.GitHubUserLogin = githubData.User.GetLogin()
authRequest.GithubUserTwoFactorAuth = githubData.User.GetTwoFactorAuthentication()

if authRequest.GithubTeamIDs != nil {
var teamIDs []string
Expand Down Expand Up @@ -169,10 +170,11 @@ func OauthAuthResultHandler(app *server.App) http.HandlerFunc {
w,
r,
model.ResponseGetAuthResult{
RedirectURI: authRequest.RedirectURI,
GitHubUserID: authRequest.GitHubUserID,
GitHubUserLogin: authRequest.GitHubUserLogin,
GithubTeamIDs: authRequest.GithubTeamIDs,
RedirectURI: authRequest.RedirectURI,
GitHubUserID: authRequest.GitHubUserID,
GitHubUserLogin: authRequest.GitHubUserLogin,
GithubTeamIDs: authRequest.GithubTeamIDs,
GithubUserTwoFactorAuth: authRequest.GithubUserTwoFactorAuth,
},
)
}
Expand Down
30 changes: 20 additions & 10 deletions internal/pkg/jwt/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ import (
)

type PayloadUser struct {
Id string `json:"id"`
Login string `json:"login"`
Teams []string `json:"teams"`
Id string `json:"id"`
Login string `json:"login"`
Teams []string `json:"teams"`
TwoFactorEnabled bool `json:"two_factor_enabled"`
}

func GenerateJwtTokenString(id string, login string, teamIds []string, key string) (string, error) {
func GenerateJwtTokenString(id string, login string, teamIds []string, key string, two_factor_enabled bool) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"id": id,
"login": login,
"teams": teamIds,
"id": id,
"login": login,
"teams": teamIds,
"two_factor_enabled": two_factor_enabled,
})
return token.SignedString([]byte(key))
}
Expand Down Expand Up @@ -49,10 +51,18 @@ func ParseTokenString(tokenString, key string) (*PayloadUser, error) {
}
}

twoFactorEnabled := false
if claims["two_factor_enabled"] != nil {
if factorEnabled, ok := claims["two_factor_enabled"].(bool); ok {
twoFactorEnabled = factorEnabled
}
}

return &PayloadUser{
Id: claims["id"].(string),
Login: claims["login"].(string),
Teams: teams,
Id: claims["id"].(string),
Login: claims["login"].(string),
Teams: teams,
TwoFactorEnabled: twoFactorEnabled,
}, nil
} else {
return nil, fmt.Errorf("invalid token")
Expand Down
28 changes: 23 additions & 5 deletions internal/pkg/jwt/jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const (

func TestGenerateJwtTokenString(t *testing.T) {
// execution
tokenString, err := GenerateJwtTokenString(id, login, testTeams, key)
tokenString, err := GenerateJwtTokenString(id, login, testTeams, key, false)

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

func TestParseTokenString(t *testing.T) {
// setup
tokenString, _ := GenerateJwtTokenString(id, login, testTeams, key)
tokenString, _ := GenerateJwtTokenString(id, login, testTeams, key, false)

// execution
payload, err := ParseTokenString(tokenString, key)
Expand All @@ -35,11 +35,12 @@ func TestParseTokenString(t *testing.T) {
assert.Equal(t, id, payload.Id)
assert.Equal(t, login, payload.Login)
assert.Equal(t, testTeams, payload.Teams)
assert.False(t, payload.TwoFactorEnabled)
}

func TestParseTokenString_EmptyTeams(t *testing.T) {
// setup
tokenString, _ := GenerateJwtTokenString(id, login, []string{}, key)
tokenString, _ := GenerateJwtTokenString(id, login, []string{}, key, false)

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

func TestParseTokenString_NoTeams(t *testing.T) {
// setup
tokenString, _ := GenerateJwtTokenString(id, login, nil, key)
tokenString, _ := GenerateJwtTokenString(id, login, nil, key, false)

// execution
payload, err := ParseTokenString(tokenString, key)
Expand All @@ -63,6 +65,22 @@ func TestParseTokenString_NoTeams(t *testing.T) {
assert.Equal(t, id, payload.Id)
assert.Equal(t, login, payload.Login)
assert.Equal(t, payload.Teams, []string{})
assert.False(t, payload.TwoFactorEnabled)
}

func TestParseTokenString_With2FAEnabled(t *testing.T) {
// setup
tokenString, _ := GenerateJwtTokenString(id, login, nil, key, true)

// execution
payload, err := ParseTokenString(tokenString, key)

// assertion
assert.NoError(t, err)
assert.Equal(t, id, payload.Id)
assert.Equal(t, login, payload.Login)
assert.Equal(t, payload.Teams, []string{})
assert.True(t, payload.TwoFactorEnabled)
}

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

func TestParseTokenString_InvalidKey(t *testing.T) {
// setup
tokenString, _ := GenerateJwtTokenString(id, login, testTeams, key)
tokenString, _ := GenerateJwtTokenString(id, login, testTeams, key, false)
invalidKey := "invalidkey"

// execution
Expand Down
54 changes: 36 additions & 18 deletions middleware_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ type Config struct {

// ConfigWhitelist the middleware configuration whitelist.
type ConfigWhitelist struct {
TwoFactorAuthRequired string `json:"two_factor_auth_required,omitempty"`

// Ids the GitHub user id list.
Ids []string `json:"ids,omitempty"`
// Logins the GitHub user login list.
Expand All @@ -53,9 +55,10 @@ func CreateConfig() *Config {
AuthPath: DefaultConfigAuthPath,
JwtSecretKey: getRandomString32(),
Whitelist: ConfigWhitelist{
Ids: []string{},
Logins: []string{},
Teams: []string{},
Ids: []string{},
Logins: []string{},
Teams: []string{},
TwoFactorAuthRequired: "false",
},
}
}
Expand All @@ -66,13 +69,14 @@ type TraefikGithubOauthMiddleware struct {
next http.Handler
name string

apiBaseUrl string
apiSecretKey string
authPath string
jwtSecretKey string
whitelistIdSet *strset.Set
whitelistLoginSet *strset.Set
whitelistTeamSet *strset.Set
apiBaseUrl string
apiSecretKey string
authPath string
jwtSecretKey string
whitelistIdSet *strset.Set
whitelistLoginSet *strset.Set
whitelistTeamSet *strset.Set
whitelistRequires2FA bool

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

apiBaseUrl: baseUrl,
apiSecretKey: config.ApiSecretKey,
authPath: authPath,
jwtSecretKey: config.JwtSecretKey,
whitelistIdSet: strset.New(config.Whitelist.Ids...),
whitelistLoginSet: strset.New(config.Whitelist.Logins...),
whitelistTeamSet: strset.New(config.Whitelist.Teams...),
apiBaseUrl: baseUrl,
apiSecretKey: config.ApiSecretKey,
authPath: authPath,
jwtSecretKey: config.JwtSecretKey,
whitelistIdSet: strset.New(config.Whitelist.Ids...),
whitelistLoginSet: strset.New(config.Whitelist.Logins...),
whitelistTeamSet: strset.New(config.Whitelist.Teams...),
whitelistRequires2FA: config.Whitelist.TwoFactorAuthRequired == "true",

logger: logger,
}, nil
Expand Down Expand Up @@ -133,6 +138,13 @@ func (middleware *TraefikGithubOauthMiddleware) handleRequest(rw http.ResponseWr
return
}

// Early check for 2FA -- if user is not whitelisted and 2FA is required, return 401
if middleware.whitelistRequires2FA && !user.TwoFactorEnabled {
setNoCacheHeaders(rw)
http.Error(rw, "", http.StatusUnauthorized)
return
}

// If cookie is present, check if user is whitelisted
// If nothing can be found, returns 404 as we don't want to leak information
// But we log the error internally
Expand Down Expand Up @@ -160,7 +172,13 @@ func (p TraefikGithubOauthMiddleware) handleAuthRequest(rw http.ResponseWriter,
}

// Generate JWTs
tokenString, err := jwt.GenerateJwtTokenString(result.GitHubUserID, result.GitHubUserLogin, result.GithubTeamIDs, p.jwtSecretKey)
tokenString, err := jwt.GenerateJwtTokenString(
result.GitHubUserID,
result.GitHubUserLogin,
result.GithubTeamIDs,
p.jwtSecretKey,
p.whitelistRequires2FA,
)
if err != nil {
p.logger.Printf("Failed to generate JWT: %s", err.Error())
http.Error(rw, "", http.StatusInternalServerError)
Expand Down

0 comments on commit ec7aa0a

Please sign in to comment.