diff --git a/blog/2023-08-09-oauth-plugin/account_creation.svg b/blog/2023-08-09-oauth-plugin/account_creation.svg new file mode 100644 index 0000000..0e890c2 --- /dev/null +++ b/blog/2023-08-09-oauth-plugin/account_creation.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/blog/2023-08-09-oauth-plugin/bad_times.svg b/blog/2023-08-09-oauth-plugin/bad_times.svg new file mode 100644 index 0000000..0efac40 --- /dev/null +++ b/blog/2023-08-09-oauth-plugin/bad_times.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/blog/2023-08-09-oauth-plugin/develop_plugin1.png b/blog/2023-08-09-oauth-plugin/develop_plugin1.png new file mode 100644 index 0000000..fe3f02c Binary files /dev/null and b/blog/2023-08-09-oauth-plugin/develop_plugin1.png differ diff --git a/blog/2023-08-09-oauth-plugin/develop_plugin2.png b/blog/2023-08-09-oauth-plugin/develop_plugin2.png new file mode 100644 index 0000000..d5d8048 Binary files /dev/null and b/blog/2023-08-09-oauth-plugin/develop_plugin2.png differ diff --git a/blog/2023-08-09-oauth-plugin/develop_plugin3.png b/blog/2023-08-09-oauth-plugin/develop_plugin3.png new file mode 100644 index 0000000..093c206 Binary files /dev/null and b/blog/2023-08-09-oauth-plugin/develop_plugin3.png differ diff --git a/blog/2023-08-09-oauth-plugin/good_times.svg b/blog/2023-08-09-oauth-plugin/good_times.svg new file mode 100644 index 0000000..57e74c5 --- /dev/null +++ b/blog/2023-08-09-oauth-plugin/good_times.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/blog/2023-08-09-oauth-plugin/index.mdx b/blog/2023-08-09-oauth-plugin/index.mdx new file mode 100644 index 0000000..66f3c48 --- /dev/null +++ b/blog/2023-08-09-oauth-plugin/index.mdx @@ -0,0 +1,115 @@ +--- +slug: oauth-for-chatgpt-plugins +title: "OAuth for ChatGPT Plugins" +authors: [kafonek] +description: "How Noteable added OAuth to its ChatGPT Plugin. By now, everyone has played with a large language model like ChatGPT. You've probably gone through those cycles of copying and pasting your email drafts, code snippets, or posts. There's a lot of power in giving large language models more context. The biggest way to do this is to provide access to documents directly. As a developer, you can enable this experience by writing a ChatGPT Plugin that uses OAuth." +image: "./oauth-plugin-social-card.png" +tags: [chatgpt, plugins, chatgpt plugins, oauth, security, architecture] +--- + +import YouTubeEmbed from "@site/src/components/YouTubeEmbed"; + +## Introduction + +By now, everyone has played with a large language model like ChatGPT. You've probably gone through those cycles of copying and pasting your email drafts, code snippets, or posts. There's a lot of power in giving large language models more context. The biggest way to do this is to provide access to documents directly. As a developer, you can enable this experience by writing a ChatGPT Plugin that uses OAuth. + +:::tip + +[OpenAI's own plugin docs](https://platform.openai.com/docs/plugins/review) include two use cases that either require OAuth or are greatly augmented by it. + +1. Retrieval over **user-specific** or otherwise hard-to-search knowledge sources + +2. Plugins that give the model **computational** abilities + +::: + +OAuth is a mechanism used to enable Single Sign-On (SSO) across applications. When you install the Noteable ChatGPT plugin, you can choose to login or sign up (it's free!) to Noteable using an existing Google, Github, or LinkedIn account. In this post, the Noteable engineering team wants to share some of the low-level details of how OAuth works, and how it's implemented in Noteable. We hope this helps other plugin developers and the community at large. + + + +## Why OAuth? + +Let’s start with why a plugin would use OAuth, compared to “no auth” or “service level auth”. Simply put, if your plugin or downstream API needs to know about a logged in user, use OAuth. For instance, if you were writing a wikipedia-reading plugin you could skip OAuth because you don’t need to have a logged in user to read Wiki. If the large language model (LLM) is creating Notebooks and running code via the Noteable plugin, which goes through role-based access control (RBAC) permission checks and user-context-aware features, we need to know what user account the request is for. + +There are many OAuth providers out there, and there's nothing stopping you from writing your own. We happen to use [Auth0](https://auth0.com/), so our examples will include their implementation details (such as `authorize` and `/oauth/token` endpoints). OpenAI and Auth0 both have good documentation about OAuth flows, I recommend reading these sections in addition to this blog post if you're working on an OAuth plugin yourself. + +- https://platform.openai.com/docs/plugins/authentication/oauth +- https://auth0.com/docs/authenticate/protocols/oauth#authorization-endpoint + +## OAuth 101 + +When you click `Install` on an OAuth-enabled ChatGPT plugin, your browser will be redirected to the OAuth provider page. Once you've completed logging in there, which may entail even more OAuth redirect jumps, the provider will redirect you back to ChatGPT. If everything goes well, ChatGPT will acquire a JSON web token (JWT) that it will include in an Authorization header on every HTTP request to your plugin. + +A JWT contains limited identity information about the authenticated user, and has an expiration. You can learn more about JWT's and decode the payloads in a JWT at [jwt.io](https://jwt.io/). + +![OAuth 101](./oauth_101.svg) + +:::note +When you are developing a plugin in localhost mode, the only authorization type allowed is "none". You cannot test OAuth flows in localhost development mode. You will need to host your plugin somewhere or use a tool like [ngrok](https://ngrok.com/) to create a proxy to your machine. + +::: + +## OAuth apps + +OAuth and JWT's are not unique to ChatGPT plugins. A typical front-end / back-end web application would use an OAuth flow very similar to the ChatGPT plugin experience. On the backend, you can validate that the JWT's you're receiving were issued by the OAuth provider you trust by using JSON Web Keys (JWK). At Noteable we use the [`jwcrypto`](https://jwcrypto.readthedocs.io/en/latest/) Python library. + +![OAuth app](./oauth_app.svg) + +The Noteable ChatGPT plugin is more or less a proxy to our main API. There's a little more going on in our application, but a plugin that is effectively a pass-through to another API can pass the JWT it got from ChatGPT right along as an Authorization header to the real API. + +![Plugin and Frontend](./oauth_combined_app.svg) + +## OAuth configuration + +Once you're ready to test out OAuth with your plugin, the first step is to have your plugin hosted somewhere besides `localhost` and for your manifest file (`ai-plugin.json`) to have its auth section set to type oauth. You'll also need to have the `client_url` and `authorization_url` point to the endpoints of your OAuth provider for the initial redirect and POST to grab the jwt respectively. + +When you click "develop your own plugin" in ChatGPT and give it the domain your plugin is hosted at, it will try to download the manifest file and OpenAPI spec file. If it sees your manifest file has type oauth, it will prompt you to enter the client_id and client_secret from your OAuth provider. + +![Develop your own Plugin step 1](./develop_plugin1.png) + +![Develop your own Plugin step 2](./develop_plugin2.png) + +After you've put those in, ChatGPT will give you a token that you need to add to your manifest file and then redeploy / restart. + +![Develop your own Plugin step 3](./develop_plugin3.png) + +If ChatGPT can pull the manifest file and see the new token, then the "develop your own plugin" flow is complete and ChatGPT will give you a plugin application id that you can use to update the redirect_uri in your OAuth provider. + +![OAuth config](./oauth_config.svg) + +:::note +Scope is optional, and is an empty string in the OpenAI example. Noteable uses scopes `openid profile email offline_access` in order get back three tokens during the OAuth process: `access_token`, `id_token`, and `refresh_token` (all are JWTs). + +- ChatGPT uses the `access_token` in Authorization headers to our plugin +- ChatGPT will automatically refresh `access_token` using the `refresh_token` +- Noteable uses the name and email from the `id_token` payload to create a User account in Noteable if one does not already exist + +You can read more about scopes [here](https://auth0.com/docs/get-started/apis/scopes/openid-connect-scopes) + +::: + +## Painful Lessons + +One decision we made early on at Noteable that turned out to be a mistake was creating User accounts using the `sub` payload from the Auth0 identity tokens, and looking up a User row in our database from the `sub` in the Auth0 access token. Each login mechanism ended up being its own separate User in our system. If you logged in to [app.noteable.io](https://app.noteable.io) using your Github social login, then installed the ChatGPT plugin and authenticated with your Google login, you would end up with all sorts of permission denied errors trying to work on Notebooks between them. It was a major pain point. + +A compounding problem was that we were enforcing email verification for username / password accounts using a rule in Auth0 that would not return a JWT until the user clicked a link in their email. In our Noteable app frontend, when you signed up that way we could direct the user what to do. However we had no control over the ChatGPT UI, and from the user perspective they would install the Noteable plugin and it would fail with no error message. Technically there was an error code in url arguments of the redirect from Auth0 back to ChatGPT, but it would take an eagle-eyed user to notice that. Our temporary solution was to disable username / password login from the Auth0 application we used for ChatGPT, funneling even more users into the multiple-account problem space. + +![Bad Times](./bad_times.svg) + +Our solution was to create a second database table we called Principals to represent the login mechanism. A Google, Github, or Auth0 username/password login with the same email all link to the same User account now. We reconfigured our ChatGPT manifest file to proxy the authorize and token endpoints through our plugin so that we could automatically create or link Noteable accounts during the OAuth flow. We moved the email verification onto our own system instead of within an Auth0 rule, with error handling in the plugin to tell the user that while they did successfully install the Noteable ChatGPT plugin, they still need to click the email verification link before it will successfully create Notebooks or run code for them. + +![Good Times](./good_times.svg) + +![Account Creation](./account_creation.svg) + +## Localhost development + +We mentioned at the top of the post that you cannot do OAuth testing in localhost development. If your backend API requires a JWT for authentication though, what do you do? Luckily at Noteable we issue our own tokens for programmatic access to our API, which we'll talk more about in other blog posts and show off in Origami documentation. + +![Localhost Development](./localhost_dev.svg) + +## Final Thoughts + +Integrating OAuth with ChatGPT plugins opens up a world of personalized possibilities, linking the reasoning capabilities of Large Language Models with personalized content. If you're a developer inspired by the idea of creating powerful, user-centric plugins, now's the time to get started. Dive into plugins, explore [OpenAI's documentation on plugins](https://platform.openai.com/docs/plugins/introduction), and make the most of OAuth to unlock the potential of personalized interaction. Join us on this journey and let's push what ChatGPT + Plugins can do! + + diff --git a/blog/2023-08-09-oauth-plugin/localhost_dev.svg b/blog/2023-08-09-oauth-plugin/localhost_dev.svg new file mode 100644 index 0000000..d35a529 --- /dev/null +++ b/blog/2023-08-09-oauth-plugin/localhost_dev.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/blog/2023-08-09-oauth-plugin/oauth-plugin-social-card.png b/blog/2023-08-09-oauth-plugin/oauth-plugin-social-card.png new file mode 100644 index 0000000..e88e578 Binary files /dev/null and b/blog/2023-08-09-oauth-plugin/oauth-plugin-social-card.png differ diff --git a/blog/2023-08-09-oauth-plugin/oauth_101.svg b/blog/2023-08-09-oauth-plugin/oauth_101.svg new file mode 100644 index 0000000..8c687f9 --- /dev/null +++ b/blog/2023-08-09-oauth-plugin/oauth_101.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/blog/2023-08-09-oauth-plugin/oauth_app.svg b/blog/2023-08-09-oauth-plugin/oauth_app.svg new file mode 100644 index 0000000..496cd81 --- /dev/null +++ b/blog/2023-08-09-oauth-plugin/oauth_app.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/blog/2023-08-09-oauth-plugin/oauth_combined_app.svg b/blog/2023-08-09-oauth-plugin/oauth_combined_app.svg new file mode 100644 index 0000000..5d3fa36 --- /dev/null +++ b/blog/2023-08-09-oauth-plugin/oauth_combined_app.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/blog/2023-08-09-oauth-plugin/oauth_config.svg b/blog/2023-08-09-oauth-plugin/oauth_config.svg new file mode 100644 index 0000000..51b3911 --- /dev/null +++ b/blog/2023-08-09-oauth-plugin/oauth_config.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/blog/2023-08-09-oauth-plugin/user_accounts.svg b/blog/2023-08-09-oauth-plugin/user_accounts.svg new file mode 100644 index 0000000..eb37511 --- /dev/null +++ b/blog/2023-08-09-oauth-plugin/user_accounts.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/blog/authors.yml b/blog/authors.yml index b1d0f90..227e4d0 100644 --- a/blog/authors.yml +++ b/blog/authors.yml @@ -3,3 +3,9 @@ kyle: title: Mad Scientist @ Noteable url: https://github.com/rgbkrk image_url: https://github.com/rgbkrk.png + +kafonek: + name: Matt Kafonek + title: Senior Software Engineer @ Noteable + url: https://github.com/kafonek + image_url: https://github.com/kafonek.png diff --git a/docusaurus.config.js b/docusaurus.config.js index ee056e2..e36e4d6 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -4,6 +4,14 @@ const lightCodeTheme = require("prism-react-renderer/themes/github"); const darkCodeTheme = require("prism-react-renderer/themes/dracula"); +// process.env.VERCEL_URL + +let siteURL = "https://platform.noteable.io"; + +if (process.env.VERCEL_URL) { + siteURL = `https://${process.env.VERCEL_URL}`; +} + /** @type {import('@docusaurus/types').Config} */ const config = { title: "Noteable Platform", @@ -11,7 +19,7 @@ const config = { favicon: "img/favicon.ico", // Set the production url of your site here - url: "https://platform.noteable.io", + url: siteURL, // Set the // pathname under which your site is served // For GitHub pages deployment, it is often '//' baseUrl: "/", diff --git a/src/components/YouTubeEmbed.tsx b/src/components/YouTubeEmbed.tsx new file mode 100644 index 0000000..d63a7d5 --- /dev/null +++ b/src/components/YouTubeEmbed.tsx @@ -0,0 +1,27 @@ +import React from "react"; + +import styles from "./styles.module.css"; + +type Props = { + videoId: string; + title: string; +}; + +const defaultProps = { + videoId: "dQw4w9WgXcQ", + title: "YouTube Video", +}; + +const YouTubeEmbed = ({ videoId, title }: Props = defaultProps) => ( +
+