Skip to content

Commit

Permalink
feat(api): This commit is to implement 2fat validation,
Browse files Browse the repository at this point in the history
we made changes to following system functions:
AuthUser/AuthUserInfo: We implemented the return of MFA status.
AuthGetToken: It adds MFA validation data to the jwt.Token.
AuthRequest: Using the data received from the token,
it adds MFA validation data to the req.Header.
AuthMFA: This is or middleware for MFA based on
the MFA status validation.
And implement five new store func for MFA.
  • Loading branch information
danyecls authored and nia committed Oct 10, 2023
1 parent b24001a commit 3800085
Show file tree
Hide file tree
Showing 21 changed files with 394 additions and 36 deletions.
10 changes: 5 additions & 5 deletions agent/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.20
require (
github.com/Masterminds/semver v1.5.0
github.com/kelseyhightower/envconfig v1.4.0
github.com/shellhub-io/shellhub v0.5.2
github.com/shellhub-io/shellhub v0.8.1
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0
)
Expand Down Expand Up @@ -47,11 +47,11 @@ require (
github.com/pkg/sftp v1.13.5 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.8.4 // indirect
golang.org/x/crypto v0.11.0 // indirect
golang.org/x/crypto v0.13.0 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/net v0.12.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/tools v0.6.0 // indirect
)

