@gasboost/auth 0.6.0 → 0.8.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 +38 -5
- package/dist/AppsScriptAuth.d.ts +1 -1
- package/dist/AppsScriptAuth.js +1 -0
- package/dist/api/AppsScriptAuthRegistration.d.ts +16 -1
- package/dist/api/AppsScriptAuthRegistration.js +81 -0
- package/dist/api/AppsScriptAuthSignUp.d.ts +6 -4
- package/dist/api/AppsScriptAuthSignUp.js +3 -1
- package/dist/api/SignUp.d.ts +8 -8
- package/dist/api/SignUp.js +19 -2
- package/dist/storage/AppsScriptAuthRepository.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -339,7 +339,8 @@ Session 発行
|
|
|
339
339
|
|
|
340
340
|
`@gasboost/auth` は、認証成功後に外部処理を実行するための lifecycle hook を提供します。
|
|
341
341
|
|
|
342
|
-
現在は `afterSignIn` を利用できます。
|
|
342
|
+
現在は `afterSignIn` を利用できます。Sign In と、Session を自動発行する Sign Up の
|
|
343
|
+
email / Apps Script 両経路で、Session 保存後に同じ Hook を1回 await します。
|
|
343
344
|
|
|
344
345
|
```ts
|
|
345
346
|
import { AppsScriptAuth, type EmailPasswordAuthOptions } from "@gasboost/auth";
|
|
@@ -384,7 +385,7 @@ hooks: {
|
|
|
384
385
|
},
|
|
385
386
|
```
|
|
386
387
|
|
|
387
|
-
hook の戻り値は Sign In result の `hooks` から取得できます。
|
|
388
|
+
hook の戻り値は Sign In / Sign Up result の `hooks` から取得できます。
|
|
388
389
|
|
|
389
390
|
```ts
|
|
390
391
|
const result = await auth.signIn.email({
|
|
@@ -409,7 +410,7 @@ const auth = new AppsScriptAuth<EmailPasswordAuthOptions, HookResult>({
|
|
|
409
410
|
});
|
|
410
411
|
```
|
|
411
412
|
|
|
412
|
-
hook を設定しない場合、従来どおり Sign In result は `user` と `session` のみを返します。
|
|
413
|
+
hook を設定しない場合、従来どおり Sign In / Sign Up result は `user` と `session` のみを返します。
|
|
413
414
|
|
|
414
415
|
```ts
|
|
415
416
|
const auth = new AppsScriptAuth({
|
|
@@ -448,7 +449,7 @@ hooks: {
|
|
|
448
449
|
|
|
449
450
|
### Hook Failure
|
|
450
451
|
|
|
451
|
-
`afterSignIn` は Sign In 処理の一部として扱われます。
|
|
452
|
+
`afterSignIn` は Sign In / Sign Up 処理の一部として扱われます。
|
|
452
453
|
|
|
453
454
|
処理順序は以下です。
|
|
454
455
|
|
|
@@ -466,9 +467,11 @@ afterSignIn
|
|
|
466
467
|
Sign In result
|
|
467
468
|
```
|
|
468
469
|
|
|
469
|
-
`afterSignIn` で例外が発生した場合、Sign In 全体が失敗します。
|
|
470
|
+
`afterSignIn` で例外が発生した場合、Sign In / Sign Up 全体が失敗します。
|
|
470
471
|
|
|
471
472
|
その際、すでに保存された Session は SessionStorage から削除されます。
|
|
473
|
+
削除が失敗しても元の Hook エラーを返します。Sign Up で作成済みの User / Account は削除しません。
|
|
474
|
+
登録は完了しているため、再認証には Sign In を使い、Sign Up を自動再試行しないでください。
|
|
472
475
|
|
|
473
476
|
```text
|
|
474
477
|
afterSignIn error
|
|
@@ -688,3 +691,33 @@ const auth = new AppsScriptAuth({
|
|
|
688
691
|
## License
|
|
689
692
|
|
|
690
693
|
MIT
|
|
694
|
+
|
|
695
|
+
### 再実行可能な複数認証方式の登録(0.8.0)
|
|
696
|
+
|
|
697
|
+
`registration.methods` は設定の `enabled` を返します。
|
|
698
|
+
`registration.ensure({ id, name, email, password? })` は、その全認証方式を
|
|
699
|
+
同じ User ID に登録します。通常の signup 設定とは独立した、サーバー側の管理用 API です。
|
|
700
|
+
呼び出し元で権限を検証し、Script Lock などで登録全体を排他制御してください。
|
|
701
|
+
Apps Script 認証が有効なら `email` は `Session.getActiveUser().getEmail()` と一致する必要があります。
|
|
702
|
+
|
|
703
|
+
```ts
|
|
704
|
+
const user = await auth.registration.ensure({
|
|
705
|
+
id: recovery.userId, // 初回書き込み前に永続化した固定 ID
|
|
706
|
+
name: "システム管理者",
|
|
707
|
+
email: Session.getActiveUser().getEmail(),
|
|
708
|
+
password: Utilities.getUuid() + Utilities.getUuid(),
|
|
709
|
+
});
|
|
710
|
+
// Email が有効な場合は password reset delivery で本人に設定を促す。
|
|
711
|
+
await auth.password?.forgot({ email: Session.getActiveUser().getEmail() });
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
既存の認証情報は維持し、不足する Account だけを固定 ID で追加します。
|
|
715
|
+
別 User が所有するメールアドレスは取り込みません。仮パスワードを本人へ送信しないでください。
|
|
716
|
+
メール通知失敗時は `password.forgot()` を再実行して新しいリセットトークンを送れます。
|
|
717
|
+
再発行すると以前のリセットトークンは失効します。
|
|
718
|
+
|
|
719
|
+
この API は Repository の任意メソッド `user.save(user)` を必要とします。
|
|
720
|
+
実装は User と各 Account を ID で upsert し、途中失敗後も同じ ID で修復できる必要があります。
|
|
721
|
+
`@gasboost/auth-sheetorm >=0.4.0` はこれを実装します。
|
|
722
|
+
既存の `registration.email()` / `.appsScript()` と `user.create()` の動作は変更しません。
|
|
723
|
+
関連: [gasboost/app#64](https://github.com/gasboost/app/issues/64)。
|
package/dist/AppsScriptAuth.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ type AppsScriptAuthPasswordApi<TEmailPassword> = TEmailPassword extends {
|
|
|
32
32
|
} ? AppsScriptAuthPassword : undefined;
|
|
33
33
|
export declare class AppsScriptAuth<TEmailPassword extends EmailPasswordAuthOptions | undefined = undefined, THookResult = undefined> {
|
|
34
34
|
readonly signIn: AppsScriptAuthSignIn<THookResult>;
|
|
35
|
-
readonly signUp: AppsScriptAuthSignUp
|
|
35
|
+
readonly signUp: AppsScriptAuthSignUp<THookResult>;
|
|
36
36
|
readonly session: AppsScriptAuthSession;
|
|
37
37
|
readonly signOut: AppsScriptAuthSignOut;
|
|
38
38
|
readonly password: AppsScriptAuthPasswordApi<TEmailPassword>;
|
package/dist/AppsScriptAuth.js
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import type { AppsScriptAuthenticationConfig } from "../authentication/AppsScriptAuthentication";
|
|
2
2
|
import type { EmailPasswordAuthConfig } from "../authentication/EmailPasswordAuthentication";
|
|
3
|
-
import
|
|
3
|
+
import { User } from "../domain/User";
|
|
4
4
|
import { type AppsScriptRegistrationInput } from "../registration/AppsScriptRegistration";
|
|
5
5
|
import { type EmailPasswordRegistrationInput } from "../registration/EmailPasswordRegistration";
|
|
6
6
|
import type { AppsScriptAuthRepository } from "../storage/AppsScriptAuthRepository";
|
|
7
7
|
export declare class AppsScriptAuthRegistration {
|
|
8
8
|
private readonly emailPasswordRegistration?;
|
|
9
9
|
private readonly appsScriptRegistration?;
|
|
10
|
+
readonly methods: {
|
|
11
|
+
readonly emailPassword: boolean;
|
|
12
|
+
readonly appsScript: boolean;
|
|
13
|
+
};
|
|
14
|
+
private readonly repository;
|
|
15
|
+
private readonly utilities;
|
|
16
|
+
private readonly session;
|
|
17
|
+
private readonly emailPassword?;
|
|
10
18
|
constructor({ emailPassword, appsScript, repository, utilities, session, }: {
|
|
11
19
|
emailPassword?: EmailPasswordAuthConfig;
|
|
12
20
|
appsScript?: AppsScriptAuthenticationConfig;
|
|
@@ -16,4 +24,11 @@ export declare class AppsScriptAuthRegistration {
|
|
|
16
24
|
});
|
|
17
25
|
email(input: EmailPasswordRegistrationInput): Promise<User>;
|
|
18
26
|
appsScript(input: AppsScriptRegistrationInput): Promise<User>;
|
|
27
|
+
/** Privileged server API. Caller must authorize and serialize provisioning. */
|
|
28
|
+
ensure(input: {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
email: string;
|
|
32
|
+
password?: string;
|
|
33
|
+
}): Promise<User>;
|
|
19
34
|
}
|
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.AppsScriptAuthRegistration = void 0;
|
|
4
|
+
const User_1 = require("../domain/User");
|
|
5
|
+
const Account_1 = require("../domain/Account");
|
|
6
|
+
const Password_1 = require("../domain/Password");
|
|
7
|
+
const AuthPattern_1 = require("../AuthPattern");
|
|
8
|
+
const AppsScriptIdentity_1 = require("../identity/AppsScriptIdentity");
|
|
9
|
+
const EmailPasswordIdentity_1 = require("../identity/EmailPasswordIdentity");
|
|
4
10
|
const AppsScriptRegistration_1 = require("../registration/AppsScriptRegistration");
|
|
5
11
|
const EmailPasswordRegistration_1 = require("../registration/EmailPasswordRegistration");
|
|
6
12
|
class AppsScriptAuthRegistration {
|
|
7
13
|
emailPasswordRegistration;
|
|
8
14
|
appsScriptRegistration;
|
|
15
|
+
methods;
|
|
16
|
+
repository;
|
|
17
|
+
utilities;
|
|
18
|
+
session;
|
|
19
|
+
emailPassword;
|
|
9
20
|
constructor({ emailPassword, appsScript, repository, utilities, session, }) {
|
|
21
|
+
this.repository = repository;
|
|
22
|
+
this.utilities = utilities;
|
|
23
|
+
this.session = session;
|
|
24
|
+
this.emailPassword = emailPassword;
|
|
25
|
+
this.methods = Object.freeze({
|
|
26
|
+
emailPassword: !!emailPassword?.enabled,
|
|
27
|
+
appsScript: !!appsScript?.enabled,
|
|
28
|
+
});
|
|
10
29
|
if (emailPassword?.enabled) {
|
|
11
30
|
this.emailPasswordRegistration = new EmailPasswordRegistration_1.EmailPasswordRegistration(repository, utilities, emailPassword);
|
|
12
31
|
}
|
|
@@ -26,5 +45,67 @@ class AppsScriptAuthRegistration {
|
|
|
26
45
|
}
|
|
27
46
|
return this.appsScriptRegistration.register(input);
|
|
28
47
|
}
|
|
48
|
+
/** Privileged server API. Caller must authorize and serialize provisioning. */
|
|
49
|
+
async ensure(input) {
|
|
50
|
+
if (!this.methods.emailPassword && !this.methods.appsScript) {
|
|
51
|
+
throw new Error("No authentication method is enabled");
|
|
52
|
+
}
|
|
53
|
+
if (!input.id || !input.email.trim())
|
|
54
|
+
throw new Error("User ID and email are required");
|
|
55
|
+
if (!this.repository.user.save)
|
|
56
|
+
throw new Error("Repository must support idempotent user.save");
|
|
57
|
+
if (this.methods.appsScript &&
|
|
58
|
+
this.session.getActiveUser().getEmail() !== input.email) {
|
|
59
|
+
throw new Error("Active user does not match registration email");
|
|
60
|
+
}
|
|
61
|
+
const existing = await this.repository.user.find(input.id);
|
|
62
|
+
const accounts = [...(existing?.accounts ?? [])];
|
|
63
|
+
for (const provider of [
|
|
64
|
+
AuthPattern_1.authPattern.emailPassword,
|
|
65
|
+
AuthPattern_1.authPattern.appsScript,
|
|
66
|
+
]) {
|
|
67
|
+
if (provider === AuthPattern_1.authPattern.emailPassword
|
|
68
|
+
? !this.methods.emailPassword
|
|
69
|
+
: !this.methods.appsScript)
|
|
70
|
+
continue;
|
|
71
|
+
const collision = await this.repository.account.findByIdentity(provider, input.email);
|
|
72
|
+
if (collision && collision.userId !== input.id)
|
|
73
|
+
throw new Error("Account belongs to another user");
|
|
74
|
+
const current = accounts.find((account) => account.identity.providerName === provider);
|
|
75
|
+
if (current) {
|
|
76
|
+
if (current.userId !== input.id ||
|
|
77
|
+
current.identity.accountId !== input.email)
|
|
78
|
+
throw new Error("Existing user identity does not match");
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (collision) {
|
|
82
|
+
accounts.push(collision);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const accountId = `${input.id}:${provider}`;
|
|
86
|
+
let identity;
|
|
87
|
+
if (provider === AuthPattern_1.authPattern.emailPassword) {
|
|
88
|
+
if (!input.password || !this.emailPassword)
|
|
89
|
+
throw new Error("Password is required");
|
|
90
|
+
const password = new Password_1.Password(input.password, this.utilities, this.emailPassword.pepper, this.emailPassword.iterations);
|
|
91
|
+
identity = new EmailPasswordIdentity_1.EmailPasswordIdentity({
|
|
92
|
+
accountId: input.email,
|
|
93
|
+
password: await password.hash(accountId),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
identity = new AppsScriptIdentity_1.AppsScriptIdentity({
|
|
98
|
+
googleAccountAddress: input.email,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
accounts.push(new Account_1.Account({ id: accountId, userId: input.id, identity }));
|
|
102
|
+
}
|
|
103
|
+
const user = new User_1.User({
|
|
104
|
+
id: input.id,
|
|
105
|
+
name: existing?.name ?? input.name,
|
|
106
|
+
accounts,
|
|
107
|
+
});
|
|
108
|
+
return this.repository.user.save(user);
|
|
109
|
+
}
|
|
29
110
|
}
|
|
30
111
|
exports.AppsScriptAuthRegistration = AppsScriptAuthRegistration;
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import type { AppsScriptAuthenticationConfig } from "../authentication/AppsScriptAuthentication";
|
|
2
2
|
import type { EmailPasswordAuthConfig } from "../authentication/EmailPasswordAuthentication";
|
|
3
|
+
import type { AfterSignInHook } from "../hooks/AuthHooks";
|
|
3
4
|
import { type AppsScriptRegistrationInput } from "../registration/AppsScriptRegistration";
|
|
4
5
|
import { type EmailPasswordRegistrationInput } from "../registration/EmailPasswordRegistration";
|
|
5
6
|
import type { AppsScriptAuthRepository } from "../storage/AppsScriptAuthRepository";
|
|
6
7
|
import type { AppsScriptSessionStorage } from "../storage/AppsScriptSessionStorage";
|
|
7
8
|
import { SignUp } from "./SignUp";
|
|
8
|
-
export declare class AppsScriptAuthSignUp {
|
|
9
|
-
email: SignUp<EmailPasswordRegistrationInput>["execute"];
|
|
10
|
-
appsScript: SignUp<AppsScriptRegistrationInput>["execute"];
|
|
11
|
-
constructor({ sessionStorage, expiresIn, repository, emailPassword, appsScript, utilities, session, }: {
|
|
9
|
+
export declare class AppsScriptAuthSignUp<THookResult = undefined> {
|
|
10
|
+
email: SignUp<EmailPasswordRegistrationInput, THookResult>["execute"];
|
|
11
|
+
appsScript: SignUp<AppsScriptRegistrationInput, THookResult>["execute"];
|
|
12
|
+
constructor({ sessionStorage, expiresIn, afterSignIn, repository, emailPassword, appsScript, utilities, session, }: {
|
|
12
13
|
sessionStorage: AppsScriptSessionStorage;
|
|
13
14
|
expiresIn: number;
|
|
15
|
+
afterSignIn?: AfterSignInHook<THookResult>;
|
|
14
16
|
repository: AppsScriptAuthRepository;
|
|
15
17
|
emailPassword?: EmailPasswordAuthConfig;
|
|
16
18
|
appsScript?: AppsScriptAuthenticationConfig;
|
|
@@ -7,7 +7,7 @@ const SignUp_1 = require("./SignUp");
|
|
|
7
7
|
class AppsScriptAuthSignUp {
|
|
8
8
|
email;
|
|
9
9
|
appsScript;
|
|
10
|
-
constructor({ sessionStorage, expiresIn, repository, emailPassword, appsScript, utilities, session, }) {
|
|
10
|
+
constructor({ sessionStorage, expiresIn, afterSignIn, repository, emailPassword, appsScript, utilities, session, }) {
|
|
11
11
|
this.email = async (input) => {
|
|
12
12
|
if (!emailPassword) {
|
|
13
13
|
throw new Error("Email and password authentication is disabled");
|
|
@@ -15,6 +15,7 @@ class AppsScriptAuthSignUp {
|
|
|
15
15
|
emailPassword.ensureSignUpEnabled();
|
|
16
16
|
const emailSignUp = new SignUp_1.SignUp({
|
|
17
17
|
sessionStorage,
|
|
18
|
+
afterSignIn,
|
|
18
19
|
expiresIn,
|
|
19
20
|
registration: new EmailPasswordRegistration_1.EmailPasswordRegistration(repository, utilities, emailPassword),
|
|
20
21
|
utilities,
|
|
@@ -28,6 +29,7 @@ class AppsScriptAuthSignUp {
|
|
|
28
29
|
appsScript.ensureSignUpEnabled();
|
|
29
30
|
const appsScriptSignUp = new SignUp_1.SignUp({
|
|
30
31
|
sessionStorage,
|
|
32
|
+
afterSignIn,
|
|
31
33
|
expiresIn,
|
|
32
34
|
registration: new AppsScriptRegistration_1.AppsScriptRegistration(utilities, session, repository),
|
|
33
35
|
utilities,
|
package/dist/api/SignUp.d.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { User } from "../domain/User";
|
|
1
|
+
import type { AfterSignInHook } from "../hooks/AuthHooks";
|
|
3
2
|
import type { Registration } from "../registration/Registration";
|
|
4
3
|
import type { AppsScriptSessionStorage } from "../storage/AppsScriptSessionStorage";
|
|
5
|
-
|
|
4
|
+
import type { SignInResult } from "./SignIn";
|
|
5
|
+
export type SignUpResult<THookResult = undefined> = SignInResult<THookResult>;
|
|
6
|
+
export declare class SignUp<T, THookResult = undefined> {
|
|
7
|
+
private readonly afterSignIn;
|
|
6
8
|
private readonly sessionStorage;
|
|
7
9
|
private readonly registration;
|
|
8
10
|
private readonly utilities;
|
|
9
11
|
private readonly expiresIn;
|
|
10
|
-
constructor({ sessionStorage, registration, utilities, expiresIn, }: {
|
|
12
|
+
constructor({ sessionStorage, registration, utilities, expiresIn, afterSignIn, }: {
|
|
11
13
|
sessionStorage: AppsScriptSessionStorage;
|
|
12
14
|
registration: Registration<T>;
|
|
13
15
|
utilities: GoogleAppsScript.Utilities.Utilities;
|
|
14
16
|
expiresIn: number;
|
|
17
|
+
afterSignIn?: AfterSignInHook<THookResult>;
|
|
15
18
|
});
|
|
16
|
-
execute(input: T): Promise<
|
|
17
|
-
user: User;
|
|
18
|
-
session: Session;
|
|
19
|
-
}>;
|
|
19
|
+
execute(input: T): Promise<SignUpResult<THookResult>>;
|
|
20
20
|
}
|
package/dist/api/SignUp.js
CHANGED
|
@@ -3,15 +3,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.SignUp = void 0;
|
|
4
4
|
const Session_1 = require("../domain/Session");
|
|
5
5
|
class SignUp {
|
|
6
|
+
afterSignIn;
|
|
6
7
|
sessionStorage;
|
|
7
8
|
registration;
|
|
8
9
|
utilities;
|
|
9
10
|
expiresIn;
|
|
10
|
-
constructor({ sessionStorage, registration, utilities, expiresIn, }) {
|
|
11
|
+
constructor({ sessionStorage, registration, utilities, expiresIn, afterSignIn, }) {
|
|
11
12
|
this.sessionStorage = sessionStorage;
|
|
12
13
|
this.registration = registration;
|
|
13
14
|
this.utilities = utilities;
|
|
14
15
|
this.expiresIn = expiresIn;
|
|
16
|
+
this.afterSignIn = afterSignIn;
|
|
15
17
|
}
|
|
16
18
|
async execute(input) {
|
|
17
19
|
const user = await this.registration.register(input);
|
|
@@ -23,7 +25,22 @@ class SignUp {
|
|
|
23
25
|
expiresAt: new Date(now.getTime() + this.expiresIn),
|
|
24
26
|
});
|
|
25
27
|
await this.sessionStorage.save(session);
|
|
26
|
-
|
|
28
|
+
if (!this.afterSignIn) {
|
|
29
|
+
return { user, session };
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const hooks = await this.afterSignIn({ user, session });
|
|
33
|
+
return { user, session, hooks };
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
try {
|
|
37
|
+
await this.sessionStorage.delete(session.id);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Preserve the original hook error. Registration remains intact.
|
|
41
|
+
}
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
27
44
|
}
|
|
28
45
|
}
|
|
29
46
|
exports.SignUp = SignUp;
|
|
@@ -9,6 +9,8 @@ export interface AppsScriptAuthRepository {
|
|
|
9
9
|
user: {
|
|
10
10
|
find: (id: string) => Promise<User | null>;
|
|
11
11
|
create: (user: User) => Promise<User>;
|
|
12
|
+
/** Upsert user/accounts by their stable IDs; retries must preserve a single user. */
|
|
13
|
+
save?: (user: User) => Promise<User>;
|
|
12
14
|
};
|
|
13
15
|
passwordCredential: {
|
|
14
16
|
findByResetTokenHash(tokenHash: string): Promise<PasswordCredential | null>;
|
package/package.json
CHANGED