def __init__(self, app, app_conf, prefix=PARAM_PREFIX, **local_conf): """ Sets up the server depending on the configuration. @type app: WSGI application @param app: wrapped application/middleware @type app_conf: dict @param app_conf: application configuration settings - ignored - this method includes this arg to fit Paste middleware / app function signature @type prefix: str @param prefix: optional prefix for parameter names included in the local_conf dict - enables these parameters to be filtered from others which don't apply to this middleware @type local_conf: dict @param local_conf: attribute settings to apply """ self._app = app self._renderingConfiguration = RenderingConfiguration( self.LAYOUT_PARAMETERS, prefix + self.LAYOUT_PREFIX, local_conf) self._set_configuration(prefix, local_conf) self.client_register = ClientRegister(self.client_register_file) self.renderer = callModuleObject(self.renderer_class, objectName=None, moduleFilePath=None, objectType=RendererInterface, objectArgs=None, objectProperties=None)
class Oauth2AuthorizationMiddleware(object): """Middleware to handle user/resource owner authorization of clients within a session. On each invocation, sets the current authorizations in the WSGI environ. At a specific URL, provides a simple form for a user to set an authorization decision. """ CLIENT_AUTHORIZATIONS_SESSION_KEY = 'oauth2_client_authorizations' SESSION_CALL_CONTEXT_KEY = 'oauth2_client_authorizations_context' PARAM_PREFIX = 'oauth2authorization.' LAYOUT_PREFIX = 'layout.' # Configuration options BASE_URL_PATH_OPTION = 'base_url_path' CLIENT_AUTHORIZATION_FORM_OPTION = 'client_authorization_form' CLIENT_AUTHORIZATIONS_KEY_OPTION = 'client_authorizations_key' CLIENT_REGISTER_OPTION = 'client_register' RENDERER_CLASS_OPTION = 'renderer_class' SESSION_KEY_OPTION = 'session_key_name' USER_IDENTIFIER_KEY_OPTION = 'user_identifier_key' method = { '/authorize': 'authorize', '/client_auth': 'client_auth' } # Configuration option defaults PROPERTY_DEFAULTS = { BASE_URL_PATH_OPTION: 'client_authorization', RENDERER_CLASS_OPTION: \ 'ndg.oauth.server.lib.render.genshi_renderer.GenshiRenderer', SESSION_KEY_OPTION: 'beaker.session.oauth2authorization', CLIENT_AUTHORIZATIONS_KEY_OPTION: 'client_authorizations', USER_IDENTIFIER_KEY_OPTION: 'REMOTE_USER' } LAYOUT_PARAMETERS = ['heading', 'title', 'message', 'leftLogo', 'leftAlt', 'leftImage', 'leftLink', 'rightAlt', 'rightImage', 'rightLink', 'footerText', 'helpIcon'] def __init__(self, app, app_conf, prefix=PARAM_PREFIX, **local_conf): """ Sets up the server depending on the configuration. @type app: WSGI application @param app: wrapped application/middleware @type app_conf: dict @param app_conf: application configuration settings - ignored - this method includes this arg to fit Paste middleware / app function signature @type prefix: str @param prefix: optional prefix for parameter names included in the local_conf dict - enables these parameters to be filtered from others which don't apply to this middleware @type local_conf: dict @param local_conf: attribute settings to apply """ self._app = app self._renderingConfiguration = RenderingConfiguration( self.LAYOUT_PARAMETERS, prefix + self.LAYOUT_PREFIX, local_conf) self._set_configuration(prefix, local_conf) self.client_register = ClientRegister(self.client_register_file) self.renderer = callModuleObject(self.renderer_class, objectName=None, moduleFilePath=None, objectType=RendererInterface, objectArgs=None, objectProperties=None) def __call__(self, environ, start_response): """ @type environ: dict @param environ: WSGI environment @type start_response: @param start_response: WSGI start response function @rtype: iterable @return: WSGI response """ log.debug("Oauth2AuthorizationMiddleware.__call__ ...") req = Request(environ) # Get session. session = environ.get(self.session_env_key) if session is None: raise Exception( 'Oauth2AuthorizationMiddleware.__call__: No beaker session key ' '"%s" found in environ' % self.session_env_key) # Determine what operation the URL specifies. actionPath = None log.debug("Request path_info: %s", req.path_info) if req.path_info.startswith(self.base_path): actionPath = req.path_info[len(self.base_path):] methodName = self.__class__.method.get(actionPath, '') if methodName: log.debug("Method: %s" % methodName) action = getattr(self, methodName) return action(req, session, start_response) elif self._app is not None: log.debug("Delegating to lower filter/application.") self._set_client_authorizations_in_environ(session, environ) return self._app(environ, start_response) else: response = "OAuth 2.0 Authorization Filter - Invalid URL" start_response(self._get_http_status_string(httplib.NOT_FOUND), [('Content-type', 'text/plain'), ('Content-length', str(len(response))) ]) return [response] def _set_client_authorizations_in_environ(self, session, environ): """ Sets the current authorizations currently granted by the user in environ, @type session: Beaker SessionObject @param session: session data @type environ: dict @param environ: WSGI environment """ client_authorizations = session.get(self.CLIENT_AUTHORIZATIONS_SESSION_KEY) if client_authorizations: log.debug("_set_client_authorizations_in_environ %r", client_authorizations) environ[self.client_authorizations_env_key] = client_authorizations else: log.debug("%s not found in session", self.CLIENT_AUTHORIZATIONS_SESSION_KEY) def authorize(self, req, session, start_response): """ Checks whether the user has already authorized the client and if not displays the authorization form. @type req: webob.Request @param req: HTTP request object @type session: Beaker SessionObject @param session: session data @type start_response: @param start_response: WSGI start response function @rtype: iterable @return: WSGI response """ client_id = req.params.get('client_id') scope = req.params.get('scope') user = req.params.get('user') original_url = req.params.get('original_url') log.debug("Client authorization request for client_id: %s scope: %s " "user: %s", client_id, scope, user) client_authorizations = session.get( self.CLIENT_AUTHORIZATIONS_SESSION_KEY) client_authorized = None if client_authorizations: client_authorized = \ client_authorizations.is_client_authorized_by_user(user, client_id, scope) if client_authorized is None: # No existing decision - let user decide. session[self.SESSION_CALL_CONTEXT_KEY] = { 'original_url': original_url, 'client_id': client_id, 'scope': scope, 'user': user } session.save() log.debug("Client not authorized by user - returning authorization " "form.") return self._client_auth_form(client_id, scope, req, start_response) else: log.debug("Client already %s authorization by user.", ("granted" if client_authorized else "denied")) log.debug("Redirecting to %s", original_url) return self._redirect(original_url, start_response) def _client_auth_form(self, client_id, scope, req, start_response): """ Returns a form for the user to enter an authorization decision. @type client_id: str @param client_id: client identifier as set in the client register @type scope: str @param scope: authorization scope @type req: webob.Request @param req: HTTP request object @type start_response: @param start_response: WSGI start response function @rtype: iterable @return: WSGI response """ client = self.client_register.register.get(client_id) if client is None: # Client ID is not registered. log.error("OAuth client of ID %s is not registered with the server", client_id) response = ( "OAuth client of ID %s is not registered with the server" % client_id) else: submit_url = req.application_url + self.base_path + '/client_auth' c = {'client_name': client.name, 'client_id': client_id, 'scope': scope, 'submit_url': submit_url, 'baseURL': req.application_url} response = self.renderer.render(self.client_authorization_form, self._renderingConfiguration.merged_parameters(c)) start_response(self._get_http_status_string(httplib.OK), [('Content-type', 'text/html'), ('Content-length', str(len(response))) ]) return [response] def client_auth(self, req, session, start_response): """ @type req: webob.Request @param req: HTTP request object @type session: Beaker SessionObject @param session: session data @type start_response: @param start_response: WSGI start response function @rtype: iterable @return: WSGI response """ call_context = session.get(self.SESSION_CALL_CONTEXT_KEY) if not call_context: log.error("No session context.") response = 'Internal server error' status_str = self._get_http_status_string( httplib.INTERNAL_SERVER_ERROR) start_response(status_str, [('Content-type', 'text/html'), ('Content-length', str(len(response))) ]) return [response] if 'submit' in req.params and 'cancel' not in req.params: log.debug("User authorized client.") granted = True else: log.debug("User declined authorization for client.") granted = False # Add authorization to those for the user. client_authorizations = session.setdefault( self.CLIENT_AUTHORIZATIONS_SESSION_KEY, ClientAuthorizationRegister()) client_id = call_context['client_id'] scope = call_context['scope'] user = call_context['user'] log.debug("Adding client authorization for client_id: %s scope: %s " "user: %s", client_id, scope, user) client_authorization = ClientAuthorization(user, client_id, scope, granted) client_authorizations.add_client_authorization(client_authorization) session[self.CLIENT_AUTHORIZATIONS_SESSION_KEY] = client_authorizations log.debug("### client_auth: %r", client_authorizations) session.save() original_url = call_context['original_url'] log.debug("Redirecting to %s", original_url) return self._redirect(original_url, start_response) def _set_configuration(self, prefix, local_conf): """Sets the configuration values. @type prefix: str @param prefix: optional prefix for parameter names included in the local_conf dict - enables these parameters to be filtered from others which don't apply to this middleware @type local_conf: dict @param local_conf: attribute settings to apply """ cls = self.__class__ self.base_path = cls._get_config_option( prefix, local_conf, cls.BASE_URL_PATH_OPTION) self.client_register_file = cls._get_config_option( prefix, local_conf, cls.CLIENT_REGISTER_OPTION) self.renderer_class = cls._get_config_option( prefix, local_conf, cls.RENDERER_CLASS_OPTION) self.session_env_key = cls._get_config_option( prefix, local_conf, cls.SESSION_KEY_OPTION) self.client_authorization_form = cls._get_config_option( prefix, local_conf, cls.CLIENT_AUTHORIZATION_FORM_OPTION) self.client_authorizations_env_key = cls._get_config_option(prefix, local_conf, cls.CLIENT_AUTHORIZATIONS_KEY_OPTION) self.user_identifier_env_key = cls._get_config_option( prefix, local_conf, cls.USER_IDENTIFIER_KEY_OPTION) @staticmethod def _get_http_status_string(status): return ("%d %s" % (status, httplib.responses[status])) def _redirect(self, url, start_response): log.debug("Redirecting to %s", url) start_response(self._get_http_status_string(httplib.FOUND), [('Location', url.encode('ascii', 'ignore'))]) return [] @classmethod def _get_config_option(cls, prefix, local_conf, key): value = local_conf.get(prefix + key, cls.PROPERTY_DEFAULTS.get(key, None)) log.debug("Oauth2AuthorizationMiddleware configuration %s=%s", key, value) return value @classmethod def filter_app_factory(cls, app, app_conf, **local_conf): return cls(app, app_conf, **local_conf)
class AuthenticationFormMiddleware(object): """Middleware to display a login form and handle the response. """ CLIENT_AUTHORIZATIONS_SESSION_KEY = 'oauth2_client_authorizations' PARAM_PREFIX = 'authenticationForm.' LAYOUT_PREFIX = 'layout.' # Configuration options AUTHENTICATION_CANCELLED_OPTION = 'login_cancelled' AUTHENTICATION_FORM_OPTION = 'login_form' BASE_URL_PATH_OPTION = 'base_url_path' CLIENT_REGISTER_OPTION = 'client_register' COMBINED_AUTHORIZATION_OPTION = 'combined_authorization' RENDERER_CLASS_OPTION = 'renderer_class' RETURN_URL_PARAM_OPTION = 'return_url_param' SESSION_KEY_OPTION = 'session_key_name' method = { '/login_form': 'login_form', '/login': '******' } # Configuration option defaults PROPERTY_DEFAULTS = { BASE_URL_PATH_OPTION: '/authentication', COMBINED_AUTHORIZATION_OPTION: 'True', RENDERER_CLASS_OPTION: 'ndg.oauth.server.lib.render.genshi_renderer.GenshiRenderer', RETURN_URL_PARAM_OPTION: 'returnurl', SESSION_KEY_OPTION: 'beaker.session.oauth2authorization' } LAYOUT_PARAMETERS = ['heading', 'title', 'message', 'leftLogo', 'leftAlt', 'leftImage', 'leftLink', 'rightAlt', 'rightImage', 'rightLink', 'footerText', 'helpIcon', 'client_id', 'client_name', 'scope'] def __init__(self, app, app_conf, prefix=PARAM_PREFIX, **local_conf): """ Sets up the server depending on the configuration. @type app: WSGI application @param app: wrapped application/middleware @type app_conf: dict @param app_conf: application configuration settings - ignored - this method includes this arg to fit Paste middleware / app function signature @type prefix: str @param prefix: optional prefix for parameter names included in the local_conf dict - enables these parameters to be filtered from others which don't apply to this middleware @type local_conf: dict @param local_conf: attribute settings to apply """ self._app = app self._renderingConfiguration = RenderingConfiguration( self.LAYOUT_PARAMETERS, prefix + self.LAYOUT_PREFIX, local_conf) self._set_configuration(prefix, local_conf) self.client_register = ClientRegister(self.client_register_file) self.renderer = callModuleObject(self.renderer_class, objectName=None, moduleFilePath=None, objectType=RendererInterface, objectArgs=None, objectProperties=None) def __call__(self, environ, start_response): """ @type environ: dict @param environ: WSGI environment @type start_response: @param start_response: WSGI start response function @rtype: iterable @return: WSGI response """ log.debug("AuthenticationFormMiddleware.__call__ ...") req = Request(environ) # Get session. session = environ.get(self.session_env_key) if session is None: raise Exception( 'AuthenticationFormMiddleware.__call__: No beaker session key ' '"%s" found in environ' % self.session_env_key) # Determine what operation the URL specifies. actionPath = None log.debug("Request path_info: %s", req.path_info) if req.path_info.startswith(self.base_path): actionPath = req.path_info[len(self.base_path):] methodName = self.__class__.method.get(actionPath, '') if methodName: log.debug("Method: %s" % methodName) action = getattr(self, methodName) return action(req, session, start_response) elif self._app is not None: log.debug("Delegating to lower filter/application.") return self._app(environ, start_response) else: response = "OAuth 2.0 Authentication Filter - Invalid URL" start_response(self._get_http_status_string(httplib.NOT_FOUND), [('Content-type', 'text/plain'), ('Content-length', str(len(response))) ]) return [response] def login_form(self, req, session, start_response): """Displays the login form. @type req: webob.Request @param req: HTTP request object @type session: Beaker SessionObject @param session: session data @type start_response: @param start_response: WSGI start response function @rtype: iterable @return: WSGI response """ submit_url = req.application_url + self.base_path + '/login' return_url = req.params.get(self.return_url_param) c = {'return_url': return_url, 'return_url_param': self.return_url_param, 'submit_url': submit_url, 'baseURL': req.application_url} # Include authorization details on form if authentication and # authorization are to be combined. if self.combined_authorization: c.update(self._parse_return_url(return_url)) response = self.renderer.render(self.authentication_form, self._renderingConfiguration.merged_parameters(c)) start_response(self._get_http_status_string(httplib.OK), [('Content-type', 'text/html'), ('Content-length', str(len(response))) ]) return [response] def login(self, req, session, start_response): """Handles submission of the login form. @type req: webob.Request @param req: HTTP request object @type session: Beaker SessionObject @param session: session data @type start_response: @param start_response: WSGI start response function @rtype: iterable @return: WSGI response """ if ('submit' in req.params) and ('cancel' not in req.params): username = req.params.get('username') password = req.params.get('password') credentials = {'login': username, 'password': password} repoze_who_api = get_api(req.environ) (identity, headers) = repoze_who_api.login(credentials) if identity is not None: logged_in_username = identity['repoze.who.userid'] log.debug("Logged in using username %s as %s", username, logged_in_username) if self.combined_authorization: self._set_client_authorization(req, session, logged_in_username) return_url = req.params.get(self.return_url_param) return self._redirect(return_url, start_response, headers) else: # Login failed - redirect to login form. return_url = req.params.get(self.return_url_param) form_url = (req.application_url + self.base_path + '/login_form' + '?' + urllib.urlencode({self.return_url_param: return_url})) return self._redirect(form_url, start_response) else: # User cancelled authentication - confirm this. c = {'baseURL': req.application_url} response = self.renderer.render(self.authentication_cancelled, self._renderingConfiguration.merged_parameters(c)) start_response(self._get_http_status_string(httplib.OK), [('Content-type', 'text/html'), ('Content-length', str(len(response))) ]) return [response] def _parse_return_url(self, return_url): """Gets client information from the return URL. @type return_url: basestring @param return_url: return URL @rtype: basestring @return: String describing client and scope if present """ if not return_url: return None u = urlparse.urlparse(return_url) query_params = urlparse.parse_qs(u.query) client_id = query_params.get('client_id', None) if client_id: client_id = client_id[0] scope = query_params.get('scope', None) if scope: scope = scope[0] client = self.client_register.register.get(client_id) result = None if client: result = {'client_name': client.name, 'client_id': client.id, 'scope': scope} return result def _set_client_authorization(self, req, session, username): """Sets the client as authorized. @type req: webob.Request @param req: HTTP request object @type session: Beaker SessionObject @param session: session data """ client_authorizations = session.setdefault( self.CLIENT_AUTHORIZATIONS_SESSION_KEY, ClientAuthorizationRegister()) client_id = req.params.get('client_id') scope = req.params.get('scope') log.debug( "Adding client authorization for client_id: %s scope: %s user: %s", client_id, scope, username) client_authorizations.add_client_authorization( ClientAuthorization(username, client_id, scope, True)) session[self.CLIENT_AUTHORIZATIONS_SESSION_KEY] = client_authorizations log.debug("### client_auth: %s", client_authorizations.__repr__()) session.save() # Make available immediately to chained middleware. session.persist() def _set_configuration(self, prefix, local_conf): """Sets the configuration values. @type prefix: str @param prefix: optional prefix for parameter names included in the local_conf dict - enables these parameters to be filtered from others which don't apply to this middleware @type local_conf: dict @param local_conf: attribute settings to apply """ cls = self.__class__ self.base_path = cls._get_config_option(prefix, local_conf, cls.BASE_URL_PATH_OPTION) self.client_register_file = cls._get_config_option(prefix, local_conf, cls.CLIENT_REGISTER_OPTION) combined_authorization = cls._get_config_option(prefix, local_conf, cls.COMBINED_AUTHORIZATION_OPTION) self.combined_authorization = (combined_authorization.lower() == 'true') self.renderer_class = cls._get_config_option(prefix, local_conf, cls.RENDERER_CLASS_OPTION) self.return_url_param = cls._get_config_option(prefix, local_conf, cls.RETURN_URL_PARAM_OPTION) self.session_env_key = cls._get_config_option(prefix, local_conf, cls.SESSION_KEY_OPTION) self.authentication_cancelled = cls._get_config_option(prefix, local_conf, cls.AUTHENTICATION_CANCELLED_OPTION) self.authentication_form = cls._get_config_option(prefix, local_conf, cls.AUTHENTICATION_FORM_OPTION) @staticmethod def _get_http_status_string(status): return ("%d %s" % (status, httplib.responses[status])) def _redirect(self, url, start_response, headers=[]): log.debug("Redirecting to %s", url) hdrs = [('Location', url.encode('ascii', 'ignore'))] hdrs.extend(headers) start_response(self._get_http_status_string(httplib.FOUND), hdrs) return [] @classmethod def _get_config_option(cls, prefix, local_conf, key): value = local_conf.get(prefix + key, cls.PROPERTY_DEFAULTS.get(key,None)) log.debug("AuthenticationFormMiddleware configuration %s=%s", key, value) return value @classmethod def filter_app_factory(cls, app, app_conf, **local_conf): return cls(app, app_conf, **local_conf)