Skip to content

Commit

Permalink
Fix #509: FIDO2 Test application: Username validation (#512)
Browse files Browse the repository at this point in the history
  • Loading branch information
jnpsk authored Sep 6, 2024
1 parent 07d125c commit b2565dd
Show file tree
Hide file tree
Showing 15 changed files with 202 additions and 10 deletions.
7 changes: 4 additions & 3 deletions powerauth-fido2-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ the PowerAuth Server on the [Developer Portal](https://developers.wultra.com/com

### PowerAuth FIDO2 Tests Configuration

| Property | Default | Note |
|-----------------------------------------------------|---------|----------------------------------------------|
| `powerauth.fido2.test.service.hideDeveloperOptions` | `false` | Whether to hide advanced settings in the UI. |
| Property | Default | Note |
|-----------------------------------------------------|---------|-----------------------------------------------------------|
| `powerauth.fido2.test.service.hideDeveloperOptions` | `false` | Whether to hide advanced settings in the UI. |
| `powerauth.fido2.test.service.emailAddressRequired` | `false` | Whether to require email address instead of any username. |

### PowerAuth Service Configuration

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
public class PowerAuthFido2TestsConfigProperties {

private boolean hideDeveloperOptions = false;
private boolean emailAddressRequired = false;

public boolean shouldHideDeveloperOptions() {
return hideDeveloperOptions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@
import com.wultra.security.powerauth.client.model.error.PowerAuthError;
import io.getlime.core.rest.model.base.response.ObjectResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import java.util.stream.Collectors;

/**
* Controller to handle exceptions.
*
Expand All @@ -48,4 +52,17 @@ public class DefaultExceptionHandler {
return new ObjectResponse<>("ERROR", error);
}

@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public @ResponseBody ObjectResponse<PowerAuthError> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
logger.error("Error occurred while processing the request: {}", ex.getMessage());
logger.debug("Exception details:", ex);
final PowerAuthError error = new PowerAuthError();
error.setCode("ERROR");
error.setMessage(ex.getBindingResult().getFieldErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(" ")));
return new ObjectResponse<>("ERROR", error);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public class HomeController {
public void addAttributes(Map<String, Object> model) {
model.put("servletContextPath", context.getContextPath());
model.put("hideDeveloperOption", powerAuthFido2TestsConfigProperties.shouldHideDeveloperOptions());
model.put("emailRequired", powerAuthFido2TestsConfigProperties.isEmailAddressRequired());
}

@GetMapping
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import com.webauthn4j.data.AuthenticatorAttachment;
import com.webauthn4j.data.PublicKeyCredentialType;
import com.wultra.security.powerauth.fido2.controller.validation.EmailConditional;
import com.wultra.security.powerauth.fido2.model.entity.AuthenticatorAttestationResponse;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
Expand All @@ -33,7 +34,7 @@ public record RegisterCredentialRequest (
@NotBlank
String applicationId,

@NotBlank
@NotBlank @EmailConditional
String userId,

boolean userVerificationRequired,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

package com.wultra.security.powerauth.fido2.controller.request;

import com.wultra.security.powerauth.fido2.controller.validation.EmailConditional;
import jakarta.validation.constraints.NotBlank;

/**
Expand All @@ -26,7 +27,7 @@
* @author Jan Pesek, [email protected]
*/
public record RegistrationOptionsRequest(
@NotBlank
@NotBlank @EmailConditional
String userId,

@NotBlank
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* PowerAuth test and related software components
* Copyright (C) 2024 Wultra s.r.o.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.wultra.security.powerauth.fido2.controller.validation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Validation annotation to validate email address. Allow null or empty values.
*
* @author Jan Pesek, [email protected]
*/
@Constraint(validatedBy = EmailConditionalValidator.class)
@Target({ ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
public @interface EmailConditional {
String message() default "Invalid email address.";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* PowerAuth test and related software components
* Copyright (C) 2024 Wultra s.r.o.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.wultra.security.powerauth.fido2.controller.validation;

import com.wultra.security.powerauth.fido2.configuration.PowerAuthFido2TestsConfigProperties;
import jakarta.validation.ConstraintValidatorContext;
import lombok.AllArgsConstructor;
import org.hibernate.validator.internal.constraintvalidators.AbstractEmailValidator;
import org.springframework.util.StringUtils;

import java.util.regex.Pattern;

/**
* Validator to validate email address. Allow null or empty values.
*
* @author Jan Pesek, [email protected]
*/
@AllArgsConstructor
public class EmailConditionalValidator extends AbstractEmailValidator<EmailConditional> {

private static final Pattern GENERIC_EMAIL_PATTERN = Pattern.compile("[^@\\s]+@[^@\\s]+\\.[^@\\s]+");

private final PowerAuthFido2TestsConfigProperties powerAuthFido2TestsConfigProperties;

@Override
public boolean isValid(final CharSequence value, final ConstraintValidatorContext context) {
if (!StringUtils.hasLength(value) || !powerAuthFido2TestsConfigProperties.isEmailAddressRequired()) {
return true;
}

return super.isValid(value, context) && GENERIC_EMAIL_PATTERN.matcher(value).matches();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ powerauth.fido2.test.service.applicationName=powerauth-fido2-tests
powerauth.fido2.test.service.applicationDisplayName=PowerAuth FIDO2 Test
powerauth.fido2.test.service.applicationEnvironment=
powerauth.fido2.test.service.hideDeveloperOptions=false
powerauth.fido2.test.service.emailAddressRequired=false

banner.application.name=${powerauth.fido2.test.service.applicationName}
banner.application.version=@project.version@
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
</head>
<body style="background: transparent">

<div class="form-wrapper" th:insert="forms/loginForm.html">
<div class="form-wrapper" th:insert="~{forms/loginForm.html}">
</div>

</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
</head>
<body style="background: transparent">

<div class="form-wrapper" th:insert="forms/paymentForm.html">
<div class="form-wrapper" th:insert="~{forms/paymentForm.html}">
</div>

</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@

<!-- User ID input -->
<div class="form-group">
<input type="text" id="userId" name="userId" autoComplete="username webauthn" th:placeholder="${hideDeveloperOption ? 'Username' : 'User ID'}" class="form-control input-text"/>
<input type="text" th:unless="${emailRequired}" id="userId" name="userId" autoComplete="username webauthn" th:placeholder="${hideDeveloperOption ? 'Username' : 'User ID'}" class="form-control input-text"/>
<input type="email" th:if="${emailRequired}" id="userId" name="userId" autoComplete="username webauthn" placeholder="Email address" class="form-control input-text" pattern="[^@\s]+@[^@\s]+\.[^@\s]+" title="Enter email address."/>
</div>

<!-- Info and login banners -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ <h2 class="headline">PowerAuth & FIDO2</h2>
</div>

<div class="col2">
<div class="form-wrapper" th:insert="forms/loginForm.html">
<div class="form-wrapper" th:insert="~{forms/loginForm.html}">
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ <h2 class="headline">PowerAuth & FIDO2</h2>
</div>

<div class="col2">
<div class="form-wrapper" th:insert="forms/paymentForm.html">
<div class="form-wrapper" th:insert="~{forms/paymentForm.html}">
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* PowerAuth test and related software components
* Copyright (C) 2024 Wultra s.r.o.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.wultra.security.powerauth.fido2.controller.validation;

import com.wultra.security.powerauth.fido2.configuration.PowerAuthFido2TestsConfigProperties;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;

/**
* Test of {@link EmailConditionalValidator}.
*
* @author Jan Pesek, [email protected]
*/
@ExtendWith(MockitoExtension.class)
class EmailConditionalValidatorTest {

@Mock(strictness = Mock.Strictness.LENIENT)
private PowerAuthFido2TestsConfigProperties properties;

@InjectMocks
private EmailConditionalValidator tested;

@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {"[email protected]", "[email protected]"})
void testValidation_emailRequired_validExamples(final String input) {
when(properties.isEmailAddressRequired()).thenReturn(true);
assertTrue(isValid(input));
}

@ParameterizedTest
@ValueSource(strings = {"abcd@abc", " "})
void testValidation_emailRequired_invalidExamples(final String input) {
when(properties.isEmailAddressRequired()).thenReturn(true);
assertFalse(isValid(input));
}

@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {"username", " ", "[email protected]"})
void testValidation_emailNotRequired(final String input) {
when(properties.isEmailAddressRequired()).thenReturn(false);
assertTrue(isValid(input));
}

private boolean isValid(final String parameter) {
return tested.isValid(parameter, null);
}

}

0 comments on commit b2565dd

Please sign in to comment.