Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🚧 OAuth - Client SDK #2483

Merged
merged 50 commits into from
Aug 12, 2024
Merged
Changes from 1 commit
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
2ded015
feat(api): support creation of oauth based AtpAgents
matthieusieben Jul 11, 2024
ac67237
tidy
devinivy Jul 30, 2024
7b7ee9d
Merge remote-tracking branch 'origin/main' into feat-oauth-client
devinivy Jul 30, 2024
ddb4f1c
codegen
devinivy Jul 30, 2024
c8b47e3
oauth client prerelease w/ version bumps
devinivy Jul 30, 2024
a2b6952
oauth: misc fixes for confidential clients
devinivy Aug 5, 2024
da9c408
add changeset, rc version for oauth client
devinivy Aug 5, 2024
3681c90
fix(xprc): remove ReadableStream.from polyfill
matthieusieben Aug 5, 2024
7436874
OAuth docs tweaks (#2679)
bnewbold Aug 5, 2024
cf86285
Merge remote-tracking branch 'origin/main' into feat-oauth-client
matthieusieben Aug 6, 2024
ac58646
restored rc version in changelogs
matthieusieben Aug 6, 2024
ab6ad9d
Revert "restored rc version in changelogs"
matthieusieben Aug 6, 2024
1711802
avoid relying on ReadableStream.from in xrpc-server tests
devinivy Aug 6, 2024
5b5dc77
lint
devinivy Aug 6, 2024
4c4149f
feat(oauth-types): expose "ALLOW_UNSECURE_ORIGINS" constant
matthieusieben Jul 16, 2024
4c3514a
feat(did): type util
matthieusieben Jul 16, 2024
f6dcfa8
feat(handle-resolver): expose "AtprotoIdentityDidMethods" type
matthieusieben Jul 16, 2024
b0b1b9d
fix(oauth-client): ensure that the oauth metadata document contains c…
matthieusieben Jul 16, 2024
f706d2a
fix(oauth-types): prevent unknown query string in loopback client id
matthieusieben Jul 16, 2024
d06ccb8
fix(identity-resolver): check that handle is in did doc's "alsoKnownAs"
matthieusieben Jul 16, 2024
8f9ec34
feat(oauth-client:oauth-resolver): allow logging in using either the …
matthieusieben Jul 16, 2024
a7b8466
fix(oauth-client): return better error in case of invalid "oauth-prot…
matthieusieben Jul 17, 2024
e45b033
refactor(did): group atproto specific checks in own
matthieusieben Aug 6, 2024
373f913
feat(api): relax typing of "appLabelers" and "labelers" AtpClient pro…
matthieusieben Aug 6, 2024
342bc50
allow any did as labeller (for tests mainly)
matthieusieben Aug 6, 2024
5b99e93
Merge remote-tracking branch 'origin/main' into feat-oauth-client
matthieusieben Aug 6, 2024
1d78b90
fix(api): allow to override "atproto-proxy" on a per-request basis
matthieusieben Aug 6, 2024
ac24b56
Merge remote-tracking branch 'origin/main' into feat-oauth-client
matthieusieben Aug 7, 2024
052f69f
remove release candidate versions from changelog
matthieusieben Aug 7, 2024
2079ac8
update changeset for api and xrpc packages
matthieusieben Aug 7, 2024
5b5f661
Add missing changeset
matthieusieben Aug 7, 2024
7073659
revert RC versions
matthieusieben Aug 7, 2024
c80eacf
Proper wording in OAUTH.md api example
matthieusieben Aug 7, 2024
eff6d01
remove "pre" changeset file
matthieusieben Aug 8, 2024
c9014de
xrpc: restore original behavior of setHEader and unsetHeader
matthieusieben Aug 8, 2024
8b68a06
docs: add comment for XrpcClient 's constructor arg
matthieusieben Aug 8, 2024
7fab6a5
feat(api): expose "schemas" publicly
matthieusieben Aug 8, 2024
6f4e44a
feat(api): allow customizing the whatwg fetch function of the AtpAgent
matthieusieben Aug 8, 2024
c9b39b3
docs(api): improve migration docs
matthieusieben Aug 8, 2024
4b4f53e
docs: change reference to BskyAgent to AtpAgent
matthieusieben Aug 9, 2024
e4d96f7
docs: mention the breaking change regarding setSessionPersistHandler
matthieusieben Aug 9, 2024
2ae6c24
fix(api): better split AtpClient concerns
matthieusieben Aug 10, 2024
88d7f8c
fix(xrpc): remove unused import
matthieusieben Aug 10, 2024
961bf0f
refactor(api): simplify class hierarchu by removeing AtpClient
matthieusieben Aug 12, 2024
c1a8953
fix(api): mock proper method for facets detection
matthieusieben Aug 12, 2024
acd9479
restore ability to restore session asynchronously
matthieusieben Aug 12, 2024
6b95467
feat(api): allow instantiating Agent with same argument as super class
matthieusieben Aug 12, 2024
3f7f32b
docs(api): properly extend Agent class
matthieusieben Aug 12, 2024
744c75d
style(xrpc): var name
matthieusieben Aug 12, 2024
7064f5a
docs(api): remove "async" to header getter
matthieusieben Aug 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Next Next commit
feat(api): support creation of oauth based AtpAgents
matthieusieben committed Jul 12, 2024
commit 2ded0156b9adf33b9cce66583a375bff922d383b
5 changes: 5 additions & 0 deletions .changeset/funny-points-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@atproto/xrpc-server": minor
---

Allow upload of payload of zero bytes (e.g. empty txt file)
matthieusieben marked this conversation as resolved.
Show resolved Hide resolved
5 changes: 5 additions & 0 deletions .changeset/honest-hornets-approve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@atproto/lexicon": patch
---

Add the ability to instantiate a Lexicon from an iterable, and to use a Lexicon as iterable.
matthieusieben marked this conversation as resolved.
Show resolved Hide resolved
35 changes: 35 additions & 0 deletions .changeset/purple-pans-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
"@atproto/lex-cli": minor
"@atproto/xrpc": minor
"@atproto/api": minor
"@atproto/xrpc-server": patch
matthieusieben marked this conversation as resolved.
Show resolved Hide resolved
"@atproto/dev-env": patch
"@atproto/lexicon": patch
"@atproto/ozone": patch
"@atproto/bsky": patch
"@atproto/pds": patch
---

**New Features**:

1) Improved Separation of Concerns: We've restructured the XRPC HTTP call
dispatcher into a distinct class. This means cleaner code organization and
better clarity on responsibilities.
2) Enhanced Evolutivity: With this refactor, the XRPC client is now more
adaptable to various use cases. You can easily extend and customize the
dispatcher perform session management, retries, and more.

