1
1
package ldapauth
2
2
3
3
import (
4
- "fmt"
5
- "time"
4
+ "errors"
5
+ "fmt"
6
+ "log"
7
+ "time"
6
8
7
- "github.com/go-ldap/ldap/v3"
8
- "github.com/golang-jwt/jwt/v4"
9
+ "github.com/go-ldap/ldap/v3"
10
+ "github.com/golang-jwt/jwt/v4"
11
+ )
12
+
13
+ var (
14
+ ErrUserNotFound = errors .New ("user not found or multiple entries returned" )
15
+
16
+ ErrTokenSigningEmptyKey = errors .New ("token signing error: no secret configured" )
17
+
18
+ ErrUnexpectedSigningMethod = errors .New ("unexpected signing method" )
19
+
20
+ ErrInvalidToken = errors .New ("invalid token" )
21
+
22
+ ErrInvalidClaims = errors .New ("invalid claims" )
23
+
24
+ ErrMissingSubClaim = errors .New ("missing sub claim" )
9
25
)
10
26
11
27
// Conn abstracts ldap.Conn methods for easier testing.
28
+
12
29
type Conn interface {
13
- Bind (username , password string ) error
14
- Search (searchReq * ldap.SearchRequest ) (* ldap.SearchResult , error )
15
- Close () error
30
+ Bind (username , password string ) error
31
+
32
+ Search (searchReq * ldap.SearchRequest ) (* ldap.SearchResult , error )
33
+
34
+ Close () error
16
35
}
17
36
18
37
// dialerFunc connects to LDAP and returns a Conn.
38
+
19
39
type dialerFunc func (addr string , opts ... ldap.DialOpt ) (Conn , error )
20
40
21
41
// defaultDialer uses ldap.DialURL under the hood.
42
+
22
43
func defaultDialer (addr string , opts ... ldap.DialOpt ) (Conn , error ) {
23
- return ldap .DialURL (addr , opts ... )
44
+
45
+ return ldap .DialURL (addr , opts ... )
46
+
24
47
}
25
48
26
49
// Config holds LDAP and JWT settings.
50
+
27
51
type Config struct {
28
- Addr string // LDAP server address (e.g., "localhost:389" or "ldap.example.com:389")
29
- BaseDN string // Base distinguished name (DN) used to search for users (e.g., "dc=example,dc=com")
30
- BindUserDN string // Optional: DN of a service account to perform search operations.
31
- // Required if anonymous search is disabled on the LDAP server.
32
-
33
- BindPassword string // Optional: Password for the service account specified in BindUserDN.
34
- // Required only if BindUserDN is set.
35
- JWTSecret string // Secret key used to sign JWT tokens issued after successful authentication.
52
+ Addr string // LDAP server address (e.g., "localhost:389" or "ldap.example.com:389")
53
+
54
+ BaseDN string // Base distinguished name (DN) used to search for users (e.g., "dc=example,dc=com")
55
+
56
+ BindUserDN string // Optional: DN of a service account to perform search operations.
57
+
58
+ // Required if anonymous search is disabled on the LDAP server.
59
+
60
+ BindPassword string // Optional: Password for the service account specified in BindUserDN.
61
+
62
+ // Required only if BindUserDN is set.
63
+
64
+ JWTSecret string // Secret key used to sign JWT tokens issued after successful authentication.
65
+
36
66
}
37
67
38
68
// Authenticator handles LDAP authentication and JWT issuance.
69
+
39
70
type Authenticator struct {
40
- cfg Config
41
- dialFn dialerFunc
71
+ cfg Config
72
+
73
+ dialFn dialerFunc
42
74
}
43
75
44
76
// New returns an Authenticator with the default dialer.
77
+
45
78
func New (cfg Config ) * Authenticator {
46
- return & Authenticator {
47
- cfg : cfg ,
48
- dialFn : defaultDialer ,
49
- }
79
+
80
+ return & Authenticator {
81
+
82
+ cfg : cfg ,
83
+
84
+ dialFn : defaultDialer ,
85
+ }
86
+
50
87
}
51
88
52
89
// WithDialer allows injecting a custom dialer (for tests).
90
+
53
91
func (a * Authenticator ) WithDialer (d dialerFunc ) {
54
- a .dialFn = d
92
+
93
+ a .dialFn = d
94
+
55
95
}
56
96
57
97
// Authenticate binds to LDAP, validates credentials, and returns a signed JWT.
98
+
58
99
func (a * Authenticator ) Authenticate (username , password string ) (string , error ) {
59
- conn , err := a .dialFn ("ldap://" + a .cfg .Addr )
60
- if err != nil {
61
- return "" , fmt .Errorf ("failed to connect LDAP: %w" , err )
62
- }
63
- defer conn .Close ()
64
100
65
- if a .cfg .BindUserDN != "" {
66
- if err := conn .Bind (a .cfg .BindUserDN , a .cfg .BindPassword ); err != nil {
67
- return "" , fmt .Errorf ("service bind failed: %w" , err )
101
+ conn , err := a .dialFn ("ldap://" + a .cfg .Addr )
102
+
103
+ if err != nil {
104
+
105
+ return "" , fmt .Errorf ("failed to connect LDAP: %w" , err )
106
+
107
+ }
108
+
109
+ defer conn .Close ()
110
+
111
+ if a .cfg .BindUserDN != "" {
112
+
113
+ bindErr := conn .Bind (a .cfg .BindUserDN , a .cfg .BindPassword )
114
+ if bindErr != nil {
115
+ return "" , fmt .Errorf ("service bind failed: %w" , bindErr )
68
116
}
69
- }
70
117
71
- userDN , err := a .lookupUserDN (conn , username )
72
- if err != nil {
73
- return "" , err
74
- }
75
118
76
- if err := a .bindUser (conn , userDN , password ); err != nil {
77
- return "" , err
78
- }
119
+ }
120
+
121
+ userDN , err := a .lookupUserDN (conn , username )
122
+
123
+ if err != nil {
124
+
125
+ return "" , err
79
126
80
- token , err := a .generateToken (username )
81
- if err != nil {
82
- return "" , err
127
+ }
128
+
129
+ authErr := a .bindUser (conn , userDN , password )
130
+ if authErr != nil {
131
+ return "" , authErr
83
132
}
84
133
85
- return token , nil
86
- }
87
134
135
+ token , err := a .generateToken (username )
136
+
137
+ if err != nil {
138
+
139
+ return "" , err
140
+
141
+ }
142
+
143
+ return token , nil
144
+
145
+ }
88
146
89
147
func (a * Authenticator ) lookupUserDN (conn Conn , username string ) (string , error ) {
90
- searchReq := ldap .NewSearchRequest (
91
- a .cfg .BaseDN ,
92
- ldap .ScopeWholeSubtree , ldap .NeverDerefAliases , 0 , 0 , false ,
93
- fmt .Sprintf ("(uid=%s)" , ldap .EscapeFilter (username )),
94
- []string {"dn" },
95
- nil ,
96
- )
97
-
98
- result , err := conn .Search (searchReq )
99
- if err != nil {
100
- return "" , fmt .Errorf ("search error: %w" , err )
101
- }
102
148
103
- if len (result .Entries ) != 1 {
104
- return "" , fmt .Errorf ("user not found or multiple entries returned" )
105
- }
149
+ searchReq := ldap .NewSearchRequest (
150
+
151
+ a .cfg .BaseDN ,
152
+
153
+ ldap .ScopeWholeSubtree , ldap .NeverDerefAliases , 0 , 0 , false ,
154
+
155
+ fmt .Sprintf ("(uid=%s)" , ldap .EscapeFilter (username )),
156
+
157
+ []string {"dn" },
158
+
159
+ nil ,
160
+ )
161
+
162
+ result , err := conn .Search (searchReq )
163
+
164
+ if err != nil {
165
+
166
+ return "" , fmt .Errorf ("search error: %w" , err )
167
+
168
+ }
169
+
170
+ if len (result .Entries ) != 1 {
171
+
172
+ return "" , ErrUserNotFound
173
+
174
+ }
175
+
176
+ return result .Entries [0 ].DN , nil
106
177
107
- return result .Entries [0 ].DN , nil
108
178
}
109
179
110
180
func (a * Authenticator ) bindUser (conn Conn , userDN , password string ) error {
111
- if err := conn .Bind (userDN , password ); err != nil {
112
- return fmt .Errorf ("invalid credentials: %w" , err )
113
- }
114
- return nil
181
+
182
+ if err := conn .Bind (userDN , password ); err != nil {
183
+
184
+ return fmt .Errorf ("invalid credentials: %w" , err )
185
+
186
+ }
187
+
188
+ return nil
189
+
115
190
}
116
191
117
192
func (a * Authenticator ) generateToken (username string ) (string , error ) {
118
- if a .cfg .JWTSecret == "" {
119
- return "" , fmt .Errorf ("token signing error: no secret configured" )
120
- }
121
193
122
- token := jwt .NewWithClaims (jwt .SigningMethodHS256 , jwt.MapClaims {
123
- "sub" : username ,
124
- "exp" : time .Now ().Add (1 * time .Hour ).Unix (),
125
- })
194
+ if a .cfg .JWTSecret == "" {
126
195
127
- signed , err := token .SignedString ([]byte (a .cfg .JWTSecret ))
128
- if err != nil {
129
- return "" , fmt .Errorf ("token signing error: %w" , err )
130
- }
196
+ return "" , ErrTokenSigningEmptyKey
131
197
132
- return signed , nil
133
- }
198
+ }
199
+
200
+ token := jwt .NewWithClaims (jwt .SigningMethodHS256 , jwt.MapClaims {
201
+
202
+ "sub" : username ,
203
+
204
+ "exp" : time .Now ().Add (1 * time .Hour ).Unix (),
205
+ })
206
+
207
+ signed , err := token .SignedString ([]byte (a .cfg .JWTSecret ))
208
+
209
+ if err != nil {
210
+
211
+ log .Printf ("token signing error: %v" , err )
134
212
213
+ return "" , ErrTokenSigningEmptyKey
214
+
215
+ }
216
+
217
+ return signed , nil
218
+
219
+ }
135
220
136
221
// ValidateToken parses and validates a JWT, returning the 'sub' claim.
222
+
137
223
func (a * Authenticator ) ValidateToken (tokenStr string ) (string , error ) {
138
- token , err := jwt .Parse (tokenStr , func (token * jwt.Token ) (interface {}, error ) {
139
- if _ , ok := token .Method .(* jwt.SigningMethodHMAC ); ! ok {
140
- return nil , fmt .Errorf ("unexpected signing method: %v" , token .Header ["alg" ])
141
- }
142
- return []byte (a .cfg .JWTSecret ), nil
143
- })
144
- if err != nil {
145
- return "" , fmt .Errorf ("token parse error: %w" , err )
146
- }
147
- if ! token .Valid {
148
- return "" , fmt .Errorf ("invalid token" )
149
- }
150
- claims , ok := token .Claims .(jwt.MapClaims )
151
- if ! ok {
152
- return "" , fmt .Errorf ("invalid claims" )
153
- }
154
- sub , ok := claims ["sub" ].(string )
155
- if ! ok {
156
- return "" , fmt .Errorf ("missing sub claim" )
224
+
225
+ token , err := jwt .Parse (tokenStr , func (token * jwt.Token ) (any , error ) {
226
+ if _ , ok := token .Method .(* jwt.SigningMethodHMAC ); ! ok {
227
+ log .Printf ("unexpected signing method: %v" , token .Header ["alg" ])
228
+ return nil , ErrUnexpectedSigningMethod
157
229
}
158
- return sub , nil
159
- }
230
+ return []byte (a .cfg .JWTSecret ), nil
231
+ })
232
+
233
+
234
+ if err != nil {
235
+
236
+ return "" , fmt .Errorf ("token parse error: %w" , err )
237
+
238
+ }
239
+
240
+ if ! token .Valid {
241
+
242
+ return "" , ErrInvalidToken
243
+
244
+ }
245
+
246
+ claims , ok := token .Claims .(jwt.MapClaims )
247
+
248
+ if ! ok {
249
+
250
+ return "" , ErrInvalidClaims
251
+
252
+ }
253
+
254
+ sub , ok := claims ["sub" ].(string )
255
+
256
+ if ! ok {
257
+
258
+ return "" , ErrMissingSubClaim
259
+
260
+ }
261
+
262
+ return sub , nil
263
+
264
+ }
0 commit comments