Home > Development > Puma: building a host application

Puma: building a host application

What you see here is the first part of our hands-on introduction to our lovable Python UMA implementation. It may not be all that brief, but it is wildly comprehensive and grants you the hottest superpower on the market: rapidly creating sleek, UMA-enabled applications.

Let’s get to work, then.

Terminology

Before we talk, we need to establish some fundamental terminology:

  • host application: a regular web application that hosts data of some kind (which you would be interested in sharing) — be it pictures, your grades, your personal data, anything.
  • requester application: a web application that would like to use data hosted by another application (the host application, described above)
  • authorization manager: at the very center of UMA sits the authorization manager. The essential piece of the puzzle — you should check UMA’s wiki for a wealth of information.
  • PAT: protection API token (previously: host access token). A simple OAuth 2.0 access token; used by the hosting application to interact with the AM.
  • AAT: authorization API token (previously: requester access token). A simple OAuth 2.0 access token; used by the requesting application to interact with the AM to identify the requesting party (the user of the requesting application)
  • RPT: requester permission token. A token used for representing permissions over at particular host. This is the token that is used during any UMA interaction between the host and requester applications.
  • policy: a setting at the AM that describes who can access what; for example: “Alice@gmail.com can access my calendar”
  • token upgrade: we say a token is upgraded when new permissions are assigned to it

Custom MIME types

Look through the list. This should give you an overview of the messages used in UMA.

  • application/uma-scope+json: represents an UMA scope
  • application/uma-resource-set+json: represents a resource set description
  • application/uma-status+json: represents a response containing information about the status of a token
  • application/uma-requested-permission+json: used when the host asks the AM to issue a new ticket to allow token upgrade.
  • application/uma-permission-ticket+json: indicates that the response contains a ticket – an opaque value that can be used by the requester application to add new permissions to a token.
  • application/uma-access-token+json: indicates that the response contains a new token. For example, this is the case when the requesting application asks the AM to issue an RPT for a new host.

Puma’s anatomy

Visualization is key!

As you may have somehow noticed, calls and objects are split into five categories:

      • Puma.UMA.*- the essence of UMA. Resource management, token handling — here’s the place.
        • .discover_am_endpoints(…)
        • .register_resource(…)
        • .check_token_status(…)
        • … et al
      • Puma.OAuth.*- everything that pertains to the OAuth-flavored side of UMA, bespoken for Puma. From trading grants, via tokens, all the way to gluing together those authorization URLs.
        • .get_authz_uri(…)
        • .trade_code_for_hat_and_store(…)
        • .refresh_token(…)
        • … et al
      • Puma.Util.*- utility functions for Puma. Not merely optional helpers, so be sure to pay attention to these.
        • .get_owner_of_resource(…)
        • … et al
      • Puma.Storage.* – an attempt to abstract away from the reality of the underlying data storage mechanisms.
      • Puma.Pouches.*- objects that act as containers and help you make sure that all those calls get just the data they need. Simple as that.
        • .ResourceSetDescription
        • .RegistrationData
        • … et al

Host application

Imagine an application that stores some of your most often used personal data (perhaps an address? or list of schools attended?); imagine also that the application can expose your data the the world via a RESTful API, such that – just as an example – your home address would be accessible under..

https://app.example.com/people/alice/address/home

For example, on our own Puma Host One, Alice’s home address is available under:

https://pumahostone.appspot.com/api/people/alicja/address/home

Architecture overview

So it’s assumed that the application has some notion of users/accounts, and that users are uniquely identifiable by an ID of some sort. That is the only part of the application’s infrastructure that Puma needs to be able to tap into (and even this in a very superficial and completely non-intrusive manner). How it builds on top of that can be seen below (explanation follows, of course):

” alt=”” width=”90%” />

Let’s step through this:

AuthorizationManager
These entities hold information about known (discovered) authorization managers. The data they hold is related to the two phases an AM introduction process goes through:

  • discovery: the AM’s endpoints (in other words, the API), used to register resources, do token magic and so on.
  • registration: the AM’s [OAuth] credentials, most importantly the client_id and client_secret
ResourceMap
A single ResourceMap entity represents and holds information on a resource, existing on the host, that has been set up for protection with UMA.
Data stored includes, among others..

  • the resource URI
  • the ID of the resource
  • the resource name & short description (optional)
UMAUser
Of course, the aforementioned entities need to be linked together somehow. After all, we’ll be asking questions such as:

  • what AM does $user want to use?
  • who owns a ResourceMap?
  • … and obviously many more (but they’re all just as trivial)

Enter UMAUser. The user’s UMA-powered alter-ego identity on the host application.
It has a monogamous relation (has) with an AuthorizationManager, an outside User entity; as well as a polygamous (has-many) relationship with ResourceMaps.

