Monday, May 20, 2019

A Basic Authorized Service in Amazon AWS

In this post, I will produce one very unimpressive web-based application: a login page giving access to a second page that displays the current date and time. I will use the following AWS technologies to accomplish this:

  • The Simple Storage Service that will serve as a web server hosting publicly available html and javascript pages.
  • Lambda Functions that will provide the login operation, the current date and the authorizer, which serves to prevent unauthorized access to the current date.
  • API Gateway, which will expose the service offered by the Lambda functions as a REST interface.

Amazon documentation is excellent in general and we will follow it whenever possible. However, it misses more elaborate combinations of services like this. The following diagram illustrates the interactions we are trying to accomplish:



The browser gets the client application from an S3 bucket (step 1). This application includes an HTML file and a React application. This latter application uses JavaScript to do the login (step 2) and get the date and time from an online service (step 3). Since this service is protected by an authorization token, this involves the verification of the token (step 4), before the actual access to the date service (step 5). A major problem with this idea is that the services and the pages are hosted in different domains, which makes the browser unable to look for the services in JavaScript, due to Cross-Origin Resource Sharing (CORS) restrictions. We will need to take that into consideration when designing the services and configuring the API Gateway. We may also put the S3 bucket behind the API Gateway, to serve the entire content from the same domain. However, that configuration is slightly more complicated than the alternative I present here.

Let us start with the client application. It involves two files. I called hello.html to the first, and counter.js (inside a js directory) to the other. The HTML file is as follows. Note that it refers the other file:

<!DOCTYPE html>
<html>
    <head>
        <title>
            Hello Authenticated World!
        </title>
        <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
  <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
  <script src="https://unpkg.com/babel-standalone@6/babel.min.js" crossorigin></script>
    <script type="text/babel" src="js/counter.js"></script>
    </head>
    <body>
        <h1>Hello Authenticated World!</h1>
        <div id="counter"/>
    </body>
</html>

The HTML code has a div element that is replaced by a React component in the counter.js file:

class Login extends React.Component {
    constructor(props) {
        super(props);
        this.state = {login : '', password : ''}

        this.handleLoginChange = this.changeLogin.bind(this);
        this.handlePasswordChange = this.changePassword.bind(this);
    }

    changeLogin(event) {
        this.setState({ login : event.target.value })
    }

    changePassword(event) {
        this.setState({ password : event.target.value })
    }

    render () {
        return (
            <form>
                Login <input type="text" value={this.state.login} onChange={this.handleLoginChange}/> <br/>
                Password <input type="password" value={this.state.password} onChange={this.handlePasswordChange}/> <br/>
                <input type="button" value="submit" onClick={this.props.login.bind(this.props.parent, this.state.login, this.state.password)}/>
            </form>
        )
    }
}


class Watch extends React.Component {
    constructor(props) {
        super(props);
        this.state = {date : ''};
    }

    componentWillMount() {
        var theobject = this
        console.log('this =', this)
        console.log('this.props =', this.props)
  fetch(this.props.timeserver, {
            headers:{
              'authorizationToken' : JSON.stringify({ 'token' : this.props.token })
            }
        }).then(data => data.json())
          .then(thedate => theobject.setState({date : thedate}))
    .catch(function(error) {
            console.log('There has been a problem with your fetch operation: ', error.message);
           });
    }

    render() {
        console.log('date =', this.state.date)
        return <h2>{this.state.date}</h2>
    }
}


class Page extends React.Component {
    constructor(props) {
        super(props);
        this.state = {token : undefined, errormessage : undefined };
    }

    authenticated(token) {
        this.setState({ token : token, errormessage : undefined })
    }

    failedauthenticated() {
        this.setState({ token : undefined, errormessage : 'Authentication Error' })
    }

    doLogin(login, password) {
        var theobject = this
        var formparameters = {
            method: 'POST', // or 'PUT'
            body: JSON.stringify({'login' : login, 'password' : password}),
            headers:{
              'Content-Type': 'application/json'
            }
        }
  fetch(this.props.loginserver, formparameters).then(function(data) {
            if(data.status!==200) {
                theobject.failedauthenticated()
                throw new Error(data.status)
            }
            else {
                var json = data.json();
                return json;
            }
  }).then(function(thetoken) {
            console.log('message =', thetoken)
            if ('token' in thetoken)
                theobject.authenticated(thetoken['token'])
  }).catch(function(error) {
            console.log('There has been a problem with your fetch operation: ', error.message);
        });
    }

    render() {
        console.log(this.state.token)
        if (this.state.errormessage != undefined)
            var errormessage = <h2>{this.state.errormessage}</h2>
        else
            var errormessage = <div/>
        if (this.state.token == undefined)
            return (
                <div>
                    {errormessage}
                    <Login parent={this} login={this.doLogin}/>
                </div>
            )
        else
            return (
                <div>
                    {errormessage}
                    <Watch timeserver={this.props.timeserver} token={this.state.token}/>
                </div>
            )
    }
}

ReactDOM.render(<Page loginserver="https://your-service.execute-api.eu-west-1.amazonaws.com/production/login" timeserver="https://your-service.execute-api.eu-west-1.amazonaws.com/production/time"/>, counter);


VERY IMPORTANT: The URLs in the end are not valid. You must replace them with the addresses that you will create on API Gateway, below in this document. (Later edit) A student of mine was complaining about the "this.state.date" and claiming that this could be solved with "this.state.date.body". I haven't tried.

