Skip to main content

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

Flask-Danger: The official Flask extension for Danger

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 and email 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 either True (allow) or False (block). Defaults to True 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
email 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).
email 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

MIT

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

Flask-Danger-1.0.9.tar.gz (8.6 kB view hashes)

Uploaded Source

Built Distribution

Flask_Danger-1.0.9-py3-none-any.whl (9.3 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page