def check_auth_params(url, body): ''' This function checks the validity of the authParams parameter as per the API. It takes the decoded json body of the request and raises an exception if a problem is found. The difference between check_auth_params and check_publisher_auth_params is that check_auth_params enforces validity to the API. In contrast, check_publisher_auth_params enforces validity as to this implementation. It checks specifically for the existence of the "username" and "password" fields. Server Errors: This section documents errors that are persisted on the server and not sent to the client. Note that the publisher is free to modify the content of these messages as they please. InvalidAuthParams: Returned when the request does not specify the authParams parameter properly. Code: InvalidAuthParams Message: An error occurred. Please contact support. Debug: Varies with the error. HTTP Error Code: 400 Required: No ''' # All of the errors in this function share a common code and status. code = 'InvalidAuthParams' status = 400 message = 'An error occurred. Please contact support.' # Note that not having an authParams key is valid. if 'authParams' not in body: return # When authParams is provided, the type must be a dictionary. if not isinstance(body['authParams'], dict): debug = 'The authParams is not a map.' raise_error(url, code, message, status, debug) # Make sure that all the values in the dictionary are strings. for key in body['authParams']: value = body['authParams'][key] if not isinstance(value, unicode) and not isinstance(value, str): debug = 'This authParams value is not a string: ' + unicode(key) raise_error(url, code, message, status, debug)
def decode_body(url, body): ''' This function checks the validity of the body parameter. It takes the request body and returns python objects by decoding the body using json. Server Errors: This section documents errors that are persisted on the server and not sent to the client. Note that the publisher is free to modify the content of these messages as they please. InvalidFormat: Returned when the publisher does not recognize the requested format. Code: InvalidFormat Message: The requested article could not be found. Debug: Varies with the error. HTTP Error Code: 400. Required: No ''' # All of the errors in this function share a common code and status. code = 'InvalidFormat' status = 400 message = 'An error occurred. Please contact support.' # If the body cannot be decoded, a proper error response must be made. # This try except block intercepts the default exception and raises # a json encoded exception using the raise_error function. try: json_body = loads(body, encoding='utf-8') except ValueError: # If a ValueError occurred, the json decoder could not decode the # body of the request. We need to return an error to the client. # Note that we do not return the body of the request as it could # contain access credentials. debug = 'Could not decode post body. json is expected.' raise_error(url, code, message, status, debug) # Make sure a valid number of parameters have been provided. if len(json_body) < 1 or len(json_body) > 2: debug = 'Post body has an invalid number of parameters.' raise_error(url, code, message, status, debug) return json_body
def check_authorization_header(url, environment): ''' Checks for the existence of the auth-scheme token. Note that the validate API entry point has a different auth-scheme token. Server Errors: This section documents errors that are persisted on the server and not sent to the client. Note that the publisher is free to modify the content of these messages as they please. InvalidAuthScheme: Returned when the publisher does not recognize the requested format. Code: InvalidAuthScheme Message: An error occurred. Please contact support. Debug: Varies with the error. HTTP Error Code: 400. Required: No ''' # All of the errors in this function share a common code and status. code = 'InvalidAuthScheme' status = 400 message = 'An error occurred. Please contact support.' # Make sure the token is provided. if 'HTTP_AUTHORIZATION' not in environment: debug = 'The authorization token has not been provided.' raise_error(url, code, message, status, debug) # Make sure the token's value is correct. The auth-scheme token isn't # important for this part of the API, but it is for others. if environment['HTTP_AUTHORIZATION'] != AUTH_AUTHORIZATION_HEADER: debug = 'The authorization token is incorrect.' raise_error(url, code, message, status, debug)
def get_session_id(url, environment): ''' Checks for the existence of the auth-scheme token and then extracts the session id and returns it. Note that the auth API entry point has a different auth-scheme token. Server Errors: This section documents errors that are persisted on the server and not sent to the client. Note that the publisher is free to modify the content of these messages as they please. InvalidAuthScheme: Returned when the publisher does not recognize the requested format. Code: InvalidAuthScheme Message: An error occurred. Please contact support. Debug: Varies with the error. HTTP Error Code: 400. Required: No ''' # All of the errors in this function share a common code and status. code = 'InvalidAuthScheme' status = 400 message = 'An error occurred. Please contact support.' # Make sure the token is provided. if 'HTTP_AUTHORIZATION' not in environment: debug = 'The authorization token has not been provided.' raise_error(url, code, message, status, debug) # Make sure the token's value is correct. This token contains the session # id. It is not passed in the http body. token = environment['HTTP_AUTHORIZATION'] scheme = SESSION_AUTHORIZATION_HEADER + ' session:' if not token.startswith(scheme): debug = 'The authorization token is incorrect.' raise_error(url, code, message, status, debug) # Try to extract the session key. The syntax below extracts the characters # from the length of the scheme string to the end. Note that whitespace # characters are stripped from the session_id. session_id = token[len(scheme):].strip() # Check to make sure a session id has actually been provided. if len(session_id) == 0: debug = 'The session id has not been provided.' raise_error(url, code, message, status, debug) # Return the session key. return session_id
def check_publisher_auth_params(url, body): ''' This function checks for the existence of the authParams dictionary and the exiistence of the "username" and "password" keys in the authParams dictionary. The difference between check_auth_params and check_publisher_auth_params is that check_auth_params enforces validity to the API. In contrast, check_publisher_auth_params enforces validity as to this implementation. It checks specifically for the existence of the "username" and "password" fields. Server Errors: This section documents errors that are persisted on the server and not sent to the client. Note that the publisher is free to modify the content of these messages as they please. InvalidAuthParams: Returned when the request does not specify the authParams parameter properly. Code: InvalidAuthParams Message: An error occurred. Please contact support. Debug: Varies with the error. HTTP Error Code: 400 Required: No ''' # All of the errors in this function share a common code and status. code = 'InvalidAuthParams' status = 400 message = 'An error occurred. Please contact support.' # In this implementation, authParams is required. The check for authParams # being a dictionary has already been carried out by check_auth_params. if 'authParams' not in body: debug = 'The authParams has not been provided.' raise_error(url, code, message, status, debug) # Make sure username is provided. The check_auth_params has already # checked the type of the username. if 'username' not in body['authParams']: debug = 'The username has not been provided.' raise_error(url, code, message, status, debug) # Make sure password is provided. The check_auth_params has already # checked the type of the password. if 'password' not in body['authParams']: debug = 'The password has not been provided.' raise_error(url, code, message, status, debug)
def validate(request, api, version, format, product_code): ''' Overview: Attempt an authorization for a product using supplied session key (transmitted via the Authorization header). This API call is used periodically to validate that the session is still valid and the user should continue to be allowed to access protected resources. If the call returns a 401, a new authentication call must be made. The base URL scheme for this entry point is: /:api/:version/:format/validate/:productcode In this particular case, the api is "paywallproxy" and the version is v1.0.0. Currently, the only supported format is "json". The URL for this entry point therefore looks like: /paywallproxy/v1.0.0/json/validate/:productcode If the product cannot be found, an "InvalidProduct" error should be returned. This error will be returned to the client. A full list of errors that this entry point returns is available below. Client errors are proxied to the client. Server errors remain on Polar's server. Parameters: There are two sets of parameters that this API entry point requires. The first set consists of the product code, which is specified in the URL and the session id, which is specified in the authorization header. The product code is part of the URL. It is a publisher-assigned unique identifier for this product. The product code is required. An auth-scheme token is expected when a call is made to this API end point. It must conform to RFC 2617 specifications. The authorization header has the following form: Authorization: PolarPaywallProxySessionv1.0.0 session:<session id> Note that the session id is passed as a parameter through the authorization token. Details regarding the various parameters are described below. Product Code: A publisher-assigned unique identifier for this product. Availability: >= v1.0.0 Required: Yes Location: URL Format: URL Type: String Max Length: 256 Session Id: A session id generated by calling the auth entry point of this API. Availability: >= v1.0.0 Required: Yes Location: Header Format: Header Type: String Max Length: 512 Response: The following parameters are returned by this API end point. The resposne is json encoded. It has two keys; "sessionKey" and "products". "sessionKey" is a key that allows the client to re-authenticate without the supplied authentication parameters. "products" is a list of product identifiers that the user has access to. sessionKey: A key that allows the client to re-authenticate without the supplied authentication parameters. Availability: >= v1.0.0 Required: Yes Location: POST Body Format: json Type: string Max Length: 512 products: A list of product identifiers that the user has access to. Availability: >= v1.0.0 Required: Yes Location: POST Body Format: json Type: list Max Length: N/A product: A publisher-assigned unique identifier for this product that the user has access to. Contained in the "products" list. Availability: >= v1.0.0 Required: Yes Location: POST Body Format: json Type: string Max Length: 256 Example: Example Request: POST /validate/gold-level HTTP/1.1 Authorization: PolarPaywallProxySessionv1.0.0 session:9c4a51cc08d1 Example Response: HTTP/1.1 200 OK Content-Type: application/json { "sessionKey": "9c4a51cc08d1", "products": [ "gold-level", "silver-level" ] } Errors: Some of the following errors are marked optional. They are included in this example for completeness and testing purposes. Implementing them makes testing the connection between Polar's server and the publishers server easier. AccountProblem: There is a problem with the user's account. The user is prompted to contact technical support. Code: InvalidPaywallCredentials Message: Your account is not valid. Please contact support. HTTP Error Code: 403 Required: Yes InvalidProduct: Thrown when the product code indicated is invalid. Code: InvalidProduct Message: The requested article could not be found. HTTP Error Code: 404 Required: Yes SessionExpired: The session key provided has expired. Re-authenticate (to obtain a new session key) and retry the request. Code: SessionExpired Message: The session key provided has expired. HTTP Error Code: 401 Required: Yes InvalidAPI: Returned when the publisher does not recognize the requested api. Code: InvalidAPI Message: An error occurred. Please contact support. Debug: The requested api is not implemented: <api> HTTP Error Code: 404 Required: No InvalidVersion: Returned when the publisher does not recognize the requested version. Code: InvalidVersion Message: An error occurred. Please contact support. Debug: The requested version is not implemented: <version> HTTP Error Code: 404 Required: No InvalidFormat: Returned when the publisher does not recognize the requested format. Code: InvalidFormat Message: An error occurred. Please contact support. Debug: The requested format is not implemented: <format> HTTP Error Code: 404 Required: No InvalidAuthScheme: Returned when the publisher does not recognize the requested format. Code: InvalidAuthScheme Message: An error occurred. Please contact support. Message: Varies with the error. HTTP Error Code: 400. Required: No ''' # Store the full URL string so that it can be used to report errors. url = request.path # Validate the request. check_base_url(url, api, version, format) if len(request.body.strip()) > 0: # If there is a body for this API call, that implies that the caller # is not conforming to the API, so raise an error. code = 'InvalidFormat' message = 'Invalid post body.' status = 400 raise_error(url, code, message, status) # Validate the session id using the data model. session_id = get_session_id(url, request._environ) products = model().validate_session(url, session_id, product_code) # Create the response body. result = {} result['sessionKey'] = session_id result['products'] = products content = dumps(result) status = 200 headers = [] content_type = 'application/json' return Response(content, headers, status, content_type)
def check_device(url, body): ''' This function checks the validity of the device parameter. It takes the decoded json body of the request and raises an error if a problem is found. Server Errors: This section documents errors that are persisted on the server and not sent to the client. Note that the publisher is free to modify the content of these messages as they please. InvalidDevice: Returned when the request does not specify the device parameters properly. Code: InvalidDevice Message: The requested article could not be found. Debug: Varies with the error. HTTP Error Code: 400 Required: No ''' # All of the errors in this function share a common code and status. code = 'InvalidDevice' status = 400 message = 'An error occurred. Please contact support.' # Validate that the device is provided as a parameter to the body. This # sample publisher does not store the device, but a production system # could store these values in order to perform analysis on the types of # devices that access products. if 'device' not in body: debug = 'The device has not been provided.' raise_error(url, code, message, status, debug) # Make sure the device is a dictionary. if not isinstance(body['device'], dict): debug = 'The device is not a map.' raise_error(url, code, message, status, debug) # Check to make sure that the manufacturer of the device has been # provided. if 'manufacturer' not in body['device']: debug = 'The manufacturer has not been provided.' raise_error(url, code, message, status, debug) # Check to make sure the manufacturer is of the right type. manufacturer = body['device']['manufacturer'] if not isinstance(manufacturer, unicode) and \ not isinstance(manufacturer, str): debug = 'The manufacturer is not a string.' raise_error(url, code, message, status, debug) # Check to make sure that the model of the device has been provided. if 'model' not in body['device']: debug = 'The model has not been provided.' raise_error(url, code, message, status, debug) # Check to make sure the model is of the right type. model = body['device']['model'] if not isinstance(model, unicode) and \ not isinstance(model, str): debug = 'The model is not a string.' raise_error(url, code, message, status, debug) # Check to make sure that the os_version of the device has been provided. if 'os_version' not in body['device']: debug = 'The os_version has not been provided.' raise_error(url, code, message, status, debug) # Check to make sure the os_version is of the right type. os_version = body['device']['os_version'] if not isinstance(os_version, unicode) and \ not isinstance(os_version, str): debug = 'The os_version is not a string.' raise_error(url, code, message, status, debug)
def validate_session(self, url, session_id, product): ''' This function takes a session id, attempts to find the users that it is associated with. If it finds a user, it first updates the session tokens and then checks to make sure that the session key is still valid. It then returns the list of products that the user has access to. If it finds no user, it reports that the session key has been expired. Client Errors: This section documents errors that are returned to the client. Note that the publisher is free to modify the content of these messages as they please. SessionExpired: Thrown when the session id cannot be validated. Code: SessionExpired Message: Your session has expired. Please log back in. HTTP Error Code: 401 Required: Yes AccountProblem: There is a problem with the user's account. The user is prompted to contact technical support. Code: AccountProblem Message: Your account is not valid. Please contact support. HTTP Error Code: 403 Required: Yes ''' self.lock.acquire() try: # Most of the errors in this function share a common code and # status. code = 'SessionExpired' status = 401 message = 'Your session has expired. Please log back in.' # Loop over all of the users and check for the valid session id. for username in model.users: # If the session id does not belong to this user, keep # searching. if session_id not in model.users[username]['session ids']: continue # Check to see if the user is valid. The check for a valid # account should come after the check for the session id as # the password validates the user's identity. if not model.users[username]['valid']: code = 'AccountProblem' message = ('Your account is not valid. Please contact ' 'support.') status = 403 raise_error(url, code, message, status) # Check to make sure the product is valid. if product not in model.users[username]['products']: code = 'InvalidProduct' message = 'The requested article could not be found.' status = 404 raise_error(url, code, message, status) # Update the list of valid session ids; the session may have # expired since the last validation. self.update_session_ids(username) # Check to see if the session id is valid; it may have been # invalidated by the call to update_session_ids. if session_id not in model.users[username]['session ids']: message = 'Your session has expired. Please log back in.' raise_error(url, code, message, status) # Check to see if the session key is registered against the # right product. The only way this can happen during normal # operation is if a product that the user has authenticated # has been deleted. session = model.users[username]['session ids'][session_id] stored_product, timestamp = session if product != stored_product: # Their session has expired. raise_error(url, code, message, status) # Return the user's products, which indicate a successful # validation. products = model.users[username]['products'] return products # If nothing has been returned at this point, raise an error. # We can only assume that their session key has expired. raise_error(url, code, message, status) finally: self.lock.release()
def authenticate_user(self, url, username, password, product): ''' This function first checks to see if a user is valid. If it is, it will then attempt to authenticate the user with the password. If the password attempt succeeds, this function generates and inserts a new session key and returns it. If any failures occur as a result of authenticating the user, an exception will be thrown. The exceptions are detailed below. Client Errors: This section documents errors that are returned to the client. Note that the publisher is free to modify the content of these messages as they please. InvalidPaywallCredentials: Thrown when the authentication parameters are invalid. Code: InvalidPaywallCredentials Message: Varies with the error. HTTP Error Code: 401 Required: Yes AccountProblem: There is a problem with the user's account. The user is prompted to contact technical support. Code: AccountProblem Message: Your account is not valid. Please contact support. HTTP Error Code: 403 Required: Yes InvalidProduct: Thrown when the product code indicated is invalid. Code: InvalidProduct Message: The requested article could not be found. HTTP Error Code: 404 Required: Yes ''' self.lock.acquire() try: # Most of the errors in this function share a common code and # status. code = 'InvalidPaywallCredentials' status = 401 # Check to see if the username is known. if username not in model.users: message = 'The credentials you have provided are not valid.' raise_error(url, code, message, status) # Check to see if the password is valid. if model.users[username]['password'] != password: message = 'The credentials you have provided are not valid.' raise_error(url, code, message, status) # Check to see if the user is valid. The check for a valid account # should come after the check for the password as the password # validates the user's identity. if not model.users[username]['valid']: code = 'AccountProblem' message = 'Your account is not valid. Please contact support.' status = 403 raise_error(url, code, message, status) # Check to see if the user has access to the requested product. if product not in model.users[username]['products']: code = 'InvalidProduct' message = 'The requested article could not be found.' status = 404 raise_error(url, code, message, status) # Update all valid session keys. While this call is not required # at this time, issuing it helps keep the database of session ids # small; particularly since the user has likely not been accessing # content recently as they are logging in. Calling update now will # hopefully free up many keys, reducing the memory footprint of # the server. self.update_session_ids(username) # Return the session id and products. session_id = self.create_session_id(username, product) products = model.users[username]['products'] return (session_id, products) finally: self.lock.release()