Giter VIP home page Giter VIP logo

datasette-indieauth's Introduction

datasette-indieauth

PyPI Changelog codecov Tests License

Datasette authentication using IndieAuth.

Demo

You can try out the latest version of this plugin at datasette-indieauth-demo.datasette.io

Installation

Install this plugin in the same environment as Datasette.

$ datasette install datasette-indieauth

Usage

Ensure you have a website with a domain that supports IndieAuth or RelMeAuth. The easiest way to do that is to add the following HTML to your homepage, linking to your personal GitHub profile:

<link href="https://github.com/simonw" rel="me">
<link rel="authorization_endpoint" href="https://indieauth.com/auth">

Your GitHub profile needs to link back to your website, to prove that your GitHub account should be a valid identifier for that page.

Now visit /-/indieauth on your Datasette instance to begin the sign-in progress.

Actor

When a user signs in using IndieAuth they will be recieve a signed ds_actor cookie identifying them as an actor that looks like this:

{
    "me": "https://simonwillison.net/",
    "display": "simonwillison.net"
}

If the IndieAuth server returned additional "profile" fields those will be merged into the actor. You can visit /-/actor on your Datasette instance to see the full actor you are currently signed in as.

Restricting access with the restrict_access plugin configuration

You can use Datasette's permissions system to control permissions of authenticated users - by default, an authenticated user will be able to perform the same actions as an unauthenticated user.

As a shortcut if you want to lock down access to your instance entirely to just specific users, you can use the restrict_access plugin configuration option like this:

{
    "plugins": {
        "datasette-indieauth": {
            "restrict_access": "https://simonwillison.net/"
        }
    }
}

This can be a string or a list of user identifiers. It can also be a space separated list, which means you can use it with the datasette publish --plugin-secret configuration option to set permissions as part of a deployment, like this:

datasette publish vercel mydb.db --project my-secret-db \
    --install datasette-indieauth \
    --plugin-secret datasette-indieauth restrict_access https://simonwillison.net/

Development

To set up this plugin locally, first checkout the code. Then create a new virtual environment:

cd datasette-indieauth
python3 -mvenv venv
source venv/bin/activate

Or if you are using pipenv:

pipenv shell

Now install the dependencies and tests:

pip install -e '.[test]'

To run the tests:

pytest

datasette-indieauth's People

Contributors

simonw avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

datasette-indieauth's Issues

More robust verification of the "me" value

I think I fixed the security hole with this change, but there are further security recommendations that I should follow, specifically around following redirects:

https://indieauth.spec.indieweb.org/#differing-user-profile-urls

Upon validation, clients MUST check the me value from the profile URL response or access token response, and take the following validation steps:

  • It MUST follow any permanent redirections from this URL to discover the canonical profile URL, in the same manner as initial profile URL discovery.
  • It MUST verify that the canonical profile URL is on the same domain as the initially-entered profile URL.
  • It MUST verify that the canonical profile URL declares the same authorization_endpoint as the initially-entered profile URL.

canonicalize_url()

https://indieauth.spec.indieweb.org/#url-canonicalization

Since IndieAuth uses https/http URLs which fall under what [URL] calls "Special URLs", a string with no path component is not a valid [URL]. As such, if a URL with no path component is ever encountered, it MUST be treated as if it had the path /. For example, if a user enters https://example.com as their profile URL, the client MUST transform it to https://example.com/ when using it and comparing it.

Since domain names are case insensitive, the hostname component of the URL MUST be compared case insensitively. Implementations SHOULD convert the hostname to lowercase when storing and using URLs.

For ease of use, clients MAY allow users to enter just a hostname part of the URL, in which case the client MUST turn that into a valid URL before beginning the IndieAuth flow, by prepending either an http or https scheme and appending the path /. For example, if the user enters example.com, the client transforms it into http://example.com/ before beginning discovery.

Refs #2

Live demo

I'll deploy a live demo to Vercel.

json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

