Giter Site home page Giter Site logo

plaidweb / authl Goto Github PK

View Code? Open in Web Editor NEW
35.0 3.0 4.0 1.15 MB

A library for managing federated identity

License: MIT License

Makefile 0.94% Python 94.41% Shell 0.05% CSS 1.24% HTML 3.36%
identity indieauth federated-identity flask oauth oauth2-client backend hacktoberfest federated-authentication python

authl's Introduction

Authl

A Python library for managing federated identity

Documentation Status

About

Authl is intended to make it easy to add federated identity to Python-based web apps without requiring the creation of site-specific user accounts, but also without requiring the user to choose from a myriad of buttons or links to select any specific login provider.

All it should take is a single login form that asks for how the user wants to be identified.

Current state

The basic API works, and provides an easy drop-in set of endpoints for Flask.

Currently supported authentication mechanisms:

  • Directly authenticating against email using a magic link
  • Federated authentication against Fediverse providers (Mastodon, Pleroma)
  • Federated authentication against IndieAuth
  • Silo authentication against Twitter
  • Test/loopback authentication for development purposes

Planned functionality:

  • Pluggable OAuth mechanism to easily support additional identity providers such as:
    • OpenID Connect (Google et al)
    • Facebook
    • GitHub
  • OpenID 1.x (Wordpress, LiveJournal, Dreamwidth, etc.)
  • A more flexible configuration system

Rationale

Identity is hard, and there are so many competing standards which try to be the be-all end-all Single Solution. OAuth and OpenID Connect want lock-in to silos, IndieAuth wants every user to self-host their own identity site, and OpenID 1.x has fallen by the wayside. Meanwhile, users just want to be able to log in with the social media they're already using (siloed or not).

Any solution which requires all users to have a certain minimum level of technical ability is not a workable solution.

All of these solutions are prone to the so-called "NASCAR problem" where every supported login provider needs its own UI. But being able to experiment with a more unified UX might help to fix some of that.

Documentation

Full API documentation is hosted on readthedocs.

Usage

Basic usage is as follows:

  1. Create an Authl object with your configured handlers

    This can be done by instancing individual handlers yourself, or you can use authl.from_config

  2. Make endpoints for initiation and progress callbacks

    The initiation callback receives an identity string (email address/URL/etc.) from the user, queries Authl for the handler and its ID, and builds a callback URL for that handler to use. Typically you'll have a single callback endpoint that includes the handler's ID as part of the URL scheme.

    The callback endpoint needs to be able to receive a GET or POST request and use that to validate the returned data from the authorization handler.

    Your callback endpoint (and generated URL thereof) should also include whatever intended forwarding destination.

  3. Handle the authl.disposition object types accordingly

    A disposition is what should be done with the agent that initiated the endpoint call. Currently there are the following:

    • Redirect: return an HTTP redirection to forward it along to another URL
    • Notify: return a notification to the user that they must take another action (e.g. check their email)
    • Verified: indicates that the user has been verified; set a session cookie (or whatever) and forward them along to their intended destination
    • Error: An error occurred; return it to the user as appropriate

Flask usage

To make life easier with Flask, Authl provides an authl.flask.AuthlFlask wrapper. You can use it from a Flask app with something like the below:

import uuid
import logging

import flask
import authl.flask

logging.basicConfig(level=logging.INFO)
LOGGER = logging.getLogger(__name__)

app = flask.Flask('authl-test')

app.secret_key = str(uuid.uuid4())
authl = authl.flask.AuthlFlask(
    app,
    {
        'SMTP_HOST': 'localhost',
        'SMTP_PORT': 25,
        'EMAIL_FROM': '[email protected]',
        'EMAIL_SUBJECT': 'Login attempt for Authl test',
        'INDIELOGIN_CLIENT_ID': authl.flask.client_id,
        'TEST_ENABLED': True,
        'MASTODON_NAME': 'authl testing',
        'MASTODON_HOMEPAGE': 'https://github.com/PlaidWeb/Authl'
    },
    tester_path='/check_url'
)


@app.route('/')
@app.route('/some-page')
def index():
    """ Just displays a very basic login form """
    LOGGER.info("Session: %s", flask.session)
    LOGGER.info("Request path: %s", flask.request.path)

    if 'me' in flask.session:
        return 'Hello {me}. Want to <a href="{logout}">log out</a>?'.format(
            me=flask.session['me'], logout=flask.url_for(
                'logout', redir=flask.request.path[1:])
        )

    return 'You are not logged in. Want to <a href="{login}">log in</a>?'.format(
        login=flask.url_for('authl.login', redir=flask.request.path[1:]))


