@@ -20,17 +20,17 @@ import (
2020 "database/sql"
2121 "errors"
2222 "fmt"
23- "github.com/gofrs/uuid"
24- "google.golang.org/protobuf/types/known/emptypb"
2523 "time"
2624
25+ "github.com/gofrs/uuid"
2726 jwt "github.com/golang-jwt/jwt/v4"
2827 "github.com/heroiclabs/nakama/v3/console"
2928 "github.com/jackc/pgtype"
3029 "go.uber.org/zap"
3130 "golang.org/x/crypto/bcrypt"
3231 "google.golang.org/grpc/codes"
3332 "google.golang.org/grpc/status"
33+ "google.golang.org/protobuf/types/known/emptypb"
3434)
3535
3636type ConsoleTokenClaims struct {
@@ -71,6 +71,11 @@ func parseConsoleToken(hmacSecretByte []byte, tokenString string) (id, username,
7171}
7272
7373func (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+
7479 role := console .UserRole_USER_ROLE_UNKNOWN
7580 var uname string
7681 var email string
@@ -81,10 +86,20 @@ func (s *ConsoleServer) Authenticate(ctx context.Context, in *console.Authentica
8186 role = console .UserRole_USER_ROLE_ADMIN
8287 uname = in .Username
8388 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." )
8499 }
85100 default :
86101 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 )
88103 if err != nil {
89104 return nil , err
90105 }
@@ -94,7 +109,10 @@ func (s *ConsoleServer) Authenticate(ctx context.Context, in *console.Authentica
94109 return nil , status .Error (codes .Unauthenticated , "Invalid credentials." )
95110 }
96111
112+ s .loginAttemptCache .Reset (uname )
113+
97114 exp := time .Now ().UTC ().Add (time .Duration (s .config .GetConsole ().TokenExpirySec ) * time .Second ).Unix ()
115+
98116 token := jwt .NewWithClaims (jwt .SigningMethodHS256 , & ConsoleTokenClaims {
99117 ExpiresAt : exp ,
100118 ID : id .String (),
@@ -132,19 +150,28 @@ func (s *ConsoleServer) AuthenticateLogout(ctx context.Context, in *console.Auth
132150 return & emptypb.Empty {}, nil
133151}
134152
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 ) {
136154 role = console .UserRole_USER_ROLE_UNKNOWN
137155 query := "SELECT id, username, email, role, password, disable_time FROM console_user WHERE username = $1 OR email = $1"
138156 var dbPassword []byte
139157 var dbDisableTime pgtype.Timestamptz
140158 err = s .db .QueryRowContext (ctx , query , unameOrEmail ).Scan (& id , & uname , & email , & role , & dbPassword , & dbDisableTime )
141159 if err != nil {
142160 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." )
144165 }
145166 return
146167 }
147168
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+
148175 // Check if it's disabled.
149176 if dbDisableTime .Status == pgtype .Present && dbDisableTime .Time .Unix () != 0 {
150177 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
155182 // Check password
156183 err = bcrypt .CompareHashAndPassword (dbPassword , []byte (password ))
157184 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+ }
158193 err = status .Error (codes .Unauthenticated , "Invalid credentials." )
159194 return
160195 }
0 commit comments