The interface is localized. Skill descriptions and documents are machine-translated by AI into your language; code, commands, paths and technical terms stay in the original, and the author's document governs.
Explore skills
Data & research · SKILL.md

plaid-authentication

plaid-authentication

Implements authentication strategies using the Plaid API (Link token flow, Item public token exchange, Auth data retrieval) for secure user bank account verification in financial applications.

Independent GitHub snapshot · Automated classification · Original documentation governs

Skill documentation

GitHub

name: plaid-authentication description: Implements authentication strategies using the Plaid API (Link token flow, Item public token exchange, Auth data retrieval) for secure user bank account verification in financial applications. license: MIT compatibility: opencode metadata: version: "1.0.0" domain: payments triggers: plaid authentication, plaid link token, bank account verification, plaid auth, public token exchange, item access token, secure login, user verification archetypes: tactical anti_triggers: - brainstorming - vague ideation response_profile: verbosity: low directive_strength: high abstraction_level: operational role: implementation scope: implementation output-format: code related-skills: plaid-transactions, plaid-identity


Plaid API Authentication

Implements secure user authentication and bank account linking via the Plaid API. Covers the Link token creation flow, public token exchange to obtain access tokens, and Auth endpoint data retrieval for income verification and identity matching.

TL;DR Checklist

  • Always create Link Tokens with a unique client_user_id per end user
  • Exchange public_token immediately in a server-side callback — never expose it client-side
  • Store access tokens encrypted (AES-256) and rotate on credential changes
  • Request only the auth product for authentication-only flows to minimize permissions

When to Use

Use this skill when:

  • You need users to securely link their bank accounts via Plaid Link without exposing credentials
  • Building Know-Your-Customer (KYC) or account verification workflows that rely on bank auth data
  • Implementing instant bank verification for loan origination, payment processing, or wealth management apps
  • Retrieving Auth data (account balances, transaction history summaries, direct deposit info) after Link session completion

When NOT to Use

