def test_fulfill(self): other_client, ignore = IntegrationClient.register( self._db, "http://other_library.org") loan, ignore = create(self._db, Loan, integration_client=other_client, license_pool=self.pool) assert_raises(CannotFulfill, self.shared_collection.fulfill, self.collection, self.client, loan, self.delivery_mechanism) loan.integration_client = self.client # If the API does not return content or a content link, the loan can't be fulfilled. assert_raises(CannotFulfill, self.shared_collection.fulfill, self.collection, self.client, loan, self.delivery_mechanism) eq_([(self.client, loan, self.delivery_mechanism)], self.api.fulfills) self.api.fulfillment = FulfillmentInfo( self.collection, self.pool.data_source.name, self.pool.identifier.type, self.pool.identifier.identifier, "http://content", "text/html", None, None, ) fulfillment = self.shared_collection.fulfill(self.collection, self.client, loan, self.delivery_mechanism) eq_([(self.client, loan, self.delivery_mechanism)], self.api.fulfills[1:]) eq_(self.delivery_mechanism, loan.fulfillment)
def test_borrow(self): # This client is registered, but isn't one of the allowed URLs for the collection # (maybe it was registered for a different shared collection). other_client, ignore = IntegrationClient.register( self._db, "http://other_library.org") # Trying to borrow raises an exception. assert_raises(AuthorizationFailedException, self.shared_collection.borrow, self.collection, other_client, self.pool) # A client that's registered with the collection can borrow. self.shared_collection.borrow(self.collection, self.client, self.pool) eq_([(self.client, self.pool)], self.api.checkouts) # If the client's checking out an existing hold, the hold must be for that client. hold, ignore = create(self._db, Hold, integration_client=other_client, license_pool=self.pool) assert_raises(CannotLoan, self.shared_collection.borrow, self.collection, self.client, self.pool, hold=hold) hold.integration_client = self.client self.shared_collection.borrow(self.collection, self.client, self.pool, hold=hold) eq_([(self.client, self.pool)], self.api.checkouts[1:])
def test_fulfill(self): other_client, ignore = IntegrationClient.register(self._db, "http://other_library.org") loan, ignore = create(self._db, Loan, integration_client=other_client, license_pool=self.pool) assert_raises(CannotFulfill, self.shared_collection.fulfill, self.collection, self.client, loan, self.delivery_mechanism) loan.integration_client = self.client # If the API does not return content or a content link, the loan can't be fulfilled. assert_raises(CannotFulfill, self.shared_collection.fulfill, self.collection, self.client, loan, self.delivery_mechanism) eq_([(self.client, loan, self.delivery_mechanism)], self.api.fulfills) self.api.fulfillment = FulfillmentInfo( self.collection, self.pool.data_source.name, self.pool.identifier.type, self.pool.identifier.identifier, "http://content", "text/html", None, None, ) fulfillment = self.shared_collection.fulfill(self.collection, self.client, loan, self.delivery_mechanism) eq_([(self.client, loan, self.delivery_mechanism)], self.api.fulfills[1:]) eq_(self.delivery_mechanism, loan.fulfillment)
def check_client_authorization(self, collection, client): """Verify that an IntegrationClient is whitelisted for access to the collection.""" external_library_urls = ConfigurationSetting.for_externalintegration( BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS, collection.external_integration ).json_value if client.url not in [IntegrationClient.normalize_url(url) for url in external_library_urls]: raise AuthorizationFailedException()
def test_revoke_loan(self): other_client, ignore = IntegrationClient.register(self._db, "http://other_library.org") loan, ignore = create(self._db, Loan, integration_client=other_client, license_pool=self.pool) assert_raises(NotCheckedOut, self.shared_collection.revoke_loan, self.collection, self.client, loan) loan.integration_client = self.client self.shared_collection.revoke_loan(self.collection, self.client, loan) eq_([(self.client, loan)], self.api.returns)
def test_revoke_hold(self): other_client, ignore = IntegrationClient.register(self._db, "http://other_library.org") hold, ignore = create(self._db, Hold, integration_client=other_client, license_pool=self.pool) assert_raises(CannotReleaseHold, self.shared_collection.revoke_hold, self.collection, self.client, hold) hold.integration_client = self.client self.shared_collection.revoke_hold(self.collection, self.client, hold) eq_([(self.client, hold)], self.api.released_holds)
def authenticated_client_from_request(_db, required=True): header = request.headers.get('Authorization') if header and 'bearer' in header.lower(): shared_secret = base64.b64decode(header.split(' ')[1]) client = IntegrationClient.authenticate(_db, shared_secret) if client: return client if not required and not header: # In the case that authentication is not required # (i.e. URN lookup) return None instead of an error. return None return INVALID_CREDENTIALS
def authenticated_client_from_request(_db, required=True): header = request.authorization if header: key, secret = header.username, header.password client = IntegrationClient.authenticate(_db, key, secret) if client: return client if not required and not header: # In the case that authentication is not required # (i.e. URN lookup) return None instead of an error. return None return INVALID_CREDENTIALS
def update_client_url(self): """Updates the URL of a IntegrationClient""" client = authenticated_client_from_request(self._db) if isinstance(client, ProblemDetail): return client url = request.args.get('client_url') if not url: return INVALID_INPUT.detailed("No 'client_url' provided") client.url = IntegrationClient.normalize_url(urllib.unquote(url)) return make_response("", HTTP_OK)
def test_revoke_loan(self): other_client, ignore = IntegrationClient.register( self._db, "http://other_library.org") loan, ignore = create(self._db, Loan, integration_client=other_client, license_pool=self.pool) assert_raises(NotCheckedOut, self.shared_collection.revoke_loan, self.collection, self.client, loan) loan.integration_client = self.client self.shared_collection.revoke_loan(self.collection, self.client, loan) eq_([(self.client, loan)], self.api.returns)
def test_revoke_hold(self): other_client, ignore = IntegrationClient.register( self._db, "http://other_library.org") hold, ignore = create(self._db, Hold, integration_client=other_client, license_pool=self.pool) assert_raises(CannotReleaseHold, self.shared_collection.revoke_hold, self.collection, self.client, hold) hold.integration_client = self.client self.shared_collection.revoke_hold(self.collection, self.client, hold) eq_([(self.client, hold)], self.api.released_holds)
def run(self, url): if not url: ValueError("No url provided. Could not create IntegrationClient.") url = " ".join(url) print "Creating IntegrationClient for '%s'" % url client, plaintext_secret = IntegrationClient.register(self._db, url) print client print ("RECORD THE FOLLOWING AUTHENTICATION DETAILS. " "The client secret cannot be recovered.") print "-" * 40 print "CLIENT KEY: %s" % client.key print "CLIENT SECRET: %s" % plaintext_secret self._db.commit()
def setup(self): super(TestSharedCollectionAPI, self).setup() self.collection = self._collection(protocol="Mock") self.shared_collection = SharedCollectionAPI(self._db, api_map={"Mock": MockAPI}) self.api = self.shared_collection.api(self.collection) ConfigurationSetting.for_externalintegration( BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS, self.collection.external_integration).value = json.dumps( ["http://library.org"]) self.client, ignore = IntegrationClient.register( self._db, "http://library.org") edition, self.pool = self._edition(with_license_pool=True, collection=self.collection) [self.delivery_mechanism] = self.pool.delivery_mechanisms
def run(self, url): if not url: ValueError("No url provided. Could not create IntegrationClient.") url = " ".join(url) print "Creating IntegrationClient for '%s'" % url client, plaintext_secret = IntegrationClient.register(self._db, url) print client print( "RECORD THE FOLLOWING AUTHENTICATION DETAILS. " "The client secret cannot be recovered.") print "-" * 40 print "CLIENT KEY: %s" % client print "CLIENT SECRET: %s" % plaintext_secret
def setup(self): super(TestSharedCollectionAPI, self).setup() self.collection = self._collection(protocol="Mock") self.shared_collection = SharedCollectionAPI( self._db, api_map = { "Mock" : MockAPI } ) self.api = self.shared_collection.api(self.collection) ConfigurationSetting.for_externalintegration( BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS, self.collection.external_integration ).value = json.dumps(["http://library.org"]) self.client, ignore = IntegrationClient.register(self._db, "http://library.org") edition, self.pool = self._edition( with_license_pool=True, collection=self.collection ) [self.delivery_mechanism] = self.pool.delivery_mechanisms
def test_borrow(self): # This client is registered, but isn't one of the allowed URLs for the collection # (maybe it was registered for a different shared collection). other_client, ignore = IntegrationClient.register(self._db, "http://other_library.org") # Trying to borrow raises an exception. assert_raises(AuthorizationFailedException, self.shared_collection.borrow, self.collection, other_client, self.pool) # A client that's registered with the collection can borrow. self.shared_collection.borrow(self.collection, self.client, self.pool) eq_([(self.client, self.pool)], self.api.checkouts) # If the client's checking out an existing hold, the hold must be for that client. hold, ignore = create(self._db, Hold, integration_client=other_client, license_pool=self.pool) assert_raises(CannotLoan, self.shared_collection.borrow, self.collection, self.client, self.pool, hold=hold) hold.integration_client = self.client self.shared_collection.borrow(self.collection, self.client, self.pool, hold=hold) eq_([(self.client, self.pool)], self.api.checkouts[1:])
def register(self, collection, auth_document_url, do_get=HTTP.get_with_timeout): """Register a library on an external circulation manager for access to this collection. The library's auth document url must be whitelisted in the collection's settings.""" if not auth_document_url: raise InvalidInputException( _("An authentication document URL is required to register a library.") ) auth_response = do_get(auth_document_url, allowed_response_codes=["2xx", "3xx"]) try: auth_document = json.loads(auth_response.content) except ValueError as e: raise RemoteInitiatedServerError( _( "Authentication document at %(auth_document_url)s was not valid JSON.", auth_document_url=auth_document_url, ), _("Remote authentication document"), ) links = auth_document.get("links") start_url = None for link in links: if link.get("rel") == "start": start_url = link.get("href") break if not start_url: raise RemoteInitiatedServerError( _( "Authentication document at %(auth_document_url)s did not contain a start link.", auth_document_url=auth_document_url, ), _("Remote authentication document"), ) external_library_urls = ConfigurationSetting.for_externalintegration( BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS, collection.external_integration, ).json_value if not external_library_urls or start_url not in external_library_urls: raise AuthorizationFailedException( _( "Your library's URL is not one of the allowed URLs for this collection. Ask the collection administrator to add %(library_url)s to the list of allowed URLs.", library_url=start_url, ) ) public_key = auth_document.get("public_key") if ( not public_key or not public_key.get("type") == "RSA" or not public_key.get("value") ): raise RemoteInitiatedServerError( _( "Authentication document at %(auth_document_url)s did not contain an RSA public key.", auth_document_url=auth_document_url, ), _("Remote authentication document"), ) public_key = public_key.get("value") encryptor = Configuration.cipher(public_key) normalized_url = IntegrationClient.normalize_url(start_url) client = get_one(self._db, IntegrationClient, url=normalized_url) if not client: client, ignore = IntegrationClient.register(self._db, start_url) shared_secret = client.shared_secret.encode("utf-8") encrypted_secret = encryptor.encrypt(shared_secret) return dict(metadata=dict(shared_secret=base64.b64encode(encrypted_secret)))
def test_register(self): # An auth document URL is required to register. assert_raises(InvalidInputException, self.shared_collection.register, self.collection, None) # If the url doesn't return a valid auth document, there's an exception. auth_response = "not json" def do_get(*args, **kwargs): return MockRequestsResponse(200, content=auth_response) assert_raises(RemoteInitiatedServerError, self.shared_collection.register, self.collection, "http://library.org/auth", do_get=do_get) # The auth document also must have a link to the library's catalog. auth_response = json.dumps({"links": []}) assert_raises(RemoteInitiatedServerError, self.shared_collection.register, self.collection, "http://library.org/auth", do_get=do_get) # If no external library URLs are configured, no one can register. auth_response = json.dumps({"links": [{"href": "http://library.org", "rel": "start"}]}) ConfigurationSetting.for_externalintegration( BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS, self.collection.external_integration ).value = None assert_raises(AuthorizationFailedException, self.shared_collection.register, self.collection, "http://library.org/auth", do_get=do_get) # If the library's URL isn't in the configuration, it can't register. auth_response = json.dumps({"links": [{"href": "http://differentlibrary.org", "rel": "start"}]}) ConfigurationSetting.for_externalintegration( BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS, self.collection.external_integration ).value = json.dumps(["http://library.org"]) assert_raises(AuthorizationFailedException, self.shared_collection.register, self.collection, "http://differentlibrary.org/auth", do_get=do_get) # Or if the public key is missing from the auth document. auth_response = json.dumps({"links": [{"href": "http://library.org", "rel": "start"}]}) assert_raises(RemoteInitiatedServerError, self.shared_collection.register, self.collection, "http://library.org/auth", do_get=do_get) auth_response = json.dumps({"public_key": { "type": "not RSA", "value": "123" }, "links": [{"href": "http://library.org", "rel": "start"}]}) assert_raises(RemoteInitiatedServerError, self.shared_collection.register, self.collection, "http://library.org/auth", do_get=do_get) auth_response = json.dumps({"public_key": { "type": "RSA" }, "links": [{"href": "http://library.org", "rel": "start"}]}) assert_raises(RemoteInitiatedServerError, self.shared_collection.register, self.collection, "http://library.org/auth", do_get=do_get) # Here's an auth document with a valid key. key = RSA.generate(2048) public_key = key.publickey().exportKey() encryptor = PKCS1_OAEP.new(key) auth_response = json.dumps({"public_key": { "type": "RSA", "value": public_key }, "links": [{"href": "http://library.org", "rel": "start"}]}) response = self.shared_collection.register(self.collection, "http://library.org/auth", do_get=do_get) # An IntegrationClient has been created. client = get_one(self._db, IntegrationClient, url=IntegrationClient.normalize_url("http://library.org/")) decrypted_secret = encryptor.decrypt(base64.b64decode(response.get("metadata", {}).get("shared_secret"))) eq_(client.shared_secret, decrypted_secret)
def test_register(self): # An auth document URL is required to register. assert_raises(InvalidInputException, self.shared_collection.register, self.collection, None) # If the url doesn't return a valid auth document, there's an exception. auth_response = "not json" def do_get(*args, **kwargs): return MockRequestsResponse(200, content=auth_response) assert_raises(RemoteInitiatedServerError, self.shared_collection.register, self.collection, "http://library.org/auth", do_get=do_get) # The auth document also must have a link to the library's catalog. auth_response = json.dumps({"links": []}) assert_raises(RemoteInitiatedServerError, self.shared_collection.register, self.collection, "http://library.org/auth", do_get=do_get) # If no external library URLs are configured, no one can register. auth_response = json.dumps( {"links": [{ "href": "http://library.org", "rel": "start" }]}) ConfigurationSetting.for_externalintegration( BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS, self.collection.external_integration).value = None assert_raises(AuthorizationFailedException, self.shared_collection.register, self.collection, "http://library.org/auth", do_get=do_get) # If the library's URL isn't in the configuration, it can't register. auth_response = json.dumps({ "links": [{ "href": "http://differentlibrary.org", "rel": "start" }] }) ConfigurationSetting.for_externalintegration( BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS, self.collection.external_integration).value = json.dumps( ["http://library.org"]) assert_raises(AuthorizationFailedException, self.shared_collection.register, self.collection, "http://differentlibrary.org/auth", do_get=do_get) # Or if the public key is missing from the auth document. auth_response = json.dumps( {"links": [{ "href": "http://library.org", "rel": "start" }]}) assert_raises(RemoteInitiatedServerError, self.shared_collection.register, self.collection, "http://library.org/auth", do_get=do_get) auth_response = json.dumps({ "public_key": { "type": "not RSA", "value": "123" }, "links": [{ "href": "http://library.org", "rel": "start" }] }) assert_raises(RemoteInitiatedServerError, self.shared_collection.register, self.collection, "http://library.org/auth", do_get=do_get) auth_response = json.dumps({ "public_key": { "type": "RSA" }, "links": [{ "href": "http://library.org", "rel": "start" }] }) assert_raises(RemoteInitiatedServerError, self.shared_collection.register, self.collection, "http://library.org/auth", do_get=do_get) # Here's an auth document with a valid key. key = RSA.generate(2048) public_key = key.publickey().exportKey() encryptor = PKCS1_OAEP.new(key) auth_response = json.dumps({ "public_key": { "type": "RSA", "value": public_key }, "links": [{ "href": "http://library.org", "rel": "start" }] }) response = self.shared_collection.register(self.collection, "http://library.org/auth", do_get=do_get) # An IntegrationClient has been created. client = get_one( self._db, IntegrationClient, url=IntegrationClient.normalize_url("http://library.org/")) decrypted_secret = encryptor.decrypt( base64.b64decode( response.get("metadata", {}).get("shared_secret"))) eq_(client.shared_secret, decrypted_secret)
_("Error decoding JWT: %(message)s", message=e.message)) # The ability to create a valid JWT indicates control over # the server, so it's not necessary to know the current # secret to set a new secret. client, is_new = IntegrationClient.for_url(self._db, url) client.randomize_secret() else: # If no JWT is provided, then we use the old logic. The first # time registration happens, no special authentication # is required apart from the ability to decode the secret. # # On subsequent attempts, the old secret must be provided to # create a new secret. try: client, is_new = IntegrationClient.register( self._db, url, submitted_secret=submitted_secret) except ValueError as e: log.error("Error in IntegrationClient.register", exc_info=e) return INVALID_CREDENTIALS.detailed(e.message) # Now that we have an IntegrationClient with a shared # secret, encrypt the shared secret with the provided public key # and send it back. encrypted_secret = encryptor.encrypt(str(client.shared_secret)) shared_secret = base64.b64encode(encrypted_secret) auth_data = dict(id=url, metadata=dict(shared_secret=shared_secret)) content = json.dumps(auth_data) headers = {'Content-Type': OPDS_2_MEDIA_TYPE} status_code = 200 if is_new:
def register(self, do_get=HTTP.get_with_timeout): # 'url' points to a document containing a public key that # should be used to sign the secret. public_key_url = request.form.get('url') if not public_key_url: return NO_AUTH_URL log = logging.getLogger("Public key integration document (%s)" % public_key_url) # 'jwt' is a JWT that proves the client making this request # controls the private key. # # For backwards compatibility purposes, it's okay not to # provide a JWT, but it may lead to situations where # the client doesn't know their shared secret and can't reset # it. jwt_token = request.form.get('jwt') try: response = do_get(public_key_url, allowed_response_codes=['2xx', '3xx']) except Exception as e: log.error("Error retrieving URL", exc_info=e) return REMOTE_INTEGRATION_ERROR.detailed( _("Could not retrieve public key URL %(url)s", url=public_key_url)) content_type = None if response.headers: content_type = response.headers.get('Content-Type') if not (response.content and content_type == OPDS_2_MEDIA_TYPE): # There's no JSON to speak of. log.error("Could not find OPDS 2 document: %s/%s", response.content, content_type) return INVALID_INTEGRATION_DOCUMENT.detailed( _("Not an integration document: %(doc)s", doc=response.content)) public_key_response = response.json() url = public_key_response.get('id') if not url: message = _( "The public key integration document is missing an id.") log.error(unicode(message)) return INVALID_INTEGRATION_DOCUMENT.detailed(message) # Remove any library-specific URL elements. def base_url(full_url): scheme, netloc, path, parameters, query, fragment = urlparse.urlparse( full_url) return '%s://%s' % (scheme, netloc) client_url = base_url(url) base_public_key_url = base_url(public_key_url) if not client_url == base_public_key_url: log.error( "ID of OPDS 2 document (%s) doesn't match submitted URL (%s)", client_url, base_public_key_url) return INVALID_INTEGRATION_DOCUMENT.detailed( _("The public key integration document id (%(id)s) doesn't match submitted url %(url)s", id=client_url, url=base_public_key_url)) public_key = public_key_response.get('public_key') if not (public_key and public_key.get('type') == 'RSA' and public_key.get('value')): message = _( "The public key integration document is missing an RSA public_key." ) log.error(unicode(message)) return INVALID_INTEGRATION_DOCUMENT.detailed(message) public_key_text = public_key.get('value') public_key = RSA.importKey(public_key_text) encryptor = PKCS1_OAEP.new(public_key) submitted_secret = None auth_header = request.headers.get('Authorization') if auth_header and isinstance( auth_header, basestring) and 'bearer' in auth_header.lower(): token = auth_header.split(' ')[1] submitted_secret = base64.b64decode(token) if jwt_token: # In the new system, the 'token' must be a JWT whose # signature can be verified with the public key. try: parsed = jwt.decode(jwt_token, public_key_text, algorithm='RS256') except Exception, e: return INVALID_CREDENTIALS.detailed( _("Error decoding JWT: %(message)s", message=e.message)) # The ability to create a valid JWT indicates control over # the server, so it's not necessary to know the current # secret to set a new secret. client, is_new = IntegrationClient.for_url(self._db, url) client.randomize_secret()
if not external_library_urls or start_url not in external_library_urls: raise AuthorizationFailedException( _("Your library's URL is not one of the allowed URLs for this collection. Ask the collection administrator to add %(library_url)s to the list of allowed URLs.", library_url=start_url)) public_key = auth_document.get("public_key") if not public_key or not public_key.get("type") == "RSA" or not public_key.get("value"): raise RemoteInitiatedServerError( _("Authentication document at %(auth_document_url)s did not contain an RSA public key.", auth_document_url=auth_document_url), _("Remote authentication document")) public_key = public_key.get("value") encryptor = Configuration.cipher(public_key) normalized_url = IntegrationClient.normalize_url(start_url) client = get_one(self._db, IntegrationClient, url=normalized_url) if not client: client, ignore = IntegrationClient.register(self._db, start_url) shared_secret = client.shared_secret encrypted_secret = encryptor.encrypt(str(shared_secret)) return dict(metadata=dict(shared_secret=base64.b64encode(encrypted_secret))) def check_client_authorization(self, collection, client): """Verify that an IntegrationClient is whitelisted for access to the collection.""" external_library_urls = ConfigurationSetting.for_externalintegration( BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS, collection.external_integration ).json_value if client.url not in [IntegrationClient.normalize_url(url) for url in external_library_urls]: raise AuthorizationFailedException()
_("Your library's URL is not one of the allowed URLs for this collection. Ask the collection administrator to add %(library_url)s to the list of allowed URLs.", library_url=start_url)) public_key = auth_document.get("public_key") if not public_key or not public_key.get( "type") == "RSA" or not public_key.get("value"): raise RemoteInitiatedServerError( _("Authentication document at %(auth_document_url)s did not contain an RSA public key.", auth_document_url=auth_document_url), _("Remote authentication document")) public_key = public_key.get("value") public_key = RSA.importKey(public_key) encryptor = PKCS1_OAEP.new(public_key) normalized_url = IntegrationClient.normalize_url(start_url) client = get_one(self._db, IntegrationClient, url=normalized_url) if not client: client, ignore = IntegrationClient.register(self._db, start_url) shared_secret = client.shared_secret encrypted_secret = encryptor.encrypt(str(shared_secret)) return dict(metadata=dict( shared_secret=base64.b64encode(encrypted_secret))) def check_client_authorization(self, collection, client): """Verify that an IntegrationClient is whitelisted for access to the collection.""" external_library_urls = ConfigurationSetting.for_externalintegration( BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS, collection.external_integration).json_value if client.url not in [
def register(self, do_get=HTTP.get_with_timeout): # 'url' points to a document containing a public key that # should be used to sign the secret. public_key_url = request.form.get('url') if not public_key_url: return NO_AUTH_URL log = logging.getLogger( "Public key integration document (%s)" % public_key_url ) # 'jwt' is a JWT that proves the client making this request # controls the private key. # # For backwards compatibility purposes, it's okay not to # provide a JWT, but it may lead to situations where # the client doesn't know their shared secret and can't reset # it. jwt_token = request.form.get('jwt') try: response = do_get( public_key_url, allowed_response_codes=['2xx', '3xx'] ) except Exception as e: log.error("Error retrieving URL", exc_info=e) return REMOTE_INTEGRATION_ERROR.detailed( _("Could not retrieve public key URL %(url)s", url=public_key_url) ) content_type = None if response.headers: content_type = response.headers.get('Content-Type') if not (response.content and content_type == OPDS_2_MEDIA_TYPE): # There's no JSON to speak of. log.error("Could not find OPDS 2 document: %s/%s", response.content, content_type) return INVALID_INTEGRATION_DOCUMENT.detailed( _("Not an integration document: %(doc)s", doc=response.content) ) public_key_response = response.json() url = public_key_response.get('id') if not url: message = _("The public key integration document is missing an id.") log.error(unicode(message)) return INVALID_INTEGRATION_DOCUMENT.detailed(message) # Remove any library-specific URL elements. def base_url(full_url): scheme, netloc, path, parameters, query, fragment = urlparse.urlparse(full_url) return '%s://%s' % (scheme, netloc) client_url = base_url(url) base_public_key_url = base_url(public_key_url) if not client_url == base_public_key_url: log.error( "ID of OPDS 2 document (%s) doesn't match submitted URL (%s)", client_url, base_public_key_url ) return INVALID_INTEGRATION_DOCUMENT.detailed( _("The public key integration document id (%(id)s) doesn't match submitted url %(url)s", id=client_url, url=base_public_key_url) ) public_key = public_key_response.get('public_key') if not (public_key and public_key.get('type') == 'RSA' and public_key.get('value')): message = _("The public key integration document is missing an RSA public_key.") log.error(unicode(message)) return INVALID_INTEGRATION_DOCUMENT.detailed(message) public_key_text = public_key.get('value') public_key = RSA.importKey(public_key_text) encryptor = PKCS1_OAEP.new(public_key) submitted_secret = None auth_header = request.headers.get('Authorization') if auth_header and isinstance(auth_header, basestring) and 'bearer' in auth_header.lower(): token = auth_header.split(' ')[1] submitted_secret = base64.b64decode(token) if jwt_token: # In the new system, the 'token' must be a JWT whose # signature can be verified with the public key. try: parsed = jwt.decode( jwt_token, public_key_text, algorithm='RS256' ) except Exception, e: return INVALID_CREDENTIALS.detailed( _("Error decoding JWT: %(message)s", message=e.message) ) # The ability to create a valid JWT indicates control over # the server, so it's not necessary to know the current # secret to set a new secret. client, is_new = IntegrationClient.for_url(self._db, url) client.randomize_secret()
# The ability to create a valid JWT indicates control over # the server, so it's not necessary to know the current # secret to set a new secret. client, is_new = IntegrationClient.for_url(self._db, url) client.randomize_secret() else: # If no JWT is provided, then we use the old logic. The first # time registration happens, no special authentication # is required apart from the ability to decode the secret. # # On subsequent attempts, the old secret must be provided to # create a new secret. try: client, is_new = IntegrationClient.register( self._db, url, submitted_secret=submitted_secret ) except ValueError as e: log.error("Error in IntegrationClient.register", exc_info=e) return INVALID_CREDENTIALS.detailed(e.message) # Now that we have an IntegrationClient with a shared # secret, encrypt the shared secret with the provided public key # and send it back. encrypted_secret = encryptor.encrypt(str(client.shared_secret)) shared_secret = base64.b64encode(encrypted_secret) auth_data = dict( id=url, metadata=dict(shared_secret=shared_secret) ) content = json.dumps(auth_data)