One of the most common requirements for AMPS instances is integration with an enterprise security system. In this blog post, we’ll show you the easiest way to get an integration up and running – by building an authentication and entitlement system from scratch!
In versions of AMPS prior to 5.0, the only way to integrate with an enterprise system was to create a server module to handle authentication and entitlement. AMPS version 5.0 and later include an optional module that can use a RESTful web service for authentication and entitlement. Using this module can be the easiest way to integrate into an existing system.
In this blog post, we’ll:
- Explain how the module works
- Show you how to configure AMPS to use a simple authentication web service
- Give you a simple sample implementation of a web service using Flask
That might sound like a lot of ground to cover, but don’t worry – this is a lot simpler than you might expect, and the Flask framework in this blog is a great starting point to use for a production-ready system, whether you’re developing a standalone service or a fully-featured integration with an existing system.
All of the code discussed here is available in a public github repository. And to give credit where credit is due, the technique for using Basic authentication with Flask is developed from a post by Armin Ronacher on the Flask snippets site.
AMPS Authentication and Entitlement
The AMPS authentication and entitlement system is designed to be straightforward.
Authentication happens when a connection logs on. The request for authentication contains a user name and a token (perhaps a password, perhaps a certificate, perhaps a single-use token).
Entitlement happens when a particular user name first requests a particular type of access to a particular resource. (For example, reading data from a particular topic by subscribing to the topic, or publishing to a particular topic.) After AMPS has checked entitlements with the module once, AMPS caches the results for future use.
So how do those steps translate to a RESTful web request?
Making it RESTful
The RESTful authentication and entitlement system does two things:
- Converts an AMPS
logon
command into an HTTP request for a permissions document, using the credentials in thelogon
command. - Parses the permissions document and uses the permissions in that document to grant entitlements when AMPS requests an entitlement check.
That’s all there is to it.
(The full documentation for the module is in the Authentication and Entitlement using a Web Service chapter of the User Guide.)
Basics of Basic Authentication
The module supports both Basic and Digest authentication. You can read more about these in RFC7617 for Basic authentication, and RFC7616 for Digest authentication.
For this post, we’ll look at Basic authentication.
In Basic authentication, the web server (in this case, our web service) denies access to resources unless the requestor (in this case, AMPS) provides an appropriate Authorization header. If the Authorization header isn’t present, or the credentials aren’t accepted, the web server returns a 401
status with a WWW-Authenticate
header that indicates that it accepts Basic authentication (and provides a token that indicates the scope of the authentication request).
It’s common for a requestor to send a request without credentials, then use the response from the server to decide what sort of authentication to use. The AMPS web service module uses this approach. When the web server indicates that it uses Basic authentication, the module returns a Authorization header with the credentials for the logon request.
GET It Done
To be able to pass credentials, the module needs to know the contents of the logon request, and the URI to use to retrieve the permissions document.
The URI is set in the module configuration (described below). The module allows you to substitute the user name of the user logging in to AMPS at any point in the URI.
To convert the logon request into an HTTP request, the module takes the user name and password and uses those to authenticate the HTTP request. The module supports both Basic and Digest authentication.
For example, AMPS receives a logon command along these lines:
{
"c":"logon",
"cid":"1",
"client_name":"subscriber-AMPSGrid-Falken-host26",
"user_id":"falken",
"pw":"joshua",
"a":"processed",
"v":"5.2.1.4:java"
}
The user name used for HTTP authentication will be falken
, and the password used for HTTP authentication will be joshua
.
If the module is configured to use http://internal-webhost:8090/{{USER_NAME}}.json
as the URI, the module substitutes falken
for the {{USER_NAME}}
token to request /falken.json
from the webserver at internal-webhost:8090
.
To determine whether to use Basic or Digest authentication, the module first sends an unauthenticated request to the server. The server responds with a 401
response that contains a WWW-Authenticate
header, as described above, and AMPS responds with an authenticated request along these lines:
GET /falken.json HTTP/1.0
User-Agent: AMPS HTTP Auth/1.0
Accept: */*
Authorization: Basic ZmFsa2VuOmpvc2h1YQ==
Notice that the Authorization
header contains a base64-encoded representation of the username and password provided on logon.
Putting It Together: AMPS Configuration
With that background, let’s configure AMPS to use the web service module for authentication and entitlement.
First, load and configure the module:
<Module>
<Name>web-entitlements</Name>
<Library>libamps_http_entitlement.so</Library>
<Options>
<ResourceURI>http://localhost:8080/{{USER_NAME}}.json</ResourceURI>
</Options>
</Module>
Then, specify that the module is used for authentication and entitlement for the instance.
<Authentication>
<Module>web-entitlements</Module>
</Authentication>
<Entitlement>
<Module>web-entitlements</Module>
</Entitlement>
With this configuration, AMPS will use the web service module for authentication and entitlement for all connections to the instance, and will be looking for a service at port 8080 on the local machine.
All we need now is a web service that can use basic authentication and serve up permissions documents!
Service, Please! Coding the Web Service
While the web service module works perfectly well when requesting static documents from a web service such as Apache or nginx, for the purposes of this post, we’ll show how simple it is to use a standard HTTP application framework to build the web service.
We’ll use Flask for this sample, since it’s readily available (and we like Python!) Further, the extension points for Flask are straightforward, which makes it very handy for demonstration purposes.
To create the service, we first import the things we’ll need from Flask and define the service:
from flask import Flask, request, Response
from functools import wraps
# create the flask app example
app = Flask(__name__)
Next, we’ll need to define a function to return a 401
response that requests Basic authentication:
def authenticate():
"""Sends a 401 response that enables basic auth"""
return Response(
'Could not verify your access level for that URL.\n'
'You have to login with proper credentials', 401,
{'WWW-Authenticate': 'Basic realm="AMPS Authentication"'}
)
This function uses Flask to return a 401
response with an appropriate WWW-Authenticate
header and a helpful message.
We also provide a function that can wrap a response: this function checks to see if the credentials provided are valid by calling a check_auth
function. If so, the wrapped function is called. If not, the wrapper calls the authenticate
function to return the 401
response.
def requires_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
if not auth or not check_auth(auth.username, auth.password):
return authenticate()
return f(*args, **kwargs)
return decorated
This function makes it easy to validate credentials before providing a permissions document. We use wraps
from the functools
module to create a @requires_auth
decorator for a function. What this means in practice is that, once we’ve defined this function, we can easily require successful authentication for a function by adding the @requires_auth
decorator on a function.
The Flask infastructure handles decoding and parsing the Authentication
header, so all we have to do is pass the values along to the check_auth
function.
Now, the infrastructure is in place – all we have to do is write two functions: check_auth
to verify the username and password, and a function to return a permissions document given an authenticated username.
For sample purposes, we just hard code the username and password in the authentication function:
def check_auth(username, password):
"""This function is called to check if a username/password is valid."""
return username == 'falken' and password == 'joshua'
Of course, in a real system, this check could do anything that it needs to, including calling out to an external system to verify the credentials.
Last, we define a function that, given an authenticated user name, returns the permissions for that user. For demonstration purposes, we just return a hard-coded JSON document. We use the wrapper defined earlier to ensure that control only enters this function if the username and password are validated by check_auth
.
@app.route('/<username>.json')
@requires_auth
def permission_document(username):
return """
{
"logon": true,
"topic": [{
"topic": ".*",
"read": true,
"write": true
}],
"admin": [{
"topic": "/amps/administrator/.*",
"read": false,
"write": false
}, {
"topic": ".*",
"read": true,
"write": true
}]
}
"""
This function is called for any HTTP request that matches the /<username>.json
pattern. The function is only invoked after the requires_auth
wrapper accepts the credentials in the request.
This permissions document grants logon
access to the instance, and allows full read and write of any topic. For the admin console, the document allows access to any resource except those under the /amps/administrator
path. As usual, the full syntax for the permissions document is described in the User Guide.
Last, we need to start the server when the script is loaded:
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
… and that’s it! With those few lines of configuration and code, we have a working authentication and entitlement system.
Would You Like To Play AMPS?
With AMPS configured as shown above and the simple Python script running, we can try out the authentication and entitlement system (remember, both the configuration and script are available from the github repository.)
We first start AMPS:
$ ampServer config.xml
AMPS 5.2.1.92.210270.984c1d9 - Copyright (c) 2006-2017 60East Technologies Inc.
(Built: 2018-03-17T23:01:09Z)
For all support questions: support@crankuptheamps.com
And then start the authentication and entitlements service:
$ python auth/main.py
* Running on http://0.0.0.0:8080/ (Press CTRL+C to quit)
Then, you can use the ping
command in spark
to test the credentials.
First, try with valid credentials.
$ spark ping -server falken:joshua@localhost:9007 -type json
Successfully connected to tcp://falken:*@localhost:9007/amps/json
Then, try with credentials that the authentication and entitlements service doesn’t recognize:
$ spark ping -server matthew:ferris@localhost:9007 -type json
Unable to connect to AMPS (com.crankuptheamps.client.exception.AuthenticationException: Logon failed for user "matthew").
It’s just that simple!
Where Next?
You can use this Flask framework as the foundation for a more fully-featured system by replacing the check_auth
and permission_document
functions. If you need to integrate with a back-end system, you can use the check_auth
function to check the credentials with that system and respond accordingly. Likewise, the permission document format has been designed to be easy to construct, so you can translate whatever format your existing permissions system uses into the permissions document provided back to AMPS.
Or, if you prefer another framework for creating an HTTP server, you can use the basic pattern here to quickly implement a server in your framework of choice. It’s up to you!
What HTTP framework do you prefer, and what authentication and entitlement systems are you integrating with? Let us know!