Skip to content
This repository was archived by the owner on Dec 9, 2025. It is now read-only.

Commit b5ce980

Browse files
authored
Merge pull request #77 from appirio-tech/feature/2fa
Feature/2fa
2 parents 7266e7e + 0902a8d commit b5ce980

File tree

11 files changed

+162
-5
lines changed

11 files changed

+162
-5
lines changed

buildtokenproperties.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ SENDGRID_WELCOME_EMAIL_TEMPLATE_ID=$(eval "echo \$${ENV}_SENDGRID_WELCOME_EMAIL_
6262
SENDGRID_SELF_SERVICE_RESEND_ACTIVATION_EMAIL_TEMPLATE_ID=$(eval "echo \$${ENV}_SENDGRID_SELF_SERVICE_RESEND_ACTIVATION_EMAIL_TEMPLATE_ID")
6363
SENDGRID_SELF_SERVICE_WELCOME_EMAIL_TEMPLATE_ID=$(eval "echo \$${ENV}_SENDGRID_SELF_SERVICE_WELCOME_EMAIL_TEMPLATE_ID")
6464
SENDGRID_2FA_INVITATION_TEMPLATE_ID=$(eval "echo \$${ENV}_SENDGRID_2FA_INVITATION_TEMPLATE_ID")
65+
SENDGRID_2FA_OTP_TEMPLATE_ID=$(eval "echo \$${ENV}_SENDGRID_2FA_OTP_TEMPLATE_ID")
6566

6667

6768
if [[ -z "$ENV" ]] ; then
@@ -145,3 +146,4 @@ perl -pi -e "s/\{\{SENDGRID_WELCOME_EMAIL_TEMPLATE_ID\}\}/$SENDGRID_WELCOME_EMAI
145146
perl -pi -e "s/\{\{SENDGRID_SELF_SERVICE_RESEND_ACTIVATION_EMAIL_TEMPLATE_ID\}\}/$SENDGRID_SELF_SERVICE_RESEND_ACTIVATION_EMAIL_TEMPLATE_ID/g" $CONFFILENAME
146147
perl -pi -e "s/\{\{SENDGRID_SELF_SERVICE_WELCOME_EMAIL_TEMPLATE_ID\}\}/$SENDGRID_SELF_SERVICE_WELCOME_EMAIL_TEMPLATE_ID/g" $CONFFILENAME
147148
perl -pi -e "s/\{\{SENDGRID_2FA_INVITATION_TEMPLATE_ID\}\}/$SENDGRID_2FA_INVITATION_TEMPLATE_ID/g" $CONFFILENAME
149+
perl -pi -e "s/\{\{SENDGRID_2FA_OTP_TEMPLATE_ID\}\}/$SENDGRID_2FA_OTP_TEMPLATE_ID/g" $CONFFILENAME

src/main/java/com/appirio/tech/core/service/identity/IdentityApplication.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ public void run(IdentityConfiguration configuration, Environment environment) th
243243
userResource.setSendgridSelfServiceTemplateId(Utils.getString("sendGridSelfServiceTemplateId"));
244244
userResource.setSendgridSelfServiceWelcomeTemplateId(Utils.getString("sendGridSelfServiceWelcomeTemplateId"));
245245
userResource.setSendgrid2faInvitationTemplateId(Utils.getString("sendGrid2faInvitationTemplateId"));
246+
userResource.setSendgrid2faOtpTemplateId(Utils.getString("sendGrid2faOtpTemplateId"));
246247
// this secret _used_ to be different from the one used in AuthorizationResource.
247248
// it _was_ the secret x2. (userResource.setSecret(getSecret()+getSecret());)
248249
// we assume this was done to further limit the usability of the oneTimeToken generated in userResource

src/main/java/com/appirio/tech/core/service/identity/clients/EventBusServiceClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,9 @@ public void reFireEvent(EventMessage eventMessage) {
8282
String authToken = Utils.generateAuthToken(m2mAuthConfiguration);
8383

8484
eventMessage.setOriginator(this.config.getAdditionalConfiguration().get("originator"));
85+
LOGGER.info("Fire event {}", new ObjectMapper().writer().writeValueAsString(eventMessage));
8586
Response response = request.header("Authorization", "Bearer " + authToken).post(Entity.entity(eventMessage.getData(), MediaType.APPLICATION_JSON_TYPE));
8687

87-
LOGGER.info("Fire event {}", new ObjectMapper().writer().writeValueAsString(eventMessage));
8888
if (response.getStatusInfo().getStatusCode() != HttpStatus.OK_200 && response.getStatusInfo().getStatusCode()!= HttpStatus.NO_CONTENT_204) {
8989
LOGGER.error("Unable to fire the event: {}", response);
9090
}

src/main/java/com/appirio/tech/core/service/identity/dao/UserDAO.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,20 @@ public abstract class UserDAO implements DaoBase<User>, Transactional<UserDAO> {
168168
"WHERE user_id=:userId")
169169
public abstract int update2faByUserId(@Bind("userId") long userId, @Bind("enabled") boolean enabled, @Bind("verified") boolean verified);
170170

171+
@SqlUpdate(
172+
"UPDATE common_oltp.user_2fa SET " +
173+
"otp=:otp, " +
174+
"otp_expire=current_timestamp + (5 ||' minutes')::interval " +
175+
"WHERE id=:id")
176+
public abstract int update2faOtp(@Bind("id") long id, @Bind("otp") String otp);
177+
178+
@SqlUpdate(
179+
"UPDATE common_oltp.user_2fa SET otp=null, otp_expire=null " +
180+
"FROM (SELECT id, otp, otp_expire FROM common_oltp.user_2fa WHERE user_id=:userId FOR UPDATE)y " +
181+
"WHERE x.id=y.id " +
182+
"RETURNING CASE WHEN y.otp=:otp and y.otp_expire > current_timestamp THEN 1 ELSE 0 END")
183+
public abstract int verify2faOtp(@Bind("userId") long userId, @Bind("otp") String otp);
184+
171185
@RegisterMapperFactory(TCBeanMapperFactory.class)
172186
@SqlQuery(
173187
"SELECT " + USER_COLUMNS + ", " +
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.appirio.tech.core.service.identity.representation;
2+
3+
public class UserOtp {
4+
5+
private Long userId;
6+
private String otp;
7+
8+
public Long getUserId() {
9+
return userId;
10+
}
11+
12+
public void setUserId(Long userId) {
13+
this.userId = userId;
14+
}
15+
16+
public String getOtp() {
17+
return otp;
18+
}
19+
20+
public void setOtp(String otp) {
21+
this.otp = otp;
22+
}
23+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.appirio.tech.core.service.identity.representation;
2+
3+
public class UserOtpResponse {
4+
5+
private Boolean verified;
6+
7+
public Boolean getVerified() {
8+
return verified;
9+
}
10+
11+
public void setVerified(Boolean verified) {
12+
this.verified = verified;
13+
}
14+
15+
}

src/main/java/com/appirio/tech/core/service/identity/resource/UserResource.java

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@
6060
import com.appirio.tech.core.service.identity.representation.Credential;
6161
import com.appirio.tech.core.service.identity.representation.CredentialRequest;
6262
import com.appirio.tech.core.service.identity.representation.User2fa;
63+
import com.appirio.tech.core.service.identity.representation.UserOtp;
64+
import com.appirio.tech.core.service.identity.representation.UserOtpResponse;
6365
import com.appirio.tech.core.service.identity.representation.Email;
6466
import com.appirio.tech.core.service.identity.representation.ProviderType;
6567
import com.appirio.tech.core.service.identity.representation.Role;
@@ -124,6 +126,8 @@ public class UserResource implements GetResource<User>, DDLResource<User> {
124126
private String sendgridSelfServiceWelcomeTemplateId;
125127

126128
private String sendgrid2faInvitationTemplateId;
129+
130+
private String sendgrid2faOtpTemplateId;
127131

128132
protected UserDAO userDao;
129133

@@ -1527,7 +1531,6 @@ public ApiResponse updateUser2fa(
15271531

15281532
Long userId = Utils.toLongValue(id);
15291533

1530-
logger.info(String.format("findUserById(%s)", resourceId));
15311534
User2fa user2faInDb = userDao.findUser2faById(userId);
15321535
if (user2faInDb == null)
15331536
throw new APIRuntimeException(SC_NOT_FOUND, MSG_TEMPLATE_USER_NOT_FOUND);
@@ -1559,7 +1562,7 @@ public ApiResponse updateUser2fa(
15591562
String.format("Got unexpected response from remote service. %d %s", response.getStatusCode(),
15601563
response.getMessage()));
15611564
}
1562-
logger.info(response.getText());
1565+
logger.info("Connection created: " + response.getText());
15631566
send2faInvitationEmailEvent(user2faInDb, diceAuth.getDiceUrl() + "/verify/" + response.getText());
15641567
}
15651568

@@ -1679,6 +1682,65 @@ public ApiResponse update2faVerification(
16791682
return ApiResponseFactory.createResponse("User verification updated");
16801683
}
16811684

1685+
@POST
1686+
@Path("/sendOtp")
1687+
@Timed
1688+
public ApiResponse createOtp(
1689+
@Valid PostPutRequest<UserOtp> postRequest,
1690+
@Context HttpServletRequest request) {
1691+
1692+
// checking param
1693+
checkParam(postRequest);
1694+
1695+
UserOtp userOtp = postRequest.getParam();
1696+
1697+
if (userOtp.getUserId() == null) {
1698+
throw new APIRuntimeException(SC_BAD_REQUEST, String.format(MSG_TEMPLATE_MANDATORY, "userId"));
1699+
}
1700+
logger.info(String.format("send otp to user (%d)", userOtp.getUserId()));
1701+
1702+
User2fa user2faInDb = userDao.findUser2faById(userOtp.getUserId());
1703+
if (user2faInDb == null)
1704+
throw new APIRuntimeException(SC_NOT_FOUND, MSG_TEMPLATE_USER_NOT_FOUND);
1705+
if (user2faInDb.getEnabled() == null || !user2faInDb.getEnabled()) {
1706+
throw new APIRuntimeException(SC_BAD_REQUEST, "2FA is not enabled for user");
1707+
}
1708+
String otp = Utils.generateRandomString(ALPHABET_DIGITS_EN, 6);
1709+
userDao.update2faOtp(user2faInDb.getId(), otp);
1710+
send2faCodeEmailEvent(user2faInDb, otp);
1711+
return ApiResponseFactory.createResponse("SUCCESS");
1712+
}
1713+
1714+
@POST
1715+
@Path("/checkOtp")
1716+
@Timed
1717+
public ApiResponse checkOtp(
1718+
@Valid PostPutRequest<UserOtp> postRequest,
1719+
@Context HttpServletRequest request) {
1720+
1721+
// checking param
1722+
checkParam(postRequest);
1723+
1724+
UserOtp userOtp = postRequest.getParam();
1725+
1726+
if (userOtp.getUserId() == null) {
1727+
throw new APIRuntimeException(SC_BAD_REQUEST, String.format(MSG_TEMPLATE_MANDATORY, "userId"));
1728+
}
1729+
if (userOtp.getOtp() == null || userOtp.getOtp().length() == 0) {
1730+
throw new APIRuntimeException(SC_BAD_REQUEST, String.format(MSG_TEMPLATE_MANDATORY, "otp"));
1731+
}
1732+
logger.info(String.format("verify otp for user (%d)", userOtp.getUserId()));
1733+
1734+
int result = userDao.verify2faOtp(userOtp.getUserId(), userOtp.getOtp());
1735+
UserOtpResponse response = new UserOtpResponse();
1736+
if (result == 1) {
1737+
response.setVerified(true);
1738+
} else {
1739+
response.setVerified(false);
1740+
}
1741+
return ApiResponseFactory.createResponse(response);
1742+
}
1743+
16821744
@POST
16831745
@Path("/oneTimeToken")
16841746
@Timed
@@ -2010,6 +2072,14 @@ public void setSendgrid2faInvitationTemplateId(String sendgrid2faInvitationTempl
20102072
this.sendgrid2faInvitationTemplateId = sendgrid2faInvitationTemplateId;
20112073
}
20122074

2075+
public String getSendgrid2faOtpTemplateId() {
2076+
return sendgrid2faOtpTemplateId;
2077+
}
2078+
2079+
public void setSendgrid2faOtpTemplateId(String sendgrid2faOtpTemplateId) {
2080+
this.sendgrid2faOtpTemplateId = sendgrid2faOtpTemplateId;
2081+
}
2082+
20132083
public String getSecret() {
20142084
return secret;
20152085
}
@@ -2123,6 +2193,34 @@ private void send2faInvitationEmailEvent(User2fa user, String inviteLink) {
21232193
this.eventBusServiceClient.reFireEvent(msg);
21242194
}
21252195

2196+
private void send2faCodeEmailEvent(User2fa user, String code) {
2197+
2198+
EventMessage msg = EventMessage.getDefault();
2199+
msg.setTopic("external.action.email");
2200+
2201+
Map<String,Object> payload = new LinkedHashMap<String,Object>();
2202+
Map<String,Object> data = new LinkedHashMap<String,Object>();
2203+
data.put("handle", user.getHandle());
2204+
data.put("code", code);
2205+
2206+
payload.put("data", data);
2207+
2208+
Map<String,Object> from = new LinkedHashMap<String,Object>();
2209+
from.put("email", String.format("Topcoder <noreply@%s>", getDomain()));
2210+
payload.put("from", from);
2211+
2212+
payload.put("version", "v3");
2213+
payload.put("sendgrid_template_id", this.getSendgrid2faOtpTemplateId());
2214+
2215+
ArrayList<String> recipients = new ArrayList<String>();
2216+
recipients.add(user.getEmail());
2217+
2218+
payload.put("recipients", recipients);
2219+
2220+
msg.setPayload(payload);
2221+
this.eventBusServiceClient.reFireEvent(msg);
2222+
}
2223+
21262224
private void sendWelcomeEmailEvent(User user) {
21272225

21282226
EventMessage msg = EventMessage.getDefault();

src/main/java/com/appirio/tech/core/service/identity/util/auth/DICEAuth.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,8 @@ public String getToken() throws Exception {
167167
}
168168
}
169169
if (cachedToken == null || isCachedTokenExpired) {
170-
Response response = new Request(
171-
"https://login.microsoftonline.com/" + getTenant() + "/oauth2/v2.0/token", "POST")
170+
String url = "https://login.microsoftonline.com/" + getTenant() + "/oauth2/v2.0/token";
171+
Response response = new Request(url, "POST")
172172
.param("grant_type", "password")
173173
.param("username", getUsername())
174174
.param("password", getPassword())
@@ -181,6 +181,7 @@ public String getToken() throws Exception {
181181
response.getText()));
182182
}
183183
cachedToken = new ObjectMapper().readValue(response.getText(), Auth0Credential.class).getIdToken();
184+
logger.info("Fetched token from URL: " + url);
184185
}
185186
return cachedToken;
186187
}

src/main/resources/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ context:
1616
sendGridSelfServiceTemplateId: @application.sendgrid.selfservice.template.id@
1717
sendGridSelfServiceWelcomeTemplateId: @application.sendgrid.selfservice.welcome.template.id@
1818
sendGrid2faInvitationTemplateId: @application.sendgrid.2fa.invitation.template.id@
19+
sendGrid2faOtpTemplateId: @application.sendgrid.2fa.otp.template.id@
1920
jwtExpirySeconds: 600
2021
cookieExpirySeconds: 7776000
2122

token.properties.localdev

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
@application.sendgrid.selfservice.template.id@=dummy
1313
@application.sendgrid.selfservice.welcome.template.id@=dummy
1414
@application.sendgrid.2fa.invitation.template.id@=dummy
15+
@application.sendgrid.2fa.otp.template.id@=dummy
1516

1617
@ldap.host@=127.0.0.1
1718
@ldap.port@=389

0 commit comments

Comments
 (0)