Home > Development > Puma: building a requester application

Puma: building a requester application

Here comes the second part of our hands-on introduction to our lovable Python UMA implementation – PUMA. As Part I, 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 Requester Applications.

Architecture overview

Just like with the host application, the requester app also has a central linking entity that is used to identify a user – RequestingParty. A RequestingParty will need an access token for each AM it interacts with, hence: multiple RequesterAccessToken entities. Every entity of this type is of course linked to an AuthorizationManager entity that represents the AM the token pertains to.

So, let’s talk about tokens. The requester application has to deal with two types of tokens:

AM tokens
these are regular OAuth tokens. There’s one per a requesting party-AM pair. This one allows you to access the AM’s endpoints.
RPT
these are used to access resources at the host applications. You will have at least* one per host. You obtain these by asking the AM for it; and for that you need to have the AM token first.

There’s a family of calls that will let you obtain, set, change, update, retrieve (and so on) these tokens, so you need not worry about storing them.

What the flow (usually) looks like

Check out this neat graph to get an idea of what the code flow in a requester application looks like. Remember that your mileage may vary slightly.


Subsequent requests are simpler.


Discovery

This part is identical to what is described in the “host” section (well, you may be interested in different endpoints, but the method is the same). Check it out.

Getting a token

Thankfully, same as host-side.

For starters: making a regular HTTP request

Assume that the user of the requester application (the requesting party, if you will) tells the application to fetch what’s under this URI:

https://pumahostone.appspot.com/api/people/alice/personal/name

In order to act smart, it makes sense to first do a pre-flight request for the resource and see how the server responds. We’re not really interested in the data itself right now, as we expect the request to fail, but the response headers (especially WWW-Authenticate) should contain some really valuable information. So let’s make a pre-flight (note: it’s a HEAD request):

uri = "https://pumahostone.appspot.com/api/people/alice/personal/name"
response = Puma.Util.preflight_request(uri)

response will be an httplib.HTTPResponse object. Two things you’ll want to check for to make sure the object is UMA protected:

  • response status is 401
  • there’s an UMA-flavored WWW-Authenticate header set, with the name of the AM protecting the resource you just asked for. So you know who to ask for access.

For example (a bit of code from the pumarequesterone application):

if response.status == 401 and Puma.Util.check_www_auth_header(response):
    am_data = Puma.Util.check_www_auth_header(response)
    am = Puma.UMA.discover_am_from_www_auth(am_data)

Something to note about the last line: if the AM had already been discovered, it will simply return the existing AM object from the storage layer.

Now that we have am, we check if we’re OAuth-registered (and if not, we register):

if not am.client_id and not am.client_secret:
    registration_data = Puma.Pouches.RegistrationData()
    registration_data.client_description = "Puma, Requester One."
    registration_data.client_icon = ""
    registration_data.client_url = "https://pumarequesterone.appspot.com"
    registration_data.client_name = "Puma Requester One"
    registration_data.redirect_url = "https://pumarequesterone.appspot.com/callback"

    Puma.OAuth.oauth_registration(am, registration_data)

Now we have the AM out of the way. We’re confident the application is properly set up to talk to it, now let’s make sure the user (also called the Requesting Party, RP for short) is just as ready:

# user_key is the logged-in user's unique identifier

rp = Puma.Util.check_if_user_has_requesting_party_identity(user_key)
if rp is None:
    # first time UMA interaction
    rp = Puma.Util.create_requesting_party_identity(user_key)

A RequestingParty “identity” (an object) is the requester-equivalent of an UMAUser. Refer to the architecture overview if your memory fails you (or you skipped it and are now helplessly scratching your head).

Great. Now, the RequesterAccessToken entity has a dichotomous role:

  • contains the AAT
  • contains a set of RPTs (internally called an RPT wallet, such an unimaginative name..)

Let’s leave that for now. We need a RequestingParty, with the AAT. How is an AAT obtained? Well, it’s a regular OAuth token, so, naturally, you have to send the user through an OAuth flow. Code should look familiar:

