Flask extension for Danger (https://usedanger.com). Provides helpers to render the Danger widget on auth forms and to send server-side events.
Project description
Danger helps you to protect your Flask app from bad actors, validate user info, set a user's default country and timezone accurately, and detect repeat signups or credential sharing.
- Country and timezone resolution: Get the user's country and timezone, with built-in fallback so your signup is never broken.
- Validate user details: Validate and normalize user-provided inputs. Check email deliverability, disposable domains, phone number reachability, parse addresses, and much more. Simple
email_valid
andemail
type properties take the complication away. - Plug and play risk protection: Ready made smart rules help you to protect your app from risks such as remote geolocation, automated bots, anonymous users (VPN, Tor, etc.), throwaway email addresses, and more. Or create custom rules to screen users based on your own criteria.
- Act on repeat signups or credential sharing: Danger can link events to the same person, even when they attempt evasions such as incognito mode, clearing cookies, or using VPNs. Danger will show you the person behind the signup.
- Dashboard: For reviewing events, configuring rules, and one-click allowlisting of safe emails or IP addresses.
For more information check out the official website and our platform documentation.
How it works
Danger's implementation is similar to captcha services such as hCaptcha and reCAPTCHA, although the product solves different problems.
There is a client-side snippet that inserts a hidden form field into your signup or login form, then when your server processes the request it makes an API call with the user's details, alongside the contents of the hidden field that was generated by the client.
The result of this server-side call will provide a simple True
(allow) or False
(block), and give your app access to enriched data about the user and their device.
Quickstart
Install with pip
:
pip install flask-danger
You'll need a site_key
and secret_key
from Danger. You can get these by signing up for a free Danger account and create your first site.
Once you have a site_key
and secret_key
, configure Flask-Danger through the standard Flask configuration. These are the available options:
DANGER_SITE_KEY
: Required. The public site key that you'll find in the site settings in the Danger dashboard.DANGER_SECRET_KEY
: Required. The private key for the site. Always keep this a secret, only use it on the server.DANGER_TIMEOUT
: Optional. Sets the lookup timeout. Value is in seconds. Defaults to 8.DANGER_FALLBACK_ALLOW
: Optional. Whether to allow or block if the Danger platform can't be reached. Set to eitherTrue
(allow) orFalse
(block). Defaults toTrue
so that the check fails open.DANGER_FALLBACK_COUNTRY
: Optional. The ISO 3166-1 alpha-2 country code to use in the event Danger can't be reached. Defaults to"US"
.DANGER_FALLBACK_TIMEZONE
: Optional. The timezone in tz database format to use if Danger can't be reached. Defaults to'UTC'
.
The full set of config options and defaults are shown below:
app.config.update(
DANGER_SITE_KEY = '<your_site_key>',
DANGER_SECRET_KEY = '<your_secret_key>',
# Optional from here
DANGER_TIMEOUT = 8,
DANGER_FALLBACK_ALLOW = True,
DANGER_FALLBACK_COUNTRY = 'US',
DANGER_FALLBACK_TIMEZONE = 'UTC'
)
Now you're ready to import Flask-Danger:
from flask import Flask
from flask_danger import Danger
app = Flask(__name__)
danger = Danger(app)
# or with the factory pattern
danger = Danger()
danger.init_app(app)
In your signup or login form template, use this Jinja syntax to insert the client-side code into your form:
{{ danger }}
Place it anywhere within the form. For example, you might insert it like this:
...
<form id="form" method="post">
<label for="name">Name:</label>
<input type="text" id="name" name="name">
<label for="email">Email:</label>
<input type="text" id="email" name="email">
{{ danger }}
<input type="submit" value="Submit">
</form>
...
[!IMPORTANT]
It's important that the domain name you declared in your site settings matches the domain where the form will be served from.
[!NOTE]
If you don't serve your form from a Flask view, for example if you use a JavaScript app for your front-end, you can include the client-side widget manually and only use Flask-Danger for the server call.
Finally, in the Flask route that handles the form, use danger.event()
to get a result:
@route("/submit", methods=["POST"])
def submit():
# First perform basic validation on the inputs
# ...
# Now get the result from Danger
result = danger.event(
bundle=request.form["danger-bundle"],
name=request.form["name"],
email=request.form["email"]
)
if result.allow:
# Continue processing here
country = result.country
timezone = result.timezone
email = result.email
phone = result.phone
address = result.address
else:
# Block the signup
The country
and timezone
properties will either have the fetched values, or fallback, but can always be relied on to provide a value. email
, phone
, and address
are similar, they have either normalized values, or the input value if a check is unsuccessful.
If you need to check validity, you can use email_valid
, phone_valid
, and address_valid
:
if not result.email_valid:
# Email is not valid (or couldn't be determined)
if not result.phone_valid:
# Phone is not valid (or couldn't be determined)
if not result.address_valid
# Address is not valid (or couldn't be determined)
email_valid
, phone_valid
, and address_valid
all return True
(valid), False
(not valid), or None
(if a result can't be determined). So check equality carefully depending on what level of certainty you need. For example:
if result.email_valid is False:
# Email is definitely not valid
if result.address_valid:
# Email is definitely valid
if not result.email_valid:
# Email is either not valid, or couldn't check
if result.email_valid is None:
# Couldn't check email validity
You can also find the full Danger result in the data
property:
device = result.data.get("device", {})
browser = device.get("browser")
# Chrome
Read the docs to learn about what the result contains.
Reference
{{ danger }}
template variable
Create the HTML to include in the client side HTML form. Use inside a template using Jinja syntax {{ danger }}
. Add this anywhere within your form.
danger.event()
Pass in the user's data (email, phone, etc.) along with the bundle that is sent in the 'danger-bundle' field on your form.
Argument | Description |
---|---|
Person's email address | |
bundle | The 'bundle' that Danger added to the form in the hidden field 'danger-bundle' |
type | (Optional) Event type, either "new_user" (default) or "login" |
name | (Optional) Person's name |
phone | (Optional) Person's phone number |
address | (Optional) Person's address, a dict with one or more of the keys address1 , address2 , city , state , country , postal_code |
ip | (Optional) The remote IP address, i.e. that of the person's connection. Defaults to request.remote_addr . |
external_id | (Optional) An external identifier for this person, i.e. your app's database ID |
See the docs for more information.
Returns a result object with the following properties:
Property | Description |
---|---|
allow | Either True (allow) or False (block) |
outcome | Either the full outcome of the event ('allow' , 'allow_review' , 'block' , 'block_review' , 'review' ), or None on failure |
country | The ISO 3166-1 alpha-2 country code for the user (e.g. 'US' ) |
timezone | The tz database timezone for the user ('America/New_York' ) |
address_valid | Validity of the address. Either True (valid), False (not valid), or None (couldn't be validated). |
address | If address_valid is True , holds the parsed address. Otherwise, holds the input address. A dict with one or more of the keys address1 , address2 , city , state , country , postal_code . If no address provided, None . |
email_valid | Validity of the email address. Either True (valid), False (not valid), or None (couldn't be validated). |
If email_valid is True , holds the normalized email address. Otherwise, holds the input email address. |
|
phone_valid | Validity of the phone number. Either True (valid), False (not valid), or None (couldn't be validated). |
phone | If phone_valid is True , holds the E164 formatted phone number. Otherwise, holds the input phone number, or None if not provided. |
ip | The IP address of the user |
data | A dict containing the full Danger result |
Read the docs to find out how to work with the full Danger result.
Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
License
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Hashes for Flask_Danger-1.0.9-py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | ba058cadaefd6aec5cf53b69800d2d52db45d96bfc5e4ab448f5f04373ef8a40 |
|
MD5 | 07dd1bcd1af0667d6aac1b46cb1d5924 |
|
BLAKE2b-256 | 93975e2d37207fdc8f1574973ec43ea405af0363b8ddeae4ba7a9fbcce62a91a |