Part 4 - the result - API Gateway APIs with Keycloak JWT authentication
In the last article I showed you how to put API Gateway in front of your Lambda. In this article I'll enable JWT in the API Gateway and show you how to configure a Keycloak client to generate a JWT for the service. I'm using my development Keycloak service but the concepts will apply to any Keycloak instance.
Note: once again I am showing you the "ClickOps" method of setting up your environment by doing everything though the AWS console. This is not a best practice by any means. Best practice is to use a toolkit such as AWS CDK or others to have an IaC (Infrastructure-as-Code) environment. An IaC environment allows you to reproduce your overall infrastructure easily and allows much simpler auditing. But this is a demo - it's not meant to be the be-all and end-all for AWS best practices. |
Let's start by going to the API Gateway page. If you're not already there, just select the AWS logo in the upper right of the page and search for API Gateway.
Once you're there, select your API. If you've copied everything I've done directly then it will be named "api-gw-dad-joke-API" but yours may be different. When you first open the API there isn't much to see:
Select the "ANY" word (yes, it's a link) to setup the JWT authorization:
And select "Attach authorization":
Obviously there isn't one here yet so lets create it. Select "Create and attach an authorizer":
Any name will do for the name field. For the "Identity source" field, let's leave it as the default of "$request.header.Authorization". What this is saying is that API Gateway will take the HTTP request - $request - and look inside the HTTP headers - header - for the field Authorization. This is the standard HTTP header when authorizing to a site and is the same as if you used a user name and password.
The Issuer URL will be derived from "well known" URL from your Keycloak server. For modern Keycloak installations the URL pattern is:
https://auth.hotjoe.com/realms/hotjoe/.well-known/openid-configuration
where "auth.hotjoe.com" is the hostname and "hotjoe" is the name of the Keycloak realm. I'll go over the Keycloak setup in just a bit but go to the well known URL in a browser (preferably one that will pretty print JSON for you) to look at the configuration. In my example, the JSON starts like:
So the issuer for me is "https://auth.hotjoe.com/realms/hotjoe". Yours will be different.
Lastly, there is the "Add audience" button. What does this do? In a JWT there are many standard fields and "audience" or "aud" is one of the standard fields. If you were to paste a JWT into something like https://jwt.io/ (thank you Okta!) you would see something like:
So, for a Keycloak token, the "audience" is "account". This is one area that still confuses me as it's very difficult to change this from the default. I would have expected the audience to be either the client id or the realm but that's not the way it is.
When you're done you'll have something like:
for your authorizer. Note that you can add multiple Audience claims but we're not going to use that for this integration.
When you come back to the Authorization screen for API Gateway you'll notice that your route shows that it now requires authorization:
If you try to hit your API now without a token you'll get a 401 Unauthorized message. That's good - that's the first part we want - no one can access our API without proper authorization.
Now, let's get access to this API. First, let's get a token from our Keycloak server:
$ curl -d "client_id=apigateway" -d 'username=awsgateway' -d 'password=abcdefghijk' -d "grant_type=password" --user 'apigateway:blahblahblah' https://auth.hotjoe.com/realms/hotjoe/protocol/openid-connect/token
Important safety note: the curl command above assumes that your created a client secret for your Keycloak client. If you did not then the command is a bit simpler:
$ curl -d "client_id=apigateway" -d 'username=awsgateway' -d 'password=abcdefghijk' -d "grant_type=password" https://auth.hotjoe.com/realms/hotjoe/protocol/openid-connect/token
This version doesn't send a client secret key.
What you get back either way is an access token with a refresh token. The pretty printed JSON looks like:
{
"access_token":"eyJhbGciOiJSU...",
"expires_in":300,
"refresh_expires_in":1800,
"refresh_token":"eyJhbGciOiJIUzUx...",
"token_type":"Bearer",
"not-before-policy":0,
"session_state":"60ea9cf0-df6d-4b9e-8e3a-ed1ed25da3c4",
"scope":"email profile"
}
You'll need to copy the "access_token" field into Postman. For Postman the URL is the URL of your API gateway. Mine is
https://2ozg2rh13f.execute-api.us-west-2.amazonaws.com/default/api-gw-dad-joke
Then, under the "Authorization" tab in Postman, set your "Auth Type" to "Bearer Token" and paste the token from above into Postman and run it:
Cool - our API is now only accessible with the proper JWT authorization.
Note one other thing in the token. There is an "expires_in" field. For my Keycloak environment the tokens that it supplies are good for 300 seconds or 5 minutes. If you wait too long to run the query in Postman you will get an unauthorized message as the token is no longer valid.
Overall setting up a Lambda with JWT authorization is pretty straight forward, my rambling aside. Keycloak works well for this purpose and is a highly performant identity provider. In future articles I'll show you other ways of implementing that go deeper into using both JWT and other methods of authorization.
Photo by Kyle Glenn on Unsplash
Comments