Monday, March 1, 2021

Getting AWS Cognito user pool users to access different S3 resources

I started working with AWS and oAuth. Compared to Azure, finding information on AWS is quite challenging. There are many docs in AWS, but they are auto-generated like isolated explanations than answering the questions you have when doing a real-life solution.

My task was to allow S3 access to users who don't have AWS accounts, using oAuth. Amazon oAuth infrastructure is in the Amazon Cognito service.


Authorization


Once the identity and User pools are created, you need to add an app client for the user pool. App client controls the attributes of keys such as the lifetime. Refresh, access, and Id tokens have unique values for the validation period. Then calling the authorize and token endpoints with appropriate parameters provide refresh token and access token. The scope should include the parameter OpenId to receive an OpenID token from the refresh/authorize keys. The access token is no good to access S3 as it doesn't contain the token issuer's details. Different identity providers such as Google and Facebook are supported by the Cognito identity pool, but I chose to use the Cognito identity provider. The identity provider does not make much of a difference to the process. Cognito identity doesn't have any deep relationship with S3 but treated as any other provider.


Identity


Once the user/identity pool is created, auth and unauth roles from the identity pool are visible in the IAM console under roles (Primary reason I made them in the same AWS account as S3). You can create granular policies in the policy section, defining who can access what resources, at what level by each role. Though the policies can be tied directly to the buckets or parts of it, I created an access point that makes managing more comfortable.


Accessing S3 resources using aws cli


I had to work for days on this. Though the AccessToken is granted from Cognito, there was no interface to add it to S3 APIs. S3 APIs look for access keys and a secret, usually associated with the IAM users. Finally, it turned out asking for keys by providing an OpenID does the trick. If the string "OpenID" is in the scope when asking for authorization, Cognito sends an OpenID with the following format. 


{

  "kid": "lXY ...... =",

  "alg": "RS256"

}

JSON {

  "at_hash": "4y9WXD5LhlIBA1jSNMnjYA",

  "sub": "c4 ,...... 88",

  "email_verified": true,

  "iss": "https://cognito-idp.<region>.amazonaws.com/<UserPoolId",

  "phone_number_verified": true,

  "cognito:username": "c3.....3",

  "given_name": "name",

  "aud": "....",

  "token_use": "id",

  "auth_time": 1613947027,

  "nickname": "<name>",

  "phone_number": "<PhoneNumber>",

  "exp": 1614557229,

  "iat": 1614553629,

  "family_name": "<Surname>",

  "email": "<email>"

}


To get the temporary ids, Cognito needs the user pool id. Though this is available in Console when browsing identities, asking for it by sending the key makes it more generalized. When sending this to retrieve a temporary credential, the key is sent in the logins option, which has the format "provider=key". Use awscli command getid to get temp credentials. aws cognito-identity get-id --identity-pool-id <identity pool id>> --logins "cognito-idp.<region>.amazonaws.com/<identity pool id>=<id token>" Which sends the user pool id in the format,


{

    "IdentityId": "<user pool id>"

}



Now we have everything required to ask for the temporary key using get-credentials-for-identity.


aws cognito-identity get-credentials-for-identity --identity-id "<user pool id>" --logins "cognito-idp.<region>.amazonaws.com/<identity pool id>=<id token>"

Response is in the format,


{

    "IdentityId": "<user pool id>",

    "Credentials": {

        "AccessKeyId": "ASIAOKHKDLENZPN72UMU",

        "SecretKey": "TKxoZruHTjhyD87kjDjhjHSJ7uj78zB9rXpbMZ2/",

        "SessionToken": "IQoJb3J ……… H+FMZ4azHYaoo26uXB3A1dBLWe4fqLNG03RCVTMahodcnPs+2LiBzA/vlPf/Zo8=",

        "Expiration": "2021-03-01T14:22:23+13:00"

    }

}


There are many ways to provide the credentials, but I chose the most straightforward method - setting them as environment variables. When aws APIs look for credentials, environment variables has the highest priority.


Set AWS_ACCESS_KEY_ID=<accessKeyId>

Set AWS_SECRET_ACCESS_KEY=<SecretKey>

Set AWS_SESSION_TOKEN=<Session Token>



Now, use the list command to list the contents of the s3 access point.


> aws s3 ls s3://<access point name>


                           PRE testdata/

2020-11-20 13:00:42       1011 test.csv

