Swagger docs for TwoFactorAuthConfigController and config classes

This commit is contained in:
Viacheslav Klimov 2022-03-23 19:38:44 +02:00
parent 472edc8409
commit 6eb8a41f9a
8 changed files with 104 additions and 14 deletions

View File

@ -19,6 +19,8 @@ import com.google.zxing.BarcodeFormat;
import com.google.zxing.client.j2se.MatrixToImageWriter; import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix; import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter; import com.google.zxing.qrcode.QRCodeWriter;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
@ -44,6 +46,8 @@ import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid; import javax.validation.Valid;
import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
@RestController @RestController
@RequestMapping("/api/2fa") @RequestMapping("/api/2fa")
@TbCoreComponent @TbCoreComponent
@ -54,6 +58,20 @@ public class TwoFactorAuthConfigController extends BaseController {
private final TwoFactorAuthService twoFactorAuthService; private final TwoFactorAuthService twoFactorAuthService;
@ApiOperation(value = "Get 2FA account config (getTwoFaAccountConfig)",
notes = "Get user's account 2FA configuration. Returns empty result if user did not configured 2FA, " +
"or if a provider for previously set up account config is not now configured." + NEW_LINE +
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER + NEW_LINE +
"Response example for TOTP 2FA: " + NEW_LINE +
"{\n" +
" \"providerType\": \"TOTP\",\n" +
" \"authUrl\": \"otpauth://totp/ThingsBoard:tenant@thingsboard.org?issuer=ThingsBoard&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII\"\n" +
"}" + NEW_LINE +
"Response example for SMS 2FA: " + NEW_LINE +
"{\n" +
" \"providerType\": \"SMS\",\n" +
" \"phoneNumber\": \"+380505005050\"\n" +
"}")
@GetMapping("/account/config") @GetMapping("/account/config")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public TwoFactorAuthAccountConfig getTwoFaAccountConfig() throws ThingsboardException { public TwoFactorAuthAccountConfig getTwoFaAccountConfig() throws ThingsboardException {
@ -61,9 +79,29 @@ public class TwoFactorAuthConfigController extends BaseController {
return twoFactorAuthConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId()).orElse(null); return twoFactorAuthConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId()).orElse(null);
} }
@ApiOperation(value = "Generate 2FA account config (generateTwoFaAccountConfig)",
notes = "Generate new 2FA account config for specified provider type. " +
"This method is only useful for TOTP 2FA, as there is nothing to generate for other provider types. " +
"For TOTP, this will return a corresponding account config template " +
"with a generated OTP auth URL (with new random secret key for each API call) that can be then " +
"converted to a QR code to scan with an authenticator app. " +
"For other provider types, this method will return an empty config. " + NEW_LINE +
"Will throw an error (Bad Request) if the provider is not configured for usage. " +
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER + NEW_LINE +
"Example of a generated account config for TOTP 2FA: " + NEW_LINE +
"{\n" +
" \"providerType\": \"TOTP\",\n" +
" \"authUrl\": \"otpauth://totp/ThingsBoard:tenant@thingsboard.org?issuer=ThingsBoard&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII\"\n" +
"}" + NEW_LINE +
"For SMS provider type it will return something like: " + NEW_LINE +
"{\n" +
" \"providerType\": \"SMS\",\n" +
" \"phoneNumber\": null\n" +
"}")
@PostMapping("/account/config/generate") @PostMapping("/account/config/generate")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public TwoFactorAuthAccountConfig generateTwoFaAccountConfig(@RequestParam TwoFactorAuthProviderType providerType) throws Exception { public TwoFactorAuthAccountConfig generateTwoFaAccountConfig(@ApiParam(value = "2FA provider type to generate new account config for", defaultValue = "TOTP", required = true)
@RequestParam TwoFactorAuthProviderType providerType) throws Exception {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
return twoFactorAuthService.generateNewAccountConfig(user, providerType); return twoFactorAuthService.generateNewAccountConfig(user, providerType);
} }
@ -83,16 +121,32 @@ public class TwoFactorAuthConfigController extends BaseController {
} }
/* TMP */ /* TMP */
@ApiOperation(value = "Submit 2FA account config (submitTwoFaAccountConfig)",
notes = "Submit 2FA account config to prepare for a future verification. " +
"Basically, this method will send a verification code for a given account config, if this has " +
"sense for a chosen 2FA provider. This code is needed to then verify and save the account config." + NEW_LINE +
"Will throw an error (Bad Request) if submitted account config is not valid, " +
"or if the provider is not configured for usage. " +
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PostMapping("/account/config/submit") @PostMapping("/account/config/submit")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public void submitTwoFaAccountConfig(@Valid @RequestBody TwoFactorAuthAccountConfig accountConfig) throws Exception { public void submitTwoFaAccountConfig(@ApiParam(value = "2FA account config value. For TOTP 2FA config, authUrl value must not be blank and must match specific pattern. " +
"For SMS 2FA, phoneNumber property must not be blank and must be of E.164 phone number format.", required = true)
@Valid @RequestBody TwoFactorAuthAccountConfig accountConfig) throws Exception {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
twoFactorAuthService.prepareVerificationCode(user, accountConfig, false); twoFactorAuthService.prepareVerificationCode(user, accountConfig, false);
} }
@ApiOperation(value = "Verify and save 2FA account config (verifyAndSaveTwoFaAccountConfig)",
notes = "Checks the verification code for submitted config, and if it is correct, saves the provided account config. " +
"The config is stored in the user's additionalInfo. " + NEW_LINE +
"Will throw an error (Bad Request) if the provider is not configured for usage. " +
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PostMapping("/account/config") @PostMapping("/account/config")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public void verifyAndSaveTwoFaAccountConfig(@Valid @RequestBody TwoFactorAuthAccountConfig accountConfig, public void verifyAndSaveTwoFaAccountConfig(@ApiParam(value = "2FA account config to save. Validation rules are the same as in submitTwoFaAccountConfig API method", required = true)
@Valid @RequestBody TwoFactorAuthAccountConfig accountConfig,
@ApiParam(value = "6-digit code from an authenticator app in case of TOTP 2FA, or the one sent via an SMS message in case of SMS 2FA", required = true)
@RequestParam String verificationCode) throws Exception { @RequestParam String verificationCode) throws Exception {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, accountConfig, false); boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, accountConfig, false);
@ -103,24 +157,34 @@ public class TwoFactorAuthConfigController extends BaseController {
} }
} }
@ApiOperation(value = "Delete 2FA account config (deleteTwoFaAccountConfig)",
notes = "Delete user's 2FA config. " + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@DeleteMapping("/account/config") @DeleteMapping("/account/config")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public void deleteTwoFactorAuthAccountConfig() throws ThingsboardException { public void deleteTwoFaAccountConfig() throws ThingsboardException {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
twoFactorAuthConfigManager.deleteTwoFaAccountConfig(user.getTenantId(), user.getId()); twoFactorAuthConfigManager.deleteTwoFaAccountConfig(user.getTenantId(), user.getId());
} }
@ApiOperation(value = "Get 2FA settings (getTwoFaSettings)",
notes = "Get settings for 2FA. If 2FA is not configured, then an empty response will be returned." +
ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping("/settings") @GetMapping("/settings")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public TwoFactorAuthSettings getTwoFactorAuthSettings() throws ThingsboardException { public TwoFactorAuthSettings getTwoFaSettings() throws ThingsboardException {
return twoFactorAuthConfigManager.getTwoFaSettings(getTenantId(), false).orElse(null); return twoFactorAuthConfigManager.getTwoFaSettings(getTenantId(), false).orElse(null);
} }
@ApiOperation(value = "Save 2FA settings (saveTwoFaSettings)",
notes = "Save settings for 2FA. If a user is sysadmin - the settings are saved as AdminSettings; " +
"if it is a tenant admin - as a tenant attribute." +
ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PostMapping("/settings") @PostMapping("/settings")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public void saveTwoFactorAuthSettings(@RequestBody TwoFactorAuthSettings twoFactorAuthSettings) throws ThingsboardException { public void saveTwoFaSettings(@ApiParam(value = "Settings value", required = true)
twoFactorAuthConfigManager.saveTwoFaSettings(getTenantId(), twoFactorAuthSettings); @RequestBody TwoFactorAuthSettings twoFaSettings) throws ThingsboardException {
twoFactorAuthConfigManager.saveTwoFaSettings(getTenantId(), twoFaSettings);
} }
} }

View File

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.service.security.auth.mfa.config; package org.thingsboard.server.service.security.auth.mfa.config;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig; import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig;
@ -27,22 +28,29 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
@Data @Data
@ApiModel
public class TwoFactorAuthSettings { public class TwoFactorAuthSettings {
@ApiModelProperty(value = "Option for tenant admins to use 2FA settings configured by sysadmin. " +
"If this param is set to true, then the settings will not be validated for constraints " +
"(if it is a tenant admin; for sysadmin this param is ignored)")
private boolean useSystemTwoFactorAuthSettings; private boolean useSystemTwoFactorAuthSettings;
@ApiModelProperty(value = "The list of 2FA providers' configs. Users will only be allowed to use 2FA providers from this list.")
@Valid @Valid
private List<TwoFactorAuthProviderConfig> providers; private List<TwoFactorAuthProviderConfig> providers;
@ApiModelProperty(example = "1:60 (1 request per minute)") @ApiModelProperty(value = "Rate limit configuration for verification code sending. The format is standard: 'amountOfRequests:periodInSeconds'. " +
"The value of '1:60' would limit verification code sending requests to one per minute.", example = "1:60", required = false)
@Pattern(regexp = "[1-9]\\d*:[1-9]\\d*", message = "verification code send rate limit configuration is invalid") @Pattern(regexp = "[1-9]\\d*:[1-9]\\d*", message = "verification code send rate limit configuration is invalid")
private String verificationCodeSendRateLimit; private String verificationCodeSendRateLimit;
@ApiModelProperty(example = "3:900 (3 requests per 15 minutes)") @ApiModelProperty(value = "Rate limit configuration for verification code checking.", example = "3:900", required = false)
@Pattern(regexp = "[1-9]\\d*:[1-9]\\d*", message = "verification code check rate limit configuration is invalid") @Pattern(regexp = "[1-9]\\d*:[1-9]\\d*", message = "verification code check rate limit configuration is invalid")
private String verificationCodeCheckRateLimit; private String verificationCodeCheckRateLimit;
@ApiModelProperty(example = "10") @ApiModelProperty(value = "Maximum number of verification failures before a user gets disabled.", example = "10", required = false)
@Min(value = 0, message = "maximum number of verification failure before user lockout must be positive") @Min(value = 0, message = "maximum number of verification failure before user lockout must be positive")
private int maxVerificationFailuresBeforeUserLockout; private int maxVerificationFailuresBeforeUserLockout;
@ApiModelProperty(value = "in seconds", example = "3600 (60 minutes)") @ApiModelProperty(value = "Total amount of time in seconds allotted for verification. " +
"Basically, this property sets a lifetime for pre-verification token. If not set, default value of 30 minutes is used.", example = "3600", required = false)
@Min(value = 1, message = "total amount of time allotted for verification must be greater than 0") @Min(value = 1, message = "total amount of time allotted for verification must be greater than 0")
private Integer totalAllowedTimeForVerification; private Integer totalAllowedTimeForVerification;

View File

@ -15,6 +15,8 @@
*/ */
package org.thingsboard.server.service.security.auth.mfa.config.account; package org.thingsboard.server.service.security.auth.mfa.config.account;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
@ -22,10 +24,12 @@ import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthPr
import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern; import javax.validation.constraints.Pattern;
@ApiModel
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Data @Data
public class SmsTwoFactorAuthAccountConfig extends OtpBasedTwoFactorAuthAccountConfig { public class SmsTwoFactorAuthAccountConfig extends OtpBasedTwoFactorAuthAccountConfig {
@ApiModelProperty(value = "Phone number to use for 2FA. Must no be blank and must be of E.164 number format.", required = true)
@NotBlank(message = "phone number cannot be blank") @NotBlank(message = "phone number cannot be blank")
@Pattern(regexp = "^\\+[1-9]\\d{1,14}$", message = "phone number is not of E.164 format") @Pattern(regexp = "^\\+[1-9]\\d{1,14}$", message = "phone number is not of E.164 format")
private String phoneNumber; private String phoneNumber;

View File

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.service.security.auth.mfa.config.account; package org.thingsboard.server.service.security.auth.mfa.config.account;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
@ -22,10 +23,12 @@ import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthPr
import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern; import javax.validation.constraints.Pattern;
@ApiModel
@Data @Data
public class TotpTwoFactorAuthAccountConfig implements TwoFactorAuthAccountConfig { public class TotpTwoFactorAuthAccountConfig implements TwoFactorAuthAccountConfig {
@ApiModelProperty(example = "otpauth://totp/ThingsBoard:tenant@thingsboard.org?issuer=ThingsBoard&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII") @ApiModelProperty(value = "OTP auth URL used to generate a QR code to scan with an authenticator app. Must not be blank and must follow specific pattern.",
example = "otpauth://totp/ThingsBoard:tenant@thingsboard.org?issuer=ThingsBoard&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII", required = true)
@NotBlank(message = "OTP auth URL cannot be blank") @NotBlank(message = "OTP auth URL cannot be blank")
@Pattern(regexp = "otpauth://totp/(\\S+?):(\\S+?)\\?issuer=(\\S+?)&secret=(\\w+?)", message = "OTP auth url is invalid") @Pattern(regexp = "otpauth://totp/(\\S+?):(\\S+?)\\?issuer=(\\S+?)&secret=(\\w+?)", message = "OTP auth url is invalid")
private String authUrl; private String authUrl;

View File

@ -22,7 +22,8 @@ import javax.validation.constraints.Min;
@Data @Data
public abstract class OtpBasedTwoFactorAuthProviderConfig implements TwoFactorAuthProviderConfig { public abstract class OtpBasedTwoFactorAuthProviderConfig implements TwoFactorAuthProviderConfig {
@ApiModelProperty(value = "in seconds", example = "60") @ApiModelProperty(value = "Verification code lifetime in seconds. Verification codes with a lifetime bigger than this param " +
"will be considered incorrect", example = "60", required = true)
@Min(value = 1, message = "verification code lifetime is required") @Min(value = 1, message = "verification code lifetime is required")
private int verificationCodeLifetime; private int verificationCodeLifetime;
} }

View File

@ -15,6 +15,8 @@
*/ */
package org.thingsboard.server.service.security.auth.mfa.config.provider; package org.thingsboard.server.service.security.auth.mfa.config.provider;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
@ -22,10 +24,13 @@ import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthPr
import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern; import javax.validation.constraints.Pattern;
@ApiModel(parent = OtpBasedTwoFactorAuthProviderConfig.class)
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Data @Data
public class SmsTwoFactorAuthProviderConfig extends OtpBasedTwoFactorAuthProviderConfig { public class SmsTwoFactorAuthProviderConfig extends OtpBasedTwoFactorAuthProviderConfig {
@ApiModelProperty(value = "SMS verification message template. Available template variables are ${verificationCode} and ${userEmail}. " +
"It must not be blank and must contain verification code variable.", required = true)
@NotBlank(message = "verification message template is required") @NotBlank(message = "verification message template is required")
@Pattern(regexp = ".*\\$\\{verificationCode}.*", message = "template must contain verification code") @Pattern(regexp = ".*\\$\\{verificationCode}.*", message = "template must contain verification code")
private String smsVerificationMessageTemplate; private String smsVerificationMessageTemplate;

View File

@ -15,14 +15,19 @@
*/ */
package org.thingsboard.server.service.security.auth.mfa.config.provider; package org.thingsboard.server.service.security.auth.mfa.config.provider;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotBlank;
@ApiModel
@Data @Data
public class TotpTwoFactorAuthProviderConfig implements TwoFactorAuthProviderConfig { public class TotpTwoFactorAuthProviderConfig implements TwoFactorAuthProviderConfig {
@ApiModelProperty(value = "Issuer name that will be displayed in an authenticator app near a username. " +
"Must not be blank.", example = "ThingsBoard", required = true)
@NotBlank(message = "issuer name must not be blank") @NotBlank(message = "issuer name must not be blank")
private String issuerName; private String issuerName;