Skip to content

Commit

Permalink
Add support for Cloudflare Captcha
Browse files Browse the repository at this point in the history
  • Loading branch information
giacomognosis committed Dec 22, 2024
1 parent 198d32e commit 4744472
Show file tree
Hide file tree
Showing 20 changed files with 233 additions and 133 deletions.
46 changes: 23 additions & 23 deletions .github/workflows/publish-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,28 +77,28 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

eks-deployment-restart:
# Run job on branch dev only
if: github.ref == 'refs/heads/dev'
runs-on: ubuntu-latest
needs: build-and-push-image
permissions:
id-token: write # Required for the OIDC, see https://github.com/aws-actions/configure-aws-credentials?tab=readme-ov-file#OIDC
contents: read
steps:
- name: configure aws credentials
uses: aws-actions/[email protected]
with:
audience: sts.amazonaws.com
role-to-assume: ${{ secrets.DEV_AWS_EKS_ROLE }}
role-session-name: GitHub_to_AWS_via_FederatedOIDC
aws-region: ${{ secrets.DEV_AWS_REGION }}
# eks-deployment-restart:
# # Run job on branch dev only
# if: github.ref == 'refs/heads/dev'
# runs-on: ubuntu-latest
# needs: build-and-push-image
# permissions:
# id-token: write # Required for the OIDC, see https://github.com/aws-actions/configure-aws-credentials?tab=readme-ov-file#OIDC
# contents: read
# steps:
# - name: configure aws credentials
# uses: aws-actions/[email protected]
# with:
# audience: sts.amazonaws.com
# role-to-assume: ${{ secrets.DEV_AWS_EKS_ROLE }}
# role-session-name: GitHub_to_AWS_via_FederatedOIDC
# aws-region: ${{ secrets.DEV_AWS_REGION }}

- name: Configure kubectl for EKS
run: aws eks update-kubeconfig --name ${{ secrets.DEV_AWS_EKS_CLUSTER }} --region ${{ secrets.DEV_AWS_REGION }}
# - name: Configure kubectl for EKS
# run: aws eks update-kubeconfig --name ${{ secrets.DEV_AWS_EKS_CLUSTER }} --region ${{ secrets.DEV_AWS_REGION }}