Simple enough. Let’s get some action.

Discovery

Any application that wishes to talk with the AM must first learn about its endpoints; each endpoint has a well-defined, specific role to fulfill. The go-to place for learning about the AM’s endpoints is always:

http://www.smartam.net/.well-known/uma-configuration

While you don’t even need to interact with it directly, it’s probably a good idea to know that an uma-configuration file looks like this:

{
    "version": "1.0",
    "issuer": "http://www.smartam.net",
    "dynamic_client_registration_supported": "yes",
    "token_types_supported": ["artifact"],
    "host_grant_types_supported": [
        "authorization_code",
        "client_credentials"
     ],
    "claim_types_supported": ["openid"],
    "client_dynamic_registration_endpoint": "http://www.smartam.net/api/oc/register",
    "host_token_endpoint": "http://www.smartam.net/oauth/token",
    "host_user_endpoint": "http://www.smartam.net/oauth/authorize",
    "resource_set_registration_endpoint": "http://www.smartam.net/api/uma/resource_reg",
    "token_status_endpoint": "http://www.smartam.net/api/uma/validation",
    "permission_registration_endpoint": "http://www.smartam.net/api/uma/permissions_reg",
    "requester_token_endpoint": "http://www.smartam.net/oauth/token",
    "requester_user_endpoint": "http://www.smartam.net/oauth/authorize",
    "permission_request_endpoint": "http://www.smartam.net/api/uma/permissions_grant/ticket"
}

All you have to do with Puma is:

am = Puma.UMA.discover_am_endpoints("www.smartam.net")

What it does:

      • reads the JSON resource found under .well-known/uma-configuration
      • parses it, extracting endpoint info
      • stores the data deep below, in the storage layer, as an AuthorizationManager entity.
      • if the call is ever made with the same hostname that was just discovered, the call simply returns the existing AM data

And you might want to keep that am object near. Because after discovery you’ll usually need…

Registration

To talk to the AM, the host needs to obtain an OAuth 2.0 token for use with the AMs endpoints. This is the PAT, or protection access token. We’ll need to go through a really standard OAuth flow to get it. For that, our applications need to be registered with the AM – in an OAuth sense; that means they need to have a client_id and client_secret.

Normally, the application is manually registered by the owner. Because it would be impossible to guarantee that every host and requester application out there would be registered with any AM they might be asked to work with (and the beauty of UMA is also in the freedom to choose an AM), AMs may support dynamic registration.

It’s pretty straightforward. Actually, one could argue that it’s easier than manual registration. So how do you register?

Puma.OAuth.oauth_registration(am, registration_data)

.. where registration_data is (let’s call it rd to make it short):

rd = Puma.Pouches.RegistrationData(
    client_name="Puma Host One",
    client_description="Puma Host One.",
    client_url="https://pumahostone.appspot.com",
    client_icon="https://pumahostone.appspot.com/static/images/resource_icon.png",
    redirect_url="https://pumahostone.appspot.com/callback"
)

After this call, you’re all set. The application has been officially introduced to the AM.

Getting a token

To interact with the AM, the application has to get a token that represents the user of behalf of whom it is to communicate. This is a standard, nearly boring, OAuth 2.0 flow. The application should direct its user to a specially crafted authorization URL. The endpoint is:

http://www.smartam.net/oauth/authorize

That’s not enough. A proper URL that will lead to an authorization dialog at the AM is more like the following:

http://www.smartam.net/oauth/authorize?
client_id=host_client_id&
redirect_uri=http://host.example.com/redirect&
response_type=code

Puma will generate this for you without a problem. It takes a unique user identifier as an input, looks up the AM the user is set to use and constructs a proper authorization dialog URL. Handy:

authz_uri = Puma.OAuth.authz_uri_for_user(uma_user, redirect_uri)

You should redirect the user to this URI. When the user selects “allow” (or similar; hopefully anyway!) they will be redirected to your redirect URI. The handler for that URI should extract the code parameter and call:

Puma.UMA.trade_code_for_hat_and_store(code, redirect_uri)

… which will go through the rest of the OAuth 2.0 flow. That is, it will send the code to the appropriate endpoint over at the AM and receive a regular OAuth access token.

Registering a resource

When the hosting application wants to start protecting a resource, it first needs to register the resource with the AM. Registration is simple and intuitive – the resource registration endpoint is:

http://www.smartam.net/api/uma/resource_reg

This endpoint expects to find a resource set description in the request – this is a JSON object that should look like this:

{
    "name": "Photo Album",
    "icon_uri": "http://www.example.com/icons/flower.png",
    "scopes": [
        "http://photoz.example.com/dev/scopes/view",
        "http://photoz.example.com/dev/scopes/all"
    ]
}

