1
|
|
|
package org.apereo.cas.adaptors.duo.authn; |
2
|
|
|
|
3
|
|
|
import com.duosecurity.client.Http; |
4
|
|
|
import com.fasterxml.jackson.databind.JsonNode; |
5
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper; |
6
|
|
|
import org.apache.commons.lang3.builder.EqualsBuilder; |
7
|
|
|
import org.apache.commons.lang3.builder.HashCodeBuilder; |
8
|
|
|
import org.apereo.cas.adaptors.duo.DuoUserAccount; |
9
|
|
|
import org.apereo.cas.adaptors.duo.DuoUserAccountAuthStatus; |
10
|
|
|
import org.apereo.cas.configuration.model.support.mfa.DuoSecurityMultifactorProperties; |
11
|
|
|
import org.apereo.cas.util.http.HttpClient; |
12
|
|
|
import org.apereo.cas.util.http.HttpMessage; |
13
|
|
|
import org.slf4j.Logger; |
14
|
|
|
import org.slf4j.LoggerFactory; |
15
|
|
|
import org.springframework.http.HttpMethod; |
16
|
|
|
|
17
|
|
|
import java.net.URL; |
18
|
|
|
import java.net.URLDecoder; |
19
|
|
|
import java.nio.charset.StandardCharsets; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* This is {@link BaseDuoSecurityAuthenticationService}. |
23
|
|
|
* |
24
|
|
|
* @author Misagh Moayyed |
25
|
|
|
* @since 5.1.0 |
26
|
|
|
*/ |
27
|
|
|
public abstract class BaseDuoSecurityAuthenticationService implements DuoSecurityAuthenticationService { |
28
|
|
|
private static final long serialVersionUID = -8044100706027708789L; |
29
|
|
|
|
30
|
|
|
private static final int AUTH_API_VERSION = 2; |
31
|
|
|
private static final String RESULT_KEY_RESPONSE = "response"; |
32
|
|
|
private static final String RESULT_KEY_STAT = "stat"; |
33
|
|
|
private static final String RESULT_KEY_RESULT = "result"; |
34
|
|
|
private static final String RESULT_KEY_ENROLL_PORTAL_URL = "enroll_portal_url"; |
35
|
|
|
private static final String RESULT_KEY_STATUS_MESSAGE = "status_msg"; |
36
|
|
|
|
37
|
|
|
private static final ObjectMapper MAPPER = new ObjectMapper().findAndRegisterModules(); |
38
|
|
|
|
39
|
|
|
private static final Logger LOGGER = LoggerFactory.getLogger(BaseDuoSecurityAuthenticationService.class); |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* Duo Properties. |
43
|
|
|
*/ |
44
|
|
|
protected final DuoSecurityMultifactorProperties duoProperties; |
45
|
|
|
|
46
|
|
|
private final transient HttpClient httpClient; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* Creates the duo authentication service. |
50
|
|
|
* |
51
|
|
|
* @param duoProperties the duo properties |
52
|
|
|
* @param httpClient the http client |
53
|
|
|
*/ |
54
|
|
|
public BaseDuoSecurityAuthenticationService(final DuoSecurityMultifactorProperties duoProperties, final HttpClient httpClient) { |
55
|
|
|
this.duoProperties = duoProperties; |
56
|
|
|
this.httpClient = httpClient; |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
@Override |
60
|
|
|
public boolean ping() { |
61
|
|
|
try { |
62
|
|
|
final String url = buildUrlHttpScheme(getApiHost().concat("/rest/v1/ping")); |
63
|
|
|
LOGGER.debug("Contacting Duo @ [{}]", url); |
64
|
|
|
|
65
|
|
|
final HttpMessage msg = this.httpClient.sendMessageToEndPoint(new URL(url)); |
66
|
|
|
if (msg != null) { |
67
|
|
|
final String response = URLDecoder.decode(msg.getMessage(), StandardCharsets.UTF_8.name()); |
68
|
|
|
LOGGER.debug("Received Duo ping response [{}]", response); |
69
|
|
|
|
70
|
|
|
final JsonNode result = MAPPER.readTree(response); |
71
|
|
|
if (result.has(RESULT_KEY_RESPONSE) && result.has(RESULT_KEY_STAT) |
72
|
|
|
&& result.get(RESULT_KEY_RESPONSE).asText().equalsIgnoreCase("pong") |
73
|
|
|
&& result.get(RESULT_KEY_STAT).asText().equalsIgnoreCase("OK")) { |
74
|
|
|
return true; |
75
|
|
|
} |
76
|
|
|
LOGGER.warn("Could not reach/ping Duo. Response returned is [{}]", result); |
77
|
|
|
} |
78
|
|
|
} catch (final Exception e) { |
79
|
|
|
LOGGER.warn("Pinging Duo has failed with error: [{}]", e.getMessage(), e); |
80
|
|
|
} |
81
|
|
|
return false; |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
@Override |
85
|
|
|
public String getApiHost() { |
86
|
|
|
return duoProperties.getDuoApiHost(); |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
@Override |
90
|
|
|
public boolean equals(final Object obj) { |
91
|
|
|
if (obj == null) { |
92
|
|
|
return false; |
93
|
|
|
} |
94
|
|
|
if (obj == this) { |
95
|
|
|
return true; |
96
|
|
|
} |
97
|
|
|
if (obj.getClass() != getClass()) { |
98
|
|
|
return false; |
99
|
|
|
} |
100
|
|
|
final BaseDuoSecurityAuthenticationService rhs = (BaseDuoSecurityAuthenticationService) obj; |
101
|
|
|
return new EqualsBuilder() |
102
|
|
|
.append(this.duoProperties.getDuoApiHost(), rhs.duoProperties.getDuoApiHost()) |
103
|
|
|
.append(this.duoProperties.getDuoApplicationKey(), rhs.duoProperties.getDuoApplicationKey()) |
104
|
|
|
.append(this.duoProperties.getDuoIntegrationKey(), rhs.duoProperties.getDuoIntegrationKey()) |
105
|
|
|
.append(this.duoProperties.getDuoSecretKey(), rhs.duoProperties.getDuoSecretKey()) |
106
|
|
|
.isEquals(); |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
@Override |
110
|
|
|
public int hashCode() { |
111
|
|
|
return new HashCodeBuilder() |
112
|
|
|
.append(this.duoProperties.getDuoApiHost()) |
113
|
|
|
.append(this.duoProperties.getDuoApplicationKey()) |
114
|
|
|
.append(this.duoProperties.getDuoIntegrationKey()) |
115
|
|
|
.append(this.duoProperties.getDuoSecretKey()) |
116
|
|
|
.toHashCode(); |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
@Override |
120
|
|
|
public DuoUserAccount getDuoUserAccount(final String username) { |
121
|
|
|
final DuoUserAccount account = new DuoUserAccount(username); |
122
|
|
|
account.setStatus(DuoUserAccountAuthStatus.AUTH); |
123
|
|
|
|
124
|
|
|
try { |
125
|
|
|
final Http userRequest = buildHttpPostUserPreAuthRequest(username); |
126
|
|
|
signHttpUserPreAuthRequest(userRequest); |
127
|
|
|
LOGGER.debug("Contacting Duo to inquire about username [{}]", username); |
128
|
|
|
final String userResponse = userRequest.executeHttpRequest().body().string(); |
129
|
|
|
final String jsonResponse = URLDecoder.decode(userResponse, StandardCharsets.UTF_8.name()); |
130
|
|
|
LOGGER.debug("Received Duo admin response [{}]", jsonResponse); |
131
|
|
|
|
132
|
|
|
final JsonNode result = MAPPER.readTree(jsonResponse); |
133
|
|
|
if (result.has(RESULT_KEY_RESPONSE) && result.has(RESULT_KEY_STAT) |
134
|
|
|
&& result.get(RESULT_KEY_STAT).asText().equalsIgnoreCase("OK")) { |
135
|
|
|
|
136
|
|
|
final JsonNode response = result.get(RESULT_KEY_RESPONSE); |
137
|
|
|
final String authResult = response.get(RESULT_KEY_RESULT).asText().toUpperCase(); |
138
|
|
|
|
139
|
|
|
final DuoUserAccountAuthStatus status = DuoUserAccountAuthStatus.valueOf(authResult); |
140
|
|
|
account.setStatus(status); |
141
|
|
|
account.setMessage(response.get(RESULT_KEY_STATUS_MESSAGE).asText()); |
142
|
|
|
if (status == DuoUserAccountAuthStatus.ENROLL) { |
143
|
|
|
final String enrollUrl = response.get(RESULT_KEY_ENROLL_PORTAL_URL).asText(); |
144
|
|
|
account.setEnrollPortalUrl(enrollUrl); |
145
|
|
|
} |
146
|
|
|
} |
147
|
|
|
} catch (final Exception e) { |
148
|
|
|
LOGGER.warn("Reaching Duo has failed with error: [{}]", e.getMessage(), e); |
149
|
|
|
} |
150
|
|
|
return account; |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
private static String buildUrlHttpScheme(final String url) { |
154
|
|
|
if (!url.startsWith("http")) { |
155
|
|
|
return "https://" + url; |
156
|
|
|
} |
157
|
|
|
return url; |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
/** |
161
|
|
|
* Build http post auth request http. |
162
|
|
|
* |
163
|
|
|
* @return the http |
164
|
|
|
*/ |
165
|
|
|
protected Http buildHttpPostAuthRequest() { |
166
|
|
|
return new Http(HttpMethod.POST.name(), |
167
|
|
|
duoProperties.getDuoApiHost(), |
168
|
|
|
String.format("/auth/v%s/auth", AUTH_API_VERSION)); |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
/** |
172
|
|
|
* Build http post get user auth request. |
173
|
|
|
* |
174
|
|
|
* @param username the username |
175
|
|
|
* @return the http |
176
|
|
|
*/ |
177
|
|
|
protected Http buildHttpPostUserPreAuthRequest(final String username) { |
178
|
|
|
final Http usersRequest = new Http(HttpMethod.POST.name(), |
179
|
|
|
duoProperties.getDuoApiHost(), |
180
|
|
|
String.format("/auth/v%s/preauth", AUTH_API_VERSION)); |
181
|
|
|
usersRequest.addParam("username", username); |
182
|
|
|
return usersRequest; |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
/** |
186
|
|
|
* Sign http request. |
187
|
|
|
* |
188
|
|
|
* @param request the request |
189
|
|
|
* @param id the id |
190
|
|
|
* @return the http |
191
|
|
|
*/ |
192
|
|
|
protected Http signHttpAuthRequest(final Http request, final String id) { |
193
|
|
|
try { |
194
|
|
|
request.addParam("username", id); |
195
|
|
|
request.addParam("factor", "auto"); |
196
|
|
|
request.addParam("device", "auto"); |
197
|
|
|
request.signRequest( |
198
|
|
|
duoProperties.getDuoIntegrationKey(), |
199
|
|
|
duoProperties.getDuoSecretKey()); |
200
|
|
|
return request; |
201
|
|
|
} catch (final Exception e) { |
202
|
|
|
throw new RuntimeException(e.getMessage(), e); |
203
|
|
|
} |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
/** |
207
|
|
|
* Sign http users request http. |
208
|
|
|
* |
209
|
|
|
* @param request the request |
210
|
|
|
* @return the http |
211
|
|
|
*/ |
212
|
|
|
protected Http signHttpUserPreAuthRequest(final Http request) { |
213
|
|
|
try { |
214
|
|
|
request.signRequest( |
215
|
|
|
duoProperties.getDuoIntegrationKey(), |
216
|
|
|
duoProperties.getDuoSecretKey()); |
217
|
|
|
return request; |
218
|
|
|
} catch (final Exception e) { |
219
|
|
|
throw new RuntimeException(e.getMessage(), e); |
220
|
|
|
} |
221
|
|
|
} |
222
|
|
|
} |
223
|
|
|
|