- name: Restart Bridge Explorer Deployment
if: github.ref == 'refs/heads/dev'
run: |
kubectl config use-context arn:aws:eks:${{ secrets.DEV_AWS_REGION }}:${{ secrets.DEV_AWS_ACCOUNT_ID }}:cluster/${{ secrets.DEV_AWS_EKS_CLUSTER }}
kubectl rollout restart deploy/${{ secrets.DEV_AWS_EKS_DEPLOYMENT_API }} -n ${{ secrets.DEV_AWS_EKS_NAMESPACE }}
# - name: Restart Deployment
# if: github.ref == 'refs/heads/dev'
# run: |
# kubectl config use-context arn:aws:eks:${{ secrets.DEV_AWS_REGION }}:${{ secrets.DEV_AWS_ACCOUNT_ID }}:cluster/${{ secrets.DEV_AWS_EKS_CLUSTER }}
# kubectl rollout restart deploy/${{ secrets.DEV_AWS_EKS_DEPLOYMENT_API }} -n ${{ secrets.DEV_AWS_EKS_NAMESPACE }}
52 changes: 26 additions & 26 deletions .github/workflows/publish-ui.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
"REACT_APP_HCAPTCHA_SITE_KEY=${{ secrets.DEV_REACT_APP_HCAPTCHA_SITE_KEY }}"
"REACT_APP_CAPTCHA_SITE_KEY=${{ secrets.DEV_REACT_APP_CAPTCHA_SITE_KEY }}"
"REACT_APP_FAUCET_API_URL=${{ secrets.DEV_REACT_APP_FAUCET_API_URL}}"
- name: Gnosis Chain - Main branch / tags - Build and push Docker image
Expand All @@ -67,7 +67,7 @@ jobs:
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-gc
labels: ${{ steps.meta.outputs.labels }}
build-args: |
"REACT_APP_HCAPTCHA_SITE_KEY=${{ secrets.PROD_GC_REACT_APP_HCAPTCHA_SITE_KEY }}"
"REACT_APP_CAPTCHA_SITE_KEY=${{ secrets.PROD_GC_REACT_APP_CAPTCHA_SITE_KEY }}"
"REACT_APP_FAUCET_API_URL=${{ secrets.PROD_GC_REACT_APP_FAUCET_API_URL}}"
- name: Chiado Chain - Main branch / tags - Build and push Docker image
Expand All @@ -79,31 +79,31 @@ jobs:
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-chiado
labels: ${{ steps.meta.outputs.labels }}
build-args: |
"REACT_APP_HCAPTCHA_SITE_KEY=${{ secrets.PROD_CHIADO_REACT_APP_HCAPTCHA_SITE_KEY }}"
"REACT_APP_CAPTCHA_SITE_KEY=${{ secrets.PROD_CHIADO_REACT_APP_CAPTCHA_SITE_KEY }}"
"REACT_APP_FAUCET_API_URL=${{ secrets.PROD_CHIADO_REACT_APP_FAUCET_API_URL}}"
eks-deployment-restart:
# Run job on branch dev only
if: github.ref == 'refs/heads/dev'
runs-on: ubuntu-latest
needs: build-and-push-image
permissions:
id-token: write # Required for the OIDC, see https://github.com/aws-actions/configure-aws-credentials?tab=readme-ov-file#OIDC
contents: read
steps:
- name: configure aws credentials
uses: aws-actions/[email protected]
with:
audience: sts.amazonaws.com
role-to-assume: ${{ secrets.DEV_AWS_EKS_ROLE }}
role-session-name: GitHub_to_AWS_via_FederatedOIDC
aws-region: ${{ secrets.DEV_AWS_REGION }}
# eks-deployment-restart:
# # Run job on branch dev only
# if: github.ref == 'refs/heads/dev'
# runs-on: ubuntu-latest
# needs: build-and-push-image
# permissions:
# id-token: write # Required for the OIDC, see https://github.com/aws-actions/configure-aws-credentials?tab=readme-ov-file#OIDC
# contents: read
# steps:
# - name: configure aws credentials
# uses: aws-actions/[email protected]
# with:
# audience: sts.amazonaws.com
# role-to-assume: ${{ secrets.DEV_AWS_EKS_ROLE }}
# role-session-name: GitHub_to_AWS_via_FederatedOIDC
# aws-region: ${{ secrets.DEV_AWS_REGION }}

- name: Configure kubectl for EKS
run: aws eks update-kubeconfig --name ${{ secrets.DEV_AWS_EKS_CLUSTER }} --region ${{ secrets.DEV_AWS_REGION }}
# - name: Configure kubectl for EKS
# run: aws eks update-kubeconfig --name ${{ secrets.DEV_AWS_EKS_CLUSTER }} --region ${{ secrets.DEV_AWS_REGION }}