The AM responds with an HTTP 201 Created response akin to this:

HTTP/1.1 201 Created
Content-Type: application/uma-status+json
ETag: 126x358adkfgw3
...

{
"status": "created",
"_id": (id of created resource set),
}

With Puma, you need to initialize and fill out an object that imitates the resource set description, Pouches.Puma.ResourceSetDescription:

description = Puma.Pouches.ResourceSetDescription()
description.scopes = [
    "https://pumahostone.appspot.com/uma/scopes/read",
    "https://pumahostone.appspot.com/uma/scopes/write"
]
description.name = "A name for the resource you're registering."
description.icon_uri = "http://unsettling-icons.org/icons/resource.png"
description._id = pre_generated_id

rx = Puma.UMA.register_resource(user_key, description)
[/pumacode]

Doing well. Now that the AM knows that it should be prepared for protecting such a resource (creating policies, handling token inquiries), all that needs to be done is tell the application itself that it should from now on...

1
(continued)

rx = Puma.UMA.register_resource(user_key, description)
response_body = response.read()

# success!
if rx.status == 201:
    json_data = json.loads(response_body) # parse the JSON data into an object

# pull ETag from headers
etag = response.getheader('ETag')
# pull the policy uri from the JSON response
policy_uri = json_data['policy_uri']

# creating a ResourceMap
map = Puma.Util.create_resource_map(real_uri, pre_generated_id, etag, name, icon_uri, policy_uri, user_key)

An insight into PumaHostOne: registering resources

PumaHostOne uses a separate handler to handle resource registration.

Available at /uma/register-resource, it handles any requests to register a resource. You will most likely want to copy this solution, if you decide to implement your own application.

A simplified (logging and some error checking has been stripped) version of the final handler is below, with comments (feel encouraged to check out the real handler as it contains some additional sanity checks that you might want to use as well!):

class RegisterResourceHandler(webapp.RequestHandler):
    @login_required
    def post(self):
      session = Session(self)
      current_user_key = session['key']

      user_key = current_user_key
# --- data from request (name, icon_uri, scopes?)
name = self.request.get('resource_name', None)
icon_uri = self.request.get('icon_uri', None)
scopes = self.request.get('scopes', None)
real_uri = self.request.get('uri', None)

# generate id (here, needed when constructing path)
pre_generated_id = Puma.Util.generate_resource_set_id(real_uri)

# devise a resource set description
description = Puma.Pouches.ResourceSetDescription()

# NOTE: hosting these scopes on AppEngine (be it static or dynamic) turned out to be unreliable
# .. bad luck, perhaps, but we kept getting timeouts. Hence the scopes moved to S3.
# Just a heads-up.

description.scopes = [
    "http://smartcdn.s3.amazonaws.com/pumahost/dev_scopes/read.json",
    "http://smartcdn.s3.amazonaws.com/pumahost/dev_scopes/write.json"
]

description.name = name
description.icon_uri = "http://cdn1.iconfinder.com/data/icons/Mobile-Icons/128/04_maps.png"
description._id = pre_generated_id

rx = Puma.UMA.register_resource(user_key, description)
body = rx.read()

# success, create map
if rx.status == 201:

    json_data = json.loads(body)

    etag = rx.getheader('ETag') # pull ETag from headers
    policy_uri = json_data['policy_uri']

    Puma.Util.create_resource_map(real_uri, pre_generated_id, etag, name, icon_uri, policy_uri, user_key)

    session.set_flash("Resource registered.")
else:
    session.set_flash("Something went wrong.")

# redirect the user back to where he came from
self.redirect('/')

An insight into PumaHostOne: smart-embedding “share”/”manage” buttons

If you’re of the curious type, you’ve already seen PumaHostOne. The buttons to the right of each resource are the subject of our little chat right now.

That’s actually an iframe. In order to allow nice separation of Puma & application code, PumaHostOne has a separate handler that is meant to be used inside an iframe, and display either:

      • a share! button, if the resource is not yet shared (insight: no ResourceMap)
      • settings & stop sharing buttons, when the resource is being protected.

How does it work?

        • the handler sits at /internal/uma_mini_panel
        • input parameters are:
          • uri – the URI of the resource; this is fundamental
          • name – a name for the resource; this is auxiliary
        • so for example, the iframe’s src could be:

/internal/uma_mini_panel?
uri=http://example.com/api/alice/address&
name=Alice’s%20Address

      • on receiving such a request, the handler first checks to see if the resource identified by uri is protected (i.e. – again – no ResourceMap for this URI*)
      • if the resource is protected..
        • retrieves the policy URI for the resource and creates a settings button
        • retrieves the ID for the resource and glues together a working stop sharing
      • if the resource is NOT protected..
        • creates a share!, which on click will send a request to /uma/register-resource, using the name parameter supplied.