You may upload these two files to an S3 bucket. To learn how to do this, please refer to AWS documentation. The crucial part has to do with making these two files available for public access. Please refer to this URL here, or find it looking for "host web site s3" or something like that in your favorite search engine. I include here the policy that I used in my bucket:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::bucket-name/*"
        }
    ]
}

Essentially, this gives authorization to any principal (user) to access objects in your bucket. Once you succeed, you should be able to point your browser to your new web site to see this:

This page is not ready to do anything yet, we need to do something with the submit button. Let us start with the login lambda function, in Python:

#import jwt
import json


#password = 'Very long, very difficult indeed! It would take ages to attack this very long string. Now the numbers that were missing so far: 34719108435'

def lambda_handler(event, context):
    print(event)
    response = {
        'statusCode': 401
    }
    if 'body' in event:
        print(event['body'])
        contents = json.loads(event['body'])
        if 'login' in contents:
            login = contents['login']
        else:
            login = None
        if 'password' in contents:
            password = contents['password']
        else:
            password = None
        if login != None and login != '' and login == password:
            #code = jwt.encode({'some': 'payload'}, password, algorithm='HS256')
            response = {
                'statusCode': 200,
                #'body': json.dumps({'token' : code.decode('UTF-8')})
                'body': json.dumps({'token' : 'authorized'})
            }

    response['headers'] = {
        'Access-Control-Allow-Origin' : '*', # Required for CORS support to work
        'Access-Control-Allow-Credentials' : True, # Required for cookies, authorization headers with HTTPS 
    }

    return response

See this on how to "create a Lambda function" or search for this exact expression on your search engine.

A few details about this function: to make everything simpler, I am just checking if the login is the same as the password. Needless to say you should never ever implement anything like that. I should have used a standard technology like JSON web tokens (JWT), but that would complicate the exercise a bit. We would need to import a library, following steps like these. Nevertheless, I commented some code that could help you with JWT.

Note also the headers in the response, which enable resource sharing with any origin. This overcomes the different domains of the S3 web site and API Gateway. Without these headers the example will not work.

Once the lambda function is ready, one should go the the API Gateway and link a resource to this lambda function. Check this documentation from AWS for that purpose. Essentially, you need to

  • Create the API.
  • Create the /login resource with CORS enabled.
  • Create a POST method in the /login resource.
  • Deploy the API.
The following figures summarize these steps:






Before actually deploying the service, we may test the /login POST method, by passing the following data in the request body:

{
    "login": "Paul",
    "password": "Paul"
}

The answer should be:

{
  "token": "authorized"
}

and the response headers:

{"Access-Control-Allow-Origin":"*","Access-Control-Allow-Credentials":"true","X-Amzn-Trace-Id":"Root=1-5ce1ad88-4eaf7477d48dd8a4b8ad10f8;Sampled=0"}

Once one deploys the service, the service should enable login, but we still miss the get date function. For that Lambda function, we need the following code (note the headers to enable CORS again):

import json
from datetime import datetime


def lambda_handler(event, context):
    now = datetime.now() # current date and time
    date_time = now.strftime("%m/%d/%Y, %H:%M:%S")
    return {
        'statusCode': 200,
        'body': json.dumps(date_time),
        'headers': {
                    'Access-Control-Allow-Origin' : '*', # Required for CORS support to work
                    'Access-Control-Allow-Credentials' : True, # Required for cookies, authorization headers with HTTPS 
        }
    }

We will associate this Lambda function to a resource called /time in the API Gateway. We enable CORS again:




This time, we need an extra action, to let the 'authorizationToken' go through the /time service. We need to manually enable CORS (checking the box is not enough) and add the 'authorizationToken' to the Access-Control-Allow-Headers list. In the end, we should get something like this in the response:


Don't ever forget to deploy after each change or you will not be able to see anything. In the deploy step you will get the URL to replace the loginserver and timeserver properties in the counter.js file.

One final step is missing: adding an authorizer to protect the /time service. Indeed, without the authorizer you can put its URL on a browser and see the result of the service. We will prevent that from happening, with the help of our final Lambda function:

#import jwt
import json

#password = 'Very long, very difficult indeed! It would take ages to attack this very long string. Now the numbers that were missing so far: 34719108435'


def lambda_handler(event, context):
    if 'authorizationToken' in event:
        #result = jwt.decode(event['authorizationToken'], password, algorithms = ['HS256'])
        result = json.loads(event['authorizationToken'])
        if 'token' in result and result['token'] == 'authorized': #should check time instead
            return generatePolicy('user', 'Allow', event['methodArn'])
        else:
            return generatePolicy('user', 'Deny', event['methodArn'])
    else:
        return 'Unauthorized'



# Help function to generate an IAM policy
def generatePolicy(principalId, effect, resource):
    authResponse = {}
    
    authResponse['principalId'] = principalId
    if effect and resource:
        policyDocument = {}
        policyDocument['Version'] = '2012-10-17'
        policyDocument['Statement'] = []
        statementOne = {}
        statementOne['Action'] = 'execute-api:Invoke'
        statementOne['Effect'] = effect
        statementOne['Resource'] = resource
        policyDocument['Statement'] = [statementOne]
        authResponse['policyDocument'] = policyDocument
    
    # Optional output with custom properties of the String, Number or Boolean type.
    # authResponse['context'] = {
    #     "stringKey": "stringval",
    #     "numberKey": 123,
    #     "booleanKey": True
    # };
    return authResponse

To create a new authorizer use the following parameters. Note the "token source" field, which must match the name of the header we are using to pass the authorization token: authorizationToken.


Then, we must go to the GET method of the /time resource and protect it with the authorizer (you may have to reload the page to see the Token authorizer):


Once you do this, the /time URL is no longer accessible on a browser and we are all set. Once you login with a username equal to the password, you should see this: