2FA support for platform authentication
This commit is contained in:
parent
e2c9a5ffdf
commit
1ad769048c
@ -15,12 +15,14 @@
|
|||||||
*/
|
*/
|
||||||
package org.thingsboard.server.config;
|
package org.thingsboard.server.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.stereotype.Component;
|
||||||
import org.thingsboard.server.common.data.security.model.JwtToken;
|
import org.thingsboard.server.common.data.security.model.JwtToken;
|
||||||
|
|
||||||
@Configuration
|
@Component
|
||||||
@ConfigurationProperties(prefix = "security.jwt")
|
@ConfigurationProperties(prefix = "security.jwt")
|
||||||
|
@Data
|
||||||
public class JwtSettings {
|
public class JwtSettings {
|
||||||
/**
|
/**
|
||||||
* {@link JwtToken} will expire after this time.
|
* {@link JwtToken} will expire after this time.
|
||||||
@ -42,34 +44,10 @@ public class JwtSettings {
|
|||||||
*/
|
*/
|
||||||
private Integer refreshTokenExpTime;
|
private Integer refreshTokenExpTime;
|
||||||
|
|
||||||
public Integer getRefreshTokenExpTime() {
|
/**
|
||||||
return refreshTokenExpTime;
|
* Issued when 2FA is being used.
|
||||||
}
|
* Valid only for 2FA verification code checking.
|
||||||
|
* */
|
||||||
|
private Integer preVerificationTokenExpirationTime;
|
||||||
|
|
||||||
public void setRefreshTokenExpTime(Integer refreshTokenExpTime) {
|
}
|
||||||
this.refreshTokenExpTime = refreshTokenExpTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getTokenExpirationTime() {
|
|
||||||
return tokenExpirationTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTokenExpirationTime(Integer tokenExpirationTime) {
|
|
||||||
this.tokenExpirationTime = tokenExpirationTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTokenIssuer() {
|
|
||||||
return tokenIssuer;
|
|
||||||
}
|
|
||||||
public void setTokenIssuer(String tokenIssuer) {
|
|
||||||
this.tokenIssuer = tokenIssuer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTokenSigningKey() {
|
|
||||||
return tokenSigningKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTokenSigningKey(String tokenSigningKey) {
|
|
||||||
this.tokenSigningKey = tokenSigningKey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSett
|
|||||||
import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig;
|
import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig;
|
||||||
import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig;
|
import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig;
|
||||||
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
|
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
|
||||||
|
import org.thingsboard.server.service.security.model.JwtTokenPair;
|
||||||
import org.thingsboard.server.service.security.model.SecurityUser;
|
import org.thingsboard.server.service.security.model.SecurityUser;
|
||||||
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
|
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
|
||||||
|
|
||||||
@ -125,4 +126,22 @@ public class TwoFactorAuthController extends BaseController {
|
|||||||
twoFactorAuthService.saveTwoFaSettings(getTenantId(), twoFactorAuthSettings);
|
twoFactorAuthService.saveTwoFaSettings(getTenantId(), twoFactorAuthSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@PostMapping("/auth/2fa/verification/check")
|
||||||
|
@PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')")
|
||||||
|
public JwtTokenPair checkTwoFaVerificationCode(@RequestParam String verificationCode) throws ThingsboardException {
|
||||||
|
SecurityUser user = getCurrentUser();
|
||||||
|
|
||||||
|
boolean verificationSuccess = twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), user.getId(),
|
||||||
|
(provider, providerConfig, accountConfig) -> {
|
||||||
|
return provider.checkVerificationCode(user, verificationCode, accountConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (verificationSuccess) {
|
||||||
|
return tokenFactory.createTokenPair(user);
|
||||||
|
} else {
|
||||||
|
throw new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.AUTHENTICATION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,6 +66,7 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide
|
|||||||
} else {
|
} else {
|
||||||
securityUser = authenticateByPublicId(principal.getValue());
|
securityUser = authenticateByPublicId(principal.getValue());
|
||||||
}
|
}
|
||||||
|
securityUser.setSessionId(unsafeUser.getSessionId());
|
||||||
|
|
||||||
if (tokenOutdatingService.isOutdated(rawAccessToken, securityUser.getId())) {
|
if (tokenOutdatingService.isOutdated(rawAccessToken, securityUser.getId())) {
|
||||||
throw new CredentialsExpiredException("Token is outdated");
|
throw new CredentialsExpiredException("Token is outdated");
|
||||||
|
|||||||
@ -16,15 +16,18 @@
|
|||||||
package org.thingsboard.server.service.security.auth.rest;
|
package org.thingsboard.server.service.security.auth.rest;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.web.WebAttributes;
|
import org.springframework.security.web.WebAttributes;
|
||||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.thingsboard.server.common.data.security.model.JwtToken;
|
|
||||||
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
|
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
|
||||||
|
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
|
||||||
|
import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig;
|
||||||
|
import org.thingsboard.server.service.security.model.JwtTokenPair;
|
||||||
import org.thingsboard.server.service.security.model.SecurityUser;
|
import org.thingsboard.server.service.security.model.SecurityUser;
|
||||||
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
|
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
|
||||||
|
|
||||||
@ -33,37 +36,44 @@ import javax.servlet.http.HttpServletRequest;
|
|||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import javax.servlet.http.HttpSession;
|
import javax.servlet.http.HttpSession;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.HashMap;
|
import java.util.Optional;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Component(value = "defaultAuthenticationSuccessHandler")
|
@Component(value = "defaultAuthenticationSuccessHandler")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
public class RestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
|
public class RestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
|
||||||
private final ObjectMapper mapper;
|
private final ObjectMapper mapper;
|
||||||
private final JwtTokenFactory tokenFactory;
|
private final JwtTokenFactory tokenFactory;
|
||||||
private final RefreshTokenRepository refreshTokenRepository;
|
private final TwoFactorAuthService twoFactorAuthService;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public RestAwareAuthenticationSuccessHandler(final ObjectMapper mapper, final JwtTokenFactory tokenFactory, final RefreshTokenRepository refreshTokenRepository) {
|
|
||||||
this.mapper = mapper;
|
|
||||||
this.tokenFactory = tokenFactory;
|
|
||||||
this.refreshTokenRepository = refreshTokenRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
|
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
|
||||||
Authentication authentication) throws IOException, ServletException {
|
Authentication authentication) throws IOException, ServletException {
|
||||||
SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
|
SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
|
||||||
|
|
||||||
JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
|
JwtTokenPair tokenPair;
|
||||||
JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
|
|
||||||
|
|
||||||
Map<String, String> tokenMap = new HashMap<String, String>();
|
Optional<TwoFactorAuthAccountConfig> twoFaAccountConfig = twoFactorAuthService.getTwoFaAccountConfig(securityUser.getTenantId(), securityUser.getId());
|
||||||
tokenMap.put("token", accessToken.getToken());
|
if (twoFaAccountConfig.isPresent()) {
|
||||||
tokenMap.put("refreshToken", refreshToken.getToken());
|
try {
|
||||||
|
twoFactorAuthService.processByTwoFaProvider(securityUser.getTenantId(), twoFaAccountConfig.get().getProviderType(),
|
||||||
|
(provider, providerConfig) -> {
|
||||||
|
provider.prepareVerificationCode(securityUser, providerConfig, twoFaAccountConfig.get());
|
||||||
|
});
|
||||||
|
tokenPair = new JwtTokenPair();
|
||||||
|
tokenPair.setToken(tokenFactory.createPreVerificationToken(securityUser).getToken());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to process 2FA for user {}. Falling back to plain auth", securityUser.getId(), e);
|
||||||
|
tokenPair = tokenFactory.createTokenPair(securityUser);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tokenPair = tokenFactory.createTokenPair(securityUser);
|
||||||
|
}
|
||||||
|
|
||||||
response.setStatus(HttpStatus.OK.value());
|
response.setStatus(HttpStatus.OK.value());
|
||||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||||
mapper.writeValue(response.getWriter(), tokenMap);
|
|
||||||
|
mapper.writeValue(response.getWriter(), tokenPair);
|
||||||
|
|
||||||
clearAuthenticationAttributes(request);
|
clearAuthenticationAttributes(request);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,10 +19,12 @@ import io.swagger.annotations.ApiModel;
|
|||||||
import io.swagger.annotations.ApiModelProperty;
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
@ApiModel(value = "JWT Token Pair")
|
@ApiModel(value = "JWT Token Pair")
|
||||||
@Data
|
@Data
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
public class JwtTokenPair {
|
public class JwtTokenPair {
|
||||||
|
|
||||||
@ApiModelProperty(position = 1, value = "The JWT Access Token. Used to perform API calls.", example = "AAB254FF67D..")
|
@ApiModelProperty(position = 1, value = "The JWT Access Token. Used to perform API calls.", example = "AAB254FF67D..")
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.User;
|
|||||||
import org.thingsboard.server.common.data.id.UserId;
|
import org.thingsboard.server.common.data.id.UserId;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
@ -31,6 +32,7 @@ public class SecurityUser extends User {
|
|||||||
private Collection<GrantedAuthority> authorities;
|
private Collection<GrantedAuthority> authorities;
|
||||||
private boolean enabled;
|
private boolean enabled;
|
||||||
private UserPrincipal userPrincipal;
|
private UserPrincipal userPrincipal;
|
||||||
|
private String sessionId;
|
||||||
|
|
||||||
public SecurityUser() {
|
public SecurityUser() {
|
||||||
super();
|
super();
|
||||||
@ -44,6 +46,7 @@ public class SecurityUser extends User {
|
|||||||
super(user);
|
super(user);
|
||||||
this.enabled = enabled;
|
this.enabled = enabled;
|
||||||
this.userPrincipal = userPrincipal;
|
this.userPrincipal = userPrincipal;
|
||||||
|
this.sessionId = UUID.randomUUID().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Collection<GrantedAuthority> getAuthorities() {
|
public Collection<GrantedAuthority> getAuthorities() {
|
||||||
@ -71,4 +74,12 @@ public class SecurityUser extends User {
|
|||||||
this.userPrincipal = userPrincipal;
|
this.userPrincipal = userPrincipal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getSessionId() {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionId(String sessionId) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,25 +15,17 @@
|
|||||||
*/
|
*/
|
||||||
package org.thingsboard.server.service.security.model.token;
|
package org.thingsboard.server.service.security.model.token;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
|
||||||
import io.jsonwebtoken.Claims;
|
|
||||||
import org.thingsboard.server.common.data.security.model.JwtToken;
|
import org.thingsboard.server.common.data.security.model.JwtToken;
|
||||||
|
|
||||||
public final class AccessJwtToken implements JwtToken {
|
public final class AccessJwtToken implements JwtToken {
|
||||||
private final String rawToken;
|
private final String rawToken;
|
||||||
@JsonIgnore
|
|
||||||
private transient Claims claims;
|
|
||||||
|
|
||||||
protected AccessJwtToken(final String token, Claims claims) {
|
public AccessJwtToken(String rawToken) {
|
||||||
this.rawToken = token;
|
this.rawToken = rawToken;
|
||||||
this.claims = claims;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getToken() {
|
public String getToken() {
|
||||||
return this.rawToken;
|
return this.rawToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Claims getClaims() {
|
|
||||||
return claims;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ package org.thingsboard.server.service.security.model.token;
|
|||||||
import io.jsonwebtoken.Claims;
|
import io.jsonwebtoken.Claims;
|
||||||
import io.jsonwebtoken.ExpiredJwtException;
|
import io.jsonwebtoken.ExpiredJwtException;
|
||||||
import io.jsonwebtoken.Jws;
|
import io.jsonwebtoken.Jws;
|
||||||
|
import io.jsonwebtoken.JwtBuilder;
|
||||||
import io.jsonwebtoken.Jwts;
|
import io.jsonwebtoken.Jwts;
|
||||||
import io.jsonwebtoken.MalformedJwtException;
|
import io.jsonwebtoken.MalformedJwtException;
|
||||||
import io.jsonwebtoken.SignatureAlgorithm;
|
import io.jsonwebtoken.SignatureAlgorithm;
|
||||||
@ -36,6 +37,7 @@ import org.thingsboard.server.common.data.security.Authority;
|
|||||||
import org.thingsboard.server.common.data.security.model.JwtToken;
|
import org.thingsboard.server.common.data.security.model.JwtToken;
|
||||||
import org.thingsboard.server.config.JwtSettings;
|
import org.thingsboard.server.config.JwtSettings;
|
||||||
import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
|
import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
|
||||||
|
import org.thingsboard.server.service.security.model.JwtTokenPair;
|
||||||
import org.thingsboard.server.service.security.model.SecurityUser;
|
import org.thingsboard.server.service.security.model.SecurityUser;
|
||||||
import org.thingsboard.server.service.security.model.UserPrincipal;
|
import org.thingsboard.server.service.security.model.UserPrincipal;
|
||||||
|
|
||||||
@ -58,6 +60,7 @@ public class JwtTokenFactory {
|
|||||||
private static final String IS_PUBLIC = "isPublic";
|
private static final String IS_PUBLIC = "isPublic";
|
||||||
private static final String TENANT_ID = "tenantId";
|
private static final String TENANT_ID = "tenantId";
|
||||||
private static final String CUSTOMER_ID = "customerId";
|
private static final String CUSTOMER_ID = "customerId";
|
||||||
|
private static final String SESSION_ID = "sessionId";
|
||||||
|
|
||||||
private final JwtSettings settings;
|
private final JwtSettings settings;
|
||||||
|
|
||||||
@ -70,39 +73,28 @@ public class JwtTokenFactory {
|
|||||||
* Factory method for issuing new JWT Tokens.
|
* Factory method for issuing new JWT Tokens.
|
||||||
*/
|
*/
|
||||||
public AccessJwtToken createAccessJwtToken(SecurityUser securityUser) {
|
public AccessJwtToken createAccessJwtToken(SecurityUser securityUser) {
|
||||||
if (StringUtils.isBlank(securityUser.getEmail()))
|
if (securityUser.getAuthority() == null) {
|
||||||
throw new IllegalArgumentException("Cannot create JWT Token without username/email");
|
|
||||||
|
|
||||||
if (securityUser.getAuthority() == null)
|
|
||||||
throw new IllegalArgumentException("User doesn't have any privileges");
|
throw new IllegalArgumentException("User doesn't have any privileges");
|
||||||
|
}
|
||||||
|
|
||||||
UserPrincipal principal = securityUser.getUserPrincipal();
|
UserPrincipal principal = securityUser.getUserPrincipal();
|
||||||
String subject = principal.getValue();
|
|
||||||
Claims claims = Jwts.claims().setSubject(subject);
|
JwtBuilder jwtBuilder = setUpToken(securityUser, securityUser.getAuthorities().stream()
|
||||||
claims.put(SCOPES, securityUser.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()));
|
.map(GrantedAuthority::getAuthority).collect(Collectors.toList()), settings.getTokenExpirationTime());
|
||||||
claims.put(USER_ID, securityUser.getId().getId().toString());
|
jwtBuilder.claim(FIRST_NAME, securityUser.getFirstName())
|
||||||
claims.put(FIRST_NAME, securityUser.getFirstName());
|
.claim(LAST_NAME, securityUser.getLastName())
|
||||||
claims.put(LAST_NAME, securityUser.getLastName());
|
.claim(ENABLED, securityUser.isEnabled())
|
||||||
claims.put(ENABLED, securityUser.isEnabled());
|
.claim(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID);
|
||||||
claims.put(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID);
|
|
||||||
if (securityUser.getTenantId() != null) {
|
if (securityUser.getTenantId() != null) {
|
||||||
claims.put(TENANT_ID, securityUser.getTenantId().getId().toString());
|
jwtBuilder.claim(TENANT_ID, securityUser.getTenantId().getId().toString());
|
||||||
}
|
}
|
||||||
if (securityUser.getCustomerId() != null) {
|
if (securityUser.getCustomerId() != null) {
|
||||||
claims.put(CUSTOMER_ID, securityUser.getCustomerId().getId().toString());
|
jwtBuilder.claim(CUSTOMER_ID, securityUser.getCustomerId().getId().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
ZonedDateTime currentTime = ZonedDateTime.now();
|
String token = jwtBuilder.compact();
|
||||||
|
|
||||||
String token = Jwts.builder()
|
return new AccessJwtToken(token);
|
||||||
.setClaims(claims)
|
|
||||||
.setIssuer(settings.getTokenIssuer())
|
|
||||||
.setIssuedAt(Date.from(currentTime.toInstant()))
|
|
||||||
.setExpiration(Date.from(currentTime.plusSeconds(settings.getTokenExpirationTime()).toInstant()))
|
|
||||||
.signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey())
|
|
||||||
.compact();
|
|
||||||
|
|
||||||
return new AccessJwtToken(token, claims);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public SecurityUser parseAccessJwtToken(RawAccessJwtToken rawAccessToken) {
|
public SecurityUser parseAccessJwtToken(RawAccessJwtToken rawAccessToken) {
|
||||||
@ -118,47 +110,40 @@ public class JwtTokenFactory {
|
|||||||
SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class))));
|
SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class))));
|
||||||
securityUser.setEmail(subject);
|
securityUser.setEmail(subject);
|
||||||
securityUser.setAuthority(Authority.parse(scopes.get(0)));
|
securityUser.setAuthority(Authority.parse(scopes.get(0)));
|
||||||
securityUser.setFirstName(claims.get(FIRST_NAME, String.class));
|
|
||||||
securityUser.setLastName(claims.get(LAST_NAME, String.class));
|
|
||||||
securityUser.setEnabled(claims.get(ENABLED, Boolean.class));
|
|
||||||
boolean isPublic = claims.get(IS_PUBLIC, Boolean.class);
|
|
||||||
UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject);
|
|
||||||
securityUser.setUserPrincipal(principal);
|
|
||||||
String tenantId = claims.get(TENANT_ID, String.class);
|
String tenantId = claims.get(TENANT_ID, String.class);
|
||||||
if (tenantId != null) {
|
if (tenantId != null) {
|
||||||
securityUser.setTenantId(TenantId.fromUUID(UUID.fromString(tenantId)));
|
securityUser.setTenantId(TenantId.fromUUID(UUID.fromString(tenantId)));
|
||||||
|
} else if (securityUser.getAuthority() == Authority.SYS_ADMIN) {
|
||||||
|
securityUser.setTenantId(TenantId.SYS_TENANT_ID);
|
||||||
}
|
}
|
||||||
String customerId = claims.get(CUSTOMER_ID, String.class);
|
securityUser.setSessionId(claims.get(SESSION_ID, String.class));
|
||||||
if (customerId != null) {
|
|
||||||
securityUser.setCustomerId(new CustomerId(UUID.fromString(customerId)));
|
if (securityUser.getAuthority() != Authority.PRE_VERIFICATION_TOKEN) {
|
||||||
|
securityUser.setFirstName(claims.get(FIRST_NAME, String.class));
|
||||||
|
securityUser.setLastName(claims.get(LAST_NAME, String.class));
|
||||||
|
securityUser.setEnabled(claims.get(ENABLED, Boolean.class));
|
||||||
|
boolean isPublic = claims.get(IS_PUBLIC, Boolean.class);
|
||||||
|
UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject);
|
||||||
|
securityUser.setUserPrincipal(principal);
|
||||||
|
String customerId = claims.get(CUSTOMER_ID, String.class);
|
||||||
|
if (customerId != null) {
|
||||||
|
securityUser.setCustomerId(new CustomerId(UUID.fromString(customerId)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
securityUser.setUserPrincipal(new UserPrincipal(UserPrincipal.Type.USER_NAME, subject));
|
||||||
}
|
}
|
||||||
|
|
||||||
return securityUser;
|
return securityUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
public JwtToken createRefreshToken(SecurityUser securityUser) {
|
public JwtToken createRefreshToken(SecurityUser securityUser) {
|
||||||
if (StringUtils.isBlank(securityUser.getEmail())) {
|
|
||||||
throw new IllegalArgumentException("Cannot create JWT Token without username/email");
|
|
||||||
}
|
|
||||||
|
|
||||||
ZonedDateTime currentTime = ZonedDateTime.now();
|
|
||||||
|
|
||||||
UserPrincipal principal = securityUser.getUserPrincipal();
|
UserPrincipal principal = securityUser.getUserPrincipal();
|
||||||
Claims claims = Jwts.claims().setSubject(principal.getValue());
|
|
||||||
claims.put(SCOPES, Collections.singletonList(Authority.REFRESH_TOKEN.name()));
|
|
||||||
claims.put(USER_ID, securityUser.getId().getId().toString());
|
|
||||||
claims.put(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID);
|
|
||||||
|
|
||||||
String token = Jwts.builder()
|
String token = setUpToken(securityUser, Collections.singletonList(Authority.REFRESH_TOKEN.name()), settings.getRefreshTokenExpTime())
|
||||||
.setClaims(claims)
|
.claim(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID)
|
||||||
.setIssuer(settings.getTokenIssuer())
|
.setId(UUID.randomUUID().toString()).compact();
|
||||||
.setId(UUID.randomUUID().toString())
|
|
||||||
.setIssuedAt(Date.from(currentTime.toInstant()))
|
|
||||||
.setExpiration(Date.from(currentTime.plusSeconds(settings.getRefreshTokenExpTime()).toInstant()))
|
|
||||||
.signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey())
|
|
||||||
.compact();
|
|
||||||
|
|
||||||
return new AccessJwtToken(token, claims);
|
return new AccessJwtToken(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SecurityUser parseRefreshToken(RawAccessJwtToken rawAccessToken) {
|
public SecurityUser parseRefreshToken(RawAccessJwtToken rawAccessToken) {
|
||||||
@ -177,9 +162,41 @@ public class JwtTokenFactory {
|
|||||||
UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject);
|
UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject);
|
||||||
SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class))));
|
SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class))));
|
||||||
securityUser.setUserPrincipal(principal);
|
securityUser.setUserPrincipal(principal);
|
||||||
|
securityUser.setSessionId(claims.get(SESSION_ID, String.class));
|
||||||
return securityUser;
|
return securityUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public JwtToken createPreVerificationToken(SecurityUser user) {
|
||||||
|
String token = setUpToken(user, Collections.singletonList(Authority.PRE_VERIFICATION_TOKEN.name()), settings.getPreVerificationTokenExpirationTime())
|
||||||
|
.claim(TENANT_ID, user.getTenantId().toString())
|
||||||
|
.compact();
|
||||||
|
return new AccessJwtToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JwtBuilder setUpToken(SecurityUser securityUser, List<String> scopes, long expirationTime) {
|
||||||
|
if (StringUtils.isBlank(securityUser.getEmail())) {
|
||||||
|
throw new IllegalArgumentException("Cannot create JWT Token without username/email");
|
||||||
|
}
|
||||||
|
|
||||||
|
UserPrincipal principal = securityUser.getUserPrincipal();
|
||||||
|
|
||||||
|
Claims claims = Jwts.claims().setSubject(principal.getValue());
|
||||||
|
claims.put(USER_ID, securityUser.getId().getId().toString());
|
||||||
|
claims.put(SCOPES, scopes);
|
||||||
|
if (securityUser.getSessionId() != null) {
|
||||||
|
claims.put(SESSION_ID, securityUser.getSessionId());
|
||||||
|
}
|
||||||
|
|
||||||
|
ZonedDateTime currentTime = ZonedDateTime.now();
|
||||||
|
|
||||||
|
return Jwts.builder()
|
||||||
|
.setClaims(claims)
|
||||||
|
.setIssuer(settings.getTokenIssuer())
|
||||||
|
.setIssuedAt(Date.from(currentTime.toInstant()))
|
||||||
|
.setExpiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant()))
|
||||||
|
.signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey());
|
||||||
|
}
|
||||||
|
|
||||||
public Jws<Claims> parseTokenClaims(JwtToken token) {
|
public Jws<Claims> parseTokenClaims(JwtToken token) {
|
||||||
try {
|
try {
|
||||||
return Jwts.parser()
|
return Jwts.parser()
|
||||||
@ -193,4 +210,11 @@ public class JwtTokenFactory {
|
|||||||
throw new JwtExpiredTokenException(token, "JWT Token expired", expiredEx);
|
throw new JwtExpiredTokenException(token, "JWT Token expired", expiredEx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public JwtTokenPair createTokenPair(SecurityUser securityUser) {
|
||||||
|
JwtToken accessToken = createAccessJwtToken(securityUser);
|
||||||
|
JwtToken refreshToken = createRefreshToken(securityUser);
|
||||||
|
return new JwtTokenPair(accessToken.getToken(), refreshToken.getToken());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -127,6 +127,8 @@ security:
|
|||||||
jwt:
|
jwt:
|
||||||
tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:9000}" # Number of seconds (2.5 hours)
|
tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:9000}" # Number of seconds (2.5 hours)
|
||||||
refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:604800}" # Number of seconds (1 week)
|
refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:604800}" # Number of seconds (1 week)
|
||||||
|
# Number of seconds. Issued when 2FA is being used; valid only for checking 2FA verification code after which usual token pair is issued
|
||||||
|
preVerificationTokenExpirationTime: "${JWT_PRE_VERIFICATION_TOKEN_EXPIRATION_TIME:30}"
|
||||||
tokenIssuer: "${JWT_TOKEN_ISSUER:thingsboard.io}"
|
tokenIssuer: "${JWT_TOKEN_ISSUER:thingsboard.io}"
|
||||||
tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}"
|
tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}"
|
||||||
# Enable/disable access to Tenant Administrators JWT token by System Administrator or Customer Users JWT token by Tenant Administrator
|
# Enable/disable access to Tenant Administrators JWT token by System Administrator or Customer Users JWT token by Tenant Administrator
|
||||||
@ -425,6 +427,9 @@ caffeine:
|
|||||||
edges:
|
edges:
|
||||||
timeToLiveInMinutes: "${CACHE_SPECS_EDGES_TTL:1440}"
|
timeToLiveInMinutes: "${CACHE_SPECS_EDGES_TTL:1440}"
|
||||||
maxSize: "${CACHE_SPECS_EDGES_MAX_SIZE:10000}"
|
maxSize: "${CACHE_SPECS_EDGES_MAX_SIZE:10000}"
|
||||||
|
twoFaVerificationCodes:
|
||||||
|
timeToLiveInMinutes: "1"
|
||||||
|
maxSize: "100000"
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
# standalone or cluster
|
# standalone or cluster
|
||||||
|
|||||||
@ -16,11 +16,12 @@
|
|||||||
package org.thingsboard.server.common.data.security;
|
package org.thingsboard.server.common.data.security;
|
||||||
|
|
||||||
public enum Authority {
|
public enum Authority {
|
||||||
|
|
||||||
SYS_ADMIN(0),
|
SYS_ADMIN(0),
|
||||||
TENANT_ADMIN(1),
|
TENANT_ADMIN(1),
|
||||||
CUSTOMER_USER(2),
|
CUSTOMER_USER(2),
|
||||||
REFRESH_TOKEN(10);
|
REFRESH_TOKEN(10),
|
||||||
|
PRE_VERIFICATION_TOKEN(11);
|
||||||
|
|
||||||
private int code;
|
private int code;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user