Skip to content

Commit

Permalink
Merge pull request #578 from KrutikaPhirangi/api-secret
Browse files Browse the repository at this point in the history
HCX-796 : API access secret regenerate and expiry
  • Loading branch information
shiva-rakshith authored Nov 28, 2023
2 parents b97a762 + ffe267d commit c869e10
Show file tree
Hide file tree
Showing 14 changed files with 713 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.swasth.common.exception.ClientException;
import org.swasth.common.helpers.EventGenerator;
import org.swasth.kafka.client.KafkaClient;
import org.swasth.postgresql.IDatabaseService;

import javax.ws.rs.core.Response;
import java.security.SecureRandom;
Expand All @@ -35,6 +36,10 @@ public class KeycloakApiAccessService {
private String keycloakAdminUserName;
@Value("${keycloak.master-realm}")
private String keycloakMasterRealm;
@Value("${postgres.api-access-secrets-expiry-table}")
private String apiAccessTable;
@Value("${api-access-secret.expiry-days}")
private int secretExpiryDays;
@Value("${keycloak.protocol-access-realm}")
private String keycloackProtocolAccessRealm;
@Value("${keycloak.admin-client-id}")
Expand All @@ -48,6 +53,8 @@ public class KeycloakApiAccessService {
@Autowired
private KafkaClient kafkaClient;
@Autowired
private IDatabaseService postgreSQLClient;
@Autowired
protected EventGenerator eventGenerator;

public void addUserWithParticipant(String email, String participantCode, String name) throws ClientException {
Expand All @@ -65,6 +72,9 @@ public void addUserWithParticipant(String email, String participantCode, String
response = usersResource.create(user);
response.close();
if (response.getStatus() == 201) {
String query = String.format("INSERT INTO %s (user_id,participant_code,secret_generation_date,secret_expiry_date,username)VALUES ('%s','%s',%d,%d,'%s');", apiAccessTable, email,
participantCode, System.currentTimeMillis(), System.currentTimeMillis() + (secretExpiryDays * 24 * 60 * 60 * 1000), userName);
postgreSQLClient.execute(query);
String message = userEmailMessage;
message = message.replace("NAME", name).replace("USER_ID", email).replace("PASSWORD", password).replace("PARTICIPANT_CODE", participantCode);
kafkaClient.send(messageTopic, EMAIL, eventGenerator.getEmailMessageEvent(message, emailSub, List.of(email), new ArrayList<>(), new ArrayList<>()));
Expand Down Expand Up @@ -95,7 +105,7 @@ private UserRepresentation createUserRequest(String userName, String name, Strin
return user;
}

private String generateRandomPassword(){
private String generateRandomPassword() {
String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#&*";
SecureRandom secureRandom = new SecureRandom();
StringBuilder password = new StringBuilder(16);
Expand Down
9 changes: 6 additions & 3 deletions hcx-apis/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ kafka:


registry:
basePath: ${registry_basePath:http://dev-hcx.swasth.app/registry}
basePath: ${registry_basePath:http://aa5c04ed467c04ea89789cead03e4275-320353178.ap-south-1.elb.amazonaws.com:8081}
hcxcode: ${registry_hcxcode:1-d2d56996-1b77-4abb-b9e9-0e6e7343c72e}
organisation-api-path: ${registry_api_path:/api/v1/Organisation}
user-api-path: ${registry_user_api_path:/api/v1/User}
Expand All @@ -46,6 +46,7 @@ postgres:
user: ${postgres_user:postgres}
password: ${postgres_password:postgres}
tablename: ${postgres_tablename:payload}
api-access-secrets-expiry-table: ${api_access_secrets_expiry_table:api_access_secrets_expiry}
onboardingTable: ${onboarding_table:onboarding}
onboardingOtpTable : ${onboarding_otp:onboarding_otp}
subscription:
Expand All @@ -55,7 +56,6 @@ postgres:
subscriptionSelectQuery: ${postgres_subscription_subscriptionSelectQuery:SELECT subscription_id,subscription_request_id,subscription_status,topic_code,sender_code,recipient_code,expiry,is_delegated FROM %s WHERE subscription_id = '%s' AND sender_code = '%s' }
updateSubscriptionQuery: ${postgres_subscription_updateSubscriptionQuery:UPDATE %s SET subscription_status = '%s' WHERE subscription_id = '%s' RETURNING %s }


#hcx error headers
plainrequest:
headers:
Expand Down Expand Up @@ -153,7 +153,10 @@ email:
user-token-subject: ${token_generate_subject:HCX - Protocol APIs Access Token Generation credentials}
user-token-message: ${user_token_message:Hi <b>NAME</b>,<br/><br/> Along with participant username and password, api access token can be generated using the following credentials. <br/> <ul><li> user_name - USER_ID </li> <li> secret - PASSWORD</li><li> participant_code - PARTICIPANT_CODE</li></ul> This API Access token can be used to make protocol API requests. Please refer to the instructions provided in the <a href = "https://github.com/Swasth-Digital-Health-Foundation/hcx-platform/blob/main/docs/user-manuals/How%20to%20generate%20an%20access%20token%20to%20make%20use%20of%20protocol%20APIs/README.md">link</a> to generate access token. <br/><br/> If you have any queries, Please reach out HCX team. <br/><br/> Thanks and Regards <br/> HCX Team.}

api-access-secret:
expiry-days: ${secret_expiry_days:90}

certificate:
validation-enabled: ${certificate_validations_enabled:true}
key-size: ${certificate_validations_key_size:2048,4096}
trusted-cas: ${trusted_cas:DigiCert Inc, Let's Encrypt, GoDaddy.com , Sectigo Limited , Amazon,swasth}
trusted-cas: ${trusted_cas:DigiCert Inc, Let's Encrypt, GoDaddy.com , Sectigo Limited , Amazon,swasth}
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ public class Constants {
public static final String ENCRYPTION_CERT = "encryption_cert";
public static final String ENCRYPTION_CERT_EXPIRY = "encryption_cert_expiry";
public static final String SIGNING_CERT_PATH_EXPIRY = "sigining_cert_expiry";
public static final String API_ACCESS_SECRET_GENERATE = "/api-access/secret/generate";
public static final String PUBLIC_KEY = "public_key";
public static final String PRIVATE_KEY = "private_key";
public static final String PASSWORD = "password";
Expand Down
45 changes: 45 additions & 0 deletions hcx-core/hcx-common/src/main/resources/networkNotifications.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -348,4 +348,49 @@
${DDMMYYYY}. Please renew your encryption key before ${DDMMYYYY} to carry on
operating on HCX."}
status: Active
is_delegate: false
- topic_code: notif-api-access-secret-expiry
title: Api access secret expiry
description: >-
Notification about the api access secret expiration for the participant in the
ecosystem.
allowed_senders:
- HIE/HIO.HCX
allowed_recipients:
- payor
- provider
- agency.tpa
- agency.regulator
- research
- member.isnp
- agency.sponsor
- HIE/HIO.HCX
type: Targeted
category: Network
priority: 1
template: >-
{"message": "Api access secret will be going to expiry in
${days} days.Please reset your api access secret."}
status: Active
is_delegate: false
- topic_code: notif-api-access-secret-expired
title: Api access secret expired
description: Notification about api access secret expired
allowed_senders:
- HIE/HIO.HCX
allowed_recipients:
- provider
- payor
- agency.tpa
- agency.regulator
- research
- member.isnp
- agency.sponsor
- HIE/HIO.HCX
type: Targeted
category: Network
priority: 1
template: >-
{"message": "Your api access secret got expired, please update your details with secret."}
status: Active
is_delegate: false
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.swasth.common.dto.Response;
import org.swasth.common.exception.ClientException;
import org.swasth.common.utils.Constants;
import org.swasth.hcx.controllers.BaseController;
import org.swasth.hcx.services.OnboardService;
Expand Down Expand Up @@ -129,4 +130,15 @@ public ResponseEntity<Object> generatePassword(@RequestBody Map<String, Object>
return exceptionHandler("", ONBOARD_APPLICANT_PASSWORD_GENERATE, new Response(), e);
}
}
@PostMapping(API_ACCESS_SECRET_GENERATE)
public ResponseEntity<Object> apiAccessSecretGenerate(@RequestBody Map<String, Object> requestBody, @RequestHeader HttpHeaders headers) throws Exception {
try {
if(!requestBody.containsKey(USER_ID) || !requestBody.containsKey(PARTICIPANT_CODE)){
throw new ClientException("user id or participant code is missing");
}
return getSuccessResponse(service.generateAndSetUserSecret(requestBody));
} catch (Exception e) {
return exceptionHandler((String) requestBody.getOrDefault(USER_ID, ""), API_ACCESS_SECRET_GENERATE, new Response(), e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.admin.client.resource.UsersResource;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
Expand Down Expand Up @@ -132,7 +134,10 @@ public class OnboardService extends BaseController {
private String keycloackParticipantRealm;
@Value("${keycloak.client-id}")
private String keycloackClientId;

@Value("${keycloak.api-access-realm}")
private String keycloakApiAccessRealm;
@Value("${postgres.table.api-access-secrets-expiry}")
private String apiAccessTable;
@Value("${endpoint.user-invite}")
private String userInviteEndpoint;
@Value("${email.user-invite-sub}")
Expand All @@ -145,7 +150,9 @@ public class OnboardService extends BaseController {
private String emailConfig;
@Value("${onboard.phone}")
private String phoneConfig;

@Value("${api-access-secret.expiry-days}")
private int secretExpiryDays;
@Autowired
private IDatabaseService postgreSQLClient;

@Resource(name = "postgresClientMockService")
Expand Down Expand Up @@ -889,6 +896,13 @@ private String userInviteAcceptParticipantTemplate(String participantName, Strin
return freemarkerService.renderTemplate("user-invite-accepted-participant.ftl", model);
}

private String apiAccessSecretTemplate(String user, String password, String participantCode) throws TemplateException, IOException {
Map<String, Object> model = new HashMap<>();
model.put("USER_ID", user);
model.put("PARTICIPANT_CODE", participantCode);
model.put("PASSWORD", password);
return freemarkerService.renderTemplate("api-access-secret.ftl", model);
}

private String userInviteUserTemplate(String email, String name, String role, URL signedURL) throws TemplateException, IOException {
Map<String, Object> model = new HashMap<>();
Expand Down Expand Up @@ -1105,22 +1119,24 @@ private Map<String, Object> updateMockDetails(Map<String, Object> mockParticipan
mockParticipantDetails.put(PASSWORD, password);
TimeUnit.SECONDS.sleep(3); // After creating participant, elasticsearch will retrieve data after one second hence added two seconds delay for search API.
Map<String,Object> registryDetails = getParticipant(PARTICIPANT_CODE,childParticipantCode);
setKeycloakPassword( password ,registryDetails);
logger.info("created Mock participant for :: parent participant code : {} :: child participant code : {}",parentParticipantCode, childParticipantCode);
ArrayList<String> osOwner = (ArrayList<String>) registryDetails.get(OS_OWNER);
setKeycloakPassword(password, osOwner.get(0), keycloackParticipantRealm);
logger.info("created Mock participant for :: parent participant code : {} :: child participant code : {} " ,parentParticipantCode, childParticipantCode);
return mockParticipantDetails;
}

public void setKeycloakPassword( String password , Map<String,Object> registryDetails) throws ClientException {
public void setKeycloakPassword(String password, String user, String realm) throws ClientException {
try {
ArrayList<String> osOwner = (ArrayList<String>) registryDetails.get(OS_OWNER);
RealmResource realmResource = keycloak.realm(keycloackParticipantRealm);
UserResource userResource = realmResource.users().get(osOwner.get(0));
RealmResource realmResource = keycloak.realm(realm);
UserResource userResource = realmResource.users().get(user);
CredentialRepresentation passwordCred = new CredentialRepresentation();
passwordCred.setTemporary(false);
passwordCred.setType(CredentialRepresentation.PASSWORD);
passwordCred.setValue(password);
userResource.resetPassword(passwordCred);
logger.info("The Keycloak password for the os_owner: {} has been successfully updated",osOwner.get(0));
String userId = userResource.toRepresentation().getId();
realmResource.users().get(userId).logout();
logger.info("The Keycloak password for the osOwner : {} has been successfully updated, and their sessions have been invalidated.", user);
} catch (Exception e) {
throw new ClientException("Unable to set keycloak password : " + e.getMessage());
}
Expand Down Expand Up @@ -1206,12 +1222,9 @@ private String generateRandomPassword(int length){
public Response generateAndSetPassword(HttpHeaders headers, String participantCode) throws Exception {
String password = generateRandomPassword(24);
Map<String, Object> registryDetails = getParticipant(PARTICIPANT_CODE, participantCode);
setKeycloakPassword( password, registryDetails);
ArrayList<String> osOwner = (ArrayList<String>) registryDetails.get(OS_OWNER);
RealmResource realmResource = keycloak.realm(keycloackParticipantRealm);
UserResource userResource=realmResource.users().get(osOwner.get(0));
String username = userResource.toRepresentation().getUsername();
kafkaClient.send(messageTopic, EMAIL, eventGenerator.getEmailMessageEvent(passwordGenerate((String) registryDetails.get(PARTICIPANT_NAME),password,username), passwordGenerateSub, Arrays.asList((String) registryDetails.get(PRIMARY_EMAIL)), getUserList(headers, participantCode), new ArrayList<>()));
setKeycloakPassword(password, osOwner.get(0), keycloackParticipantRealm);
kafkaClient.send(messageTopic, EMAIL, eventGenerator.getEmailMessageEvent(passwordGenerate((String) registryDetails.get(PARTICIPANT_NAME), password, (String) registryDetails.get(PRIMARY_EMAIL)), passwordGenerateSub, Collections.singletonList((String) registryDetails.get(PRIMARY_EMAIL)), getUserList(headers, participantCode), new ArrayList<>()));
return getSuccessResponse();
}

Expand Down Expand Up @@ -1269,4 +1282,23 @@ private List<String> getUserList(HttpHeaders headers, String participantCode) th
return new ArrayList<>();
}
}

public Response generateAndSetUserSecret(Map<String , Object> requestBody) throws Exception {
String password = generateRandomPassword(24);
Map<String , Object> participant = getParticipant(PARTICIPANT_CODE, (String) requestBody.get(PARTICIPANT_CODE));
String userName = String.format("%s:%s", requestBody.get(PARTICIPANT_CODE), requestBody.get(USER_ID));
String selectQuery = String.format("SELECT * FROM %s WHERE username = '%s';", apiAccessTable, userName);
ResultSet resultSet = (ResultSet) postgreSQLClient.executeQuery(selectQuery);
if (resultSet.next()) {
String query = String.format("UPDATE %s SET secret_generation_date=%d,secret_expiry_date=%d WHERE username='%s';", apiAccessTable, System.currentTimeMillis(), System.currentTimeMillis() + (secretExpiryDays * 24 * 60 * 60 * 1000), userName);
postgreSQLClient.execute(query);
}
RealmResource realmResource = keycloak.realm(keycloakApiAccessRealm);
UsersResource usersResource = realmResource.users();
List<UserRepresentation> existingUsers = usersResource.search(userName);
String userId = existingUsers.get(0).getId();
setKeycloakPassword(password, userId, keycloakApiAccessRealm);
kafkaClient.send(messageTopic, EMAIL, eventGenerator.getEmailMessageEvent(apiAccessSecretTemplate((String) requestBody.get(USER_ID), password, (String) participant.get(PARTICIPANT_CODE)), passwordGenerateSub, Arrays.asList((String) requestBody.get(USER_ID)), new ArrayList<>(), new ArrayList<>()));
return getSuccessResponse();
}
}
7 changes: 6 additions & 1 deletion hcx-onboard/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ postgres:
onboard-verification: ${onboard_verification_table:onboard_verification}
onboard-verifier: ${onboard_verifier_table:onboard_verifier}
onboard-user-invite: ${onboard_user_invite_table:onboard_user_invite_details}
api-access-secrets-expiry: ${api_access_secrets_expiry_table:api_access_secrets_expiry}
mock-service:
url: ${mock_service_db_url:jdbc:postgresql://localhost:5432/mock_service}
table:
Expand All @@ -99,6 +100,10 @@ keycloak:
master-realm: ${keycloak_master_realm:master}
client-id: ${keycloack_client_id:admin-cli}
participant-realm: ${keycloack_users_realm:swasth-hcx-participants}
api-access-realm: ${keycloak_api_access_realm:api-access}

endpoint:
user-invite: ${user_invite_endpoint:/onboarding/user/invite}
user-invite: ${user_invite_endpoint:/onboarding/user/invite}

api-access-secret:
expiry-days: ${secret_expiry_days:90}
15 changes: 15 additions & 0 deletions hcx-onboard/src/main/resources/templates/api-access-secret.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<body>
Hello ${USER_ID},<br/><br/>
API Access secret has been generated successfully for the user_id : ${USER_ID}. <br/><br/>
Use below credentials for token generation:
<ul>
<li> username : ${USER_ID} </li>
<li> secret : ${PASSWORD} </li>
<li> participant_code : ${PARTICIPANT_CODE} </li>
</ul>
<br/>Thanks,<br/>
HCX Team
</body>
</html>
Loading

0 comments on commit c869e10

Please sign in to comment.