diff --git a/.circleci/config.yml b/.circleci/config.yml index 232f6a5..e53c464 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -146,7 +146,7 @@ workflows: context : org-global filters: branches: - only: [dev, 'feature/jira-plat-152'] + only: [dev, 'feature/jira-plat-152', 'auth0-kt'] # Production build is executed on "master" branch only. - "build-prod": context : org-global diff --git a/build/build-image.sh b/build/build-image.sh index e7cf902..6f3cb30 100755 --- a/build/build-image.sh +++ b/build/build-image.sh @@ -25,7 +25,7 @@ VER=`date "+%Y%m%d%H%M"` # } # configure_aws_cli -aws s3 cp "s3://appirio-platform-$CONFIG/services/common/dockercfg" ~/.dockercfg +# aws s3 cp "s3://appirio-platform-$CONFIG/services/common/dockercfg" ~/.dockercfg # Elastic Beanstalk Application name # dev @@ -97,6 +97,12 @@ cat $WORK_DIR/config/sumo-template.conf | sed -e "s/@APINAME@/${SERVICE}/g" | se cat $WORK_DIR/config/sumo-sources-template.json | sed -e "s/@APINAME@/${SERVICE}/g" | sed -e "s/@CONFIG@/${CONFIG}/g" > $DOCKER_DIR/sumo-sources.json cat $WORK_DIR/config/newrelic-template.yml | sed -e "s/@APINAME@/${SERVICE}/g" | sed -e "s/@CONFIG@/${CONFIG}/g" > $DOCKER_DIR/newrelic.yml +echo "Logging into docker" +echo "############################" +DOCKER_USER=$(aws ssm get-parameter --name /$CONFIG/build/dockeruser --with-decryption --output text --query Parameter.Value) +DOCKER_PASSWD=$(aws ssm get-parameter --name /$CONFIG/build/dockercfg --with-decryption --output text --query Parameter.Value) +echo $DOCKER_PASSWD | docker login -u $DOCKER_USER --password-stdin + echo "building docker image: ${IMAGE}" docker build -t $TAG $DOCKER_DIR handle_error "docker build failed." diff --git a/buildtokenproperties.sh b/buildtokenproperties.sh index 048aa68..6fc7183 100755 --- a/buildtokenproperties.sh +++ b/buildtokenproperties.sh @@ -15,6 +15,12 @@ AUTH0_NEW_ID=$(eval "echo \$${ENV}_AUTH0_NEW_ID") AUTH0_NEW_ID_SECRET=$(eval "echo \$${ENV}_AUTH0_NEW_ID_SECRET") AUTH0_NEW_NONINTERACTIVE_ID=$(eval "echo \$${ENV}_AUTH0_NEW_NONINTERACTIVE_ID") AUTH0_NEW_NONINTERACTIVE_ID_SECRET=$(eval "echo \$${ENV}_AUTH0_NEW_NONINTERACTIVE_ID_SECRET") +DICEAUTH_DICE_URL=$(eval "echo \$${ENV}_DICEAUTH_DICE_URL") +DICEAUTH_DICE_API_URL=$(eval "echo \$${ENV}_DICEAUTH_DICE_API_URL") +DICEAUTH_DICE_VERIFIER=$(eval "echo \$${ENV}_DICEAUTH_DICE_VERIFIER") +DICEAUTH_DICE_API_KEY=$(eval "echo \$${ENV}_DICEAUTH_DICE_API_KEY") +DICEAUTH_CREDDEFID=$(eval "echo \$${ENV}_DICEAUTH_CREDDEFID") +DICEAUTH_OTP_DURATION=$(eval "echo \$${ENV}_DICEAUTH_OTP_DURATION") ZENDESK_ID=$(eval "echo \$${ENV}_ZENDESK_ID") SERVICEACC02_UID=$(eval "echo \$${ENV}_SERVICEACC02_UID") AUTH_SECRET=$(eval "echo \$${ENV}_AUTH_SECRET") @@ -33,6 +39,9 @@ M2MAUTHCONFIG_USERPROFILES_CREATE=$(eval "echo \$${ENV}_M2MAUTHCONFIG_USERPROFIL M2MAUTHCONFIG_USERPROFILES_UPDATE=$(eval "echo \$${ENV}_M2MAUTHCONFIG_USERPROFILES_UPDATE") M2MAUTHCONFIG_USERPROFILES_READ=$(eval "echo \$${ENV}_M2MAUTHCONFIG_USERPROFILES_READ") M2MAUTHCONFIG_USERPROFILES_DELETE=$(eval "echo \$${ENV}_M2MAUTHCONFIG_USERPROFILES_DELETE") +M2MAUTHCONFIG_USER2FA_ENABLE=$(eval "echo \$${ENV}_M2MAUTHCONFIG_USER2FA_ENABLE") +M2MAUTHCONFIG_USER2FA_VERIFY=$(eval "echo \$${ENV}_M2MAUTHCONFIG_USER2FA_VERIFY") +M2MAUTHCONFIG_USER2FA_CREDENTIAL=$(eval "echo \$${ENV}_M2MAUTHCONFIG_USER2FA_CREDENTIAL") DOMAIN=$(eval "echo \$${ENV}_DOMAIN") SMTP=$(eval "echo \$${ENV}_SMTP") @@ -47,6 +56,8 @@ SENDGRID_RESEND_ACTIVATION_EMAIL_TEMPLATE_ID=$(eval "echo \$${ENV}_SENDGRID_RESE SENDGRID_WELCOME_EMAIL_TEMPLATE_ID=$(eval "echo \$${ENV}_SENDGRID_WELCOME_EMAIL_TEMPLATE_ID") SENDGRID_SELF_SERVICE_RESEND_ACTIVATION_EMAIL_TEMPLATE_ID=$(eval "echo \$${ENV}_SENDGRID_SELF_SERVICE_RESEND_ACTIVATION_EMAIL_TEMPLATE_ID") SENDGRID_SELF_SERVICE_WELCOME_EMAIL_TEMPLATE_ID=$(eval "echo \$${ENV}_SENDGRID_SELF_SERVICE_WELCOME_EMAIL_TEMPLATE_ID") +SENDGRID_2FA_INVITATION_TEMPLATE_ID=$(eval "echo \$${ENV}_SENDGRID_2FA_INVITATION_TEMPLATE_ID") +SENDGRID_2FA_OTP_TEMPLATE_ID=$(eval "echo \$${ENV}_SENDGRID_2FA_OTP_TEMPLATE_ID") if [[ -z "$ENV" ]] ; then @@ -79,6 +90,12 @@ perl -pi -e "s/\{\{AUTH0_NEW_ID\}\}/$AUTH0_NEW_ID/g" $CONFFILENAME perl -pi -e "s/\{\{AUTH0_NEW_ID_SECRET\}\}/$AUTH0_NEW_ID_SECRET/g" $CONFFILENAME perl -pi -e "s/\{\{AUTH0_NEW_NONINTERACTIVE_ID\}\}/$AUTH0_NEW_NONINTERACTIVE_ID/g" $CONFFILENAME perl -pi -e "s/\{\{AUTH0_NEW_NONINTERACTIVE_ID_SECRET\}\}/$AUTH0_NEW_NONINTERACTIVE_ID_SECRET/g" $CONFFILENAME +perl -pi -e "s|\{\{DICEAUTH_DICE_URL\}\}|$DICEAUTH_DICE_URL|g" $CONFFILENAME +perl -pi -e "s|\{\{DICEAUTH_DICE_API_URL\}\}|$DICEAUTH_DICE_API_URL|g" $CONFFILENAME +perl -pi -e "s|\{\{DICEAUTH_DICE_VERIFIER\}\}|$DICEAUTH_DICE_VERIFIER|g" $CONFFILENAME +perl -pi -e "s|\{\{DICEAUTH_DICE_API_KEY\}\}|$DICEAUTH_DICE_API_KEY|g" $CONFFILENAME +perl -pi -e "s/\{\{DICEAUTH_CREDDEFID\}\}/$DICEAUTH_CREDDEFID/g" $CONFFILENAME +perl -pi -e "s/\{\{DICEAUTH_OTP_DURATION\}\}/$DICEAUTH_OTP_DURATION/g" $CONFFILENAME perl -pi -e "s/\{\{ZENDESK_KEY\}\}/$ZENDESK_KEY/g" $CONFFILENAME perl -pi -e "s/\{\{ZENDESK_ID\}\}/$ZENDESK_ID/g" $CONFFILENAME perl -pi -e "s/\{\{SERVICEACC01_CID\}\}/$SERVICEACC01_CID/g" $CONFFILENAME @@ -109,9 +126,14 @@ perl -pi -e "s|\{\{M2MAUTHCONFIG_USERPROFILES_CREATE\}\}|$M2MAUTHCONFIG_USERPROF perl -pi -e "s|\{\{M2MAUTHCONFIG_USERPROFILES_UPDATE\}\}|$M2MAUTHCONFIG_USERPROFILES_UPDATE|g" $CONFFILENAME perl -pi -e "s|\{\{M2MAUTHCONFIG_USERPROFILES_READ\}\}|$M2MAUTHCONFIG_USERPROFILES_READ|g" $CONFFILENAME perl -pi -e "s|\{\{M2MAUTHCONFIG_USERPROFILES_DELETE\}\}|$M2MAUTHCONFIG_USERPROFILES_DELETE|g" $CONFFILENAME +perl -pi -e "s|\{\{M2MAUTHCONFIG_USER2FA_ENABLE\}\}|$M2MAUTHCONFIG_USER2FA_ENABLE|g" $CONFFILENAME +perl -pi -e "s|\{\{M2MAUTHCONFIG_USER2FA_VERIFY\}\}|$M2MAUTHCONFIG_USER2FA_VERIFY|g" $CONFFILENAME +perl -pi -e "s|\{\{M2MAUTHCONFIG_USER2FA_CREDENTIAL\}\}|$M2MAUTHCONFIG_USER2FA_CREDENTIAL|g" $CONFFILENAME perl -pi -e "s/\{\{AUTH0_NEW_DOMAIN\}\}/$AUTH0_NEW_DOMAIN/g" $CONFFILENAME perl -pi -e "s/\{\{AUTH0_DOMAIN\}\}/$AUTH0_DOMAIN/g" $CONFFILENAME perl -pi -e "s/\{\{SENDGRID_RESEND_ACTIVATION_EMAIL_TEMPLATE_ID\}\}/$SENDGRID_RESEND_ACTIVATION_EMAIL_TEMPLATE_ID/g" $CONFFILENAME perl -pi -e "s/\{\{SENDGRID_WELCOME_EMAIL_TEMPLATE_ID\}\}/$SENDGRID_WELCOME_EMAIL_TEMPLATE_ID/g" $CONFFILENAME perl -pi -e "s/\{\{SENDGRID_SELF_SERVICE_RESEND_ACTIVATION_EMAIL_TEMPLATE_ID\}\}/$SENDGRID_SELF_SERVICE_RESEND_ACTIVATION_EMAIL_TEMPLATE_ID/g" $CONFFILENAME perl -pi -e "s/\{\{SENDGRID_SELF_SERVICE_WELCOME_EMAIL_TEMPLATE_ID\}\}/$SENDGRID_SELF_SERVICE_WELCOME_EMAIL_TEMPLATE_ID/g" $CONFFILENAME +perl -pi -e "s/\{\{SENDGRID_2FA_INVITATION_TEMPLATE_ID\}\}/$SENDGRID_2FA_INVITATION_TEMPLATE_ID/g" $CONFFILENAME +perl -pi -e "s/\{\{SENDGRID_2FA_OTP_TEMPLATE_ID\}\}/$SENDGRID_2FA_OTP_TEMPLATE_ID/g" $CONFFILENAME diff --git a/src/main/java/com/appirio/tech/core/service/identity/IdentityApplication.java b/src/main/java/com/appirio/tech/core/service/identity/IdentityApplication.java index c6d5175..d1520c8 100644 --- a/src/main/java/com/appirio/tech/core/service/identity/IdentityApplication.java +++ b/src/main/java/com/appirio/tech/core/service/identity/IdentityApplication.java @@ -234,13 +234,16 @@ public void run(IdentityConfiguration configuration, Environment environment) th configuration.getEventBusServiceClientConfig(), configuration.getM2mAuthConfiguration()); // Resources::users CacheService cacheService = configuration.getCache().createCacheService(); - UserResource userResource = new UserResource(userDao, roleDao, cacheService, eventProducer, eventBusServiceClient, configuration.getM2mAuthConfiguration().getUserProfiles()); + UserResource userResource = new UserResource(userDao, roleDao, cacheService, eventProducer, eventBusServiceClient, configuration.getM2mAuthConfiguration().getUserProfiles(), configuration.getM2mAuthConfiguration().getUser2fa()); userResource.setAuth0Client(configuration.getAuth0()); // TODO: constructor + userResource.setDiceAuth(configuration.getDiceAuth()); userResource.setDomain(configuration.getAuthDomain()); userResource.setSendgridTemplateId(Utils.getString("sendGridTemplateId")); userResource.setSendgridWelcomeTemplateId(Utils.getString("sendGridWelcomeTemplateId")); userResource.setSendgridSelfServiceTemplateId(Utils.getString("sendGridSelfServiceTemplateId")); userResource.setSendgridSelfServiceWelcomeTemplateId(Utils.getString("sendGridSelfServiceWelcomeTemplateId")); + userResource.setSendgrid2faInvitationTemplateId(Utils.getString("sendGrid2faInvitationTemplateId")); + userResource.setSendgrid2faOtpTemplateId(Utils.getString("sendGrid2faOtpTemplateId")); // this secret _used_ to be different from the one used in AuthorizationResource. // it _was_ the secret x2. (userResource.setSecret(getSecret()+getSecret());) // we assume this was done to further limit the usability of the oneTimeToken generated in userResource diff --git a/src/main/java/com/appirio/tech/core/service/identity/IdentityConfiguration.java b/src/main/java/com/appirio/tech/core/service/identity/IdentityConfiguration.java index db258e1..47bc920 100644 --- a/src/main/java/com/appirio/tech/core/service/identity/IdentityConfiguration.java +++ b/src/main/java/com/appirio/tech/core/service/identity/IdentityConfiguration.java @@ -11,6 +11,7 @@ import com.appirio.clients.BaseClientConfiguration; import com.appirio.tech.core.api.v3.dropwizard.APIBaseConfiguration; import com.appirio.tech.core.service.identity.util.auth.Auth0Client; +import com.appirio.tech.core.service.identity.util.auth.DICEAuth; import com.appirio.tech.core.service.identity.util.auth.ServiceAccountAuthenticatorFactory; import com.appirio.tech.core.service.identity.util.cache.CacheServiceFactory; import com.appirio.tech.core.service.identity.util.event.EventSystemFactory; @@ -61,6 +62,10 @@ public class IdentityConfiguration extends APIBaseConfiguration { @Valid @JsonProperty private Auth0Client auth0New = new Auth0Client(); + + @Valid + @JsonProperty + private DICEAuth diceAuth = new DICEAuth(); @Valid @NotNull @@ -135,6 +140,10 @@ public Auth0Client getAuth0() { public Auth0Client getAuth0New() { return auth0New; } + + public DICEAuth getDiceAuth() { + return diceAuth; + } public LDAPServiceFactory getLdap() { return ldap; diff --git a/src/main/java/com/appirio/tech/core/service/identity/M2mAuthConfiguration.java b/src/main/java/com/appirio/tech/core/service/identity/M2mAuthConfiguration.java index 8b27c23..17d0f2c 100644 --- a/src/main/java/com/appirio/tech/core/service/identity/M2mAuthConfiguration.java +++ b/src/main/java/com/appirio/tech/core/service/identity/M2mAuthConfiguration.java @@ -1,5 +1,6 @@ package com.appirio.tech.core.service.identity; +import com.appirio.tech.core.service.identity.util.m2mscope.User2faFactory; import com.appirio.tech.core.service.identity.util.m2mscope.UserProfilesFactory; import com.fasterxml.jackson.annotation.JsonProperty; import javax.validation.constraints.NotNull; @@ -65,6 +66,9 @@ public class M2mAuthConfiguration { @JsonProperty private UserProfilesFactory userProfiles = new UserProfilesFactory(); + @JsonProperty + private User2faFactory user2fa = new User2faFactory(); + public UserProfilesFactory getUserProfiles() { return userProfiles; } @@ -73,6 +77,14 @@ public void setUserProfiles(UserProfilesFactory userProfiles) { this.userProfiles = userProfiles; } + public User2faFactory getUser2fa() { + return user2fa; + } + + public void setUser2fa(User2faFactory user2fa) { + this.user2fa = user2fa; + } + /** * Get clientId * diff --git a/src/main/java/com/appirio/tech/core/service/identity/clients/EventBusServiceClient.java b/src/main/java/com/appirio/tech/core/service/identity/clients/EventBusServiceClient.java index 7cf189a..8f72b06 100644 --- a/src/main/java/com/appirio/tech/core/service/identity/clients/EventBusServiceClient.java +++ b/src/main/java/com/appirio/tech/core/service/identity/clients/EventBusServiceClient.java @@ -9,6 +9,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.ws.rs.ProcessingException; import javax.ws.rs.client.Client; import javax.ws.rs.client.Entity; import javax.ws.rs.client.Invocation; @@ -81,13 +82,17 @@ public void reFireEvent(EventMessage eventMessage) { String authToken = Utils.generateAuthToken(m2mAuthConfiguration); eventMessage.setOriginator(this.config.getAdditionalConfiguration().get("originator")); + LOGGER.info("Fire event {}", new ObjectMapper().writer().writeValueAsString(eventMessage)); Response response = request.header("Authorization", "Bearer " + authToken).post(Entity.entity(eventMessage.getData(), MediaType.APPLICATION_JSON_TYPE)); - LOGGER.info("Fire event {}", new ObjectMapper().writer().writeValueAsString(eventMessage)); if (response.getStatusInfo().getStatusCode() != HttpStatus.OK_200 && response.getStatusInfo().getStatusCode()!= HttpStatus.NO_CONTENT_204) { LOGGER.error("Unable to fire the event: {}", response); } - } catch (Exception e) { + } catch (ProcessingException e) { + if(!e.getMessage().equals("java.net.SocketTimeoutException: Read timed out")) { + LOGGER.error("Failed to fire the event: {}", e); + } + } catch (Exception e) { LOGGER.error("Failed to fire the event: {}", e); } } diff --git a/src/main/java/com/appirio/tech/core/service/identity/dao/UserDAO.java b/src/main/java/com/appirio/tech/core/service/identity/dao/UserDAO.java index 254513a..e25ee07 100644 --- a/src/main/java/com/appirio/tech/core/service/identity/dao/UserDAO.java +++ b/src/main/java/com/appirio/tech/core/service/identity/dao/UserDAO.java @@ -36,6 +36,7 @@ import com.appirio.tech.core.service.identity.representation.Achievement; import com.appirio.tech.core.service.identity.representation.Country; import com.appirio.tech.core.service.identity.representation.Credential; +import com.appirio.tech.core.service.identity.representation.User2fa; import com.appirio.tech.core.service.identity.representation.Email; import com.appirio.tech.core.service.identity.representation.GroupMembership; import com.appirio.tech.core.service.identity.representation.ProviderType; @@ -96,10 +97,12 @@ public abstract class UserDAO implements DaoBase, Transactional { @RegisterMapperFactory(TCBeanMapperFactory.class) @SqlQuery( "SELECT " + USER_COLUMNS + ", " + - "s.password AS credential$encodedPassword, e.address AS email, e.status_id AS emailStatus " + + "s.password AS credential$encodedPassword, e.address AS email, e.status_id AS emailStatus, " + + "mfa.enabled AS mfaEnabled, mfa.verified AS mfaVerified " + "FROM common_oltp.user AS u " + "LEFT OUTER JOIN common_oltp.email AS e ON u.user_id = e.user_id AND e.email_type_id = 1 AND e.primary_ind = 1 " + "LEFT OUTER JOIN common_oltp.security_user AS s ON u.user_id = s.login_id " + + "LEFT JOIN common_oltp.user_2fa mfa ON mfa.user_id = u.user_id " + "WHERE u.user_id = :id" ) public abstract User findUserById(@Bind("id") long id); @@ -107,9 +110,11 @@ public abstract class UserDAO implements DaoBase, Transactional { @RegisterMapperFactory(TCBeanMapperFactory.class) @SqlQuery( "SELECT " + USER_COLUMNS + ", " + - "e.address AS email, e.status_id AS emailStatus " + + "e.address AS email, e.status_id AS emailStatus, " + + "mfa.enabled AS mfaEnabled, mfa.verified AS mfaVerified " + "FROM common_oltp.user AS u " + "LEFT OUTER JOIN common_oltp.email AS e ON u.user_id = e.user_id AND e.email_type_id = 1 " + + "LEFT JOIN common_oltp.user_2fa mfa ON mfa.user_id = u.user_id " + "WHERE u.handle_lower = LOWER(:handle)" ) public abstract User findUserByHandle(@Bind("handle") String handle); @@ -117,17 +122,73 @@ public abstract class UserDAO implements DaoBase, Transactional { @RegisterMapperFactory(TCBeanMapperFactory.class) @SqlQuery( "SELECT " + USER_COLUMNS + ", " + - "e.address AS email, e.status_id AS emailStatus " + + "e.address AS email, e.status_id AS emailStatus, " + + "mfa.enabled AS mfaEnabled, mfa.verified AS mfaVerified " + "FROM common_oltp.user AS u JOIN common_oltp.email AS e ON e.user_id = u.user_id " + + "LEFT JOIN common_oltp.user_2fa mfa ON mfa.user_id = u.user_id " + "WHERE LOWER(e.address) = LOWER(:email)" ) public abstract List findUsersByEmail(@Bind("email") String email); + @RegisterMapperFactory(TCBeanMapperFactory.class) + @SqlQuery( + "SELECT mfa.id AS id, u.user_id AS userId, u.handle AS handle, u.first_name AS firstName, e.address AS email, mfa.enabled AS enabled, mfa.verified AS verified " + + "FROM common_oltp.user AS u JOIN common_oltp.email AS e ON e.user_id = u.user_id " + + "LEFT JOIN common_oltp.user_2fa AS mfa ON mfa.user_id = u.user_id " + + "WHERE LOWER(e.address) = LOWER(:email)" + ) + public abstract List findUser2faByEmail(@Bind("email") String email); + + @RegisterMapperFactory(TCBeanMapperFactory.class) + @SqlQuery( + "SELECT mfa.id AS id, u.user_id AS userId, u.handle AS handle, u.first_name AS firstName, e.address AS email, mfa.enabled AS enabled, mfa.verified AS verified " + + "FROM common_oltp.user AS u LEFT JOIN common_oltp.email AS e ON e.user_id = u.user_id " + + "LEFT JOIN common_oltp.user_2fa AS mfa ON mfa.user_id = u.user_id " + + "WHERE u.user_id = :userId" + ) + public abstract User2fa findUser2faById(@Bind("userId") long userId); + + @SqlUpdate( + "INSERT INTO common_oltp.user_2fa " + + "(user_id, enabled) VALUES " + + "(:userId, :enabled)") + public abstract int insertUser2fa(@Bind("userId") long userId, @Bind("enabled") boolean enabled); + + @SqlUpdate( + "UPDATE common_oltp.user_2fa SET " + + "enabled=:enabled, " + + "verified=:verified " + + "WHERE id=:id") + public abstract int update2fa(@Bind("id") long id, @Bind("enabled") boolean enabled, @Bind("verified") boolean verified); + + @SqlUpdate( + "UPDATE common_oltp.user_2fa SET " + + "enabled=:enabled, " + + "verified=:verified " + + "WHERE user_id=:userId") + public abstract int update2faByUserId(@Bind("userId") long userId, @Bind("enabled") boolean enabled, @Bind("verified") boolean verified); + + @SqlUpdate( + "UPDATE common_oltp.user_2fa SET " + + "otp=:otp, " + + "otp_expire=current_timestamp + (:duration ||' minutes')::interval " + + "WHERE id=:id") + public abstract int update2faOtp(@Bind("id") long id, @Bind("otp") String otp, @Bind("duration") int duration); + + @SqlQuery( + "UPDATE common_oltp.user_2fa x SET otp=null, otp_expire=null " + + "FROM (SELECT id, otp, otp_expire FROM common_oltp.user_2fa WHERE user_id=:userId FOR UPDATE)y " + + "WHERE x.id=y.id " + + "RETURNING CASE WHEN y.otp=:otp and y.otp_expire > current_timestamp THEN 1 ELSE 0 END") + public abstract int verify2faOtp(@Bind("userId") long userId, @Bind("otp") String otp); + @RegisterMapperFactory(TCBeanMapperFactory.class) @SqlQuery( "SELECT " + USER_COLUMNS + ", " + - "e.address AS email, e.status_id AS emailStatus " + + "e.address AS email, e.status_id AS emailStatus, " + + "mfa.enabled AS mfaEnabled, mfa.verified AS mfaVerified " + "FROM common_oltp.user AS u JOIN common_oltp.email AS e ON e.user_id = u.user_id " + + "LEFT JOIN common_oltp.user_2fa AS mfa ON mfa.user_id = u.user_id " + "WHERE e.address = :email" ) public abstract List findUsersByEmailCS(@Bind("email") String email); @@ -135,8 +196,10 @@ public abstract class UserDAO implements DaoBase, Transactional { @RegisterMapperFactory(TCBeanMapperFactory.class) @SqlQuery( "SELECT " + USER_COLUMNS + ", " + - "e.address AS email, e.status_id AS emailStatus " + + "e.address AS email, e.status_id AS emailStatus, " + + "mfa.enabled AS mfaEnabled, mfa.verified AS mfaVerified " + "FROM common_oltp.user AS u " + + "LEFT JOIN common_oltp.user_2fa AS mfa ON mfa.user_id = u.user_id " + " common_oltp.email AS e ON u.user_id = e.user_id AND e.primary_ind = 1 " + " " + " " + @@ -364,6 +427,22 @@ public User findUserByEmail(String email) { // nothing matched with email parameter in the result, returns the first one. return users.get(0); } + + public User2fa findUserCredentialByEmail(String email) { + List users = findUser2faByEmail(email); + if(users==null || users.size()==0) + return null; + + if(users.size()==1) + return users.get(0); + + for (User2fa user : users) { + if(user.getEmail().equals(email)) + return user; + } + + return users.get(0); + } /** * diff --git a/src/main/java/com/appirio/tech/core/service/identity/representation/CredentialRequest.java b/src/main/java/com/appirio/tech/core/service/identity/representation/CredentialRequest.java new file mode 100644 index 0000000..1a61ae0 --- /dev/null +++ b/src/main/java/com/appirio/tech/core/service/identity/representation/CredentialRequest.java @@ -0,0 +1,23 @@ +package com.appirio.tech.core.service.identity.representation; + +public class CredentialRequest { + + private String email; + private String connectionId; + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getConnectionId() { + return connectionId; + } + + public void setConnectionId(String connectionId) { + this.connectionId = connectionId; + } +} diff --git a/src/main/java/com/appirio/tech/core/service/identity/representation/User.java b/src/main/java/com/appirio/tech/core/service/identity/representation/User.java index dafa301..a904b92 100644 --- a/src/main/java/com/appirio/tech/core/service/identity/representation/User.java +++ b/src/main/java/com/appirio/tech/core/service/identity/representation/User.java @@ -41,6 +41,8 @@ public class User extends AbstractIdResource { private String utmMedium; private String utmCampaign; private List roles; + private Boolean mfaEnabled; + private Boolean mfaVerified; /** * Represents the ssoLogin attribute. @@ -188,6 +190,22 @@ public void setUtmCampaign(String utmCampaign) { public List getRoles() { return roles; } public void setRoles(List roles) { this.roles = roles; } + + public Boolean getMfaEnabled() { + return mfaEnabled; + } + + public void setMfaEnabled(Boolean mfaEnabled) { + this.mfaEnabled = mfaEnabled; + } + + public Boolean getMfaVerified() { + return mfaVerified; + } + + public void setMfaVerified(Boolean mfaVerified) { + this.mfaVerified = mfaVerified; + } @JsonIgnore public boolean isReferralProgramCampaign() { diff --git a/src/main/java/com/appirio/tech/core/service/identity/representation/User2fa.java b/src/main/java/com/appirio/tech/core/service/identity/representation/User2fa.java new file mode 100644 index 0000000..57da5da --- /dev/null +++ b/src/main/java/com/appirio/tech/core/service/identity/representation/User2fa.java @@ -0,0 +1,68 @@ +package com.appirio.tech.core.service.identity.representation; + +public class User2fa { + + private long id; + private long userId; + private String handle; + private String firstName; + private String email; + private Boolean enabled; + private Boolean verified; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public long getUserId() { + return userId; + } + + public void setUserId(long userId) { + this.userId = userId; + } + + public String getHandle() { + return handle; + } + + public void setHandle(String handle) { + this.handle = handle; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public Boolean getEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public Boolean getVerified() { + return verified; + } + + public void setVerified(Boolean verified) { + this.verified = verified; + } +} diff --git a/src/main/java/com/appirio/tech/core/service/identity/representation/UserOtp.java b/src/main/java/com/appirio/tech/core/service/identity/representation/UserOtp.java new file mode 100644 index 0000000..4845f63 --- /dev/null +++ b/src/main/java/com/appirio/tech/core/service/identity/representation/UserOtp.java @@ -0,0 +1,23 @@ +package com.appirio.tech.core.service.identity.representation; + +public class UserOtp { + + private Long userId; + private String otp; + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public String getOtp() { + return otp; + } + + public void setOtp(String otp) { + this.otp = otp; + } +} diff --git a/src/main/java/com/appirio/tech/core/service/identity/representation/UserOtpResponse.java b/src/main/java/com/appirio/tech/core/service/identity/representation/UserOtpResponse.java new file mode 100644 index 0000000..081d28f --- /dev/null +++ b/src/main/java/com/appirio/tech/core/service/identity/representation/UserOtpResponse.java @@ -0,0 +1,15 @@ +package com.appirio.tech.core.service.identity.representation; + +public class UserOtpResponse { + + private Boolean verified; + + public Boolean getVerified() { + return verified; + } + + public void setVerified(Boolean verified) { + this.verified = verified; + } + +} diff --git a/src/main/java/com/appirio/tech/core/service/identity/resource/UserResource.java b/src/main/java/com/appirio/tech/core/service/identity/resource/UserResource.java index a84eda0..ada5e94 100644 --- a/src/main/java/com/appirio/tech/core/service/identity/resource/UserResource.java +++ b/src/main/java/com/appirio/tech/core/service/identity/resource/UserResource.java @@ -3,15 +3,19 @@ import static com.appirio.tech.core.service.identity.util.Constants.*; import static javax.servlet.http.HttpServletResponse.*; +import com.appirio.tech.core.service.identity.util.m2mscope.User2faFactory; import com.appirio.tech.core.service.identity.util.m2mscope.UserProfilesFactory; import io.dropwizard.auth.Auth; import io.dropwizard.jersey.PATCH; import java.net.HttpURLConnection; +import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Calendar; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import java.util.LinkedHashMap; import javax.servlet.http.HttpServletRequest; @@ -54,6 +58,10 @@ import com.appirio.tech.core.service.identity.representation.Achievement; import com.appirio.tech.core.service.identity.representation.Country; import com.appirio.tech.core.service.identity.representation.Credential; +import com.appirio.tech.core.service.identity.representation.CredentialRequest; +import com.appirio.tech.core.service.identity.representation.User2fa; +import com.appirio.tech.core.service.identity.representation.UserOtp; +import com.appirio.tech.core.service.identity.representation.UserOtpResponse; import com.appirio.tech.core.service.identity.representation.Email; import com.appirio.tech.core.service.identity.representation.ProviderType; import com.appirio.tech.core.service.identity.representation.Role; @@ -61,14 +69,20 @@ import com.appirio.tech.core.service.identity.representation.UserProfile; import com.appirio.tech.core.service.identity.util.Constants; import com.appirio.tech.core.service.identity.util.Utils; +import com.appirio.tech.core.service.identity.util.HttpUtil.Request; +import com.appirio.tech.core.service.identity.util.HttpUtil.Response; import com.appirio.tech.core.service.identity.util.auth.Auth0Client; +import com.appirio.tech.core.service.identity.util.auth.DICEAuth; import com.appirio.tech.core.service.identity.util.auth.OneTimeToken; import com.appirio.tech.core.service.identity.util.cache.CacheService; import com.appirio.tech.core.service.identity.util.event.MailRepresentation; import com.appirio.tech.core.service.identity.util.event.NotificationPayload; import com.appirio.tech.core.service.identity.util.ldap.MemberStatus; import com.codahale.metrics.annotation.Timed; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; /** @@ -110,6 +124,10 @@ public class UserResource implements GetResource, DDLResource { private String sendgridSelfServiceTemplateId; private String sendgridSelfServiceWelcomeTemplateId; + + private String sendgrid2faInvitationTemplateId; + + private String sendgrid2faOtpTemplateId; protected UserDAO userDao; @@ -119,6 +137,8 @@ public class UserResource implements GetResource, DDLResource { private Auth0Client auth0; + private DICEAuth diceAuth; + private final EventProducer eventProducer; private ObjectMapper objectMapper = new ObjectMapper(); @@ -133,6 +153,8 @@ public class UserResource implements GetResource, DDLResource { private final EventBusServiceClient eventBusServiceClient; private final UserProfilesFactory userProfilesFactory; + + private final User2faFactory user2faFactory; /** * Create UserResource @@ -149,7 +171,8 @@ public UserResource( RoleDAO roleDao, CacheService cacheService, EventProducer eventProducer, - EventBusServiceClient eventBusServiceClient, UserProfilesFactory userProfilesFactory) { + EventBusServiceClient eventBusServiceClient, UserProfilesFactory userProfilesFactory, + User2faFactory user2faFactory) { this.userDao = userDao; this.roleDao = roleDao; this.cacheService = cacheService; @@ -161,6 +184,12 @@ public UserResource( } else { this.userProfilesFactory = userProfilesFactory; } + if (user2faFactory == null) { + // create a default one + this.user2faFactory = new User2faFactory(); + } else { + this.user2faFactory = user2faFactory; + } } /** @@ -178,7 +207,7 @@ public UserResource( CacheService cacheService, EventProducer eventProducer, EventBusServiceClient eventBusServiceClient) { - this(userDao, roleDao, cacheService, eventProducer, eventBusServiceClient, null); + this(userDao, roleDao, cacheService, eventProducer, eventBusServiceClient, null, null); } protected void setObjectMapper(ObjectMapper objectMapper) { @@ -189,6 +218,10 @@ public void setAuth0Client(Auth0Client auth0) { this.auth0 = auth0; } + public void setDiceAuth(DICEAuth diceAuth) { + this.diceAuth = diceAuth; + } + private static void checkAccessAndUserProfile(AuthUser authUser, long userId, UserProfile profile, String[] allowedScopes) { Utils.checkAccess(authUser, allowedScopes, Utils.AdminRoles); @@ -784,7 +817,7 @@ public ApiResponse roles( @FormParam("email") String email, @FormParam("handle") String handle, @Context HttpServletRequest request) throws Exception { - + logger.info("auth0 roles request."); if(Utils.isEmpty(email) && Utils.isEmpty(handle)) throw new APIRuntimeException(SC_BAD_REQUEST, String.format(MSG_TEMPLATE_MANDATORY, "email/handle")); @@ -799,13 +832,13 @@ public ApiResponse roles( if(user==null) { throw new APIRuntimeException(SC_UNAUTHORIZED, "Credentials are incorrect."); } - + logger.info("auth0 roles: user found."); List roles = null; if (user.getId() != null) { roles = roleDao.getRolesBySubjectId(Long.parseLong(user.getId().getId())); } user.setRoles(roles); - + logger.info("auth0 roles: roles assigned"); // temp - just for testing user.setRegSource(userDao.generateSSOToken(Long.parseLong(user.getId().getId()))); @@ -868,6 +901,7 @@ public ApiResponse changePassword( logger.debug(String.format("Auth0: updating password for user: %s", dbUser.getHandle())); userDao.updatePassword(dbUser); + userDao.update2faByUserId(Utils.toLongValue(dbUser.getId()), false, false); return ApiResponseFactory.createResponse("password updated successfully."); } @@ -1474,6 +1508,242 @@ public ApiResponse validateSocial( createValidationResult((err == null), err)); } + @PATCH + @Path("/{resourceId}/2fa") + @Timed + public ApiResponse updateUser2fa( + @Auth AuthUser authUser, + @PathParam("resourceId") String resourceId, + @Valid PostPutRequest postRequest, + @Context HttpServletRequest request) { + + TCID id = new TCID(resourceId); + validateResourceIdAndCheckPermission(authUser, id, user2faFactory.getEnableScopes()); + // checking param + checkParam(postRequest); + + User2fa user2fa = postRequest.getParam(); + + if (user2fa.getEnabled() == null) { + throw new APIRuntimeException(SC_BAD_REQUEST, String.format(MSG_TEMPLATE_MANDATORY, "enabled")); + } + logger.info(String.format("update user 2fa(%s) - %b", resourceId, user2fa.getEnabled())); + + Long userId = Utils.toLongValue(id); + + User2fa user2faInDb = userDao.findUser2faById(userId); + if (user2faInDb == null) + throw new APIRuntimeException(SC_NOT_FOUND, MSG_TEMPLATE_USER_NOT_FOUND); + + Boolean shouldSendInvite = false; + if (user2faInDb.getEnabled() == null) { + userDao.insertUser2fa(userId, user2fa.getEnabled()); + shouldSendInvite = user2fa.getEnabled(); + } else if (!user2faInDb.getEnabled().equals(user2fa.getEnabled())) { + userDao.update2fa(user2faInDb.getId(), user2fa.getEnabled(), false); + shouldSendInvite = user2fa.getEnabled(); + } + + if (shouldSendInvite) { + Response response; + try { + response = new Request(diceAuth.getDiceApiUrl() + "/connection/invitation", "POST") + .param("emailId", user2faInDb.getEmail()) + .header("x-api-key", diceAuth.getDiceApiKey()) + .execute(); + } catch (Exception e) { + logger.error("Error when calling 2fa submit api", e); + userDao.update2fa(user2faInDb.getId(), false, false); + throw new APIRuntimeException(SC_INTERNAL_SERVER_ERROR, "Error when calling 2fa submit api"); + } + if (response.getStatusCode() != HttpURLConnection.HTTP_CREATED) { + userDao.update2fa(user2faInDb.getId(), false, false); + throw new APIRuntimeException(HttpURLConnection.HTTP_INTERNAL_ERROR, + String.format("Got unexpected response from remote service. %d %s", response.getStatusCode(), + response.getMessage())); + } + logger.info("Connection created: " + response.getText()); + send2faInvitationEmailEvent(user2faInDb, diceAuth.getDiceUrl() + "/verify/" + response.getText()); + } + + return ApiResponseFactory.createResponse("SUCCESS"); + } + + @POST + @Path("/2faCredentials") + @Timed + public ApiResponse issueCredentials( + @Auth AuthUser authUser, + @Valid PostPutRequest postRequest, + @Context HttpServletRequest request) { + Utils.checkAccess(authUser, user2faFactory.getCredentialIssuerScopes(), Utils.AdminRoles); + checkParam(postRequest); + CredentialRequest credential = postRequest.getParam(); + + if(credential.getEmail() == null || credential.getEmail().length() == 0) { + throw new APIRuntimeException(SC_BAD_REQUEST, String.format(MSG_TEMPLATE_MANDATORY, "Email address")); + } + if(credential.getConnectionId() == null || credential.getConnectionId().length() == 0) { + throw new APIRuntimeException(SC_BAD_REQUEST, String.format(MSG_TEMPLATE_MANDATORY, "Connection Id")); + } + logger.info(String.format("issue credential (%s)", credential.getEmail())); + + // find user by email + User2fa user = userDao.findUserCredentialByEmail(credential.getEmail()); + + // return 404 if user is not found + if(user == null) + throw new APIRuntimeException(SC_NOT_FOUND, MSG_TEMPLATE_USER_NOT_FOUND); + if(user.getEnabled() == null || !user.getEnabled()) { + throw new APIRuntimeException(SC_BAD_REQUEST, "2FA is not enabled for user"); + } + List roles = roleDao.getRolesBySubjectId(user.getUserId()); + ObjectMapper mapper = new ObjectMapper(); + ObjectNode body = mapper.createObjectNode(); + body.put("comment", "TC credential"); + body.put("connection_id", credential.getConnectionId()); + body.put("cred_def_id", diceAuth.getCredDefId()); + ObjectNode preview = mapper.createObjectNode(); + preview.put("@type", diceAuth.getCredPreview()); + ArrayNode attributes = mapper.createArrayNode(); + ObjectNode name = attributes.addObject(); + ObjectNode email = attributes.addObject(); + ObjectNode role = attributes.addObject(); + ObjectNode validUntil = attributes.addObject(); + name.put("name", "Name"); + name.put("value", user.getFirstName()); + email.put("name", "Email"); + email.put("value", user.getEmail()); + role.put("name", "Role"); + role.put("value", roles.stream().map(x -> x.getRoleName()).collect(Collectors.joining(","))); + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.YEAR, 1); + validUntil.put("name", "Valid_Till"); + validUntil.put("value", new SimpleDateFormat("yyyy-MM-dd HH:mm:ssZ").format(cal.getTime())); + body.set("credential_preview", preview); + preview.set("attributes", attributes); + Response response; + try { + response = new Request(diceAuth.getDiceApiUrl() + "/cred/issuance/offer", "POST") + .header("x-api-key", diceAuth.getDiceApiKey()) + .json(mapper.writeValueAsString(body)) + .execute(); + } catch (JsonProcessingException e) { + logger.error("Error when processing JSON content", e); + throw new APIRuntimeException(SC_INTERNAL_SERVER_ERROR, "Error when calling credentialoffer api"); + } catch (Exception e) { + logger.error("Error when calling credentialoffer api", e); + throw new APIRuntimeException(SC_INTERNAL_SERVER_ERROR, "Error when calling credentialoffer api"); + } + if (response.getStatusCode() != HttpURLConnection.HTTP_CREATED) { + throw new APIRuntimeException(HttpURLConnection.HTTP_INTERNAL_ERROR, + String.format("Got unexpected response from remote service. %d %s", response.getStatusCode(), + response.getMessage())); + } + if (user.getVerified()) { + userDao.update2fa(user.getId(), true, false); + } + return ApiResponseFactory.createResponse("SUCCESS"); + } + + @PUT + @Path("/2faVerification") + @Timed + public ApiResponse update2faVerification( + @Auth AuthUser authUser, + @Valid PostPutRequest putRequest, + @Context HttpServletRequest request) { + + Utils.checkAccess(authUser, user2faFactory.getVerifyScopes(), Utils.AdminRoles); + checkParam(putRequest); + User2fa credential = putRequest.getParam(); + + if(credential.getEmail() == null || credential.getEmail().length() == 0) { + throw new APIRuntimeException(SC_BAD_REQUEST, String.format(MSG_TEMPLATE_MANDATORY, "Email address")); + } + if(credential.getVerified() == null) { + throw new APIRuntimeException(SC_BAD_REQUEST, String.format(MSG_TEMPLATE_MANDATORY, "Verified")); + } + logger.info(String.format("update 2fa verification (%s) - %b", credential.getEmail(), credential.getVerified())); + + // find user by email + User2fa credVerification = userDao.findUserCredentialByEmail(credential.getEmail()); + + // return 404 if user is not found + if(credVerification == null) + throw new APIRuntimeException(SC_NOT_FOUND, MSG_TEMPLATE_USER_NOT_FOUND); + + if(credVerification.getEnabled() == null || !credVerification.getEnabled()) { + throw new APIRuntimeException(SC_BAD_REQUEST, "2FA is not enabled for user"); + } + // update only if it's true. We need to prevent changing verification status from true to false + // Otherwise 2fa will be skipped during the login flow. + // The only way to set verification to false is disabling the 2fa for that user. + if(credential.getVerified()) { + userDao.update2fa(credVerification.getId(), true, credential.getVerified()); + } + return ApiResponseFactory.createResponse("User verification updated"); + } + + @POST + @Path("/sendOtp") + @Timed + public ApiResponse createOtp( + @Valid PostPutRequest postRequest, + @Context HttpServletRequest request) { + + // checking param + checkParam(postRequest); + + UserOtp userOtp = postRequest.getParam(); + + if (userOtp.getUserId() == null) { + throw new APIRuntimeException(SC_BAD_REQUEST, String.format(MSG_TEMPLATE_MANDATORY, "userId")); + } + logger.info(String.format("send otp to user (%d)", userOtp.getUserId())); + + User2fa user2faInDb = userDao.findUser2faById(userOtp.getUserId()); + if (user2faInDb == null) + throw new APIRuntimeException(SC_NOT_FOUND, MSG_TEMPLATE_USER_NOT_FOUND); + if (user2faInDb.getEnabled() == null || !user2faInDb.getEnabled()) { + throw new APIRuntimeException(SC_BAD_REQUEST, "2FA is not enabled for user"); + } + String otp = Utils.generateRandomString(ALPHABET_DIGITS_EN, 6); + userDao.update2faOtp(user2faInDb.getId(), otp, diceAuth.getOtpDuration()); + send2faCodeEmailEvent(user2faInDb, otp, diceAuth.getOtpDuration()); + return ApiResponseFactory.createResponse("SUCCESS"); + } + + @POST + @Path("/checkOtp") + @Timed + public ApiResponse checkOtp( + @Valid PostPutRequest postRequest, + @Context HttpServletRequest request) { + + // checking param + checkParam(postRequest); + + UserOtp userOtp = postRequest.getParam(); + + if (userOtp.getUserId() == null) { + throw new APIRuntimeException(SC_BAD_REQUEST, String.format(MSG_TEMPLATE_MANDATORY, "userId")); + } + if (userOtp.getOtp() == null || userOtp.getOtp().length() == 0) { + throw new APIRuntimeException(SC_BAD_REQUEST, String.format(MSG_TEMPLATE_MANDATORY, "otp")); + } + logger.info(String.format("verify otp for user (%d)", userOtp.getUserId())); + + int result = userDao.verify2faOtp(userOtp.getUserId(), userOtp.getOtp()); + UserOtpResponse response = new UserOtpResponse(); + if (result == 1) { + response.setVerified(true); + } else { + response.setVerified(false); + } + return ApiResponseFactory.createResponse(response); + } + @POST @Path("/oneTimeToken") @Timed @@ -1797,6 +2067,22 @@ public void setSendgridSelfServiceTemplateId(String sendgridSelfServiceTemplateI this.sendgridSelfServiceTemplateId = sendgridSelfServiceTemplateId; } + public String getSendgrid2faInvitationTemplateId() { + return sendgrid2faInvitationTemplateId; + } + + public void setSendgrid2faInvitationTemplateId(String sendgrid2faInvitationTemplateId) { + this.sendgrid2faInvitationTemplateId = sendgrid2faInvitationTemplateId; + } + + public String getSendgrid2faOtpTemplateId() { + return sendgrid2faOtpTemplateId; + } + + public void setSendgrid2faOtpTemplateId(String sendgrid2faOtpTemplateId) { + this.sendgrid2faOtpTemplateId = sendgrid2faOtpTemplateId; + } + public String getSecret() { return secret; } @@ -1881,6 +2167,64 @@ private void sendActivationEmailEvent(User user, String redirectUrl) { } } + private void send2faInvitationEmailEvent(User2fa user, String inviteLink) { + + EventMessage msg = EventMessage.getDefault(); + msg.setTopic("external.action.email"); + + Map payload = new LinkedHashMap(); + Map data = new LinkedHashMap(); + data.put("handle", user.getHandle()); + data.put("link", inviteLink); + data.put("verifier", diceAuth.getDiceVerifier()); + + payload.put("data", data); + + Map from = new LinkedHashMap(); + from.put("email", String.format("Topcoder ", getDomain())); + payload.put("from", from); + + payload.put("version", "v3"); + payload.put("sendgrid_template_id", this.getSendgrid2faInvitationTemplateId()); + + ArrayList recipients = new ArrayList(); + recipients.add(user.getEmail()); + + payload.put("recipients", recipients); + + msg.setPayload(payload); + this.eventBusServiceClient.reFireEvent(msg); + } + + private void send2faCodeEmailEvent(User2fa user, String code, Integer duration) { + + EventMessage msg = EventMessage.getDefault(); + msg.setTopic("external.action.email"); + + Map payload = new LinkedHashMap(); + Map data = new LinkedHashMap(); + data.put("handle", user.getHandle()); + data.put("code", code); + data.put("duration", duration); + + payload.put("data", data); + + Map from = new LinkedHashMap(); + from.put("email", String.format("Topcoder ", getDomain())); + payload.put("from", from); + + payload.put("version", "v3"); + payload.put("sendgrid_template_id", this.getSendgrid2faOtpTemplateId()); + + ArrayList recipients = new ArrayList(); + recipients.add(user.getEmail()); + + payload.put("recipients", recipients); + + msg.setPayload(payload); + this.eventBusServiceClient.reFireEvent(msg); + } + private void sendWelcomeEmailEvent(User user) { EventMessage msg = EventMessage.getDefault(); diff --git a/src/main/java/com/appirio/tech/core/service/identity/util/auth/DICEAuth.java b/src/main/java/com/appirio/tech/core/service/identity/util/auth/DICEAuth.java new file mode 100644 index 0000000..3d21257 --- /dev/null +++ b/src/main/java/com/appirio/tech/core/service/identity/util/auth/DICEAuth.java @@ -0,0 +1,95 @@ +package com.appirio.tech.core.service.identity.util.auth; + +import javax.validation.constraints.NotNull; + +public class DICEAuth { + + @NotNull + private String diceUrl; + + @NotNull + private String diceApiUrl; + + @NotNull + private String diceVerifier; + + @NotNull + private String diceApiKey; + + @NotNull + private String credDefId; + + @NotNull + private Integer otpDuration; + + private String credPreview = "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/issue-credential/1.0/credential-preview"; + + public DICEAuth() { + } + + public DICEAuth(String diceUrl, String diceApiUrl, String diceVerifier, String diceApiKey, String credDefId, + Integer otpDuration) { + this.diceUrl = diceUrl; + this.diceApiUrl = diceApiUrl; + this.diceVerifier = diceVerifier; + this.diceApiKey = diceApiKey; + this.credDefId = credDefId; + this.otpDuration = otpDuration; + } + + public String getDiceUrl() { + return diceUrl; + } + + public void setDiceUrl(String diceUrl) { + this.diceUrl = diceUrl; + } + + public String getDiceApiUrl() { + return diceApiUrl; + } + + public void setDiceApiUrl(String diceApiUrl) { + this.diceApiUrl = diceApiUrl; + } + + public String getDiceVerifier() { + return diceVerifier; + } + + public void setDiceVerifier(String diceVerifier) { + this.diceVerifier = diceVerifier; + } + + public String getDiceApiKey() { + return diceApiKey; + } + + public void setDiceApiKey(String diceApiKey) { + this.diceApiKey = diceApiKey; + } + + public String getCredDefId() { + return credDefId; + } + + public void setCredDefId(String credDefId) { + this.credDefId = credDefId; + } + + public Integer getOtpDuration() { + return otpDuration; + } + + public void setOtpDuration(Integer otpDuration) { + this.otpDuration = otpDuration; + } + + public String getCredPreview() { + return credPreview; + } + + public void setCredPreview(String credPreview) { + this.credPreview = credPreview; + } +} diff --git a/src/main/java/com/appirio/tech/core/service/identity/util/m2mscope/User2faFactory.java b/src/main/java/com/appirio/tech/core/service/identity/util/m2mscope/User2faFactory.java new file mode 100644 index 0000000..ff8bf37 --- /dev/null +++ b/src/main/java/com/appirio/tech/core/service/identity/util/m2mscope/User2faFactory.java @@ -0,0 +1,110 @@ +package com.appirio.tech.core.service.identity.util.m2mscope; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * the configurationn for scopes of user 2fa. + */ +public class User2faFactory { + + public static final String SCOPE_DELIMITER = ","; + + /** + * Represents the create scopes for machine token validation. + */ + public static final String[] EnableScopes = { "enable:user_2fa", "all:user_2fa" }; + + /** + * Represents the create scopes for machine token validation. + */ + public static final String[] VerifyScopes = { "verify:user_2fa", "all:user_2fa" }; + + /** + * Represents the update scopes for machine token validation. + */ + public static final String[] CredentialIssuerScopes = { "cred:user_2fa", "all:user_2fa" }; + + /** + * Represents the enable attribute + */ + @JsonProperty + private String enable; + + /** + * Represents the verify attribute + */ + @JsonProperty + private String verify; + + /** + * Represents the credential attribute + */ + @JsonProperty + private String credential; + + public User2faFactory() { + } + + public String getEnable() { + return enable; + } + + public void setEnable(String enable) { + this.enable = enable; + } + + public String getVerify() { + return verify; + } + + public void SetVerify(String verify) { + this.verify = verify; + } + + public String getCredential() { + return credential; + } + + public void setCredential(String credential) { + this.credential = credential; + } + + /** + * Gets the enable scopes. + * + * @return the enable scopes. + */ + public String[] getEnableScopes() { + if (enable != null && enable.trim().length() != 0) { + return enable.split(SCOPE_DELIMITER); + } + + return EnableScopes; + } + + /** + * Gets the verify scopes. + * + * @return the verify scopes. + */ + public String[] getVerifyScopes() { + if (verify != null && verify.trim().length() != 0) { + return verify.split(SCOPE_DELIMITER); + } + + return VerifyScopes; + } + + /** + * Gets the credential issuer scopes. + * + * @return the credential issuer scopes. + */ + public String[] getCredentialIssuerScopes() { + if (credential != null && credential.trim().length() != 0) { + return credential.split(SCOPE_DELIMITER); + } + + return CredentialIssuerScopes; + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index fe805ea..8f398c6 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -15,6 +15,8 @@ context: sendGridWelcomeTemplateId: @application.sendgrid.welcome.template.id@ sendGridSelfServiceTemplateId: @application.sendgrid.selfservice.template.id@ sendGridSelfServiceWelcomeTemplateId: @application.sendgrid.selfservice.welcome.template.id@ + sendGrid2faInvitationTemplateId: @application.sendgrid.2fa.invitation.template.id@ + sendGrid2faOtpTemplateId: @application.sendgrid.2fa.otp.template.id@ jwtExpirySeconds: 600 cookieExpirySeconds: 7776000 @@ -91,6 +93,13 @@ auth0New: nonInteractiveClientId : @auth0.new.nonInteractive.clientId@ nonInteractiveClientSecret: @auth0.new.nonInteractive.clientSecret@ +diceAuth: + diceUrl: @diceAuth.diceUrl@ + diceApiUrl: @diceAuth.diceApiUrl@ + diceVerifier: @diceAuth.diceVerifier@ + diceApiKey: @diceAuth.diceApiKey@ + credDefId: @diceAuth.credDefId@ + otpDuration: @diceAuth.otpDuration@ # Authorized accounts serviceAccount: @@ -156,6 +165,10 @@ m2mAuthConfig: read: @m2mAuthConfig.userProfiles.read@ update: @m2mAuthConfig.userProfiles.update@ delete: @m2mAuthConfig.userProfiles.delete@ + user2fa: + enable: @m2mAuthConfig.user2fa.enable@ + verify: @m2mAuthConfig.user2fa.verify@ + credential: @m2mAuthConfig.user2fa.credential@ # Server settings server: diff --git a/src/main/resources/config.yml.localdev b/src/main/resources/config.yml.localdev index 7df95c7..6cf3a94 100644 --- a/src/main/resources/config.yml.localdev +++ b/src/main/resources/config.yml.localdev @@ -85,6 +85,14 @@ auth0New: nonInteractiveClientId : AUTH0-NI-CLIENT-ID nonInteractiveClientSecret: AUTH0-NI-CLIENT-SECRET +diceAuth: + diceUrl: dummy + diceApiUrl: dummy + diceVerifier: dummy + diceApiKey: dummy + credDefId: dummy + otpDuration: 10 + # LDAP Settings ldap: host: ${DOCKER_IP} @@ -156,6 +164,10 @@ m2mAuthConfig: read: read:user_profiles,all:user_profiles update: update:user_profiles,all:user_profiles delete: delete:user_profiles,all:user_profiles + user2fa: + enable: enable:user-2fa,all:user-2fa + verify: verify:user-2fa,all:user-2fa + credential: cred:user-2fa,all:user-2fa # Server settings server: diff --git a/src/main/resources/sql/User_2fa_Create.sql b/src/main/resources/sql/User_2fa_Create.sql new file mode 100644 index 0000000..98efdc3 --- /dev/null +++ b/src/main/resources/sql/User_2fa_Create.sql @@ -0,0 +1 @@ +CREATE TABLE common_oltp.user_2fa (id SERIAL NOT NULL, user_id NUMERIC(10,0) NOT NULL, enabled BOOLEAN DEFAULT false NOT NULL, verified BOOLEAN DEFAULT false NOT NULL, otp CHARACTER VARYING(6), otp_expire TIMESTAMP(6) WITHOUT TIME ZONE, CONSTRAINT user_2fa_pk PRIMARY KEY (id), CONSTRAINT user_2fa_user_id_fkey FOREIGN KEY (user_id) REFERENCES "user" ("user_id"), UNIQUE (user_id)); diff --git a/src/test/java/com/appirio/tech/core/service/identity/resource/UserResourceTest.java b/src/test/java/com/appirio/tech/core/service/identity/resource/UserResourceTest.java index fc6164a..e5a4646 100644 --- a/src/test/java/com/appirio/tech/core/service/identity/resource/UserResourceTest.java +++ b/src/test/java/com/appirio/tech/core/service/identity/resource/UserResourceTest.java @@ -27,6 +27,7 @@ import com.appirio.tech.core.service.identity.util.cache.CacheService; import com.appirio.tech.core.service.identity.util.event.MailRepresentation; import com.appirio.tech.core.service.identity.util.ldap.MemberStatus; +import com.appirio.tech.core.service.identity.util.m2mscope.User2faFactory; import com.appirio.tech.core.service.identity.util.m2mscope.UserProfilesFactory; import com.fasterxml.jackson.databind.ObjectMapper; @@ -68,6 +69,8 @@ public class UserResourceTest { private final RoleDAO mockRoleDao = mock(RoleDAO.class); private final UserProfilesFactory userProfilesFactory = new UserProfilesFactory(); + + private final User2faFactory user2faFactory = new User2faFactory(); @Before @SuppressWarnings("serial") @@ -144,7 +147,7 @@ public void testCreateSSOUserLogin() throws Exception { doNothing().when(eventProducer).publish(anyString(), anyString()); ObjectMapper objectMapper = mock(ObjectMapper.class); when(objectMapper.writeValueAsString(anyObject())).thenReturn("payload"); - UserResource testee = spy(new UserResource(userDao, mockRoleDao, cache, eventProducer, null, userProfilesFactory)); + UserResource testee = spy(new UserResource(userDao, mockRoleDao, cache, eventProducer, null, userProfilesFactory, user2faFactory)); // Creating mock: PostPutRequest - give mock user UserProfile userProfile = new UserProfile(); diff --git a/token.properties.localdev b/token.properties.localdev index 301e55b..3b6096b 100644 --- a/token.properties.localdev +++ b/token.properties.localdev @@ -7,6 +7,13 @@ @auth.secret@=AUTH_SECRET +@application.sendgrid.template.id@=dummy +@application.sendgrid.welcome.template.id@=dummy +@application.sendgrid.selfservice.template.id@=dummy +@application.sendgrid.selfservice.welcome.template.id@=dummy +@application.sendgrid.2fa.invitation.template.id@=dummy +@application.sendgrid.2fa.otp.template.id@=dummy + @ldap.host@=127.0.0.1 @ldap.port@=389 @ldap.password@=dummy @@ -24,6 +31,13 @@ @auth0.new.clientSecret@= @auth0.new.domain@=dummy.auth0.com +@diceAuth.diceUrl@=dummy +@diceAuth.diceApiUrl@=dummy +@diceAuth.diceVerifier@=dummy +@diceAuth.diceApiKey@=dummy +@diceAuth.credDefId@=dummy +@diceAuth.otpDuration@=10 + @zendesk.secret@=ZENDESK_SECRET @zendesk.idprefix@=ZENDESK_PREFIX @@ -59,3 +73,6 @@ @m2mAuthConfig.userProfiles.read@=read:user_profiles,all:user_profiles @m2mAuthConfig.userProfiles.update@=update:user_profiles,all:user_profiles @m2mAuthConfig.userProfiles.delete@=delete:user_profiles,all:user_profiles +@m2mAuthConfig.user2fa.enable@=enable:user_2fa,all:user_2fa +@m2mAuthConfig.user2fa.verify@=verify:user_2fa,all:user_2fa +@m2mAuthConfig.user2fa.credential@=cred:user_2fa,all:user_2fa \ No newline at end of file diff --git a/token.properties.template b/token.properties.template index 8ff9523..1e2d82d 100644 --- a/token.properties.template +++ b/token.properties.template @@ -12,6 +12,8 @@ @application.sendgrid.welcome.template.id@={{SENDGRID_WELCOME_EMAIL_TEMPLATE_ID}} @application.sendgrid.selfservice.template.id@={{SENDGRID_SELF_SERVICE_RESEND_ACTIVATION_EMAIL_TEMPLATE_ID}} @application.sendgrid.selfservice.welcome.template.id@={{SENDGRID_SELF_SERVICE_WELCOME_EMAIL_TEMPLATE_ID}} +@application.sendgrid.2fa.invitation.template.id@={{SENDGRID_2FA_INVITATION_TEMPLATE_ID}} +@application.sendgrid.2fa.otp.template.id@={{SENDGRID_2FA_OTP_TEMPLATE_ID}} @ldap.host@={{LDAP_SERVER}} @ldap.port@=389 @@ -49,6 +51,13 @@ @auth0.new.nonInteractive.clientSecret@={{AUTH0_NEW_NONINTERACTIVE_ID_SECRET}} @auth0.new.domain@={{AUTH0_NEW_DOMAIN}} +@diceAuth.diceUrl@={{DICEAUTH_DICE_URL}} +@diceAuth.diceApiUrl@={{DICEAUTH_DICE_API_URL}} +@diceAuth.diceVerifier@={{DICEAUTH_DICE_VERIFIER}} +@diceAuth.diceApiKey@={{DICEAUTH_DICE_API_KEY}} +@diceAuth.credDefId@={{DICEAUTH_CREDDEFID}} +@diceAuth.otpDuration@={{DICEAUTH_OTP_DURATION}} + @zendesk.secret@={{ZENDESK_KEY}} @zendesk.idprefix@={{ZENDESK_ID}} @@ -86,3 +95,6 @@ @m2mAuthConfig.userProfiles.read@={{M2MAUTHCONFIG_USERPROFILES_READ}} @m2mAuthConfig.userProfiles.update@={{M2MAUTHCONFIG_USERPROFILES_UPDATE}} @m2mAuthConfig.userProfiles.delete@={{M2MAUTHCONFIG_USERPROFILES_DELETE}} +@m2mAuthConfig.user2fa.enable@={{M2MAUTHCONFIG_USER2FA_ENABLE}} +@m2mAuthConfig.user2fa.verify@={{M2MAUTHCONFIG_USER2FA_VERIFY}} +@m2mAuthConfig.user2fa.credential@={{M2MAUTHCONFIG_USER2FA_CREDENTIAL}}