Again, don’t be shy and take a peek at the source code. The handler you’re looking for is called UMAMiniPanelHandler. We’re not always that bad at coming up with names, promise.

Checking token status

Whenever an UMA requester makes a request, the requester’s access token is included inside a WWW-Authenticate header. Like this:

WWW-Authenticate: Bearer 39mal1487ig819ree417ee389

The host’s job, in order to determine if the requester is allowed to access the requested resource, is to pass that token to the token status endpoint at the AM — effectively asking “what can this token do?”. Upon receiving a token status description, the host can check if the required access (e.g. a DELETE)

While this step is done by the Warden – a piece of middleware that you can simply wrap your API handler in, here’s what the Warden calls under the hood:

token_status = Puma.UMA.check_token_status(am, hat, request_body)

token_status is an httplib.HTTPResponse object. If you call .read() on it, you’ll have the response body in front of you. An example response looks like this:

[
   {
        "resource_set_id": "119n278b3600",
        "scopes": [
            "https://host.example.com/actions/read",
            "https://host.example.com/actions/write"
        ],
        "exp": 1500819380
    },
    {
        "resource_set_id": "mn07689b5v74",
        "scopes": [
            "https://host.example.com/actions/read",
        ],
        "exp": 1500814321
    }
]

For example (way too carefully detailed):

      1. a GET request for /alice/personal/name arrives at the host
      2. host finds a requester token in WWW-Authenticate
      3. host looks up the AM that protects this resource
      4. host asks AM about the status of the token
      5. AM replies with response just like the one above
      6. host translates /alice/personal/name to a resource id
      7. turns out the id is mn07689b5v74 — there are permissions associated with this resource for this token, good!
      8. host translates GET to https://host.example.com/actions/read (the application must know how to translate actions on particular resources to actions/scopes. There’s no magic here, this is pretty much configured by the owner of the app.)
      9. host finds that this token can read (GET) this resource (mn07689b5v74) — actions for this resource does contain the correct scope URI.
      10. access is granted!

So if the application finds that the the token has sufficient permissions associated with it, it may simply grant access, and the story ends..

If it turns out that the RPT did not have enough permissions to access the requested resource, the host application goes to the AM and asks to register a sufficient permission with the token. The AM sends back a ticket, an opaque value, which is then sent back to the requester by the host. The token is ready to be upgraded.

Upon receiving a ticket (in the WWW-Authenticate header) from the host, the requester contacts the appropriate AM endpoint, passes the ticket, and receives a list of claims required in order to finish the upgrade (i.e. association of new permissions with the RPT).

All this business is swiftly handled by the Warden.

So at last, as a bonus:

Understanding the warden

One of the central parts of UMA is the decision process that happens at the host application whenever a resource is requested. Imagine a request for https://host-application.com/api/people/alice/personal/name, a resource representing Alice’s personal name, is received by the HTTP server. The response is routed to the appropriate handler script that has to carefully analyze the request, and – in most non-trivial cases – consult the AM, inquiring about the validity of the token presented and any associated permissions.

The structure of PumaHostOne can be broken down into two main scripts:

  1. main.py
    the application logic, frontend, the tangible web application itself
  2. restapi-ng.py
    handler for the RESTful API provided by the application

Now, the server knows to route any requests for paths beginning with "/api" to restapi-ng.py, and everything else to main.py.

However, when UMA comes into play, someone has to intercept every API-destined  request and give a verdict on what to do with it. The first, most basic decision is: is the resource UMA protected? Has the user chosen to restrict access to this resource with UMA? If the requested resource is protected, a *ResourceMap* object for the resource’s URI exists in the datastore. Take a look at this truly trivial if .. else, taken straight from the Warden’s code (note that the warden uses a pure WSGI interface to ease any eventual porting efforts):

path = environ['PATH_INFO']</pre>
map = ResourceMap.gql("WHERE real_uri = :1", path.strip('/')).get() # (stripping for consistency)
if map:
# check if resource contains an access token
# speak UMA from now on.
else:
# resource is not protected, pass
# (this may be unclear to people ignorant of CGI/WSGI, read up?)
return self.app(environ, start_response)

Case one: If no such object exists, the user hasn’t decided to use UMA for access management and the request is let through to the real API handler, which prepares a response based on its own logic and responds back to the requester. The warden interferes no more.

Case two: If, however, a ResourceMap object is found, the warden has to act accordingly to the UMA protocol – check for an access token, ask the AM about the status of the token, respond to the requester with a UMA response and so on.

And for the chart-inclined:

It looks like we’re done here. Got questions? We’re here for you. Now go build awesome stuff!

About these ads
Categories: Development
  1. No comments yet.
  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: