@gasboost/auth-app 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +331 -0
- package/dist/AuthState.d.ts +5 -0
- package/dist/AuthState.js +2 -0
- package/dist/AuthenticatedInput.d.ts +3 -0
- package/dist/AuthenticatedInput.js +2 -0
- package/dist/authentication.d.ts +4 -0
- package/dist/authentication.js +24 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# @gasboost/auth-app
|
|
2
|
+
|
|
3
|
+
`@gasboost/auth` と `@gasboost/app` を接続する authentication middleware adapter です。
|
|
4
|
+
|
|
5
|
+
認証必須 RPC の input に session token を含め、RPC 実行前に既存の `auth.session.get(token)` を利用して session を検証します。
|
|
6
|
+
|
|
7
|
+
有効な session は `AppsScript` の state に保存されます。
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
RPC input
|
|
11
|
+
↓
|
|
12
|
+
token
|
|
13
|
+
↓
|
|
14
|
+
@gasboost/auth-app
|
|
15
|
+
↓
|
|
16
|
+
auth.session.get(token)
|
|
17
|
+
↓
|
|
18
|
+
Session
|
|
19
|
+
↓
|
|
20
|
+
AppsScript state
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`@gasboost/app` 自体は `@gasboost/auth` を認識しません。
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
@gasboost/auth
|
|
27
|
+
↑
|
|
28
|
+
@gasboost/auth-app → @gasboost/app
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pnpm add @gasboost/auth-app @gasboost/auth @gasboost/app
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
npm:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install @gasboost/auth-app @gasboost/auth @gasboost/app
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## AuthenticatedInput
|
|
44
|
+
|
|
45
|
+
認証が必要な RPC では `AuthenticatedInput` を handler input に指定します。
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import type { AuthenticatedInput } from "@gasboost/auth-app";
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
token だけ必要な場合:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
type Input = AuthenticatedInput;
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
これは以下の型になります。
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
type Input = {
|
|
61
|
+
token: string;
|
|
62
|
+
};
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
追加の input がある場合は generic parameter に object を指定します。
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
type Input = AuthenticatedInput<{
|
|
69
|
+
name: string;
|
|
70
|
+
}>;
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
これは以下の型になります。
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
type Input = {
|
|
77
|
+
name: string;
|
|
78
|
+
token: string;
|
|
79
|
+
};
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Authentication middleware
|
|
83
|
+
|
|
84
|
+
既存の `AppsScriptAuth` instance を `authentication()` に渡します。
|
|
85
|
+
|
|
86
|
+
session storage や repository を middleware 側で再定義する必要はありません。
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { AppsScript } from "@gasboost/app";
|
|
90
|
+
import { AppsScriptAuth } from "@gasboost/auth";
|
|
91
|
+
import { authentication, type AuthState } from "@gasboost/auth-app";
|
|
92
|
+
|
|
93
|
+
const auth = new AppsScriptAuth({
|
|
94
|
+
repository,
|
|
95
|
+
|
|
96
|
+
runtime: {
|
|
97
|
+
utilities: Utilities,
|
|
98
|
+
session: Session,
|
|
99
|
+
cacheService: CacheService,
|
|
100
|
+
propertiesService: PropertiesService,
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
session: {
|
|
104
|
+
storageType: "cache",
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const app = new AppsScript<AuthState>().use(authentication(auth));
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`authentication()` は `AppsScriptAuth` の既存 session API を利用します。
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
const session = await auth.session.get(token);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
session の取得だけを目的とした別の authentication API は使用しません。
|
|
118
|
+
|
|
119
|
+
## Authenticated RPC
|
|
120
|
+
|
|
121
|
+
認証が必要な RPC は `AuthenticatedInput` を利用します。
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { AppsScript } from "@gasboost/app";
|
|
125
|
+
import {
|
|
126
|
+
authentication,
|
|
127
|
+
type AuthenticatedInput,
|
|
128
|
+
type AuthState,
|
|
129
|
+
} from "@gasboost/auth-app";
|
|
130
|
+
|
|
131
|
+
const app = new AppsScript<AuthState>()
|
|
132
|
+
.use(authentication(auth))
|
|
133
|
+
.call("getProfile", (_input: AuthenticatedInput) => {
|
|
134
|
+
const session = app.state.get("session");
|
|
135
|
+
|
|
136
|
+
if (!session) {
|
|
137
|
+
throw new Error("Unauthorized");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return getProfile(session.userId);
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
クライアント側では token が必須になります。
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
client.getProfile({
|
|
148
|
+
token,
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
token を渡さない呼び出しは RPC contract 上エラーになります。
|
|
153
|
+
|
|
154
|
+
追加 input が必要な場合:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
const app = new AppsScript<AuthState>().use(authentication(auth)).call(
|
|
158
|
+
"updateProfile",
|
|
159
|
+
(
|
|
160
|
+
input: AuthenticatedInput<{
|
|
161
|
+
name: string;
|
|
162
|
+
}>,
|
|
163
|
+
) => {
|
|
164
|
+
const session = app.state.get("session");
|
|
165
|
+
|
|
166
|
+
if (!session) {
|
|
167
|
+
throw new Error("Unauthorized");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return updateProfile({
|
|
171
|
+
userId: session.userId,
|
|
172
|
+
name: input.name,
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
クライアント:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
client.updateProfile({
|
|
182
|
+
token,
|
|
183
|
+
name: "Taro",
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Public RPC
|
|
188
|
+
|
|
189
|
+
middleware はすべての RPC に認証を要求するわけではありません。
|
|
190
|
+
|
|
191
|
+
token を持たない RPC input はそのまま後続 handler へ流れます。
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
const app = new AppsScript<AuthState>()
|
|
195
|
+
.use(authentication(auth))
|
|
196
|
+
.call("signIn", (input: { email: string; password: string }) => {
|
|
197
|
+
return auth.signIn.email(input);
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
クライアント側でも token は不要です。
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
client.signIn({
|
|
205
|
+
email: "user@example.com",
|
|
206
|
+
password: "password",
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
input 自体を持たない公開 RPC も利用できます。
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
const app = new AppsScript<AuthState>()
|
|
214
|
+
.use(authentication(auth))
|
|
215
|
+
.call("health", () => {
|
|
216
|
+
return {
|
|
217
|
+
ok: true,
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Session state
|
|
223
|
+
|
|
224
|
+
認証に成功すると middleware は取得した session を `AppsScript` state に設定します。
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
context.state.set("session", session);
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
application handler からは次のように取得できます。
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
const session = app.state.get("session");
|
|
234
|
+
|
|
235
|
+
if (!session) {
|
|
236
|
+
throw new Error("Unauthorized");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
session.id;
|
|
240
|
+
session.userId;
|
|
241
|
+
session.createdAt;
|
|
242
|
+
session.expiresAt;
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
application user が必要な場合は `session.userId` を利用して application 側で取得します。
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
const session = app.state.get("session");
|
|
249
|
+
|
|
250
|
+
if (!session) {
|
|
251
|
+
throw new Error("Unauthorized");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const user = await userRepository.find(session.userId);
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`@gasboost/auth-app` は application User の取得までは担当しません。
|
|
258
|
+
|
|
259
|
+
## Unauthorized
|
|
260
|
+
|
|
261
|
+
token プロパティが存在する場合、middleware は token を検証します。
|
|
262
|
+
|
|
263
|
+
以下の場合は `Unauthorized` error になります。
|
|
264
|
+
|
|
265
|
+
- `token` が string ではない
|
|
266
|
+
- `auth.session.get(token)` が session を返さない
|
|
267
|
+
- session が期限切れ
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
throw new Error("Unauthorized");
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
期限切れ session の判定と削除は `@gasboost/auth` の既存 `session.get()` が担当します。
|
|
274
|
+
|
|
275
|
+
middleware 側で session storage を直接操作することはありません。
|
|
276
|
+
|
|
277
|
+
## Apps Script identity
|
|
278
|
+
|
|
279
|
+
`Session.getActiveUser().getEmail()` と application session は別の概念です。
|
|
280
|
+
|
|
281
|
+
```text
|
|
282
|
+
Session.getActiveUser().getEmail()
|
|
283
|
+
≠
|
|
284
|
+
application session
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
この middleware では application の認証状態を RPC input の session token から確認します。
|
|
288
|
+
|
|
289
|
+
```text
|
|
290
|
+
token
|
|
291
|
+
↓
|
|
292
|
+
auth.session.get(token)
|
|
293
|
+
↓
|
|
294
|
+
application Session
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Apps Script Active User を application session の代替として扱いません。
|
|
298
|
+
|
|
299
|
+
## Custom state
|
|
300
|
+
|
|
301
|
+
application 独自の state と組み合わせる場合は `AuthState` と intersection できます。
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
import { authentication, type AuthState } from "@gasboost/auth-app";
|
|
305
|
+
|
|
306
|
+
type AppState = AuthState & {
|
|
307
|
+
requestId: string;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const app = new AppsScript<AppState>().use(authentication(auth));
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Responsibility
|
|
314
|
+
|
|
315
|
+
`@gasboost/auth-app` が担当するもの:
|
|
316
|
+
|
|
317
|
+
- authenticated RPC input の token contract
|
|
318
|
+
- RPC input から token の取得
|
|
319
|
+
- `auth.session.get(token)` による session 検証
|
|
320
|
+
- session の AppsScript state への設定
|
|
321
|
+
- invalid token の拒否
|
|
322
|
+
|
|
323
|
+
担当しないもの:
|
|
324
|
+
|
|
325
|
+
- session storage の構築
|
|
326
|
+
- repository の構築
|
|
327
|
+
- User の取得
|
|
328
|
+
- authorization
|
|
329
|
+
- `Session.getActiveUser()` による identity 解決
|
|
330
|
+
|
|
331
|
+
これらはそれぞれ `@gasboost/auth` または application の責務です。
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AppsScriptMiddleware } from "@gasboost/app";
|
|
2
|
+
import type { AppsScriptAuth } from "@gasboost/auth";
|
|
3
|
+
import type { AuthState } from "./AuthState";
|
|
4
|
+
export declare function authentication(auth: Pick<AppsScriptAuth, "session">): AppsScriptMiddleware<AuthState>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.authentication = authentication;
|
|
4
|
+
function authentication(auth) {
|
|
5
|
+
return async (context, next) => {
|
|
6
|
+
if (context.invocation.type !== "call") {
|
|
7
|
+
return next();
|
|
8
|
+
}
|
|
9
|
+
const input = context.invocation.input;
|
|
10
|
+
if (typeof input !== "object" || input === null || !("token" in input)) {
|
|
11
|
+
return next();
|
|
12
|
+
}
|
|
13
|
+
const token = input.token;
|
|
14
|
+
if (typeof token !== "string") {
|
|
15
|
+
throw new Error("Unauthorized");
|
|
16
|
+
}
|
|
17
|
+
const session = await auth.session.get(token);
|
|
18
|
+
if (session === null) {
|
|
19
|
+
throw new Error("Unauthorized");
|
|
20
|
+
}
|
|
21
|
+
context.state.set("session", session);
|
|
22
|
+
return next();
|
|
23
|
+
};
|
|
24
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.authentication = void 0;
|
|
4
|
+
var authentication_1 = require("./authentication");
|
|
5
|
+
Object.defineProperty(exports, "authentication", { enumerable: true, get: function () { return authentication_1.authentication; } });
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gasboost/auth-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Authentication middleware adapter for @gasboost/auth and @gasboost/app.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"google-apps-script",
|
|
7
|
+
"gas",
|
|
8
|
+
"authentication",
|
|
9
|
+
"auth",
|
|
10
|
+
"middleware",
|
|
11
|
+
"rpc",
|
|
12
|
+
"typescript",
|
|
13
|
+
"gasboost"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"default": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/gasboost/auth.git",
|
|
31
|
+
"directory": "packages/auth-app"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/gasboost/auth/issues"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/gasboost/auth#readme",
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@gasboost/app": "^3.0.0",
|
|
42
|
+
"@gasboost/auth": "^0.2.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@gasboost/app": "^3.0.0",
|
|
46
|
+
"@types/google-apps-script": "^2.0.13",
|
|
47
|
+
"@gasboost/auth": "0.2.1"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"format": "prettier --check . --config ../../.prettierrc.json --ignore-path ../../.prettierignore",
|
|
51
|
+
"format:fix": "prettier --write . --config ../../.prettierrc.json --ignore-path ../../.prettierignore",
|
|
52
|
+
"lint": "eslint . --config ../../eslint.config.mjs",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json"
|
|
56
|
+
}
|
|
57
|
+
}
|