@gasboost/auth
v0.11.0
Published
Modern authentication for Google Apps Script with email/password, Apps Script authentication, sessions, and password reset.
Maintainers
Readme
@gasboost/auth
Google Apps Script 向けの認証ライブラリです。
@gasboost/auth は、GAS Web App の公開設定とは独立して、アプリケーション上の User / Account / Identity / Session を扱います。
初期実装では以下の認証方式を提供します。
- Email / Password
- Apps Script Active User
また、Session の保存先として以下を利用できます。
CacheServicePropertiesService
Install
pnpm add @gasboost/authnpm:
npm install @gasboost/authConcepts
認証 Identity と Application User は直接同一視しません。
ProviderIdentity
↓
Account
↓
User
↓
Session1つの User に複数の Account を紐付けることができます。
User
├─ Account(emailPassword)
└─ Account(appsScript)Identity は (provider, accountId) の組み合わせで識別します。
そのため、同じメールアドレスでも認証方式が異なれば別の Identity として扱えます。
(emailPassword, [email protected])
(appsScript, [email protected])Repository
@gasboost/auth は特定の Database / ORM に依存しません。
利用側で AppsScriptAuthRepository を実装して注入します。
import type { AppsScriptAuthRepository } from "@gasboost/auth";
const repository: AppsScriptAuthRepository = {
account: {
async findByIdentity(provider, identifier) {
// Account を検索
return null;
},
},
user: {
async find(id) {
// User を検索
return null;
},
async create(user) {
// User + Account を保存
return user;
},
},
};将来的に SheetORM 向け adapter は auth-core とは別 package として提供する想定です。
Authorization
アプリケーション内部の認可には、GAS runtime に依存しない AuthorizationPolicy と、Backend service の AppsScriptAuthorization を利用できます。
import {
AppsScriptAuthorization,
createAuthorizationTables,
} from "@gasboost/auth";
import { AuthorizationPolicy } from "@gasboost/auth/authorization";
export const authorizationPolicy = new AuthorizationPolicy({
project: ["read", "create", "update", "delete"],
authorization: ["manage"],
} as const)
.linkRole("admin", {
project: ["read", "create", "update", "delete"],
authorization: ["manage"],
})
.linkRole("member", {
project: ["read"],
});
const authorizationTables = createAuthorizationTables();
const authorization = new AppsScriptAuthorization({
policy: authorizationPolicy,
repository: authorizationRepository,
});@gasboost/auth/authorization は pure な subpath export です。Frontend / shared code では AuthorizationPolicy だけを import し、Backend で取得した effective permissions の表示制御に同じ can() を使えます。
const permissions = await authorization.resolve(session.userId);
authorizationPolicy.can(permissions, {
project: ["update"],
});Role definition と permission vocabulary は application code が source of truth です。Repository には assignment だけを保存し、未知の role / permission は grant せず fail closed になります。
Initialize
AppsScriptAuth に Repository、GAS runtime、Session 設定、認証方式の設定を渡します。
import { AppsScriptAuth } from "@gasboost/auth";
const pepper =
PropertiesService.getScriptProperties().getProperty("AUTH_PEPPER") ?? "";
const auth = new AppsScriptAuth({
repository,
runtime: {
utilities: Utilities,
session: Session,
cacheService: CacheService,
propertiesService: PropertiesService,
},
session: {
storageType: "cache",
expiresIn: 60 * 20 * 1000,
},
emailPassword: {
enabled: true,
isSignupEnabled: true,
pepper,
},
appsScript: {
enabled: true,
isSignupEnabled: true,
},
});runtime を constructor injection するため、auth-core 内部では GAS global を直接参照しません。
これにより、テスト環境では @gasboost/fake-core / @gasboost/fake-node などの fake implementation を注入できます。
Session Storage
session.expiresIn の単位はミリ秒(ms)で、省略時は20分(1,200,000ms)です。CacheService に保存する際は、TTLを秒に変換します。
CacheService
session: {
storageType: "cache",
expiresIn: 60 * 20 * 1000,
}CacheService の TTL を利用して Session を保持します。
低レイテンシな Session Storage として利用できます。
PropertiesService
session: {
storageType: "properties",
expiresIn: 60 * 20 * 1000,
}PropertiesService では Session を以下の key 形式で保存します。
session:<sessionId>そのため、同じ Properties に保存された API_KEY などの別データとは分離されます。
期限切れ Session は cleanup() で削除できます。
Email / Password Sign Up
const { user, session } = await auth.signUp.email({
name: "Taro",
email: "[email protected]",
password: "password",
});処理フロー:
email identity の重複確認
↓
password hash
↓
EmailPasswordIdentity
↓
Account
↓
User
↓
Session 発行Email / Password Sign In
const { user, session } = await auth.signIn.email({
email: "[email protected]",
password: "password",
});処理フロー:
email
↓
Account 検索
↓
password verify
↓
User 検索
↓
Session 発行Registration (without Session)
auth.registration は、Session を発行せずに
User と Account を登録するサーバー内部APIです。
管理者によるユーザー登録など、 登録対象ユーザーとしてサインインする必要がない場合に使用します。
Email / Password
const user = await auth.registration.email({
name: "Taro",
email: "[email protected]",
password: temporaryPassword,
});Apps Script
const user = await auth.registration.appsScript({
name: "Taro",
email: "[email protected]",
});Registration は指定された対象者の新規 User と最初の Account を作成します。 Session は参照しません。同じ provider / email の重複は拒否し、 別方式の User とメールが一致しても自動統合しません。
Sign Upとの違い
| API | User + Account | Session発行 |
| -------------------------------- | -------------- | ----------- |
| auth.registration.email() | 作成 | なし |
| auth.registration.appsScript() | 作成 | なし |
| auth.signUp.email() | 作成 | あり |
| auth.signUp.appsScript() | 作成 | あり |
isSignupEnabled: false でも、
認証方式自体が enabled: true ならRegistrationは利用できます。
Registrationはメール送信やRole付与を行いません。 呼び出し元で認証・認可を実施してください。
Apps Script Sign Up
Apps Script 認証では Session.getActiveUser().getEmail() を Identity として利用します。
const { user, session } = await auth.signUp.appsScript({
name: "Taro",
});Active User の email を取得できない場合は登録できません。
Apps Script Sign In
const { user, session } = await auth.signIn.appsScript({});処理フロー:
Session.getActiveUser().getEmail()
↓
AppsScriptIdentity
↓
Account 検索
↓
User 検索
↓
Session 発行Authentication Hooks
@gasboost/auth は、認証成功後に外部処理を実行するための lifecycle hook を提供します。
現在は afterSignIn を利用できます。Sign In と、Session を自動発行する Sign Up の
email / Apps Script 両経路で、Session 保存後に同じ Hook を1回 await します。
import { AppsScriptAuth, type EmailPasswordAuthOptions } from "@gasboost/auth";
type HookResult = {
readonly customToken: string;
};
const auth = new AppsScriptAuth<EmailPasswordAuthOptions, HookResult>({
repository,
runtime,
session: {
storageType: "cache",
},
emailPassword: {
enabled: true,
pepper,
},
hooks: {
afterSignIn: ({ user, session }) => ({
customToken: `${user.id}:${session.id}`,
}),
},
});afterSignIn には、認証された User と発行済みの Session が渡されます。
hooks: {
afterSignIn: ({ user, session }) => {
user.id;
session.id;
return {
customToken: "...",
};
},
},hook の戻り値は Sign In / Sign Up result の hooks から取得できます。
const result = await auth.signIn.email({
email: "[email protected]",
password: "password",
});
result.user;
result.session;
result.hooks.customToken;hook result の型は AppsScriptAuth の第2型引数として指定できます。
type HookResult = {
readonly customToken: string;
};
const auth = new AppsScriptAuth<EmailPasswordAuthOptions, HookResult>({
// ...
});hook を設定しない場合、従来どおり Sign In / Sign Up result は user と session のみを返します。
const auth = new AppsScriptAuth({
repository,
runtime,
session: {
storageType: "cache",
},
});
const result = await auth.signIn.appsScript({});
result.user;
result.session;Async Hook
afterSignIn は async 処理にも対応しています。
hooks: {
afterSignIn: async ({ user, session }) => {
const value = await createExternalCredential({
user,
session,
});
return {
value,
};
},
},Hook Failure
afterSignIn は Sign In / Sign Up 処理の一部として扱われます。
処理順序は以下です。
authentication
↓
User
↓
Session 発行
↓
Session 保存
↓
afterSignIn
↓
Sign In resultafterSignIn で例外が発生した場合、Sign In / Sign Up 全体が失敗します。
その際、すでに保存された Session は SessionStorage から削除されます。 削除が失敗しても元の Hook エラーを返します。Sign Up で作成済みの User / Account は削除しません。 登録は完了しているため、再認証には Sign In を使い、Sign Up を自動再試行しないでください。
afterSignIn error
↓
Session 削除
↓
error を呼び出し元へ伝播これにより、外部認証情報などの生成に失敗したにもかかわらず、Gasboost 側の Session だけが有効な状態になることを防ぎます。
Package Boundary
hooks API 自体は Firebase やその他の外部サービスを認識しません。
@gasboost/auth
↓
afterSignIn
↓
external integration例えば Firebase Custom Token を発行する場合でも、Firebase 固有の token generation は @gasboost/auth の責務には含めません。
@gasboost/auth
↓
afterSignIn
↓
Firebase integration package
↓
Firebase Custom Token@gasboost/auth は認証 lifecycle と拡張ポイントのみを提供し、外部サービス固有の処理は別 package から hook として接続します。
Session
Get Session
const session = await auth.session.get(sessionId);
if (!session) {
// Session が存在しない、または期限切れ
}Session は以下の情報を持ちます。
session.id;
session.userId;
session.createdAt;
session.expiresAt;Cleanup Expired Sessions
await auth.session.cleanup();PropertiesService 使用時は期限切れ Session を削除します。
CacheService は自身で expiration を管理するため、cleanup は no-op です。
Sign Out
await auth.signOut.execute(sessionId);対象 Session を SessionStorage から削除します。
Configuration
Email / Password
emailPassword: {
enabled: true,
isSignupEnabled: true,
pepper: "...",
}enabled が true の場合、pepper は必須です。
空文字または空白だけの pepper を指定すると AppsScriptAuth の初期化時に失敗します。
new AppsScriptAuth({
// ...
emailPassword: {
enabled: true,
pepper: "",
},
});Pepper is required when email and password authentication is enabledisSignupEnabled を false にすると Sign In は許可したまま Sign Up だけを無効化できます。
emailPassword: {
enabled: true,
isSignupEnabled: false,
pepper: "...",
}Apps Script
appsScript: {
enabled: true,
isSignupEnabled: true,
}こちらも Sign In と Sign Up を個別に制御できます。
Password
Email / Password 認証では以下を利用します。
- per-password salt
- application pepper
- iterations
- HMAC-SHA256 を利用した反復処理
現在の default iterations は 300 です。
pepper は User / Account の保存先とは分離し、Script Properties などで管理することを推奨します。
const pepper =
PropertiesService.getScriptProperties().getProperty("AUTH_PEPPER");Testing
GAS API を利用するテストでは gasboost/fake を使用できます。
pnpm add -D \
@gasboost/fake-core \
@gasboost/fake-node例:
import {
InMemoryCacheService,
InMemoryPropertiesService,
InMemorySession,
} from "@gasboost/fake-core";
import { NodeUtilities } from "@gasboost/fake-node";AppsScriptAuth の runtime に fake implementation をそのまま注入できます。
const auth = new AppsScriptAuth({
repository,
runtime: {
utilities: new NodeUtilities(),
session: fakeSession,
cacheService: new InMemoryCacheService(),
propertiesService: new InMemoryPropertiesService(),
},
session: {
storageType: "cache",
},
});Scope
現在の auth-core が提供するもの:
- User / Account / Session
- ProviderIdentity
- Email / Password authentication
- Apps Script Active User authentication
- Sign In
- Sign Up
- Sign Out
- Authentication lifecycle hooks (
afterSignIn) - Session management
- CacheService SessionStorage
- PropertiesService SessionStorage
- GAS runtime dependency injection
以下は auth-core の責務外として後続 package / Issue で扱います。
- Schema / modelName / field mapping
- database session storage
- plugin system
- admin plugin
- Google OAuth
- organization / multi-tenant
- permission model
- MCP OAuth
- frontend UI
@gasboost/appmiddleware integration
License
MIT
既存 User への Account 追加(0.9.0)
registration.methods は設定の enabled を返します。
auth.account.add(userId, input) は明示した既存 User に Account を追加して返します。
新規 User や Session は作成せず、isSignupEnabled に依存しません。
同一 Identity の重複、他 User の Identity、同じ User の同一方式の追加は拒否します。
対象の認証方式は enabled: true が必要です。
// 呼び出し元で初期化権限・実行主体を検証した後に実行
const email = Session.getEffectiveUser().getEmail();
const user = auth.registration.methods.appsScript
? await auth.registration.appsScript({ name: "システム管理者", email })
: await auth.registration.email({
name: "システム管理者",
email,
password: temporaryPassword,
});
if (
auth.registration.methods.appsScript &&
auth.registration.methods.emailPassword
) {
await auth.account.add(user.id, {
provider: "emailPassword",
email,
password: temporaryPassword,
});
}
// Apps Script Account を既存 User に追加する場合:
// await auth.account.add(userId, { provider: "appsScript", email });メールとパスワードの Account は新しい Account ID を salt としてパスワードをハッシュ化します。
既存 Account や User 名は変更しません。
呼び出し元で対象 User への追加権限を検証してください。
@gasboost/auth-app の handlers(auth) はこの管理用 API を RPC に公開しません。
公開 signUp.appsScript({ name }) は引き続き ActiveUser を登録し、Session を発行します。
Repository と移行
0.9.0 では registration.appsScript({ name }) を { name, email } に変更しました。
0.8.0 の registration.ensure() と任意の repository.user.save() は削除しました。
旧 PR の EffectiveUser 専用登録経路も提供しません。
通常の registration.email() と公開 SignUp の契約は維持します。
Account 追加には任意の Repository メソッド account.create(account): Promise<Account> が必要です。
未対応の Repository は既存 API を継続利用でき、account.add 呼び出し時にエラーになります。
実装は存在する User への追加だけを許可し、Account ID・Identity・User ごとの同一方式の重複を
保存と同じ transaction 内で再検証してください。@gasboost/auth-sheetorm >=0.4.1 が対応します。
複数 API にまたがる初期化の排他制御、途中失敗時の保存済み User ID の復旧や
再開処理は呼び出し元(Bootstrap 等)の責務です。
認証済みユーザーの拡張(0.10.0)
user.data に同期・非同期の読み取り処理を指定すると、戻り値の型が推論されます。
認可、従業員、組織、プロフィールなど consumer 固有のサービスを利用できます。
const auth = new AppsScriptAuth({
repository,
runtime,
session: { storageType: "properties" },
appsScript: { enabled: true, isSignupEnabled: true },
user: {
data: async ({ user }) => ({
permissions: await authorization.resolve(user.id),
}),
},
});
const result = await auth.signIn.appsScript({});
result.user.data.permissions; // authorization.resolve の戻り値型
const restored = await auth.session.getWithUser(result.session.id);
restored?.user.data.permissions;4種の SignIn / SignUp と session.getWithUser(id) は同じ拡張ユーザー型を返します。
拡張処理は毎回実行し、User / Session の保存スキーマを変更しません。
user.data 未指定なら User の型・ログイン応答は従来どおりです。
session.get(id) は既存の Session | null のままで、拡張処理を実行しません。
getWithUser(id) は Session の既存フィールドに user を加えた応答を返します。
無効・期限切れのセッションや取得できない User は null になります。
拡張取得エラーは呼び出し元へ伝播し、無効な認証を示す null と区別します。
SignIn / SignUp はデータ取得後に Session を保存し、hooks.afterSignIn を実行します。
取得失敗時に Session は発行しません。SignUp で作成済みの User は残るため、復旧後は SignIn してください。
復元時の取得失敗では既存 Session は保持し、afterSignIn は実行しません。
既存の hook 戻り値(Firebase customToken 等)は result.hooks に維持されます。
実際の RPC の認可はバックエンドで毎回検証してください。
Auth errors
ブラウザとサーバーで共通のコードを使う場合は、GAS・Node API・認証実装に
依存しない @gasboost/auth/errors を import してください。
フロントエンドで @gasboost/auth のルートを import する必要はありません。
import {
AuthError,
AUTH_ERROR_CODES,
type AuthErrorCode,
} from "@gasboost/auth/errors";
const code: AuthErrorCode = AUTH_ERROR_CODES.FORBIDDEN;
throw new AuthError({ code, message: "Forbidden" });AuthError の message には、クライアントに公開してよい内容だけを渡します。
| Code | 既知の失敗 | Message |
| ------------------------ | ----------------------------------------------------------------------------- | ------------------------------ |
| INVALID_CREDENTIALS | メールの未登録、パスワード不一致、Apps Script の active user / アカウントなし | Invalid credentials |
| UNAUTHORIZED | auth-app の metadata 不正、session なし・期限切れ | Unauthorized |
| FORBIDDEN | auth-app の権限不足 | Forbidden |
| INVALID_PASSWORD_RESET | パスワードリセットトークンの不正・期限切れ・使用済み | Invalid password reset token |
| REGISTRATION_FAILED | 登録の重複 | Registration failed |
認証失敗のコード・メッセージではアカウントの存在有無を区別しません。 設定不備、identity の不整合、参照先 user の欠落、storage 障害などは通常の 例外のままです。管理用の account API も内部エラーを公開用コードに変換しません。 RPC での共通変換は auth-app の authErrorHandler を利用できます。
