class RemoteVerifier(object): implements(IBrowserIdVerifier) def __init__(self, audiences=None, trusted_issuers=None, allowed_issuers=None, verifier_url=None): # Since we don't parse the assertion locally, we cannot support # list- or pattern-based audience strings. if audiences is not None: assert isinstance(audiences, basestring) self.audiences = audiences if isinstance(trusted_issuers, basestring): trusted_issuers = trusted_issuers.split() self.trusted_issuers = trusted_issuers if isinstance(allowed_issuers, basestring): allowed_issuers = allowed_issuers.split() self.allowed_issuers = allowed_issuers if verifier_url is None: verifier_url = "https://verifier.accounts.firefox.com/v2" self.verifier_url = verifier_url self.session = requests.Session() self.session.verify = True def verify(self, assertion, audience=None): if audience is None: audience = self.audiences body = {'assertion': assertion, 'audience': audience} if self.trusted_issuers is not None: body['trustedIssuers'] = self.trusted_issuers headers = {'content-type': 'application/json'} try: response = self.session.post(self.verifier_url, data=json.dumps(body), headers=headers) except (socket.error, requests.RequestException), e: msg = "Failed to POST %s. Reason: %s" % (self.verifier_url, str(e)) raise ConnectionError(msg) if response.status_code != 200: raise ConnectionError('server returned invalid response') try: data = json.loads(response.text) except ValueError: raise ConnectionError("server returned invalid response") if data.get('status') != "okay": reason = data.get('reason', 'unknown error') if "audience mismatch" in reason: raise AudienceMismatchError(data.get("audience"), audience) if "expired" in reason or "issued later than" in reason: raise ExpiredSignatureError(reason) raise InvalidSignatureError(reason) if self.allowed_issuers is not None: issuer = data.get('issuer') if issuer not in self.allowed_issuers: raise InvalidIssuerError("Issuer not allowed: %s" % (issuer, )) return data
def secure_urlopen(url, data=None, timeout=None, ca_certs=None): """More secure replacement for urllib2.urlopen. This function provides an alternative to urllib2.urlopen which does basic validation and verification of HTTPS server certificates. """ global DEFAULT_CACERT_FILE # Try to find platform default ca-cert file if none was specified. if ca_certs is None: ca_certs = DEFAULT_CACERT_FILE if ca_certs is None: for filenm in POSSIBLE_CACERT_FILES: if os.path.exists(filenm): ca_certs = DEFAULT_CACERT_FILE = filenm break if ca_certs is None: msg = "Could not locate a CA certificates file for HTTPS."\ " Your requests will be vulnerable to man-in-the-middle"\ " attacks. It is *HIGHLY RECOMMENDED* that you specify"\ " the ca_certs parameter with the path to a valid"\ " certificates file." warnings.warn(msg, stacklevel=2) # Use a cached opener if possible. try: opener = _OPENER_CACHE[ca_certs] except KeyError: opener = urllib2.build_opener(ValidatingHTTPSHandler(ca_certs)) _OPENER_CACHE[ca_certs] = opener try: return opener.open(url, data, timeout) except (EnvironmentError, httplib.HTTPException), e: raise ConnectionError(str(e))
def _get(url): """Fetch resource with requests.""" try: return requests.get(url) except RequestException, e: msg = "Impossible to get %s. Reason: %s" % (url, str(e)) raise ConnectionError(msg)
def verify(self, token): try: userinfo = self._client.verify_token(token, self.scope) except (socket.error, requests.RequestException), e: msg = 'Verification request to %s failed; reason: %s' msg %= (self.server_url, str(e)) raise ConnectionError(msg)
def request(method, url, **kwds): """Make an HTTP request to the given URL.""" try: return requests.request(method, url, **kwds) except (RequestException, socket.error) as e: msg = "Failed to %s %s. Reason: %s" % (method, url, str(e)) raise ConnectionError(msg)
def urlread(url, data=None): """Read the given URL, return response as a string.""" # Anything that goes wrong inside this function will # be re-raised as an instance of ConnectionError. try: resp = secure_urlopen(url, data) try: info = resp.info() except AttributeError: info = {} content_length = info.get("Content-Length") if content_length is None: data = resp.read() else: try: data = resp.read(int(content_length)) except ValueError: raise ConnectionError("server sent invalid content-length") except Exception, e: raise ConnectionError(str(e))
def verify(self, assertion, audience=None): if audience is None: audience = self.audiences body = {'assertion': assertion, 'audience': audience} if self.trusted_issuers is not None: body['trustedIssuers'] = self.trusted_issuers headers = {'content-type': 'application/json'} try: response = self.session.post(self.verifier_url, data=json.dumps(body), headers=headers) except (socket.error, requests.RequestException), e: msg = "Failed to POST %s. Reason: %s" % (self.verifier_url, str(e)) raise ConnectionError(msg)
def verify(self, assertion, audience=None): if audience is None: audience = self.audiences body = {'assertion': assertion, 'audience': audience} if self.trusted_issuers is not None: body['trustedIssuers'] = self.trusted_issuers headers = {'content-type': 'application/json'} try: response = self.session.post(self.verifier_url, data=json.dumps(body), headers=headers, timeout=self.timeout) except (socket.error, requests.RequestException) as e: msg = "Failed to POST %s. Reason: %s" % (self.verifier_url, str(e)) raise ConnectionError(msg) if response.status_code != 200: raise ConnectionError('server returned invalid response code') try: data = json.loads(response.text) except ValueError: raise ConnectionError("server returned invalid response body") if data.get('status') != "okay": reason = data.get('reason', 'unknown error') if "audience mismatch" in reason: raise AudienceMismatchError(data.get("audience"), audience) if "expired" in reason or "issued later than" in reason: raise ExpiredSignatureError(reason) raise InvalidSignatureError(reason) if self.allowed_issuers is not None: issuer = data.get('issuer') if issuer not in self.allowed_issuers: raise InvalidIssuerError("Issuer not allowed: %s" % (issuer,)) return data
def verify(self, token): try: userinfo = self._client.verify_token(token, self.scope) except (socket.error, requests.RequestException) as e: msg = 'Verification request to %s failed; reason: %s' msg %= (self.server_url, str(e)) raise ConnectionError(msg) issuer = userinfo.get('issuer', self.default_issuer) if not issuer or not isinstance(issuer, basestring): msg = 'Could not determine issuer from verifier response' raise fxa.errors.TrustError(msg) idpclaims = {} if userinfo.get('generation') is not None: idpclaims['fxa-generation'] = userinfo['generation'] return { 'email': userinfo['user'] + '@' + issuer, 'idpClaims': idpclaims, }
def verify(self, assertion, audience=None): """Verify the given BrowserID assertion. This method posts the given BrowserID assertion to the remote verifier service. If it is successfully verified then a dict giving the email and audience is returned. If it is not valid then an error is raised. If the 'audience' argument is given, it first verifies that the audience of the assertion matches the one given. This can help avoid doing lots of crypto for assertions that can't be valid. If you don't specify an audience, you *MUST* validate the audience value returned by this method. """ # Check the audience locally. # No point talking to the network if we know it's going to fail. # If no explicit audience was given, this will also parse it out # for inclusion in the request to the remote verifier service. audience = self.check_audience(assertion, audience) response = netutils.post(self.verifier_url, { 'assertion': assertion, 'audience': audience }) # BrowserID server sends "500 server error" for broken assertions. # For now, just translate that directly. Should check by hand. if response.status_code == 500: raise ValueError('Malformed assertion') try: data = json.loads(response.text) except ValueError: raise ConnectionError("server returned invalid response") # Did it come back clean? if data.get('status') != "okay": raise InvalidSignatureError(str(data)) if data.get('audience') != audience: raise AudienceMismatchError(data.get("audience"), audience) return data
def test_handling_of_503_error_from_server(self): with patched_urlopen(exc=ConnectionError("503 Back Off")): self.assertRaises(ConnectionError, self.verifier.verify, EXPIRED_ASSERTION)
class RemoteVerifier(Verifier): """Class for remote verification of BrowserID identity assertions. This class submits assertions to the browserid.org verifier service for remote verification. It's slower but potentially a little bit safer than the still-under-development LocalVerifier class. """ def __init__(self, audiences=None, verifier_url=None): if verifier_url is None: verifier_url = BROWSERID_VERIFIER_URL super(RemoteVerifier, self).__init__(audiences) self.verifier_url = verifier_url def verify(self, assertion, audience=None): """Verify the given BrowserID assertion. This method posts the given BrowserID assertion to the remote verifier service. If it is successfully verified then a dict giving the email and audience is returned. If it is not valid then an error is raised. If the 'audience' argument is given, it first verifies that the audience of the assertion matches the one given. This can help avoid doing lots of crypto for assertions that can't be valid. If you don't specify an audience, you *MUST* validate the audience value returned by this method. """ # Check the audience locally. # No point talking to the network if we know it's going to fail. # If no explicit audience was given, this will also parse it out # for inclusion in the request to the remote verifier service. audience = self.check_audience(assertion, audience) # Encode the data into x-www-form-urlencoded. post_data = {"assertion": assertion, "audience": audience} post_data = "&".join("%s=%s" % item for item in post_data.items()) # Post it to the verifier. try: resp = secure_urlopen(self.verifier_url, post_data) except ConnectionError, e: # BrowserID server sends "500 server error" for broken assertions. # For now, just translate that directly. Should check by hand. if "500" in str(e): raise ValueError("Malformed assertion") raise # Read the response, being careful to raise an appropriate # error if the server does something funny. try: try: info = resp.info() except AttributeError: info = {} content_length = info.get("Content-Length") if content_length is None: data = resp.read() else: data = resp.read(int(content_length)) data = json.loads(data) except ValueError: raise ConnectionError("server returned invalid response") # Did it come back clean? if data.get('status') != "okay": raise InvalidSignatureError(str(data)) if data.get('audience') != audience: raise AudienceMismatchError(data.get("audience"), audience) return data