OAuth Flow

Integrate authentication for provider that use a standard OAuth flow.

📘

This flow is used by Google and Outlook.


To authenticate users using the OAuth flow:

  1. Create a "Sign In" button that requests an OAuth URL by calling the Start Auth Intent endpoint.
  2. Redirect the user to the OAuth URL returned by the API.
  3. Set up a callback route in your application to receive the redirect after the provider’s OAuth flow is complete.


Get an OAuth URL

In your frontend, create a page with a "Sign In" button.

📘

Branding Guidelines

Make sure to follow the branding rules of the respective providers


Next, create a backend route to handle the button click. This route should:

  • CallStart Auth Intent
    Pass the provider name and the oauth_callback_redirect_uri.
    For example, during local development: http://localhost:3000/oauth-callback. In production, use your actual domain name.
  • Redirect the user to the OauthUrl returned by the API response.

Server code

// index.ts

import express, { Request, Response } from "express";
import cors from "cors";
import parser from "body-parser";
import "dotenv/config";

import { UnipileCustomAuth, UnipileAccounts } from "unipile";

const key = process.env.UNIPILE_API_KEY ?? "";

const customAuthApi = new UnipileCustomAuth({ key });
const accountsApi = new UnipileAccounts({ key });

const app = express();

app.use(cors());

app.use(parser.json());
app.use(parser.urlencoded({ extended: true }));
app.use(express.json());

app.post("/auth-intent", async (req: Request, res: Response) => {
  const response = await customAuthApi.startAuthIntent({
    body: req.body,
  });

  // Handle Errors
  if (response.error) {
    res.status(response.response.status).send(response.error);
    return;
  }

  res.send(response.data);
});

app.listen(3000, () => {
  console.log("Server is running on port 3000");
});
curl --request POST \
     --url https://api.unipile.com/v2/auth/intent \
     --header 'X-API-KEY: api-key' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --data '
{
  "provider": "google",
  "oauth_callback_redirect_uri": "http\\://localhost:3000/oauth-callback"
}
'
# main.py

import json
import os
from typing import Any, Optional

import unipile
import uvicorn
from dotenv import load_dotenv
from fastapi import Body, FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, PlainTextResponse, Response
from unipile.api.accounts_api import AccountsApi
from unipile.api.custom_auth_api import CustomAuthApi
from unipile.exceptions import ApiException

load_dotenv()

key = os.getenv("UNIPILE_API_KEY", "")

configuration = unipile.Configuration()
if key:
    configuration.api_key["apiKey"] = key
api_client = unipile.ApiClient(configuration)

custom_auth_api = CustomAuthApi(api_client)
accounts_api = AccountsApi(api_client)

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


def _to_response_data(value: Any) -> Any:
    if hasattr(value, "to_dict") and callable(value.to_dict):
        return value.to_dict()
    return value


def _api_exception_to_body(exception: ApiException) -> Any:
    if exception.data is not None:
        return _to_response_data(exception.data)

    if exception.body:
        try:
            return json.loads(exception.body)
        except json.JSONDecodeError:
            return exception.body

    return exception.reason or "Unknown error"


def _error_response(exception: ApiException) -> Response:
    status_code = exception.status or 500
    body = _api_exception_to_body(exception)
    if isinstance(body, (dict, list)):
        return JSONResponse(status_code=status_code, content=body)
    return PlainTextResponse(status_code=status_code, content=str(body))


@app.post("/auth-intent")
def auth_intent(body: Any = Body(...)) -> Response:
    try:
        params = custom_auth_api._start_auth_intent_serialize(
            start_auth_intent_request=body,
            _request_auth=None,
            _content_type=None,
            _headers=None,
            _host_index=0,
        )
        response_data = custom_auth_api.api_client.call_api(*params)
        response_data.read()
        response = custom_auth_api.api_client.response_deserialize(
            response_data=response_data,
            response_types_map={"200": "SolveCheckpoint200Response"},
        ).data
        return JSONResponse(content=_to_response_data(response))
    except ApiException as exception:
return _error_response(exception)

