125k

認証

プライベートでパーソナライズされたコンポーネントのために、レジストリを認証で保護します。

認証を利用すると、プライベートレジストリを運用したり、コンポーネントへのアクセスを制御したり、チームやユーザーごとに異なるコンテンツを提供したりできます。このガイドでは、一般的な認証パターンとその設定方法を紹介します。

認証によって、以下のユースケースが実現できます。

  • プライベートコンポーネント: ビジネスロジックや社内コンポーネントを安全に保ちます
  • チーム固有のリソース: チームごとに異なるコンポーネントを提供します
  • アクセス制御: 機密性の高いコンポーネントや実験的なコンポーネントを閲覧できる人を制限します
  • 利用状況の分析: 組織内で誰がどのコンポーネントを使っているかを把握します
  • ライセンス管理: プレミアムまたはライセンス付きコンポーネントの配布先を制御します

一般的な認証パターン

トークンベースの認証

最も一般的な方法は、Bearer トークンや API キーを使用する方法です。

components.json
{
  "registries": {
    "@private": {
      "url": "https://registry.company.com/{name}.json",
      "headers": {
        "Authorization": "Bearer ${REGISTRY_TOKEN}"
      }
    }
  }
}

トークンは環境変数に設定します。

.env.local
REGISTRY_TOKEN=your_secret_token_here

API キー認証

レジストリによっては、ヘッダーで API キーを使用します。

components.json
{
  "registries": {
    "@company": {
      "url": "https://api.company.com/registry/{name}.json",
      "headers": {
        "X-API-Key": "${API_KEY}",
        "X-Workspace-Id": "${WORKSPACE_ID}"
      }
    }
  }
}

クエリパラメーター認証

よりシンプルな構成では、クエリパラメーターを使用します。

components.json
{
  "registries": {
    "@internal": {
      "url": "https://registry.company.com/{name}.json",
      "params": {
        "token": "${ACCESS_TOKEN}"
      }
    }
  }
}

この設定では、次の URL が生成されます。https://registry.company.com/button.json?token=your_token

サーバー側の実装

レジストリサーバーに認証を追加する方法は次のとおりです。

Next.js API ルートの例

app/api/registry/[name]/route.ts
import { NextRequest, NextResponse } from "next/server"
 
export async function GET(
  request: NextRequest,
  { params }: { params: { name: string } }
) {
  // Get token from Authorization header.
  const authHeader = request.headers.get("authorization")
  const token = authHeader?.replace("Bearer ", "")
 
  // Or from query parameters.
  const queryToken = request.nextUrl.searchParams.get("token")
 
  // Check if token is valid.
  if (!isValidToken(token || queryToken)) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
  }
 
  // Check if token can access this component.
  if (!hasAccessToComponent(token, params.name)) {
    return NextResponse.json({ error: "Forbidden" }, { status: 403 })
  }
 
  // Return the component.
  const component = await getComponent(params.name)
  return NextResponse.json(component)
}
 
function isValidToken(token: string | null) {
  // Add your token validation logic here.
  // Check against database, JWT validation, etc.
  return token === process.env.VALID_TOKEN
}
 
function hasAccessToComponent(token: string, componentName: string) {
  // Add role-based access control here.
  // Check if token can access specific component.
  return true // Your logic here.
}

Express.js の例

server.js
app.get("/registry/:name.json", (req, res) => {
  const token = req.headers.authorization?.replace("Bearer ", "")
 
  if (!isValidToken(token)) {
    return res.status(401).json({ error: "Unauthorized" })
  }
 
  const component = getComponent(req.params.name)
  if (!component) {
    return res.status(404).json({ error: "Component not found" })
  }
 
  res.json(component)
})

高度な認証パターン

チームベースのアクセス

チームごとに異なるコンポーネントを提供します。

api/registry/route.ts
async function GET(request: NextRequest) {
  const token = extractToken(request)
  const team = await getTeamFromToken(token)
 
  // Get components for this team.
  const components = await getComponentsForTeam(team)
  return NextResponse.json(components)
}

ユーザーごとにパーソナライズされたレジストリ

ユーザーの設定に基づいてコンポーネントを提供します。

async function GET(request: NextRequest) {
  const user = await authenticateUser(request)
 
  // Get user's style and framework preferences.
  const preferences = await getUserPreferences(user.id)
 
  // Get personalized component version.
  const component = await getPersonalizedComponent(params.name, preferences)
 
  return NextResponse.json(component)
}

一時アクセストークン

有効期限付きのトークンを使うと、セキュリティが向上します。

