this repo has no description

add README

+137 -16
README.md
···
-
# expo-atproto-auth
+
# Expo Atproto OAuth
+
+
This is an Expo client library for Atproto OAuth. It implements the required native crypto functions for supporting JWTs in React Native and uses
+
the base `OAuthClient` interface found in [the Atproto repository](https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client).
+
+
## Prerequisites
+
+
Before using this library, there are a few additional libraries that you must install within your Expo application.
+
+
- [react-native-mmkv](https://www.npmjs.com/package/react-native-mmkv)
+
- [expo-web-browser](https://www.npmjs.com/package/expo-web-browser)
+
- [@atproto/oauth-client](https://www.npmjs.com/package/@atproto/oauth-client)
+
- [event-target-polyfill](https://www.npmjs.com/package/event-target-polyfill) (or similar)
+
- [abortcontroller-polyfill](https://www.npmjs.com/package/abortcontroller-polyfill) (or similar)
+
+
Apply the two polyfills inside your application's entrypoint (usually `index.ts`). They should be placed _before_ anything else in the file, and particularly before `registerRootComponent(App)`.
+
+
> [!CAUTION]
+
> As of current (Expo 53), you _must_ apply an Expo patch for this library to work. You may use the patch found [here](https://github.com/haileyok/expo-atproto-auth/blob/main/patches/expo%2B53.0.19.patch).
+
A fix for this has been submitted up stream and merged, so will hopefully be fixed in Expo 54 (see the PR [here](https://github.com/expo/expo/pull/38122)).
+
+
### In bare React Native projects
+
+
For bare React Native projects, you must ensure that you have [installed and configured the `expo` package](https://docs.expo.dev/bare/installing-expo-modules/)
+
before continuing.
+
+
## Installation
+
+
Once you have satisfied the prerequisites, you can simply install the library with `yarn add expo-atproto-auth`.
+
+
## Usage
+
+
### Serve your `oauth-client-metadata.json`
+
+
You will need to server an `oauth-client-metadata.json` from your application's website. An example of this metadata
+
would look like this:
+
+
```
+
{
+
"client_id": "https://hailey.at/oauth-client-metadata.json",
+
"client_name": "React Native OAuth Client Demo",
+
"client_uri": "https://hailey.at",
+
"redirect_uris": [
+
"at.hailey:/auth/callback"
+
],
+
"scope": "atproto transition:generic",
+
"token_endpoint_auth_method": "none",
+
"response_types": [
+
"code"
+
],
+
"grant_types": [
+
"authorization_code",
+
"refresh_token"
+
],
+
"application_type": "native",
+
"dpop_bound_access_tokens": true
+
}
+
```
+
+
- The `client_id` should be the same URL as where you are serving your `oauth-client-metadata.json` from
+
- The `client_uri` can be the home page of where you are serving your metadata from
+
- Your `redirect_uris` should contain the native redirect URI in the first position. Additionally, the scheme must be
+
formatted as the _reverse_ of the domain you are serving the metadata from. Since I am serving mine from `hailey.at`,
+
I use `at.hailey` as the scheme. If my domain were `atproto.expo.dev`, I would use `dev.expo.atproto`. Additionally, the scheme _must_ contain _only one trailing slash_ after the `:`. `at.hailey://` would be invalid.
+
- The `application_type` must be `native`
+
+
For a real-world example, see [Skylight's client metadata](https://skylight.expo.app/oauth/client-metadata.json).
-
Atproto OAuth for Expo applications
+
For more information about client metadata, see [the Atproto documentation](https://atproto.com/specs/oauth#client-id-metadata-document).
+
+
### Create a client
-
# API documentation
+
Next, you want to create an `ExpoOAuthClient`. You will need to pass in the same client metadata to the client as you are serving in your `oauth-client-metadata.json`.
-
- [Documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/atproto-auth/)
-
- [Documentation for the main branch](https://docs.expo.dev/versions/unversioned/sdk/atproto-auth/)
+
```ts
+
const client = new ExpoOAuthClient({
+
clientMetadata: {
+
client_id: 'https://hailey.at/oauth-client-metadata.json',
+
client_name: 'React Native OAuth Client Demo',
+
client_uri: 'https://hailey.at',
+
redirect_uris: ['at.hailey:/auth/callback'],
+
scope: 'atproto transition:generic',
+
token_endpoint_auth_method: 'none',
+
response_types: ['code'],
+
grant_types: ['authorization_code', 'refresh_token'],
+
application_type: 'native',
+
dpop_bound_access_tokens: true,
+
},
+
handleResolver: 'https://bsky.social',
+
})
+
```
-
# Installation in managed Expo projects
+
### Sign a user in
-
For [managed](https://docs.expo.dev/archive/managed-vs-bare/) Expo projects, please follow the installation instructions in the [API documentation for the latest stable release](#api-documentation). If you follow the link and there is no documentation available then this library is not yet usable within managed projects — it is likely to be included in an upcoming Expo SDK release.
+
Whenever you are ready, you can initiate a sign in attempt for the user using the client using `client.signIn(input)`
-
# Installation in bare React Native projects
+
`input` must be one of the following:
+
- A valid Atproto user handle, e.g. `hailey.bsky.team` or `hailey.at`
+
- A valid DID, e.g. `did:web:hailey.at` or `did:plc:oisofpd7lj26yvgiivf3lxsi`
+
- A valid PDS host, e.g. `https://cocoon.hailey.at` or `https://bsky.social`
-
For bare React Native projects, you must ensure that you have [installed and configured the `expo` package](https://docs.expo.dev/bare/installing-expo-modules/) before continuing.
+
> [!NOTE]
+
> If you wish to allow a user to _create_ an account instead of signing in, simply use a valid PDS hostname rather than
+
> a handle. They will be presented the option to either Sign In with an existing account, or create a new one.
-
### Add the package to your npm dependencies
+
The response of `signIn` will be a promise resolving to the following:
+
```ts
+
| { status: WebBrowserResultType } // See Expo Web Browser documentation
+
| { status: 'error'; error: unknown }
+
| { status: 'success'; session: OAuthSession }
```
-
npm install expo-atproto-auth
+
+
For example:
+
+
```ts
+
const res = await client.signIn(input ?? '')
+
if (res.status === 'success') {
+
setSession(res.session)
+
const newAgent = new Agent(res.session)
+
setAgent(newAgent)
+
} else if (res.status === 'error') {
+
Alert.alert('Error', (res.error as any).toString())
+
} else {
+
Alert.alert(
+
'Error',
+
`Received unknown WebResultType: ${res.status}`
+
)
+
}
```
-
### Configure for Android
+
### Create an `Agent`
+
To interface with the various Atproto APIs, you will need to create an `Agent`. You will pass your `OAuthSession` to the `Agent`.
+
```ts
+
const newAgent = new Agent(res.session)
+
```
+
Session refreshes will be handled for you for the lifetime of the agent.
+
+
### Restoring a session
-
### Configure for iOS
+
After, for example, closing the application, you will probably need to restore the user's session. You can do this by using the user's DID on the `ExpoOAuthClient`.
+
+
```ts
+
const restoreRes = await client.restore('did:plc:oisofpd7lj26yvgiivf3lxsi')
+
const newAgent = new Agent(restoreRes)
+
```
-
Run `npx pod-install` after installing the npm package.
+
If the session needs to be refreshed, `.restore()` will do this for you before returning a session.
-
# Contributing
+
## Additional Reading
-
Contributions are very welcome! Please refer to guidelines described in the [contributing guide]( https://github.com/expo/expo#contributing).
+
- [Atproto OAuth Spec](https://atproto.com/specs/oauth)
+
- [Atproto Web OAuth Example](https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-browser-example)
+5 -7
example/App.tsx
···
getRandomValues,
createJwt,
generateJwk,
-
ReactNativeOAuthClient,
+
ExpoOAuthClient,
} from 'expo-atproto-auth'
import { OAuthSession } from '@atproto/oauth-client'
import { Agent } from '@atproto/api'
-
import type { ReactNativeKey } from 'expo-atproto-auth'
+
import type { ExpoKey } from 'expo-atproto-auth'
-
const client = new ReactNativeOAuthClient({
+
const client = new ExpoOAuthClient({
clientMetadata: {
client_id: 'https://hailey.at/oauth-client-metadata.json',
client_name: 'React Native OAuth Client Demo',
···
const [values, setValues] = React.useState<Uint8Array>()
const [sha, setSha] = React.useState<Uint8Array>()
const [jwt, setJwt] = React.useState<string>()
-
const [privateJwk, setPrivateJwk] = React.useState<
-
ReactNativeKey | undefined
-
>()
+
const [privateJwk, setPrivateJwk] = React.useState<ExpoKey | undefined>()
const [session, setSession] = React.useState<OAuthSession>()
const [input, setInput] = React.useState<string>()
const [agent, setAgent] = React.useState<Agent>()
···
<Button
title="Create JWK"
onPress={() => {
-
let newJwk: ReactNativeKey | undefined
+
let newJwk: ExpoKey | undefined
try {
newJwk = generateJwk('ES256')
} catch (e: any) {
+3 -3
src/index.ts
···
createJwt,
getRandomValues,
verifyJwt,
-
ReactNativeKey,
-
} from './react-native-key'
-
export { ExpoOAuthClient as ReactNativeOAuthClient } from './react-native-oauth-client'
+
ExpoKey,
+
} from './expo-key'
+
export { ExpoOAuthClient } from './expo-oauth-client'
+4 -4
src/react-native-key.ts src/expo-key.ts
···
export function createJwt(
header: string,
payload: string,
-
key: ReactNativeKey
+
key: ExpoKey
): string {
if (!key.privateJwk || !isECKey(key.privateJwk)) {
throw new Error('Invalid key')
···
}
// @ts-expect-error
-
export class ReactNativeKey implements Key {
+
export class ExpoKey implements Key {
#jwk: Readonly<JWK>
constructor(jwk: Readonly<JWK>) {
···
}
}
-
export function generateJwk(algoritihim: string): ReactNativeKey {
+
export function generateJwk(algoritihim: string): ExpoKey {
const privJwk = NativeModule.generatePrivateJwk(algoritihim)
-
return new ReactNativeKey(privJwk)
+
return new ExpoKey(privJwk)
}
+6 -6
src/react-native-oauth-client.ts src/expo-oauth-client.ts
···
OAuthClient,
OAuthSession,
} from '@atproto/oauth-client'
-
import { ReactNativeRuntimeImplementation } from './react-native-runtime-implementation'
-
import { ReactNativeOAuthDatabase } from './react-native-oauth-database'
+
import { ExpoRuntimeImplementation } from './expo-runtime-implementation'
+
import { ExpoOAuthDatabase } from './expo-oauth-database'
import { openAuthSessionAsync, WebBrowserResultType } from 'expo-web-browser'
export type Simplify<T> = { [K in keyof T]: T[K] } & NonNullable<unknown>
-
export type ReactNativeOAuthClientOptions = Simplify<
+
export type ExpoOAuthClientOptions = Simplify<
{
clientMetadata?: Readonly<OAuthClientMetadataInput>
responseMode?: Exclude<OAuthResponseMode, 'form_post'>
···
constructor({
responseMode = 'fragment',
...options
-
}: ReactNativeOAuthClientOptions) {
-
const database = new ReactNativeOAuthDatabase()
+
}: ExpoOAuthClientOptions) {
+
const database = new ExpoOAuthDatabase()
if (!['query', 'fragment'].includes(responseMode)) {
throw new TypeError(`Invalid response mode: ${responseMode}`)
···
options.clientMetadata ?? atprotoLoopbackClientMetadata('localhost'), // HACK: this fixes a type error for now, look into it later
responseMode,
keyset: undefined,
-
runtimeImplementation: new ReactNativeRuntimeImplementation(),
+
runtimeImplementation: new ExpoRuntimeImplementation(),
sessionStore: database.getSessionStore(),
stateStore: database.getStateStore(),
didCache: database.getDidCache(),
+6 -9
src/react-native-oauth-database.ts src/expo-oauth-database.ts
···
import { type SimpleStore, type Value } from '@atproto-labs/simple-store'
import { MMKV } from 'react-native-mmkv'
import { JWK } from './ExpoAtprotoAuth.types'
-
import { ReactNativeKey } from './react-native-key'
+
import { ExpoKey } from './expo-key'
type Item<V> = {
value: V
···
return encodedKey
}
-
async function decodeKey(encoded: EncodedKey): Promise<ReactNativeKey> {
-
console.log(encoded)
-
return new ReactNativeKey(encoded.keyPair.privateKey)
+
async function decodeKey(encoded: EncodedKey): Promise<ExpoKey> {
+
return new ExpoKey(encoded.keyPair.privateKey)
}
export type Schema = {
···
'protectedResourceMetadataCache',
]
-
export type ReactNativeOAuthDatabaseOptions = {
+
export type ExpoOAuthDatabaseOptions = {
name?: string
durability?: 'strict' | 'relaxed'
cleanupInterval?: number
}
-
export class ReactNativeOAuthDatabase {
+
export class ExpoOAuthDatabase {
#cleanupInterval?: ReturnType<typeof setInterval>
#mmkv?: MMKV
-
constructor(options?: ReactNativeOAuthDatabaseOptions) {
+
constructor(options?: ExpoOAuthDatabaseOptions) {
this.#cleanupInterval = setInterval(() => {
this.cleanup()
}, options?.cleanupInterval ?? 30e3)
···
): DatabaseStore<V> {
return {
get: async (key) => {
-
console.log(`getting ${name}.${key}`)
const item = this.#mmkv?.getString(`${name}.${key}`)
if (item === undefined) return undefined
···
}
const res = decode(JSON.parse(item))
-
console.log(res)
return res
},
+2 -2
src/react-native-runtime-implementation.ts src/expo-runtime-implementation.ts
···
import type { Key, RuntimeImplementation } from '@atproto/oauth-client'
import { default as NativeModule } from './ExpoAtprotoAuthModule'
-
import { generateJwk } from './react-native-key'
+
import { generateJwk } from './expo-key'
-
export class ReactNativeRuntimeImplementation implements RuntimeImplementation {
+
export class ExpoRuntimeImplementation implements RuntimeImplementation {
async createKey(algs: string[]): Promise<Key> {
if (!algs.includes('ES256')) {
throw TypeError('ES256 is the only supported algo')