@gasboost/auth-app
v0.8.0
Published
Authentication middleware adapter for @gasboost/auth and @gasboost/app.
Downloads
825
Maintainers
Readme
@gasboost/auth-app
@gasboost/auth と @gasboost/app を接続する authentication middleware adapter です。
RPC input とは独立した metadata に session token を含め、RPC 実行前に既存の auth.session.get(token) を利用して session を検証します。
有効な session は AppsScript の state に保存されます。
RPC metadata["@gasboost/auth"]
↓
token
↓
@gasboost/auth-app
↓
auth.session.get(token)
↓
Session
↓
AppsScript state@gasboost/app 自体は @gasboost/auth を認識しません。
@gasboost/auth
↑
@gasboost/auth-app → @gasboost/appInstall
pnpm add @gasboost/auth-app @gasboost/auth @gasboost/appnpm:
npm install @gasboost/auth-app @gasboost/auth @gasboost/appAuth metadata
@gasboost/app@^5.2.0 の RPC metadata を利用します。
key / schema / validator は frontend と backend で共通の
@gasboost/auth-client/metadata が提供します。
import {
AUTH_METADATA_KEY,
type AuthMetadata,
} from "@gasboost/auth-client/metadata";
const metadata = {
[AUTH_METADATA_KEY]: { token } satisfies AuthMetadata,
};
await app.dispatch("updateProfile", { name: "Taro" }, metadata);handler input は業務入力だけを定義します。
AuthenticatedInput<T> は 0.4.0 で削除しました。
input の token は認証には使いません。旧方式との後方互換性はありません。
@gasboost/client では metadata provider を登録することで、
RPC ごとに token を渡さずに利用できます。
frontend では pnpm add @gasboost/client @gasboost/auth-client で依存を追加します。
import { appsScriptClient } from "@gasboost/client";
import { AUTH_METADATA_KEY } from "@gasboost/auth-client/metadata";
const { client } = appsScriptClient<AppType>({
metadataProviders: [
() => {
const token = getCurrentToken();
return token ? { [AUTH_METADATA_KEY]: { token } } : undefined;
},
],
});
await client.updateProfile({ name: "Taro" });session lifecycle 管理には @gasboost/auth-client/appsscript の
appsScriptAuth({ client, storage }) と authMetadataProvider(storage) を利用できます。
詳細は auth-client README を参照してください。
Authentication middleware
既存の AppsScriptAuth instance を authentication() に渡します。
session storage や repository を middleware 側で再定義する必要はありません。
import { AppsScript } from "@gasboost/app";
import { AppsScriptAuth } from "@gasboost/auth";
import { authentication } from "@gasboost/auth-app";
const auth = new AppsScriptAuth({
repository,
runtime: {
utilities: Utilities,
session: Session,
cacheService: CacheService,
propertiesService: PropertiesService,
},
session: {
storageType: "cache",
},
});
const app = new AppsScript().use(authentication(auth));authentication() は AppsScriptAuth の既存 session API を利用します。
const session = await auth.session.get(token);有効な session が取得できた場合、middleware はその session を AppsScript state に設定します。
@gasboost/app v5.2 以降では authentication() の適用後、後続 handler の context.state から session が non-nullable として取得できます。
Auth handlers
handlers(auth) は AppsScriptAuth の公開 API を @gasboost/app の RPC handler として登録できる形へ変換します。
import { AppsScript } from "@gasboost/app";
import { AppsScriptAuth } from "@gasboost/auth";
import { authentication, handlers } from "@gasboost/auth-app";
const auth = new AppsScriptAuth({
repository,
runtime,
session: {
storageType: "cache",
},
});
const app = new AppsScript().calls(handlers(auth)).use(authentication(auth));以下の RPC が公開されます。
signInEmailsignInAppsScriptsignUpEmailsignUpAppsScriptgetSessionsignOut
Password Reset が有効な場合は、さらに以下も公開されます。
forgotPasswordresetPassword
signIn / signUp / Password Reset の input と result の型は @gasboost/auth の既存 API から推論されます。
@gasboost/auth@^0.7.0 で afterSignIn を設定すると、Sign In / Sign Up の
email / Apps Script 全 RPC に hooks の戻り値型が伝播します。
@gasboost/auth-client の Consumer Hook も、その RPC 型から結果を推論します。
Firebase の接続例は auth-client README を参照してください。
getSession と signOut は、RPC の単一 object input contract に合わせて以下の形式になります。
client.getSession({
sessionId: "session-id",
});
client.signOut({
sessionId: "session-id",
});signOut / forgotPassword / resetPassword の RPC 成功時の result は
JSON として扱える null です(0.5.0 以降)。
@gasboost/auth 側の API 自体は変更されません。
await auth.session.get("session-id");
await auth.signOut.execute("session-id");Authorization Middleware
authorization() は、authentication(auth) が state に保存した session.userId を使って AppsScriptAuthorization.can() に委譲します。token の再検証、Session Storage へのアクセス、Repository への直接アクセスは行いません。
import {
authentication,
authorization as requireAuthorization,
} from "@gasboost/auth-app";
const app = new AppsScript()
.use(authentication(auth))
.use(
requireAuthorization(authorization, {
project: ["update"],
}),
)
.call("updateProject", updateProject);許可されない場合は Forbidden を throw します。認証に失敗した場合の Unauthorized とは区別されます。
authorizationHandlers(authorization) は role / permission assignment 用の handler を生成します。
const handlers = authorizationHandlers(authorization);
app.call("assignRole", handlers.role.assign);
app.call("allowPermission", handlers.permission.allow);これらの handler は強い権限を持つ master operation です。自動登録や暗黙の管理権限付与は行わないため、公開する RPC 名と保護用 middleware は application 側で明示してください。
Authenticated RPC
認証が必要な RPC は authentication(auth) より後に登録します。
import { AppsScript } from "@gasboost/app";
import { authentication } from "@gasboost/auth-app";
const app = new AppsScript()
.use(authentication(auth))
.call("getProfile", (_input, context) => {
const session = context.state.get("session");
return getProfile(session.userId);
});authentication(auth) の通過後は session が state に存在することが型として保証されるため、undefined check は不要です。
クライアント側では metadata provider が token を供給します。
client.getProfile();追加 input が必要な場合:
const app = new AppsScript().use(authentication(auth)).call(
"updateProfile",
(
input: {
name: string;
},
context,
) => {
const session = context.state.get("session");
return updateProfile({
userId: session.userId,
name: input.name,
});
},
);クライアント:
client.updateProfile({
name: "Taro",
});Public RPC
middleware はすべての RPC に認証を要求するわけではありません。
公開 RPC は authentication(auth) より前に登録します。middleware より後の RPC は input の有無にかかわらず認証必須です。
const app = new AppsScript()
.call("signIn", (input: { email: string; password: string }) => {
return auth.signIn.email(input);
})
.use(authentication(auth));クライアント側でも token は不要です。
client.signIn({
email: "[email protected]",
password: "password",
});input 自体を持たない公開 RPC も利用できます。
const app = new AppsScript()
.call("health", () => ({ ok: true }))
.use(authentication(auth));Session state
認証に成功すると middleware は取得した session を AppsScript state に設定します。
context.state.set("session", session);authentication(auth) より後の application handler では、context.state から取得できます。
const app = new AppsScript()
.use(authentication(auth))
.call("getProfile", (_input, context) => {
const session = context.state.get("session");
session.id;
session.userId;
session.createdAt;
session.expiresAt;
return getProfile(session.userId);
});authentication(auth) が session state を保証するため、後続 handler では session は undefined になりません。
application user が必要な場合は session.userId を利用して application 側で取得します。
const app = new AppsScript()
.use(authentication(auth))
.call("getProfile", async (_input, context) => {
const session = context.state.get("session");
const user = await userRepository.find(session.userId);
return user;
});@gasboost/auth-app は application User の取得までは担当しません。
Unauthorized
middleware より後に登録されたすべての RPC で auth metadata を検証します。
以下の場合は Unauthorized error になります。
- auth metadata が存在しない、または object ではない
tokenが存在しない、string ではない、空文字、空白のみauth.session.get(token)が session を返さない- session が期限切れ
throw new Error("Unauthorized");期限切れ session の判定と削除は @gasboost/auth の既存 session.get() が担当します。
middleware 側で session storage を直接操作することはありません。
Apps Script identity
Session.getActiveUser().getEmail() と application session は別の概念です。
Session.getActiveUser().getEmail()
≠
application sessionこの middleware では application の認証状態を RPC metadata の session token から確認します。
token
↓
auth.session.get(token)
↓
application SessionApps Script Active User を application session の代替として扱いません。
Custom state
@gasboost/app v5.2 以降では middleware が追加する state は .use() によって型へ反映されます。
const app = new AppsScript().use(authentication(auth));authentication(auth) より後の handler では session state が保証されます。
const app = new AppsScript()
.calls(handlers(auth))
.call("health", () => ({
ok: true,
}))
.use(authentication(auth))
.call("getProfile", (_input, context) => {
const session = context.state.get("session");
return getProfile(session.userId);
});application 独自の state を追加する場合も、対応する middleware を .use() で組み合わせます。
Responsibility
@gasboost/auth-app が担当するもの:
- 共有 auth metadata contract の解釈
- RPC metadata から token の取得
auth.session.get(token)による session 検証- session の AppsScript state への設定
- invalid token の拒否
担当しないもの:
- session storage の構築
- repository の構築
- User の取得
Session.getActiveUser()による identity 解決
これらはそれぞれ @gasboost/auth または application の責務です。
拡張ユーザーの RPC(0.7.0)
AppsScriptAuth の user.data を設定すると、handlers(auth) の4種の
SignIn / SignUp と getSession に戻り値の型が自動伝播します。追加設定は不要です。
const app = new AppsScript().calls(handlers(auth));
const restored = await handlers(auth).getSession({ sessionId });
restored?.user.data.permissions; // consumer が user.data で定義した型getSession は auth.session.getWithUser(id) を呼び、従来の
id, userId, createdAt, expiresAt を維持したまま user を追加します。
User の取得・拡張・エラー処理は auth 本体が担当します。
authentication(auth) は引き続き session.get によるセッション検証を行います。
共通の認証エラー処理
authErrorHandler() は 対象 RPC を .calls() / .call() で登録する前に
.use() で登録してください。@gasboost/app@^5.5.0 が必要です。
import { AppsScript } from "@gasboost/app";
import { authErrorHandler, authentication, handlers } from "@gasboost/auth-app";
const app = new AppsScript()
.use(authErrorHandler())
.calls(handlers(auth))
.use(authentication(auth))
.call(
"someProtectedRpc",
(_input, context) => context.state.get("session").userId,
);handlers(auth)、認証・認可 middleware、Consumer RPC 内で直接呼び出した
Auth API の AuthError を、同じ code と message の AppsScriptError
に変換します。その他の例外はそのまま再 throw し、RPC 境界で
INTERNAL_ERROR と安全な共通メッセージに正規化されます。
フロントエンドでは @gasboost/client@^0.6.0 が復元したエラーを判定します。
@gasboost/auth/errors はブラウザでも安全に import できます。
import { AppsScriptRpcError } from "@gasboost/client";
import { AUTH_ERROR_CODES } from "@gasboost/auth/errors";
try {
await client.signInEmail({ email, password });
} catch (error) {
if (
error instanceof AppsScriptRpcError &&
error.code === AUTH_ERROR_CODES.INVALID_CREDENTIALS
) {
// 認証情報の再入力など、アプリ側の表示処理
}
}@gasboost/auth-client はエラーコードを複製せず、RPC エラーも復元しません。
