Sunwave Health Digest Authentication
Usage
Customer Generation and Usage
See Bearer token for instructions on setting up user to access apis. OAUTH 2 Access Code Auth Flow for Rest APIs | Generate Access Code
Generate Digest Example Code
/*
* 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() + ":"
+ createHMAC(createSeed());
}
}
/**
* Purpose: Create Base 64 encoded HMAC hash-based message authentication
* code (@see <a href="https://en.wikipedia.org/wiki/HMAC">HMAC</a>).
* Sunwave uses the SHA-512 Secure Hash Algorithm 2
* (@see <a href="https://en.wikipedia.org/wiki/SHA-2">SHAR-512</a>).
* @param seed - The seed string to generate the HMAC on.
* @return - HMAC Code as BAse 64 encoded String.
* @throws NoSuchAlgorithmException - When Mac class does not support
* the give hashing algorithm. Note it support HmacSHA512 so no problem.
* @throws InvalidKeyException - {@link #clientSecret} key can not be
* used by the SecretKeySpec class.
*/
private String createHMAC(final String seed)
throws NoSuchAlgorithmException, InvalidKeyException {
byte[] byteKey = clientSecret.getBytes(StandardCharsets.UTF_8);
final String hmacSha512 = "HmacSHA512";
Mac sha512Hmac = Mac.getInstance(hmacSha512);
SecretKeySpec keySpec = new SecretKeySpec(byteKey, hmacSha512);
sha512Hmac.init(keySpec);
byte[] macData = sha512Hmac.
doFinal(seed.getBytes(StandardCharsets.UTF_8));
return Base64.getUrlEncoder().encodeToString(macData);
}
/**
* Purpose: Create DigestCreator object populated with all the information
* to generate digest authentication token.
* @param userId - {@link #userId}
* @param clientId - {@link #clientId}
* @param clientSecret - {@link #clientSecret}
* @param clinicId - {@link #clinicId}
* @param transactionId - {@link #transactionId}
* @param payload - {@link #payload}
*/
public DigestCreator(final String userId, final String clientId,
final String clientSecret, final String clinicId,
final String transactionId, final String payload) {
this.userId = userId;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.clinicId = clinicId;
this.transactionId = transactionId;
this.payload = payload;
this.dateTime = getDateTimeBase64();
}
/**
* Purpose: Provides command line interface to generate digest
* authentication token.
* @param args - Command line parameters.
*/
public static void main(final String[] args) {
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("java -cp ./target/DigestCreator-1.0-SNAPSHOT.jar com.sunwave.DigestCreator 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);
try {
System.out.println("Token: " + dc.createToken());
} catch (Exception e) {
System.err.println("Unable to create authentication token.");
e.printStackTrace(System.err);
System.exit(-2);
}
System.exit(0);
}
}
<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>
Generate GET Request Digest
java -cp ./target/DigestCreater-1.0-SNAPSHOT.jar com.sunwave.DigestCreator sunwave-admin1 131 vQl91X514m11dTHHGYQPQkxJqNPxgbdJ C0iGincSREijXqeuB3P9sDdj1ZU6UwqVaUc6VLwhpcx2sBQmB85k8zWuIKSc6gkCAcnXm4JTk2YBFpH5fFDEPH0JyKg4SgchallGmNDc9fNkO1ojZxyKaZ5murQZFDvSW7iJl1CM5JESube8P0cdlqtiLoHb7BP4293S6FqG557TbIPS61ACp0lfAOu9fNXD6L2LD24j7QMRZpM8GE6GQOnY5nTaHGn42eBMjB8iMS9gx4P7iStJirC0vjq2miSC 0000002
Generate POST Request Digest
Data Dictionary
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. 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 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 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. |
Software Design
GET Request Validation Trace
API Security Filter Digest Validation PATH https://github.com/sunwavehealth/SunwaveEMR/blob/51252a9bb7a7d193a9cb929a7c22b04c2ad7fcf5/src/main/java/com/sunwave/emr/server/security/APISecurityFilter.java#L44
Validate GET if any of the validations fail “Invalid Request“ is returned. https://github.com/sunwavehealth/SunwaveEMR/blob/d46b653451ce24623a0c6c72d0aa4e2313c5c0f9/src/main/java/com/sunwave/emr/server/security/DigestValidator.java#L41
Validate Date and Transaction Id https://github.com/sunwavehealth/SunwaveEMR/blob/d46b653451ce24623a0c6c72d0aa4e2313c5c0f9/src/main/java/com/sunwave/emr/server/security/DigestValidator.java#L49
Validate Date - The request timestamp must be with in 300000 millisecond (5 minute) time window before now. https://github.com/sunwavehealth/SunwaveEMR/blob/d46b653451ce24623a0c6c72d0aa4e2313c5c0f9/src/main/java/com/sunwave/emr/server/security/DigestValidator.java#L91
Validate Transaction Id - Transaction Id can not be reused this checks for that in the
sw_api_transaction
table using transaction_id and clinic_id https://github.com/sunwavehealth/SunwaveEMR/blob/d46b653451ce24623a0c6c72d0aa4e2313c5c0f9/src/main/java/com/sunwave/emr/server/security/DigestValidator.java#L79If the transaction id is not found the validation passes and the transaction id is inserted into
sw_api_transaction
table.
Validate The user exists and has an email address from user_email table. https://github.com/sunwavehealth/SunwaveEMR/blob/d46b653451ce24623a0c6c72d0aa4e2313c5c0f9/src/main/java/com/sunwave/emr/server/security/DigestValidator.java#L58
Validate HMAC if the passed in HMAC does not match the calculated HMAC the validation fails.
Get Private Key from
sw_external_application
table: https://github.com/sunwavehealth/SunwaveEMR/blob/d46b653451ce24623a0c6c72d0aa4e2313c5c0f9/src/main/java/com/sunwave/emr/server/security/DigestValidator.java#L72Calculate the HMAC using the seed and private key https://github.com/sunwavehealth/SunwaveEMR/blob/d46b653451ce24623a0c6c72d0aa4e2313c5c0f9/src/main/java/com/sunwave/emr/server/security/DigestValidator.java#L62 and https://github.com/sunwavehealth/SunwaveEMR/blob/d46b653451ce24623a0c6c72d0aa4e2313c5c0f9/src/main/java/com/sunwave/emr/server/util/JWT.java#L97
Validate API Calls aka limit the number of times the user can use the rest endpoints