From 6eb8a41f9a2607469b5adbd39f9fd1e046cc76ca Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Wed, 23 Mar 2022 19:38:44 +0200 Subject: [PATCH] Swagger docs for TwoFactorAuthConfigController and config classes --- .../TwoFactorAuthConfigController.java | 78 +++++++++++++++++-- .../mfa/config/TwoFactorAuthSettings.java | 16 +++- .../SmsTwoFactorAuthAccountConfig.java | 4 + .../TotpTwoFactorAuthAccountConfig.java | 5 +- .../account/TwoFactorAuthAccountConfig.java | 2 +- .../OtpBasedTwoFactorAuthProviderConfig.java | 3 +- .../SmsTwoFactorAuthProviderConfig.java | 5 ++ .../TotpTwoFactorAuthProviderConfig.java | 5 ++ 8 files changed, 104 insertions(+), 14 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java index 85991aab32..29df21bca4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java @@ -19,6 +19,8 @@ import com.google.zxing.BarcodeFormat; import com.google.zxing.client.j2se.MatrixToImageWriter; import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.QRCodeWriter; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; @@ -44,6 +46,8 @@ import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; +import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; + @RestController @RequestMapping("/api/2fa") @TbCoreComponent @@ -54,6 +58,20 @@ public class TwoFactorAuthConfigController extends BaseController { 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") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") public TwoFactorAuthAccountConfig getTwoFaAccountConfig() throws ThingsboardException { @@ -61,9 +79,29 @@ public class TwoFactorAuthConfigController extends BaseController { 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") @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(); return twoFactorAuthService.generateNewAccountConfig(user, providerType); } @@ -83,16 +121,32 @@ public class TwoFactorAuthConfigController extends BaseController { } /* 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") @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(); 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") @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 { SecurityUser user = getCurrentUser(); 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") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - public void deleteTwoFactorAuthAccountConfig() throws ThingsboardException { + public void deleteTwoFaAccountConfig() throws ThingsboardException { SecurityUser user = getCurrentUser(); 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") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - public TwoFactorAuthSettings getTwoFactorAuthSettings() throws ThingsboardException { + public TwoFactorAuthSettings getTwoFaSettings() throws ThingsboardException { 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") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - public void saveTwoFactorAuthSettings(@RequestBody TwoFactorAuthSettings twoFactorAuthSettings) throws ThingsboardException { - twoFactorAuthConfigManager.saveTwoFaSettings(getTenantId(), twoFactorAuthSettings); + public void saveTwoFaSettings(@ApiParam(value = "Settings value", required = true) + @RequestBody TwoFactorAuthSettings twoFaSettings) throws ThingsboardException { + twoFactorAuthConfigManager.saveTwoFaSettings(getTenantId(), twoFaSettings); } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java index 65e7ce2c04..a39cfd1e37 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.security.auth.mfa.config; +import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig; @@ -27,22 +28,29 @@ import java.util.List; import java.util.Optional; @Data +@ApiModel 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; + @ApiModelProperty(value = "The list of 2FA providers' configs. Users will only be allowed to use 2FA providers from this list.") @Valid private List 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") 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") 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") 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") private Integer totalAllowedTimeForVerification; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java index c921c5aafe..ece6b780a4 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java @@ -15,6 +15,8 @@ */ 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.EqualsAndHashCode; 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.Pattern; +@ApiModel @EqualsAndHashCode(callSuper = true) @Data 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") @Pattern(regexp = "^\\+[1-9]\\d{1,14}$", message = "phone number is not of E.164 format") private String phoneNumber; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java index 7c92955043..c67b1ee345 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.security.auth.mfa.config.account; +import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; 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.Pattern; +@ApiModel @Data 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") @Pattern(regexp = "otpauth://totp/(\\S+?):(\\S+?)\\?issuer=(\\S+?)&secret=(\\w+?)", message = "OTP auth url is invalid") private String authUrl; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java index d1947a72be..31ee1e2807 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java @@ -27,7 +27,7 @@ import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthPr use = JsonTypeInfo.Id.NAME, property = "providerType") @JsonSubTypes({ - @Type(name = "TOTP", value = TotpTwoFactorAuthAccountConfig.class ), + @Type(name = "TOTP", value = TotpTwoFactorAuthAccountConfig.class), @Type(name = "SMS", value = SmsTwoFactorAuthAccountConfig.class) }) public interface TwoFactorAuthAccountConfig { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java index 597c3e1e46..6b4ef92cda 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java @@ -22,7 +22,8 @@ import javax.validation.constraints.Min; @Data 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") private int verificationCodeLifetime; } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java index 8b34419041..a589905292 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java @@ -15,6 +15,8 @@ */ 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.EqualsAndHashCode; 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.Pattern; +@ApiModel(parent = OtpBasedTwoFactorAuthProviderConfig.class) @EqualsAndHashCode(callSuper = true) @Data 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") @Pattern(regexp = ".*\\$\\{verificationCode}.*", message = "template must contain verification code") private String smsVerificationMessageTemplate; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TotpTwoFactorAuthProviderConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TotpTwoFactorAuthProviderConfig.java index e1604bb618..8c0c324ae0 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TotpTwoFactorAuthProviderConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TotpTwoFactorAuthProviderConfig.java @@ -15,14 +15,19 @@ */ package org.thingsboard.server.service.security.auth.mfa.config.provider; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; import javax.validation.constraints.NotBlank; +@ApiModel @Data 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") private String issuerName;