Skip to content

Commit

Permalink
include README info from old panda-hmac repo
Browse files Browse the repository at this point in the history
  • Loading branch information
andrew-nowak committed Nov 20, 2023
1 parent 88579c0 commit c10b409
Show file tree
Hide file tree
Showing 9 changed files with 209 additions and 2 deletions.
86 changes: 84 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,15 @@ On their return the existing cookie is updated with the new expiry time.

## What's provided

Pan domain auth is split into 5 modules.
Pan domain auth is split into 6 modules.

The [pan-domain-auth-verification](###-to-verify-logins) library provides the basic functionality for sigining and verifying login cookies in Scala.
For JVM applications that only need to *VERIFY* an existing login (rather than issue logins themselves) this is the library to use.

The `pan-domain-auth-core` library provides the core utilities to load settings, create and validate the cookie and
check if the user has mutli-factor auth turned on when usng Google as the provider.

The [pan-domain-auth-play_2-6](###if-your-application-needs-to-issue-logins) library provide an implementation for play apps. There is an auth action
The [pan-domain-auth-play_2-8, 2-9 and 3-0](###if-your-application-needs-to-issue-logins) libraries provide an implementation for play apps. There is an auth action
that can be applied to the endpoints in your application that will do checking and setting of the cookie and will give you the OAuth authentication
mechanism and callback. This is the only framework specific implementation currently (due to play being the framework predominantly used at The
Guardian), this can be used as reference if you need to implement another framework implementation. This library is for applications
Expand All @@ -64,6 +64,9 @@ The `pan-domain-auth-example` provides an example Play 2.9 app with authenticati
of how to set up an nginx configuration to allow you to run multiple authenticated apps locally as if they were all on the same domain which
is useful during development.

The [panda-hmac](###to-verify-machines) libraries build on pan-domain-auth-play to also verify machine clients,
who cannot perform OAuth authentication, by using HMAC-SHA-256.

## Requirements

If you are adding a new application to an existing deployment of pan-domain-authentication then you can skip to
Expand Down Expand Up @@ -321,6 +324,85 @@ function(request) {
```


### To verify machines

Add a dependency on the correct version of `pan-domain-auth-play` and configure to allow authentication of users using OAuth 2. Then, adding support should be as simple as adding a dependency on the relevant panda-hmac-play library, and mixing `HMACAuthActions` into your controllers.

Example:

```scala
import com.gu.pandahmac.HMACAuthActions

// ...

@Singleton
class MyController @Inject() (
override val config: Configuration,
override val controllerComponents: ControllerComponents,
override val wsClient: WSClient,
override val refresher: InjectableRefresher
) extends AbstractController(controllerComponents)
with PanDomainAuthActions
with HMACAuthActions {

override def secretKeys = List("currentSecret") // You're likely to get your secret from configuration or a cloud service like AWS Secrets Manager

def myApiActionWithBody = APIHMACAuthAction.async(circe.json(2048)) { request =>
// ... do something with the request
}

def myRegularAction = HMACAuthAction {}

def myRegularAsyncAction = HMACAuthAction.async {}
}
```

#### Setting up a machine client

There are example clients for Scala, Javascript and Python in the `hmac-examples/` directory.

Each client needs a copy of the shared secret, defined as "currentSecret" in the controller example above.
Each request needs a standard (RFC-7231) HTTP Date header, and an authorization digest that is calculated like this:

1. Make a "string to sign" consisting of the HTTP Date and the Path part of the URI you're trying to access,
seperated by a literal newline (unix-style, not CRLF)
2. Calculate the HMAC digest of the "string to sign" using the shared secret as a key and the HMAC-SHA-256 algorithm
3. Base64 encode the binary output of the HMAC digest to get a random-looking string
4. Add the HTTP date to the request headers with the header name **'X-Gu-Tools-HMAC-Date'**
5. Add another header called **'X-Gu-Tools-HMAC-Token'** and set its value to the literal string **HMAC** followed by a
space and the digest, like this: `X-Gu-Tools-HMAC-Token: HMAC boXSTNumKWRX3eQk/BBeHYk`
6. Send the request and the server should respond with a success.
7. The default allowable clock skew is 5 minutes, if you have problems then this is the first thing to check.

#### Testing HMAC-authenticated endpoints in isolation

[Postman](https://www.postman.com/) is a common environment for testing HTTP requests. We can add a [pre-request script](https://learning.postman.com/docs/writing-scripts/pre-request-scripts/) that automatically adds HMAC headers when we hit send.

<details>
<summary>Pre-request script</summary>

```js
const URL = require("url");

const uri = pm.request.url.toString();
const secret = "Secret goes here :)";

const httpDate = new Date().toUTCString();
const path = new URL.parse(uri).path;
const stringToSign = `${httpDate}\n${path}`;
const stringToSignBytes = CryptoJS.enc.Utf8.parse(stringToSign);
const secretBytes = CryptoJS.enc.Utf8.parse(secret);

const signature = CryptoJS.enc.Base64.stringify(CryptoJS.HmacSHA256(stringToSignBytes, secretBytes));
const authToken = `HMAC ${signature}`;

pm.request.headers.add({ key: 'X-Gu-Tools-HMAC-Date', value: httpDate });
pm.request.headers.add({ key: 'X-Gu-Tools-HMAC-Token', value: authToken });
```

</details>


### Dealing with auth expiry in a single page webapp

In a single page webapp there will typically be an initial page load and then all communication with the server will be initiated by JavaScript.
Expand Down
3 changes: 3 additions & 0 deletions hmac-examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Client Examples

Sometimes the best way to learn is by example. This folder contains a few example client implementations for various languages to show how you might call a HMAC'd service. If you've recently worked against `play-hmac` in a language without an example it would be great if you could add a snippet here.
1 change: 1 addition & 0 deletions hmac-examples/js/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
14.17.3
37 changes: 37 additions & 0 deletions hmac-examples/js/hmac-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const crypto = require('crypto');
const reqwest = require('reqwest');

// The secret you share with the remote service.
// Should *NOT* be hard coded, put it somewhere private (S3, Dynamo, properties file, etc.)
const sharedSecret = "Sanguine, my brother.";

// Make a hmac token from the required components. You probably want to copy this :)
function makeHMACToken(secret, date, uri) {
const hmac = crypto.createHmac('sha256', secret);

const content = date + '\n' + uri;

hmac.update(content, 'utf-8');

return "HMAC " + hmac.digest('base64');
}

// It's important to remember the leading /
const uri = "/api/examples";
const date = (new Date()).toUTCString();
const token = makeHMACToken(sharedSecret, date, uri);

// Make a request to our example API with the generated HMAC
reqwest({
url: "http://example.com" + uri,
method: 'GET',
headers: {
'X-Gu-Tools-HMAC-Date': date,
'X-Gu-Tools-HMAC-Token': token,
'X-Gu-Tools-Service-Name': 'example-service-name'
}
}).then(function(resp) {
console.log('We did it!');
}, function(err, msg) {
console.error('Something went wrong :(');
});
6 changes: 6 additions & 0 deletions hmac-examples/js/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"dependencies": {
"reqwest": "2.0.5",
"xhr2": "0.1.3"
}
}
51 changes: 51 additions & 0 deletions hmac-examples/python/hmac-client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/python

import hashlib
import hmac
from optparse import OptionParser
from datetime import datetime
import base64
from email.utils import formatdate
import requests
from time import mktime
from urlparse import urlparse
from pprint import pprint

def get_token(uri, secret):
httpdate = formatdate(timeval=mktime(datetime.now().timetuple()),localtime=False,usegmt=True)
url_parts = urlparse(uri)

string_to_sign = "{0}\n{1}".format(httpdate, url_parts.path)
print "string_to_sign: " + string_to_sign
hm = hmac.new(secret, string_to_sign,hashlib.sha256)
return "HMAC {0}".format(base64.b64encode(hm.digest())), httpdate

#START MAIN
parser = OptionParser()
parser.add_option("--host", dest="host", help="host to access", default="video.local.dev-gutools.co.uk")
parser.add_option("-a", "--atom", dest="atom", help="uuid of the atom to request")
parser.add_option("-s", "--secret", dest="secret", help="shared secret to use")
(options, args) = parser.parse_args()

if options.secret is None:
print "You must supply the password in --secret"
exit(1)

uri = "https://{host}/pluto/resend/{id}".format(host=options.host, id=options.atom)
print "uri is " + uri
authtoken, httpdate = get_token(uri, options.secret)
print authtoken

headers = {
'X-Gu-Tools-HMAC-Date': httpdate,
'X-Gu-Tools-HMAC-Token': authtoken
}

print headers
response = requests.post(uri,headers=headers)
print "Server returned {0}".format(response.status_code)
pprint(response.headers)
if response.status_code==200:
pprint(response.json())
else:
print response.text
1 change: 1 addition & 0 deletions hmac-examples/python/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
requests==2.18.4
21 changes: 21 additions & 0 deletions hmac-examples/scala/HMACClient.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package example

import java.net.URI
import com.gu.hmac.HMACHeaders

// When using scala you get the `hmac-headers` library and use it directly to generate your HMAC tokens
object HMACClient extends HMACHeaders {
val secret = "Sanguine, my brother."

// Unlike the javascript example, with the hmac-headers library you don't provide it a date, it generates one for you
def makeHMACToken(uri: String): HMACHeaderValues = {
createHMACHeaderValues(new URI(uri))
}
}

object ExampleRequestSender {
def sendRequest = {
val uri = "/api/examples"
ws.url("example.com" + uri)
}
}
5 changes: 5 additions & 0 deletions hmac-examples/scala/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
name := "scala-hmac-client-example"

scalaVersion := "2.11.8"

libraryDependencies += "com.gu" %% "panda-hmac" % "1.1"

0 comments on commit c10b409

Please sign in to comment.