- name: Restart Bridge Explorer Deployment
if: github.ref == 'refs/heads/dev'
run: |
kubectl config use-context arn:aws:eks:${{ secrets.DEV_AWS_REGION }}:${{ secrets.DEV_AWS_ACCOUNT_ID }}:cluster/${{ secrets.DEV_AWS_EKS_CLUSTER }}
kubectl rollout restart deploy/${{ secrets.DEV_AWS_EKS_DEPLOYMENT_UI }} -n ${{ secrets.DEV_AWS_EKS_NAMESPACE }}
# - name: Restart Deployment
# if: github.ref == 'refs/heads/dev'
# run: |
# kubectl config use-context arn:aws:eks:${{ secrets.DEV_AWS_REGION }}:${{ secrets.DEV_AWS_ACCOUNT_ID }}:cluster/${{ secrets.DEV_AWS_EKS_CLUSTER }}
# kubectl rollout restart deploy/${{ secrets.DEV_AWS_EKS_DEPLOYMENT_UI }} -n ${{ secrets.DEV_AWS_EKS_NAMESPACE }}
6 changes: 4 additions & 2 deletions api/.env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
FAUCET_AMOUNT=0.1
FAUCET_AMOUNT=0.001
FAUCET_PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000000
FAUCET_RPC_URL=https://rpc.chiadochain.net
FAUCET_CHAIN_ID=10200
FAUCET_DATABASE_URI=sqlite://
CAPTCHA_VERIFY_ENDPOINT=https://api.hcaptcha.com/siteverify
CAPTCHA_SECRET_KEY=0x0000000000000000000000000000000000000000
CAPTCHA_SITE_KEY=xxxxx-xxxxx-xxxxx-xxxxx
CAPTCHA_SITE_KEY=xxxxx-xxxxx-xxxxx-xxxxx
CSRF_PRIVATE_KEY="!!CREATE_YOUR_RSA_PRIVATE_KEY!!"
CSRF_SECRET_SALT="test-salt"
4 changes: 4 additions & 0 deletions api/api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
claim_token)
from .services.database import AccessKey, Token, Transaction


apiv1 = Blueprint("version1", "version1")


Expand Down Expand Up @@ -116,6 +117,9 @@ def ask():

@apiv1.route("/cli/ask", methods=["POST"])
def cli_ask():
if not current_app.config['FAUCET_ENABLE_CLI_API']:
return jsonify(errors=['Endpoint disabled']), 403

access_key_id = request.headers.get('X-faucet-access-key-id', None)
secret_access_key = request.headers.get('X-faucet-secret-access-key', None)

Expand Down
1 change: 1 addition & 0 deletions api/api/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
from .token import Token
from .transaction import Web3Singleton, claim_native, claim_token
from .validator import AskEndpointValidator
from .captcha import CaptchaSingleton
72 changes: 59 additions & 13 deletions api/api/services/captcha.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,62 @@
logging.basicConfig(level=logging.INFO)


def captcha_verify(client_response, catpcha_api_url, secret_key, remote_ip, site_key):
request = requests.post(catpcha_api_url, data={
'response': client_response,
'secret': secret_key,
'remoteip': remote_ip,
'sitekey': site_key
})

logging.info('Captcha verify response: %s' % request.json())

if request.status_code != 200:
return False
return request.json()['success'] == True
class Captcha:
def __init__(self, provider):
self.provider = provider

def verify(self, client_response, catpcha_api_url, secret_key, remote_ip, site_key=None):
logging.info('Captcha: Remote IP %s' % remote_ip)

if self.provider == 'HCAPTCHA':
request = requests.post(catpcha_api_url, data={
'response': client_response,
'secret': secret_key,
'remoteip': remote_ip,
'sitekey': site_key
})

logging.info('Captcha: verify response %s' % request.json())

if request.status_code != 200:
return False
return request.json()['success'] is True
elif self.provider == 'CLOUDFLARE':
request = requests.post(catpcha_api_url, data={
'response': client_response,
'secret': secret_key,
'remoteip': remote_ip
})

logging.info('Captcha: verify response %s' % request.json())

if request.status_code != 200:
return False
return request.json()['success'] is True
else:
raise NotImplementedError


class CaptchaSingleton:
_instance = None

def __new__(cls, provider):
if not hasattr(cls, 'instance'):
cls.instance = Captcha(provider)
return cls.instance


# def captcha_verify(client_response, catpcha_api_url, secret_key, remote_ip, site_key):
# logging.info('Captcha: Remote IP %s' % remote_ip)
# request = requests.post(catpcha_api_url, data={
# 'response': client_response,
# 'secret': secret_key,
# 'remoteip': remote_ip,
# 'sitekey': site_key
# })

# logging.info('Captcha: verify response %s' % request.json())

# if request.status_code != 200:
# return False
# return request.json()['success'] == True
13 changes: 7 additions & 6 deletions api/api/services/csrf.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA

