@@ -3,10 +3,18 @@ package services
33import (
44 "context"
55 "crypto/rsa"
6+ "slices"
7+ "strings"
68 "time"
79
10+ "github.com/shellhub-io/shellhub/pkg/api/authorizer"
11+ "github.com/shellhub-io/shellhub/pkg/api/jwttoken"
812 "github.com/shellhub-io/shellhub/pkg/api/requests"
13+ "github.com/shellhub-io/shellhub/pkg/clock"
14+ "github.com/shellhub-io/shellhub/pkg/hash"
915 "github.com/shellhub-io/shellhub/pkg/models"
16+ "github.com/shellhub-io/shellhub/pkg/uuid"
17+ log "github.com/sirupsen/logrus"
1018)
1119
1220type AuthService interface {
@@ -54,7 +62,137 @@ func (s *service) AuthDevice(ctx context.Context, req requests.DeviceAuth, remot
5462}
5563
5664func (s * service ) AuthLocalUser (ctx context.Context , req * requests.AuthLocalUser , sourceIP string ) (* models.UserAuthResponse , int64 , string , error ) {
57- return nil , 0 , "" , nil
65+ // if s, err := s.store.SystemGet(ctx); err != nil || !s.Authentication.Local.Enabled {
66+ // return nil, 0, "", NewErrAuthMethodNotAllowed(models.UserAuthMethodLocal.String())
67+ // }
68+
69+ var err error
70+ var user * models.User
71+
72+ if req .Identifier .IsEmail () {
73+ user , err = s .store .UserGetByEmail (ctx , strings .ToLower (string (req .Identifier )))
74+ } else {
75+ user , err = s .store .UserGetByUsername (ctx , strings .ToLower (string (req .Identifier )))
76+ }
77+
78+ if err != nil || ! slices .Contains (user .Preferences .AuthMethods , models .UserAuthMethodLocal ) {
79+ return nil , 0 , "" , NewErrAuthUnathorized (nil )
80+ }
81+
82+ switch user .Status {
83+ case models .UserStatusInvited :
84+ return nil , 0 , "" , NewErrAuthUnathorized (nil )
85+ case models .UserStatusNotConfirmed :
86+ return nil , 0 , "" , NewErrUserNotConfirmed (nil )
87+ default :
88+ break
89+ }
90+
91+ // Checks whether the user is currently blocked from new login attempts
92+ if lockout , attempt , _ := s .cache .HasAccountLockout (ctx , sourceIP , user .ID ); lockout > 0 {
93+ log .WithFields (log.Fields {
94+ "lockout" : lockout ,
95+ "attempt" : attempt ,
96+ "source_ip" : sourceIP ,
97+ "user_id" : user .ID ,
98+ }).
99+ Warn ("attempt to login blocked" )
100+
101+ return nil , lockout , "" , NewErrAuthUnathorized (nil )
102+ }
103+
104+ if ! hash .CompareWith (req .Password , user .Password ) {
105+ lockout , _ , err := s .cache .StoreLoginAttempt (ctx , sourceIP , user .ID )
106+ if err != nil {
107+ log .WithError (err ).
108+ WithField ("source_ip" , sourceIP ).
109+ WithField ("user_id" , user .ID ).
110+ Warn ("unable to store login attempt" )
111+ }
112+
113+ return nil , lockout , "" , NewErrAuthUnathorized (nil )
114+ }
115+
116+ // Reset the attempt and timeout values when succeeds
117+ if err := s .cache .ResetLoginAttempts (ctx , sourceIP , user .ID ); err != nil {
118+ log .WithError (err ).
119+ WithField ("source_ip" , sourceIP ).
120+ WithField ("user_id" , user .ID ).
121+ Warn ("unable to reset authentication attempts" )
122+ }
123+
124+ // Users with MFA enabled must authenticate to the cloud instead of community.
125+ if user .MFA .Enabled {
126+ mfaToken := uuid .Generate ()
127+ if err := s .cache .Set (ctx , "mfa-token={" + mfaToken + "}" , user .ID , 30 * time .Minute ); err != nil {
128+ log .WithError (err ).
129+ WithField ("source_ip" , sourceIP ).
130+ WithField ("user_id" , user .ID ).
131+ Warn ("unable to store mfa-token" )
132+ }
133+
134+ return nil , 0 , mfaToken , nil
135+ }
136+
137+ tenantID := ""
138+ role := ""
139+ // Populate the tenant and role when the user is associated with a namespace. If the member status is pending, we
140+ // ignore the namespace.
141+ if ns , _ := s .store .NamespaceGetPreferred (ctx , user .ID ); ns != nil && ns .TenantID != "" {
142+ if m , _ := ns .FindMember (user .ID ); m .Status != models .MemberStatusPending {
143+ tenantID = ns .TenantID
144+ role = m .Role .String ()
145+ }
146+ }
147+
148+ claims := authorizer.UserClaims {
149+ ID : user .ID ,
150+ Origin : user .Origin .String (),
151+ TenantID : tenantID ,
152+ Username : user .Username ,
153+ MFA : user .MFA .Enabled ,
154+ }
155+
156+ token , err := jwttoken .EncodeUserClaims (claims , s .privKey )
157+ if err != nil {
158+ return nil , 0 , "" , NewErrTokenSigned (err )
159+ }
160+
161+ // Updates last_login and the hash algorithm to bcrypt if still using SHA256
162+ changes := & models.UserChanges {LastLogin : clock .Now (), PreferredNamespace : & tenantID }
163+ if ! strings .HasPrefix (user .Password , "$" ) {
164+ if neo , _ := models .HashUserPassword (req .Password ); neo .Hash != "" {
165+ changes .Password = neo .Hash
166+ }
167+ }
168+
169+ // TODO: evaluate make this update in a go routine.
170+ if err := s .store .UserUpdate (ctx , user .ID , changes ); err != nil {
171+ return nil , 0 , "" , NewErrUserUpdate (user , err )
172+ }
173+
174+ if err := s .AuthCacheToken (ctx , tenantID , user .ID , token ); err != nil {
175+ log .WithError (err ).
176+ WithFields (log.Fields {"id" : user .ID }).
177+ Warn ("unable to cache the authentication token" )
178+ }
179+
180+ res := & models.UserAuthResponse {
181+ ID : user .ID ,
182+ Origin : user .Origin .String (),
183+ AuthMethods : user .Preferences .AuthMethods ,
184+ User : user .Username ,
185+ Name : user .Name ,
186+ Email : user .Email ,
187+ RecoveryEmail : user .RecoveryEmail ,
188+ MFA : user .MFA .Enabled ,
189+ Tenant : tenantID ,
190+ Role : role ,
191+ Token : token ,
192+ MaxNamespaces : user .MaxNamespaces ,
193+ }
194+
195+ return res , 0 , "" , nil
58196}
59197
60198func (s * service ) CreateUserToken (ctx context.Context , req * requests.CreateUserToken ) (* models.UserAuthResponse , error ) {
0 commit comments