How to subscribe to AWS SNS using HTTPS with Basic Authentication

Amazon Web Services (AWS) provides a Simple Notification Service (SNS) that allows you to publish messages to a topic that multiple subscribers can consume. One type of subscriber that SNS offers is an HTTPS endpoint with optional basic authentication. Sounds straightforward, right?

Here’s what I thought I would need to do going in:

  1. Buy a domain
  2. Buy an SSL certificate
  3. Build an endpoint with basic auth that handled the subscription confirmation request
  4. Create the SNS Subscription

Here’s what I discovered along the way:

  1. You need to bundle your SSL certificate
  2. You need to specify a realm in your WWW-Authenticate header
  3. You need to create the subscription again and again instead of requesting confirmations for an existing subscription that is pending confirmation
  4. You need to override the Content-Type header for Hapi.js

What I thought I would need to do

This should be relatively easy.

Buy a domain

No surprises here. Any domain from any registrar should do.

Buy an SSL certificate

AWS only trusts a specific set of Certificate Authorities (CA). Luckily, the very cheap Comodo PositiveSSL is trusted ($9 at the time of writing).

comodoaaaca, Apr 22, 2014, trustedCertEntry,
Certificate fingerprint (SHA1): D1:EB:23:A4:6D:17:D6:8F:D9:25:64:C2:F1:F1:60:17:64:D8:E3:49

This was my first time purchasing an SSL cert, and SSLs.com made it easy. At the end of the process, I was emailed a ZIP file containing four files:

  1. AddTrustExternalCARoot.crt
  2. COMODORSAAddTrustCA.crt
  3. COMODORSADomainValidationSecureServerCA.crt
  4. example_com.crt

example_com.crt looked like what I needed so I uploaded it to AWS using their CLI along with the PEM I created during the SSLs.com process.

aws iam upload-server-certificate \
--server-certificate-name example \
--certificate-body file://example_com.crt \
--private-key file://example_com.pem

Build an endpoint

To build an endpoint with basic authentication, I chose Hapi.js and its hapi-auth-basic plugin.

If you want to skip ahead to a working end result, here is the example repository on GitHub

Defining dependencies, a server, an authentication scheme, payload validation, and a route to handle the SNS subscription confirmation request is just over 100 lines of code. npm start and I was up and running.

Deploying this server is beyond the scope of this post. I used AWS Elastic Beanstalk which comes into play when configuring HTTPS and reading logs. If you’ve read this far into a post like this, I trust you can get your deployment configured accordingly.

At this point, I bought a domain, an SSL cert, deployed my Node.js app, and configured my DNS records for the domain to point at the deployed app. Because I deployed to Elastic Beanstalk and had uploaded my certificate, configuring HTTPS through the AWS’ console was easy.

Create the SNS Subscription

I already had an SNS Topic created. You can use the AWS CLI to quickly create your own: aws sns create-topic --name example_topic. You’ll need the ARN for the subscription.

I thought I had completed all of the prerequisites for creating an HTTPS Subscription with Basic Authentication. It should have been one painless command:

aws sns subscribe \
--topic-arn $ARN_FROM_CREATE_TOPIC \
--protocol https \
--notification-endpoint https://alice:example@example.com/sns

Instead of receiving the subscription’s ARN, “Pending Confirmation” was returned. This began a long and frustrating debugging adventure.

What I discovered along the way

“Pending Confirmation” wasn’t right, so now what?

You need to bundle your SSL certificate

One feature of Elastic Beanstalk that I really like is the automatic configuration of a load balancer. You can easily auto-scale multiple instances of your app and have requests routed through an automatically configured instance of nginx. A hidden benefit of this setup is that you get access logs from nginx and logs from your app. (Elastic Beanstalk also manages logging for you!) When my subscription wasn’t immediately successful, I went to the logs.

Problem: no requests were logged. I curl’d my endpoint to verify.

curl -X POST https://alice:example@example.com/sns

{“statusCode”:400,”error”:”Bad Request”,”message”:”\”value\” must be an object”,”validation”:{“source”:”payload”,”keys”:[“value”]}}

My request was reaching the server, but the SNS Subscription Confirmation request wasn’t. After some searching and browsing forums, it sounded like my SSL certificate could be to blame. (Remember AWS won’t deliver messages to untrusted CAs) Thankfully, I found this helpful gist explaining how to combine the CRT files I received into a bundle.

cat example_com.crt COMODORSADomainValidationSecureServerCA.crt COMODORSAAddTrustCA.crt AddTrustExternalCARoot.crt > ssl-bundle.crt