# Waiting period: the minimum time interval between UI asks for the CSFR token
# and the time it asks for funds.
CSRF_TIMESTAMP_MIN_SECONDS = 15
# Waiting period: the minimum time interval between the UI asks
# for the CSFR token to /api/v1/info and the time the UI can ask for funds.
# This check aims to block any bots that could be triggering actions through the UI.
CSRF_TIMESTAMP_MIN_SECONDS = 5


class CSRFTokenItem:
Expand Down Expand Up @@ -45,9 +46,9 @@ def validate_token(self, request_id, token, timestamp):
decrypted_text = cipher_rsa.decrypt(bytes.fromhex(token)).decode()
expected_text = '%s%s%f' % (request_id, self._salt, timestamp)
if decrypted_text == expected_text:
# Check that timestamp is OK, the diff between now() and creation time in seconds
# must be greater than min. waiting period.
# Waiting period: the minimum time interval between UI asks for the CSFR token and the time it asks for funds.
# Check that the timestamp is OK, the diff between now() and creation time in seconds
# must be greater than the minimum waiting period.
# Waiting period: the minimum time interval between UI asks for the CSFR token and the time the UI can ask for funds.
seconds_diff = (datetime.now()-datetime.fromtimestamp(timestamp)).total_seconds()
if seconds_diff > CSRF_TIMESTAMP_MIN_SECONDS:
return True
Expand Down
15 changes: 12 additions & 3 deletions api/api/services/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from flask import current_app, request
from web3 import Web3

from .captcha import captcha_verify
from .captcha import CaptchaSingleton
from .csrf import CSRF
from .database import AccessKeyConfig, BlockedUsers, Token, Transaction
from .rate_limit import Strategy
Expand Down Expand Up @@ -140,14 +140,23 @@ def data_validation(self):

