pythonjsonauthenticationturbogears2repoze.who

How can I return JSON output when repoze.who authentication fails?


I'm writing a repoze.who plugin and want to return JSON from the repoze.who authentication middleware and still controll the HTTP status code. How can this be done?


Solution

  • One way to accomplish this, is to implement a repoze.who Challenger interface. The following solution takes advantage of the fact that the WebOb exceptions, in webob.exc, can be used as a WSGI application. The following example shows how this can be used in a hypothetical Facebook plugin, where the 2.x API lets users not grant access to their email, which might be required for a successful registration/authentication:

    import json
    from webob.acceptparse import MIMEAccept
    from webob.exc import HTTPUnauthorized, HTTPBadRequest
    
    
    FACEBOOK_CONNECT_REPOZE_WHO_NOT_GRANTED = 'repoze.who.facebook_connect.not_granted'
    
    
    class ExampleJSONChallengerPlugin(object):
        json_content_type = 'application/json'
        mime_candidates = ['text/html',
                           'application/xhtml+xml',
                           json_content_type,
                           'application/xml',
                           'text/xml']
    
        def is_json_request_env(self, environ):
            """Checks whether the current request is a json request as deemed by
            TurboGears (i.e. response_type is already set) or if the http
            accept header favours 'application/json' over html.
            """
            if environ['PATH_INFO'].endswith('.json'):
                return True
    
            if 'HTTP_ACCEPT' not in environ:
                return False
    
            # Try to mimic what Decoration.lookup_template_engine() does.
            return MIMEAccept(environ['HTTP_ACCEPT']) \
                .best_match(self.mime_candidates) is self.json_content_type
    
        def challenge(self, environ, status, app_headers, forget_headers):
            if FACEBOOK_CONNECT_REPOZE_WHO_NOT_GRANTED in environ:
                response = HTTPBadRequest(detail={
                    'not_granted': 
                    environ.pop(FACEBOOK_CONNECT_REPOZE_WHO_NOT_GRANTED),
                })
            elif status.startswith('401 '):
                response = HTTPUnauthorized()
            else:
                response = None
    
            if response is not None and self.is_json_request_env(environ):
                response.body = json.dumps({
                    'code': response.code,
                    'status': response.title,
                    'explanation': response.explanation,
                    'detail': response.detail,
                })
                response.content_type = self.json_content_type
    
            return response
    

    A central point here is that response, an instance of a sub-class of webob.exc.WSGIHTTPException, is used as a WSGI application, but also that if response's body attribute is set, then it is not automatically generated, a fact we use to explicitly set the response's body to a JSON-formatted string representation of our dictionary. If the above challenger is invoked during the handling of a request to a URL ending in '.json' or the Accept header includes application/json, the body of the response might render as something like:

    {
        "status": "Bad Request",
        "explanation": "The server could not comply with the request since it is either malformed or otherwise incorrect.",
        "code": 400, 
        "detail": {"not_granted": ["email"]}
    }
    

    and if not, then the body will be rendered as HTML:

    <html>
     <head>
      <title>400 Bad Request</title>
     </head>
     <body>
      <h1>400 Bad Request</h1>
      The server could not comply with the request since it is either 
      malformed or otherwise incorrect.<br /><br />
    {'not_granted': [u'email']}
    
    
     </body>
    </html>