-
Notifications
You must be signed in to change notification settings - Fork 0
/
auth.go
2683 lines (2247 loc) · 94.8 KB
/
auth.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package core
import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
"math"
"net"
"net/http"
"reflect"
"sort"
"strconv"
"strings"
"sync"
"github.com/croessner/nauthilus/server/backend"
"github.com/croessner/nauthilus/server/config"
errors2 "github.com/croessner/nauthilus/server/errors"
"github.com/croessner/nauthilus/server/global"
"github.com/croessner/nauthilus/server/localcache"
"github.com/croessner/nauthilus/server/logging"
"github.com/croessner/nauthilus/server/lualib"
"github.com/croessner/nauthilus/server/lualib/action"
"github.com/croessner/nauthilus/server/lualib/filter"
"github.com/croessner/nauthilus/server/rediscli"
"github.com/croessner/nauthilus/server/stats"
"github.com/croessner/nauthilus/server/util"
"github.com/gin-gonic/gin"
"github.com/go-kit/log/level"
"github.com/go-webauthn/webauthn/webauthn"
openapi "github.com/ory/hydra-client-go/v2"
"github.com/prometheus/client_golang/prometheus"
)
// ClaimHandler represents a claim handler struct.
// A claim handler in this context is something to work with JSON Web Tokens (JWT), often used for APIs.
type ClaimHandler struct {
// Type is the reflected Kind of the claim value.
Type reflect.Kind
// ApplyFunc is a function that takes in three parameters: the claim value, the map of claims and the claim key.
// The function is intended to apply some process on the claim using the provided parameters,
// and return a boolean result.
ApplyFunc func(value any, claims map[string]any, claimKey string) bool
}
// BackendServer represents a type for managing a slive of config.BackendServer
type BackendServer struct {
// backendServer is a slice of pointers to config.BackendServer objects
backendServer []*config.BackendServer
// mu provides a read/write mutex for thread-safe operations on the backendServer
mu sync.RWMutex
}
// Update updates the backendServer field of the BackendServer object with the provided servers slice.
func (n *BackendServer) Update(servers []*config.BackendServer) {
n.mu.Lock()
defer n.mu.Unlock()
n.backendServer = servers
}
func (n *BackendServer) GetTotalServers() int {
n.mu.RLock()
defer n.mu.RUnlock()
return len(n.backendServer)
}
// NewBackendServer creates a new instance of the BackendServer struct.
// It returns a pointer to the newly created BackendServer.
func NewBackendServer() *BackendServer {
return &BackendServer{}
}
// JSONRequest is a data structure containing the details of a client's request in JSON format.
type JSONRequest struct {
// Username is the identifier of the client/user sending the request.
Username string `json:"username"`
// Password is the authentication credential of the client/user sending the request.
Password string `json:"password"`
// ClientIP is the IP address of the client/user making the request.
ClientIP string `json:"client_ip"`
// ClientPort is the port number from which the client/user is sending the request.
ClientPort string `json:"client_port"`
// ClientHostname is the hostname of the client which is sending the request.
ClientHostname string `json:"client_hostname"`
// ClientID is the unique identifier of the client/user, usually assigned by the application.
ClientID string `json:"client_id"`
// LocalIP is the IP address of the server or endpoint receiving the request.
LocalIP string `json:"local_ip"`
// LocalPort is the port number of the server or endpoint receiving the request.
LocalPort string `json:"local_port"`
// Service is the specific service that the client/user is trying to access with the request.
Service string `json:"service"`
// Method is the HTTP method used in the request (i.e., PLAIN, LOGIN, etc.)
Method string `json:"method"`
// AuthLoginAttempt is a flag indicating if the request is an attempt to authenticate (login). This is expressed as an unsigned integer where applicable flags/types are usually interpreted from the application's specific logic.
AuthLoginAttempt uint `json:"auth_login_attempt"`
}
// Authentication represents a struct that holds information related to authentication process.
type Authentication struct {
// UsernameReplace is a flag that is set, if a user was found in a Database.
UsernameReplace bool
// NoAuth is a flag that is set, if the request mode does not require authentication.
NoAuth bool
// ListAccounts is a flag that is set, if Nauthilus is requested to send a full list of available user accounts.
ListAccounts bool
// UserFound is a flag that is set, if a password Database found the user.
UserFound bool
// PasswordsAccountSeen is a counter that is increased whenever a new failed password was detected for the current account.
PasswordsAccountSeen uint
// PasswordsTotalSeen is a counter that is increased whenever a new failed password was detected.
PasswordsTotalSeen uint
// LoginAttempts is a counter that is incremented for each failed login request
LoginAttempts uint
// StatusCodeOk is the HTTP status code that is set by setStatusCodes.
StatusCodeOK int
// StatusCodeInternalError is the HTTP status code that is set by setStatusCodes.
StatusCodeInternalError int
// StatusCodeFail is the HTTP status code that is set by setStatusCodes.
StatusCodeFail int
// GUID is a global unique identifier that is inherited in all functions and methods that deal with the
// authentication process. It is needed to track log lines belonging to one request.
GUID *string
// Method is set by the "Auth-Method" HTTP request header (Nginx protocol). It is typically something like "plain"
// or "login".
Method *string
// AccountField is the name of either a SQL field name or an LDAP attribute that was used to retrieve a user account.
AccountField *string
// Username is the value that was taken from the HTTP header "Auth-User" (Nginx protocol).
Username string
// UsernameOrig is a copy from the username that was set by the HTTP request header "Auth-User" (Nginx protocol).
UsernameOrig string
// Password is the value that was taken from the HTTP header "Auth-Pass" (Nginx protocol).
Password string
// ClientIP is the IP of a client that is to be authenticated. The value is set by the HTTP request header
// "Client-IP" (Nginx protocol).
ClientIP string
// XClientPort adds the remote client TCP port, which is set by the HTTP request header "X-Client-Port".
XClientPort string
// ClientHost is the DNS A name of the remote client. It is set with the HTTP request header "Client-Host" (Nginx
// protocol).
ClientHost string
// HAProxy specific headers
XSSL string // %[ssl_fc]
XSSLSessionID string // %[ssl_fc_session_id,hex]
XSSLClientVerify string // %[ssl_c_verify]
XSSLClientDN string // %{+Q}[ssl_c_s_dn]
XSSLClientCN string // %{+Q}[ssl_c_s_dn(cn)]
XSSLIssuer string // %{+Q}[ssl_c_i_dn]
XSSLClientNotBefore string // %{+Q}[ssl_c_notbefore]
XSSLClientNotAfter string // %{+Q}[ssl_c_notafter]
XSSLSubjectDN string // %{+Q}[ssl_c_s_dn]
XSSLIssuerDN string // %{+Q}[ssl_c_i_dn]
XSSLClientSubjectDN string // %{+Q}[ssl_c_s_dn]
XSSLClientIssuerDN string // %{+Q}[ssl_c_i_dn]
XSSLProtocol string // %[ssl_fc_protocol]
XSSLCipher string // %[ssl_fc_cipher]
// XClientID is delivered by some mail user agents when using IMAP. This value is set by the HTTP request header
// "X-Client-Id".
XClientID string
// XLocalIP is the TCP/IP address of the server that asks for authentication. Its value is set by the HTTP request
// header "X-Local-IP".
XLocalIP string
// XPort is the TCP port of the server that asks for authentication. Its value is set by the HTTP request
// header "X-Local-Port".
XPort string
// UserAgent may have been seent by a mail user agent and is set by the HTTP request header "User-Agent".
UserAgent *string
// StatusMessage is the HTTP response payload that is sent to the remote server that asked for authentication.
StatusMessage string
// Service is set by Nauthilus depending on the router endpoint. Look at httpQueryHandler for the structure of available
// endpoints.
Service string
// BruteForceName is the canonical name of a brute force bucket that was triggered by a rule.
BruteForceName string
// FeatureName is the name of a feature that has triggered a reject.
FeatureName string
// TOTPSecret is used to store a TOTP secret in a SQL Database.
TOTPSecret *string
// TOTPSecretField is the SQL field or LDAP attribute that resolves the TOTP secret for two-factor authentication.
TOTPSecretField *string
// TOTPRecoveryField NYI
TOTPRecoveryField *string
// UniqueUserIDField is a string representing a unique user identifier.
UniqueUserIDField *string
// DisplayNameField is the display name of a user
DisplayNameField *string
// AdditionalLogging is a slice of strings that can be filled from Lua features and a Lua backend. Its result will be
// added to the regular log lines.
AdditionalLogs []any
// BruteForceCounter is a map that increments failed login requests. The key is a rule name defined in the
// configuration file.
BruteForceCounter map[string]uint
// SourcePassDBBackend is a marker for the Database that is responsible for a specific user. It is set by the
// password Database and stored in Redis to track the authentication flow accross databases (including proxy).
SourcePassDBBackend global.Backend
// UsedPassDBBackend is set by the password Database that answered the current authentication request.
UsedPassDBBackend global.Backend
// UsedBackendIP is set by a filter Lua script for the Nginx endpoint to set the HTTP response header 'Auth-Server'.
UsedBackendIP string
// UsedBackendPort is set by a filter Lua script for the Nginx endpoint to set the HTTP response header 'Auth-Port'.
UsedBackendPort int
// Attributes is a result container for SQL and LDAP queries. Databases store their result by using a field or
// attribute name as key and the corresponding result as value.
Attributes backend.DatabaseResult
// Protocol is set by the HTTP request header "Auth-Protocol" (Nginx protocol).
Protocol *config.Protocol
// HTTPClientContext tracks the context for an HTTP client connection.
HTTPClientContext *gin.Context
// MonitoringFlags is a slice of global.Monitoring that is used to skip certain steps while processing an authentication request.
MonitoringFlags []global.Monitoring
// MasterUserMode is a flag for a backend to indicate a master user mode is ongoing.
MasterUserMode bool
*backend.PasswordHistory
*lualib.Context
}
// PassDBResult is used in all password databases to store final results of an authentication process.
type PassDBResult struct {
// Authenticated is a flag that is set if a user was not only found, but also succeeded authentication.
Authenticated bool
// UserFound is a flag that is set if the user was found in a password Database.
UserFound bool
// AccountField is the SQL field or LDAP attribute that was used for the user account.
AccountField *string
// TOTPSecretField is set by the Database which has found the user.
TOTPSecretField *string
// TOTPRecoveryField NYI
TOTPRecoveryField *string
// UniqueUserIDField is a string representing a unique user identifier.
UniqueUserIDField *string
// DisplayNameField is the display name of a user
DisplayNameField *string
// Backend is set by the Database backend which has found the user.
Backend global.Backend
// Attributes is the result catalog returned by the underlying password Database.
Attributes backend.DatabaseResult
}
type (
// PassDBOption
// This type specifies the signature of a password database.
PassDBOption func(auth *Authentication) (*PassDBResult, error)
// PassDBMap is a struct type that represents a mapping between a backend type and a PassDBOption function.
// It is used in the verifyPassword method of the Authentication struct to perform password verification against multiple databases.
// The backend field represents the type of database backend (global.Backend) and the fn field represents the PassDBOption function.
// The PassDBOption function takes an Authentication pointer as input and returns a PassDBResult pointer and an error.
// The PassDBResult pointer contains the result of the password verification process.
// This struct is used to store the database mappings in an array and loop through them in the verifyPassword method.
PassDBMap struct {
backend global.Backend
fn PassDBOption
}
)
type (
// AccountList is a slice of strings containing the list of all user accounts.
AccountList []string
// AccountListOption is the function signature for an account Database.
AccountListOption func(a *Authentication) (AccountList, error)
// AccountListMap is a struct type that represents a mapping between a backend and an account list option function for authentication.
AccountListMap struct {
backend global.Backend
fn AccountListOption
}
)
// WebAuthnCredentialDBFunc defines a signature for WebAuthn credential object lookups
type WebAuthnCredentialDBFunc func(uniqueUserID string) ([]webauthn.Credential, error)
// AddTOTPSecretFunc is a function signature that takes a *Authentication and *TOTPSecret as arguments and returns an error.
type AddTOTPSecretFunc func(auth *Authentication, totp *TOTPSecret) (err error)
var BackendServers = NewBackendServer()
// String returns an Authentication object as string excluding the user password.
func (a *Authentication) String() string {
var result string
value := reflect.ValueOf(a)
typeOfValue := value.Type()
for index := range value.NumField() {
switch typeOfValue.Field(index).Name {
case "GUID":
continue
case "Password":
if config.EnvConfig.DevMode {
result += fmt.Sprintf(" %s='%v'", typeOfValue.Field(index).Name, value.Field(index).Interface())
} else {
result += fmt.Sprintf(" %s='<hidden>'", typeOfValue.Field(index).Name)
}
default:
result += fmt.Sprintf(" %s='%v'", typeOfValue.Field(index).Name, value.Field(index).Interface())
}
}
return result[1:]
}
// LogLineMail returns an array of key-value pairs used for logging mail information.
// The array includes the following information:
// - session: the session GUID
// - protocol: the protocol used
// - local_ip: the local IP address
// - port: the port number
// - client_ip: the client IP address
// - client_port: the client port number
// - client_host: the client host
// - tls_protocol: the TLS protocol used
// - tls_cipher: the TLS cipher used
// - auth_method: the authentication method
// - username: the username
// - orig_username: the original username
// - passdb_backend: the used password database backend
// - current_password_retries: the number of current password retries
// - account_passwords_seen: the number of account passwords seen
// - total_passwords_seen: the total number of passwords seen
// - user_agent: the user agent
// - client_id: the client ID
// - brute_force_bucket: the brute force bucket name
// - feature: the feature name
// - status_message: the status message
// - uri_path: the URI path
// - authenticated: the authentication status
func (a *Authentication) LogLineMail(status string, endpoint string) []any {
var keyvals []any
keyvals = []any{
global.LogKeyGUID, util.WithNotAvailable(*a.GUID),
global.LogKeyProtocol, util.WithNotAvailable(a.Protocol.String()),
global.LogKeyLocalIP, util.WithNotAvailable(a.XLocalIP),
global.LogKeyPort, util.WithNotAvailable(a.XPort),
global.LogKeyClientIP, util.WithNotAvailable(a.ClientIP),
global.LogKeyClientPort, util.WithNotAvailable(a.XClientPort),
global.LogKeyClientHost, util.WithNotAvailable(a.ClientHost),
global.LogKeyTLSSecure, util.WithNotAvailable(a.XSSLProtocol),
global.LogKeyTLSCipher, util.WithNotAvailable(a.XSSLCipher),
global.LogKeyAuthMethod, util.WithNotAvailable(a.Method),
global.LogKeyUsername, util.WithNotAvailable(a.Username),
global.LogKeyOrigUsername, util.WithNotAvailable(a.UsernameOrig),
global.LogKeyUsedPassdbBackend, util.WithNotAvailable(a.UsedPassDBBackend.String()),
global.LogKeyLoginAttempts, a.LoginAttempts,
global.LogKeyPasswordsAccountSeen, a.PasswordsAccountSeen,
global.LogKeyPasswordsTotalSeen, a.PasswordsTotalSeen,
global.LogKeyUserAgent, util.WithNotAvailable(a.UserAgent),
global.LogKeyClientID, util.WithNotAvailable(a.XClientID),
global.LogKeyBruteForceName, util.WithNotAvailable(a.BruteForceName),
global.LogKeyFeatureName, util.WithNotAvailable(a.FeatureName),
global.LogKeyStatusMessage, util.WithNotAvailable(a.StatusMessage),
global.LogKeyUriPath, endpoint,
global.LogKeyStatus, util.WithNotAvailable(status),
}
if len(a.AdditionalLogs) > 0 {
if len(a.AdditionalLogs)%2 == 0 {
for index := range a.AdditionalLogs {
keyvals = append(keyvals, a.AdditionalLogs[index])
}
}
}
return keyvals
}
// getAccount returns the account value from the Authentication object. If the account field is not set or the account
// value is not found in the attributes, an empty string is returned
func (a *Authentication) getAccount() string {
if a.AccountField == nil {
return ""
}
if account, okay := a.Attributes[*a.AccountField]; okay {
if value, assertOk := account[global.LDAPSingleValue].(string); assertOk {
return value
}
}
return ""
}
// getAccountOk returns the account name of a user. If there is no account, it returns the empty string "". A boolean
// is set to return a "found" flag.
func (a *Authentication) getAccountOk() (string, bool) {
account := a.getAccount()
return account, account != ""
}
// getTOTPSecret returns the TOTP secret for a user. If there is no secret, it returns the empty string "".
func (a *Authentication) getTOTPSecret() string {
if a.TOTPSecretField == nil {
return ""
}
if totpSecret, okay := a.Attributes[*a.TOTPSecretField]; okay {
if value, assertOk := totpSecret[global.LDAPSingleValue].(string); assertOk {
return value
}
}
return ""
}
// getTOTPSecretOk returns the TOTP secret for a user. If there is no secret, it returns the empty string "". A boolean
// is set to return a "found" flag.
func (a *Authentication) getTOTPSecretOk() (string, bool) {
totpSecret := a.getTOTPSecret()
return totpSecret, totpSecret != ""
}
// getUniqueUserID returns the unique WebAuthn user identifier for a user. If there is no id, it returns the empty string "".
func (a *Authentication) getUniqueUserID() string {
if a.UniqueUserIDField == nil {
return ""
}
if webAuthnUserID, okay := a.Attributes[*a.UniqueUserIDField]; okay {
if value, assertOk := webAuthnUserID[global.LDAPSingleValue].(string); assertOk {
return value
}
}
return ""
}
// GetUniqueUserIDOk returns the unique identifier for a user. If there is no id, it returns the empty string "". A boolean
// is set to return a "found" flag.
func (a *Authentication) GetUniqueUserIDOk() (string, bool) {
uniqueUserID := a.getUniqueUserID()
return uniqueUserID, uniqueUserID != ""
}
// getDisplayName returns the display name for a user. If there is no account, it returns the empty string "".
func (a *Authentication) getDisplayName() string {
if a.DisplayNameField == nil {
return ""
}
if account, okay := a.Attributes[*a.DisplayNameField]; okay {
if value, assertOk := account[global.SliceWithOneElement].(string); assertOk {
return value
}
}
return ""
}
// GetDisplayNameOk returns the display name of a user. If there is no account, it returns the empty string "". A boolean
// is set to return a "found" flag.
func (a *Authentication) GetDisplayNameOk() (string, bool) {
displayName := a.getDisplayName()
return displayName, displayName != ""
}
// authOK is the general method to indicate authentication success.
func (a *Authentication) authOK(ctx *gin.Context) {
setCommonHeaders(ctx, a)
switch a.Service {
case global.ServNginx:
setNginxHeaders(ctx, a)
case global.ServDovecot:
setDovecotHeaders(ctx, a)
case global.ServUserInfo, global.ServJSON:
setUserInfoHeaders(ctx, a)
}
cachedAuth := ctx.GetBool(global.CtxLocalCacheAuthKey)
if cachedAuth {
ctx.Header("X-Auth-Cache", "Hit")
} else {
ctx.Header("X-Auth-Cache", "Miss")
}
handleLogging(ctx, a)
stats.LoginsCounter.WithLabelValues(global.LabelSuccess).Inc()
}
// setCommonHeaders sets common headers for the given gin.Context and Authentication.
// It sets the "Auth-Status" header to "OK" and the "X-Nauthilus-Session" header to the GUID of the Authentication.
// If the Authentication's Service is not global.ServBasicAuth and the UsernameReplace flag is true, it retrieves the account from the Authentication and sets the "Auth-User" header
func setCommonHeaders(ctx *gin.Context, a *Authentication) {
ctx.Header("Auth-Status", "OK")
ctx.Header("X-Nauthilus-Session", *a.GUID)
if a.Service != global.ServBasicAuth && a.UsernameReplace {
if account, found := a.getAccountOk(); found {
ctx.Header("Auth-User", account)
}
}
}
// setNginxHeaders sets the appropriate headers for the given gin.Context and Authentication based on the configuration and feature flags.
// If the global.FeatureBackendServersMonitoring feature is enabled, it checks if the Authentication's UsedBackendAddress and UsedBackendPort are set.
// If they are, it sets the "Auth-Server" header to the UsedBackendAddress and the "Auth-Port" header to the UsedBackendPort.
// If the global.FeatureBackendServersMonitoring feature is disabled, it checks the Authentication's Protocol.
// If the Protocol is global.ProtoSMTP, it sets the "Auth-Server" header to the SMTPBackendAddress and the "Auth-Port" header to the SMTPBackendPort.
// If the Protocol is global.ProtoIMAP, it sets the "Auth-Server" header to the IMAPBackendAddress and the "Auth-Port" header to the IMAPBackendPort.
// If the Protocol is global.ProtoPOP3, it sets the "Auth-Server" header to the POP3BackendAddress and the "Auth-Port" header to the POP3BackendPort.
func setNginxHeaders(ctx *gin.Context, a *Authentication) {
if config.LoadableConfig.HasFeature(global.FeatureBackendServersMonitoring) {
if BackendServers.GetTotalServers() == 0 {
ctx.Header("Auth-Status", "Internal failure")
} else {
if a.UsedBackendIP != "" && a.UsedBackendPort > 0 {
ctx.Header("Auth-Server", a.UsedBackendIP)
ctx.Header("Auth-Port", fmt.Sprintf("%d", a.UsedBackendPort))
}
}
} else {
switch a.Protocol.Get() {
case global.ProtoSMTP:
ctx.Header("Auth-Server", config.EnvConfig.SMTPBackendAddress)
ctx.Header("Auth-Port", fmt.Sprintf("%d", config.EnvConfig.SMTPBackendPort))
case global.ProtoIMAP:
ctx.Header("Auth-Server", config.EnvConfig.IMAPBackendAddress)
ctx.Header("Auth-Port", fmt.Sprintf("%d", config.EnvConfig.IMAPBackendPort))
case global.ProtoPOP3:
ctx.Header("Auth-Server", config.EnvConfig.POP3BackendAddress)
ctx.Header("Auth-Port", fmt.Sprintf("%d", config.EnvConfig.POP3BackendPort))
}
}
}
// setDovecotHeaders sets the specified headers in the given gin.Context based on the attributes in the Authentication object.
// It iterates through the attributes and calls the handleAttributeValue function for each attribute.
//
// Parameters:
// - ctx: The gin.Context object to set the headers on.
// - a: The Authentication object containing the attributes.
//
// Example:
//
// a := &Authentication{
// Attributes: map[string][]any{
// "Attribute1": []any{"Value1"},
// "Attribute2": []any{"Value2_1", "Value2_2"},
// },
// }
// setDovecotHeaders(ctx, a)
//
// Resulting headers in ctx:
// - X-Nauthilus-Attribute1: "Value1"
// - X-Nauthilus-Attribute2: "Value2_1,Value2_2"
func setDovecotHeaders(ctx *gin.Context, a *Authentication) {
if a.Attributes != nil && len(a.Attributes) > 0 {
for name, value := range a.Attributes {
handleAttributeValue(ctx, name, value)
}
}
}
// handleAttributeValue sets the value of a header in the given gin.Context based on the name and value provided.
// If the value length is 1, it formats the value as a string and assigns it to the headerValue variable.
// If the value length is greater than 1, it formats each value and joins them with a comma separator, unless the name is "dn",
// in which case it joins them with a semicolon separator.
// Finally, it adds the header "X-Nauthilus-" + name with the value of headerValue to the gin.Context.
// Parameters:
// - ctx: the gin.Context to set the header in
// - name: the name of the header
// - value: the value of the header
func handleAttributeValue(ctx *gin.Context, name string, value []any) {
var headerValue string
if valueLen := len(value); valueLen > 0 {
switch {
case valueLen == 1:
headerValue = fmt.Sprintf("%v", value[global.LDAPSingleValue])
default:
stringValues := formatValues(value)
separator := ","
if name == global.DistinguishedName {
separator = ";"
}
headerValue = strings.Join(stringValues, separator)
}
ctx.Header("X-Nauthilus-"+name, fmt.Sprintf("%v", headerValue))
}
}
// formatValues takes an array of values and formats them into strings.
// It creates an empty slice of strings called stringValues.
// It then iterates over each value in the values array and appends the formatted string representation of that value to stringValues using fmt.Sprintf("%v", values[index]).
// After iterating over all the values, it returns stringValues.
// Example usage:
// values := []any{"one", "two", "three"}
// result := formatValues(values)
// fmt.Println(result) // Output: ["one", "two", "three"]
func formatValues(values []any) []string {
var stringValues []string
for index := range values {
stringValues = append(stringValues, fmt.Sprintf("%v", values[index]))
}
return stringValues
}
// setUserInfoHeaders sets the necessary headers for the user info response.
// It includes the Content-Type header with the value "application/json; charset=UTF-8".
// It also includes the X-User-Found header with the string representation of a.UserFound.
// Finally, it uses ctx.JSON to send a JSON response with a status code of a.StatusCodeOK and a body of backend.PositivePasswordCache.
func setUserInfoHeaders(ctx *gin.Context, a *Authentication) {
ctx.Header("Content-Type", "application/json; charset=UTF-8")
ctx.Header("X-User-Found", fmt.Sprintf("%v", a.UserFound))
ctx.JSON(a.StatusCodeOK, &backend.PositivePasswordCache{
AccountField: a.AccountField,
TOTPSecretField: a.TOTPSecretField,
Backend: a.SourcePassDBBackend,
Attributes: a.Attributes,
})
}
// handleLogging logs information about the authentication request if the verbosity level is greater than LogLevelWarn.
// It uses the logging.DefaultLogger to log the information.
// The logged information includes the result of the a.LogLineMail() function, which returns either "ok" or an empty string depending on the value of a.NoAuth,
// and the path of the request URL obtained from ctx.Request.URL.Path.
func handleLogging(ctx *gin.Context, a *Authentication) {
level.Info(logging.DefaultLogger).Log(a.LogLineMail(func() string {
if !a.NoAuth {
return "ok"
}
return ""
}(), ctx.Request.URL.Path)...)
}
// increaseLoginAttempts increments the number of login attempts for the Authentication object.
// If the number of login attempts exceeds the maximum value allowed (MaxUint8), it sets it to the maximum value.
// If the Authentication service is equal to ServNginx and the number of login attempts is less than the maximum login attempts specified in the environment configuration,
// it increments the number of login attempts by one.
// The usage example of this method can be found in the authFail function.
func (a *Authentication) increaseLoginAttempts() {
if a.LoginAttempts > math.MaxUint8 {
a.LoginAttempts = math.MaxUint8
}
if a.Service == global.ServNginx {
if a.LoginAttempts < uint(config.EnvConfig.MaxLoginAttempts) {
a.LoginAttempts++
}
}
}
// setFailureHeaders sets the failure headers for the given authentication context.
// It sets the "Auth-Status" header to the value of global.PasswordFail constant.
// It sets the "X-Nauthilus-Session" header to the value of the authentication's GUID field.
// It updates the StatusMessage of the authentication to global.PasswordFail.
//
// If the Service field of the authentication is equal to global.ServUserInfo, it also sets the following headers:
// - "Content-Type" header to "application/json; charset=UTF-8"
// - "X-User-Found" header to the string representation of the UserFound field of the authentication
// - If the PasswordHistory field is not nil, it responds with a JSON representation of the PasswordHistory.
// If the PasswordHistory field is nil, it responds with an empty JSON object.
//
// If the Service field is not equal to global.ServUserInfo, it responds with the StatusMessage of the authentication as plain text.
func (a *Authentication) setFailureHeaders(ctx *gin.Context) {
if a.StatusMessage == "" {
a.StatusMessage = global.PasswordFail
}
ctx.Header("Auth-Status", a.StatusMessage)
ctx.Header("X-Nauthilus-Session", *a.GUID)
if a.Service == global.ServUserInfo {
ctx.Header("Content-Type", "application/json; charset=UTF-8")
ctx.Header("X-User-Found", fmt.Sprintf("%v", a.UserFound))
if a.PasswordHistory != nil {
ctx.JSON(a.StatusCodeFail, *a.PasswordHistory)
} else {
ctx.JSON(a.StatusCodeFail, struct{}{})
}
} else {
ctx.String(a.StatusCodeFail, a.StatusMessage)
}
}
// loginAttemptProcessing performs processing for a failed login attempt.
// It checks the verbosity level in the environment configuration and logs the failed login attempt if it is greater than LogLevelWarn.
// It then increments the LoginsCounter with the LabelFailure.
//
// Example usage:
//
// a := &Authentication{}
// ctx := &gin.Context{}
// a.loginAttemptProcessing(ctx)
func (a *Authentication) loginAttemptProcessing(ctx *gin.Context) {
level.Info(logging.DefaultLogger).Log(a.LogLineMail("fail", ctx.Request.URL.Path)...)
stats.LoginsCounter.WithLabelValues(global.LabelFailure).Inc()
}
// authFail handles the failure of authentication.
// It increases the login attempts, sets failure headers on the context, and performs login attempt processing.
func (a *Authentication) authFail(ctx *gin.Context) {
a.increaseLoginAttempts()
a.setFailureHeaders(ctx)
a.loginAttemptProcessing(ctx)
}
// setSMPTHeaders sets SMTP headers in the specified `gin.Context` if the `Service` is `ServNginx` and the `Protocol` is `ProtoSMTP`.
// It adds the `Auth-Error-Code` header with the value `TempFailCode` from the declaration package.
//
// Example usage:
//
// a.setSMPTHeaders(ctx)
func (a *Authentication) setSMPTHeaders(ctx *gin.Context) {
if a.Service == global.ServNginx && a.Protocol.Get() == global.ProtoSMTP {
ctx.Header("Auth-Error-Code", global.TempFailCode)
}
}
// setUserInfoHeaders sets the necessary headers for UserInfo service in a Gin context
// Usage example:
//
// func (a *Authentication) authTempFail(ctx *gin.Context, reason string) {
// ...
// if a.Service == global.ServUserInfo {
// a.setUserInfoHeaders(ctx, reason)
// return
// }
// ...
// }
//
// params:
// - ctx: Gin context
// - reason: Error reason to include in the response
func (a *Authentication) setUserInfoHeaders(ctx *gin.Context, reason string) {
type errType struct {
Error string
}
ctx.Header("Content-Type", "application/json; charset=UTF-8")
ctx.Header("X-User-Found", fmt.Sprintf("%v", a.UserFound))
ctx.JSON(a.StatusCodeInternalError, &errType{Error: reason})
}
// authTempFail sets the necessary headers and status message for temporary authentication failure.
// If the service is "user", it also sets headers specific to user information.
// After setting the headers, it returns the appropriate response based on the service.
// If the service is not "user", it returns an internal server error response with the status message.
// If the service is "user", it calls the setUserInfoHeaders method to set additional headers and returns.
//
// Parameters:
// - ctx: The gin context object.
// - reason: The reason for the authentication failure.
//
// Usage example:
//
// func (a *Authentication) generic(ctx *gin.Context) {
// ...
// a.authTempFail(ctx, global.TempFailDefault)
// ...
// }
// func (a *Authentication) saslAuthd(ctx *gin.Context) {
// ...
// a.authTempFail(ctx, global.TempFailDefault)
// ...
// }
//
// Declaration and usage of authTempFail:
//
// A: func (a *Authentication) authTempFail(ctx *gin.Context, reason string) {
// ...
// }
func (a *Authentication) authTempFail(ctx *gin.Context, reason string) {
ctx.Header("Auth-Status", reason)
ctx.Header("X-Nauthilus-Session", *a.GUID)
a.setSMPTHeaders(ctx)
a.StatusMessage = reason
if a.Service == global.ServUserInfo {
a.setUserInfoHeaders(ctx, reason)
return
}
ctx.String(a.StatusCodeInternalError, a.StatusMessage)
level.Info(logging.DefaultLogger).Log(a.LogLineMail("tempfail", ctx.Request.URL.Path)...)
}
// isMasterUser checks whether the current user is a master user based on the MasterUser configuration in the LoadableConfig.
// It returns true if MasterUser is enabled and the number of occurrences of the delimiter in the Username is equal to 1, otherwise it returns false.
func (a *Authentication) isMasterUser() bool {
if config.LoadableConfig.Server.MasterUser.Enabled {
if strings.Count(a.Username, config.LoadableConfig.Server.MasterUser.Delimiter) == 1 {
parts := strings.Split(a.Username, config.LoadableConfig.Server.MasterUser.Delimiter)
if len(parts[0]) > 0 && len(parts[1]) > 0 {
return true
}
}
}
return false
}
// isInNetwork checks an IP address against a network and returns true if it matches.
func (a *Authentication) isInNetwork(networkList []string) (matchIP bool) {
ipAddress := net.ParseIP(a.ClientIP)
for _, ipOrNet := range networkList {
if net.ParseIP(ipOrNet) == nil {
_, network, err := net.ParseCIDR(ipOrNet)
if err != nil {
a.logNetworkError(ipOrNet, err)
continue
}
a.checkAndLogNetwork(network)
if network.Contains(ipAddress) {
matchIP = true
break
}
} else {
a.checkAndLogIP(ipOrNet)
if a.ClientIP == ipOrNet {
matchIP = true
break
}
}
}
return
}
// logNetworkError logs a network error message.
//
// Parameters:
// - ipOrNet (string): The IP or network causing the error.
// - err (error): The error information.
//
// Usage example:
// a.logNetworkError(ipOrNet, err)
func (a *Authentication) logNetworkError(ipOrNet string, err error) {
level.Error(logging.DefaultErrLogger).Log(global.LogKeyGUID, a.GUID, global.LogKeyMsg, "%s is not a network", ipOrNet, global.LogKeyError, err)
}
// checkAndLogNetwork logs the information about checking a network for the given authentication object.
func (a *Authentication) checkAndLogNetwork(network *net.IPNet) {
util.DebugModule(
global.DbgWhitelist,
global.LogKeyGUID, a.GUID, global.LogKeyMsg, fmt.Sprintf("Checking: %s -> %s", a.ClientIP, network.String()),
)
}
// checkAndLogIP logs the IP address of the client along with the IP address or network being checked.
func (a *Authentication) checkAndLogIP(ipOrNet string) {
util.DebugModule(global.DbgWhitelist, global.LogKeyGUID, a.GUID, global.LogKeyMsg, fmt.Sprintf("Checking: %s -> %s", a.ClientIP, ipOrNet))
}
// verifyPassword takes in an array of PassDBMap and performs the following steps:
// - Check if there are any password databases available
// - Iterate over each password database and call the corresponding function
// - Log debug information for each database and its result
// - Handle any backend errors and store them in a map
// - If there is no error, authenticate the user using the result returned by the database function
// - If authentication is successful or NoAuth flag is set, return the passDBResult and nil error
//
// Parameters:
// - passDBs: an array of PassDBMap which contains the backend type and the corresponding function to be called
//
// Return values:
// - passDBResult: a pointer to a PassDBResult struct which contains the authentication result
// - err: an error that occurred during the verification process
func (a *Authentication) verifyPassword(passDBs []*PassDBMap) (*PassDBResult, error) {
var (
passDBResult *PassDBResult
err error
)
configErrors := make(map[global.Backend]error, len(passDBs))
for passDBIndex, passDB := range passDBs {
passDBResult, err = passDB.fn(a)
logDebugModule(a, passDB, passDBResult)
if err != nil {
err = handleBackendErrors(passDBIndex, passDBs, passDB, err, a, configErrors)
if err != nil {
break
}
} else {
passDBResult, err = authenticateUser(passDBResult, a, passDB)
if err != nil || a.UserFound {
break
}
}
}
// Enforce authentication
if a.NoAuth {
passDBResult.Authenticated = true
}
return passDBResult, err
}
// logDebugModule logs debug information about the authentication process.
//
// Parameters:
// - a: The Authentication object associated with the authentication process.
// - passDB: The PassDBMap object representing the password database.
// - passDBResult: The PassDBResult object containing the result of the authentication process.
//
// The logDebugModule function calls the util.DebugModule function to log the debug information.
// It passes the module declaration (global.DbgAuth) as the first parameter, followed by key-value pairs of additional information.
// The key-value pairs include "session" as the key and a.GUID as the value, "passdb" as the key and passDB.backend.String() as the value,
// and "result" as the key and fmt.Sprintf("%v", passDBResult) as the value.
//
// Example Usage:
//
// logDebugModule(a, passDB, passDBResult)
//
// This function uses the util.DebugModule function from the package to log the debug information.