**Compatibility**:

Most of the changes introduced in this version are backward-compatible. However,
there are a couple of breaking changes you should be aware of:

- Customizing `fetchHandler`: The ability to customize the fetchHandler on the
XRPC Client and AtpAgent classes has been modified. Please review your code if
you rely on custom fetch handlers.
- Managing Sessions: Previously, you had the ability to manage sessions directly
through AtpAgent instances. Now, session management must be handled through a
dedicated `SessionManager` instance. If you were making authenticated
requests, you'll need to update your code to use explicit session management.
- The `fetch()` method, as well as WhatWG compliant `Request` and `Headers`
constructors, must be globally available in your environment.
6 changes: 6 additions & 0 deletions .changeset/rare-feet-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@atproto/xrpc": minor
"@atproto/api": minor
---

Add the ability to use `fetch()` compatible `BodyInit` body when making XRPC calls.
3 changes: 3 additions & 0 deletions .github/workflows/repo.yaml
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 8
run_install: false
@@ -42,6 +43,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 8
run_install: false
@@ -62,6 +64,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 8
run_install: false
273 changes: 273 additions & 0 deletions packages/api/OAUTH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
# Oauth based authentication in a PWA

## Introduction

This document describes how to implement OAuth based authentication in a PWA, to
communicate with Bluesky's [[ATPROTO]] API.

## Prerequisites

- You need a web server, or at very least a static file server, to host your PWA.

