@things-factory/auth-base 10.1.205 → 10.1.208
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/dist-server/controllers/passkey.d.ts +35 -0
- package/dist-server/controllers/passkey.js +182 -0
- package/dist-server/controllers/passkey.js.map +1 -0
- package/dist-server/controllers/signin.d.ts +24 -0
- package/dist-server/controllers/signin.js +63 -39
- package/dist-server/controllers/signin.js.map +1 -1
- package/dist-server/middlewares/index.d.ts +0 -1
- package/dist-server/middlewares/index.js +0 -1
- package/dist-server/middlewares/index.js.map +1 -1
- package/dist-server/router/auth-signin-router.js +2 -1
- package/dist-server/router/auth-signin-router.js.map +1 -1
- package/dist-server/router/webauthn-router.js +55 -93
- package/dist-server/router/webauthn-router.js.map +1 -1
- package/dist-server/service/access-request/index.d.ts +1 -1
- package/dist-server/service/audit/audit-kinds.js +3 -1
- package/dist-server/service/audit/audit-kinds.js.map +1 -1
- package/dist-server/service/audit/index.d.ts +1 -1
- package/dist-server/service/domain-link/index.d.ts +1 -1
- package/dist-server/service/domain-owner/index.d.ts +1 -1
- package/dist-server/service/index.d.ts +1 -1
- package/dist-server/service/index.js +2 -1
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/open-fields.js +4 -1
- package/dist-server/service/open-fields.js.map +1 -1
- package/dist-server/service/role-template/index.d.ts +1 -1
- package/dist-server/service/web-auth-credential/index.d.ts +3 -0
- package/dist-server/service/web-auth-credential/index.js +4 -1
- package/dist-server/service/web-auth-credential/index.js.map +1 -1
- package/dist-server/service/web-auth-credential/web-auth-credential-mutation.d.ts +7 -0
- package/dist-server/service/web-auth-credential/web-auth-credential-mutation.js +67 -0
- package/dist-server/service/web-auth-credential/web-auth-credential-mutation.js.map +1 -0
- package/dist-server/service/web-auth-credential/web-auth-credential-query.d.ts +5 -0
- package/dist-server/service/web-auth-credential/web-auth-credential-query.js +29 -0
- package/dist-server/service/web-auth-credential/web-auth-credential-query.js.map +1 -0
- package/dist-server/service/web-auth-credential/web-auth-credential.d.ts +4 -0
- package/dist-server/service/web-auth-credential/web-auth-credential.js +24 -2
- package/dist-server/service/web-auth-credential/web-auth-credential.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/tests/audit-db.test.ts +4 -4
- package/tests/passkey-authenticator.ts +104 -0
- package/tests/passkey-db.test.ts +286 -0
- package/translations/ko.json +2 -2
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type AuthenticationResponseJSON, type PublicKeyCredentialCreationOptionsJSON, type PublicKeyCredentialRequestOptionsJSON, type RegistrationResponseJSON } from '@simplewebauthn/server';
|
|
2
|
+
import { User } from '../service/user/user.js';
|
|
3
|
+
import { WebAuthCredential } from '../service/web-auth-credential/web-auth-credential.js';
|
|
4
|
+
/** Where a ceremony runs. The relying party is the host the person reached (ADR-0156 「남기는 것」). */
|
|
5
|
+
export type PasskeySite = {
|
|
6
|
+
rpID: string;
|
|
7
|
+
origin: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function siteOf(context: any): PasskeySite;
|
|
10
|
+
export declare function rememberChallenge(context: any, challenge: string): void;
|
|
11
|
+
/** The session's challenge, removed - whatever the answer turns out to be. */
|
|
12
|
+
export declare function takeChallenge(context: any): string;
|
|
13
|
+
/** What to ask the browser when a signed-in person adds a passkey. */
|
|
14
|
+
export declare function registrationOptions(user: User, rpName: string, site: PasskeySite): Promise<PublicKeyCredentialCreationOptionsJSON>;
|
|
15
|
+
/**
|
|
16
|
+
* What to ask the browser when somebody signs in. No credential is named: the browser offers what
|
|
17
|
+
* it holds for this site, or a phone nearby.
|
|
18
|
+
*/
|
|
19
|
+
export declare function signinOptions(site: PasskeySite): Promise<PublicKeyCredentialRequestOptionsJSON>;
|
|
20
|
+
/** What to ask the browser when a signed-in person confirms it is them - only their own passkeys. */
|
|
21
|
+
export declare function reverifyOptions(user: User, site: PasskeySite): Promise<PublicKeyCredentialRequestOptionsJSON | null>;
|
|
22
|
+
/** Keeps the passkey the browser made for `user`. `name` is the person's label for it. */
|
|
23
|
+
export declare function registerPasskey(user: User, response: RegistrationResponseJSON, name: string | undefined, challenge: string, site: PasskeySite): Promise<WebAuthCredential>;
|
|
24
|
+
/**
|
|
25
|
+
* Signing in with a passkey: the answer is checked, then the account goes through the same gate as
|
|
26
|
+
* a password sign-in - deleted, locked, inactive - and the sign-in is audited with its method
|
|
27
|
+
* (ADR-0156 decision 4). A passkey does not lift a lock and does not count as a failed password.
|
|
28
|
+
*/
|
|
29
|
+
export declare function signinWithPasskey(response: AuthenticationResponseJSON, challenge: string, site: PasskeySite, context?: any): Promise<{
|
|
30
|
+
user: User;
|
|
31
|
+
token: string;
|
|
32
|
+
domains: import("@things-factory/shell").Domain[];
|
|
33
|
+
}>;
|
|
34
|
+
/** A signed-in person confirming it is them, with one of their own passkeys. */
|
|
35
|
+
export declare function reverifyWithPasskey(user: User, response: AuthenticationResponseJSON, challenge: string, site: PasskeySite): Promise<void>;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.siteOf = siteOf;
|
|
4
|
+
exports.rememberChallenge = rememberChallenge;
|
|
5
|
+
exports.takeChallenge = takeChallenge;
|
|
6
|
+
exports.registrationOptions = registrationOptions;
|
|
7
|
+
exports.signinOptions = signinOptions;
|
|
8
|
+
exports.reverifyOptions = reverifyOptions;
|
|
9
|
+
exports.registerPasskey = registerPasskey;
|
|
10
|
+
exports.signinWithPasskey = signinWithPasskey;
|
|
11
|
+
exports.reverifyWithPasskey = reverifyWithPasskey;
|
|
12
|
+
const server_1 = require("@simplewebauthn/server");
|
|
13
|
+
const shell_1 = require("@things-factory/shell");
|
|
14
|
+
const auth_error_js_1 = require("../errors/auth-error.js");
|
|
15
|
+
const web_auth_credential_js_1 = require("../service/web-auth-credential/web-auth-credential.js");
|
|
16
|
+
const signin_js_1 = require("./signin.js");
|
|
17
|
+
/**
|
|
18
|
+
* Passkeys (ADR-0156).
|
|
19
|
+
*
|
|
20
|
+
* Every ceremony asks the authenticator to verify the person - fingerprint, face or device PIN - and
|
|
21
|
+
* the server refuses an answer without that flag (decision 2). Registration leaves the authenticator
|
|
22
|
+
* to the browser, so a phone or a security key can be chosen as well as this device; that is also
|
|
23
|
+
* what lets a phone sign a computer in (decision 3).
|
|
24
|
+
*
|
|
25
|
+
* A challenge is good for one answer. It is taken out of the session before the answer is checked,
|
|
26
|
+
* so a second try with the same challenge finds none.
|
|
27
|
+
*/
|
|
28
|
+
const USER_VERIFICATION = 'required';
|
|
29
|
+
function siteOf(context) {
|
|
30
|
+
return { rpID: context.hostname, origin: context.origin };
|
|
31
|
+
}
|
|
32
|
+
function rememberChallenge(context, challenge) {
|
|
33
|
+
context.session.challenge = challenge;
|
|
34
|
+
}
|
|
35
|
+
/** The session's challenge, removed - whatever the answer turns out to be. */
|
|
36
|
+
function takeChallenge(context) {
|
|
37
|
+
const challenge = context.session?.challenge;
|
|
38
|
+
if (context.session)
|
|
39
|
+
delete context.session.challenge;
|
|
40
|
+
if (!challenge) {
|
|
41
|
+
throw new auth_error_js_1.AuthError({ errorCode: auth_error_js_1.AuthError.ERROR_CODES.AUTHN_VERIFICATION_FAILED, detail: { reason: 'no challenge' } });
|
|
42
|
+
}
|
|
43
|
+
return challenge;
|
|
44
|
+
}
|
|
45
|
+
/** What to ask the browser when a signed-in person adds a passkey. */
|
|
46
|
+
async function registrationOptions(user, rpName, site) {
|
|
47
|
+
const registered = await (0, shell_1.getRepository)(web_auth_credential_js_1.WebAuthCredential).find({ where: { user: { id: user.id } } });
|
|
48
|
+
return await (0, server_1.generateRegistrationOptions)({
|
|
49
|
+
rpName,
|
|
50
|
+
rpID: site.rpID,
|
|
51
|
+
userName: user.email,
|
|
52
|
+
userDisplayName: user.name,
|
|
53
|
+
attestationType: 'none',
|
|
54
|
+
/* The browser says so when this authenticator already holds one of this person's passkeys. */
|
|
55
|
+
excludeCredentials: registered.map(credential => ({
|
|
56
|
+
id: credential.credentialId,
|
|
57
|
+
transports: (credential.transports ?? undefined)
|
|
58
|
+
})),
|
|
59
|
+
authenticatorSelection: {
|
|
60
|
+
/* A passkey that cannot sign in without a name typed first is not taken (decision 2). */
|
|
61
|
+
residentKey: 'required',
|
|
62
|
+
userVerification: USER_VERIFICATION
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* What to ask the browser when somebody signs in. No credential is named: the browser offers what
|
|
68
|
+
* it holds for this site, or a phone nearby.
|
|
69
|
+
*/
|
|
70
|
+
async function signinOptions(site) {
|
|
71
|
+
return await (0, server_1.generateAuthenticationOptions)({ rpID: site.rpID, userVerification: USER_VERIFICATION });
|
|
72
|
+
}
|
|
73
|
+
/** What to ask the browser when a signed-in person confirms it is them - only their own passkeys. */
|
|
74
|
+
async function reverifyOptions(user, site) {
|
|
75
|
+
const registered = await (0, shell_1.getRepository)(web_auth_credential_js_1.WebAuthCredential).find({ where: { user: { id: user.id } } });
|
|
76
|
+
if (registered.length === 0)
|
|
77
|
+
return null;
|
|
78
|
+
return await (0, server_1.generateAuthenticationOptions)({
|
|
79
|
+
rpID: site.rpID,
|
|
80
|
+
userVerification: USER_VERIFICATION,
|
|
81
|
+
allowCredentials: registered.map(credential => ({
|
|
82
|
+
id: credential.credentialId,
|
|
83
|
+
transports: (credential.transports ?? undefined)
|
|
84
|
+
}))
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function verificationFailed(reason) {
|
|
88
|
+
return new auth_error_js_1.AuthError({ errorCode: auth_error_js_1.AuthError.ERROR_CODES.AUTHN_VERIFICATION_FAILED, detail: { reason } });
|
|
89
|
+
}
|
|
90
|
+
/** Keeps the passkey the browser made for `user`. `name` is the person's label for it. */
|
|
91
|
+
async function registerPasskey(user, response, name, challenge, site) {
|
|
92
|
+
let verification;
|
|
93
|
+
try {
|
|
94
|
+
verification = await (0, server_1.verifyRegistrationResponse)({
|
|
95
|
+
response,
|
|
96
|
+
expectedChallenge: challenge,
|
|
97
|
+
expectedOrigin: site.origin,
|
|
98
|
+
expectedRPID: site.rpID,
|
|
99
|
+
expectedType: 'webauthn.create',
|
|
100
|
+
requireUserVerification: true
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
throw verificationFailed(error?.message || 'registration not verified');
|
|
105
|
+
}
|
|
106
|
+
if (!verification.verified || !verification.registrationInfo) {
|
|
107
|
+
throw verificationFailed('registration not verified');
|
|
108
|
+
}
|
|
109
|
+
const { credential } = verification.registrationInfo;
|
|
110
|
+
const label = name?.trim().slice(0, 64);
|
|
111
|
+
return await (0, shell_1.getRepository)(web_auth_credential_js_1.WebAuthCredential).save({
|
|
112
|
+
user,
|
|
113
|
+
credentialId: credential.id,
|
|
114
|
+
publicKey: Buffer.from(credential.publicKey).toString('base64'),
|
|
115
|
+
counter: credential.counter,
|
|
116
|
+
transports: credential.transports?.length ? credential.transports : (response.response?.transports ?? null),
|
|
117
|
+
name: label || null,
|
|
118
|
+
creator: user,
|
|
119
|
+
updater: user
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Checks an answer against the stored passkey and moves its counter on. `ownerId`, when given, is
|
|
124
|
+
* who the passkey must belong to - a confirmation is answered only by the person's own passkey.
|
|
125
|
+
* `attempt.userId` is filled as soon as the passkey is found, so a failed sign-in names its owner.
|
|
126
|
+
*/
|
|
127
|
+
async function verifyAssertion(response, challenge, site, attempt = {}, ownerId) {
|
|
128
|
+
const repository = (0, shell_1.getRepository)(web_auth_credential_js_1.WebAuthCredential);
|
|
129
|
+
const credential = response?.id
|
|
130
|
+
? await repository.findOne({ where: { credentialId: response.id }, relations: ['user', 'user.domains'] })
|
|
131
|
+
: null;
|
|
132
|
+
if (!credential?.user) {
|
|
133
|
+
throw verificationFailed('unknown credential');
|
|
134
|
+
}
|
|
135
|
+
attempt.userId = credential.user.id;
|
|
136
|
+
if (ownerId && credential.user.id !== ownerId) {
|
|
137
|
+
throw verificationFailed('credential of another user');
|
|
138
|
+
}
|
|
139
|
+
let verification;
|
|
140
|
+
try {
|
|
141
|
+
verification = await (0, server_1.verifyAuthenticationResponse)({
|
|
142
|
+
response,
|
|
143
|
+
expectedChallenge: challenge,
|
|
144
|
+
expectedOrigin: site.origin,
|
|
145
|
+
expectedRPID: site.rpID,
|
|
146
|
+
requireUserVerification: true,
|
|
147
|
+
credential: {
|
|
148
|
+
id: credential.credentialId,
|
|
149
|
+
publicKey: new Uint8Array(Buffer.from(credential.publicKey, 'base64')),
|
|
150
|
+
counter: credential.counter,
|
|
151
|
+
transports: (credential.transports ?? undefined)
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
throw verificationFailed(error?.message || 'assertion not verified');
|
|
157
|
+
}
|
|
158
|
+
if (!verification.verified) {
|
|
159
|
+
throw verificationFailed('assertion not verified');
|
|
160
|
+
}
|
|
161
|
+
await repository.update({ id: credential.id }, { counter: verification.authenticationInfo.newCounter });
|
|
162
|
+
return credential;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Signing in with a passkey: the answer is checked, then the account goes through the same gate as
|
|
166
|
+
* a password sign-in - deleted, locked, inactive - and the sign-in is audited with its method
|
|
167
|
+
* (ADR-0156 decision 4). A passkey does not lift a lock and does not count as a failed password.
|
|
168
|
+
*/
|
|
169
|
+
async function signinWithPasskey(response, challenge, site, context) {
|
|
170
|
+
return await (0, signin_js_1.auditedSignin)('passkey', context, async (attempt) => {
|
|
171
|
+
const credential = await verifyAssertion(response, challenge, site, attempt);
|
|
172
|
+
const answer = await (0, signin_js_1.admitSignin)(credential.user, context, async () => { });
|
|
173
|
+
await (0, shell_1.getRepository)(web_auth_credential_js_1.WebAuthCredential).update({ id: credential.id }, { lastUsedAt: new Date() });
|
|
174
|
+
return answer;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/** A signed-in person confirming it is them, with one of their own passkeys. */
|
|
178
|
+
async function reverifyWithPasskey(user, response, challenge, site) {
|
|
179
|
+
const credential = await verifyAssertion(response, challenge, site, {}, user.id);
|
|
180
|
+
await (0, shell_1.getRepository)(web_auth_credential_js_1.WebAuthCredential).update({ id: credential.id }, { lastUsedAt: new Date() });
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=passkey.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"passkey.js","sourceRoot":"","sources":["../../server/controllers/passkey.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n generateAuthenticationOptions,\n generateRegistrationOptions,\n verifyAuthenticationResponse,\n verifyRegistrationResponse,\n type AuthenticationResponseJSON,\n type AuthenticatorTransportFuture,\n type PublicKeyCredentialCreationOptionsJSON,\n type PublicKeyCredentialRequestOptionsJSON,\n type RegistrationResponseJSON\n} from '@simplewebauthn/server'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { AuthError } from '../errors/auth-error.js'\nimport { User } from '../service/user/user.js'\nimport { WebAuthCredential } from '../service/web-auth-credential/web-auth-credential.js'\nimport { admitSignin, auditedSignin } from './signin.js'\n\n/**\n * Passkeys (ADR-0156).\n *\n * Every ceremony asks the authenticator to verify the person - fingerprint, face or device PIN - and\n * the server refuses an answer without that flag (decision 2). Registration leaves the authenticator\n * to the browser, so a phone or a security key can be chosen as well as this device; that is also\n * what lets a phone sign a computer in (decision 3).\n *\n * A challenge is good for one answer. It is taken out of the session before the answer is checked,\n * so a second try with the same challenge finds none.\n */\n\nconst USER_VERIFICATION = 'required' as const\n\n/** Where a ceremony runs. The relying party is the host the person reached (ADR-0156 「남기는 것」). */\nexport type PasskeySite = { rpID: string; origin: string }\n\nexport function siteOf(context): PasskeySite {\n return { rpID: context.hostname, origin: context.origin }\n}\n\nexport function rememberChallenge(context, challenge: string) {\n context.session.challenge = challenge\n}\n\n/** The session's challenge, removed - whatever the answer turns out to be. */\nexport function takeChallenge(context): string {\n const challenge = context.session?.challenge\n if (context.session) delete context.session.challenge\n\n if (!challenge) {\n throw new AuthError({ errorCode: AuthError.ERROR_CODES.AUTHN_VERIFICATION_FAILED, detail: { reason: 'no challenge' } })\n }\n\n return challenge\n}\n\n/** What to ask the browser when a signed-in person adds a passkey. */\nexport async function registrationOptions(\n user: User,\n rpName: string,\n site: PasskeySite\n): Promise<PublicKeyCredentialCreationOptionsJSON> {\n const registered = await getRepository(WebAuthCredential).find({ where: { user: { id: user.id } } })\n\n return await generateRegistrationOptions({\n rpName,\n rpID: site.rpID,\n userName: user.email,\n userDisplayName: user.name,\n attestationType: 'none',\n /* The browser says so when this authenticator already holds one of this person's passkeys. */\n excludeCredentials: registered.map(credential => ({\n id: credential.credentialId,\n transports: (credential.transports ?? undefined) as AuthenticatorTransportFuture[]\n })),\n authenticatorSelection: {\n /* A passkey that cannot sign in without a name typed first is not taken (decision 2). */\n residentKey: 'required',\n userVerification: USER_VERIFICATION\n }\n })\n}\n\n/**\n * What to ask the browser when somebody signs in. No credential is named: the browser offers what\n * it holds for this site, or a phone nearby.\n */\nexport async function signinOptions(site: PasskeySite): Promise<PublicKeyCredentialRequestOptionsJSON> {\n return await generateAuthenticationOptions({ rpID: site.rpID, userVerification: USER_VERIFICATION })\n}\n\n/** What to ask the browser when a signed-in person confirms it is them - only their own passkeys. */\nexport async function reverifyOptions(user: User, site: PasskeySite): Promise<PublicKeyCredentialRequestOptionsJSON | null> {\n const registered = await getRepository(WebAuthCredential).find({ where: { user: { id: user.id } } })\n if (registered.length === 0) return null\n\n return await generateAuthenticationOptions({\n rpID: site.rpID,\n userVerification: USER_VERIFICATION,\n allowCredentials: registered.map(credential => ({\n id: credential.credentialId,\n transports: (credential.transports ?? undefined) as AuthenticatorTransportFuture[]\n }))\n })\n}\n\nfunction verificationFailed(reason: string): AuthError {\n return new AuthError({ errorCode: AuthError.ERROR_CODES.AUTHN_VERIFICATION_FAILED, detail: { reason } })\n}\n\n/** Keeps the passkey the browser made for `user`. `name` is the person's label for it. */\nexport async function registerPasskey(\n user: User,\n response: RegistrationResponseJSON,\n name: string | undefined,\n challenge: string,\n site: PasskeySite\n): Promise<WebAuthCredential> {\n let verification: Awaited<ReturnType<typeof verifyRegistrationResponse>>\n try {\n verification = await verifyRegistrationResponse({\n response,\n expectedChallenge: challenge,\n expectedOrigin: site.origin,\n expectedRPID: site.rpID,\n expectedType: 'webauthn.create',\n requireUserVerification: true\n })\n } catch (error) {\n throw verificationFailed(error?.message || 'registration not verified')\n }\n\n if (!verification.verified || !verification.registrationInfo) {\n throw verificationFailed('registration not verified')\n }\n\n const { credential } = verification.registrationInfo\n const label = name?.trim().slice(0, 64)\n\n return await getRepository(WebAuthCredential).save({\n user,\n credentialId: credential.id,\n publicKey: Buffer.from(credential.publicKey).toString('base64'),\n counter: credential.counter,\n transports: credential.transports?.length ? credential.transports : (response.response?.transports ?? null),\n name: label || null,\n creator: user,\n updater: user\n })\n}\n\n/**\n * Checks an answer against the stored passkey and moves its counter on. `ownerId`, when given, is\n * who the passkey must belong to - a confirmation is answered only by the person's own passkey.\n * `attempt.userId` is filled as soon as the passkey is found, so a failed sign-in names its owner.\n */\nasync function verifyAssertion(\n response: AuthenticationResponseJSON,\n challenge: string,\n site: PasskeySite,\n attempt: { userId?: string } = {},\n ownerId?: string\n): Promise<WebAuthCredential> {\n const repository = getRepository(WebAuthCredential)\n const credential = response?.id\n ? await repository.findOne({ where: { credentialId: response.id }, relations: ['user', 'user.domains'] })\n : null\n\n if (!credential?.user) {\n throw verificationFailed('unknown credential')\n }\n\n attempt.userId = credential.user.id\n\n if (ownerId && credential.user.id !== ownerId) {\n throw verificationFailed('credential of another user')\n }\n\n let verification: Awaited<ReturnType<typeof verifyAuthenticationResponse>>\n try {\n verification = await verifyAuthenticationResponse({\n response,\n expectedChallenge: challenge,\n expectedOrigin: site.origin,\n expectedRPID: site.rpID,\n requireUserVerification: true,\n credential: {\n id: credential.credentialId,\n publicKey: new Uint8Array(Buffer.from(credential.publicKey, 'base64')),\n counter: credential.counter,\n transports: (credential.transports ?? undefined) as AuthenticatorTransportFuture[]\n }\n })\n } catch (error) {\n throw verificationFailed(error?.message || 'assertion not verified')\n }\n\n if (!verification.verified) {\n throw verificationFailed('assertion not verified')\n }\n\n await repository.update({ id: credential.id }, { counter: verification.authenticationInfo.newCounter })\n\n return credential\n}\n\n/**\n * Signing in with a passkey: the answer is checked, then the account goes through the same gate as\n * a password sign-in - deleted, locked, inactive - and the sign-in is audited with its method\n * (ADR-0156 decision 4). A passkey does not lift a lock and does not count as a failed password.\n */\nexport async function signinWithPasskey(response: AuthenticationResponseJSON, challenge: string, site: PasskeySite, context?) {\n return await auditedSignin('passkey', context, async attempt => {\n const credential = await verifyAssertion(response, challenge, site, attempt)\n const answer = await admitSignin(credential.user, context, async () => {})\n\n await getRepository(WebAuthCredential).update({ id: credential.id }, { lastUsedAt: new Date() })\n\n return answer\n })\n}\n\n/** A signed-in person confirming it is them, with one of their own passkeys. */\nexport async function reverifyWithPasskey(user: User, response: AuthenticationResponseJSON, challenge: string, site: PasskeySite) {\n const credential = await verifyAssertion(response, challenge, site, {}, user.id)\n await getRepository(WebAuthCredential).update({ id: credential.id }, { lastUsedAt: new Date() })\n}\n"]}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { User } from '../service/user/user.js';
|
|
2
|
+
export type SigninMethod = 'password' | 'passkey';
|
|
3
|
+
export type SigninAttempt = {
|
|
4
|
+
userId?: string;
|
|
5
|
+
};
|
|
2
6
|
/**
|
|
3
7
|
* A sign-in, and its audit (ADR-0141 §3.3): a success or a failure with its code. A failure is
|
|
4
8
|
* written on its own and stays when the request fails (§4-3). An account that does not exist leaves
|
|
@@ -12,3 +16,23 @@ export declare function signin(attrs: {
|
|
|
12
16
|
token: string;
|
|
13
17
|
domains: import("@things-factory/shell").Domain[];
|
|
14
18
|
}>;
|
|
19
|
+
/**
|
|
20
|
+
* The audit around one way of signing in. `run` puts the person in `attempt` as soon as it knows
|
|
21
|
+
* who it is, so a failure after that names them. `detail.method` tells a password from a passkey
|
|
22
|
+
* (ADR-0156 decision 4).
|
|
23
|
+
*/
|
|
24
|
+
export declare function auditedSignin<T extends {
|
|
25
|
+
user: User;
|
|
26
|
+
}>(method: SigninMethod, context: any, run: (attempt: SigninAttempt) => Promise<T>): Promise<T>;
|
|
27
|
+
/**
|
|
28
|
+
* The account gate every way of signing in goes through (ADR-0156 decision 4): deleted, locked -
|
|
29
|
+
* and a lock that has run out lifts here - then the secret, then inactive. Only `verifySecret`
|
|
30
|
+
* differs between a password and a passkey; a passkey arrives already verified and passes nothing.
|
|
31
|
+
*
|
|
32
|
+
* `user` must carry `domains`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function admitSignin(user: User, context: any, verifySecret: () => Promise<void>): Promise<{
|
|
35
|
+
user: User;
|
|
36
|
+
token: string;
|
|
37
|
+
domains: import("@things-factory/shell").Domain[];
|
|
38
|
+
}>;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.signin = signin;
|
|
4
|
+
exports.auditedSignin = auditedSignin;
|
|
5
|
+
exports.admitSignin = admitSignin;
|
|
4
6
|
const typeorm_1 = require("typeorm");
|
|
5
7
|
const shell_1 = require("@things-factory/shell");
|
|
6
8
|
const unlock_user_js_1 = require("../controllers/unlock-user.js");
|
|
@@ -16,24 +18,32 @@ const audit_target_js_1 = require("../service/audit/audit-target.js");
|
|
|
16
18
|
* no actor - the name typed in is not a person.
|
|
17
19
|
*/
|
|
18
20
|
async function signin(attrs, context) {
|
|
21
|
+
return await auditedSignin('password', context, attempt => signinOnce(attrs, context, attempt));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The audit around one way of signing in. `run` puts the person in `attempt` as soon as it knows
|
|
25
|
+
* who it is, so a failure after that names them. `detail.method` tells a password from a passkey
|
|
26
|
+
* (ADR-0156 decision 4).
|
|
27
|
+
*/
|
|
28
|
+
async function auditedSignin(method, context, run) {
|
|
19
29
|
const attempt = {};
|
|
20
30
|
const domainId = context?.state?.domain?.id ?? null;
|
|
31
|
+
const detail = { method };
|
|
21
32
|
/*
|
|
22
33
|
* Not awaited: the answer does not wait on its record, and a write that fails is counted, never
|
|
23
34
|
* thrown (`recordAuditAction` without a transaction).
|
|
24
35
|
*/
|
|
25
36
|
try {
|
|
26
|
-
const answer = await
|
|
27
|
-
void (0, audit_recorder_js_1.recordAuditAction)({ kind: 'auth.signin', outcome: 'ok' }, { actorId: answer.user.id, domainId });
|
|
37
|
+
const answer = await run(attempt);
|
|
38
|
+
void (0, audit_recorder_js_1.recordAuditAction)({ kind: 'auth.signin', outcome: 'ok', detail }, { actorId: answer.user.id, domainId });
|
|
28
39
|
return answer;
|
|
29
40
|
}
|
|
30
41
|
catch (error) {
|
|
31
|
-
void (0, audit_recorder_js_1.recordAuditAction)({ kind: 'auth.signin', outcome: 'error', code: (0, audit_target_js_1.errorCodeOf)(error) }, { actorId: attempt.userId ?? null, domainId });
|
|
42
|
+
void (0, audit_recorder_js_1.recordAuditAction)({ kind: 'auth.signin', outcome: 'error', code: (0, audit_target_js_1.errorCodeOf)(error), detail }, { actorId: attempt.userId ?? null, domainId });
|
|
32
43
|
throw error;
|
|
33
44
|
}
|
|
34
45
|
}
|
|
35
46
|
async function signinOnce(attrs, context, attempt) {
|
|
36
|
-
const { domain } = context?.state || {};
|
|
37
47
|
const { username } = attrs;
|
|
38
48
|
const repository = (0, shell_1.getRepository)(user_js_1.User);
|
|
39
49
|
var user = await repository.findOne({
|
|
@@ -53,6 +63,54 @@ async function signinOnce(attrs, context, attempt) {
|
|
|
53
63
|
errorCode: auth_error_js_1.AuthError.ERROR_CODES.USER_NOT_FOUND
|
|
54
64
|
});
|
|
55
65
|
attempt.userId = user.id;
|
|
66
|
+
return await admitSignin(user, context, async () => {
|
|
67
|
+
if (!user_js_1.User.verify(user.password, attrs.password, user.salt)) {
|
|
68
|
+
user.failCount++;
|
|
69
|
+
if (user.failCount >= 5) {
|
|
70
|
+
user.status = user_js_1.UserStatus.LOCKED;
|
|
71
|
+
/* 언제 풀리나를 계산할 수 있게 시각을 남긴다. 이 칸이 비면 판정 규칙이
|
|
72
|
+
잠금을 유지하는 쪽으로 기운다. */
|
|
73
|
+
user.lockedAt = new Date();
|
|
74
|
+
}
|
|
75
|
+
await repository.save(user);
|
|
76
|
+
if (user.status == user_js_1.UserStatus.LOCKED) {
|
|
77
|
+
(0, unlock_user_js_1.sendUnlockUserEmail)({
|
|
78
|
+
user,
|
|
79
|
+
context
|
|
80
|
+
});
|
|
81
|
+
throw new auth_error_js_1.AuthError({
|
|
82
|
+
errorCode: auth_error_js_1.AuthError.ERROR_CODES.USER_LOCKED,
|
|
83
|
+
detail: {
|
|
84
|
+
username: user.username,
|
|
85
|
+
email: user.email
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
throw new auth_error_js_1.AuthError({
|
|
90
|
+
errorCode: auth_error_js_1.AuthError.ERROR_CODES.PASSWORD_NOT_MATCHED,
|
|
91
|
+
detail: {
|
|
92
|
+
username: user.username,
|
|
93
|
+
email: user.email,
|
|
94
|
+
failCount: user.failCount
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
user.failCount = 0;
|
|
100
|
+
await repository.save(user);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The account gate every way of signing in goes through (ADR-0156 decision 4): deleted, locked -
|
|
106
|
+
* and a lock that has run out lifts here - then the secret, then inactive. Only `verifySecret`
|
|
107
|
+
* differs between a password and a passkey; a passkey arrives already verified and passes nothing.
|
|
108
|
+
*
|
|
109
|
+
* `user` must carry `domains`.
|
|
110
|
+
*/
|
|
111
|
+
async function admitSignin(user, context, verifySecret) {
|
|
112
|
+
const { domain } = context?.state || {};
|
|
113
|
+
const repository = (0, shell_1.getRepository)(user_js_1.User);
|
|
56
114
|
if (user.status == user_js_1.UserStatus.DELETED) {
|
|
57
115
|
throw new auth_error_js_1.AuthError({
|
|
58
116
|
errorCode: auth_error_js_1.AuthError.ERROR_CODES.USER_DELETED
|
|
@@ -86,41 +144,7 @@ async function signinOnce(attrs, context, attempt) {
|
|
|
86
144
|
user.lockedAt = null;
|
|
87
145
|
await repository.save(user);
|
|
88
146
|
}
|
|
89
|
-
|
|
90
|
-
user.failCount++;
|
|
91
|
-
if (user.failCount >= 5) {
|
|
92
|
-
user.status = user_js_1.UserStatus.LOCKED;
|
|
93
|
-
/* 언제 풀리나를 계산할 수 있게 시각을 남긴다. 이 칸이 비면 판정 규칙이
|
|
94
|
-
잠금을 유지하는 쪽으로 기운다. */
|
|
95
|
-
user.lockedAt = new Date();
|
|
96
|
-
}
|
|
97
|
-
await repository.save(user);
|
|
98
|
-
if (user.status == user_js_1.UserStatus.LOCKED) {
|
|
99
|
-
(0, unlock_user_js_1.sendUnlockUserEmail)({
|
|
100
|
-
user,
|
|
101
|
-
context
|
|
102
|
-
});
|
|
103
|
-
throw new auth_error_js_1.AuthError({
|
|
104
|
-
errorCode: auth_error_js_1.AuthError.ERROR_CODES.USER_LOCKED,
|
|
105
|
-
detail: {
|
|
106
|
-
username: user.username,
|
|
107
|
-
email: user.email
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
throw new auth_error_js_1.AuthError({
|
|
112
|
-
errorCode: auth_error_js_1.AuthError.ERROR_CODES.PASSWORD_NOT_MATCHED,
|
|
113
|
-
detail: {
|
|
114
|
-
username: user.username,
|
|
115
|
-
email: user.email,
|
|
116
|
-
failCount: user.failCount
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
user.failCount = 0;
|
|
122
|
-
await repository.save(user);
|
|
123
|
-
}
|
|
147
|
+
await verifySecret();
|
|
124
148
|
if (user.status == user_js_1.UserStatus.INACTIVE) {
|
|
125
149
|
throw new auth_error_js_1.AuthError({
|
|
126
150
|
errorCode: auth_error_js_1.AuthError.ERROR_CODES.USER_NOT_ACTIVATED,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signin.js","sourceRoot":"","sources":["../../server/controllers/signin.ts"],"names":[],"mappings":"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","sourcesContent":["import { ILike } from 'typeorm'\nimport { getRepository } from '@things-factory/shell'\n\nimport { sendUnlockUserEmail } from '../controllers/unlock-user.js'\nimport { AuthError } from '../errors/auth-error.js'\nimport { User, UserStatus } from '../service/user/user.js'\nimport { isAccountLocked } from '../utils/account-lock-rule.js'\nimport { lockDurationMinutes } from '../utils/lock-duration.js'\nimport { recordAuditAction } from '../service/audit/audit-recorder.js'\nimport { errorCodeOf } from '../service/audit/audit-target.js'\n\n/**\n * A sign-in, and its audit (ADR-0141 §3.3): a success or a failure with its code. A failure is\n * written on its own and stays when the request fails (§4-3). An account that does not exist leaves\n * no actor - the name typed in is not a person.\n */\nexport async function signin(attrs: { username: string; password: string }, context?) {\n const attempt: { userId?: string } = {}\n const domainId = context?.state?.domain?.id ?? null\n\n /*\n * Not awaited: the answer does not wait on its record, and a write that fails is counted, never\n * thrown (`recordAuditAction` without a transaction).\n */\n try {\n const answer = await signinOnce(attrs, context, attempt)\n void recordAuditAction({ kind: 'auth.signin', outcome: 'ok' }, { actorId: answer.user.id, domainId })\n return answer\n } catch (error) {\n void recordAuditAction({ kind: 'auth.signin', outcome: 'error', code: errorCodeOf(error) }, { actorId: attempt.userId ?? null, domainId })\n throw error\n }\n}\n\nasync function signinOnce(attrs: { username: string; password: string }, context, attempt: { userId?: string }) {\n const { domain } = context?.state || {}\n const { username } = attrs\n\n const repository = getRepository(User)\n\n var user = await repository.findOne({\n where: { username },\n relations: ['domains']\n })\n\n if (!user && /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(username)) {\n user = await repository.findOne({\n where: {\n email: ILike(username)\n },\n relations: ['domains']\n })\n }\n\n if (!user)\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.USER_NOT_FOUND\n })\n\n attempt.userId = user.id\n\n if (user.status == UserStatus.DELETED) {\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.USER_DELETED\n })\n }\n\n if (user.status == UserStatus.LOCKED) {\n /* 시계는 한 번만 읽는다 — 두 번 읽으면 경계에 걸린 잠금이 두 판정 사이에서 갈린다. */\n const now = new Date()\n\n if (isAccountLocked(user, now, lockDurationMinutes())) {\n sendUnlockUserEmail({\n user,\n context\n })\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.USER_LOCKED,\n detail: {\n username: user.username,\n email: user.email\n }\n })\n }\n\n /*\n * 시간이 지나 스스로 풀렸다 — 그 자리에서 행을 되돌리고 계속 진행한다. 주기 작업으로\n * 풀지 않는 이유는 `account-lock-rule.ts` 에 있다(계정이 10만이면 그것이 따로 일이 된다).\n *\n * `failCount` 도 함께 0 으로 둔다. 그래서 **잠금이 곧 실패 셈의 창**이 된다 — 이 계정이\n * 다시 잠기려면 지금부터 다섯 번을 틀려야 한다.\n */\n user.status = UserStatus.ACTIVATED\n user.failCount = 0\n user.lockedAt = null\n\n await repository.save(user)\n }\n\n if (!User.verify(user.password, attrs.password, user.salt)) {\n user.failCount++\n if (user.failCount >= 5) {\n user.status = UserStatus.LOCKED\n /* 언제 풀리나를 계산할 수 있게 시각을 남긴다. 이 칸이 비면 판정 규칙이\n 잠금을 유지하는 쪽으로 기운다. */\n user.lockedAt = new Date()\n }\n await repository.save(user)\n if (user.status == UserStatus.LOCKED) {\n sendUnlockUserEmail({\n user,\n context\n })\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.USER_LOCKED,\n detail: {\n username: user.username,\n email: user.email\n }\n })\n }\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.PASSWORD_NOT_MATCHED,\n detail: {\n username: user.username,\n email: user.email,\n failCount: user.failCount\n }\n })\n } else {\n user.failCount = 0\n await repository.save(user)\n }\n\n if (user.status == UserStatus.INACTIVE) {\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.USER_NOT_ACTIVATED,\n detail: {\n username: user.username,\n email: user.email\n }\n })\n }\n\n return {\n user,\n token: await user.sign({ subdomain: domain?.subdomain }),\n domains: user.domains || []\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"signin.js","sourceRoot":"","sources":["../../server/controllers/signin.ts"],"names":[],"mappings":";;AAoBA,wBAEC;AAOD,sCAqBC;AAyED,kCA2DC;AAtLD,qCAA+B;AAC/B,iDAAqD;AAErD,kEAAmE;AACnE,2DAAmD;AACnD,qDAA0D;AAC1D,wEAA+D;AAC/D,gEAA+D;AAC/D,0EAAsE;AACtE,sEAA8D;AAM9D;;;;GAIG;AACI,KAAK,UAAU,MAAM,CAAC,KAA6C,EAAE,OAAQ;IAClF,OAAO,MAAM,aAAa,CAAC,UAAU,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAA;AACjG,CAAC;AAED;;;;GAIG;AACI,KAAK,UAAU,aAAa,CACjC,MAAoB,EACpB,OAAO,EACP,GAA2C;IAE3C,MAAM,OAAO,GAAkB,EAAE,CAAA;IACjC,MAAM,QAAQ,GAAG,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,IAAI,IAAI,CAAA;IACnD,MAAM,MAAM,GAAG,EAAE,MAAM,EAAE,CAAA;IAEzB;;;OAGG;IACH,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,CAAA;QACjC,KAAK,IAAA,qCAAiB,EAAC,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAA;QAC7G,OAAO,MAAM,CAAA;IACf,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,KAAK,IAAA,qCAAiB,EAAC,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAA,6BAAW,EAAC,KAAK,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,IAAI,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAA;QAClJ,MAAM,KAAK,CAAA;IACb,CAAC;AACH,CAAC;AAED,KAAK,UAAU,UAAU,CAAC,KAA6C,EAAE,OAAO,EAAE,OAAsB;IACtG,MAAM,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAA;IAE1B,MAAM,UAAU,GAAG,IAAA,qBAAa,EAAC,cAAI,CAAC,CAAA;IAEtC,IAAI,IAAI,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC;QAClC,KAAK,EAAE,EAAE,QAAQ,EAAE;QACnB,SAAS,EAAE,CAAC,SAAS,CAAC;KACvB,CAAC,CAAA;IAEF,IAAI,CAAC,IAAI,IAAI,4BAA4B,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzD,IAAI,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC;YAC9B,KAAK,EAAE;gBACL,KAAK,EAAE,IAAA,eAAK,EAAC,QAAQ,CAAC;aACvB;YACD,SAAS,EAAE,CAAC,SAAS,CAAC;SACvB,CAAC,CAAA;IACJ,CAAC;IAED,IAAI,CAAC,IAAI;QACP,MAAM,IAAI,yBAAS,CAAC;YAClB,SAAS,EAAE,yBAAS,CAAC,WAAW,CAAC,cAAc;SAChD,CAAC,CAAA;IAEJ,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAA;IAExB,OAAO,MAAM,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,EAAE;QACjD,IAAI,CAAC,cAAI,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3D,IAAI,CAAC,SAAS,EAAE,CAAA;YAChB,IAAI,IAAI,CAAC,SAAS,IAAI,CAAC,EAAE,CAAC;gBACxB,IAAI,CAAC,MAAM,GAAG,oBAAU,CAAC,MAAM,CAAA;gBAC/B;uCACuB;gBACvB,IAAI,CAAC,QAAQ,GAAG,IAAI,IAAI,EAAE,CAAA;YAC5B,CAAC;YACD,MAAM,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YAC3B,IAAI,IAAI,CAAC,MAAM,IAAI,oBAAU,CAAC,MAAM,EAAE,CAAC;gBACrC,IAAA,oCAAmB,EAAC;oBAClB,IAAI;oBACJ,OAAO;iBACR,CAAC,CAAA;gBACF,MAAM,IAAI,yBAAS,CAAC;oBAClB,SAAS,EAAE,yBAAS,CAAC,WAAW,CAAC,WAAW;oBAC5C,MAAM,EAAE;wBACN,QAAQ,EAAE,IAAI,CAAC,QAAQ;wBACvB,KAAK,EAAE,IAAI,CAAC,KAAK;qBAClB;iBACF,CAAC,CAAA;YACJ,CAAC;YACD,MAAM,IAAI,yBAAS,CAAC;gBAClB,SAAS,EAAE,yBAAS,CAAC,WAAW,CAAC,oBAAoB;gBACrD,MAAM,EAAE;oBACN,QAAQ,EAAE,IAAI,CAAC,QAAQ;oBACvB,KAAK,EAAE,IAAI,CAAC,KAAK;oBACjB,SAAS,EAAE,IAAI,CAAC,SAAS;iBAC1B;aACF,CAAC,CAAA;QACJ,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,SAAS,GAAG,CAAC,CAAA;YAClB,MAAM,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC7B,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;GAMG;AACI,KAAK,UAAU,WAAW,CAAC,IAAU,EAAE,OAAO,EAAE,YAAiC;IACtF,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,EAAE,KAAK,IAAI,EAAE,CAAA;IACvC,MAAM,UAAU,GAAG,IAAA,qBAAa,EAAC,cAAI,CAAC,CAAA;IAEtC,IAAI,IAAI,CAAC,MAAM,IAAI,oBAAU,CAAC,OAAO,EAAE,CAAC;QACtC,MAAM,IAAI,yBAAS,CAAC;YAClB,SAAS,EAAE,yBAAS,CAAC,WAAW,CAAC,YAAY;SAC9C,CAAC,CAAA;IACJ,CAAC;IAED,IAAI,IAAI,CAAC,MAAM,IAAI,oBAAU,CAAC,MAAM,EAAE,CAAC;QACrC,sDAAsD;QACtD,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAA;QAEtB,IAAI,IAAA,sCAAe,EAAC,IAAI,EAAE,GAAG,EAAE,IAAA,sCAAmB,GAAE,CAAC,EAAE,CAAC;YACtD,IAAA,oCAAmB,EAAC;gBAClB,IAAI;gBACJ,OAAO;aACR,CAAC,CAAA;YACF,MAAM,IAAI,yBAAS,CAAC;gBAClB,SAAS,EAAE,yBAAS,CAAC,WAAW,CAAC,WAAW;gBAC5C,MAAM,EAAE;oBACN,QAAQ,EAAE,IAAI,CAAC,QAAQ;oBACvB,KAAK,EAAE,IAAI,CAAC,KAAK;iBAClB;aACF,CAAC,CAAA;QACJ,CAAC;QAED;;;;;;WAMG;QACH,IAAI,CAAC,MAAM,GAAG,oBAAU,CAAC,SAAS,CAAA;QAClC,IAAI,CAAC,SAAS,GAAG,CAAC,CAAA;QAClB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAA;QAEpB,MAAM,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC7B,CAAC;IAED,MAAM,YAAY,EAAE,CAAA;IAEpB,IAAI,IAAI,CAAC,MAAM,IAAI,oBAAU,CAAC,QAAQ,EAAE,CAAC;QACvC,MAAM,IAAI,yBAAS,CAAC;YAClB,SAAS,EAAE,yBAAS,CAAC,WAAW,CAAC,kBAAkB;YACnD,MAAM,EAAE;gBACN,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,KAAK,EAAE,IAAI,CAAC,KAAK;aAClB;SACF,CAAC,CAAA;IACJ,CAAC;IAED,OAAO;QACL,IAAI;QACJ,KAAK,EAAE,MAAM,IAAI,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QACxD,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,EAAE;KAC5B,CAAA;AACH,CAAC","sourcesContent":["import { ILike } from 'typeorm'\nimport { getRepository } from '@things-factory/shell'\n\nimport { sendUnlockUserEmail } from '../controllers/unlock-user.js'\nimport { AuthError } from '../errors/auth-error.js'\nimport { User, UserStatus } from '../service/user/user.js'\nimport { isAccountLocked } from '../utils/account-lock-rule.js'\nimport { lockDurationMinutes } from '../utils/lock-duration.js'\nimport { recordAuditAction } from '../service/audit/audit-recorder.js'\nimport { errorCodeOf } from '../service/audit/audit-target.js'\n\nexport type SigninMethod = 'password' | 'passkey'\n\nexport type SigninAttempt = { userId?: string }\n\n/**\n * A sign-in, and its audit (ADR-0141 §3.3): a success or a failure with its code. A failure is\n * written on its own and stays when the request fails (§4-3). An account that does not exist leaves\n * no actor - the name typed in is not a person.\n */\nexport async function signin(attrs: { username: string; password: string }, context?) {\n return await auditedSignin('password', context, attempt => signinOnce(attrs, context, attempt))\n}\n\n/**\n * The audit around one way of signing in. `run` puts the person in `attempt` as soon as it knows\n * who it is, so a failure after that names them. `detail.method` tells a password from a passkey\n * (ADR-0156 decision 4).\n */\nexport async function auditedSignin<T extends { user: User }>(\n method: SigninMethod,\n context,\n run: (attempt: SigninAttempt) => Promise<T>\n): Promise<T> {\n const attempt: SigninAttempt = {}\n const domainId = context?.state?.domain?.id ?? null\n const detail = { method }\n\n /*\n * Not awaited: the answer does not wait on its record, and a write that fails is counted, never\n * thrown (`recordAuditAction` without a transaction).\n */\n try {\n const answer = await run(attempt)\n void recordAuditAction({ kind: 'auth.signin', outcome: 'ok', detail }, { actorId: answer.user.id, domainId })\n return answer\n } catch (error) {\n void recordAuditAction({ kind: 'auth.signin', outcome: 'error', code: errorCodeOf(error), detail }, { actorId: attempt.userId ?? null, domainId })\n throw error\n }\n}\n\nasync function signinOnce(attrs: { username: string; password: string }, context, attempt: SigninAttempt) {\n const { username } = attrs\n\n const repository = getRepository(User)\n\n var user = await repository.findOne({\n where: { username },\n relations: ['domains']\n })\n\n if (!user && /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(username)) {\n user = await repository.findOne({\n where: {\n email: ILike(username)\n },\n relations: ['domains']\n })\n }\n\n if (!user)\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.USER_NOT_FOUND\n })\n\n attempt.userId = user.id\n\n return await admitSignin(user, context, async () => {\n if (!User.verify(user.password, attrs.password, user.salt)) {\n user.failCount++\n if (user.failCount >= 5) {\n user.status = UserStatus.LOCKED\n /* 언제 풀리나를 계산할 수 있게 시각을 남긴다. 이 칸이 비면 판정 규칙이\n 잠금을 유지하는 쪽으로 기운다. */\n user.lockedAt = new Date()\n }\n await repository.save(user)\n if (user.status == UserStatus.LOCKED) {\n sendUnlockUserEmail({\n user,\n context\n })\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.USER_LOCKED,\n detail: {\n username: user.username,\n email: user.email\n }\n })\n }\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.PASSWORD_NOT_MATCHED,\n detail: {\n username: user.username,\n email: user.email,\n failCount: user.failCount\n }\n })\n } else {\n user.failCount = 0\n await repository.save(user)\n }\n })\n}\n\n/**\n * The account gate every way of signing in goes through (ADR-0156 decision 4): deleted, locked -\n * and a lock that has run out lifts here - then the secret, then inactive. Only `verifySecret`\n * differs between a password and a passkey; a passkey arrives already verified and passes nothing.\n *\n * `user` must carry `domains`.\n */\nexport async function admitSignin(user: User, context, verifySecret: () => Promise<void>) {\n const { domain } = context?.state || {}\n const repository = getRepository(User)\n\n if (user.status == UserStatus.DELETED) {\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.USER_DELETED\n })\n }\n\n if (user.status == UserStatus.LOCKED) {\n /* 시계는 한 번만 읽는다 — 두 번 읽으면 경계에 걸린 잠금이 두 판정 사이에서 갈린다. */\n const now = new Date()\n\n if (isAccountLocked(user, now, lockDurationMinutes())) {\n sendUnlockUserEmail({\n user,\n context\n })\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.USER_LOCKED,\n detail: {\n username: user.username,\n email: user.email\n }\n })\n }\n\n /*\n * 시간이 지나 스스로 풀렸다 — 그 자리에서 행을 되돌리고 계속 진행한다. 주기 작업으로\n * 풀지 않는 이유는 `account-lock-rule.ts` 에 있다(계정이 10만이면 그것이 따로 일이 된다).\n *\n * `failCount` 도 함께 0 으로 둔다. 그래서 **잠금이 곧 실패 셈의 창**이 된다 — 이 계정이\n * 다시 잠기려면 지금부터 다섯 번을 틀려야 한다.\n */\n user.status = UserStatus.ACTIVATED\n user.failCount = 0\n user.lockedAt = null\n\n await repository.save(user)\n }\n\n await verifySecret()\n\n if (user.status == UserStatus.INACTIVE) {\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.USER_NOT_ACTIVATED,\n detail: {\n username: user.username,\n email: user.email\n }\n })\n }\n\n return {\n user,\n token: await user.sign({ subdomain: domain?.subdomain }),\n domains: user.domains || []\n }\n}\n"]}
|
|
@@ -3,5 +3,4 @@ export declare function initMiddlewares(app: Koa): void;
|
|
|
3
3
|
export * from './jwt-authenticate-middleware.js';
|
|
4
4
|
export * from './domain-authenticate-middleware.js';
|
|
5
5
|
export * from './signin-middleware.js';
|
|
6
|
-
export * from './webauthn-middleware.js';
|
|
7
6
|
export * from './authenticate-401-middleware.js';
|
|
@@ -57,6 +57,5 @@ process.on('bootstrap-module-subscription', (app, subscriptionMiddleware) => {
|
|
|
57
57
|
tslib_1.__exportStar(require("./jwt-authenticate-middleware.js"), exports);
|
|
58
58
|
tslib_1.__exportStar(require("./domain-authenticate-middleware.js"), exports);
|
|
59
59
|
tslib_1.__exportStar(require("./signin-middleware.js"), exports);
|
|
60
|
-
tslib_1.__exportStar(require("./webauthn-middleware.js"), exports);
|
|
61
60
|
tslib_1.__exportStar(require("./authenticate-401-middleware.js"), exports);
|
|
62
61
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/middlewares/index.ts"],"names":[],"mappings":";;AAmBA,0CAgDC;;AAlED,sEAAiC;AACjC,wEAAmC;AAEnC,6CAA4C;AAC5C,0DAA+C;AAE/C,qFAA4E;AAC5E,2FAAkF;AAClF,6FAAoF;AACpF,qFAA4E;AAC5E,+EAAsE;AACtE,2EAAkE;AAElE,qDAA8C;AAC9C,wDAAiD;AAEjD,MAAM,uBAAuB,GAAG,YAAM,CAAC,GAAG,CAAC,yBAAyB,EAAE,KAAK,CAAC,CAAA;AAE5E,SAAgB,eAAe,CAAC,GAAQ;IACtC,4GAA4G;IAC5G,GAAG,CAAC,GAAG,CAAC,gDAAoB,CAAC,CAAA;IAE7B,IAAI,uBAAuB,EAAE,CAAC;QAC5B,GAAG,CAAC,GAAG,CAAC,oDAAsB,CAAC,CAAA;QAC/B,OAAM;IACR,CAAC;IAED,yDAAyD;IACzD,GAAG,CAAC,IAAI,GAAG,CAAC,sBAAM,CAAC,CAAA;IACnB,GAAG,CAAC,GAAG,CACL,IAAA,qBAAO,EACL;QACE,GAAG,EAAE,WAAW;QAChB,MAAM,EAAE,oBAAO;QACf,SAAS,EAAE,IAAI;QACf,QAAQ,EAAE,IAAI;QACd,MAAM,EAAE,IAAI;QACZ,OAAO,EAAE,KAAK;QACd,KAAK,EAAE,KAAK;KACb,EACD,GAAG,CACJ,CACF,CAAA;IAED,sBAAQ,CAAC,aAAa,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE;QACvC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;IACrB,CAAC,CAAC,CAAA;IAEF,sBAAQ,CAAC,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;QAC/C,IAAI,CAAC,IAAI,EAAE,MAAM,cAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAA;IAC3C,CAAC,CAAC,CAAA;IAEF,yBAAyB;IACzB,GAAG,CAAC,GAAG,CAAC,sBAAQ,CAAC,UAAU,EAAE,CAAC,CAAA;IAE9B,kDAAkD;IAClD,GAAG,CAAC,GAAG,CAAC,sBAAQ,CAAC,OAAO,EAAE,CAAC,CAAA;IAE3B,mCAAmC;IACnC,GAAG,CAAC,GAAG,CAAC,0DAAyB,CAAC,CAAA;IAElC;;;OAGG;IACH,GAAG,CAAC,GAAG,CAAC,kEAA6B,CAAC,CAAA;AACxC,CAAC;AAED,OAAO,CAAC,EAAE,CAAC,+BAAsC,EAAE,CAAC,GAAG,EAAE,sBAAsB,EAAE,EAAE;IACjF,sBAAsB,CAAC,IAAI,CAAC,0DAAyB,EAAE,gEAA4B,CAAC,CAAA;AACtF,CAAC,CAAC,CAAA;AAEF,2EAAgD;AAChD,8EAAmD;AACnD,iEAAsC;AACtC,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/middlewares/index.ts"],"names":[],"mappings":";;AAmBA,0CAgDC;;AAlED,sEAAiC;AACjC,wEAAmC;AAEnC,6CAA4C;AAC5C,0DAA+C;AAE/C,qFAA4E;AAC5E,2FAAkF;AAClF,6FAAoF;AACpF,qFAA4E;AAC5E,+EAAsE;AACtE,2EAAkE;AAElE,qDAA8C;AAC9C,wDAAiD;AAEjD,MAAM,uBAAuB,GAAG,YAAM,CAAC,GAAG,CAAC,yBAAyB,EAAE,KAAK,CAAC,CAAA;AAE5E,SAAgB,eAAe,CAAC,GAAQ;IACtC,4GAA4G;IAC5G,GAAG,CAAC,GAAG,CAAC,gDAAoB,CAAC,CAAA;IAE7B,IAAI,uBAAuB,EAAE,CAAC;QAC5B,GAAG,CAAC,GAAG,CAAC,oDAAsB,CAAC,CAAA;QAC/B,OAAM;IACR,CAAC;IAED,yDAAyD;IACzD,GAAG,CAAC,IAAI,GAAG,CAAC,sBAAM,CAAC,CAAA;IACnB,GAAG,CAAC,GAAG,CACL,IAAA,qBAAO,EACL;QACE,GAAG,EAAE,WAAW;QAChB,MAAM,EAAE,oBAAO;QACf,SAAS,EAAE,IAAI;QACf,QAAQ,EAAE,IAAI;QACd,MAAM,EAAE,IAAI;QACZ,OAAO,EAAE,KAAK;QACd,KAAK,EAAE,KAAK;KACb,EACD,GAAG,CACJ,CACF,CAAA;IAED,sBAAQ,CAAC,aAAa,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE;QACvC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;IACrB,CAAC,CAAC,CAAA;IAEF,sBAAQ,CAAC,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;QAC/C,IAAI,CAAC,IAAI,EAAE,MAAM,cAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAA;IAC3C,CAAC,CAAC,CAAA;IAEF,yBAAyB;IACzB,GAAG,CAAC,GAAG,CAAC,sBAAQ,CAAC,UAAU,EAAE,CAAC,CAAA;IAE9B,kDAAkD;IAClD,GAAG,CAAC,GAAG,CAAC,sBAAQ,CAAC,OAAO,EAAE,CAAC,CAAA;IAE3B,mCAAmC;IACnC,GAAG,CAAC,GAAG,CAAC,0DAAyB,CAAC,CAAA;IAElC;;;OAGG;IACH,GAAG,CAAC,GAAG,CAAC,kEAA6B,CAAC,CAAA;AACxC,CAAC;AAED,OAAO,CAAC,EAAE,CAAC,+BAAsC,EAAE,CAAC,GAAG,EAAE,sBAAsB,EAAE,EAAE;IACjF,sBAAsB,CAAC,IAAI,CAAC,0DAAyB,EAAE,gEAA4B,CAAC,CAAA;AACtF,CAAC,CAAC,CAAA;AAEF,2EAAgD;AAChD,8EAAmD;AACnD,iEAAsC;AACtC,2EAAgD","sourcesContent":["import Koa from 'koa'\nimport session from 'koa-session'\nimport passport from 'koa-passport'\n\nimport { config } from '@things-factory/env'\nimport { SECRET } from '../utils/get-secret.js'\n\nimport { authenticate401Middleware } from './authenticate-401-middleware.js'\nimport { domainAuthenticateMiddleware } from './domain-authenticate-middleware.js'\nimport { graphqlAuthenticateMiddleware } from './graphql-authenticate-middleware.js'\nimport { jwtAuthenticateMiddleware } from './jwt-authenticate-middleware.js'\nimport { bypassSigninMiddleware } from './bypass-signin-middleware.js'\nimport { auditScopeMiddleware } from './audit-scope-middleware.js'\n\nimport { User } from '../service/user/user.js'\nimport { MAX_AGE } from '../constants/max-age.js'\n\nconst bypassUserSigninProcess = config.get('bypassUserSigninProcess', false)\n\nexport function initMiddlewares(app: Koa) {\n /* Before everything else here, so sign-in and the GraphQL authentication run inside the request's audit. */\n app.use(auditScopeMiddleware)\n\n if (bypassUserSigninProcess) {\n app.use(bypassSigninMiddleware)\n return\n }\n\n /* oauth2orize-koa 에서 oauth 트랜잭션 관리를 위해서 session을 사용함. */\n app.keys = [SECRET]\n app.use(\n session(\n {\n key: 'tfsession',\n maxAge: MAX_AGE,\n overwrite: true,\n httpOnly: true,\n signed: true,\n rolling: false,\n renew: false\n },\n app\n )\n )\n\n passport.serializeUser((profile, done) => {\n done(null, profile)\n })\n\n passport.deserializeUser(async (profile, done) => {\n done(null, await User.checkAuth(profile))\n })\n\n /* passport initialize */\n app.use(passport.initialize())\n\n /* passport use session - for oauth transaction */\n app.use(passport.session())\n\n /* authentication error handling */\n app.use(authenticate401Middleware)\n\n /*\n * post:graphql 에 대해서는 graphqlAuthenticationMiddleware를 적용한다.\n * graphql app을 router에 적용하지 못하기 때문임.\n */\n app.use(graphqlAuthenticateMiddleware)\n}\n\nprocess.on('bootstrap-module-subscription' as any, (app, subscriptionMiddleware) => {\n subscriptionMiddleware.push(jwtAuthenticateMiddleware, domainAuthenticateMiddleware)\n})\n\nexport * from './jwt-authenticate-middleware.js'\nexport * from './domain-authenticate-middleware.js'\nexport * from './signin-middleware.js'\nexport * from './authenticate-401-middleware.js'\n"]}
|
|
@@ -48,6 +48,7 @@ exports.authSigninRouter.post('/auth/signin', async (ctx, next) => {
|
|
|
48
48
|
}
|
|
49
49
|
var redirectTo = `/auth/checkin${domain ? '/' + domain.subdomain : ''}?redirect_to=${encodeURIComponent(reqBody.redirectTo || '/')}`;
|
|
50
50
|
(0, access_token_cookie_js_1.setAccessTokenCookie)(context, token);
|
|
51
|
-
|
|
51
|
+
/* A person who typed a password is offered a passkey on the way in (ADR-0156 decision 5). */
|
|
52
|
+
context.redirect(`/auth/passkey-offer?redirect_to=${encodeURIComponent(redirectTo)}`);
|
|
52
53
|
});
|
|
53
54
|
//# sourceMappingURL=auth-signin-router.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-signin-router.js","sourceRoot":"","sources":["../../server/router/auth-signin-router.ts"],"names":[],"mappings":";;;;AAAA,oEAA+B;AAE/B,6CAA4C;AAC5C,sDAA0D;AAC1D,oDAA6C;AAC7C,4EAAsE;AAEtE,MAAM,wBAAwB,GAAG,YAAM,CAAC,GAAG,CAAC,0BAA0B,EAAE,KAAK,CAAC,CAAA;AAC9E,MAAM,0BAA0B,GAAG,YAAM,CAAC,GAAG,CAAC,iCAAiC,EAAE,KAAK,CAAC,CAAA;AACvF,MAAM,SAAS,GAAG,YAAM,CAAC,GAAG,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAA;AAErD,MAAM,SAAS,GAAG,YAAM,CAAC,GAAG,CAAC,KAAK,EAAE,EAAS,CAAC,CAAA;AAC9C,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC;KACtC,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,IAAI,IAAI,KAAK,CAAC;KAC1C,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE;IACvB,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAA;AACxB,CAAC,CAAC,CAAA;AAES,QAAA,gBAAgB,GAAG,IAAI,oBAAM,EAAE,CAAA;AAE5C,wBAAgB,CAAC,GAAG,CAAC,cAAc,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;IAC3D,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAE/C,MAAM,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE;QAChC,WAAW,EAAE,aAAa;QAC1B,aAAa,EAAE,iBAAiB;QAChC,IAAI,EAAE;YACJ,QAAQ;YACR,UAAU,EAAE,WAAW;YACvB,QAAQ,EAAE,QAAQ;YAClB,wBAAwB;YACxB,0BAA0B;YAC1B,SAAS;SACV;KACF,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,wBAAgB,CAAC,IAAI,CACnB,cAAc,EACd,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IAClB,6FAA6F;IAC7F,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QACzD,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAA;IACpD,CAAC;IACD,MAAM,IAAI,EAAE,CAAA;AACd,CAAC,EACD,2BAAgB,EAChB,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;IACtB,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,GAAG,OAAO,CAAA;IAC9B,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IACvC,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAA;IAEzC,IAAI,CAAC,IAAA,oBAAO,EAAC,MAAM,CAAC,MAAM,EAAE,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC;QAClD,OAAO,CAAC,IAAI,GAAG,KAAK,CAAA;QACpB,OAAM;IACR,CAAC;IAED,IAAI,UAAU,GAAG,gBAAgB,MAAM,CAAC,CAAC,CAAC,GAAG,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,gBAAgB,kBAAkB,CACrG,OAAO,CAAC,UAAU,IAAI,GAAG,CAC1B,EAAE,CAAA;IAEH,IAAA,6CAAoB,EAAC,OAAO,EAAE,KAAK,CAAC,CAAA;IAEpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"auth-signin-router.js","sourceRoot":"","sources":["../../server/router/auth-signin-router.ts"],"names":[],"mappings":";;;;AAAA,oEAA+B;AAE/B,6CAA4C;AAC5C,sDAA0D;AAC1D,oDAA6C;AAC7C,4EAAsE;AAEtE,MAAM,wBAAwB,GAAG,YAAM,CAAC,GAAG,CAAC,0BAA0B,EAAE,KAAK,CAAC,CAAA;AAC9E,MAAM,0BAA0B,GAAG,YAAM,CAAC,GAAG,CAAC,iCAAiC,EAAE,KAAK,CAAC,CAAA;AACvF,MAAM,SAAS,GAAG,YAAM,CAAC,GAAG,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAA;AAErD,MAAM,SAAS,GAAG,YAAM,CAAC,GAAG,CAAC,KAAK,EAAE,EAAS,CAAC,CAAA;AAC9C,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC;KACtC,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,IAAI,IAAI,KAAK,CAAC;KAC1C,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE;IACvB,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAA;AACxB,CAAC,CAAC,CAAA;AAES,QAAA,gBAAgB,GAAG,IAAI,oBAAM,EAAE,CAAA;AAE5C,wBAAgB,CAAC,GAAG,CAAC,cAAc,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;IAC3D,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAE/C,MAAM,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE;QAChC,WAAW,EAAE,aAAa;QAC1B,aAAa,EAAE,iBAAiB;QAChC,IAAI,EAAE;YACJ,QAAQ;YACR,UAAU,EAAE,WAAW;YACvB,QAAQ,EAAE,QAAQ;YAClB,wBAAwB;YACxB,0BAA0B;YAC1B,SAAS;SACV;KACF,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,wBAAgB,CAAC,IAAI,CACnB,cAAc,EACd,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IAClB,6FAA6F;IAC7F,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QACzD,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAA;IACpD,CAAC;IACD,MAAM,IAAI,EAAE,CAAA;AACd,CAAC,EACD,2BAAgB,EAChB,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;IACtB,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,GAAG,OAAO,CAAA;IAC9B,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IACvC,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAA;IAEzC,IAAI,CAAC,IAAA,oBAAO,EAAC,MAAM,CAAC,MAAM,EAAE,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC;QAClD,OAAO,CAAC,IAAI,GAAG,KAAK,CAAA;QACpB,OAAM;IACR,CAAC;IAED,IAAI,UAAU,GAAG,gBAAgB,MAAM,CAAC,CAAC,CAAC,GAAG,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,gBAAgB,kBAAkB,CACrG,OAAO,CAAC,UAAU,IAAI,GAAG,CAC1B,EAAE,CAAA;IAEH,IAAA,6CAAoB,EAAC,OAAO,EAAE,KAAK,CAAC,CAAA;IAEpC,6FAA6F;IAC7F,OAAO,CAAC,QAAQ,CAAC,mCAAmC,kBAAkB,CAAC,UAAU,CAAC,EAAE,CAAC,CAAA;AACvF,CAAC,CACF,CAAA","sourcesContent":["import Router from 'koa-router'\n\nimport { config } from '@things-factory/env'\nimport { signinMiddleware } from '../middlewares/index.js'\nimport { accepts } from '../utils/accepts.js'\nimport { setAccessTokenCookie } from '../utils/access-token-cookie.js'\n\nconst disableUserSignupProcess = config.get('disableUserSignupProcess', false)\nconst disableUserFavoredLanguage = config.get('i18n/disableUserFavoredLanguage', false)\nconst languages = config.get('i18n/languages', false)\n\nconst SSOConfig = config.get('sso', {} as any)\nconst SSOLinks = Object.values(SSOConfig)\n .filter(({ link, title }) => link && title)\n .map(({ link, title }) => {\n return { link, title }\n })\n\nexport const authSigninRouter = new Router()\n\nauthSigninRouter.get('/auth/signin', async (context, next) => {\n const { redirect_to, username } = context.query\n\n await context.render('auth-page', {\n pageElement: 'auth-signin',\n elementScript: '/auth/signin.js',\n data: {\n username,\n redirectTo: redirect_to,\n ssoLinks: SSOLinks,\n disableUserSignupProcess,\n disableUserFavoredLanguage,\n languages\n }\n })\n})\n\nauthSigninRouter.post(\n '/auth/signin',\n async (ctx, next) => {\n /* For backward compatibility, cover the case of logging in with email instead of username */\n if (!ctx.request.body.username && ctx.request.body.email) {\n ctx.request.body.username = ctx.request.body.email\n }\n await next()\n },\n signinMiddleware,\n async (context, next) => {\n const { request, t } = context\n const { token, domain } = context.state\n const { body: reqBody, header } = request\n\n if (!accepts(header.accept, ['text/html', '*/*'])) {\n context.body = token\n return\n }\n\n var redirectTo = `/auth/checkin${domain ? '/' + domain.subdomain : ''}?redirect_to=${encodeURIComponent(\n reqBody.redirectTo || '/'\n )}`\n\n setAccessTokenCookie(context, token)\n\n /* A person who typed a password is offered a passkey on the way in (ADR-0156 decision 5). */\n context.redirect(`/auth/passkey-offer?redirect_to=${encodeURIComponent(redirectTo)}`)\n }\n)\n"]}
|