Skip to content

Commit

Permalink
Merge pull request #582 from AnswerDotAI/oauth_docs_new_intro
Browse files Browse the repository at this point in the history
New intro to oath class in docs
  • Loading branch information
jph00 authored Nov 19, 2024
2 parents 803d974 + d82c77b commit 61ecb09
Showing 1 changed file with 104 additions and 10 deletions.
114 changes: 104 additions & 10 deletions nbs/explains/oauth.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,122 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"OAuth is an open standard for 'access delegation', commonly used as a way for Internet users to grant websites or applications access to their information on other websites but without giving them the passwords. It is the mechanism that enables \"Log in with Google\" on many sites, saving you from having to remember and manage yet another password. Like many auth-related topics, there's a lot of depth and complexity to the OAuth standard, but once you understand the basic usage it can be a very convenient alternative to managing your own user accounts."
"OAuth is an open standard for 'access delegation', commonly used as a way for Internet users to grant websites or applications access to their information on other websites but without giving them the passwords. It is the mechanism that enables \"Log in with Google\" on many sites, saving you from having to remember and manage yet another password. Like many auth-related topics, there's a lot of depth and complexity to the OAuth standard, but once you understand the basic usage it can be a very convenient alternative to managing your own user accounts.\n",
"\n",
"On this page you'll see how to use OAuth with FastHTML to implement some common pieces of functionality."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"On this page you'll see how to use OAuth with FastHTML to implement some common pieces of functionality."
"## Creating an Client"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"FastHTML has Client classes for managing settings and state for different OAuth providers. Currently implemented are: GoogleAppClient, GitHubAppClient, HuggingFaceClient and DiscordAppClient - see the [source](https://github.com/AnswerDotAI/fasthtml/blob/main/nbs/api/08_oauth.ipynb) if you need to add other providers. You'll need a `client_id` and `client_secret` from the provider (see the from-scratch example later in this page for an example of registering with GitHub) to create the client. We recommend storing these in environment variables, rather than hardcoding them in your code."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"from fasthtml.oauth import GoogleAppClient\n",
"client = GoogleAppClient(os.getenv(\"AUTH_CLIENT_ID\"),\n",
" os.getenv(\"AUTH_CLIENT_SECRET\"))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The client is used to obtain a login link and to manage communications between your app and the OAuth provider (`client.login_link(redirect_uri=\"/redirect\")`)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In FastHTML you set up a client like `GoogleAppClient`. The client is responsible for storing the client ID and client secret, and for handling the OAuth flow. Let's run through three examples, illustrating some important concepts across three different OAuth providers."
"## Using the OAuth class"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Once you've set up a client, adding OAuth to a FastHTML app can be as simple as:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from fasthtml.oauth import OAuth\n",
"from fasthtml.common import FastHTML, RedirectResponse\n",
"\n",
"class Auth(OAuth):\n",
" def get_auth(self, info, ident, session, state):\n",
" email = info.email or ''\n",
" if info.email_verified and email.split('@')[-1]=='answer.ai':\n",
" return RedirectResponse('/', status_code=303)\n",
"\n",
"app = FastHTML()\n",
"oauth = Auth(app, client)\n",
"\n",
"@app.get('/')\n",
"def home(auth): return P('Logged in!'), A('Log out', href='/logout')\n",
"\n",
"@app.get('/login')\n",
"def login(req): return Div(P(\"Not logged in\"), A('Log in', href=oauth.login_link(req)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"There's a fair bit going on here, so let's unpack what's happening in that code:\n",
"\n",
"- OAuth (and by extension our custom Auth class) has a number of default arguments, including some key URLs: `redir_path='/redirect', error_path='/error', logout_path='/logout', login_path='/login'`. It will create and handle the redirect and logout paths, and it's up to you to handle `/login` (where unsuccessful login attempts will be redirected) and `/error` (for oauth errors).\n",
"- When we run `oauth = Auth(app, client)` it adds the redirect and logout paths to the app and also adds some beforeware. This beforeware runs on any requests (apart from any specified with the `skip` parameter). \n",
"\n",
"The added beforeware specifies some app behaviour:\n",
"\n",
"- If someone who isn't logged in attempts to visit our homepage (`/`) here, they will be redirected to `/login`.\n",
"- If they are logged in, it calls a `check_invalid` method. This defaults to False, which let's the user continue to the page they requested. The behaviour can be modified by defining your own `check_invalid` method in the Auth class - for example, you could have this forcibly log out users who have recently been banned.\n",
"\n",
"So how does someone log in? If they visit (or are redirected to) the login page at `/login`, we show them a login link. This sends them to the OAuth provider, where they'll go through the steps of selecting their account, giving permissions etc. Once done they will be redirected back to `/redirect`. Behind the scenes a code that comes as part of their request gets turned into user info, which is then passed to the key function `get_auth(self, info, ident, session, state)`. Here is where you'd handle looking up or adding a user in a database, checking for some condition (for example, this code checks if the email is an answer.ai email address) or choosing the destination based on state. The arguments are:\n",
"\n",
"- `self`: the Auth object, which you can use to access the client (`self.cli`)\n",
"- `info`: the information provided by the OAuth provider, typically including a unique user id, email address, username and other metadata.\n",
"- `ident`: a unique identifier for this user. What this looks like varies between providers. This is useful for managing a database of users, for example.\n",
"- `session`: the current session, that you can store information in securely\n",
"- `state`: you can optionally pass in some state when creating the login link. This persists and is returned after the user goes through the Oath steps, which is useful for returning them to the same page they left. It can also be used as added security against CSRF attacks. \n",
"\n",
"In our example, we check the email in `info` (we use a GoogleAppClient, not all providers will include an email). If we aren't happy, and get_auth returns False or nothing (as in the case here for non-answerai people) then the user is redirected back to the login page. But if everything looks good we return a redirect to the homepage, and an `auth` key is added to the session and the scope containing the users identity `ident`. So, for example, in the homepage route we could use `auth` to look up this particular user's profile info and customize the page accordingly. This auth will persist in their session until they clear the browser cache, so by default they'll stay logged in. To log them out, remove it ( `session.pop('auth', None)`) or send them to `/logout` which will do that for you. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Explaining OAuth with a from-scratch implementation"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Hopefully the example above is enough to get you started. You can also check out the (fairly minimal) [source code](https://github.com/AnswerDotAI/fasthtml/blob/main/nbs/api/08_oauth.ipynb) where this is implemented, and the [examples here](https://github.com/AnswerDotAI/fasthtml-example/blob/main/oauth_example).\n",
"\n",
"If you're wanting to learn more about how this works, and to see where you might add additional functionality, the rest of this page will walk through some examples **without** the OAuth convenience class, to illustrate the concepts. This was writted before said OAuth class was available, and is kep here for educational purposes - we recommend you stick with the new approach shown above in most cases."
]
},
{
Expand Down Expand Up @@ -343,13 +444,6 @@
"source": [
"This page (and OAuth support in FastHTML) is a work in progress. Questions, PRs, and feedback are welcome!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
Expand Down

0 comments on commit 61ecb09

Please sign in to comment.