-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
handler_sign_duo.go
314 lines (238 loc) · 10.3 KB
/
handler_sign_duo.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
package handlers
import (
"fmt"
"net/url"
"github.com/authelia/authelia/v4/internal/duo"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/utils"
)
// DuoPOST handler for sending a push notification via duo api.
func DuoPOST(duoAPI duo.API) middlewares.RequestHandler {
return func(ctx *middlewares.AutheliaCtx) {
var (
bodyJSON = &bodySignDuoRequest{}
device, method string
userSession session.UserSession
err error
)
if err = ctx.ParseBody(bodyJSON); err != nil {
ctx.Logger.WithError(err).Errorf(logFmtErrParseRequestBody, regulation.AuthTypeDuo)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
if userSession, err = ctx.GetSession(); err != nil {
ctx.Error(fmt.Errorf("error occurred retrieving user session: %w", err), messageMFAValidationFailed)
return
}
remoteIP := ctx.RemoteIP().String()
duoDevice, err := ctx.Providers.StorageProvider.LoadPreferredDuoDevice(ctx, userSession.Username)
if err != nil {
ctx.Logger.Debugf("Error identifying preferred device for user %s: %s", userSession.Username, err)
ctx.Logger.Debugf("Starting Duo PreAuth for initial device selection of user: %s", userSession.Username)
device, method, err = HandleInitialDeviceSelection(ctx, &userSession, duoAPI, bodyJSON)
} else {
ctx.Logger.Debugf("Starting Duo PreAuth to check preferred device of user: %s", userSession.Username)
device, method, err = HandlePreferredDeviceCheck(ctx, &userSession, duoAPI, duoDevice.Device, duoDevice.Method, bodyJSON)
}
if err != nil {
ctx.Error(err, messageMFAValidationFailed)
return
}
if device == "" || method == "" {
return
}
ctx.Logger.Debugf("Starting Duo Auth attempt for %s with device %s and method %s from IP %s", userSession.Username, device, method, remoteIP)
values, err := SetValues(userSession, device, method, remoteIP, bodyJSON.TargetURL, bodyJSON.Passcode)
if err != nil {
ctx.Logger.Errorf("Failed to set values for Duo Auth Call for user '%s': %+v", userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
authResponse, err := duoAPI.AuthCall(ctx, &userSession, values)
if err != nil {
ctx.Logger.Errorf("Failed to perform Duo Auth Call for user '%s': %+v", userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
if authResponse.Result != allow {
_ = markAuthenticationAttempt(ctx, false, nil, userSession.Username, regulation.AuthTypeDuo,
fmt.Errorf("duo auth result: %s, status: %s, message: %s", authResponse.Result, authResponse.Status,
authResponse.StatusMessage))
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
if err = markAuthenticationAttempt(ctx, true, nil, userSession.Username, regulation.AuthTypeDuo, nil); err != nil {
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
HandleAllow(ctx, &userSession, bodyJSON)
}
}
// HandleInitialDeviceSelection handler for retrieving all available devices.
func HandleInitialDeviceSelection(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, duoAPI duo.API, bodyJSON *bodySignDuoRequest) (device string, method string, err error) {
result, message, devices, enrollURL, err := DuoPreAuth(ctx, userSession, duoAPI)
if err != nil {
ctx.Logger.Errorf("Failed to perform Duo PreAuth for user '%s': %+v", userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return "", "", err
}
switch result {
case enroll:
ctx.Logger.Debugf("Duo user: %s not enrolled", userSession.Username)
if err := ctx.SetJSONBody(DuoSignResponse{Result: enroll, EnrollURL: enrollURL}); err != nil {
return "", "", fmt.Errorf("unable to set JSON body in response")
}
return "", "", nil
case deny:
ctx.Logger.Infof("Duo user: %s not allowed to authenticate: %s", userSession.Username, message)
if err := ctx.SetJSONBody(DuoSignResponse{Result: deny}); err != nil {
return "", "", fmt.Errorf("unable to set JSON body in response")
}
return "", "", nil
case allow:
ctx.Logger.Debugf("Duo authentication was bypassed for user: %s", userSession.Username)
HandleAllow(ctx, userSession, bodyJSON)
return "", "", nil
case auth:
device, method, err = HandleAutoSelection(ctx, devices, userSession.Username)
if err != nil {
return "", "", err
}
return device, method, nil
}
return "", "", fmt.Errorf("unknown result: %s", result)
}
// HandlePreferredDeviceCheck handler to check if the saved device and method is still valid.
func HandlePreferredDeviceCheck(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, duoAPI duo.API, device string, method string, bodyJSON *bodySignDuoRequest) (string, string, error) {
result, message, devices, enrollURL, err := DuoPreAuth(ctx, userSession, duoAPI)
if err != nil {
ctx.Logger.Errorf("Failed to perform Duo PreAuth for user '%s': %+v", userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return "", "", nil
}
switch result {
case enroll:
ctx.Logger.Debugf("Duo user: %s no longer enrolled removing preferred device", userSession.Username)
if err := ctx.Providers.StorageProvider.DeletePreferredDuoDevice(ctx, userSession.Username); err != nil {
return "", "", fmt.Errorf("unable to delete preferred Duo device and method for user %s: %w", userSession.Username, err)
}
if err := ctx.SetJSONBody(DuoSignResponse{Result: enroll, EnrollURL: enrollURL}); err != nil {
return "", "", fmt.Errorf("unable to set JSON body in response")
}
return "", "", nil
case deny:
ctx.Logger.Infof("Duo user: %s not allowed to authenticate: %s", userSession.Username, message)
ctx.ReplyUnauthorized()
return "", "", nil
case allow:
ctx.Logger.Debugf("Duo authentication was bypassed for user: %s", userSession.Username)
HandleAllow(ctx, userSession, bodyJSON)
return "", "", nil
case auth:
if devices == nil {
ctx.Logger.Debugf("Duo user: %s has no compatible device/method available removing preferred device", userSession.Username)
if err := ctx.Providers.StorageProvider.DeletePreferredDuoDevice(ctx, userSession.Username); err != nil {
return "", "", fmt.Errorf("unable to delete preferred Duo device and method for user %s: %w", userSession.Username, err)
}
if err := ctx.SetJSONBody(DuoSignResponse{Result: enroll}); err != nil {
return "", "", fmt.Errorf("unable to set JSON body in response")
}
return "", "", nil
}
if len(devices) > 0 {
for i := range devices {
if devices[i].Device == device {
if utils.IsStringInSlice(method, devices[i].Capabilities) {
return device, method, nil
}
}
}
}
return HandleAutoSelection(ctx, devices, userSession.Username)
}
return "", "", fmt.Errorf("unknown result: %s", result)
}
// HandleAutoSelection handler automatically selects preferred device if there is only one suitable option.
func HandleAutoSelection(ctx *middlewares.AutheliaCtx, devices []DuoDevice, username string) (string, string, error) {
if devices == nil {
ctx.Logger.Debugf("No compatible device/method available for Duo user: %s", username)
if err := ctx.SetJSONBody(DuoSignResponse{Result: enroll}); err != nil {
return "", "", fmt.Errorf("unable to set JSON body in response")
}
return "", "", nil
}
if len(devices) > 1 {
ctx.Logger.Debugf("Multiple devices available for Duo user: %s require manual selection", username)
if err := ctx.SetJSONBody(DuoSignResponse{Result: auth, Devices: devices}); err != nil {
return "", "", fmt.Errorf("unable to set JSON body in response")
}
return "", "", nil
}
if len(devices[0].Capabilities) > 1 {
ctx.Logger.Debugf("Multiple methods available for Duo user: %s require manual selection", username)
if err := ctx.SetJSONBody(DuoSignResponse{Result: auth, Devices: devices}); err != nil {
return "", "", fmt.Errorf("unable to set JSON body in response")
}
return "", "", nil
}
device := devices[0].Device
method := devices[0].Capabilities[0]
ctx.Logger.Debugf("Exactly one device: '%s' and method: '%s' found, saving as new preferred Duo device and method for user: %s", device, method, username)
if err := ctx.Providers.StorageProvider.SavePreferredDuoDevice(ctx, model.DuoDevice{Username: username, Method: method, Device: device}); err != nil {
return "", "", fmt.Errorf("unable to save new preferred Duo device and method for user %s: %w", username, err)
}
return device, method, nil
}
// HandleAllow handler for successful logins.
func HandleAllow(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, bodyJSON *bodySignDuoRequest) {
var (
err error
)
if err = ctx.RegenerateSession(); err != nil {
ctx.Logger.WithError(err).Errorf(logFmtErrSessionRegenerate, regulation.AuthTypeDuo, userSession.Username)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
userSession.SetTwoFactorDuo(ctx.Clock.Now())
if err = ctx.SaveSession(*userSession); err != nil {
ctx.Logger.WithError(err).Errorf(logFmtErrSessionSave, "authentication time", regulation.AuthTypeTOTP, logFmtActionAuthentication, userSession.Username)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
if bodyJSON.Workflow == workflowOpenIDConnect {
handleOIDCWorkflowResponse(ctx, userSession, bodyJSON.TargetURL, bodyJSON.WorkflowID)
} else {
Handle2FAResponse(ctx, bodyJSON.TargetURL)
}
}
// SetValues sets all appropriate Values for the Auth Request.
func SetValues(userSession session.UserSession, device string, method string, remoteIP string, targetURL string, passcode string) (url.Values, error) {
values := url.Values{}
values.Set("username", userSession.Username)
values.Set("ipaddr", remoteIP)
values.Set("factor", method)
switch method {
case duo.Push:
values.Set("device", device)
if userSession.DisplayName != "" {
values.Set("display_username", userSession.DisplayName)
}
if targetURL != "" {
values.Set("pushinfo", fmt.Sprintf("target%%20url=%s", targetURL))
}
case duo.Phone:
values.Set("device", device)
case duo.SMS:
values.Set("device", device)
case duo.OTP:
if passcode != "" {
values.Set("passcode", passcode)
} else {
return nil, fmt.Errorf("no passcode received from user: %s", userSession.Username)
}
}
return values, nil
}