def check_site(): try: params = parse_params(request.forms.decode(), domain=string_param('domain', required=True, strip=True, min_length=1, max_length=DOMAIN_MAX_LENGTH), no_rescan=boolean_param('no_rescan', default=False, empty=True, strip=True)) except ParamParseError: return template('gpc_invalid', domain=None) domain = normalise_domain(params['domain']) if not check_domain(domain): return template('gpc_invalid', domain=domain) result = es_dao.get(domain) if result is not None: if params['no_rescan'] or result['status'] == 'pending': redirect(f'/sites/{domain}') # Non-pending scans should have a scan datetime. last_scan_dt = rfc3339.parse_datetime(result['last_scan_dt']) # If the last scan hasn't expired yet, don't rescan. if rfc3339.now() < last_scan_dt + SCAN_TTL: if testing_mode: log.info('Would have redirected to existing scan for %(domain)s if on prod.', {'domain': domain}) else: redirect(f'/sites/{domain}') r = requests.post(well_known_sites_endpoint, data={'domain': domain, 'rescan': 'true'}) r.raise_for_status() redirect(f'/sites/{domain}')
def logout(): params = parse_params(request.query.decode(), continue_url=string_param('continue', strip=True, max_length=2000)) continue_url = params.get('continue_url') if continue_url: check_continue_url(continue_url) if request.session is not None: session_handler.clear_session() redirect(continue_url or DEFAULT_CONTINUE_URL)
def get_sites(): params = parse_params(request.params.decode(), page=integer_param('page', default=0, positive=True)) page = params['page'] offset = page * SITES_PAGE_SIZE total, results = es_dao.find(supports_gpc=True, is_base_domain=True, sort=['id'], offset=offset, limit=SITES_PAGE_SIZE, timeout=30) domains = [result[0]['domain'] for result in results] previous_page = page - 1 if page > 0 else None next_page = page + 1 next_offset = next_page * SITES_PAGE_SIZE if next_offset >= total: next_page = None return template('sites', domains=domains, previous_page=previous_page, next_page=next_page)
def get_login(): params = parse_params(request.query.decode(), continue_url=string_param('continue', strip=True, max_length=2000)) continue_url = params.get('continue_url') if continue_url: check_continue_url(continue_url) state, nonce, oidc_login_uri = construct_oidc_request('openid') # NOTE: The state field works as Login CSRF protection. session_handler.set_oidc_data(state, nonce, continue_url or DEFAULT_CONTINUE_URL) return template('login', oidc_name=oidc_name, oidc_about_url=oidc_about_url, oidc_login_uri=oidc_login_uri)
def index(): try: params = parse_params(request.query.decode(), domain=string_param('domain', strip=True, min_length=1, max_length=DOMAIN_MAX_LENGTH)) domain = params.get('domain') except ParamParseError: domain = None if domain: domain = normalise_domain(domain) if not check_domain(domain): domain = None scanned_count, supporting_count = es_dao.count(timeout=30) r = template('index', domain=domain, scanned_count=scanned_count, supporting_count=supporting_count) set_headers(r, STATIC_FILE_HEADERS) return r
def submit_secret(): if request.session: user_id = request.session['user_id'] else: user_id = None params = parse_params(request.forms.decode(), description=string_param('description', strip=True, max_length=100), secret=string_param('secret', required=True, max_length=2000), ttl=string_param('ttl', required=True, enum=VALID_TTLS.keys())) description = params.get('description') secret = params['secret'] ttl = params['ttl'] now_dt = rfc3339.now() secret_id = generate_id() secret = Secret(secret_id=secret_id, user_id=user_id, description=description, secret=secret, create_dt=now_dt, expire_dt=now_dt + VALID_TTLS[ttl][0]) dao.insert_secret(secret) response.status = 202 response.set_header('Location', f'{service_path}/secrets/{secret_id}') return template('submit_result', service_address=service_address, user_id=user_id, secret_id=secret_id, ttl=VALID_TTLS[ttl][1])
def get_oidc_callback(): # NOTE: Generally raise 500 for unexpected issues with the oidc flow, caused either by our # oidc state management or the oidc provider response. All traffic to this endpoint # should be via the oidc flow, and the provider should be working to spec. If not, # something is likely wrong with the flow implementation, so 500 is appropriate. # Check state and error before anything else, to make sure nothing's fishy params = parse_params(request.query.decode(), state=string_param('state', strip=True), error=string_param('error', strip=True), error_description=string_param( 'error_description', strip=True)) # Use 500 rather than "nicer" error if state is missing. state = params.get('state') if not state: log.warning('Received OIDC callback with no state.') abort(500) oidc_data = session_handler.get_oidc_data(state) if not oidc_data: log.warning('Received OIDC callback with no OIDC data cookie.') abort(500) # Only part of the state is used when fetching the OIDC data, so still need to check it. if state != oidc_data['state']: log.warning('Received OIDC callback with mismatching state.') abort(500) # State seems to be OK, so this is a valid response for this request. Drop the OIDC data # so this state can't be used again. # NOTE: This won't take effect if a later error occurs. session_handler.clear_oidc_data(state) error = params.get('error') if error: # If they rejected the OIDC scopes, send them back home. Up to them to initiate again. # TODO: Should we show them a message explaining this somehow? if error == 'access_denied': redirect('/') # Any other error... else: error_description = params.get('error_description') log_msg = 'Received OIDC callback with error %(error)s' log_params = {'error': error} if error_description: log_msg += ': %(error_description)s' log_params['error_description'] = error_description log.warning(log_msg, log_params) abort(500) # If there wasn't an error, there should be a code params = parse_params(request.query.decode(), code=string_param('code', strip=True)) code = params.get('code') # Once again, use 500 rather than a "nicer" error if code is missing if not code: log.warning('Received OIDC callback with no code.') abort(500) # Check continue_url before attempting to call the OIDC provider, just in case. continue_url = oidc_data['continue_url'] # NOTE: we check this again, since the oidc cookie data isn't signed so could be changed by # the user agent. check_continue_url(continue_url) r = requests.post(oidc_token_endpoint, timeout=10, auth=(oidc_client_id, oidc_client_secret), data={ 'grant_type': 'authorization_code', 'client_id': oidc_client_id, 'redirect_uri': oidc_redirect_uri, 'code': code }) # Only supported response status code. if r.status_code == 200: pass else: log.warning( 'OIDC token endpoint returned unexpected status code %(status_code)s.', {'status_code': r.status_code}) r.raise_for_status() raise NotImplementedError( f'Unsupported status code {r.status_code}.') try: id_token = r.json()['id_token'] id_token_jwt = token_decoder.decode_id_token(id_token) except (ValueError, KeyError, InvalidTokenError) as e: log.warning( 'OIDC token endpoint returned invalid response: %(error)s', {'error': e}) abort(500) if 'nonce' not in id_token_jwt: log.warning('OIDC token endpoint didn\'t return nonce in token.') abort(500) if id_token_jwt['nonce'] != hash_urlsafe(oidc_data['nonce']): log.warning( 'OIDC token endpoint returned incorrect nonce in token.') abort(500) session_handler.set_session(id_token) redirect(continue_url)