home / tils / til

Menu
  • GraphQL API

til: auth0_oauth-with-auth0.md

This data as json

path topic title url body html shot created created_utc updated updated_utc shot_hash slug
auth0_oauth-with-auth0.md auth0 Simplest possible OAuth authentication with Auth0 https://github.com/simonw/til/blob/main/auth0/oauth-with-auth0.md [Auth0](https://auth0.com/) provides an authentication API which you can use to avoid having to deal with user accounts in your own web application. We used them last year for [VaccinateCA VIAL](https://github.com/CAVaccineInventory/vial), using the [Python Social Auth](https://github.com/python-social-auth/social-app-django) library recommended by the [Auth0 Django tutorial](https://auth0.com/docs/quickstart/webapp/django/01-login). That was quite a lot of code, so today I decided to figure out how to implement Auth0 authentication from first principles. Auth0 uses standard OAuth 2. Their documentation [leans very heavily](https://auth0.com/docs/quickstart/webapp) towards client libraries, but if you dig around enough you can find the [Authentication API](https://auth0.com/docs/api/authentication) documentation with the information you need. I found that pretty late, and figured out most of this by following [their Flask tutorial](https://auth0.com/docs/quickstart/webapp/python) and then [reverse engineering](https://github.com/natbat/pillarpointstewards/issues/6) what the prototype was actually doing. ## Initial setup To start, you need to create a new Auth0 application and note down three values. Mine looked something like this: ```python AUTH0_DOMAIN = "pillarpointstewards.us.auth0.com" AUTH0_CLIENT_ID = "DLXBMPbtamC2STUyV7R6OFJFDsSTHqEA" AUTH0_CLIENT_SECRET = "..." # Get it from that page ``` You also need to decide on the "callback URL" that authenticated users will be redirected to, then add that to the "Allowed Callback URLs" setting in Auth0. You can set this as a comma-separated list. My callback URL started out as `http://localhost:8000/callback`. ## Redirecting to Auth0 The first step is to redirect the user to Auth0 to sign in. The redirect URL looks something like this: ``` https://pillarpointstewards.us.auth0.com/authorize? response_type=code &client_id=DLXBMPbtamC2STUyV7R6OFJFDsSTHqEA &redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Fcallback &scope=openid+profile+email &state=FtYFQBczDZemVurdBan5PjRiePPGhU ``` You can also hit https://pillarpointstewards.us.auth0.com/.well-known/openid-configuration to get back JSON describing all of the end points, but I prefer to hard-code them in rather than take on the performance overhead of that additional HTTP request. The `state=` field there is a random string that you generate. You should store this in a cookie so you can compare it later on to [protect against CSRF attacks](https://auth0.com/docs/secure/attack-protection/state-parameters). ## User redirects back to your callback URL The user signs in on Auth0 (which they may do via Google SSO, or by creating or using an Auth0 account). Auth0 then redirects them back to your callback URL, like this: https://your-site/callback?code=CODE_HERE&state=STATE_YOU_PROVIDED Check that state against the cookie you set earlier (optional but a good idea). Now you need to exchange the `code=` for an access token. You do that with an authenticated server-side HTTP POST to the following URL: `https://pillarpointstewards.us.auth0.com/oauth/token` With these POST parameters: ``` grant_type=authorization_code &redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback &code=804-RPsfJb9zIiLNtgP5LVKUnYor8_lN7vltl7DkRpxF- ``` The redirect URI is the same as before. The code is the code that was passed to you in the URL to the `/callback` page. This call needs to be authenticated using HTTP basic authentication with the username set to your client ID and the password set to your client secret. In HTTP, that looks like a `Authorization: Basic BASE64` header, where `BASE64` is the base-64 encoded string of `username:password` (or `client_id:client_secret`. In Python using [HTTPX](https://www.python-httpx.org) that looks like this: ```python response = httpx.post( "https://{}/oauth/token".format(config["domain"]), data={ "grant_type": "authorization_code", "redirect_uri": redirect_uri, "code": code, }, auth=(config["client_id"], config["client_secret"]), ) access_token = response.json()["access_token"] ``` The response from that is a JSON object with a `"access_token"` key containing an access token. ## Fetching the user information The `access_token` can then be used to make an authenticated API call to `https://pillarpointstewards.us.auth0.com/userinfo` to get back the user's profile: ```python profile_response = httpx.get( "https://{}/userinfo".format(config["domain"]), headers={"Authorization": "Bearer {}".format(access_token)}, ) profile = profile_response.json() ``` And that's it! ## datasette-auth0 I implemented this in a new authentication plugin for Datasette called [datasette-auth0](https://datasette.io/plugins/datasette-auth0). The bulk of the implementation is in this file: <https://github.com/simonw/datasette-auth0/blob/0.1a0/datasette_auth0/__init__.py> <p><a href="https://auth0.com/" rel="nofollow">Auth0</a> provides an authentication API which you can use to avoid having to deal with user accounts in your own web application.</p> <p>We used them last year for <a href="https://github.com/CAVaccineInventory/vial">VaccinateCA VIAL</a>, using the <a href="https://github.com/python-social-auth/social-app-django">Python Social Auth</a> library recommended by the <a href="https://auth0.com/docs/quickstart/webapp/django/01-login" rel="nofollow">Auth0 Django tutorial</a>.</p> <p>That was quite a lot of code, so today I decided to figure out how to implement Auth0 authentication from first principles.</p> <p>Auth0 uses standard OAuth 2. Their documentation <a href="https://auth0.com/docs/quickstart/webapp" rel="nofollow">leans very heavily</a> towards client libraries, but if you dig around enough you can find the <a href="https://auth0.com/docs/api/authentication" rel="nofollow">Authentication API</a> documentation with the information you need.</p> <p>I found that pretty late, and figured out most of this by following <a href="https://auth0.com/docs/quickstart/webapp/python" rel="nofollow">their Flask tutorial</a> and then <a href="https://github.com/natbat/pillarpointstewards/issues/6">reverse engineering</a> what the prototype was actually doing.</p> <h2> <a id="user-content-initial-setup" class="anchor" href="#initial-setup" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Initial setup</h2> <p>To start, you need to create a new Auth0 application and note down three values. Mine looked something like this:</p> <div class="highlight highlight-source-python"><pre><span class="pl-v">AUTH0_DOMAIN</span> <span class="pl-c1">=</span> <span class="pl-s">"pillarpointstewards.us.auth0.com"</span> <span class="pl-v">AUTH0_CLIENT_ID</span> <span class="pl-c1">=</span> <span class="pl-s">"DLXBMPbtamC2STUyV7R6OFJFDsSTHqEA"</span> <span class="pl-v">AUTH0_CLIENT_SECRET</span> <span class="pl-c1">=</span> <span class="pl-s">"..."</span> <span class="pl-c"># Get it from that page</span></pre></div> <p>You also need to decide on the "callback URL" that authenticated users will be redirected to, then add that to the "Allowed Callback URLs" setting in Auth0. You can set this as a comma-separated list.</p> <p>My callback URL started out as <code>http://localhost:8000/callback</code>.</p> <h2> <a id="user-content-redirecting-to-auth0" class="anchor" href="#redirecting-to-auth0" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Redirecting to Auth0</h2> <p>The first step is to redirect the user to Auth0 to sign in. The redirect URL looks something like this:</p> <pre><code>https://pillarpointstewards.us.auth0.com/authorize? response_type=code &amp;client_id=DLXBMPbtamC2STUyV7R6OFJFDsSTHqEA &amp;redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Fcallback &amp;scope=openid+profile+email &amp;state=FtYFQBczDZemVurdBan5PjRiePPGhU </code></pre> <p>You can also hit <a href="https://pillarpointstewards.us.auth0.com/.well-known/openid-configuration" rel="nofollow">https://pillarpointstewards.us.auth0.com/.well-known/openid-configuration</a> to get back JSON describing all of the end points, but I prefer to hard-code them in rather than take on the performance overhead of that additional HTTP request.</p> <p>The <code>state=</code> field there is a random string that you generate. You should store this in a cookie so you can compare it later on to <a href="https://auth0.com/docs/secure/attack-protection/state-parameters" rel="nofollow">protect against CSRF attacks</a>.</p> <h2> <a id="user-content-user-redirects-back-to-your-callback-url" class="anchor" href="#user-redirects-back-to-your-callback-url" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>User redirects back to your callback URL</h2> <p>The user signs in on Auth0 (which they may do via Google SSO, or by creating or using an Auth0 account). Auth0 then redirects them back to your callback URL, like this:</p> <pre><code>https://your-site/callback?code=CODE_HERE&amp;state=STATE_YOU_PROVIDED </code></pre> <p>Check that state against the cookie you set earlier (optional but a good idea).</p> <p>Now you need to exchange the <code>code=</code> for an access token.</p> <p>You do that with an authenticated server-side HTTP POST to the following URL:</p> <p><code>https://pillarpointstewards.us.auth0.com/oauth/token</code></p> <p>With these POST parameters:</p> <pre><code>grant_type=authorization_code &amp;redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback &amp;code=804-RPsfJb9zIiLNtgP5LVKUnYor8_lN7vltl7DkRpxF- </code></pre> <p>The redirect URI is the same as before. The code is the code that was passed to you in the URL to the <code>/callback</code> page.</p> <p>This call needs to be authenticated using HTTP basic authentication with the username set to your client ID and the password set to your client secret.</p> <p>In HTTP, that looks like a <code>Authorization: Basic BASE64</code> header, where <code>BASE64</code> is the base-64 encoded string of <code>username:password</code> (or <code>client_id:client_secret</code>.</p> <p>In Python using <a href="https://www.python-httpx.org" rel="nofollow">HTTPX</a> that looks like this:</p> <div class="highlight highlight-source-python"><pre><span class="pl-s1">response</span> <span class="pl-c1">=</span> <span class="pl-s1">httpx</span>.<span class="pl-en">post</span>( <span class="pl-s">"https://{}/oauth/token"</span>.<span class="pl-en">format</span>(<span class="pl-s1">config</span>[<span class="pl-s">"domain"</span>]), <span class="pl-s1">data</span><span class="pl-c1">=</span>{ <span class="pl-s">"grant_type"</span>: <span class="pl-s">"authorization_code"</span>, <span class="pl-s">"redirect_uri"</span>: <span class="pl-s1">redirect_uri</span>, <span class="pl-s">"code"</span>: <span class="pl-s1">code</span>, }, <span class="pl-s1">auth</span><span class="pl-c1">=</span>(<span class="pl-s1">config</span>[<span class="pl-s">"client_id"</span>], <span class="pl-s1">config</span>[<span class="pl-s">"client_secret"</span>]), ) <span class="pl-s1">access_token</span> <span class="pl-c1">=</span> <span class="pl-s1">response</span>.<span class="pl-en">json</span>()[<span class="pl-s">"access_token"</span>]</pre></div> <p>The response from that is a JSON object with a <code>"access_token"</code> key containing an access token.</p> <h2> <a id="user-content-fetching-the-user-information" class="anchor" href="#fetching-the-user-information" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Fetching the user information</h2> <p>The <code>access_token</code> can then be used to make an authenticated API call to <code>https://pillarpointstewards.us.auth0.com/userinfo</code> to get back the user's profile:</p> <div class="highlight highlight-source-python"><pre><span class="pl-s1">profile_response</span> <span class="pl-c1">=</span> <span class="pl-s1">httpx</span>.<span class="pl-en">get</span>( <span class="pl-s">"https://{}/userinfo"</span>.<span class="pl-en">format</span>(<span class="pl-s1">config</span>[<span class="pl-s">"domain"</span>]), <span class="pl-s1">headers</span><span class="pl-c1">=</span>{<span class="pl-s">"Authorization"</span>: <span class="pl-s">"Bearer {}"</span>.<span class="pl-en">format</span>(<span class="pl-s1">access_token</span>)}, ) <span class="pl-s1">profile</span> <span class="pl-c1">=</span> <span class="pl-s1">profile_response</span>.<span class="pl-en">json</span>()</pre></div> <p>And that's it!</p> <h2> <a id="user-content-datasette-auth0" class="anchor" href="#datasette-auth0" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>datasette-auth0</h2> <p>I implemented this in a new authentication plugin for Datasette called <a href="https://datasette.io/plugins/datasette-auth0" rel="nofollow">datasette-auth0</a>.</p> <p>The bulk of the implementation is in this file: <a href="https://github.com/simonw/datasette-auth0/blob/0.1a0/datasette_auth0/__init__.py">https://github.com/simonw/datasette-auth0/blob/0.1a0/datasette_auth0/__init__.py</a></p> <Binary: 84,792 bytes> 2022-03-26T14:57:42-07:00 2022-03-26T21:57:42+00:00 2022-03-26T19:27:47-07:00 2022-03-27T02:27:47+00:00 1b78ef3b2c3b7a00a06af069ac7e9070 oauth-with-auth0
Powered by Datasette · How this site works · Code of conduct