Skip to content

Commit

Permalink
Merge pull request #25 from noteable-io/oauth-blog
Browse files Browse the repository at this point in the history
OAuth blog
  • Loading branch information
rgbkrk authored Aug 9, 2023
2 parents a2d6f66 + b12a5d7 commit 11d04ab
Show file tree
Hide file tree
Showing 18 changed files with 184 additions and 1 deletion.
1 change: 1 addition & 0 deletions blog/2023-08-09-oauth-plugin/account_creation.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions blog/2023-08-09-oauth-plugin/bad_times.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added blog/2023-08-09-oauth-plugin/develop_plugin1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added blog/2023-08-09-oauth-plugin/develop_plugin2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added blog/2023-08-09-oauth-plugin/develop_plugin3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions blog/2023-08-09-oauth-plugin/good_times.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
115 changes: 115 additions & 0 deletions blog/2023-08-09-oauth-plugin/index.mdx
Original file line number Diff line number Diff line change
@@ -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.

<YouTubeEmbed videoId="-J1nZ4eJ1x0" title="OAuth for ChatGPT Plugins" />

## 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!

<!-- Want to build a your plugin to the next level? Check out our developer resources and join the Noteable community today. Together, we'll build the future of interactive computing. -->
1 change: 1 addition & 0 deletions blog/2023-08-09-oauth-plugin/localhost_dev.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions blog/2023-08-09-oauth-plugin/oauth_101.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions blog/2023-08-09-oauth-plugin/oauth_app.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions blog/2023-08-09-oauth-plugin/oauth_combined_app.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions blog/2023-08-09-oauth-plugin/oauth_config.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions blog/2023-08-09-oauth-plugin/user_accounts.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions blog/authors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 9 additions & 1 deletion docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,22 @@
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",
tagline: "Data Driven Documents for Developers",
favicon: "img/favicon.ico",

// Set the production url of your site here
url: "https://platform.noteable.io",
url: siteURL,
// Set the /<baseUrl>/ pathname under which your site is served
// For GitHub pages deployment, it is often '/<projectName>/'
baseUrl: "/",
Expand Down
27 changes: 27 additions & 0 deletions src/components/YouTubeEmbed.tsx
Original file line number Diff line number Diff line change
@@ -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) => (
<div className={styles.embed}>
<iframe
title={title}
src={`https://www.youtube.com/embed/${videoId}`}
frameBorder="0"
allow="autoplay; encrypted-media"
allowFullScreen
/>
</div>
);

export default YouTubeEmbed;
18 changes: 18 additions & 0 deletions src/components/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.embed {
position: relative;
padding-bottom: 56.25%;
height: 0;
overflow: hidden;
max-width: 100%;
height: auto;
margin: 0 auto;
display: block;
}

.embed iframe {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}

0 comments on commit 11d04ab

Please sign in to comment.