natbat / pillarpointstewards

Website for pillarpointstewards.com
Apache License 2.0
7 stars 0 forks source link

Get Auth0 login working for Codespaces #78

Closed simonw closed 1 year ago

simonw commented 1 year ago

One thing I couldn't figure out: how to get Auth0 login working. That requires that your redirect URI is configured on the Auth0 website, but with GitHub Codespaces you get a new URL every time you launch a development environment.

May have to solve that with a special mechanism on production where hits to https://www.pillarpointstewards.com/auth0-dev-login/?host=... are redirected to the right place - but need to make sure that scheme is secure.

Maybe the GitHub Codespaces developer environment uses a signed string such that the production environment knows that the redirect is safe to follow.

Originally posted by @simonw in https://github.com/natbat/pillarpointstewards/issues/73#issuecomment-1627842631

simonw commented 1 year ago

I'm going to do this with a shared secret between the development environments on Codespaces and the production https://www.pillarpointstewards.com/ website.

Then I'll construct a special auth0 callback URL like this:

/auth0-callback/aGVsbG8gdGhlcmU6VTdhZk03OWxDNVJzQXNiTzYtaWhZZWdiVHBSemxySE5ZcHhneW1XUE1Maw==

Relevant code:

https://github.com/natbat/pillarpointstewards/blob/1abdbe15deaca234100b48062e7a098ffd8d9322/pillarpointstewards/auth0_login/views.py#L20-L21

That weird string in the path is a URL-safe base64 encoded signed string with the URL that everything should be forwarded BACK to once the authentication has completed.

Console exploration:

>>> from django.core import signing
>>> import base64
>>> msg = signing.Signer(key='shared').sign("hello there")
>>> msg
'hello there:U7afM79lC5RsAsbO6-ihYegbTpRzlrHNYpxgymWPMLk'
>>> base64.urlsafe_b64encode(msg.encode()).decode()
'aGVsbG8gdGhlcmU6VTdhZk03OWxDNVJzQXNiTzYtaWhZZWdiVHBSemxySE5ZcHhneW1XUE1Maw=='
simonw commented 1 year ago

https://docs.github.com/en/codespaces/managing-your-codespaces/managing-encrypted-secrets-for-your-codespaces shows how to set secrets for Codespaces.

simonw commented 1 year ago

Started work on a test:

def test_auth0_login_with_forward_url(client, settings):
    forward_url = "https://www.pillarpointstewards.com/auth0-callback/"
    settings.AUTH0_FORWARD_URL = forward_url
    settings.AUTH0_FORWARD_SECRET = "my-secret"

    response = client.get("/login/")
    assert response.status_code == 302
    location = response.headers["location"]
    # Should redirect to auth0 with ?redirect_uri=AUTH0_FORWARD_URL with a base64 extension
    assert location.startswith(
        f"https://{settings.AUTH0_DOMAIN}/authorize?"
    )
    # Use parse_qs to extract the redirect_uri
    qs = parse_qs(urllib.parse.urlparse(location).query)
    redirect_uri = qs["redirect_uri"][0]

    assert redirect_uri.startswith(forward_url)
    base64bit = redirect_uri[len(forward_url) :]
    assert base64bit
    # Decode that as URL safe base64
    decoded = base64.urlsafe_b64decode(base64bit.encode()).decode()

    # That should be a signed message - unsign it with the secret
    signer = signing.Signer(key="my-secret")
    unsigned = signer.unsign(decoded)

    # And that should contain our original redirect URL
    assert unsigned == "http://testserver/auth0-callback/"

    del settings.AUTH0_FORWARD_URL
    del settings.AUTH0_FORWARD_SECRET
simonw commented 1 year ago

The following settings should now implement half of what's needed:

AUTH0_FORWARD_URL = "https://www.pillarpointstewards.com/auth0-callback/"
AUTH0_FORWARD_SECRET = "some-special-secret"

These settings, implemented in a test environment, will cause the login link to direct the user to Auth0 such that it redirects BACK to https://www.pillarpointstewards.com/auth0-callback/some-base64-signed-value

