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

Add OAuth utils #20

Merged
merged 7 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/utils-reference/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
- [getAvatarIcon](utils-reference/icons/getAvatarIcon.md)
- [getFavicon](utils-reference/icons/getFavicon.md)
- [getProgressIcon](utils-reference/icons/getProgressIcon.md)
- [OAuth](utils-reference/oauth/README.md)
- [OAuthService](utils-reference/oauth/OAuthService.md)
- [withAccessToken](utils-reference/oauth/withAccessToken.md)
- [getAccessToken](utils-reference/oauth/getAccessToken.md)
- [React hooks](utils-reference/react-hooks/README.md)
- [useCachedState](utils-reference/react-hooks/useCachedState.md)
- [usePromise](utils-reference/react-hooks/usePromise.md)
Expand Down
179 changes: 179 additions & 0 deletions docs/utils-reference/oauth/OAuthService.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# `OAuthService`

The `OAuthService` class is designed to abstract the OAuth authorization process using the PKCE (Proof Key for Code Exchange) flow, simplifying the integration with various OAuth providers such as Asana, GitHub, and others.

Use [OAuthServiceOptions](#OAuthServiceOptions) to configure the `OAuthService` class.

## Example

```ts
const client = new OAuth.PKCEClient({
redirectMethod: OAuth.RedirectMethod.Web,
providerName: "GitHub",
providerIcon: "extension_icon.png",
providerId: "github",
description: "Connect your GitHub account",
});

const github = new OAuthService({
client,
clientId: "7235fe8d42157f1f38c0",
scopes: "notifications repo read:org read:user read:project",
authorizeUrl: "https://github.oauth.raycast.com/authorize",
tokenUrl: "https://github.oauth.raycast.com/token",
});
```

## Signature

```ts
constructor(options: OAuthServiceOptions): OAuthService
```

### Methods

#### `authorize`

Initiates the OAuth authorization process or refreshes existing tokens if necessary. Returns a promise that resolves with the access token from the authorization flow.

##### Signature

```typescript
authorize(): Promise<string>;
```

##### Example

```typescript
const accessToken = await oauthService.authorize();
```

### Built-in Services

Some services are exposed by default to make it easy to authenticate with them. Here's the full list:

- [Asana](#asana)
- [GitHub](#github)
- [Google](#google)
- [Jira](#jira)
- [Linear](#linear)
- [Slack](#slack)
- [Zoom](#zoom)

These services are all instances of `OAuthService` with the default options being set. However, you're free to configure your own client ID, and URLs for a specific service.

#### Asana

```tsx
const asana = OAuthService.asana({
clientId: 'custom-client-id', // Optional: If omitted, defaults to a pre-configured client ID
scope: 'default', // Specify the scopes your application requires
personalAccessToken: 'personal-access-token', // Optional: For accessing the API directly
});
```

#### GitHub

```tsx
const github = OAuthService.github({
clientId: 'custom-client-id', // Optional: If omitted, defaults to a pre-configured client ID
scope: 'repo user', // Specify the scopes your application requires
personalAccessToken: 'personal-access-token', // Optional: For accessing the API directly
});
```

#### Google

```tsx
const google = OAuthService.google({
clientId: 'custom-client-id', // Optional: If omitted, defaults to a pre-configured client ID
scope: 'https://www.googleapis.com/auth/drive.readonly', // Specify the scopes your application requires
personalAccessToken: 'personal-access-token', // Optional: For accessing the API directly
});
```

#### Jira

```tsx
const jira = OAuthService.jira({
clientId: 'custom-client-id', // Optional: If omitted, defaults to a pre-configured client ID
scope: 'read:jira-user read:jira-work', // Specify the scopes your application requires
personalAccessToken: 'personal-access-token', // Optional: For accessing the API directly
});
```

#### Linear

```tsx
const linear = OAuthService.linear({
clientId: 'custom-client-id', // Optional: If omitted, defaults to a pre-configured client ID
scope: 'read write', // Specify the scopes your application requires
personalAccessToken: 'personal-access-token', // Optional: For accessing the API directly
});
```

#### Slack

```tsx
const slack = OAuthService.slack({
clientId: 'custom-client-id', // Optional: If omitted, defaults to a pre-configured client ID
scope: 'emoji:read', // Specify the scopes your application requires
personalAccessToken: 'personal-access-token', // Optional: For accessing the API directly
});
```

#### Zoom

```tsx
const zoom = OAuthService.zoom({
clientId: 'custom-client-id', // Optional: If omitted, defaults to a pre-configured client ID
scope: '', // Specify the scopes your application requires
personalAccessToken: 'personal-access-token', // Optional: For accessing the API directly
});
```

## Subclassing

You can subclass `OAuthService` to create a tailored service for other OAuth providers by setting predefined defaults.

Here's an example:

```ts
export class CustomOAuthService extends OAuthService {
constructor(options: ClientConstructor) {
super({
client: new OAuth.PKCEClient({
redirectMethod: OAuth.RedirectMethod.Web,
providerName: "PROVIDER_NAME",
providerIcon: "provider.png",
providerId: "PROVIDER-ID",
description: "Connect your {PROVIDER_NAME} account",
}),
clientId: "YOUR_CLIENT_ID",
authorizeUrl: "YOUR_AUTHORIZE_URL",
tokenUrl: "YOUR_TOKEN_URL",
scope: "YOUR_SCOPES"
extraParameters: {
actor: "user",
},
});
}
}
```

## Types

### OAuthServiceOptions
Here's an updated markdown table with a "Type" column:

| Property Name | Description | Type |
|---------------|-------------|------|
| client<mark style="color:red;">*</mark> | The PKCE Client defined using `OAuth.PKCEClient` from `@raycast/api` | `OAuth.PKCEClient` |
| clientId<mark style="color:red;">*</mark> | The app's client ID | `string` |
| scope<mark style="color:red;">*</mark> | The scope of the access requested from the provider | `string` |
| authorizeUrl<mark style="color:red;">*</mark> | The URL to start the OAuth flow | `string` |
| tokenUrl<mark style="color:red;">*</mark> | The URL to exchange the authorization code for an access token | `string` |
| refreshTokenUrl | The URL to refresh the access token if applicable | `string` |
| personalAccessToken | A personal token if the provider supports it | `string` |
| extraParameters | The extra parameters you may need for the authorization request | `Record<string, string>` |
| bodyEncoding | Specifies the format for sending the body of the request. | `json` \| `url-encoded` |
58 changes: 58 additions & 0 deletions docs/utils-reference/oauth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# OAuth

Dealing with OAuth can be tedious. So we've built a set of utilities to make that task way easier. There's two part to our utilities:

1. Authenticating with the service using [OAuthService](utils-reference/oauth/OAuthService.md) or some built-in providers (e.g GitHub with `OAuthService.github`)
2. Bringing authentication to Raycast commands using [withAccessToken](utils-reference/oauth/withAccessToken.md) and [`getAccessToken`](utils-reference/oauth/withAccessToken.md#getAccessToken)

Here are two different use-cases where you can use the utilities.

## Using a built-in provider

We provide 3rd party providers that you can use out of the box such as GitHub or Linear. Here's how you can use them:

```tsx
import { Detail, LaunchProps } from "@raycast/api";
import { withAccessToken, getAccessToken, OAuthService } from "@raycast/utils";

const github = OAuthService.github({
scopes: "notifications repo read:org read:user read:project"
});

function AuthorizedComponent(props: LaunchProps) {
const { token } = getAccessToken();
return <Detail markdown={`Access token: ${token}`} />;
}

export default withAccessToken(github)(AuthorizedComponent);
```

## Using your own client

```tsx
import { OAuth, Detail, LaunchProps } from "@raycast/api";
import { withAccessToken, getAccessToken, OAuthService } from "@raycast/utils/oauth";

const client = new OAuth.PKCEClient({
redirectMethod: OAuth.RedirectMethod.Web,
providerName: "Your Provider Name",
providerIcon: "provider_icon.png",
providerId: "yourProviderId",
description: "Connect your {PROVIDER_NAME} account",
});

const provider = new OAuthService({
client,
clientId: "YOUR_CLIENT_ID",
scopes: "YOUR SCOPES",
authorizeUrl: "YOUR_AUTHORIZE_URL",
tokenUrl: "YOUR_TOKEN_URL",
});

function AuthorizedComponent(props: LaunchProps) {
const { token } = getAccessToken();
return <Detail markdown={`Access token: ${token}`} />;
}

export default withAccessToken({ authorize: provider.authorize })(AuthorizedComponent);
```
38 changes: 38 additions & 0 deletions docs/utils-reference/oauth/getAccessToken.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# `getAccessToken`

Utility function designed for retrieving authorization tokens within a component. It ensures that your React components have the necessary authentication state, either through OAuth or a personal access token.

{% hint style="info" %}
`getAccessToken` **must** be used within components that are nested inside a component wrapped with [`withAccessToken`](utils-reference/oauth/withAccessToken.md). Otherwise, the function will fail with an error.
{% endhint %}

## Signature

```tsx
function getAccessToken(): {
token: string;
type: "oauth" | "personal";
}
```

### Return

The function returns an object containing the following properties:
- `token`: A string representing the access token.
- `type`: An optional string that indicates the type of token retrieved. It can either be `oauth` for OAuth tokens or `personal` for personal access tokens.

## Example

Here's a simple example:

```tsx
import { Detail } from "@raycast/api";
import { authorize } from "./oauth"

function AuthorizedComponent() {
const { token } = getAccessToken();
return <Detail markdown={`Access token: ${token}`} />;
}

export default withAccessToken({ authorize })(AuthorizedComponent);
```
87 changes: 87 additions & 0 deletions docs/utils-reference/oauth/withAccessToken.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# `withAccessToken`

Higher-order function fetching an authorization token to then access it. This makes it easier to handle OAuth in your different commands whether they're `view` commands, `no-view` commands, or `menu-bar` commands.

## Signature

```tsx
function withAccessToken<T>(
options: WithAccessTokenParameters,
): <U extends (() => Promise<void> | void) | React.ComponentType<T>>(
fnOrComponent: U,
) => U extends () => Promise<void> | void ? Promise<void> : React.FunctionComponent<T>;
```

### Arguments

`options` is an object containing:
- `options.authorize` is a function that initiates the OAuth token retrieval process. It returns a promise that resolves to an access token.
- `options.personalAccessToken` is an optional string that represents an already obtained personal access token. When `options.personalAccessToken` is provided, it uses that token. Otherwise, it calls `options.authorize` to fetch an OAuth token asynchronously.
- `options.client` is an optional instance of a PKCE Client that you can create using Raycast API. This client is used to return the `idToken` as part of the `onAuthorize` callback below.
- `options.onAuthorize` is an optional callback function that is called once the user has been properly logged in through OAuth. This function is called with the `token`, its type (whether it comes from an OAuth flow or if it's a personal access token) and an `idToken` if `options.client` is provided and if it's returned in the initial token set.

### Return

Returns the wrapped component if used in a `view` command or the wrapped function if used in a `no-view` command.

{% hint style="info" %}
Note that the access token isn't injected into the wrapped component props. Instead, it's been set as a global variable that you can get with [getAccessToken](utils-reference/oauth/getAccessToken.md).
{% endhint %}

## Example


{% tabs %}
{% tab title="view.tsx" %}

```tsx
import { Detail } from "@raycast/api";
import { withAccessToken } from "@raycast/utils";
import { authorize } from "./oauth"

function AuthorizedComponent(props) {
return <List ... />;
}

export default withAccessToken({ authorize })(AuthorizedComponent);
```

{% endtab %}

{% tab title="no-view.tsx" %}

```tsx
import { Detail } from "@raycast/api";
import { withAccessToken } from "@raycast/utils";
import { authorize } from "./oauth"

async function AuthorizedCommand() {
await showHUD("Authorized");
}

export default withAccessToken({ authorize })(AuthorizedCommand);
```

{% endtab %}
{% endtabs %}

## Types

### WithAccessTokenParameters

```ts
type OAuthType = "oauth" | "personal";

type OnAuthorizeParams = {
token: string;
type: OAuthType;
idToken: string | null; // only present if `options.client` has been provided
};

type WithAccessTokenParameters = {
client?: OAuth.PKCEClient;
authorize: () => Promise<string>;
personalAccessToken?: string;
onAuthorize?: (params: OnAuthorizeParams) => void;
};
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@raycast/utils",
"version": "1.10.1",
"version": "1.11.0",
"description": "Set of utilities to streamline building Raycast extensions",
"author": "Raycast Technologies Ltd.",
"homepage": "https://developers.raycast.com/utils-reference",
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export * from "./useFrecencySorting";

export * from "./icon";

export * from "./oauth";

export * from "./run-applescript";
export * from "./showFailureToast";

Expand Down
Loading
Loading