> [!TIP]
>
> During development, you can use a local server to host your client metadata.
> You will need to use a tunneling service like [ngrok](https://ngrok.com/) to
> make your local server accessible from the internet.

> [!TIP]
>
> You can use a service like [GitHub Pages](https://pages.github.com/) to host
> your client metadata and PWA for free.

- You must be able to build and deploy a PWA to your server.

## Step 1: Create your client metadata

Based on your hosting server endpoint, you will first need to choose a
`client_id`. That `client_id` will be used to identify your client to Bluesky's
Authorization Servers. A `client_id` must be a URL that points to a JSON file
that contains your client metadata. The client metadata **must** contain a
`client_id` that is the URL used to access the metadata.

Here is an example client metadata.

```json
{
"client_id": "https://example.com/client-metadata.json",
"client_name": "Example PWA",
"client_uri": "https://example.com",
"logo_uri": "https://example.com/logo.png",
"tos_uri": "https://example.com/tos",
"policy_uri": "https://example.com/policy",
"redirect_uris": ["https://example.com/callback"],
"scope": "offline_access",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none",
"application_type": "web",
"dpop_bound_access_tokens": true
}
```

- `redirect_uris`: An array of URLs that will be used as the redirect URIs for
the OAuth flow. This should typically contain a single URL that points to a
page on your PWA that will handle the OAuth response. This URL must be HTTPS.

- `client_id`: The URL where the client metadata is hosted. This field must be
the exact same as the URL used to access the metadata.

- `client_name`: The name of your client. Will be displayed to the user during
the authentication process.

- `client_uri`: The URL of your client. Whether or not this value is actually
displayed / used is up to the Authorization Server.

- `logo_uri`: The URL of your client's logo. Should be displayed to the user
during the authentication process. Whether your logo is actually displayed
during the authentication process or not is up to the Authorization Server.

- `tos_uri`: The URL of your client's terms of service. Will be displayed to
the user during the authentication process.

- `policy_uri`: The URL of your client's privacy policy. Will be displayed to
the user during the authentication process.

- If you don't want or need the user to stay authenticated for long periods
(better for security), you can remove the `offline_access` scope, and
`refresh_token` from the `grant_types`.

> [!NOTE]
>
> To mitigate phishing attacks, the Authentication Server will typically _not_
> display the `client_uri`, `logo_uri` to the user. If you don't see your logo
> or client name during the authentication process, don't worry, this is normal.

Upload this JSON file so that it is accessible at the URL you chose for your
`client_id`.

## Step 2: Setup you PWA

Start by setting up your PWA. You can use any framework you like, or none at
all. In this example, we will use TypeScript and Parcel, with plain JavaScript.

```bash
npm init -y
npm install --save-dev @atproto/oauth-client-browser
npm install --save-dev @atproto/api
npm install --save-dev parcel
npm install --save-dev parcel-reporter-static-files-copy
mkdir -p src
mkdir -p static
```

Create a `.parcelrc` file with the following (exact) content:

```json
{
"extends": ["@parcel/config-default"],
"reporters": ["...", "parcel-reporter-static-files-copy"]
}
```

Create an `src/index.html` file with the following content:

```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My First OAuth App</title>
<script type="module" src="app.ts"></script>
</head>
<body>
Loading...
</body>
</html>
```

And an `src/app.ts` file, with the following content:

```typescript
console.log('Hello from PWA!')
```

Start the app in development mode:

```bash
npx parcel src/index.html
```

In another terminal, open a tunnel to your local server:

```bash
ngrok http 1234
```

Create a `static/client-metadata.json` file with the client metadata you created
in [Step 1](#step-1-create-your-client-metadata). Use the hostname provided by
ngrok as the `client_id`:

```json
{
"client_id": "https://<RANDOM_VALUE>.ngrok.app/client-metadata.json",
"client_name": "My First ATPROTO OAuth App",
"client_uri": "https://<RANDOM_VALUE>.ngrok.app",
"redirect_uris": ["https://<RANDOM_VALUE>.ngrok.app/"],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"token_endpoint_auth_method": "none",
"application_type": "web",
"dpop_bound_access_tokens": true
}
```

## Step 3: Implement the OAuth flow

Replace the content of the `src/app.ts` file, with the following content:

```typescript
import { BrowserOAuthClient } from '@atproto/oauth-client-browser'

async function main() {
const oauthClient = await BrowserOAuthClient.load({
clientId: '<YOUR_CLIENT_ID>',
handleResolver: 'https://bsky.social/',
})

// TO BE CONTINUED
}

document.addEventListener('DOMContentLoaded', main)
```

> [!CAUTION]
>
> By using `https://bsky.app/` as the `handleResolver`, you are using Bluesky's
> servers as handle resolver. This has the advantage of not requiring you to
> host your own handle resolver, but it also means that Bluesky will be able to
> see the IP addresses of your users (and their associated handle). If you want
> to avoid this, you will need to host your own handle resolver. If you are a
> PDS self-hoster, you can use your PDS's URL here. If you rely on Bluesky's
> handle resolver, you are required to inform your users that their IP addresses
> will be shared with Bluesky in your app's privacy policy.

The `oauthClient` is now configured to communicate with Bluesky's Authorization.
We can now initialize it in order to detect if the user is already
authenticated. Replace the `// TO BE CONTINUED` comment with the following code:

```typescript
const result = await oauthClient.init()
const agent = result?.agent

// TO BE CONTINUED
```

At this point you can detect if the user is already authenticated or not (by
checking if `agent` is `undefined`).

Let's initiate an authentication flow if the user is not authenticated. Replace
the `// TO BE CONTINUED` comment with the following code:

```typescript
if (!agent) {
const handle = prompt('Enter your Bluesky handle to authenticate')
if (!handle) throw new Error('Authentication process canceled by the user')

const url = await oauthClient.authorize(handle)

// Redirect the user to the authorization page
window.open(url, '_self', 'noopener')

// Protect against browser's back-forward cache
await new Promise<never>((resolve, reject) => {
setTimeout(
reject,
10_000,
new Error('User navigated back from the authorization page'),
)
})
}

// TO BE CONTINUED
```

At this point in the script, the user **will** be authenticated. API calls can
be made using an `ApiClient` instance. Let's make a simple call to the API to
retrieve the user's profile. Replace the `// TO BE CONTINUED` comment with the
following code:

```typescript
if (agent) {
const fetchProfile = async () => {
const profile = await agent.getProfile({ actor: agent.did })
return profile.data
}

// Update the user interface

document.body.textContent = `Authenticated as ${agent.did}`

const profileBtn = document.createElement('button')
document.body.appendChild(profileBtn)
profileBtn.textContent = 'Fetch Profile'
profileBtn.onclick = async () => {
const profile = await fetchProfile()
outputPre.textContent = JSON.stringify(profile, null, 2)
}

const logoutBtn = document.createElement('button')
document.body.appendChild(logoutBtn)
logoutBtn.textContent = 'Logout'
logoutBtn.onclick = async () => {
await oauthAgent.signOut()
window.location.reload()
}

const outputPre = document.createElement('pre')
document.body.appendChild(outputPre)
}
```

[API]: ./README.md
Loading