Kong has lots of plugins available to do varies kind of tasks, Security is a big topic among the plugins , no one wants to expose API to internet without any protection. we use HMAC Plugin to do API level authentication. with HMAC Plugin the password never go through the wire, and we could also enable the body verification as an option. this is a good option for us to open API to the public internet
enable the hmac auth plugin
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
name: hmac-auth
plugin: hmac-auth
this will enable the plugin in k8s env and we could enable the plugin by putting an annotation value in the ingress yaml file
annotations:
plugins.konghq.com: hmac-auth
config username and password
hmac plugin need a Consumer and 1 or more credentials, the credentials should be attached to the Consumer
apiVersion: configuration.konghq.com/v1
kind: KongConsumer
metadata:
name: consumer-hmac
username: api-user
---
apiVersion: configuration.konghq.com/v1
kind: KongCredential
metadata:
name: credential-hmac
consumerRef: consumer-hmac
type: hmac-auth
config:
username: whoami1
secret: xxxxxxxxxxxxx
---
apiVersion: configuration.konghq.com/v1
kind: KongCredential
metadata:
name: credential-hmac
consumerRef: consumer-hmac
type: hmac-auth
config:
username: whoami2
secret: xxxxxxxxxxxxx
when we deploy these consumere and credentails, the ingress which enalbe the hmac-auth plugin is protected.
generate client code to call the server
to call the hmac-auth protected api, we must generate the client signature and pass it in http header, to generate hmac hash, kong support sha1, sha256,sha384 and sha512, sha1
is not recommended as it's not secure, I will use sha256 in this article. like code below we could use password xxxxxxxxxxxxx
to generate hash data for any given data
private String hmacHash(String key, String data)
throws Exception {
String algorithm = "HmacSHA256";
SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), algorithm);
Mac mac = Mac.getInstance(algorithm);
mac.init(signingKey);
return Base64.getEncoder().encodeToString(mac.doFinal(data.getBytes()));
}
with Kong hmac we must pass Date
in GMT format through http header and pass Authorization
for the calulcated hash, Authorization
is formated as hmac username="whoami1", algorithm="hmac-sha256", headers="Date", signature="hmacHash(dateString, xxxxxxxxxxxxx)"
- hmac is telling the server we are using hmac plugin to to authentication
- username="whoami1" is the user name we created in the KongCredential, then server could located this user's password on server side
- algorithm="hmac-sha256" will tell server we are using sha265 algorithm to calculate hash
- headers="Date" will tell server we use Date as the input source to calculate hash
- signature="hmacHash(dateString, xxxxxxxxxxxxx)" is the signature with result of hmacHash calculated on client side based on "Date" header to send back to server. the server will sueee the same password for
whoami1
to caluclate hash for "Date" header, if the calculated hash match given signature, auth pass, otherwise auth failed.
private Map<String, String> headers(String username, String password) throws Exception {
Map<String, String> headers = Maps.newLinkedHashMap();
DateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss");
df.setTimeZone(TimeZone.getTimeZone("GMT"));
String date = df.format(new Date()) + " GMT";
headers.put("Date", date);
String rawSign = "Date: " + date;
String authHeader = String.format(
"hmac username=\"%s\", algorithm=\"hmac-sha256\", headers=\"Date\", signature=\"%s\"",
username, hmacHash(password, rawSign));
headers.put("Authorization", authHeader);
return headers;
}
test auth in java code
code below will function well with only Google Guava as a dependency, we could use http client to write more elegant code to handle http request and response.
@Test
public void testRequest() throws Exception {
Map<String, String> headers = headers("whoami1", "xxxxxxxxxxxxx");
URL url = new URL("http://api.vipmind.me/hello/v1");
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("POST");
con.setDoOutput(true);
//set the header
headers.entrySet().forEach(entry -> con.setRequestProperty(entry.getKey(), entry.getValue()));
//set the timeout
con.setConnectTimeout(5000);
con.setReadTimeout(5000);
//send out the call the server
try (OutputStream os = con.getOutputStream()) {
byte[] input = Files.toByteArray(new File(Resources.getResource("data.json").getFile()));
os.write(input, 0, input.length);
}
if (con.getResponseCode() == 200) {
//receive response
try (BufferedReader br = new BufferedReader(
new InputStreamReader(con.getInputStream(), "utf-8"))) {
StringBuilder response = new StringBuilder();
String responseLine = null;
while ((responseLine = br.readLine()) != null) {
response.append(responseLine.trim());
}
System.out.println(response.toString());
}
} else {
try (BufferedReader br = new BufferedReader(
new InputStreamReader(con.getErrorStream(), "utf-8"))) {
StringBuilder response = new StringBuilder();
String responseLine = null;
while ((responseLine = br.readLine()) != null) {
response.append(responseLine.trim());
}
System.out.println(response.toString());
}
}
con.disconnect();
}
test auth in curl
we could also construct curl command to call the api
@Test
public void testCurl() throws Exception {
StringBuilder call = new StringBuilder();
call.append("curl -XPOST");
Map<String, String> headers = headers("whoami1", "xxxxxxxxxxxxx");
headers.entrySet().forEach(entry -> call
.append(" -H '")
.append(entry.getKey())
.append(": ")
.append(entry.getValue())
.append("'"));
call.append(" -d \"@data.json\"");
call.append(" http://api.vipmind.me/hello/v1");
System.out.println(call.toString());
}