class APIDiscourseClient(object): def __init__(self, settings): self.settings = settings self.timeout = int(settings['url.timeout']) self.discourse_base_url = settings['discourse.url'] self.discourse_public_url = settings['discourse.public_url'] self.api_key = settings['discourse.api_key'] self.sso_key = str(settings.get('discourse.sso_secret')) # no unicode self.discourse_userid_cache = {} # FIXME: are we guaranteed usernames can never change? -> no! self.discourse_username_cache = {} self.client = DiscourseClient( self.discourse_base_url, api_username='******', # the built-in Discourse user api_key=self.api_key, timeout=self.timeout) def get_userid(self, userid): discourse_userid = self.discourse_userid_cache.get(userid) if not discourse_userid: discourse_user = self.client.by_external_id(userid) discourse_userid = discourse_user['id'] self.discourse_userid_cache[userid] = discourse_userid self.discourse_username_cache[userid] = discourse_user['username'] return discourse_userid def get_username(self, userid): discourse_username = self.discourse_username_cache.get(userid) if not discourse_username: discourse_user = self.client.by_external_id(userid) self.discourse_userid_cache[userid] = discourse_user['id'] discourse_username = discourse_user['username'] self.discourse_username_cache[userid] = discourse_username return discourse_username def sync_sso(self, user): result = self.client.sync_sso(sso_secret=self.sso_key, name=user.name, username=user.forum_username, email=user.email, external_id=user.id) if result: self.discourse_userid_cache[user.id] = result['id'] return result def logout(self, userid): discourse_userid = self.get_userid(userid) self.client.log_out(discourse_userid) return discourse_userid def suspend(self, userid, duration, reason): discourse_userid = self.get_userid(userid) return self.client.suspend(discourse_userid, duration, reason) def unsuspend(self, userid): discourse_userid = self.get_userid(userid) return self.client.unsuspend(discourse_userid) # Below this: SSO provider def decode_payload(self, payload): decoded = b64decode(payload.encode('utf-8')).decode('utf-8') assert 'nonce' in decoded assert len(payload) > 0 return decoded def check_signature(self, payload, signature): key = self.sso_key.encode('utf-8') h = hmac.new(key, payload.encode('utf-8'), digestmod=hashlib.sha256) this_signature = h.hexdigest() if this_signature != signature: log.error('Signature mismatch') raise HTTPBadRequest('discourse login failed') def request_nonce(self): url = '%s/session/sso' % self.discourse_base_url try: r = requests.get(url, allow_redirects=False, timeout=self.timeout) assert r.status_code == 302 except Exception: log.error('Could not request nonce', exc_info=True) raise Exception('Could not request nonce') location = r.headers['Location'] parsed = urllib.parse.urlparse(location) params = urllib.parse.parse_qs(parsed.query) sso = params['sso'][0] sig = params['sig'][0] self.check_signature(sso, sig) payload = self.decode_payload(sso) return parse_qs(payload)['nonce'][0] def create_response_payload(self, user, nonce, url_part): assert nonce is not None, 'No nonce passed' params = { 'nonce': nonce, 'email': user.email, 'external_id': user.id, 'username': user.forum_username, 'name': user.name, } key = self.sso_key.encode('utf-8') r_payload = b64encode(urllib.parse.urlencode(params).encode('utf-8')) h = hmac.new(key, r_payload, digestmod=hashlib.sha256) qs = urllib.parse.urlencode({'sso': r_payload, 'sig': h.hexdigest()}) return '%s%s?%s' % (self.discourse_public_url, url_part, qs) def get_nonce_from_sso(self, sso, sig): payload = urllib.parse.unquote(sso) try: decoded = self.decode_payload(payload) except Exception as e: log.error('Failed to decode payload', e) raise HTTPBadRequest('discourse login failed') self.check_signature(payload, sig) # Build the return payload qs = parse_qs(decoded) return qs['nonce'][0] def redirect(self, user, sso, signature): nonce = self.get_nonce_from_sso(sso, signature) return self.create_response_payload(user, nonce, '/session/sso_login') def redirect_without_nonce(self, user): nonce = self.request_nonce() return self.create_response_payload(user, nonce, '/session/sso_login')