diff --git a/docs/guides/auth/authconnection.md b/docs/guides/auth/authconnection.md index 2f60c1e..a0c0605 100644 --- a/docs/guides/auth/authconnection.md +++ b/docs/guides/auth/authconnection.md @@ -21,67 +21,137 @@ AuthConnection provides a secure way to: - OAuth2 application configured with your chosen provider - Basic understanding of OAuth2 flows - Function service enabled (for runtime integration) +- An SDK project with a `tailor.config.ts` — see the [SDK Quickstart](/sdk/quickstart) -## CLI Commands +## Setup Flow -The `tailorctl workspace authconnection` command manages OAuth2 connections in your workspace. +An auth connection can be managed two ways: through your **SDK config** (`defineAuth()` in `tailor.config.ts`, deployed with the rest of your app) or entirely through the **Console**. Either way, setting up a connection is two steps: -### Create an Auth Connection +1. **Create** the connection (registers the OAuth2 provider credentials) +2. **Authorize** the connection (runs the OAuth2 flow to obtain and store tokens) -```bash -tailorctl workspace authconnection create \ - --name \ - --type TYPE_OAUTH2 \ - --provider-url \ - --issuer-url \ - --client-id \ - --client-secret +A given connection can only be managed by one side at a time — see [Choosing a Management Method](#choosing-a-management-method) below. + +### Option A: Manage via SDK Config + +#### 1. Configure the Connection + +Add a `connections` block to `defineAuth()` in `tailor.config.ts`: + +```typescript +import { defineAuth } from "@tailor-platform/sdk"; + +export const auth = defineAuth("my-auth", { + // ...other auth config + connections: { + "google-connection": { + type: "oauth2", + providerUrl: "https://accounts.google.com", + issuerUrl: "https://accounts.google.com", + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + }, + }, +}); ``` -**Parameters:** +Store the client ID and secret in your `.env` file or CI secrets — never commit them. + +**Connection config fields:** -- `--name` (required): Unique connection name (lowercase, alphanumeric and hyphens only) -- `--type` (required): Authentication type (currently only `TYPE_OAUTH2` supported) -- `--provider-url` (required): OAuth2 provider URL -- `--issuer-url` (optional): Token issuer URL for JWT validation -- `--client-id` (required): OAuth2 client ID from the provider -- `--client-secret` (required): OAuth2 client secret from the provider +| Field | Type | Required | Description | +| -------------- | -------- | -------- | --------------------------------------------- | +| `type` | `string` | Yes | Connection type. Currently only `"oauth2"`. | +| `providerUrl` | `string` | Yes | OAuth2 provider URL. | +| `issuerUrl` | `string` | Yes | Token issuer URL for JWT validation. | +| `clientId` | `string` | Yes | OAuth2 client ID from the provider. | +| `clientSecret` | `string` | Yes | OAuth2 client secret from the provider. | +| `authUrl` | `string` | No | Override for the authorization endpoint. | +| `tokenUrl` | `string` | No | Override for the token endpoint. | -### List Auth Connections +Deploy to register the connection with the platform: ```bash -tailorctl workspace authconnection list +tailor-sdk deploy ``` -### Authorize an Auth Connection +This creates (or updates) the connection record. A newly created connection exists but is not yet authorized — it has no tokens yet. + +> [!NOTE] +> Deploy updates existing connections **in-place**, preserving the OAuth token. If a change requires re-authorization (for example, the provider URL or client ID changed), the deploy will warn you to run `authconnection authorize` again. + +> [!TIP] +> Once a connection has been deployed at least once, later deploys can pass `clientSecret: ""` (an empty string) without erasing the secret already stored on the platform — an empty value is simply left out of the update, so nothing about the secret changes. This lets a CI pipeline redeploy an existing connection (for example, to pick up a `providerUrl` change) without ever having access to the real secret. The very first deploy that creates the connection still needs the real secret at least once, from wherever that value is available. +> +> `clientSecret` is a required field, so it must resolve to an actual string — `undefined` fails config validation before deploy even runs. Use a fallback so an unset variable still resolves to a string: +> +> ```typescript +> clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? "", // Falls back to "" when the var is unset, e.g. in CI +> ``` +> +> With the `.env` file itself, an explicit empty assignment (`GOOGLE_CLIENT_SECRET=`) also works, since that sets the variable to `""` rather than leaving it unset. + +#### 2. Authorize the Connection ```bash -tailorctl workspace authconnection authorize \ - --scopes \ - --port +tailor-sdk authconnection authorize --name google-connection \ + --scopes "openid,profile,email" \ + --port 8080 ``` **Parameters:** -- `connection-name` (required): Name of the existing auth connection -- `--scopes` (optional): OAuth2 scopes to request (default: openid,profile,email) -- `--port` (optional): Local callback server port (default: 8080) +- `--name` (required): Name of the connection defined in `defineAuth()` +- `--scopes` (optional): OAuth2 scopes to request (default: `openid,profile,email`) +- `--port` (optional): Local callback server port (default: `8080`) - `--no-browser` (optional): Don't open browser automatically This command: -1. Starts a local HTTP server for OAuth2 callback +1. Starts a local HTTP server for the OAuth2 callback 2. Opens your browser to the provider's authorization page 3. Handles the callback after authorization -4. Exchanges the authorization code for tokens using stored client secret +4. Exchanges the authorization code for tokens using the client secret from your config 5. Stores tokens securely on the server -### Revoke an Auth Connection +Alternatively, run `tailor-sdk authconnection open` to authorize the connection from the Console instead of the local CLI flow — useful on a machine without browser access: ```bash -tailorctl workspace authconnection revoke +tailor-sdk authconnection open ``` +Verify the connection is authorized: + +```bash +tailor-sdk authconnection list +``` + +### Option B: Manage Entirely via the Console + +No `tailor.config.ts` changes are needed — create, authorize, and manage the connection directly in the Tailor Platform Console: + +```bash +tailor-sdk authconnection open +``` + +This opens the workspace's connections settings page, where you can: + +1. **Create** a new connection — enter the type, provider URL, issuer URL, client ID, and client secret directly in the Console form +2. **Authorize** it — the Console walks you through the OAuth2 consent flow in the browser +3. View its status, re-authorize, revoke its tokens, or delete it later from the same page + +This is a good fit for connections you don't want tracked in git — for example, ones that differ per developer, or that only ever target a single shared workspace and don't need to travel with the rest of your app's config. + +### Choosing a Management Method + +A connection name is owned by exactly one side at a time — the same connection cannot be managed by both the Console and your SDK config simultaneously: + +- A connection created via the Console (Option B) carries no SDK ownership label. `tailor-sdk deploy` leaves it completely untouched as long as it isn't also declared in `connections`. +- If you later add a connection with the **same name** to `defineAuth()`'s `connections` and run `deploy`, the SDK finds it already exists without an ownership label and pauses to ask whether it should take it over ("Allow tailor-sdk to manage these resources?"). Confirming adopts it into config management — from that point on, every `deploy` overwrites its fields to match your config, and removing it from `connections` deletes the connection. (`--yes` confirms automatically, which is useful for CI but means this adoption happens without a prompt.) +- Until you confirm that handover, `deploy` refuses to proceed, so a Console-managed connection is never silently overwritten by config just because a connection with the same name appears in `tailor.config.ts`. + +In short: use the Console (Option B) for connections you intend to manage by hand, and SDK config (Option A) for connections you want defined, reviewed, and deployed alongside the rest of your app. Don't mix the two for the same connection name. + ## Provider Configuration Examples ### Google OAuth2 @@ -92,18 +162,24 @@ First, create OAuth2 credentials in [Google Cloud Console](https://console.cloud 2. Create OAuth 2.0 Client ID 3. Configure authorized redirect URIs +```typescript +connections: { + "google-oauth": { + type: "oauth2", + providerUrl: "https://accounts.google.com", + issuerUrl: "https://accounts.google.com", + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + }, +}, +``` + ```bash -# 1. Create the connection -tailorctl workspace authconnection create \ - --name google-oauth \ - --type TYPE_OAUTH2 \ - --provider-url "https://accounts.google.com" \ - --issuer-url "https://accounts.google.com" \ - --client-id "YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com" \ - --client-secret "YOUR_GOOGLE_CLIENT_SECRET" +# 1. Deploy the connection +tailor-sdk deploy # 2. Authorize and get tokens -tailorctl workspace authconnection authorize google-oauth \ +tailor-sdk authconnection authorize --name google-oauth \ --scopes "https://www.googleapis.com/auth/admin.directory.user.readonly" ``` @@ -122,17 +198,24 @@ First, register an application in [Azure Portal](https://portal.azure.com/): 3. Configure redirect URIs under "Authentication" 4. Create client secret under "Certificates & secrets" +```typescript +connections: { + "ms365-oauth": { + type: "oauth2", + providerUrl: "https://login.microsoftonline.com/YOUR_TENANT_ID", + issuerUrl: "https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0", + clientId: process.env.AZURE_CLIENT_ID!, + clientSecret: process.env.AZURE_CLIENT_SECRET!, + }, +}, +``` + ```bash -tailorctl workspace authconnection create \ - --name ms365-oauth \ - --type TYPE_OAUTH2 \ - --provider-url "https://login.microsoftonline.com/YOUR_TENANT_ID" \ - --issuer-url "https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0" \ - --client-id "YOUR_AZURE_APP_CLIENT_ID" \ - --client-secret "YOUR_AZURE_APP_CLIENT_SECRET" +# 1. Deploy the connection +tailor-sdk deploy # 2. Authorize and get tokens -tailorctl workspace authconnection authorize ms365-oauth \ +tailor-sdk authconnection authorize --name ms365-oauth \ --scopes "https://graph.microsoft.com/.default" ``` @@ -153,14 +236,20 @@ First, create an app in [Intuit Developer Portal](https://developer.intuit.com/) 3. Configure redirect URIs 4. Get your client ID and secret from "Keys & OAuth" +```typescript +connections: { + "quickbooks-oauth": { + type: "oauth2", + providerUrl: "https://appcenter.intuit.com/connect/oauth2", + issuerUrl: "https://oauth.platform.intuit.com/op/v1", + clientId: process.env.QUICKBOOKS_CLIENT_ID!, + clientSecret: process.env.QUICKBOOKS_CLIENT_SECRET!, + }, +}, +``` + ```bash -tailorctl workspace authconnection create \ - --name quickbooks-oauth \ - --type TYPE_OAUTH2 \ - --provider-url "https://appcenter.intuit.com/connect/oauth2" \ - --issuer-url "https://oauth.platform.intuit.com/op/v1" \ - --client-id "YOUR_QUICKBOOKS_CLIENT_ID" \ - --client-secret "YOUR_QUICKBOOKS_CLIENT_SECRET" +tailor-sdk deploy ``` **Common QuickBooks OAuth2 URLs:** @@ -183,10 +272,14 @@ AuthConnection integrates seamlessly with the Function service, allowing your fu ### Basic Usage -```javascript +Use `authconnection.getConnectionToken()` from `@tailor-platform/sdk/runtime` to retrieve the current access token by connection name. When `connections` is defined in `defineAuth()`, the connection name is type-checked and autocompleted against the defined keys: + +```typescript +import { authconnection } from "@tailor-platform/sdk/runtime"; + export default async () => { // Get access token for a connection - const tokens = await tailor.authconnection.getConnectionToken("google-oauth"); + const tokens = await authconnection.getConnectionToken("google-oauth"); // Use the access token to call external APIs const response = await fetch("https://www.googleapis.com/oauth2/v2/userinfo", { @@ -204,14 +297,22 @@ export default async () => { }; ``` +```typescript +// authconnection.getConnectionToken("unknown-connection"); // ❌ TypeScript error +``` + +Type narrowing is provided by the generated `tailor.d.ts` (the `ConnectionNameRegistry` interface). Run `tailor-sdk generate` (or `deploy`) after defining new connections to refresh it. Before the first generate, or when `connections` is not defined in `defineAuth()`, `getConnectionToken()` accepts any string. + ### Advanced Usage with Error Handling -```javascript +```typescript +import { authconnection } from "@tailor-platform/sdk/runtime"; + export default async () => { try { // Get tokens for multiple connections - const googleTokens = await tailor.authconnection.getConnectionToken("google-oauth"); - const msTokens = await tailor.authconnection.getConnectionToken("ms365-oauth"); + const googleTokens = await authconnection.getConnectionToken("google-oauth"); + const msTokens = await authconnection.getConnectionToken("ms365-oauth"); // Call Google API const googleResponse = await fetch("https://www.googleapis.com/oauth2/v2/userinfo", { @@ -242,23 +343,36 @@ export default async () => { ### Token Properties -The `getConnectionToken()` method returns an object with the following properties: +The `getConnectionToken()` method returns an object with the following property: The OAuth2 access token for API calls - - The refresh token (if available from provider) - +## Managing Connections via CLI - - Token expiration timestamp in ISO format - +These commands work on any connection regardless of which option created it: - - Token type (typically "Bearer") - +```bash +# Open the connections page in the Console +tailor-sdk authconnection open + +# Authorize (opens browser for OAuth2 flow) +tailor-sdk authconnection authorize --name + +# List all connections +tailor-sdk authconnection list + +# Revoke a connection's tokens (keeps the connection; re-authorize later) +tailor-sdk authconnection revoke --name + +# Delete a connection entirely +tailor-sdk authconnection delete --name +``` + +To delete a connection managed via SDK config (Option A), remove it from `connections` and redeploy instead of using `authconnection delete` directly — manage each connection through one method only, as described above. + +For connections not defined in `defineAuth()`'s `connections`, `authconnection.getConnectionToken()` still accepts any string, so you can read tokens for Console-managed connections the same way — you just lose the type-checked autocompletion. ## Best Practices @@ -279,24 +393,22 @@ Use descriptive names that indicate the provider and environment: ### Environment-Specific Configurations -For development vs production environments, create separate connections: +Since each environment is deployed to its own workspace from the same `tailor.config.ts`, keep a single connection definition and vary the value via a per-environment `.env` file — the same pattern used for any other environment-specific value (see [Multi-Environment Configuration](/sdk/multi-environment)): + +```typescript +connections: { + "google-oauth": { + type: "oauth2", + providerUrl: "https://accounts.google.com", + issuerUrl: "https://accounts.google.com", + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + }, +}, +``` ```bash -# Development -tailorctl workspace authconnection create \ - --name google-oauth-dev \ - --type TYPE_OAUTH2 \ - --provider-url "https://accounts.google.com" \ - --client-id "DEV_CLIENT_ID.apps.googleusercontent.com" \ - --client-secret "DEV_CLIENT_SECRET" - -# Production -tailorctl workspace authconnection create \ - --name google-oauth-prod \ - --type TYPE_OAUTH2 \ - --provider-url "https://accounts.google.com" \ - --client-id "PROD_CLIENT_ID.apps.googleusercontent.com" \ - --client-secret "PROD_CLIENT_SECRET" +tailor-sdk deploy -w --env-file .env.production ``` ## Troubleshooting @@ -322,6 +434,11 @@ tailorctl workspace authconnection create \ - Implement refresh token flow in your application - Monitor token expiration times +**Re-authorization Required After Deploy** + +- If `tailor-sdk deploy` warns that a connection needs re-authorization, run `tailor-sdk authconnection authorize --name ` again +- This happens when identity-changing fields (`providerUrl`, `issuerUrl`, `clientId`, `type`) are modified + **Function Runtime Errors** - Ensure the connection name exists and is authorized @@ -330,6 +447,9 @@ tailorctl workspace authconnection create \ ## Next Steps +- [Setting up Auth Connections tutorial](/tutorials/setup-auth/setup-auth-connections) - Step-by-step SDK walkthrough +- [Auth Service SDK reference](/sdk/services/auth#auth-connections) - Full auth connections API reference +- [Runtime API](/sdk/runtime#namespaces) - `@tailor-platform/sdk/runtime` namespaces, including `authconnection` - [Learn about Function service](/guides/function/overview) - Understand Function runtime capabilities - [Secret Manager documentation](/guides/secretmanager) - Secure secret storage patterns - [Auth service overview](/guides/auth/overview) - Complete authentication system diff --git a/docs/tutorials/setup-auth/setup-auth-connections.md b/docs/tutorials/setup-auth/setup-auth-connections.md index d31f67a..1926d00 100644 --- a/docs/tutorials/setup-auth/setup-auth-connections.md +++ b/docs/tutorials/setup-auth/setup-auth-connections.md @@ -4,6 +4,8 @@ Auth connections enable your application to authenticate with external OAuth2 pr - To follow along, first complete the [SDK Quickstart](../../sdk/quickstart) and [Setting up Auth](overview). +This tutorial manages the connection through SDK config (`defineAuth()`), which is one of two supported approaches. You can instead create, authorize, and manage a connection entirely from the Console with `tailor-sdk authconnection open` — see [Setup Flow](/guides/auth/authconnection#setup-flow) in the AuthConnection guide for both options and why a given connection should only be managed by one of them. + ## What you'll build A connection to Google's API that your resolvers and functions can use to call Google services without managing OAuth2 tokens manually. @@ -58,16 +60,16 @@ Store `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` in your `.env` file or CI se ### 2. Deploy the Connection -Run `tailor-sdk apply` to register the connection with the platform: +Run `tailor-sdk deploy` to register the connection with the platform: ```bash -tailor-sdk apply +tailor-sdk deploy ``` This creates the connection record. The connection exists but is not yet authorized (it has no tokens yet). -::: info Hash-based change detection -The SDK only re-deploys connections whose config has changed since the last `apply`. Delete `.tailor-sdk/` to force all connections to re-sync. +::: info In-place updates +Redeploying updates an existing connection **in-place**, preserving the OAuth token. If a change requires re-authorization (for example, the provider URL or client ID changed), `deploy` will warn you to run `authconnection authorize` again. ::: ### 3. Authorize the Connection @@ -81,6 +83,12 @@ tailor-sdk authconnection authorize --name google-connection \ This opens a browser tab for the OAuth2 consent screen. After you approve, the platform exchanges the authorization code for tokens and stores them securely. Your app code never handles the tokens directly. +You can also run `tailor-sdk authconnection open` to authorize from the Console instead of the local CLI flow: + +```bash +tailor-sdk authconnection open +``` + Verify the connection is authorized: ```bash @@ -89,12 +97,12 @@ tailor-sdk authconnection list ### 4. Use the Connection Token at Runtime -Use `auth.getConnectionToken()` in a resolver, executor, or workflow to retrieve the current access token: +Use `authconnection.getConnectionToken()` from `@tailor-platform/sdk/runtime` in a resolver, executor, or workflow to retrieve the current access token: ```typescript // resolvers/fetch-google-profile.ts import { createResolver, t } from "@tailor-platform/sdk"; -import { auth } from "../tailor.config"; +import { authconnection } from "@tailor-platform/sdk/runtime"; export default createResolver({ name: "fetchGoogleProfile", @@ -105,7 +113,7 @@ export default createResolver({ name: t.string(), }), body: async () => { - const tokens = await auth.getConnectionToken("google-connection"); + const tokens = await authconnection.getConnectionToken("google-connection"); const response = await fetch("https://www.googleapis.com/oauth2/v2/userinfo", { headers: { @@ -119,12 +127,14 @@ export default createResolver({ }); ``` -The connection name is **type-checked**. If you rename or remove a connection from `defineAuth()`, TypeScript will flag any call sites immediately. +The connection name is **type-checked** against the connections defined in `defineAuth()`. If you rename or remove a connection, TypeScript will flag any call sites immediately. ```typescript -// auth.getConnectionToken("unknown-connection"); // ❌ TypeScript error +// authconnection.getConnectionToken("unknown-connection"); // ❌ TypeScript error ``` +Type narrowing comes from the generated `tailor.d.ts`. Run `tailor-sdk generate` (or `deploy`) after adding or renaming connections to refresh it. + ### 5. Manage Connections via CLI ```bash @@ -146,7 +156,7 @@ Here's a full executor that calls the Google Calendar API whenever a meeting rec ```typescript // executor/sync-to-google-calendar.ts import { createExecutor, recordCreatedTrigger } from "@tailor-platform/sdk"; -import { auth } from "../tailor.config"; +import { authconnection } from "@tailor-platform/sdk/runtime"; import { meeting } from "../db/meeting"; export default createExecutor({ @@ -156,7 +166,7 @@ export default createExecutor({ operation: { kind: "function", body: async ({ newRecord }) => { - const tokens = await auth.getConnectionToken("google-connection"); + const tokens = await authconnection.getConnectionToken("google-connection"); await fetch("https://www.googleapis.com/calendar/v3/calendars/primary/events", { method: "POST",