interface TemporaryToken {
  token: string
  expiresAt: Date
  scope: string[]
}
 
async function validateTemporaryToken(token: string) {
  const tokenData = await getTokenData(token)
 
  if (!tokenData) return false
  if (new Date() > tokenData.expiresAt) return false
 
  return true
}

マルチレジストリ認証

名前空間付きレジストリを使うと、認証方法の異なる複数のレジストリを設定できます。

components.json
{
  "registries": {
    "@public": "https://public.company.com/{name}.json",
    "@internal": {
      "url": "https://internal.company.com/{name}.json",
      "headers": {
        "Authorization": "Bearer ${INTERNAL_TOKEN}"
      }
    },
    "@premium": {
      "url": "https://premium.company.com/{name}.json",
      "headers": {
        "X-License-Key": "${LICENSE_KEY}"
      }
    }
  }
}

この構成では、次のことが可能です。

  • パブリックレジストリとプライベートレジストリを混在させます
  • レジストリごとに異なる認証を使用します
  • アクセスレベルごとにコンポーネントを整理します

セキュリティのベストプラクティス

環境変数を使用する

トークンをバージョン管理にコミットしないでください。必ず環境変数を使用します。

.env.local
REGISTRY_TOKEN=your_secret_token_here
API_KEY=your_api_key_here

components.json では、次のように参照します。

{
  "registries": {
    "@private": {
      "url": "https://registry.company.com/{name}.json",
      "headers": {
        "Authorization": "Bearer ${REGISTRY_TOKEN}"
      }
    }
  }
}

HTTPS を使用する

転送中のトークンを保護するため、レジストリには必ず HTTPS の URL を使用します。

{
  "@secure": "https://registry.company.com/{name}.json" // ✅
  "@insecure": "http://registry.company.com/{name}.json" // ❌
}

レート制限を追加する

レジストリを不正利用から保護します。

import rateLimit from "express-rate-limit"
 
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
})
 
app.use("/registry", limiter)

トークンをローテーションする

アクセストークンは定期的に変更します。

// Create new token with expiration.
function generateToken() {
  const token = crypto.randomBytes(32).toString("hex")
  const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30 days.
 
  return { token, expiresAt }
}

アクセスをログに記録する

セキュリティと分析のために、レジストリへのアクセスを記録します。

async function logAccess(request: Request, component: string, userId: string) {
  await db.accessLog.create({
    timestamp: new Date(),
    userId,
    component,
    ip: request.ip,
    userAgent: request.headers["user-agent"],
  })
}

認証のテスト

認証付きレジストリをローカルでテストします。

# Test with curl.
curl -H "Authorization: Bearer your_token" \
  https://registry.company.com/button.json
 
# Test with the CLI.
REGISTRY_TOKEN=your_token npx shadcn@latest add @private/button

エラー処理

shadcn CLI は認証エラーを適切に処理します。

  • 401 Unauthorized: トークンが無効か、不足しています
  • 403 Forbidden: このリソースに対するトークンの権限が不足しています
  • 429 Too Many Requests: レート制限を超過しました

カスタムエラーメッセージ

レジストリサーバーはレスポンスボディにカスタムエラーメッセージを返すことができ、CLI はそれをユーザーに表示します。

// Registry server returns custom error
return NextResponse.json(
  {
    error: "Unauthorized",
    message:
      "Your subscription has expired. Please renew at company.com/billing",
  },
  { status: 403 }
)

ユーザーには次のように表示されます。

Your subscription has expired. Please renew at company.com/billing

この仕組みを使うと、状況に応じたガイダンスを提供できます。

// Different error messages for different scenarios
if (!token) {
  return NextResponse.json(
    {
      error: "Unauthorized",
      message:
        "Authentication required. Set REGISTRY_TOKEN in your .env.local file",
    },
    { status: 401 }
  )
}
 
if (isExpiredToken(token)) {
  return NextResponse.json(
    {
      error: "Unauthorized",
      message: "Token expired. Request a new token at company.com/tokens",
    },
    { status: 401 }
  )
}
 
if (!hasTeamAccess(token, component)) {
  return NextResponse.json(
    {
      error: "Forbidden",
      message: `Component '${component}' is restricted to the Design team`,
    },
    { status: 403 }
  )
}

次のステップ

複数のレジストリでの認証設定や高度なパターンを利用するには、名前空間付きレジストリのドキュメントを参照してください。以下の内容が解説されています。

  • 複数の認証付きレジストリを設定する方法
  • 名前空間ごとに異なる認証を使用する方法
  • レジストリをまたいだ依存関係の解決
  • 高度な認証パターン