...
Generate Digest Example Code
Code Block | ||
---|---|---|
| ||
package com.sunwave; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.io.UnsupportedEncodingException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.util.Base64; import java.util.TimeZone; import java.util.Date; import org.apache.commons.codec.digest.DigestUtils; public class DigestCreator { private String userId; private String clientId; private String clientSecret; // Realm Private Key "select client_secret from sw_external_application where client_id = ? and clinic_id = ? private String clinicId; private String transactionId; //"select id from sw_api_transaction where transaction_id = ? and clinic_id = ?" private String payload; private String createMd5Digest() throws NoSuchAlgorithmException, UnsupportedEncodingException {/* * Copyright 2023 Sunwave Health * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.sunwave; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.util.Base64; import java.util.TimeZone; import org.apache.commons.codec.digest.DigestUtils; public class DigestCreator { /** * userId is the email assigned to the user from sw_user_clinic.user_email. * If the combination of userId and clinic_id are not in sw_user_clinic * table the validation will fail. */ private final String userId; /** * clientId is the realm assigned to the user from * sw_user_clinic.clinic_id. If the combination of userId and clinic_id * are not in sw_user_clinic table the validation will fail. */ private final String clientId; /** * clinicId is the realm the user has permission to run rest calls against. */ private final String clinicId; /** * clientSecret is the secret key that was generated by the user's id and * clinic id. This comes from the sw_external_application table and is * used to calculate the HMAC. The HMAC the DigesterCreator calculates and * SunwaveEMR's DigestValidator must match. */ private final String clientSecret; /** * transactionId is a user chosen string that must be unique for the * whole clinic. This value is validated against the sw_api_transaction * table by transaction id and clinic id, the row is returned the * validation fails. */ private final String transactionId; /** * payload is only used on POST and PUT HTTP requests and is used to * calculate MD5 digest hash. */ private final String payload; /** * dateTime holds the current time in the following format * "Mon, 6 Feb 2023 16:35:45 +0000" as Base64 encoded string. This is * generated by getDateTimeBase64. ({@link #getDateTimeBase64()}) */ private final String dateTime; /** * PURPOSE: To calculate the MD5 digest of the payload and return the * lower case version of numeric digest as base 64 value. * @return Base64 value of the MD5 hash. * (@see <a href="https://en.wikipedia.org/wiki/MD5">MD5 Digest</a>) */ private String createMd5Digest() { byte[] digest = DigestUtils.md5Hex(payload.getBytes()).getBytes(); return Base64.getEncoder().encodeToString(digest); } /** * Purpose - To create the seed to be used with the user's private key to * generate the HMAC hash. In the case of GET HTTP requests the seed * comprises user id, client id, current time as Base64 string * {@link #dateTime}, clinic id and transaction id. In the case * of HTTP POST and PUT requests the MD5 digest of the payload is added * to the seed. * @return - Returns seed as clear text. */ private String createSeed() { String seed; if (payload == null) { seed = userId + ":" + clientId + ":" + dateTime + ":" + clinicId + ":" + transactionId; } else { seed = userId + ":" + clientId + ":" + dateTime + ":" + clinicId + ":" + transactionId + ":" + createMd5Digest(); } return seed; } /** * PURPOSE: To generate Base64 of the current timestamp. The format is * "Mon, 6 Feb 2023 16:42:14 +0000" Note it is using GMT * the local +0000 to generate the timestamp. * @return - Current time as Base64 string. */ private String getDateTimeBase64() { SimpleDateFormat df = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z"); df.setTimeZone(TimeZone.getTimeZone("GMT")); java.sql.Timestamp now = new java.sql.Timestamp(new java.util.Date().getTime()); String currentTime = df.format(now); byte[] encodedDate = Base64.getEncoder().encode(currentTime.getBytes()); return new String(encodedDate, StandardCharsets.UTF_8); } /** * Purpose: Create the digest token that can be used in Authorization Head * key with value of Digest concatenated with token: * "Digest sunwave-admin1:vQl91X514m11dTHHGYQPQ ...". The token consists of * user id, client id, current date time, clinic id, transaction id * HMAC code in the case of HTTP Get Request. For HTTP PUT and POST the * token consists of user id, client id, current date time, clinic id, * transaction id, MD5 digest of the payload and HMAC code. * (@see <a href="https://en.wikipedia.org/wiki/Digest_access_authentication">Digest Authentication</a>) * @return - digest token * @throws NoSuchAlgorithmException - Handles the message signature of * {@link #createHMAC(String)} * @throws InvalidKeyException - Handles the message signature of * {@link #createHMAC(String)} */ private String createToken() throws NoSuchAlgorithmException, InvalidKeyException { if (payload == null) { return userId + ":" + clientId + ":" + dateTime + ":" + clinicId + ":" + transactionId + ":" + createHMAC(createSeed()); } else { return userId + ":" + clientId + ":" + dateTime + ":" + clinicId + ":" + transactionId + ":" + createMd5Digest() + ":" return Base64.getEncoder().encodeToString(DigestUtils.md5Hex(payload.getBytes()).getBytes + createHMAC(createSeed()); } } private String createSeed() throws NoSuchAlgorithmException, UnsupportedEncodingException/** { * Purpose: Create Base String64 seedencoded = null; HMAC hash-based message authentication * ifcode (payload@see <a == null) {href="https://en.wikipedia.org/wiki/HMAC">HMAC</a>). * Sunwave uses the SHA-512 Secure Hash Algorithm 2 seed = userId + ":" +* clientId(@see +<a href="https://en.wikipedia.org/wiki/SHA-2">SHAR-512</a>). + getDateTimeBase64() + ":" + clinicId +* ":"@param +seed transactionId;- The seed string to generate the HMAC on. } else { * @return - HMAC Code as BAse 64 encoded String. seed = userId + ":" +* clientId@throws +NoSuchAlgorithmException ":"- +When getDateTimeBase64()Mac +class ":"does +not clinicIdsupport + ":" + transactionId + ":"* + createMd5Digest(); }the give hashing algorithm. Note it support HmacSHA512 so no problem. * @throws InvalidKeyException return- seed;{@link #clientSecret} key can not }be private* Stringused getDateTimeBase64()by throwsthe UnsupportedEncodingExceptionSecretKeySpec {class. */ SimpleDateFormat df =private newString SimpleDateFormatcreateHMAC("EEE,final dString MMMseed) yyyy HH:mm:ss Z"); throws NoSuchAlgorithmException, df.setTimeZone(TimeZone.getTimeZone("GMT"));InvalidKeyException { Datebyte[] dbyteKey = new Date(clientSecret.getBytes(StandardCharsets.UTF_8); java.sql.Timestamp now = new java.sql.Timestamp(new java.util.Date().getTime())final String hmacSha512 = "HmacSHA512"; StringMac dateTimesha512Hmac = dfMac.formatgetInstance(nowhmacSha512); byte[]SecretKeySpec encodedDatekeySpec = new Base64.getEncoder().encode(dateTime.getBytes())SecretKeySpec(byteKey, hmacSha512); return new String(encodedDate, "UTF8"sha512Hmac.init(keySpec); } byte[] private String createTransactionId() {macData = sha512Hmac. return null;doFinal(seed.getBytes(StandardCharsets.UTF_8)); } return Base64.getUrlEncoder().encodeToString(macData); private String createToken()} throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException { /** * ifPurpose: (payloadCreate ==DigestCreator null)object populated with all the information * to returngenerate userIddigest + ":" + clientId + ":" + getDateTimeBase64() + ":" + clinicId + ":" + transactionId + ":" + createHMAC(createSeed()); authentication token. * @param userId - {@link #userId} * @param clientId - {@link #clientId} else * @param clientSecret - {@link #clientSecret} * return@param userIdclinicId + ":" + clientId + ":" + getDateTimeBase64() + ":" + clinicId + ":" + transactionId + ":" + createMd5Digest() + ":" + createHMAC(createSeed()); } private String createHMAC(String message) throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException {- {@link #clinicId} * @param transactionId - {@link #transactionId} * @param payload - {@link #payload} */ public DigestCreator(final String userId, final String clientId, byte[] byteKey = clientSecret.getBytes("UTF-8"); final String HMAC_SHA512 = "HmacSHA512"; final String clientSecret, final String clinicId, Mac sha512_HMAC = Mac.getInstance(HMAC_SHA512); SecretKeySpec keySpec = new SecretKeySpec(byteKey, HMAC_SHA512); final String sha512_HMAC.init(keySpec); transactionId, final String payload) { byte[] mac_data = sha512_HMACthis.userId = userId; this.clientId doFinal(message.getBytes("UTF-8"))= clientId; return Base64.getUrlEncoder().encodeToString(mac_data); this.clientSecret = clientSecret; } this.clinicId public DigestCreator(String userId, String clientId, String clientSecret, String clinic_id, String transactionId, String payload) {= clinicId; this.transactionId = transactionId; this.userIdpayload = userIdpayload; this.clientIddateTime = clientIdgetDateTimeBase64(); } this.clientSecret = clientSecret;/** * Purpose: Provides command line this.clinicIdinterface = clinic_id; to generate digest * authentication this.transactionId = transactionId;token. * @param args - Command line parameters. */ this.payload = payload;public static void main(final String[] }args) { public static void main( Stringif args[] ) throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException((args.length < 5) || (args.length > 6)) { if ((args.length < 5) || (args.length > 6)) { System.out.println("java -cp ./target/DigestCreator-1.0-SNAPSHOT.jar com.sunwave.DigestCreator [user_id] [clinic_id] [client_id] [client_secret] [transaction_id] <payload>"); System.out.println("usage: java -cp ./target/DigestCreator-1.0-SNAPSHOT.jar orgcom.baudekinsunwave.DigestCreator user_id clinic_id client_id, client_secret, transaction_id, <pay load>sunwave-admin1 131 vQl91X514m11dTHHGYQPQkxJqNPxgbdJ C0iGincSREijXqeuB3P9sDdj1ZU6UwqVaUc6VLwhpcx2sBQmB85k8zWuIKSc6gkCAcnXm4JTk2YBFpH5fFDEPH0JyKg4SgchallGmNDc9fNkO1ojZxyKaZ5murQZFDvSW7iJl1CM5JESube8P0cdlqtiLoHb7BP4293S6FqG557TbIPS61ACp0lfAOu9fNXD6L2LD24j7QMRZpM8GE6GQOnY5nTaHGn42eBMjB8iMS9gx4P7iStJirC0vjq2miSC 0000002"); System.exit(-1); } String payload = null; // We are doing post if (args.length == 6) { payload = args[5]; } DigestCreator dc = new DigestCreator(args[0], args[2], args[3], args[1], args[4], payload);], args[3], args[1], args[4], payload); try { System.out.println("Token: " + dc.createToken()); } catch (Exception e) { System.outerr.println("Token: " + dc.createToken())Unable to create authentication token."); e.printStackTrace(System.err); System.out.println("md5 digest: " + dcSystem.createMd5Digestexit(-2)); ; } System.exit(0); } } } |
Code Block |
---|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>DigestCreater</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <dependencies> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.15</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.4.2</version> <executions> <execution> <phase>package</phase> <goals> <goal>single</goal> </goals> <configuration> <archive> <manifest> <mainClass> com.sunwave.DigestCreator </mainClass> </manifest> </archive> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>8</source> <target>8</target> </configuration> </plugin> </plugins> </build> </project> |
...
Term | Definition |
---|---|
Client Id | The unique identifier we assign to user of external API call. Random 32 character string generated using custom algorithm. https://github.com/sunwavehealth/SunwaveEMR/blob/d46b653451ce24623a0c6c72d0aa4e2313c5c0f9/src/main/java/com/sunwave/emr/server/security/ExternalApplicationProcessor.java#L96 |
Client Secret | Secret we assign to the user of the Rest API. It is 256 characters randomly generated by custom algorithm. https://github.com/sunwavehealth/SunwaveEMR/blob/d46b653451ce24623a0c6c72d0aa4e2313c5c0f9/src/main/java/com/sunwave/emr/server/security/ExternalApplicationProcessor.java#L96 |
User Id | The use’s account for Sunwave Health. I It must have an email to be used for this validation. |
Clinic Id | Realm to the user id is to be validated against. |
Transaction Id | A unique id the customer generates for each transaction. Can not be reused. |
Payload | Only used for POST operations and is the base 64 encoded string representing the data to be put into Sunwave. |
MD5 Digest | Message-digest algorithm for producing 128-bit hash values. See https://en.wikipedia.org/wiki/MD5 Sunwave uses it as a checksum to validate the request has not been modified. |
Seed | For GET requests: |
HMAC | Hash Based Method Authentication code see https://en.wikipedia.org/wiki/HMAC Used to verify both the data integrity and authenticity of the user’s request. |
Token | This is the string to used as the Digest. |
...
Code Block | ||
---|---|---|
| ||
##### Begin GET Validation Trace 01 Feb 2023 10:21:01,666~ DEBUG ~ com.sunwave.emr.server.util.LoggerUtils:37 ~ ~ *********** com.sunwave.emr.server.security.DigestValidator::validateTransactionId line: 79 01 Feb 2023 10:21:01,671~ DEBUG ~ com.sunwave.emr.server.util.LoggerUtils:38 ~ ~ select id from sw_api_transaction where transaction_id = '0000002' and clinic_id = '131' 01 Feb 2023 10:21:08,263~ DEBUG ~ com.sunwave.emr.server.util.LoggerUtils:37 ~ ~ *********** com.sunwave.emr.server.security.DigestValidator::validateTransactionId line: 84 01 Feb 2023 10:21:08,265~ DEBUG ~ com.sunwave.emr.server.util.LoggerUtils:38 ~ ~ insert into sw_api_transaction (transaction_id, created_on,clinic_id) values ('0000002',str_to_date('2023-02-01 10:21:06','%Y-%m-%d %H:%i:%s'),'131') 01 Feb 2023 10:22:09,078~ DEBUG ~ com.sunwave.emr.server.util.LoggerUtils:37 ~ ~ *********** com.sunwave.emr.server.security.DigestValidator::validateGET line: 52 01 Feb 2023 10:22:09,081~ DEBUG ~ com.sunwave.emr.server.util.LoggerUtils:38 ~ ~ select user_email from sw_user_clinic where sw_user_clinic.clinic_id = '131' and sw_user_clinic.user_email = 'sunwave-admin1' 01 Feb 2023 10:22:12,955~ DEBUG ~ com.sunwave.emr.server.util.LoggerUtils:37 ~ ~ *********** com.sunwave.emr.server.security.DigestValidator::getPrivateKey line: 73 01 Feb 2023 10:22:12,959~ DEBUG ~ com.sunwave.emr.server.util.LoggerUtils:38 ~ ~ select client_secret from sw_external_application where client_id = 'vQl91X514m11dTHHGYQPQkxJqNPxgbdJ' and clinic_id = '131' ##### End GET Validation Trace ##### Begin GET Users Trace 01 Feb 2023 10:23:55,725~ DEBUG ~ com.sunwave.emr.server.util.LoggerUtils:37 ~ ~ *********** com.sunwave.emr.server.security.APISecurityFilter::validateAPICalls line: 198 01 Feb 2023 10:23:55,726~ DEBUG ~ com.sunwave.emr.server.util.LoggerUtils:38 ~ ~ select api_call_limit_per_day from sw_clinic where clinic_id = '131' 01 Feb 2023 10:23:55,739~ DEBUG ~ com.sunwave.emr.server.util.LoggerUtils:37 ~ ~ *********** com.sunwave.emr.server.security.APISecurityFilter::validateAPICalls line: 200 01 Feb 2023 10:23:55,739~ DEBUG ~ com.sunwave.emr.server.util.LoggerUtils:38 ~ ~ select call_log_count from sw_api_call_count_log where call_log_date = '2023-02-01' and clinic_id = '131' 01 Feb 2023 10:23:55,747~ DEBUG ~ com.sunwave.emr.server.util.LoggerUtils:37 ~ ~ *********** com.sunwave.emr.server.security.APISecurityFilter::validateAPICalls line: 207 01 Feb 2023 10:23:55,747~ DEBUG ~ com.sunwave.emr.server.util.LoggerUtils:38 ~ ~ update sw_api_call_count_log set call_log_count = call_log_count + 1 where call_log_date = '2023-02-01' and clinic_id = '131' 01 Feb 2023 10:23:55,757~ DEBUG ~ com.sunwave.emr.server.util.LoggerUtils:37 ~ ~ *********** com.sunwave.emr.server.Processor::getParentClinicId line: 1535 01 Feb 2023 10:23:55,757~ DEBUG ~ com.sunwave.emr.server.util.LoggerUtils:38 ~ ~ SELECT parent_clinic_id FROM sw_clinic where clinic_id='131' 01 Feb 2023 10:23:55,762~ DEBUG ~ com.sunwave.emr.server.util.LoggerUtils:37 ~ ~ *********** com.sunwave.emr.server.Processor::getTimezoneId line: 1584 01 Feb 2023 10:23:55,762~ DEBUG ~ com.sunwave.emr.server.util.LoggerUtils:38 ~ ~ SELECT timezone_id, clinic_id FROM sw_clinic where id='131' 01 Feb 2023 10:23:55,768~ DEBUG ~ com.sunwave.emr.server.util.LoggerUtils:37 ~ ~ *********** com.sunwave.emr.dao.UserDao::getUsersByClinicExcludingSunwaveUsers line: 763 01 Feb 2023 10:23:55,768~ DEBUG ~ com.sunwave.emr.server.util.LoggerUtils:38 ~ ~ SELECT mb2_user.id, mb2_user.email, mb2_user.first_name, mb2_user.last_name, ifnull(mb2_user.created_on,sw_user_clinic.created_on) created_on, mb2_user.created_by, if(sw_user_clinic.last_login='2011-01-01 00:00:00', '', sw_user_clinic.last_login) last_login FROM mb2_user inner join sw_user_clinic on sw_user_clinic.user_email = mb2_user.email WHERE sw_user_clinic.clinic_id='131' and ((mb2_user.is_sunwave != 'true' and mb2_user.is_sunwave_user != 'true') or (mb2_user.is_sunwave is null and mb2_user.is_sunwave_user is null)) ##### End GET Users Trace |
Validate API Calls aka limit the number of times the user can use the rest endpoints