@@ -20,17 +20,17 @@ import (
20
20
"database/sql"
21
21
"errors"
22
22
"fmt"
23
- "github.com/gofrs/uuid"
24
- "google.golang.org/protobuf/types/known/emptypb"
25
23
"time"
26
24
25
+ "github.com/gofrs/uuid"
27
26
jwt "github.com/golang-jwt/jwt/v4"
28
27
"github.com/heroiclabs/nakama/v3/console"
29
28
"github.com/jackc/pgtype"
30
29
"go.uber.org/zap"
31
30
"golang.org/x/crypto/bcrypt"
32
31
"google.golang.org/grpc/codes"
33
32
"google.golang.org/grpc/status"
33
+ "google.golang.org/protobuf/types/known/emptypb"
34
34
)
35
35
36
36
type ConsoleTokenClaims struct {
@@ -71,6 +71,11 @@ func parseConsoleToken(hmacSecretByte []byte, tokenString string) (id, username,
71
71
}
72
72
73
73
func (s * ConsoleServer ) Authenticate (ctx context.Context , in * console.AuthenticateRequest ) (* console.ConsoleSession , error ) {
74
+ ip , _ := extractClientAddressFromContext (s .logger , ctx )
75
+ if ! s .loginAttemptCache .Allow (in .Username , ip ) {
76
+ return nil , status .Error (codes .ResourceExhausted , "Try again later." )
77
+ }
78
+
74
79
role := console .UserRole_USER_ROLE_UNKNOWN
75
80
var uname string
76
81
var email string
@@ -81,10 +86,20 @@ func (s *ConsoleServer) Authenticate(ctx context.Context, in *console.Authentica
81
86
role = console .UserRole_USER_ROLE_ADMIN
82
87
uname = in .Username
83
88
id = uuid .Nil
89
+ } else {
90
+ if lockout , until := s .loginAttemptCache .Add (s .config .GetConsole ().Username , ip ); lockout != LockoutTypeNone {
91
+ switch lockout {
92
+ case LockoutTypeAccount :
93
+ s .logger .Info (fmt .Sprintf ("Console admin account locked until %v." , until ))
94
+ case LockoutTypeIp :
95
+ s .logger .Info (fmt .Sprintf ("Console admin IP locked until %v." , until ))
96
+ }
97
+ }
98
+ return nil , status .Error (codes .Unauthenticated , "Invalid credentials." )
84
99
}
85
100
default :
86
101
var err error
87
- id , uname , email , role , err = s .lookupConsoleUser (ctx , in .Username , in .Password )
102
+ id , uname , email , role , err = s .lookupConsoleUser (ctx , in .Username , in .Password , ip )
88
103
if err != nil {
89
104
return nil , err
90
105
}
@@ -94,7 +109,10 @@ func (s *ConsoleServer) Authenticate(ctx context.Context, in *console.Authentica
94
109
return nil , status .Error (codes .Unauthenticated , "Invalid credentials." )
95
110
}
96
111
112
+ s .loginAttemptCache .Reset (uname )
113
+
97
114
exp := time .Now ().UTC ().Add (time .Duration (s .config .GetConsole ().TokenExpirySec ) * time .Second ).Unix ()
115
+
98
116
token := jwt .NewWithClaims (jwt .SigningMethodHS256 , & ConsoleTokenClaims {
99
117
ExpiresAt : exp ,
100
118
ID : id .String (),
@@ -132,19 +150,28 @@ func (s *ConsoleServer) AuthenticateLogout(ctx context.Context, in *console.Auth
132
150
return & emptypb.Empty {}, nil
133
151
}
134
152
135
- func (s * ConsoleServer ) lookupConsoleUser (ctx context.Context , unameOrEmail , password string ) (id uuid.UUID , uname string , email string , role console.UserRole , err error ) {
153
+ func (s * ConsoleServer ) lookupConsoleUser (ctx context.Context , unameOrEmail , password , ip string ) (id uuid.UUID , uname string , email string , role console.UserRole , err error ) {
136
154
role = console .UserRole_USER_ROLE_UNKNOWN
137
155
query := "SELECT id, username, email, role, password, disable_time FROM console_user WHERE username = $1 OR email = $1"
138
156
var dbPassword []byte
139
157
var dbDisableTime pgtype.Timestamptz
140
158
err = s .db .QueryRowContext (ctx , query , unameOrEmail ).Scan (& id , & uname , & email , & role , & dbPassword , & dbDisableTime )
141
159
if err != nil {
142
160
if err == sql .ErrNoRows {
143
- err = nil
161
+ if lockout , until := s .loginAttemptCache .Add ("" , ip ); lockout == LockoutTypeIp {
162
+ s .logger .Info (fmt .Sprintf ("Console user IP locked until %v." , until ))
163
+ }
164
+ err = status .Error (codes .Unauthenticated , "Invalid credentials." )
144
165
}
145
166
return
146
167
}
147
168
169
+ // Check lockout again as the login attempt may have been through email.
170
+ if ! s .loginAttemptCache .Allow (uname , ip ) {
171
+ err = status .Error (codes .ResourceExhausted , "Try again later." )
172
+ return
173
+ }
174
+
148
175
// Check if it's disabled.
149
176
if dbDisableTime .Status == pgtype .Present && dbDisableTime .Time .Unix () != 0 {
150
177
s .logger .Info ("Console user account is disabled." , zap .String ("username" , unameOrEmail ))
@@ -155,6 +182,14 @@ func (s *ConsoleServer) lookupConsoleUser(ctx context.Context, unameOrEmail, pas
155
182
// Check password
156
183
err = bcrypt .CompareHashAndPassword (dbPassword , []byte (password ))
157
184
if err != nil {
185
+ if lockout , until := s .loginAttemptCache .Add (uname , ip ); lockout != LockoutTypeNone {
186
+ switch lockout {
187
+ case LockoutTypeAccount :
188
+ s .logger .Info (fmt .Sprintf ("Console user account locked until %v." , until ))
189
+ case LockoutTypeIp :
190
+ s .logger .Info (fmt .Sprintf ("Console user IP locked until %v." , until ))
191
+ }
192
+ }
158
193
err = status .Error (codes .Unauthenticated , "Invalid credentials." )
159
194
return
160
195
}
0 commit comments