AuthController.java
17.7 KB
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
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.model.ActivateUserRequest;
import org.thingsboard.server.service.security.model.ChangePasswordRequest;
import org.thingsboard.server.service.security.model.ResetPasswordEmailRequest;
import org.thingsboard.server.service.security.model.ResetPasswordRequest;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import javax.servlet.http.HttpServletRequest;
import java.net.URI;
import java.net.URISyntaxException;
@RestController
@TbCoreComponent
@RequestMapping("/api")
@Slf4j
@RequiredArgsConstructor
public class AuthController extends BaseController {
@Value("${server.rest.rate_limits.reset_password_per_user:5:3600}")
private String defaultLimitsConfiguration;
private final BCryptPasswordEncoder passwordEncoder;
private final JwtTokenFactory tokenFactory;
private final MailService mailService;
private final SystemSecurityService systemSecurityService;
private final RateLimitService rateLimitService;
private final ApplicationEventPublisher eventPublisher;
@ApiOperation(value = "Get current User (getUser)",
notes = "Get the information about the User which credentials are used to perform this REST API call.")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/auth/user", method = RequestMethod.GET)
public @ResponseBody
User getUser() throws ThingsboardException {
SecurityUser securityUser = getCurrentUser();
return userService.findUserById(securityUser.getTenantId(), securityUser.getId());
}
@ApiOperation(value = "Logout (logout)",
notes = "Special API call to record the 'logout' of the user to the Audit Logs. Since platform uses [JWT](https://jwt.io/), the actual logout is the procedure of clearing the [JWT](https://jwt.io/) token on the client side. ")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/auth/logout", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
public void logout(HttpServletRequest request) throws ThingsboardException {
logLogoutAction(request);
}
@ApiOperation(value = "Change password for current User (changePassword)",
notes = "Change the password for the User which credentials are used to perform this REST API call. Be aware that previously generated [JWT](https://jwt.io/) tokens will be still valid until they expire.")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/auth/changePassword", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
public ObjectNode changePassword(
@ApiParam(value = "Change Password Request")
@RequestBody ChangePasswordRequest changePasswordRequest) throws ThingsboardException {
String currentPassword = changePasswordRequest.getCurrentPassword();
String newPassword = changePasswordRequest.getNewPassword();
SecurityUser securityUser = getCurrentUser();
UserCredentials userCredentials = userService.findUserCredentialsByUserId(TenantId.SYS_TENANT_ID, securityUser.getId());
if (!passwordEncoder.matches(currentPassword, userCredentials.getPassword())) {
throw new ThingsboardException("Current password doesn't match!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
systemSecurityService.validatePassword(newPassword, userCredentials);
if (passwordEncoder.matches(newPassword, userCredentials.getPassword())) {
throw new ThingsboardException("New password should be different from existing!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
userCredentials.setPassword(passwordEncoder.encode(newPassword));
userService.replaceUserCredentials(securityUser.getTenantId(), userCredentials);
eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId()));
ObjectNode response = JacksonUtil.newObjectNode();
response.put("token", tokenFactory.createAccessJwtToken(securityUser).getToken());
response.put("refreshToken", tokenFactory.createRefreshToken(securityUser).getToken());
return response;
}
@ApiOperation(value = "Get the current User password policy (getUserPasswordPolicy)",
notes = "API call to get the password policy for the password validation form(s).")
@RequestMapping(value = "/noauth/userPasswordPolicy", method = RequestMethod.GET)
@ResponseBody
public UserPasswordPolicy getUserPasswordPolicy() throws ThingsboardException {
SecuritySettings securitySettings =
checkNotNull(systemSecurityService.getSecuritySettings());
return securitySettings.getPasswordPolicy();
}
@ApiOperation(value = "Check Activate User Token (checkActivateToken)",
notes = "Checks the activation token and forwards user to 'Create Password' page. " +
"If token is valid, returns '303 See Other' (redirect) response code with the correct address of 'Create Password' page and same 'activateToken' specified in the URL parameters. " +
"If token is not valid, returns '409 Conflict'.")
@RequestMapping(value = "/noauth/activate", params = {"activateToken"}, method = RequestMethod.GET)
public ResponseEntity<String> checkActivateToken(
@ApiParam(value = "The activate token string.")
@RequestParam(value = "activateToken") String activateToken) {
HttpHeaders headers = new HttpHeaders();
HttpStatus responseStatus;
UserCredentials userCredentials = userService.findUserCredentialsByActivateToken(TenantId.SYS_TENANT_ID, activateToken);
if (userCredentials != null) {
String createURI = "/login/createPassword";
try {
URI location = new URI(createURI + "?activateToken=" + activateToken);
headers.setLocation(location);
responseStatus = HttpStatus.SEE_OTHER;
} catch (URISyntaxException e) {
log.error("Unable to create URI with address [{}]", createURI);
responseStatus = HttpStatus.BAD_REQUEST;
}
} else {
responseStatus = HttpStatus.CONFLICT;
}
return new ResponseEntity<>(headers, responseStatus);
}
@ApiOperation(value = "Request reset password email (requestResetPasswordByEmail)",
notes = "Request to send the reset password email if the user with specified email address is present in the database. " +
"Always return '200 OK' status for security purposes.")
@RequestMapping(value = "/noauth/resetPasswordByEmail", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
public void requestResetPasswordByEmail(
@ApiParam(value = "The JSON object representing the reset password email request.")
@RequestBody ResetPasswordEmailRequest resetPasswordByEmailRequest,
HttpServletRequest request) throws ThingsboardException {
try {
String email = resetPasswordByEmailRequest.getEmail();
UserCredentials userCredentials = userService.requestPasswordReset(TenantId.SYS_TENANT_ID, email);
User user = userService.findUserById(TenantId.SYS_TENANT_ID, userCredentials.getUserId());
String baseUrl = systemSecurityService.getBaseUrl(user.getTenantId(), user.getCustomerId(), request);
String resetUrl = String.format("%s/api/noauth/resetPassword?resetToken=%s", baseUrl,
userCredentials.getResetToken());
mailService.sendResetPasswordEmailAsync(resetUrl, email);
} catch (Exception e) {
log.warn("Error occurred: {}", e.getMessage());
}
}
@ApiOperation(value = "Check password reset token (checkResetToken)",
notes = "Checks the password reset token and forwards user to 'Reset Password' page. " +
"If token is valid, returns '303 See Other' (redirect) response code with the correct address of 'Reset Password' page and same 'resetToken' specified in the URL parameters. " +
"If token is not valid, returns '409 Conflict'.")
@RequestMapping(value = "/noauth/resetPassword", params = {"resetToken"}, method = RequestMethod.GET)
public ResponseEntity<String> checkResetToken(
@ApiParam(value = "The reset token string.")
@RequestParam(value = "resetToken") String resetToken) {
HttpHeaders headers = new HttpHeaders();
HttpStatus responseStatus;
String resetURI = "/login/resetPassword";
UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken);
if (userCredentials != null) {
if (!rateLimitService.checkRateLimit(LimitedApi.PASSWORD_RESET, userCredentials.getUserId(), defaultLimitsConfiguration)) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).build();
}
try {
URI location = new URI(resetURI + "?resetToken=" + resetToken);
headers.setLocation(location);
responseStatus = HttpStatus.SEE_OTHER;
} catch (URISyntaxException e) {
log.error("Unable to create URI with address [{}]", resetURI);
responseStatus = HttpStatus.BAD_REQUEST;
}
} else {
responseStatus = HttpStatus.CONFLICT;
}
return new ResponseEntity<>(headers, responseStatus);
}
@ApiOperation(value = "Activate User",
notes = "Checks the activation token and updates corresponding user password in the database. " +
"Now the user may start using his password to login. " +
"The response already contains the [JWT](https://jwt.io) activation and refresh tokens, " +
"to simplify the user activation flow and avoid asking user to input password again after activation. " +
"If token is valid, returns the object that contains [JWT](https://jwt.io/) access and refresh tokens. " +
"If token is not valid, returns '404 Bad Request'.")
@RequestMapping(value = "/noauth/activate", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
@ResponseBody
public JwtPair activateUser(
@ApiParam(value = "Activate user request.")
@RequestBody ActivateUserRequest activateRequest,
@RequestParam(required = false, defaultValue = "true") boolean sendActivationMail,
HttpServletRequest request) throws ThingsboardException {
String activateToken = activateRequest.getActivateToken();
String password = activateRequest.getPassword();
systemSecurityService.validatePassword(password, null);
String encodedPassword = passwordEncoder.encode(password);
UserCredentials credentials = userService.activateUserCredentials(TenantId.SYS_TENANT_ID, activateToken, encodedPassword);
User user = userService.findUserById(TenantId.SYS_TENANT_ID, credentials.getUserId());
UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled(), principal);
userService.setUserCredentialsEnabled(user.getTenantId(), user.getId(), true);
String baseUrl = systemSecurityService.getBaseUrl(user.getTenantId(), user.getCustomerId(), request);
String loginUrl = String.format("%s/login", baseUrl);
String email = user.getEmail();
if (sendActivationMail) {
try {
mailService.sendAccountActivatedEmail(loginUrl, email);
} catch (Exception e) {
log.info("Unable to send account activation email [{}]", e.getMessage());
}
}
return tokenFactory.createTokenPair(securityUser);
}
@ApiOperation(value = "Reset password (resetPassword)",
notes = "Checks the password reset token and updates the password. " +
"If token is valid, returns the object that contains [JWT](https://jwt.io/) access and refresh tokens. " +
"If token is not valid, returns '404 Bad Request'.")
@RequestMapping(value = "/noauth/resetPassword", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
@ResponseBody
public JwtPair resetPassword(
@ApiParam(value = "Reset password request.")
@RequestBody ResetPasswordRequest resetPasswordRequest,
HttpServletRequest request) throws ThingsboardException {
String resetToken = resetPasswordRequest.getResetToken();
String password = resetPasswordRequest.getPassword();
UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken);
if (userCredentials != null) {
systemSecurityService.validatePassword(password, userCredentials);
if (passwordEncoder.matches(password, userCredentials.getPassword())) {
throw new ThingsboardException("New password should be different from existing!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
String encodedPassword = passwordEncoder.encode(password);
userCredentials.setPassword(encodedPassword);
userCredentials.setResetToken(null);
userCredentials = userService.replaceUserCredentials(TenantId.SYS_TENANT_ID, userCredentials);
User user = userService.findUserById(TenantId.SYS_TENANT_ID, userCredentials.getUserId());
UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), principal);
String baseUrl = systemSecurityService.getBaseUrl(user.getTenantId(), user.getCustomerId(), request);
String loginUrl = String.format("%s/login", baseUrl);
String email = user.getEmail();
mailService.sendPasswordWasResetEmail(loginUrl, email);
eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId()));
return tokenFactory.createTokenPair(securityUser);
} else {
throw new ThingsboardException("Invalid reset token!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
}
private void logLogoutAction(HttpServletRequest request) throws ThingsboardException {
var user = getCurrentUser();
systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(request), ActionType.LOGOUT, null);
eventPublisher.publishEvent(new UserSessionInvalidationEvent(user.getSessionId()));
}
}