Skip to content

Commit

Permalink
Use captcha in register & reset password endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
yebenes committed Apr 11, 2017
1 parent d8088eb commit 54f106f
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 32 deletions.
11 changes: 11 additions & 0 deletions iam/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,17 @@
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
</dependency>
<dependency>
<groupId>io.github.keetraxx</groupId>
<artifactId>recaptcha</artifactId>
<version>0.5</version>
<exclusions>
<exclusion>
<artifactId>logback-classic</artifactId>
<groupId>ch.qos.logback</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<build>
Expand Down
35 changes: 17 additions & 18 deletions iam/src/main/java/io/corbel/iam/api/UserResource.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.corbel.iam.api;

import com.google.common.net.HttpHeaders;
import io.corbel.iam.exception.DuplicatedOauthServiceIdentityException;
import io.corbel.iam.exception.IdentityAlreadyExistsException;
import io.corbel.iam.exception.UserProfileConfigurationException;
Expand All @@ -12,10 +13,7 @@
import io.corbel.iam.model.User;
import io.corbel.iam.model.UserWithIdentity;
import io.corbel.iam.repository.CreateUserException;
import io.corbel.iam.service.DeviceService;
import io.corbel.iam.service.DomainService;
import io.corbel.iam.service.IdentityService;
import io.corbel.iam.service.UserService;
import io.corbel.iam.service.*;
import io.corbel.iam.utils.Message;
import io.corbel.lib.queries.builder.ResourceQueryBuilder;
import io.corbel.lib.queries.jaxrs.QueryParameters;
Expand All @@ -28,6 +26,7 @@
import io.corbel.lib.ws.annotation.Rest;
import io.corbel.lib.ws.api.error.ErrorResponseFactory;
import io.corbel.lib.ws.auth.AuthorizationInfo;
import io.corbel.lib.ws.model.CustomHeaders;
import io.corbel.lib.ws.model.Error;
import io.dropwizard.auth.Auth;

Expand All @@ -45,16 +44,7 @@
import java.util.stream.Collectors;

import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
Expand All @@ -78,16 +68,18 @@
private final UserService userService;
private final IdentityService identityService;
private final DomainService domainService;
private final CaptchaService captchaService;
private final Clock clock;
private final DeviceService deviceService;
private final AggregationResultsFactory<JsonElement> aggregationResultsFactory;