rat = Puma.Storage.get_rat_for_am(rp, am) #
if rat is None:
    Puma.Util.set_pending_registration(rp, am)
    self.redirect(Puma.OAuth.get_rat_authz_uri_for_am(am, 'https://pumarequesterone.appspot.com/callback'))
    return

Callback expects an authorization code grant will be passed to it via the code parameter:

#::-- inside callback handler ('/callback')

code = self.request.get("code")
if code:
Puma.OAuth.trade_code_for_rat_and_store(
current_user_key,
code,
"https://pumarequesterone.appspot.com/callback",
)

session.set_flash("Callback redirect you here. You should have a requester token now.")
self.redirect('/')
#::--

TBD

host_id = am_data["host_id"]
rpt = Puma.Util.get_rpt_for_host_id(rp, am, host_id)
if not rpt:
    rpt = Puma.Util.obtain_and_store_rpt_for_host_id(rp, am, host_id)

urlparsed = urlparse(resource_uri)

if urlparsed.scheme == 'https':
    hx = httplib.HTTPSConnection(urlparsed.netloc)
else:
    hx = httplib.HTTPConnection(urlparsed.netloc)

headers = {
    'Authorization': 'Bearer ' + str(rpt),
}

hx.request('GET', urlparsed.path, '', headers)

re = hx.getresponse()

This sends a token-equipped (RPT-equipped) HTTP request to the host application. The response will generally be one of:

  • HTTP 200 (OK): the host application determined that the token has enough permissions assigned to it, and allowed access; the response body is the requested resource.
  • HTTP 403 (Forbidden): the token is fine & valid, but it doesn’t have the required permissions associated with it yet. The response will contain a WWW-Authenticate
    header, with the ticket parameter set. This ticket will be used to upgrade (i.e. assign new permissions to a token).

Since the 403 response is practically guaranteed to be the response with a freshly-acquired RPT, here’s the code that handles it:

if re.status == 403:
www_auth = re.getheader('www-authenticate')
ticket = Puma.Util.ticket_from_www_auth(www_auth)

claims_requested = Puma.UMA.send_ticket_to_claims_endpoint(rat, ticket, www_auth)
# claims_requested will be an array of claims that the requester needs to provide in order to get access

# at the moment, there's only one type of claim that you will encounter:
# that's redirect_required, which asks for a redirect that validates your identity at the AM.
for claim in claims_requested:
if claim['claim_type'] == 'redirect_required':
logging.info("Redirecting to %s" % claim['claim_value'])
self.redirect(claim['claim_value'])

After the redirect is complete, you should end up back at the requester application, your RPT now bearing new powers. You should now try to access the resource again (this can be automated – look at the code for /callback to see the trickery.

This time the request should be a 200 OK:

if re.status == 200:
    # resource requested in response body
    response_body = re.read()

And you’re done.

An insight into PumaRequesterOne: the tiny mighty callback handler

Apart from acting as a regular OAuth 2.0 callback handler, the requester’s /callback also needs to double as a permission upgrade redirect handler (vague? see previous paragraphs about the redirect_required claim).

So /callback handles four cases, as outlined below:

class RequesterCallbackHandler(webapp.RequestHandler):
    def get(self):

    #
    # two ways through this:
    # - regular OAuth flow (code)
    # - claims flow (x-oauth_*):
    # ~ alice to alice
    # ~ alice to bob
    #

    code = self.request.get("code")
    x_oauth_access_granted = self.request.get("x-oauth_access_granted") # alice-to-alice
    x_oauth_access_req = self.request.get("x-oauth_access_req") # alice-to-bob
    claim_status = self.request.get("claim_status") # openid claim

    if code:
        # standard OAuth 2.0 flow

    elif x_oauth_access_granted:
        # post-claims redirect; access granted (Alice-to-Alice)

    elif x_oauth_access_req:
        # post-claims redirect; access requested (Alice-to-Bob)

    elif claim_status:
        #

    else:
        # fallback.

    if session['transaction_type'] == 'fetch':
        self.redirect('/fetch')
    elif session['transaction_type'] == 'update':
        self.redirect('/update')
Categories: Development
  1. No comments yet.
  1. No trackbacks yet.

Leave a comment