@app.route('/logout/')
@app.route('/logout/<path:redir>')
def logout(redir=''):
    """ Log out from the thing """
    LOGGER.info("Logging out")
    LOGGER.info("Redir: %s", redir)
    LOGGER.info("Request path: %s", flask.request.path)

    flask.session.clear()
    return flask.redirect('/' + redir)

This will configure the Flask app to allow IndieLogin, Mastodon, and email-based authentication (using the server's local sendmail), and use the default login endpoint of /login/. The index() endpoint handler always redirects logins and logouts back to the same page when you log in or log out (the [1:] is to trim off the initial / from the path). The logout handler simply clears the session and redirects back to the redirection path.

The above configuration uses Flask's default session lifetime of one month (this can be configured by setting app.permanent_session_lifetime to a timedelta object, e.g. app.permanent_session_lifetime = datetime.timedelta(hours=20)). Sessions will also implicitly expire whenever the application server is restarted, as app.secret_key is generated randomly at every startup.

Accessing the default stylesheet

If you would like to access authl.flask's default stylesheet, you can do it by passing the argument asset='css' to the login endpoint. For example, if you are using the default endpoint name of authl.login, you can use:

flask.url_for('authl.login', asset='css')

from Python, or e.g.

<link rel="stylesheet" href="{{url_for('authl.login', asset='css')}}">

from a Jinja template.

authl's People

Contributors

dependabot[bot] avatar fluffy-critter avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

authl's Issues

InvalidURL exception when giving non-Mastodon Webfinger address

Via @snarfed, attempting to log on via @[email protected] generates an exception:

Traceback (most recent call last):
  File "/Users/fluffy/.local/share/virtualenvs/Authl-bMFs1yld/lib/python3.7/site-packages/requests/models.py", line 379, in prepare_url
    scheme, auth, host, port, path, query, fragment = parse_url(url)
  File "/Users/fluffy/.local/share/virtualenvs/Authl-bMFs1yld/lib/python3.7/site-packages/urllib3/util/url.py", line 234, in parse_url
    raise LocationParseError(url)
urllib3.exceptions.LocationParseError: Failed to parse: //@[email protected]

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/fluffy/.local/share/virtualenvs/Authl-bMFs1yld/lib/python3.7/site-packages/flask/app.py", line 2463, in __call__
    return self.wsgi_app(environ, start_response)
  File "/Users/fluffy/.local/share/virtualenvs/Authl-bMFs1yld/lib/python3.7/site-packages/flask/app.py", line 2449, in wsgi_app
    response = self.handle_exception(e)
  File "/Users/fluffy/.local/share/virtualenvs/Authl-bMFs1yld/lib/python3.7/site-packages/flask/app.py", line 1866, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/Users/fluffy/.local/share/virtualenvs/Authl-bMFs1yld/lib/python3.7/site-packages/flask/_compat.py", line 39, in reraise
    raise value
  File "/Users/fluffy/.local/share/virtualenvs/Authl-bMFs1yld/lib/python3.7/site-packages/flask/app.py", line 2446, in wsgi_app
    response = self.full_dispatch_request()
  File "/Users/fluffy/.local/share/virtualenvs/Authl-bMFs1yld/lib/python3.7/site-packages/flask/app.py", line 1951, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/Users/fluffy/.local/share/virtualenvs/Authl-bMFs1yld/lib/python3.7/site-packages/flask/app.py", line 1820, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/Users/fluffy/.local/share/virtualenvs/Authl-bMFs1yld/lib/python3.7/site-packages/flask/_compat.py", line 39, in reraise
    raise value
  File "/Users/fluffy/.local/share/virtualenvs/Authl-bMFs1yld/lib/python3.7/site-packages/flask/app.py", line 1949, in full_dispatch_request
    rv = self.dispatch_request()
  File "/Users/fluffy/.local/share/virtualenvs/Authl-bMFs1yld/lib/python3.7/site-packages/flask/app.py", line 1935, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/Users/fluffy/projects/Authl/authl/flask.py", line 179, in login
    handler, hid, id_url = instance.get_handler_for_url(me_url)
  File "/Users/fluffy/projects/Authl/authl/__init__.py", line 37, in get_handler_for_url
    request = request_url(url)
  File "/Users/fluffy/projects/Authl/authl/__init__.py", line 63, in request_url
    return requests.get(url)
  File "/Users/fluffy/.local/share/virtualenvs/Authl-bMFs1yld/lib/python3.7/site-packages/requests/api.py", line 75, in get
    return request('get', url, params=params, **kwargs)
  File "/Users/fluffy/.local/share/virtualenvs/Authl-bMFs1yld/lib/python3.7/site-packages/requests/api.py", line 60, in request
    return session.request(method=method, url=url, **kwargs)
  File "/Users/fluffy/.local/share/virtualenvs/Authl-bMFs1yld/lib/python3.7/site-packages/requests/sessions.py", line 519, in request
    prep = self.prepare_request(req)
  File "/Users/fluffy/.local/share/virtualenvs/Authl-bMFs1yld/lib/python3.7/site-packages/requests/sessions.py", line 462, in prepare_request
    hooks=merge_hooks(request.hooks, self.hooks),
  File "/Users/fluffy/.local/share/virtualenvs/Authl-bMFs1yld/lib/python3.7/site-packages/requests/models.py", line 313, in prepare
    self.prepare_url(url, params)
  File "/Users/fluffy/.local/share/virtualenvs/Authl-bMFs1yld/lib/python3.7/site-packages/requests/models.py", line 381, in prepare_url
    raise InvalidURL(*e.args)
requests.exceptions.InvalidURL: Failed to parse: //@[email protected]

Issue appears to be that the "this isn't a valid URL" exception handler needs to also catch requests.exceptions.InvalidURL.

token_store should be an actual class/interface

Right now token_store is always just an itsdangerous signer, but really it should be an interface with set(value,redir) get and pop, and the default should be an implementation that is backed by the signer. get and pop should return a tuple of (value,redir). signer-backed impl uses utils.unpack_token as its implementation.

Partial NASCAR interface

Maybe for major silo providers that don’t need an identity there could be NASCAR buttons that go straight to the appropriate login endpoint. Like

class Twitter(Handler):
    @property
    def iconic(self):
        return<img src= https://whatever/twitter.png>”, “https://twitter.com

And then the flask template could be like

<form method=post>
{%for icon,me in auth.handlers%}
{%if icon %}<button type=submit name=me value=“{me}”>{{icon|safe}}</button>{%endif%}
{%endfor%}
</form>

Retain `me` value through error flow

If there's an error logging in, the disposition.Error should carry the failed identity URL so that the login form can present it as a default value.

Also, on the Flask handler, the error disposition could be passed through to the template, instead of using the message flashing mechanism which is a bit unwieldy.

Remove url parameter from Handler.check_callback

None of the handlers use that argument and I can't see any way that it would even be used anyway. I think it was a leftover from a time when the handlers were going to form their own callback URL path, rather than leaving that up to the fronting application/framework wrapper.

Sign in with Tumblr

Tumblr is still a thing, would be nice to allow logins with that.

TBD if it's actually reasonable to do though, because of the capricious nature of their profile URLs. According to the API docs, there is a permanent UID (which they incorrectly call a uuid) so the Twitter userID-in-fragment hack (e.g. username.tumblr.com#uid) could work.

There is an official client which should make this a lot easier, and looks very similar in usage to the Twitter client.

Standardize user profile fields

Come up with a set of profile fields to make use of and map into a common format for display/recording purposes

Possible things to track:

  • display name
  • email address (if permitted by service)
  • profile URL (not necessarily the same as the identity URL, e.g. Twitter or Facebook)
  • homepage (not necessarily the same as the profile URL)
  • pronouns
  • arbitrary key/val pairs as provided by some services (e.g. Mastodon)
  • birthday?

On the Flask wrapper, only the identity URL should be stored in the session by default; everything else should be provided via the on_verified callback so that it's up to the app to store it reasonably (for example, in a UserProfile table or something).

Investigate using HTTP authentication instead of session cookies

The various protocols such as IndieAuth and AutoAuth intend for authorization bearing to be handled by an auth header, rather than a cookie jar. In the Flask configuration especially this seems like a thing that should be abided by.

The basic protocol is described in RFC 7235 and the IndieAuth/AutoAuth usage is discussed in the AutoAuth spec.

This is probably something that needs to be handled on a per-application basis but implementing it in the tests and documenting it in a central place will be helpful for others.

I am also probably severely misinterpreting something, because as far as I can tell the IndieAuth flow doesn't say anything about how authentication is stored between the site and the end user, and all of the IndieAuth-enabled things I'm finding seem to use a cookie jar for storing the actual session. So I'm not clear on how the AutoAuth flow is supposed to work in the first place.

Twitter auth needs external or stateless storage

Provide a token storage interface in the config API (dict-like interface). Default to in-process LRUDict but provide some sample implementations:

  • redis
  • memcached
  • flask session
  • ...?

While we're at it, the existing token_store thing should probably be renamed to make it less confusing (especially since it isn't a store anymore)