2020-11-20 13:00:42       2338 test - (Copy).csv


Download a file using cp.

aws s3 cp s3://<access point arn>/test.csv c:\temp


download: s3://<arn>/test.csv to ..\..\temp\test.csv







Friday, July 24, 2020

Azure/SQL table to Kafka topic in JSON format using jq

When working with any complex environment, using a static set of data makes developing and testing easier at early stages. With Kafka, one way to do this is sending each raw in the table as a message to Kafka. Providing them in JSON or similar is better than CSV though there is a higher overhead due to the headers in each message, yet it is viable than dealing with missing fields etc. in CSV.


I used the Centos system that has Kafka and SQL command line installed. Starting from command line given in my earlier post here, we can get table convert to JSON.


sqlcmd -S <youraccount>.database.windows.net -U user -P PW -d northwind -s~ -y 0  -Q "select * from orders" | jq -c --slurp --raw-input --raw-output  "split(\"\n\") |  map(split(\"~\")) |map({\"OrderID\":.[0], \"CustomerID\":.[1], \"EmployeeID\":.[2], \"OrderDate\":.[3], \"RequiredDate\":.[4], \"ShippedDate\":.[5], \"ShipVia\":.[6], \"Freight\":.[7], \"ShipName\":.[8], \"ShipAddress\":.[9],\"ShipCity\":.[10],\"ShipRegion\":.[11],\"ShipPostalCode\":.[12], \"ShipCountry\":.[13]})" 

When sending messages to Kafka, each row has to be a JSON object rather than an array.  jq 'Array/Object value iterator (.[]) does exactly that. 

sqlcmd -S <youraccount>.database.windows.net -U user -P PW -d northwind -s~ -y 0  -Q "select * from orders" | jq -c --slurp --raw-input --raw-output  "split(\"\n\") |  map(split(\"~\")) |map({\"OrderID\":.[0], \"CustomerID\":.[1], \"EmployeeID\":.[2], \"OrderDate\":.[3], \"RequiredDate\":.[4], \"ShippedDate\":.[5], \"ShipVia\":.[6], \"Freight\":.[7], \"ShipName\":.[8], \"ShipAddress\":.[9],\"ShipCity\":.[10],\"ShipRegion\":.[11],\"ShipPostalCode\":.[12], \"ShipCountry\":.[13]})" | jq .[]

This is not ideal as it makes the JSON pretty by adding new lines after each field. Using -c option solves the problem.

sqlcmd -S <youraccount>.database.windows.net -U user -P PW -d northwind -s~ -y 0  -Q "select * from orders" | jq -c --slurp --raw-input --raw-output  "split(\"\n\") |  map(split(\"~\")) |map({\"OrderID\":.[0], \"CustomerID\":.[1], \"EmployeeID\":.[2], \"OrderDate\":.[3], \"RequiredDate\":.[4], \"ShippedDate\":.[5], \"ShipVia\":.[6], \"Freight\":.[7], \"ShipName\":.[8], \"ShipAddress\":.[9],\"ShipCity\":.[10],\"ShipRegion\":.[11],\"ShipPostalCode\":.[12], \"ShipCountry\":.[13]})" | jq -c .[]

Now each row is a single JSON object as below.


Next task is to create a topic in Kafka, say orders. Using the bundled command line script to create topics, and using the default values,

/opt/kafka/bin/kafka-topics.sh --zookeeper localhost:2181 --create --topic orders --partitions 1 --replication-factor 1

Now it is just a matter of sending the data to the Kafka topic using the bundled producer script. Simply pipe the output of sqlcmd to the command line producer as below.

sqlcmd -S <youraccount>.database.windows.net -U user -P PW -d northwind -s~ -y 0  -Q "select * from orders" | jq -c --slurp --raw-input --raw-output  "split(\"\n\") |  map(split(\"~\")) |map({\"OrderID\":.[0], \"CustomerID\":.[1], \"EmployeeID\":.[2], \"OrderDate\":.[3], \"RequiredDate\":.[4], \"ShippedDate\":.[5], \"ShipVia\":.[6], \"Freight\":.[7], \"ShipName\":.[8], \"ShipAddress\":.[9],\"ShipCity\":.[10],\"ShipRegion\":.[11],\"ShipPostalCode\":.[12], \"ShipCountry\":.[13]})" | jq -c .[] | /opt/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic orders