public UserResource(UserService userService, DomainService domainService, IdentityService identityService, DeviceService deviceService,
AggregationResultsFactory<JsonElement> aggregationResultsFactory, Clock clock) {
CaptchaService captchaService, AggregationResultsFactory<JsonElement> aggregationResultsFactory, Clock clock) {
this.userService = userService;
this.domainService = domainService;
this.identityService = identityService;
this.deviceService = deviceService;
this.captchaService = captchaService;
this.clock = clock;
this.aggregationResultsFactory = aggregationResultsFactory;
}
Expand All @@ -111,7 +103,11 @@ public Response getUsers(@PathParam("domain") String domain, @Rest QueryParamete
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response postUser(@PathParam("domain") String domainId, @Valid UserWithIdentity user, @Context UriInfo uriInfo,
@Auth AuthorizationInfo authorizationInfo) {
@Auth AuthorizationInfo authorizationInfo, @HeaderParam(CustomHeaders.X_CAPTCHA) String captcha) {

if(!captchaService.verifyRequestCaptcha(domainId, captcha)) {
return IamErrorResponseFactory.getInstance().unauthorized();
}

return domainService.getDomain(domainId).map(domain -> {
user.setEmailValidated(false);
Expand Down Expand Up @@ -414,8 +410,11 @@ public Response getUserProfiles(@PathParam("domain") String domainId, @Auth Auth
@Path("/resetPassword")
@GET
public Response generateResetPasswordEmail(@PathParam("domain") String domainId, @QueryParam("email") String email,
@Auth AuthorizationInfo authorizationInfo) {
userService.sendMailResetPassword(email, authorizationInfo.getClientId(), domainId);
@HeaderParam(CustomHeaders.X_CAPTCHA) String captcha,
@Auth AuthorizationInfo authorizationInfo) {
if(captchaService.verifyRequestCaptcha(domainId, captcha)) {
userService.sendMailResetPassword(email, authorizationInfo.getClientId(), domainId);
}
return Response.noContent().build();
}

Expand Down
11 changes: 9 additions & 2 deletions iam/src/main/java/io/corbel/iam/ioc/IamIoc.java
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,10 @@ public TokenResource getTokenResource(AuthorizationService authorizationService,

@Bean
public UserResource getUserResource(UserService userService, DomainService domainService, IdentityService identityService,
DeviceService deviceService, AggregationResultsFactory aggregationResultsFactory) {
return new UserResource(userService, domainService, identityService, deviceService, aggregationResultsFactory, Clock.systemUTC());
DeviceService deviceService, CaptchaService captchaService,
AggregationResultsFactory aggregationResultsFactory) {
return new UserResource(userService, domainService, identityService, deviceService, captchaService,
aggregationResultsFactory, Clock.systemUTC());
}

@Bean
Expand Down Expand Up @@ -340,6 +342,11 @@ public UpgradeTokenService getUpgradeTokenService(ScopeService scopeService) {
return new DefaultUpgradeTokenService(getTokenUpgradeJsonTokenParser(), scopeService, userTokenRepository);
}

@Bean
public CaptchaService getCaptchaService(DomainService domainService) {
return new DefaultCaptchaService(domainService);
}

@Bean
public TokenCookieFactory getSessionCookieFactory() {
return new DefaultTokenCookieFactory(env.getProperty("token.cookie.path"), env.getProperty("token.cookie.domain"),
Expand Down
10 changes: 10 additions & 0 deletions iam/src/main/java/io/corbel/iam/service/CaptchaService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.corbel.iam.service;

/**
* @author Alberto J. Rubios
*/
public interface CaptchaService {

boolean verifyRequestCaptcha(String domainId, String captcha);

}
41 changes: 41 additions & 0 deletions iam/src/main/java/io/corbel/iam/service/DefaultCaptchaService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.corbel.iam.service;

import ch.compile.recaptcha.ReCaptchaVerify;
import ch.compile.recaptcha.model.SiteVerifyResponse;
import io.corbel.iam.api.IamErrorResponseFactory;

import javax.ws.rs.NotFoundException;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;

/**
* @author Alberto J. Rubio
*/
public class DefaultCaptchaService implements CaptchaService {

private static final String CAPTCHA_SECRET_KEY = "secret_key";
private static final String CAPTCHA_KEY = "captcha";

private final DomainService domainService;

public DefaultCaptchaService(DomainService domainService) {
this.domainService = domainService;
}

@Override
public boolean verifyRequestCaptcha(String domainId, String captcha) {
return domainService.getDomain(domainId).map(domain -> {
Map<String, String> configuration = domain.getAuthConfigurations().get(CAPTCHA_KEY);
if(configuration != null) {
ReCaptchaVerify reCaptchaVerify = new ReCaptchaVerify(configuration.get(CAPTCHA_SECRET_KEY));
try {
SiteVerifyResponse siteVerifyResponse = reCaptchaVerify.verify(captcha, null);
return siteVerifyResponse.isSuccess();
} catch (IOException e) {
return false;
}
} else return true;
}).orElse(false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@
import static org.mockito.Mockito.when;
import io.corbel.iam.model.Device;
import io.corbel.iam.model.DeviceResponse;
import io.corbel.iam.service.DeviceService;
import io.corbel.iam.service.DomainService;
import io.corbel.iam.service.IdentityService;
import io.corbel.iam.service.UserService;
import io.corbel.iam.service.*;
import io.corbel.lib.queries.exception.MalformedJsonQueryException;
import io.corbel.lib.queries.parser.AggregationParser;
import io.corbel.lib.queries.parser.PaginationParser;
Expand Down Expand Up @@ -77,6 +74,7 @@ public class UserResourceDevicesTest extends UserResourceTestBase {
private static final IdentityService identityServiceMock = mock(IdentityService.class);
private static final AuthorizationInfo authorizationInfoMock = mock(AuthorizationInfo.class);
private static final DeviceService devicesServiceMock = mock(DeviceService.class);
private static final CaptchaService captchaServiceMock = mock(CaptchaService.class);
private static final AuthorizationInfoProvider authorizationInfoProvider = new AuthorizationInfoProvider();
private static final String TEST_DEVICE_NAME = "My device name";
private static final String TEST_DEVICE_URI = "Test device URI";
Expand All @@ -96,7 +94,7 @@ public class UserResourceDevicesTest extends UserResourceTestBase {
.builder()
.addResource(
new UserResource(userServiceMock, domainServiceMock, identityServiceMock, devicesServiceMock,
aggregationResultsFactory, FIXED_CLOCK))
captchaServiceMock, aggregationResultsFactory, FIXED_CLOCK))
.addProvider(filter)
.addProvider(authorizationInfoProvider.getBinder())
.addProvider(
Expand Down
12 changes: 6 additions & 6 deletions iam/src/test/java/io/corbel/iam/api/UserResourceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.gson.JsonElement;
import io.corbel.iam.model.*;
import io.corbel.iam.service.*;
import io.corbel.lib.token.TokenInfo;
import io.corbel.lib.token.reader.TokenReader;
import io.corbel.lib.ws.gson.GsonMessageReaderWriterProvider;
Expand All @@ -35,10 +36,6 @@
import io.corbel.iam.exception.IdentityAlreadyExistsException;
import io.corbel.iam.exception.UserProfileConfigurationException;
import io.corbel.iam.repository.CreateUserException;
import io.corbel.iam.service.DeviceService;
import io.corbel.iam.service.DomainService;
import io.corbel.iam.service.IdentityService;
import io.corbel.iam.service.UserService;
import io.corbel.lib.queries.builder.ResourceQueryBuilder;
import io.corbel.lib.queries.exception.MalformedJsonQueryException;
import io.corbel.lib.queries.parser.*;
Expand Down Expand Up @@ -78,6 +75,7 @@ public class UserResourceTest extends UserResourceTestBase {
private static final TokenInfo tokenInfoMock = mock(TokenInfo.class);
private static final QueryParser queryParserMock = mock(QueryParser.class);
private static final DeviceService devicesServiceMock = mock(DeviceService.class);
private static final CaptchaService captchaServiceMock = mock(CaptchaService.class);
private static final String NOT_ALLOWED_SCOPE = "notAllowedScope";
private static final String GROUPS_PATH = "/group";
private static AggregationResultsFactory<JsonElement> aggregationResultsFactory = new JsonAggregationResultsFactory();
Expand All @@ -92,7 +90,8 @@ public class UserResourceTest extends UserResourceTestBase {
@ClassRule public static ResourceTestRule RULE = ResourceTestRule
.builder()
.addProvider(new GsonMessageReaderWriterProvider())
.addResource(new UserResource(userServiceMock, domainServiceMock, identityServiceMock, devicesServiceMock, aggregationResultsFactory, Clock.systemUTC()))
.addResource(new UserResource(userServiceMock, domainServiceMock, identityServiceMock, devicesServiceMock, captchaServiceMock,
aggregationResultsFactory, Clock.systemUTC()))
.addProvider(filter)
.addProvider(new AuthorizationInfoProvider().getBinder())
.addProvider(
Expand Down Expand Up @@ -123,6 +122,7 @@ protected ResourceTestRule getTestRule() {
public void setUp() {
reset(userServiceMock, domainServiceMock, identityServiceMock);
when(TEST_DOMAIN.getDefaultScopes()).thenReturn(ImmutableSet.of("defaultScope1", "defaultScope2"));
when(captchaServiceMock.verifyRequestCaptcha(TEST_DOMAIN_ID, null)).thenReturn(true);
when(domainServiceMock.getDomain(TEST_DOMAIN_ID)).thenReturn(Optional.of(TEST_DOMAIN));
}

Expand Down Expand Up @@ -1188,7 +1188,7 @@ public void testGetProfilesWithValidQuery() throws MalformedJsonQueryException,
public void testGenerateResetPasswordEmail() {
Response response = RULE.client().target("/v1.0/" + TEST_DOMAIN_ID + "/user/resetPassword")
.request(MediaType.APPLICATION_JSON_TYPE).header(AUTHORIZATION, "Bearer " + TEST_TOKEN).get(Response.class);

when(captchaServiceMock.verifyRequestCaptcha(TEST_DOMAIN_ID, null)).thenReturn(true);
assertThat(response.getStatus()).isEqualTo(Response.Status.NO_CONTENT.getStatusCode());
}

Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
<lib-mongodb.version>1.1.0</lib-mongodb.version>
<lib-token.version>0.9.0</lib-token.version>
<lib-queries.version>0.16.0</lib-queries.version>
<lib-ws.version>0.23.0-SNAPSHOT</lib-ws.version>
<lib-ws.version>0.23.0</lib-ws.version>
<lib-rabbitmq.version>0.9.0</lib-rabbitmq.version>
<events.version>1.32.0</events.version>
<spring.version>4.3.3.RELEASE</spring.version>
Expand Down

0 comments on commit 54f106f

Please sign in to comment.