The next step is to teach /auth0-callback/... to decode that some-base64-signed-value, check its signature against the AUTH0_FORWARD_SECRET setting and, if it verifies, redirect the user to that location.

simonw commented 1 year ago

Looks like /auth0-callback/... is rejected as an invalid redirect_uri by Auth0. But this works:

redirect_uri = forward_url + '?forward=' + signed_base64(
    redirect_uri, settings.AUTH0_FORWARD_SECRET
)

That resulted in a redirect to:

https://pillarpointstewards.us.auth0.com/authorize
  ?response_type=code
  &client_id=DLXBMP...
  &redirect_uri=https%3A%2F%2Fwww.pillarpointstewards.com%2Fauth0-callback%2F%3Fforward%3DaHR0cHM6Ly9zdXBlci1kdXBlci1wYXJha2VldC01Z3c2ZzRwN2NwZ3hxLTgwMDAuYXBwLmdpdGh1Yi5kZXYvYXV0aDAtY2FsbGJhY2svOmhJaVNKYmtfcG1JVFQ1ZnRfNUVOa0loT2FSN3NOdjdYTkU5N3N2RU9faUk%3D
  &scope=openid+profile+email
  &state=a6eb...

Which then tried to redirect back to:

https://www.pillarpointstewards.com/auth0-callback/
  ?forward=aHR0cHM6Ly9zdXBlci1kdXBl...2RU9faUk%3D
  &code=pIq...
  &state=a6e...

Which threw a 500 error saying "state check failed, your authentication request is no longer valid" - because I haven't yet taught the production instance how to redirect back to that decoded forward URL.

simonw commented 1 year ago

I've created a secret token called PILLARPOINTSTEWARDS_AUTH0_FORWARD_SECRET and saved it in 1Password.

I've assigned it as an environment variable in Fly like this:

fly secrets set AUTH0_FORWARD_SECRET=xxx -a pillarpointstewards
simonw commented 1 year ago

Now I'm setting it in my Codespaces secrets: https://github.com/settings/codespaces

image image
simonw commented 1 year ago

I destroyed my Codespace and created a brand new one.

In that new one I ran this:

env | grep AUTH

Which confirmed that my AUTH0_FORWARD_SECRET was available.

Then I ran:

AUTH0_FORWARD_URL='https://www.pillarpointstewards.com/auth0-callback/' python pillarpointstewards/manage.py runserver

I tried signing in... but I ended up signed into https://www.pillarpointstewards.com/ without being redirected back to my dev environment.

The network panel in my browser seemed to indicate that the ?forward= parameter did not survive all the way through the auth0 redirects.

Actually no I think it's my bug - the /login/ page redirected to:

https://pillarpointstewards.us.auth0.com/authorize
  ?response_type=code
  &client_id=DLXBMPbt...
  &redirect_uri=https%3A%2F%2Fwww.pillarpointstewards.com%2Fauth0-callback%2F
  &scope=openid+profile+email&state=b51...

It looks like it forgot the ?forward= option there.

simonw commented 1 year ago

The first reason it didn't work is that clicking the "Login" link in the dev environment was hard-coded to this:

https://github.com/natbat/pillarpointstewards/blob/f6cd32262b9b5a176961de12c034bfb7d22393e4/pillarpointstewards/templates/index.html#L154

But when I did login I got an error, which was caused by this bit:

https://github.com/natbat/pillarpointstewards/blob/f6cd32262b9b5a176961de12c034bfb7d22393e4/pillarpointstewards/auth0_login/views.py#L89-L102

It needs the redirect_uri AGAIN, but I need to make sure I send it the fake one from AUTH0_FORWARD_URL

Also I needed to start the dev server like this:

AUTH0_CLIENT_SECRET='014...' \
  DJANGO_DEBUG=1 \
  AUTH0_FORWARD_URL='https://www.pillarpointstewards.com/auth0-callback/' \
  python pillarpointstewards/manage.py runserver

It needs that Auth0 secret in order to do the rest of the steps.

simonw commented 1 year ago

That worked! I had to start the server with the correct AUTH0_CLIENT_SECRET (I had copied and pasted only part of it the first time) but I have now successfully logged in via SSO on a dev environment in GitHub Codespaces.