Skip to content

Commit

Permalink
feat: add Ory Action example for a vpn / geo check
Browse files Browse the repository at this point in the history
  • Loading branch information
kmherrmann committed Jul 19, 2023
1 parent 2face08 commit 1d70358
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 0 deletions.
5 changes: 5 additions & 0 deletions ory-action-vpncheck-py/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
venv
node_modules
package-lock.json
.gcloudignore

92 changes: 92 additions & 0 deletions ory-action-vpncheck-py/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Ory Action to check IP addresses against vpnapi.io

This is an example Action (webhook) to check client IP addresses against vpnapi.com and block requests
- coming from TOR clients
- coming from known VPNs
- coming from certain geographies (in this example: RU)

It's intended for use as a post-login Action on Ory Network and returns a message that can be parsed by Ory and displayed to the user.

The example implementation is written in Python with Flask for deployment on GCP Cloud Functions, and can easily be adapted for different scenarios.

## How to run it


### You'll need

Accounts:
* A Google Cloud project with Cloud Functions active (or an alternate way to deploy)
* A vpnapi.com account

For Development:
* python 3.9+
* flask
* requests
* google cloud logging


### Run locally

```
export BEARER_TOKEN=SOME_SECRET_API_KEY_FOR_YOUR_WEBHOOK;
export VPNAPIIO_API_KEY=YOUR_VPNAPI_KEY;
$ python3 main.py
```

### Send a sample request

```
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_WEBHOOK_API_SECRET" \
-d '{"ip_address": "8.8.8.8"}' \
http://localhost:5000/vpncheck -v
```
For blocked requests, you'll get `HTTP 400` responses with a payload like

```
{"messages":[{"messages":[{"text":"Request blocked: VPN"}]}]}
```

When successful, you'll get a `HTTP 200` response.

## Deploy to GCP

After setting up your GCP project (see, for example, [this guide](https://cloud.google.com/functions/docs/create-deploy-http-python)), you can deploy the Action as a cloud function:

```
gcloud functions deploy vpncheck --runtime python39 --trigger-http --allow-unauthenticated --set-env-vars BEARER_TOKEN=$SOME_SECRET_API_KEY_FOR_YOUR_WEBHOOK,VPNAPIIO_API_KEY=$VPNAPIIO_API_KEY --source=.
```
Note: You may need to create a `venv` for dependencies to load correctly.

You'll receive an endpoint address, which you can plug into the `curl` command above. On Google's Cloud Console, you can also see logs to verify it's working as intended.

## Integrating with Ory

To set up your Ory Network project to use the Action, go to Ory Console > Developers > Actions and create a new post-login webhook:

![Console Actions Screen](docs/images/actions-console-2.png)

Configure it for the Login flow, select "After" execution, and POST as the method, and enter your deployed URL.

Because we want the Action to cancel logins from disallowed IP addresses, we need to run in synchronous. Enabling `parse response` allows us to show a nice error message to users, rather than a system error.

![Console Actions Screen](docs/images/actions-console-1.png)

On the second screen, configure authentication with `Key`, select `Header` as the transport mode and put in your API key in the format `Bearer: $SOME_SECRET_API_KEY_FOR_YOUR_WEBHOOK` as the key value. You can of course use other ways to authenticate - this is just how the example implemented a basic check.

Our webhook expects a simple payload with just an `ip_address` field. We can get the IP address from the context with a simple JSONNET transformation:

```
function(ctx) {
ip_address: ctx.request_headers['True-Client-Ip'][0],
}
```

![Console Actions Screen](docs/images/actions-console-3.png)

## Seeing it in action

With everything set up, we can test the behavior using the Ory Account Experience. When logging in via VPN, the request now gets blocked and the message is shown to users!

![Account Experience displaying error](docs/images/ax-with-message.png)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
96 changes: 96 additions & 0 deletions ory-action-vpncheck-py/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from flask import Flask, request, jsonify
import requests

import os

# set up the Google Cloud Logging python client library
import google.cloud.logging
client = google.cloud.logging.Client()
client.setup_logging()
# use Python’s standard logging library to send logs to GCP
import logging

app = Flask(__name__)

# Define the bearer token for authentication
BEARER_TOKEN = os.environ.get("BEARER_TOKEN")
VPNAPIIO_API_KEY = os.environ.get("VPNAPIIO_API_KEY")

if not BEARER_TOKEN or not VPNAPIIO_API_KEY:
raise ValueError("BEARER_TOKEN or VPNAPIIO_API_KEY not set in environment variables.")

@app.route("/vpncheck", methods=["POST"])
def handle_vpncheck():
return vpncheck(request)

def vpncheck(request):
# Check for bearer token authentication
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
return jsonify({"error": "Unauthorized"}), 401

provided_token = auth_header.split("Bearer ")[1]
if provided_token != BEARER_TOKEN:
return jsonify({"error": "Unauthorized"}), 401

# Parse the JSON payload and extract the IP address
data = request.get_json()
logging.info(f"request: {data}")
ip_address = data.get("ip_address")
if not ip_address:
return error_response("Cannot determine Client IP address")

# Call vpnapi.io to check the IP address
# if the API fails, we permit by default
try:
vpn_result = query_vpn_io(ip_address)
except Exception as e:
return jsonify({"warning": "Unable to check VPN: ", "details": str(e)}), 200

# Check the response from vpnapi.io
if "error" in vpn_result and vpn_result["error"] == "Blocked":
return error_response("Request blocked: Blocked by vpn api")

if "security" in vpn_result:
security_info = vpn_result["security"]
if "vpn" in security_info and security_info["vpn"] == True:
logging.info(f"vpn block: {security_info['vpn']}")
return error_response("Request blocked: VPN")
if "tor" in security_info and security_info["tor"] == True:
logging.info(f"tor block: {security_info['tor']}")
return error_response("Request blocked: Tor")

if (
"location" in vpn_result
and "country_code" in vpn_result["location"]
and vpn_result["location"]["country_code"] == "RU"
):
logging.info(f"geoblock: {vpn_result['location']['country_code']}")
return error_response("Request blocked: Geolocation")

# Return the result as success or error details
return jsonify(vpn_result), 200


def error_response(msg):
return jsonify({"messages": [{ "messages": [{ "text": msg }] }]}), 400


def query_vpn_io(ip_address):
# Implement the logic to call vpnapi.io and retrieve the result
# You can use libraries like requests or httpx for making HTTP requests
# Return the response as a dictionary
# For example:

url = f"https://vpnapi.io/api/{ip_address}?key={VPNAPIIO_API_KEY}"
response = requests.get(url, timeout=1.5)
if response.status_code != 200:
raise Exception(f"vpnapi.io returned {response.status_code}")

result = response.json()

return result


if __name__ == "__main__":
app.run()
8 changes: 8 additions & 0 deletions ory-action-vpncheck-py/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"devDependencies": {
"@google-cloud/functions-framework": "^3.3.0"
},
"dependencies": {
"flask": "^0.2.10"
}
}
2 changes: 2 additions & 0 deletions ory-action-vpncheck-py/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
requests
google-cloud-logging

0 comments on commit 1d70358

Please sign in to comment.