@ecauth/auth-js 共有 JavaScript パッケージ 実装計画
クライアントサイド認証 JS ライブラリの実装計画。npm パッケージ構成、API、配信戦略を含む。
概要
EC-CUBE プラグイン(4系・2系)および将来のマルチプラットフォーム展開で共通利用するクライアントサイド JavaScript を npm パッケージとして管理する。
| 項目 | 値 |
|---|---|
| パッケージ名 | @ecauth/auth-js |
| レジストリ | npmjs.com(公開パッケージ) |
| リポジトリ | EcAuth/ecauth-auth-js |
| ライセンス | LGPL-2.1-or-later |
| 初回リリーススコープ | WebAuthn(B2B パスキー用、既存 webauthn.js の
TypeScript 化) |
| 将来スコープ | B2C パスキー、PKCE ユーティリティ、OAuth2 リダイレクトヘルパー |
背景
現在 ec-cube4-ecauth/Resource/assets/js/webauthn.js
に実装されている WebAuthn ヘルパーは、フレームワーク非依存の純粋な
JavaScript であり、以下のすべてのユースケースで共通利用できる:
| 機能 | Phase | 必要な JS |
|---|---|---|
| B2B パスキー(EC-CUBE 4系) | Phase 1 | WebAuthn authenticate/register |
| B2B パスキー(EC-CUBE 2系) | Phase 1 | 同上 |
| B2C パスキー | Phase 2 | 同上(パラメータが異なるのみ) |
| B2C OIDC フェデレーション | Phase 3 | PKCE code_verifier/code_challenge 生成(薄い) |
| B2B SSO | Phase 4 | リダイレクトのみ(JS 最小限) |
| WordPress / Magento 等 | Phase 5 | 上記すべて |
npmjs.com を選択した理由
GitHub Packages ではなく npmjs.com を採用する:
- 認証不要: GitHub Packages は読み取りにも PAT
が必要だが、npmjs.com の公開パッケージは
npm installだけで取得可能 - CI の簡素化:
.npmrcの認証設定やトークン管理が不要 - 消費者が限定的: パッケージを
npm installするのは自チームのビルドプロセスのみ。EC-CUBE プラグインのエンドユーザーはプラグイン zip に同梱された UMD ファイルを利用する - 非公開にする意味がない: クライアントサイド JS はプラグインに同梱されて公開される
Step 1: リポジトリ作成・プロジェクト構成
1.1 GitHub リポジトリ作成
gh repo create EcAuth/ecauth-auth-js --public \
--description "EcAuth client-side authentication library (WebAuthn, PKCE)"1.2 ディレクトリ構成
ecauth-auth-js/
├── src/
│ ├── index.ts # エクスポート
│ ├── webauthn.ts # WebAuthn authenticate/register
│ └── base64url.ts # Base64URL encode/decode
├── tests/
│ ├── webauthn.test.ts # WebAuthn ユニットテスト
│ └── base64url.test.ts # Base64URL ユニットテスト
├── dist/ # ビルド成果物(gitignore)
│ ├── ecauth-auth.esm.js # ESM(モダンバンドラー向け)
│ ├── ecauth-auth.umd.js # UMD(<script> タグ直接読み込み)
│ └── index.d.ts # 型定義
├── package.json
├── tsconfig.json
├── vite.config.ts # ライブラリモードビルド
├── vitest.config.ts # テスト設定
├── .github/workflows/
│ ├── ci.yml # lint + テスト
│ └── publish.yml # npm publish(Release 公開時)
├── .gitignore
├── CLAUDE.md
├── README.md
└── LICENSE
1.3 package.json
{
"name": "@ecauth/auth-js",
"version": "0.1.0",
"description": "EcAuth client-side authentication library (WebAuthn, PKCE)",
"license": "LGPL-2.1-or-later",
"type": "module",
"main": "dist/ecauth-auth.umd.js",
"module": "dist/ecauth-auth.esm.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/ecauth-auth.esm.js",
"require": "./dist/ecauth-auth.umd.js",
"types": "./dist/index.d.ts"
}
},
"files": ["dist"],
"scripts": {
"build": "vite build",
"test": "vitest run",
"test:watch": "vitest",
"lint": "tsc --noEmit"
},
"devDependencies": {
"typescript": "^6.0",
"vite": "^6.0",
"vite-plugin-dts": "^4.0",
"vitest": "^3.0"
}
}1.4 vite.config.ts
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
export default defineConfig({
plugins: [dts({ rollupTypes: true })],
build: {
lib: {
entry: 'src/index.ts',
name: 'EcAuth',
formats: ['es', 'umd'],
fileName: (format) => `ecauth-auth.${format === 'es' ? 'esm' : 'umd'}.js`,
},
},
});1.5 tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2020", "DOM"],
"strict": true,
"declaration": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}Step 2: 既存 webauthn.js の TypeScript 化
2.1 src/base64url.ts
現在の webauthn.js から Base64URL 関数を分離:
/**
* ArrayBuffer を Base64URL 文字列にエンコードする。
*/
export function base64UrlEncode(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
/**
* Base64URL 文字列を ArrayBuffer にデコードする。
*/
export function base64UrlDecode(str: string): ArrayBuffer {
let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
const padding = base64.length % 4;
if (padding) {
base64 += '===='.substring(padding);
}
const binary = atob(base64);
const buffer = new ArrayBuffer(binary.length);
const bytes = new Uint8Array(buffer);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return buffer;
}2.2 src/webauthn.ts
IIFE パターンから ESM エクスポートに変換。型定義を追加:
import { base64UrlEncode, base64UrlDecode } from './base64url';
export interface AuthenticateOptions {
optionsUrl: string;
verifyUrl: string;
csrfToken?: string;
headers?: Record<string, string>;
}
export interface RegisterOptions {
optionsUrl: string;
verifyUrl: string;
b2bSubject: string;
csrfToken?: string;
headers?: Record<string, string>;
deviceName?: string;
}
export interface AuthenticateResult {
redirect_url: string;
}
export interface RegisterResult {
success: boolean;
credential_id: string;
}
function buildHeaders(csrfToken?: string, extra?: Record<string, string>): Record<string, string> {
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (csrfToken) {
headers['X-CSRF-TOKEN'] = csrfToken;
}
return { ...headers, ...extra };
}
/**
* パスキー認証を実行する。
*/
export async function authenticate(options: AuthenticateOptions): Promise<AuthenticateResult> {
const headers = buildHeaders(options.csrfToken, options.headers);
// 1. 認証オプション取得
const optionsResponse = await fetch(options.optionsUrl, {
method: 'POST',
headers,
body: JSON.stringify({}),
});
if (!optionsResponse.ok) {
throw new Error(`Failed to get authentication options: ${optionsResponse.status}`);
}
const serverOptions = await optionsResponse.json();
// 2. WebAuthn API 用にデータ変換
const publicKeyOptions: PublicKeyCredentialRequestOptions = {
challenge: base64UrlDecode(serverOptions.challenge),
rpId: serverOptions.rpId,
userVerification: serverOptions.userVerification || 'preferred',
timeout: serverOptions.timeout || 60000,
};
if (serverOptions.allowCredentials?.length > 0) {
publicKeyOptions.allowCredentials = serverOptions.allowCredentials.map(
(cred: { id: string; type: PublicKeyCredentialType; transports?: AuthenticatorTransport[] }) => ({
id: base64UrlDecode(cred.id),
type: cred.type,
transports: cred.transports || [],
}),
);
}
// 3. ブラウザ認証ダイアログ表示
const assertion = (await navigator.credentials.get({ publicKey: publicKeyOptions })) as PublicKeyCredential;
const assertionResponse = assertion.response as AuthenticatorAssertionResponse;
// 4. 認証結果をサーバーに送信
const assertionData: Record<string, unknown> = {
response: {
id: assertion.id,
rawId: base64UrlEncode(assertion.rawId),
response: {
authenticatorData: base64UrlEncode(assertionResponse.authenticatorData),
clientDataJSON: base64UrlEncode(assertionResponse.clientDataJSON),
signature: base64UrlEncode(assertionResponse.signature),
...(assertionResponse.userHandle && {
userHandle: base64UrlEncode(assertionResponse.userHandle),
}),
},
type: assertion.type,
clientExtensionResults: assertion.getClientExtensionResults(),
},
};
const verifyResponse = await fetch(options.verifyUrl, {
method: 'POST',
headers,
body: JSON.stringify(assertionData),
});
if (!verifyResponse.ok) {
throw new Error(`Authentication verification failed: ${verifyResponse.status}`);
}
return verifyResponse.json();
}
/**
* パスキー登録を実行する。
*/
export async function register(options: RegisterOptions): Promise<RegisterResult> {
const headers = buildHeaders(options.csrfToken, options.headers);
// 1. 登録オプション取得
const requestBody: Record<string, string> = { b2b_subject: options.b2bSubject };
if (options.deviceName) {
requestBody.device_name = options.deviceName;
}
const optionsResponse = await fetch(options.optionsUrl, {
method: 'POST',
headers,
body: JSON.stringify(requestBody),
});
if (!optionsResponse.ok) {
throw new Error(`Failed to get registration options: ${optionsResponse.status}`);
}
const serverOptions = await optionsResponse.json();
// 2. WebAuthn API 用にデータ変換
const publicKeyOptions: PublicKeyCredentialCreationOptions = {
challenge: base64UrlDecode(serverOptions.challenge),
rp: { id: serverOptions.rp.id, name: serverOptions.rp.name },
user: {
id: base64UrlDecode(serverOptions.user.id),
name: serverOptions.user.name,
displayName: serverOptions.user.displayName,
},
pubKeyCredParams: serverOptions.pubKeyCredParams,
authenticatorSelection: serverOptions.authenticatorSelection || {},
timeout: serverOptions.timeout || 60000,
attestation: serverOptions.attestation || 'none',
};
if (serverOptions.excludeCredentials?.length > 0) {
publicKeyOptions.excludeCredentials = serverOptions.excludeCredentials.map(
(cred: { id: string; type: PublicKeyCredentialType; transports?: AuthenticatorTransport[] }) => ({
id: base64UrlDecode(cred.id),
type: cred.type,
transports: cred.transports || [],
}),
);
}
// 3. ブラウザ登録ダイアログ表示
const credential = (await navigator.credentials.create({ publicKey: publicKeyOptions })) as PublicKeyCredential;
const credentialResponse = credential.response as AuthenticatorAttestationResponse;
// 4. 登録結果をサーバーに送信
const transports = credentialResponse.getTransports ? credentialResponse.getTransports() : [];
const credentialData: Record<string, unknown> = {
response: {
id: credential.id,
rawId: base64UrlEncode(credential.rawId),
response: {
attestationObject: base64UrlEncode(credentialResponse.attestationObject),
clientDataJSON: base64UrlEncode(credentialResponse.clientDataJSON),
transports,
},
type: credential.type,
clientExtensionResults: credential.getClientExtensionResults(),
},
};
if (options.deviceName) {
credentialData.device_name = options.deviceName;
}
const verifyResponse = await fetch(options.verifyUrl, {
method: 'POST',
headers,
body: JSON.stringify(credentialData),
});
if (!verifyResponse.ok) {
throw new Error(`Registration verification failed: ${verifyResponse.status}`);
}
return verifyResponse.json();
}2.3 src/index.ts
export { base64UrlEncode, base64UrlDecode } from './base64url';
export * as webauthn from './webauthn';
export type {
AuthenticateOptions,
AuthenticateResult,
RegisterOptions,
RegisterResult,
} from './webauthn';Step 3: テスト
3.1 vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
},
});3.2 tests/base64url.test.ts
Base64URL エンコード/デコードのラウンドトリップテスト。ブラウザ API 不要のためユニットテストで網羅。
3.3 tests/webauthn.test.ts
fetch と navigator.credentials
をモックし、authenticate/register
のリクエスト構造・エラーハンドリングを検証。
Step 4: CI/CD
4.1 .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm ci
- run: npm run lint
- run: npm run test
- run: npm run build4.2 .github/workflows/publish.yml
GitHub Release 公開時に npm publish を実行:
name: Publish
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm run build
- run: npm run test
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}4.3 npmjs.com の設定
- npmjs.com で
ecauthOrganization を作成 - Automation トークンを生成し、GitHub リポジトリの
NPM_TOKENシークレットに設定 --provenanceフラグで npm provenance(GitHub Actions からのビルド証明)を付与
Step 5: 既存プラグインへの導入
5.1 ec-cube4-ecauth での利用
cd ec-cube4-ecauth
pnpm add @ecauth/auth-jsビルドスクリプトの追加(package.json):
{
"scripts": {
"build:js": "cp node_modules/@ecauth/auth-js/dist/ecauth-auth.umd.js Resource/assets/js/ecauth-auth.umd.js"
}
}テンプレート修正(login_passkey.twig):
<!-- 修正前 -->
<script src="{{ asset('bundles/ecauthlogin43/js/webauthn.js') }}"></script>
<script>
EcAuthWebAuthn.authenticate(optionsUrl, verifyUrl, csrfToken);
</script>
<!-- 修正後 -->
<script src="{{ asset('bundles/ecauthlogin43/js/ecauth-auth.umd.js') }}"></script>
<script>
EcAuth.webauthn.authenticate({
optionsUrl: optionsUrl,
verifyUrl: verifyUrl,
csrfToken: csrfToken
});
</script>リリースパッケージ(deploy.yml): UMD ファイルを
Resource/assets/js/ に配置してプラグイン zip
に同梱。エンドユーザーは npm を使わない。
5.2 ec-cube2-ecauth での利用(将来)
<script src="/plugin/ecauth/js/ecauth-auth.umd.js"></script>
<script>
EcAuth.webauthn.authenticate({
optionsUrl: '<!--{$ecauth_options_url}-->',
verifyUrl: '<!--{$ecauth_verify_url}-->',
csrfToken: '<!--{$transactionid}-->'
});
</script>5.3 WordPress / Magento 等での利用(将来)
import { webauthn } from '@ecauth/auth-js';
await webauthn.authenticate({ optionsUrl, verifyUrl });Step 6: 将来の拡張
各 Phase で必要に応じてモジュールを追加する。
Phase 2: B2C パスキー
WebAuthn の authenticate / register
はそのまま利用可能。B2C 固有のパラメータ(user_type
等)がある場合は options インターフェースを拡張。
Phase 3: PKCE ユーティリティ
// src/pkce.ts
export async function generateCodeVerifier(): Promise<string>;
export async function generateCodeChallenge(verifier: string): Promise<string>;OIDC フェデレーション時にクライアントサイドで PKCE パラメータを生成する場合に使用。ただし EC-CUBE プラグインではサーバーサイドで PKCE を処理する設計のため、優先度は低い。
Phase 4: B2B SSO
SSO はサーバーサイドリダイレクトが主体のため、JS の追加は最小限。必要に応じてリダイレクトヘルパーを追加。
API 互換性方針
| バージョン | 互換性ルール |
|---|---|
| 0.x | Breaking changes 許容(初期開発期間) |
| 1.0 | Semantic Versioning 準拠。Breaking changes はメジャーバージョンアップ |
UMD ビルドのグローバル名 EcAuth は変更しない。
依存関係図
@ecauth/auth-js (npm)
↑ devDependency
├── ec-cube4-ecauth (EC-CUBE 4系プラグイン)
│ └── UMD を Resource/assets/js/ にコピー → プラグイン zip に同梱
├── ec-cube2-ecauth (EC-CUBE 2系プラグイン、将来)
│ └── 同上
└── wordpress-ecauth (WordPress プラグイン、将来)
└── ESM import または UMD