def captcha_validation(self):
error_key = 'captcha'
# check hcatpcha
catpcha_verified = captcha_verify(

captcha = CaptchaSingleton(current_app.config['CAPTCHA_PROVIDER'])
catpcha_verified = captcha.verify(
self.request_data.get('captcha'),
current_app.config['CAPTCHA_VERIFY_ENDPOINT'],
current_app.config['CAPTCHA_SECRET_KEY'],
self.ip_address,
current_app.config['CAPTCHA_SITE_KEY']
)
# check hcatpcha
# catpcha_verified = captcha_verify(
# self.request_data.get('captcha'),
# current_app.config['CAPTCHA_VERIFY_ENDPOINT'],
# current_app.config['CAPTCHA_SECRET_KEY'],
# self.ip_address,
# current_app.config['CAPTCHA_SITE_KEY']
# )

if not catpcha_verified:
self.errors.append('%s: validation failed' % error_key)
Expand Down
6 changes: 5 additions & 1 deletion api/api/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,18 @@
FAUCET_ADDRESS: LocalAccount = Account.from_key(FAUCET_PRIVATE_KEY).address
FAUCET_RATE_LIMIT_STRATEGY = rate_limit_strategy
FAUCET_RATE_LIMIT_TIME_LIMIT_SECONDS = int(os.getenv('FAUCET_RATE_LIMIT_TIME_LIMIT_SECONDS', 86400)) # 86400 = 24h
FAUCET_ENABLE_CLI_API = os.getenv('FAUCET_ENABLE_CLI_API', "False") == "True"

SQLALCHEMY_DATABASE_URI = os.getenv('FAUCET_DATABASE_URI')

CORS_ALLOWED_ORIGINS = os.getenv('CORS_ALLOWED_ORIGINS', '*')

CAPTCHA_PROVIDER = os.getenv('CAPTCHA_PROVIDER', 'HCAPTCHA')
CAPTCHA_VERIFY_ENDPOINT = os.getenv('CAPTCHA_VERIFY_ENDPOINT')
CAPTCHA_SECRET_KEY = os.getenv('CAPTCHA_SECRET_KEY')
CAPTCHA_SITE_KEY = os.getenv('CAPTCHA_SITE_KEY')
CAPTCHA_SITE_KEY = os.getenv('CAPTCHA_SITE_KEY', None) # It's mandatory for HCAPTCHA
if CAPTCHA_PROVIDER == 'HCAPTCHA' and CAPTCHA_SITE_KEY is None:
raise ValueError('CAPTCHA_SITE_KEY is mandatory for HCAPTCHA')

CSRF_PRIVATE_KEY = os.getenv('CSRF_PRIVATE_KEY')
CSRF_SECRET_SALT = os.getenv('CSRF_SECRET_SALT')
11 changes: 8 additions & 3 deletions api/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from api.services import CSRF, Strategy
from api.services.database import Token, db
from flask.testing import FlaskClient

from api import create_app

Expand All @@ -25,13 +24,19 @@ def mock_claim_erc20(self, *args):
tx_hash = '0x1' + '%d' % self.erc20_tx_counter * 63
self.erc20_tx_counter += 1
return tx_hash

def mock_captcha_verify(self, *args):
class Test:
def verify(self, *args):
return True
return Test

def _mock(self, env_variables=None):
# Mock values
self.patchers = [
mock.patch('api.routes.claim_native', self.mock_claim_native),
mock.patch('api.routes.claim_token', self.mock_claim_erc20),
mock.patch('api.services.validator.captcha_verify', return_value=True),
mock.patch('api.services.validator.CaptchaSingleton', self.mock_captcha_verify),
mock.patch('api.api.print_info', return_value=None)
]
if env_variables:
Expand Down Expand Up @@ -133,4 +138,4 @@ def setUp(self):
self.csrf = CSRF.instance
# use same token for the whole test
# use a timestamp that would be actually validated by the CSRF class.
self.csrf_token = self.csrf.generate_token(timestamp=self.valid_csrf_timestamp)
self.csrf_token = self.csrf.generate_token(timestamp=self.valid_csrf_timestamp)
1 change: 1 addition & 0 deletions api/tests/temp_env_var.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
'FAUCET_RATE_LIMIT_TIME_LIMIT_SECONDS': '10',
'FAUCET_DATABASE_URI': 'sqlite://', # run in-memory
# 'FAUCET_DATABASE_URI': 'sqlite:///test.db',
'FAUCET_ENABLE_CLI_API': 'True',
'CAPTCHA_SECRET_KEY': CAPTCHA_TEST_SECRET_KEY,
'CSRF_PRIVATE_KEY': privatekey.export_key().decode(),
'CSRF_SECRET_SALT': 'testsalt'
Expand Down
2 changes: 1 addition & 1 deletion api/tests/test_api_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
ERC20_TOKEN_ADDRESS, FAUCET_CHAIN_ID)


class TestAPICli(BaseTest):
class TestAPICliEnabledEndpoints(BaseTest):
def test_ask_route_parameters(self):
access_key_id, secret_access_key = generate_access_key()
http_headers = {
Expand Down
2 changes: 1 addition & 1 deletion app/.env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
REACT_APP_HCAPTCHA_SITE_KEY=
REACT_APP_CAPTCHA_SITE_KEY=
REACT_APP_FAUCET_API_URL=http://localhost:8000/api/v1
4 changes: 2 additions & 2 deletions app/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ RUN yarn
COPY . .

ENV NODE_ENV production
ARG REACT_APP_HCAPTCHA_SITE_KEY
ARG REACT_APP_CAPTCHA_SITE_KEY
ARG REACT_APP_FAUCET_API_URL
ENV REACT_APP_HCAPTCHA_SITE_KEY ${REACT_APP_HCAPTCHA_SITE_KEY}
ENV REACT_APP_CAPTCHA_SITE_KEY ${REACT_APP_HAPTCHA_SITE_KEY}
ENV REACT_APP_FAUCET_API_URL ${REACT_APP_FAUCET_API_URL}
RUN yarn build

Expand Down
Loading

0 comments on commit 4744472

Please sign in to comment.