Avoid this skill for:

  • Direct payment processing — use Plaid Transaction Processor or a payment gateway instead
  • Identity document verification (passport, driver's license) — use Plaid Identity with identity product
  • Cryptocurrency account linking — Plaid does not support crypto assets

Core Workflow

  1. Initialize Plaid Client — Configure client ID, secret, environment (sandbox, development, production), and supported products.
  2. Create Link Token — Generate a server-side link token scoped to the user session with the auth product.
  3. Render Plaid Link — Pass the token to the frontend SDK; the user authenticates with their financial institution.
  4. Exchange Public Token — On completion, exchange the temporary public_token for a persistent access_token.
  5. Retrieve Auth Data — Call /auth/get to verify identity and extract account-level data.

Implementation Patterns

Pattern 1: Production-Grade Link Token Service

import plaid
from dataclasses import dataclass
from typing import Optional, List
from datetime import datetime


@dataclass
class PlaidConfig:
    client_id: str
    secret: str
    environment: str = "sandbox"
    products: List[str] = None
    country_codes: List[str] = None
    language: str = "en"

    def __post_init__(self):
        if self.products is None:
            self.products = ["auth"]
        if self.country_codes is None:
            self.country_codes = ["US"]


class PlaidAuthService:
    """Service for managing Plaid authentication flows."""

    def __init__(self, config: PlaidConfig):
        self.client = plaid.Client(
            client_id=config.client_id,
            secret=config.secret,
            environment=getattr(plaid.Environment, config.environment),
        )
        self.config = config

    def create_link_token(self, user_id: str, web_redirect_url: Optional[str] = None) -> dict:
        """Create a Plaid Link token for a specific user."""
        request = {
            "user": {
                "client_user_id": user_id,
            },
            "client_name": "My Financial App",
            "products": self.config.products,
            "country_codes": self.config.country_codes,
            "language": self.config.language,
        }

        if web_redirect_url:
            request["redirect_uri"] = web_redirect_url
            request["auth_mode"] = "redirect"

        response = self.client.LinkTokenCreate(request)
        return {"link_token": response["link_token"], "expires_at": datetime.utcnow().isoformat()}

    def exchange_public_token(
        self, public_token: str
    ) -> dict:
        """Exchange a Plaid public token for an access token and item ID."""
        response = self.client.ItemPublicTokenExchange({"public_token": public_token})
        return {
            "access_token": response["access_token"],
            "item_id": response["item_id"],
            "new_access_token": response.get("new_access_token"),  # If token rotation occurred
        }

    def get_auth_data(self, access_token: str) -> dict:
        """Retrieve authentication data for an item (balances, identity, accounts)."""
        auth_response = self.client.AuthGet(access_token=access_token)
        return {
            "accounts": auth_response["accounts"],
            "balances": auth_response["balances"],
            "transactions_count": auth_response.get("transactions_count", 0),
            "direct_deposits": auth_response.get("direct_deposit", None),
        }

    def refresh_access_token(self, access_token: str) -> dict:
        """Refresh an access token that may have expired due to credential changes."""
        response = self.client.ItemPublicTokenExchange({"access_token": access_token})
        return {"new_access_token": response.get("new_access_token")}

Pattern 2: Secure Token Storage with Encryption

from cryptography.fernet import Fernet
import json
import os


class SecureTokenStore:
    """Stores Plaid access tokens using AES-256 encryption."""

    def __init__(self, db):
        self.db = db
        key = os.environ.get("PLAAD_TOKEN_ENCRYPTION_KEY")
        if not key:
            raise ValueError("PLAID_TOKEN_ENCRYPTION_KEY environment variable is required")
        self.cipher = Fernet(key.encode())

    def store_access_token(self, user_id: str, access_token: str, item_id: str):
        """Encrypt and store an access token."""
        payload = {
            "access_token": access_token,
            "item_id": item_id,
            "created_at": datetime.utcnow().isoformat(),
        }
        encrypted = self.cipher.encrypt(json.dumps(payload).encode())
        self.db.execute(
            "INSERT INTO plaid_tokens (user_id, encrypted_token) VALUES (?, ?)",
            (user_id, encrypted),
        )

    def get_access_token(self, user_id: str) -> Optional[str]:
        """Retrieve and decrypt an access token."""
        row = self.db.execute("SELECT encrypted_token FROM plaid_tokens WHERE user_id = ?", (user_id,))
        if not row:
            return None
        decrypted = self.cipher.decrypt(row["encrypted_token"])
        payload = json.loads(decrypted)
        return payload["access_token"]

Pattern 3: Webhook Handler for Auth Events

from flask import Flask, request, jsonify
import hashlib
import hmac

app = Flask(__name__)


def verify_plaid_webhook_signature(payload_bytes: bytes, signature: str, webhook_version: str) -> bool:
    """Verify that the webhook originated from Plaid."""
    secret = os.environ["PLAID_WEBHOOK_SECRET"]
    expected = hmac.new(secret.encode(), payload_bytes, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)


@app.route("/webhook/plaid", methods=["POST"])
def plaid_webhook():
    """Handle Plaid webhook notifications for auth-related events."""
    signature = request.headers.get("X-Plaid-Signature", "")
    webhook_version = request.headers.get("X-Plaid-Version", "None")
    payload_bytes = request.get_data()

    if not verify_plaid_webhook_signature(payload_bytes, signature, webhook_version):
        return jsonify({"error": "Invalid signature"}), 401

    event = request.json
    event_type = event.get("event_id")
    item_id = event.get("item_id")

    if event["webhook_type"] == "ITEM":
        if event["event_code"] == "DEFAULT_UPDATE":
            # Credentials changed — trigger token refresh
            refresh_plaid_item(item_id)
        elif event["event_code"] in ("CONNECT_EXPIRED", "INVALID_CREDENTIALS"):
            notify_user_to_reauthenticate(item_id)
        elif event["event_code"] == "LOGIN_FAILURE":
            log_failed_login_attempt(item_id)

    return jsonify({"status": "ok"}), 200

Error Handling Reference

Error CodeCauseAction
ITEM_LOGIN_REQUIREDUser credentials expiredTrigger re-auth via Link
RATE_LIMIT_EXCEEDEDToo many API callsBack off; respect Retry-After header
INVALID_TOKENExpired or revoked access tokenRe-exchange public token or prompt new Link session
PRODUCT_NOT_ENABLEDProduct not in link token requestAdd product to initial LinkTokenCreate request
USER_AUTHORIZE_FAILEDUser denied authorization in LinkLog and allow re-attempt with fresh Link Token

Constraints

MUST DO

  • Always create a new Link Token per user session — never share tokens across users.
  • Exchange public_token server-side immediately after Link completion; it expires in 30 minutes.
  • Encrypt access tokens at rest using AES-256 (Fernet or equivalent).
  • Register webhook endpoints to handle ITEM events (credential changes, deauthorization).
  • Validate the Plaid webhook signature using HMAC-SHA256 before processing any event.

MUST NOT DO

  • Store unencrypted access tokens in databases, logs, or environment variables.
  • Hardcode client secrets, API keys, or webhook secrets in source code.
  • Use sandbox credentials in production — always validate with production environment.
  • Share access tokens with frontend clients — they grant full read/write access to financial data.
  • Ignore ITEM webhook events — credential changes invalidate your existing access token.

Output Template

When implementing Plaid authentication, output must contain:

  1. Link Token Configuration — Products, country codes, redirect URI settings
  2. Token Exchange Logic — Public token → access token conversion with error handling
  3. Storage Strategy — Encryption method and database schema for access tokens
  4. Webhook Handler — Signature verification and event routing logic

Related Skills

SkillPurpose
plaid-transactionsRetrieve and analyze transaction history after authentication
plaid-identityVerify user identity data from bank accounts post-authentication

Live References

Authoritative documentation links for this skill's domain. The model follows markdown links to resolve external references and inline content.

Saved skills