if __name__ == "__main__":
    uvicorn.run("src.main:app", host="0.0.0.0", port=4000, reload=True)

Client code

// lib/unipile.ts

import type {
  StartAuthIntentData,
  StartAuthIntentResponse,
} from "unipile";

export async function startAuthIntent(body: StartAuthIntentData) {
  const response = await fetch("http://localhost:4000/auth-intent", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });

  if (!response.ok) {
    const error = await response.text();
    throw new Error(error);
  }

  const data = await response.json();

  return data as StartAuthIntentResponse;
}


// components/containers/OAuthFlowContainer.tsx

import { startAuthIntent } from "../../lib/unipile";

export function OAuthFlowContainer({
  provider,
}: {
  provider: "outlook" | "google";
}) {
  const handleClick = async () => {
    const response = await startAuthIntent({
      provider,
      oauth_callback_redirect_uri: "http://localhost:3000/oauth-callback",
    });
    

    // Redirect the user to the given url
    if (response.object === "OauthUrl") {
      window.location.href = response.url;
    }
  };

  return <button onClick={handleClick}>Sign in with {provider}</button>;
}

// App.tsx

import { OAuthFlowContainer } from "./components/containers/OAuthFlowContainer";

export function App() {
  return <OAuthFlowContainer provider="google" />
}


Create a callback route

After completing the OAuth flow, the provider will redirect the user to your callback route.
This route should be a GET endpoint that accepts the following query parameters:

keydescription
account_idID of the linked account
providerProvider of the linked account
state?(Optional) The same state you passed to Start Auth Intent , which you can use to validate the authenticity of the call.

Once this route is hit, you should:

  • Redirect the user to a success screen
  • CallGet an Account using the account_id to retrieve and display additional account details.

Server code

// index.ts

//...

app.get("/oauth-callback", async (req: Request, res: Response) => {
  const account_id = req.query.account_id;
  if (typeof account_id !== "string") {
    res.status(400).send("Missing account_id");
    return;
  }
  
  // Get the full Account object from the account_id given in the query
  const response = await accountsApi.getAccount({ path: { account_id } });

  // Handle Errors
  if (response.error) {
    res.status(response.response.status).send(response.error);
    return;
  }

  res.send(
    `Account ${response.data?.name ?? response.data?.id} is successfully authenticated`,
  );
});
# main.py

# ...

@app.get("/oauth-callback")
def oauth_callback(account_id: Optional[str] = None) -> Response:
    if account_id is None:
        return PlainTextResponse(status_code=400, content="Missing account_id")

    try:
        response = accounts_api.get_account(account_id)
    except ApiException as exception:
        return _error_response(exception)

    account_name = response.name or response.id
    return PlainTextResponse(
        content=f"Account {account_name} is successfully authenticated"
    )
    
# ...

You need a server to redirect users

Handle callback errors

If the account cannot be linked with unipile, the error is given as query parameters. This route should also accept the following:

keydescription
error_typeType of the error. See Error responses
error_titleTitle of the error
error_detailDetail of the error

Then, you can handle errors gracefully:


Server code

// index.ts

//...

app.get("/oauth-callback", async (req: Request, res: Response) => {
  let q = url.parse(req.url, true).query;
  
  if (q.error_type) {
    console.log('Error:' + q.error_title);
    
    if (q.error_type === "api/already_exists") {
      // Handle duplicate accounts
      console.log("Existing account", q.error_detail) // acc_xxxxx
    }

    if (q.error_type === "api/restricted_account") {
      // Handle restricted accounts
      console.log("Reason", q.error_detail) // reason
    }
    
    // Handle unexpected errors


  } else {
    // Get the full Account object from the account_id given in the query
    const response = await unipile.accounts.getAccount(
      q.account_id
    );

    // Handle error
    if (response.error || !response.data) {
      console.error(response.error);
      res
        .status(response.error?.status || 500)
        .send(response.error?.type || "Internal Server Error");

      return;
    }

    res.send(`Account ${response.data.name} is successfully authenticated`);
  }  
});
You need a server to redirect users