Authentication ~ A custom Usecase & a different approch
hase 1 :The Usecase
Hey folks,
Recently I came across a very unique & interesting AWS architectural usecase & trust me it’s worth reading!!!
Daily our team was generating some kind of reports through Bitbucket pipeline for 70+ repositories and were pushing those generated report to AWS S3. They were posting S3 URL of all the reports into a group chat. So, request was to setup and authentication architecture which allows only few of them to access the report. Meaning whenever any user clicks any of those links posted in group, there should exist any authentication mechanism
So now let’s first, list down all the constrains :-
So, very first solution I thought of is to setup nginx proxy, a very basic one, which can easily use normal username & password to authenticate user. It actually worked!!!
But you know, for some solution you don’t get that gut feeling. When I demonstrated this solution to my manager, he said “Can we have a more resilient solution
CloudFront Cost (Free Tier) :-
Cost of lambda edge :-
As our files size were in kbs, we were not going to use even 1% of the above stated quota. Hence, cost of this solution is almost around $0 per month !!!
Phase 2 : The SOLUTION
My architecture :-
Recommended by LinkedIn
Here major R&D part was to create a lambda edge function which implements an appropriate authentication mechanism. So, let’s begin with the solution.
So first I added an SSH Public key for the IAM user who would require access. Generally, these SSH key is used for AWS CodeCommit, but here I would be using it for authenticating user.
Generating ssh keys :-
Adding ssh key for authorized IAM user :-
One of the major challenges is how viewer request lambda function can take username & key as input from client. For this I used www-authenticate header. My logic flow was, first time when user hit the CloudFront URL, there would be no authorization header present. Thus, I would be returning www-authenticate header (with basic authentication) as response. Then, because client received www-authenticate header as response, a pop-up box will appear on client browser, which will be asking username & password. As soon as client submit his/her credentials, again request would be sent to CloudFront & again my viewer request lambda edge function will be executed. This time authorization header containing credential submitted by client would be present. So then, function would be decoding and extracting username and key submitted by client. And finally username & key would be compare with actual authorized IAM user’s key(which we have stored earlier). For getting actual authorized IAM user’s key, I used Boto3.
Demo policy I attached to lambda function to access IAM service & get key:-
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"iam:ListSSHPublicKeys",
"iam:GetSSHPublicKey",
"iam:GetUser"
],
"Resource": "arn:aws:iam::173185879405:user/ayush.ganatra"
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": "iam:ListUsers",
"Resource": "*"
}
]
}
Demo viewer request lambda function which I created :-
import base64
import boto3
def lambda_handler(event, context):
# Connecting To IAM and getting key
client = boto3.client('iam')
authorize_username = "ayush.ganatra"
response = client.get_ssh_public_key(UserName=authorize_username,SSHPublicKeyId='APKASQUVOZVWW7RMWW4J',Encoding='SSH')
authorize_key = response["SSHPublicKey"]["SSHPublicKeyBody"]
# Getting username & key submitted by Client
request = event['Records'][0]['cf']['request']
headers = request["headers"]
get_val = headers.get('authorization')
if get_val:
authString = headers["authorization"][0]["value"]
credentials = authString.split()[-1]
decoded_credentials = base64.b64decode(credentials).decode('ascii')
input_username = decoded_credentials.split(":")[0]
input_key = decoded_credentials.split(":")[-1]
# Authenticating
if ( (authorize_username != input_username) or (authorize_key != input_key) ):
response = {'status': '401',
'statusDescription': 'Unauthorized',
'body': 'Unauthorized',
'headers': {'www-authenticate': [{'key': 'WWW-Authenticate', 'value':'Basic'}]},}
return response
else:
response = {'status': '401',
'statusDescription': 'Unauthorized',
'body': 'Unauthorized',
'headers': {'www-authenticate': [{'key': 'WWW-Authenticate', 'value':'Basic'}]},}
return response
return request
I was actually astonished by the fact that, by simply combining 2-3 general concepts we can solve such unique & interesting use cases. By using this approach, I was successfully able to authenticate user. See the demo video below :-
Thanks for reading,
Happy Learning & Happy Exploring 😊
Great article 👍☺️
helpful