Pseudo-code Application Agent

From RavenWiki
Jump to navigationJump to search

Pseudo-code Application Agent
=============================

Jon Warbrick
University of Cambridge Computing Service
Dec 2004 - V1.5

This is a pseudo-code example of how an Application Agent might be
implemented. This block of code should be applied to every HTTP
request that represents a resource managed by the Application Agent,
and the possible outcomes are indicated by the return values of
'FORBIDDEN', 'SYSTEM_ERROR', 'AUTHENTICATED', or 'REDIRECT'. Because
the outcome of this processing may be an HTTP redirect, this
processing must take place before any other output is sent as part of
the HTTP response.

This example is not intended for any particular environment, but is
based on what passes for the logic within the Ucam-WebAuth Perl-CGI
and Apache agents. It (deliberately) doesn't address how configuration
parameters get their values and omits much housekeeping and checking
that would be vital in a real agent. It does assume that configuration
parameters do not change between one request and the next. It also
uses short, cryptic error messages to stand for what should probably
be longer messages (for users) and/or log entries with additional
information (for system administrators).

The code uses CONFIG.<whatever> (e.g. CONFIG.max_life) to refer to
configuration parameters, REQUEST.<whatever> and REPLY.<whatever> to
refer to fields in *authentication* requests and responses (see
waa2wls-protocol.txt for details), and STATE.<whatever> to refer to
data stored in some sort of 'browser session state' storage. 

STATE can most simply be implemented using an HTTP Cookie, but could
be stored in any other way. If using a cookie, the cookie could either
contain the state information directly, or could just contain a
session-id that indexes a server-side state store. Since cookies can
be disabled it's vital to check that they are working if you use
them.  The obvious way to do this is to set one when redirecting in
phase 3 and then to check that it's still there when catching the
response in phase 2. However note that inappropriate cookie paths
and/or a mismatch between the host name used for the initially request
and the 'official' host name of the server can frustrate such
checks. You have been warned.

----------------------------------------------------------------------

# PHASE 0: Attempt to get, but at the moment don't try to process, an
# authentication reply in the current URL

REPLY = get_authreply_from_url()

# PHASE 1: See if we have valid state stored. If so but the status
# code in it isn't 200 ('OK') then we report a suitable message -
# distinguishing between the expected ('user chose to cancel') and the
# unexpected ('it all fell apart'). We delete the state after
# displaying an error messages so that a retry for this request may be
# able to proceed differently. If status is 'OK' but the state
# information has expired we remember that fact. Otherwise, providing
# we don't _also_ have a authentication reply to deal with, we have
# authenticated successfully

IF exists(STATE) THEN

    IF NOT valid(STATE) THEN
        tell_user "Invalid, corrupt or forged state"
        delete STATE
        return SYSTEM_ERROR
        
    ELSEIF STATE.status_code == 410 THEN
        tell_user "You cancelled the authentication"
        delete STATE
        return FORBIDDEN

    ELSEIF STATE.status_code != 200 THEN
        tell_user "ERROR: STATE.message"
        delete STATE
        return SYSTEM_ERROR

    ELSEIF now() < STATE.issue OR now() < STATE.last THEN 
        tell_user "ERROR: session start date or last used in the future?"
        delete STATE
        return SYSTEM_ERROR

    ELSEIF now() >= STATE.expire OR 
                now() >= STATE.last+config.inactive_timeout THEN 
        STATE.principal = ""
        message = "Your existing session has timed-out"

    ELSIF NOT exists(REPLY)
        IF STATE.request_data THEN
            request_data = STATE.request_data
        ENDIF
        STATE.last = now()
	return AUTHENTICATED

    ENDIF
    
ENDIF

# We get here if we didn't have any state information or if it had
# expired, or if we had both valid state _and_ an authentication reply.

# PHASE 2: Process an authentication reply if we got one.  If we can't
# decode the reply, it's been tampered with, or the URL looks bogus
# then we return SYSTEM_ERROR immediately.  Then, if we managed to get
# an authentication reply _and_ valid state (accessing the same
# protected resource in two browser windows at the same time is the
# easiest way to do this) we just redirect to the URL from the
# response. Otherwise we do the remaining validation checks on the
# response, stuffing the result of this validation into the state, and
# redirect to the original URL. We redirect wherever possible so that
# the user is left with the originally-requested URL in the browser
# status bar, rather than the original URL with the authentication
# reply tacked on the end. These redirections will subsequently be
# caught in phase 1.