Got this on https://datasette-indieauth-demo.datasette.io/-/indieauth/done?code=N4t1BH3NL_mJTE3PKpZJ4A%3D%3D.-dL4zf8DNpRa0OQHvtdIYImxaPRGwQGjs8l-7thgfeXnDyOYCm0ms03kD6FZzCSCSiGhCGrVNRyiP_m3s0IWbLdTHHSGiYG0ivRwtnnN4NccPOikjKs8dC1gIukZQDtMETWo293_t_V3plLPFh0borO0eOMI64NLqBt9zc9fHmtu6suStEaiTDeom0OF0vtw1JjdK8tptEHc0EDirvQzLQBJX2oPP0D730GYtHnMtrHiEyEU2WzX0ikl-Bae9XYAjUeQTyyO227UJVro19Db6RCJTpyQxizkvP5y4kd9OBxiHFmObqNuuFv6-__C9BGJr3Ie5UC4OstQU0tyS9Mbz-OZP_4-jkrjQRcA2R1hYFVh7s7EPHIrLdzF0da2J-rVOeswgL26PPGWTNll6n6mBQ-WVU7wS7pJP2rC_70mo9P8jXYcay4AMvYoYlthifxt8feSAhcOfazR6TuzhxDRl1hJtOyQXS3bVhfBV7c45kmC5Y98o6_6MmDHPwEV4sTYgeHySBxdNR7vbn2hmuBGh5Z0am8_vKpl4jSGHcS6LCBZqP86R-m8mx-yYEL70f3xPa2_y0AlKNb-cdwIsN6Yud59tiZHd1dGSg9qcUjaF4ttpYcNVO2P7BxFXgr2NYzEaVG19KlEKfDe5Mmj-bPU2nKCH6osibW3L6n30Q8zdRkiuhB2FBwgtBrhigee7c2Zvqidp-HUeuKZlPZjbegVOBSOlJqZ1TLBDhZr._riHxeCe4GkLAaS9&me=https%3A%2F%2Fsimon-indieauth.vercel.app%2F&state=eyJhIjoiaHR0cHM6Ly9pbmRpZWF1dGguY29tL2F1dGgifQ.i88YqkPoMc3yX2sBxKKMANAnNW4

Full traceback:

Traceback (most recent call last):
File "/var/task/datasette/app.py", line 1033, in route_path
  response = await view(request, send)
File "/var/task/datasette/app.py", line 1210, in async_view_fn
  datasette=datasette,
File "/var/task/datasette/utils/__init__.py", line 915, in async_call_with_supported_arguments
  return await fn(*call_with)
File "/var/task/datasette_indieauth/__init__.py", line 130, in indieauth_done
  info = response.json()
File "/var/task/httpx/_models.py", line 1113, in json
  return jsonlib.loads(self.content.decode(encoding), **kwargs)
File "/var/lang/lib/python3.6/json/__init__.py", line 354, in loads
  return _default_decoder.decode(s)
File "/var/lang/lib/python3.6/json/decoder.py", line 339, in decode
  obj, end = self.raw_decode(s, idx=_w(s, 0).end())
File "/var/lang/lib/python3.6/json/decoder.py", line 357, in raw_decode
  raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

async def discover_endpoints(url)

This is the function which, given a URL, discovers the authorization_endpoint and optionally token_endpoint for it.

Clients MUST start by making a GET or HEAD request to [Fetch] the user's profile URL to discover the necessary values. Clients MUST follow HTTP redirects (up to a self-imposed limit). If one or more successive HTTP permanent redirects (HTTP 301 or 308) are encountered starting with the very first request, the client MUST use the final permanent redirection's target URL as the canonical profile URL. If any other redirection (such as HTTP 302, 303, or 307) is encountered, it must still be resolved for endpoint discovery, but this redirection does not modify the canonical profile URL, nor do subsequent permanent redirects.

Clients MUST check for an HTTP Link header [RFC8288] with the appropriate rel value. If the content type of the document is HTML, then the client MUST check for an HTML <link> element with the appropriate rel value. If more than one of these is present, the first HTTP Link header takes precedence, followed by the first <link> element in document order.

The endpoints discovered MAY be relative URLs, in which case the client MUST resolve them relative to the current document URL according to [URL].

I'm just going to implement GET - I won't bother with HEAD.

Refs #2

Remove IndieAuth.com fallback

These TODOs:

if not authorization_endpoint:
# Redirect to IndieAuth.com as a fallback
# TODO: Only do this if rel=me detected
# TODO: make this a configurable preference
indieauth_url = "https://indieauth.com/auth?" + urllib.parse.urlencode(
{
"me": me,
"client_id": urls.client_id,
"redirect_uri": urls.indie_auth_com_redirect_uri,
}
)
return Response.redirect(indieauth_url)

verify_client_identifier()

https://indieauth.spec.indieweb.org/#client-identifier

Clients are identified by a [URL]. Client identifier URLs MUST have either an https or http scheme, MUST contain a path component, MUST NOT contain single-dot or double-dot path segments, MAY contain a query string component, MUST NOT contain a fragment component, MUST NOT contain a username or password component, and MAY contain a port. Additionally, hostnames MUST be domain names or a loopback interface and MUST NOT be IPv4 or IPv6 addresses except for IPv4 127.0.0.1 or IPv6 [::1].