Either make it stateless, or allow persistent storage

On cloud-based deployments like Heroku, and on load balancers (I repeat myself), there's not currently any way for state to be preserved across process boundaries; all of the state is stored in an ExpiringDict.

It would be better to go back to storing the state value in an itsdangerous-signed token, which was the original design and is generally safe (modulo the concern of replay attacks, which is mitigated by making the signature expire anyway).

Alternately, allow for a persistent backing store for the state tokens, but that's got a lot of other implications to worry about and should only be a last resort.

OpenID 1.x handler

In case we really want to support Ubuntu Launchpad and Livejournal. And Dreamwidth except they're talking about adding IndieAuth support anyway.

Login form loses redirection path on error

If an error occurs during authentication, the login redirection path is lost.

Reproduction: in test.py, attempt to log on from localhost:5000/some-path via Mastodon/Twitter/etc., and deny the login. Result: a successful login goes to localhost:5000/ instead.

Email callback failed

User error report from @SpudRat3

error 500 Exception occurred
request time 2020-02-27 14:18:57-08:00
url https://beesbuzz.biz/_cb/e?t=WyJyYXlpaWlAd29ya3Nob3AzZC5jb20iLCIvNTMzNyJd.XlhAGQ.MaBvCkXF2EBLhIGP97C4_Gr7Xt8
path /_cb/e
full_path /_cb/e?t=WyJyYXlpaWlAd29ya3Nob3AzZC5jb20iLCIvNTMzNyJd.XlhAGQ.MaBvCkXF2EBLhIGP97C4_Gr7Xt8
endpoint authl.callback
url_rule /_cb/<hid>
Exception information KeyError: '_authl.prefill'