I turned off HTTPS on Elastic Beanstalk, deleted the previous certificate from AWS, uploaded this new bundled certificate to AWS, and turned HTTPS back on for my app in Elastic Beanstalk. Then, I requested confirmation for my SNS subscription through the AWS Console. The logs now showed a request from “Amazon Simple Notification Service Agent” with a response status code of 401!

You need to specify a realm in your WWW-Authenticate header

It’s not clear from AWS’ documentation, but what I gleaned from their forums is that SNS makes two requests when confirming an authenticated endpoint. The first request sends no authentication header. The second request includes your configured authentication. I was only seeing the first request and not the retry.

Again, I dug through the AWS forums and found this.

Your server must respond with both status 401 and WWW-Authenticate: Basic realm=“foo” for SNS to retry with auth provided.

I knew I was sending 401 from the nginx logs, but what about that WWW-Authenticate header? By adding the verbose argument to an unauthenticated curl request, I was able to inspect the current state of my response headers (trimmed for clarity).

curl -v -X POST https://example.com/sns

< HTTP/1.1 401 Unauthorized
< WWW-Authenticate: Basic
< content-type: application/json; charset=utf-8

No realm. What is a realm? How could I set one? Who was setting that WWW-Authenticate header? How is the 401 response generated? It was time to start reading the source code of my dependencies.

How is the 401 response generated?

hapi-auth-basic replies with a Boom.unauthorized error when there is no authorization header [source]

Who was setting that WWW-Authenticate header?

Boom sets the WWW-Authenticate header [source]

How could I set one?

Boom.unauthorized allows you to pass an optional attributes object whose keys and values are joined with an equals sign [source]

If I could specify realm as a key in those attributes, my response header should look like that one random forum comment and maybe get me closer to successfully subscribing with my endpoint.

Contributing to Open Source

Not all contributions are groundbreaking as you can see by my PR. The Hapi community is very welcoming and helpful. If you’re looking to dip your toe in the water, I highly suggest checking out their projects.

While waiting on a new version to be published, I used my fork of hapi-auth-basic and added logging to inspect request headers before authentication. Again, I requested confirmation for my SNS subscription through the AWS Console. This time, I saw both requests, but both were responding with an unauthorized error (i.e. 401).

You need to create the subscription again

The second request should have authenticated successfully. My scheme is as simple as it gets, comparing the username and password to hard-coded values, so I added more logging to make sure I was getting the right credentials.

Once more, I requested confirmation for my SNS subscription through the AWS Console. I did this a few more times because I couldn’t believe what I was seeing in the logs.

151115/165554.290, [log,debug], data: {"username":"alice","password”:”****”}

Those stars (“****”) aren’t me holding back my example password. AWS was using obfuscated credentials for the authenticated call. It became crystal clear why the second “authenticated” request was being denied.

The AWS Console displayed the subscription endpoint with the password obfuscated so I guessed they were retrying with that value directly. Inspecting the network request that was made after clicking “Request confirmations” in the browser mostly proved this to be true. Here’s the simplified request:

curl 'https://console.aws.amazon.com/sns/v2/Subscribe' \
—data-binary ‘{\
“topicArn”:”arn:aws:sns:us-east-1:0123456789:example”,\
”endpoint”:”https://alice:****@example.com/sns”,\
”protocol”:”https”\
}’

Again, those stars are literal values.

I didn’t know what else to do but create a duplicate subscription. Doing so passed the correct credentials on the second request and the logs showed a response code of … 415!?

You need to override the Content-Type header for Hapi.js

I’ll admit this is about the point where I stopped caring about completely understanding what exactly was going wrong and just wanted to make this work. I knew this 415 Unsupported Media Type error wasn’t coming from hapi-auth-basic or my code so it must be coming from within hapi itself. Instead of diving into hapi’s source, I started searching for other people befuddled by a 415 response and found this discussion.

Probably has something to do with the Content-Type header

Because I was already logging the headers to debug the auth, I could see that Amazon was specifying text/plain as the Content-Type header. When I was debugging the WWW-Authenticate header, I noticed the Content-Type of the response was application/json. I thought maybe I could “sudo make request like response” and, sure enough, Hapi’s route options allowed overriding the payload Content-Type header.

Push the change, create the subscription again, check the logs, and finally a successful confirmation. When I refreshed the AWS Console, the Subscription ARN changed from “PendingConfirmation” to an actual ARN.

arn:aws:sns:us-east-1:0123456789:example-topic:long-uuid-looking-thing

Conclusion

I hope this helps someone else out there (including future me). If you have questions about the code, please open an issue.

How to subscribe to AWS SNS using HTTPS with Basic Authentication

Leave your thoughts

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s