Refs #2

Hit 100% code coverage

Currently at 96%:

Module statements missing excluded coverage
Total 226 8 0 96%
datasette_indieauth/init.py 109 8 0 93%
datasette_indieauth/utils.py 117 0 0 100%

Implement IndieAuth directly, don't just pass through to IndieAuth.com

This plugin currently depends entirely on IndieAuth.com

This is necessary to support RelMeAuth, but it would be better if this plugin first attempted to fetch the page itself and scanned for <link rel="authorization_endpoint" href="..."> and attempted to authenticate directly.

Then fall back to IndieAuth.com only if that is missing but there's at least one rel="me" in the page header.

Correctly handle redirects when seeking authorization_endpoint

https://indieauth.spec.indieweb.org/#discovery-by-clients

Clients need to discover a few pieces of information when a user signs in. The client needs to discover the user's authorization_endpoint, and optionally token_endpoint if the client needs an access token. When using the Authorization flow to obtain an access token for use at a [Micropub] endpoint, the client will also discover the micropub endpoint.

Clients MUST start by making a GET or HEAD request to [Fetch] the user's profile URL to discover the necessary values. Clients MUST follow HTTP redirects (up to a self-imposed limit). If one or more successive HTTP permanent redirects (HTTP 301 or 308) are encountered starting with the very first request, the client MUST use the final permanent redirection's target URL as the canonical profile URL. If any other redirection (such as HTTP 302, 303, or 307) is encountered, it must still be resolved for endpoint discovery, but this redirection does not modify the canonical profile URL, nor do subsequent permanent redirects.

Clients MUST check for an HTTP Link header [RFC8288] with the appropriate rel value. If the content type of the document is HTML, then the client MUST check for an HTML <link> element with the appropriate rel value. If more than one of these is present, the first HTTP Link header takes precedence, followed by the first <link> element in document order.

The endpoints discovered MAY be relative URLs, in which case the client MUST resolve them relative to the current document URL according to [URL].

Needed by #22

Implement client_id page with h-app microformat

Relates to #2. From the spec: https://www.w3.org/TR/indieauth/#application-information

Clients SHOULD have a web page at their client_id URL with basic information about the application, at least the application's name and icon. This page serves as a good landing page for human visitors, but can also serve as the place to include machine-readable information about the application. The HTML on the client_id URL SHOULD be marked up with [h-app] [Microformats] to indicate the name and icon of the application. Authorization servers SHOULD support parsing the [h-app] Microformat from the client_id, and if there is an [h-app] with a url property matching the client_id URL, then it should use the name and icon and display them on the authorization prompt.

Smarter error handling for indieauth/done

[GET] /-/indieauth/done?code=
19:18:32:08
Traceback (most recent call last): File "/var/task/datasette/app.py", line 1033, in route_path response = await view(request, send) File "/var/task/datasette/app.py", line 1210, in async_view_fn datasette=datasette, File "/var/task/datasette/utils/init.py", line 915, in async_call_with_supported_arguments return await fn(*call_with) File "/var/task/datasette_indieauth/init.py", line 95, in indieauth_done state_bits = datasette.unsign(state, DATASETTE_INDIEAUTH_STATE) File "/var/task/datasette/app.py", line 339, in unsign return URLSafeSerializer(self._secret, namespace).loads(signed) File "/var/task/itsdangerous/serializer.py", line 186, in loads return self.load_payload(signer.unsign(s)) File "/var/task/itsdangerous/signer.py", line 164, in unsign if sep not in signed_value:TypeError: argument of type 'NoneType' is not iterable

verify_profile_url()

https://indieauth.spec.indieweb.org/#user-profile-url

Users are identified by a [URL]. Profile URLs MUST have either an https or http scheme, MUST contain a path component (/ is a valid path), MUST NOT contain single-dot or double-dot path segments, MAY contain a query string component, MUST NOT contain a fragment component, MUST NOT contain a username or password component, and MUST NOT contain a port. Additionally, hostnames MUST be domain names and MUST NOT be ipv4 or ipv6 addresses.

Refs #2

Support username@host identifiers

See https://twitter.com/aaronpk/status/1329278243711533056

so it turns out this works. I can type in “[email protected]” in an indieauth prompt and it works. because that is a URL.

I’ll admit it’s a bit of a “hack”. The trick is “[email protected]” is a URL because if you assume the http scheme then you get http://[email protected] which is a username but no password with HTTP basic auth. The server can switch what it returns based on that username.

As a client developer you have to: 1) follow the spec by assuming “http” if no scheme is entered, and 2) allow the user-entered URL to contain a username component.

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.