User reported second attempt worked

See about making the JS partially work on older browsers

If the JS code can be structured such that there's no top-level async functions, and old browsers (e.g. IE11) simply elide the attempt to check the remote URL, older browsers could at least have the enable/disable flag working on the button. Which is a slight, if trivial, improvement.

Retrieval of IndieWeb endpoints

When the IndieAuth profile is fetched it should also stash the various other indieweb endpoint links somewhere, possibly in profile['links'] or something.

Clarify OAuth identities

Technically an OAuth-based identity does not need a full user URL, it only requires providing the common domain name of the service (E.g. Mastodon.social, twitter.com, etc). It might be nice to signal that in some way, or to even make the standard UX just forward to the authority if the user clicks on the appropriate link for the service in the list (like linking to /login?me=http://twitter.com or whatever).

But it might be nice to just pretend that the full URL is important for the sake of consistency anyway. (But it might look like a bug if someone has two identities on a remote service and they want to specify one but the handler gives them another...)

Sign In with Apple

Should be straightforward to implement, and very useful for mobile app services.

IndieAuth handler doesn't verify the `me` in the verification response

The IndieAuth handler erroneously accepts whatever me value comes in from the verification response. This is a major security concern, as anyone could set up a rogue authorization_endpoint which provides any arbitrary me value, allowing someone to log in as anyone else.

This is a high-priority vulnerability in versions of Authl <= 0.3.0.

Remove pipenv dependency from development

Instead of using a Pipfile/Pipfile.lock for local dev environment it'd be better for the Makefile to set up a local virtualenv for running the tools from, or something.

In the long run maybe we should switch to poetry instead of setuptools but that has its own set of issues that seem tangential to what we care about with Authl.

Flask: throttle initiation requests per me/ip

To prevent a site from being used in an amplification attack or part of an email bomb or whatever, the login endpoint should throttle requests made based on both the me parameter and on the originating IP address (as determined by eg flask.request.headers.get(“x-forwarded-for”,flask.request.remote_addr) or whatever the correct invocation is). The timeout should probably be stored in an expiringdict with the next timeout computed by adding the delta between the current timeout and the current time multiplied by some constant, with the initial timeout and constant being configurable.

Care should be taken to not accidentally make this a vector for maliciously locking people out, though.

Differentiate between profile URL redirections

Per indieweb/indieauth#36, IndieAuth profiles (and probably other profiles) need a better way to handle URL redirections than simply accepting the final URL. Currently we just chase all redirections and use the final response URL as the canonical URL, but instead we should keep track of redirections and use the last URL that came before the first temporary redirect.

This is the edge-casiest of edge cases and only really applies to IndieAuth/IndieLogin (which are the only handlers which even retrieve the profile page) but it would still be helpful to do the redirection chase. See below for an example of what (again, very rare) edge case would fail.

For that matter, if the canonical profile URL is different, the URL-based detection logic could be rerun so that e.g. https://beesbuzz.biz/twitter will be treated as https://twitter.com/fluffy and not dropped as an unhandled auth type.

IndieAuth test cases

Provided profile URL Redirection chain me URL Pass/fail
http://alice.example.com permanent -> https://alice.example.com https://alice.example.com pass
http://alice.example.com temporary -> https://alice.example.com https://alice.example.com fail? (different scheme)
http://alice.example.com temporary -> https://alice.example.com http://alice.example.com pass
https://alice.example.com permanent -> https://example.com/~alice https://alice.example.com fail (different domain)
https://alice.example.com temporary -> https://example.com/~alice https://alice.example.com pass
https://alice.example.com permanent -> https://example.com/~alice https://example.com/~alice/ pass

Flask: Improve the default login template

Provide a better login template for the default login page, possibly with some customization options (such as being able to provide a stylesheet or additional markup or whatever).

Ideally it will include support for the friendly callback discovery/listing functionality that was mocked up in the UI wireframes (and provided by #3).

IndieAuth direct handler

While the IndieLogin handler is useful for things like RelMeAuth and the like, it'd also be nice to support IndieAuth directly, since that buys things like AutoAuth and also means that sites won't need to each register with IndieLogin.com (or another broker) to support IndieAuth.

Make callback URLs protocol-stable

Right now the callback URL just uses its position in the handler list, which isn't compatible with some OAuth providers (notably Twitter) which require registering a stable callback URL. It also means that if configuration changes while someone is logging in, they will have an odd result occur.

It would be better if the handler gets a stable name that's associated with it.

Flask: just redirect if the user is already logged in

If the user is already logged in with the same identity as the me argument, just redirect to the redir target. That way someone can set a bookmark of, say, https://example.com/login/blog?me=http://fred.example.com and just keep the same login if they're already identified, or be redirected to their own login page if not.

fediverse: caches error responses indefinitely

If an instance is down the lru_cache decorators around the instance check and client builder will prevent it from ever working again, until the app restarts or whatever. It would be good to use a cache with a timeout instead, and also to raise instead of return None as appropriate to avoid transitory results from being cached in the first place.

Short-term fix could just be to remove the caching because really who cares about a few extra requests.

Mastodon handler is way too permissive

Right now the Mastodon handler just trusts whatever profile uri the server gives back. There should be a validation step that ensures that the uri is at least one under the instance’s control, to prevent a trivial attack.

Flask: provide callback list and match endpoint

Add a callback-related endpoint _cb/check/<url>, which checks the URL and returns the pertinent info from Authl.get_handler_for_url(url), providing the name and URL schema in a JSON blob like:

{
    'name': 'Email',
    'schema': '%',
    'placeholder': '[email protected]'
}

or the like.

If no matching endpoint exists, it should return null or undefined or false or something.

Mastodon handler

collected notes for Mastodon handler:

we can use Mastodon.py for this. basic process appears to be:

client_id, client_secret = Mastodon.create_app(
    'authl-beesbuzz.biz',
    scopes=('read'),
    api_base_url='https://instance.name',
    redirect_uris=[callback_uri])
client = Mastodon(
    client_id=client_id, client_secret=client_secret,
    api_base_url='https://instance.name')
return disposition.Redirect(
    client.auth_request_url(redirect_uris=callback_uri,scopes=('read')))

and then the callback handler will somehow get a client token that can then be used to somehow look up the user ID.

Abstract token storage

There should be a central store for request tokens and other things (Mastodon client tokens, email request throttles, etc), which can then be offloaded into a shared/persisted data store (memcached/redis/Postgres/whatever) to allow for load balancing and the like.

Improve email safety

The EmailHandler should keep track of the addresses it's sent a link to in the last few minutes, and not allow spamming an address. This is both to prevent Authl from being used as an intermediary for flooding someone else's email address, and to avoid issues with a /_login/[email protected] address living in someone's history causing the user to get spammed (due to browser prefetch or whatever)

Allow testing multiple WebFinger URLs

If a WebFinger profile provides multiple profile links, iterate through all of them until one href provides a supported identity type.

This will require some rearchitecting; currently webfinger detection is a quick-and-dirty hack.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.