IF exists(REPLY) THEN
    
    IF NOT decode(REPLY) THEN
        tell_user "ERROR: can't read reply"
        return SYSTEM_ERROR
    ENDIF    

    # Check that the URL in the response actually corresponds to the
    # current URL, ignoring PATH_INFO and QUERY_STRING. If it doesn't
    # it may not be safe to redirect to this URL

    IF scheme(REPLY.url)  + host(REPLY.url)  + path(REPLY.url)   !=
       scheme(this_url()) + host(this_url()) + path(this_url()) THEN
        tell_user "ERROR: URL in response doesn't match current URL"
        return SYSTEM_ERROR
    ENDIF    

    # If we got and identity from STATE, but had to come here to deal
    # with an authentication response the we can redirect now

    IF exists(STATE) and STATE.principal <> "" THEN
        return REDIRECT(REPLY.url)
    ENDIF    

    # Otherwise, check that state storage is actually working (because
    # if it isn't then we would loop indefinitely).

    IF NOT exists(STATE) THEN
        tell_user "ERROR: state storage not available"
        return SYSTEM_ERROR
    ENDIF

    STATE.id = REPLY.id

    # Do we understand the protocol version?

    IF REPLY.ver != CONFIG.ver THEN
        STATE.message = "ERROR: wrong protocol version in reply"
        STATE.status_code = 600
        return REDIRECT(REPLY.url)
    ENDIF        

    # If the response wasn't OK then copy through the code

    IF REPLY.status != 200 THEN
        STATE.message = "ERROR: authentication failed: REPLY.status"
        if REPLY.msg THEN
            STATE.message = STATE.message + REPLY.msg
        ENDIF
        STATE.status_code = REPLY.status
        return REDIRECT(REPLY.url)
    ENDIF    

    # Check if the reply issue time is believable

    IF REPLY.issue > now()+CONFIG.max_clock_skew+1 THEN
        STATE.message = "ERROR: reply issued in the future?"
        STATE.status_code = 600
        return REDIRECT(REPLY.url) 
    ENDIF    

    IF now()-CONFIG.max_clock_skew-1 > REPLY.issue+CONFIG.timeout  THEN
        STATE.message = "ERROR: reply stale"
        STATE.status_code = 600
        return REDIRECT(REPLY.url) 
    ENDIF    

    # Check that the type of authentication performed matches what we
    # will have asked for (see phase 3)

    IF CONFIG.aauth and NOT matches(CONFIG.aauth,REPLY.auth,REPLY.sso)
        STATE.message = "ERROR: auth types don't match requirement"
        STATE.status_code = 600
	return REDIRECT(REPLY.url)
    ENDIF

    IF CONFIG.iact == 'yes' and REPLY.auth == ''
        STATE.message = "ERROR: forced interaction request not honoured"
        STATE.status_code = 600
	return REDIRECT(REPLY.url)
    ENDIF

    # Check that the reply had a valid signature (which is only
    # required for non-error responses, hence checking it here)
    
    IF NOT valid_sig(REPLY)
        STATE.message = "ERROR: can't reverify signature on response"
        STATE.status_code = 600
        return REDIRECT(REPLY.url)
    ENDIF

    # If the reply checks out we setup state, including issue and
    # expire fields, and then redirect to the original URL.
    
    STATE.status_code = 200
    STATE.issue       = now()
    STATE.last        = now()
    STATE.life        = min(CONFIG.max_sesion_life,REPLY.life)
    STATE.id          = REPLY.id
    STATE.principal   = REPLY.principal
    STATE.aauth       = REPLY.aauth
    STATE.sso         = REPLY.sso
    STATE.params      = REPLY.params 
    
    return REDIRECT(REPLY.url) 
    
ENDIF # exists(REPLY)
    
# We get here only if we had no valid state information (or if it
# had expired) and there was no authentication reply 
    
# PHASE 3: Initialise state so we can test that it's working in
# phase 2. Then build an authentication request from the
# information that we have to hand and redirect to the URL that
# represents that request. We expect that as a result of this we
# will be called with an authentication reply that can be picked
# up by phase 2.
    
STATE.test = 'testing'

# We should store non-GET request data (e.g. POST data) in state so
# that it can subsequently be retrieved by the application because it
# will be lost by the redirections. How this is done is left as an
# exercise for the reader...

if method != 'GET'
  STATE.request_data = request_data
ENDIF

REQUEST.base    = CONFIG.url
REQUEST.ver     = CONFIG.ver
REQUEST.url     = this_url()
REQUEST.desc    = CONFIG.desc  if CONFIG.desc
REQUEST.aauth   = CONFIG.aauth if CONFIG.aauth
REQUEST.iact    = CONFIG.iact  if CONFIG.iact
REQUEST.msg     = $message
REQUEST.params  = CONFIG.params
REQUEST.date    = now()
REQUEST.fail    = 'yes' if CONFIG.fail
    
return REDIRECT(as_url(REQUEST))

# ----


FUNCTION as_url(request)

    # Return the authentication request in 'request' as a URL

ENDFUNC


FUNCTION decode(reply)

    # Decode and return the authentication response represented by 'reply'

ENDFUNC    


FUNCTION exists(thing)

    # Check if 'thing' (state, reply, etc) exists in some sense

ENDFUNC


FUNCTION get_authreply_from_url

    # Extract and return an authentication reply from the current URL

ENDFUNC


FUNCTION host(url)

    # Return the host component of 'url'
    
ENDFUNC


FUNCTION matches(rqd_authtype,given_authtype...)

    # Check that the required authentication type 'rqd_authtype'
    # appears at least once in the sequences of authentication types
    # represented by the multiple parameters given_authtype...'

ENDFUNC 


FUNCTION min(a,b)

    IF a < b THEN
        RETURN a
    ELSE
        RETURN b
    ENDIF

ENDFUNC


FUNCTION now

    # Return the current time

ENDFUNC


FUNCTION path(url)

    # Return the path component of 'url'
    
ENDFUNC


FUNCTION scheme(url)

    # Return the scheme component of 'url'

ENDFUNC


FUNCTION tell_user(msg)

    # Pass 'msg' to the browser user and/or record it happened in a log

ENDFUNC

    
FUNCTION this_url

    # Return a URL representing the resource currently being
    # requested. Note that this is harder than it looks, and that it
    # may be impossible to return the exact URL that the user
    # requested. Consistently returning some URL that identifies the
    # current resource is acceptable. However note that this must not
    # not depend on any information under the control of the user,
    # such as the content of a Host: header, since his may allow one
    # AA to proxy requests to another
    
ENDFUNC     


FUNCTION valid(state)

    # Check that 'state' is not corrupt and has not been tampered with

ENDFUNC


FUNCTION valid_sig(reply)

    # Test if the RSA signature in an authentication reply uses a
    # currently valid WLS key and is valid

ENDFUNC