Expand Down
18 changes: 9 additions & 9 deletions agent/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
Expand All @@ -118,8 +118,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand All @@ -140,18 +140,18 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
Expand Down
2 changes: 1 addition & 1 deletion api/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ require (
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
golang.org/x/net v0.12.0 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions api/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,8 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down
63 changes: 58 additions & 5 deletions api/routes/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package routes

import (
"errors"
"fmt"
"net/http"
"strconv"

jwt "github.com/golang-jwt/jwt"
"github.com/labstack/echo/v4"
Expand All @@ -24,6 +26,7 @@ const (
AuthUserURLV2 = "/auth/user"
AuthUserTokenURL = "/auth/token/:tenant" //nolint:gosec
AuthPublicKeyURL = "/auth/ssh"
AuthMFAURL = "/auth/mfa"
)

const (
Expand Down Expand Up @@ -90,10 +93,30 @@ func (h *Handler) AuthRequest(c gateway.Context) error {
}
}

setHeader(c, "X-Tenant-ID", claims.Tenant)
setHeader(c, "X-Username", claims.Username)
setHeader(c, "X-ID", claims.ID)
setHeader(c, "X-Role", claims.Role)
MFA, err := h.service.AuthMFA(c.Ctx(), claims.ID)
if err != nil {
return err
}

if MFA != claims.MFA.Status {
if MFA {
if !claims.MFA.Validate {
return svc.NewErrAuthUnathorized(errors.New("necessary make validate MFA"))
}
}
}

fmt.Println("AUTH REQUEST SET HEADER MFA:", claims.MFA.Status)
fmt.Println("AUTH REQUEST SET HEADER validate:", claims.MFA.Validate)
fmt.Println("AUTH REQUEST SET HEADERs:", c.Response().Header())

// Extract datas of user from JWT
c.Response().Header().Set("X-Tenant-ID", claims.Tenant)
c.Response().Header().Set("X-Username", claims.Username)
c.Response().Header().Set("X-ID", claims.ID)
c.Response().Header().Set("X-Role", claims.Role)
c.Response().Header().Set("X-MFA", strconv.FormatBool(claims.MFA.Status))
c.Response().Header().Set("X-Validate-MFA", strconv.FormatBool(claims.MFA.Validate))

return c.NoContent(http.StatusOK)
case AuthRequestDeviceToken:
Expand Down Expand Up @@ -147,6 +170,7 @@ func (h *Handler) AuthUser(c gateway.Context) error {
}

res, err := h.service.AuthUser(c.Ctx(), req)

if err != nil {
if errors.Is(err, svc.ErrUserNotFound) {
return errs.NewErrUnauthorized(err)
Expand All @@ -173,6 +197,7 @@ func (h *Handler) AuthUserInfo(c gateway.Context) error {

func (h *Handler) AuthGetToken(c gateway.Context) error {
var req requests.AuthTokenGet

if err := c.Bind(&req); err != nil {
return err
}
Expand All @@ -198,7 +223,6 @@ func (h *Handler) AuthSwapToken(c gateway.Context) error {
if err := c.Validate(&req); err != nil {
return err
}

var id string
if v := c.ID(); v != nil {
id = v.ID
Expand Down Expand Up @@ -246,3 +270,32 @@ func AuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return jwt(next)(c)
}
}

func AuthMiddlewareMFA(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Verify is the user have a 2fa enable

statusMFA := c.Request().Header.Get("X-MFA")
validateMFA := c.Request().Header.Get("X-Validate-MFA")

if statusMFA != "" {
status, err := strconv.ParseBool(statusMFA)
if err != nil {
return err
}

if status {
validate, err := strconv.ParseBool(validateMFA)
if err != nil {
return err
}

if !validate {
return svc.NewErrMFAUnathorized(nil)
}
}
}

return (next)(c)
}
}
2 changes: 1 addition & 1 deletion api/routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func NewRouter(service services.Service) *echo.Echo {
handler := NewHandler(service)

// Public routes for external access through API gateway
publicAPI := e.Group("/api")
publicAPI := e.Group("/api", gateway.Middleware(AuthMiddlewareMFA))

// Internal routes only accessible by other services in the local container network
internalAPI := e.Group("/internal")
Expand Down
80 changes: 79 additions & 1 deletion api/services/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ type AuthService interface {
AuthPublicKey(ctx context.Context, req requests.PublicKeyAuth) (*models.PublicKeyAuthResponse, error)
AuthSwapToken(ctx context.Context, ID, tenant string) (*models.UserAuthResponse, error)
AuthUserInfo(ctx context.Context, username, tenant, token string) (*models.UserAuthResponse, error)
AuthGetCacheMFA(ctx context.Context, username string) (bool, error)
AuthMFA(ctx context.Context, id string) (bool, error)
PublicKey() *rsa.PublicKey
}

Expand Down Expand Up @@ -138,7 +140,6 @@ func (s *service) AuthDevice(ctx context.Context, req requests.DeviceAuth, remot

func (s *service) AuthUser(ctx context.Context, req requests.UserAuth) (*models.UserAuthResponse, error) {
var user *models.User

userFromUsername, errUsername := s.store.UserGetByUsername(ctx, strings.ToLower(req.Username))
userFromEmail, errEmail := s.store.UserGetByEmail(ctx, strings.ToLower(req.Username))

Expand Down Expand Up @@ -172,6 +173,16 @@ func (s *service) AuthUser(ctx context.Context, req requests.UserAuth) (*models.
}
}

status, err := s.AuthMFA(ctx, user.ID)
if err != nil {
return nil, NewErrUserNotFound(user.ID, err)
}

validate, err := s.AuthGetCacheMFA(ctx, user.Username)
if err != nil {
return nil, NewErrUserNotFound(user.ID, err)
}

password := sha256.Sum256([]byte(req.Password))
if user.Password == hex.EncodeToString(password[:]) {
token := jwt.NewWithClaims(jwt.SigningMethodRS256, models.UserAuthClaims{
Expand All @@ -183,6 +194,10 @@ func (s *service) AuthUser(ctx context.Context, req requests.UserAuth) (*models.
AuthClaims: models.AuthClaims{
Claims: "user",
},
MFA: models.MFA{
Status: status,
Validate: validate,
},
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(clock.Now().Add(time.Hour * 72)),
},
Expand All @@ -209,6 +224,7 @@ func (s *service) AuthUser(ctx context.Context, req requests.UserAuth) (*models.
Tenant: tenant,
Role: role,
Email: user.Email,
MFA: status,
}, nil
}

Expand Down Expand Up @@ -237,6 +253,16 @@ func (s *service) AuthGetToken(ctx context.Context, id string) (*models.UserAuth
}
}

status, err := s.AuthMFA(ctx, user.ID)
if err != nil {
return nil, NewErrUserNotFound(id, err)
}

validate, err := s.AuthGetCacheMFA(ctx, user.Username)
if err != nil {
return nil, NewErrUserNotFound(user.ID, err)
}

token := jwt.NewWithClaims(jwt.SigningMethodRS256, models.UserAuthClaims{
Username: user.Username,
Admin: true,
Expand All @@ -246,6 +272,10 @@ func (s *service) AuthGetToken(ctx context.Context, id string) (*models.UserAuth
AuthClaims: models.AuthClaims{
Claims: "user",
},
MFA: models.MFA{
Status: status,
Validate: validate,
},
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(clock.Now().Add(time.Hour * 72)),
},
Expand All @@ -264,6 +294,7 @@ func (s *service) AuthGetToken(ctx context.Context, id string) (*models.UserAuth
Tenant: tenant,
Role: role,
Email: user.Email,
MFA: status,
}, nil
}

Expand Down Expand Up @@ -314,6 +345,16 @@ func (s *service) AuthSwapToken(ctx context.Context, id, tenant string) (*models
}
}

status, err := s.AuthMFA(ctx, user.ID)
if err != nil {
return nil, NewErrUserNotFound(id, err)
}

validate, err := s.AuthGetCacheMFA(ctx, user.Username)
if err != nil {
return nil, NewErrUserNotFound(user.ID, err)
}

for _, member := range namespace.Members {
if user.ID == member.ID {
token := jwt.NewWithClaims(jwt.SigningMethodRS256, models.UserAuthClaims{
Expand All @@ -325,6 +366,10 @@ func (s *service) AuthSwapToken(ctx context.Context, id, tenant string) (*models
AuthClaims: models.AuthClaims{
Claims: "user",
},
MFA: models.MFA{
Status: status,
Validate: validate,
},
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(clock.Now().Add(time.Hour * 72)),
},
Expand All @@ -345,6 +390,7 @@ func (s *service) AuthSwapToken(ctx context.Context, id, tenant string) (*models
Role: role,
Tenant: namespace.TenantID,
Email: user.Email,
MFA: status,
}, nil
}
}
Expand Down Expand Up @@ -373,6 +419,11 @@ func (s *service) AuthUserInfo(ctx context.Context, username, tenant, token stri

token = strings.Replace(token, "Bearer ", "", 1)

MFA, err := s.AuthMFA(ctx, user.ID)
if err != nil {
return nil, NewErrUserNotFound(user.ID, err)
}

return &models.UserAuthResponse{
Token: token,
Name: user.Name,
Expand All @@ -381,6 +432,7 @@ func (s *service) AuthUserInfo(ctx context.Context, username, tenant, token stri
Role: role,
ID: user.ID,
Email: user.Email,
MFA: MFA,
}, nil
}

Expand Down Expand Up @@ -422,3 +474,29 @@ func (s *service) AuthIsCacheToken(ctx context.Context, tenant, id string) (bool
func (s *service) AuthUncacheToken(ctx context.Context, tenant, id string) error {
return s.cache.Delete(ctx, "token_"+tenant+id)
}

// AuthGetCacheMFA checks if the 'validate_mfa' status is cached;
//
// It receives a context, used to "control" the request flow and the user ID.
//
// AuthGetCacheMFA returns a string to indicate if the validate_mfa is cached and
//
// an error when it could not get the status of validate_mfa.
func (s *service) AuthGetCacheMFA(ctx context.Context, username string) (bool, error) {
var data bool

if err := s.cache.Get(ctx, "validate_mfa_"+username, &data); err != nil {
return false, err
}

return data, nil
}

func (s *service) AuthMFA(ctx context.Context, id string) (bool, error) {
status, err := s.store.GetStatusMFA(ctx, id)
if err != nil {
return false, err
}

return status, nil
}
Loading

0 comments on commit 3800085

Please sign in to comment.