Skip to content

Commit

Permalink
Add OAuth utils
Browse files Browse the repository at this point in the history
  • Loading branch information
mathieudutour authored and thomaslombart committed Jan 9, 2024
1 parent 87d8d13 commit b2fae42
Show file tree
Hide file tree
Showing 15 changed files with 1,522 additions and 905 deletions.
3 changes: 3 additions & 0 deletions docs/utils-reference/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
- [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)
- [React hooks](utils-reference/react-hooks/README.md)
- [useCachedState](utils-reference/react-hooks/useCachedState.md)
- [usePromise](utils-reference/react-hooks/usePromise.md)
Expand Down
102 changes: 102 additions & 0 deletions docs/utils-reference/oauth/OAuthService.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# `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.

## Initialization

Construct an instance of a provider-specific `OAuthService` with the required parameters:

- `client`: The PKCE Client defined using `OAuth.PKCEClient` from `@raycast/api`
- `clientId`: The app's client ID.
- `scope`: The scope of the access requested from the provider.
- `authorizeUrl`: The URL to start the OAuth flow.
- `tokenUrl`: The URL to exchange the authorization code for an access token.
- `refreshTokenUrl`: (Optional) The URL to refresh the access token if applicable.
- `personalAccessToken`: (Optional) A personal token if the provider supports it.
- `extraParameters`: (Optional) The extra parameters you may need for the authorization request.
- `bodyEncoding`: (Optional) Specifies the format for sending the body of the request. Can be `json` for JSON-encoded body content or `url-encoded` for URL-encoded form data.

### 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",
});
```

## Subclassing

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

Here's an example where `LinearOAuthService` subclasses `OAuthService`:

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

## Built-in 3rd-party providers

Some 3rd-party providers subclassing `OAuthService` are exposed by default to make it easy to authenticate with them. Here's the full list:

- `AsanaOAuthService`
- `GitHubOAuthService`
- `GoogleOAuthService`
- `JiraOAuthService`
- `LinearOAuthService`
- `SlackOAuthService`
- `ZoomOAuthService`

### Example

Here's a basic example of how you'd use `GitHubOAuthService`:

```tsx
import { GitHubOAuthService } from "@your/library";

const github = new GitHubOAuthService({ scope: "notifications repo read:org read:user read:project" });
```

Note that you can also use your own client ID and provide a personal access token if needed:

```tsx
import { GitHubOAuthService } from "@your/library";

const preferences = getPreferenceValues<Preferences>()

const github = new GitHubOAuthService({
clientId: "YOUR_CLIENT_ID",
personalAccessToken: preferences.token,
scope: "notifications repo read:org read:user read:project"
});
```
62 changes: 62 additions & 0 deletions docs/utils-reference/oauth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# OAuth

Authenticating with OAuth in Raycast extensions is 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) and providers we provide out of the box (e.g GitHub with `GitHubOAuthService`)
2. Bringing authentication to React components 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, GitHubOAuthService } from "@raycast/utils";

const github = new GitHubOAuthService({
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, getAuthorizeFunction } 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);
```

## Additional information

If you need more information, please take a look at the subpages: [OAuthService](utils-reference/oauth/OAuthService.md) and [withAccessToken](utils-reference/oauth/withAccessToken.md)
111 changes: 111 additions & 0 deletions docs/utils-reference/oauth/withAccessToken.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# `withAccessToken`

Higher-order component fetching an authorization token to then access it. This makes it easier to handle OAuth in your different commands/components.

## Signature

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

type OnAuthorizeParams = {
token: string;
type: OAuthType;
idToken: string | null;
};

type WithAccessTokenParameters = {
client?: OAuth.PKCEClient;
authorize: () => Promise<string>;
personalAccessToken?: string;
onAuthorize?: (params: OnAuthorizeParams) => void;
};

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.client` is an instance of a PKCE Client that you can create using Raycast API.
- `options.authorize` is a promise that initiates the OAuth token retrieval process and resolves to a token string.
- `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.onAuthorize` is a callback function that is called with the `token`, its `type`, and its `idToken` once the user has been properly logged in through OAuth.

### 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`.
{% endhint %}

## Example

```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);
```

Note that it also works for `no-view` commands as stated above:

```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);
```

## `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`.
{% 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);
```
2 changes: 1 addition & 1 deletion src/icon/progress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function getProgressIcon(
color: Color | string = Color.Red,
options?: { background?: Color | string; backgroundOpacity?: number },
): Image.Asset {
const background = options?.background || (environment.appearance === "light" ? "black" : "white");
const background = options?.background || (environment.theme === "light" ? "black" : "white");
const backgroundOpacity = options?.backgroundOpacity || 0.1;

const stroke = 10;
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

0 comments on commit b2fae42

Please sign in to comment.