-
-
Notifications
You must be signed in to change notification settings - Fork 68
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add Ory Action example for a vpn / geo check
- Loading branch information
1 parent
2face08
commit 1d70358
Showing
9 changed files
with
203 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
venv | ||
node_modules | ||
package-lock.json | ||
.gcloudignore | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
requests | ||
google-cloud-logging |