@bitblit/ratchet-warden-server 6.0.146-alpha → 6.0.147-alpha
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/package.json +9 -8
- package/src/build/ratchet-warden-server-info.ts +19 -0
- package/src/server/provider/warden-default-send-magic-link-command-validator.ts +21 -0
- package/src/server/provider/warden-default-user-decoration-provider.ts +23 -0
- package/src/server/provider/warden-dynamo-storage-provider-options.ts +8 -0
- package/src/server/provider/warden-dynamo-storage-provider.ts +278 -0
- package/src/server/provider/warden-event-processing-provider.ts +10 -0
- package/src/server/provider/warden-mailer-and-expiring-code-ratchet-single-use-code-provider.ts +155 -0
- package/src/server/provider/warden-mailer-and-expiring-code-ratchet-single-user-provider-options.ts +10 -0
- package/src/server/provider/warden-message-sending-provider.ts +14 -0
- package/src/server/provider/warden-no-op-event-processing-provider.ts +12 -0
- package/src/server/provider/warden-s3-single-file-storage-provider-options.ts +6 -0
- package/src/server/provider/warden-s3-single-file-storage-provider.ts +139 -0
- package/src/server/provider/warden-send-magic-link-command-validator.ts +12 -0
- package/src/server/provider/warden-single-use-code-provider.ts +27 -0
- package/src/server/provider/warden-storage-provider.ts +22 -0
- package/src/server/provider/warden-third-party-authentication-provider.ts +18 -0
- package/src/server/provider/warden-twilio-verify-single-use-code-provider-options.ts +5 -0
- package/src/server/provider/warden-twilio-verify-single-use-code-provider.ts +38 -0
- package/src/server/provider/warden-user-decoration-provider.ts +10 -0
- package/src/server/warden-authorizer.ts +130 -0
- package/src/server/warden-entry-builder.ts +56 -0
- package/src/server/warden-service-options.ts +20 -0
- package/src/server/warden-service.spec.ts +102 -0
- package/src/server/warden-service.ts +815 -0
- package/src/server/warden-web-authn-export-token.ts +6 -0
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitblit/ratchet-warden-server",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.147-alpha",
|
|
4
4
|
"description": "Typescript library to simplify using simplewebauthn and secondary auth methods over GraphQL",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"files": [
|
|
8
|
+
"src/**",
|
|
8
9
|
"lib/**",
|
|
9
10
|
"bin/**"
|
|
10
11
|
],
|
|
@@ -48,10 +49,10 @@
|
|
|
48
49
|
"dependencies": {
|
|
49
50
|
"@aws-sdk/client-s3": "3.922.0",
|
|
50
51
|
"@aws-sdk/client-ses": "3.922.0",
|
|
51
|
-
"@bitblit/ratchet-aws": "6.0.
|
|
52
|
-
"@bitblit/ratchet-common": "6.0.
|
|
53
|
-
"@bitblit/ratchet-node-only": "6.0.
|
|
54
|
-
"@bitblit/ratchet-warden-common": "6.0.
|
|
52
|
+
"@bitblit/ratchet-aws": "6.0.147-alpha",
|
|
53
|
+
"@bitblit/ratchet-common": "6.0.147-alpha",
|
|
54
|
+
"@bitblit/ratchet-node-only": "6.0.147-alpha",
|
|
55
|
+
"@bitblit/ratchet-warden-common": "6.0.147-alpha",
|
|
55
56
|
"@simplewebauthn/browser": "13.2.2",
|
|
56
57
|
"@simplewebauthn/server": "13.2.2",
|
|
57
58
|
"jsonwebtoken": "9.0.2"
|
|
@@ -59,9 +60,9 @@
|
|
|
59
60
|
"peerDependencies": {
|
|
60
61
|
"@aws-sdk/client-s3": "^3.922.0",
|
|
61
62
|
"@aws-sdk/client-ses": "^3.922.0",
|
|
62
|
-
"@bitblit/ratchet-aws": "6.0.
|
|
63
|
-
"@bitblit/ratchet-common": "6.0.
|
|
64
|
-
"@bitblit/ratchet-warden-common": "6.0.
|
|
63
|
+
"@bitblit/ratchet-aws": "6.0.147-alpha",
|
|
64
|
+
"@bitblit/ratchet-common": "6.0.147-alpha",
|
|
65
|
+
"@bitblit/ratchet-warden-common": "6.0.147-alpha",
|
|
65
66
|
"@simplewebauthn/browser": "^13.2.2",
|
|
66
67
|
"@simplewebauthn/server": "^13.2.2",
|
|
67
68
|
"jsonwebtoken": "^9.0.2"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BuildInformation } from '@bitblit/ratchet-common/build/build-information';
|
|
2
|
+
|
|
3
|
+
export class RatchetWardenServerInfo {
|
|
4
|
+
// Empty constructor prevents instantiation
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
6
|
+
private constructor() {}
|
|
7
|
+
|
|
8
|
+
public static buildInformation(): BuildInformation {
|
|
9
|
+
const val: BuildInformation = {
|
|
10
|
+
version: 'LOCAL-SNAPSHOT',
|
|
11
|
+
hash: 'LOCAL-HASH',
|
|
12
|
+
branch: 'LOCAL-BRANCH',
|
|
13
|
+
tag: 'LOCAL-TAG',
|
|
14
|
+
timeBuiltISO: 'LOCAL-TIME-ISO',
|
|
15
|
+
notes: 'LOCAL-NOTES',
|
|
16
|
+
};
|
|
17
|
+
return val;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The user details gets jammed into the JWT token upon login. If one is not provided,
|
|
3
|
+
* the default only puts the WardenEntrySummary in there
|
|
4
|
+
*/
|
|
5
|
+
import { WardenEntry } from '@bitblit/ratchet-warden-common/common/model/warden-entry';
|
|
6
|
+
import { SendMagicLink } from '@bitblit/ratchet-warden-common/common/command/send-magic-link';
|
|
7
|
+
import { WardenSendMagicLinkCommandValidator } from './warden-send-magic-link-command-validator.js';
|
|
8
|
+
|
|
9
|
+
export class WardenDefaultSendMagicLinkCommandValidator implements WardenSendMagicLinkCommandValidator {
|
|
10
|
+
public async allowMagicLinkCommand(cmd: SendMagicLink, _origin: string, _loggedInUser: WardenEntry): Promise<void> {
|
|
11
|
+
if (!cmd) {
|
|
12
|
+
throw new Error('Cannot process null magic link');
|
|
13
|
+
}
|
|
14
|
+
if (cmd.ttlSeconds && cmd.ttlSeconds > 3600) {
|
|
15
|
+
throw new Error('TTL may not exceed 3600 seconds');
|
|
16
|
+
}
|
|
17
|
+
if (cmd.overrideDestinationContact) {
|
|
18
|
+
throw new Error('You may not specify an overrideDestinationContact');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The user details gets jammed into the JWT token upon login. If one is not provided,
|
|
3
|
+
* the default only puts the WardenEntrySummary in there
|
|
4
|
+
*/
|
|
5
|
+
import { WardenEntrySummary } from '@bitblit/ratchet-warden-common/common/model/warden-entry-summary';
|
|
6
|
+
import { WardenEntry } from '@bitblit/ratchet-warden-common/common/model/warden-entry';
|
|
7
|
+
import { WardenUtils } from '@bitblit/ratchet-warden-common/common/util/warden-utils';
|
|
8
|
+
import { WardenUserDecoration } from '@bitblit/ratchet-warden-common/common/model/warden-user-decoration';
|
|
9
|
+
import { WardenUserDecorationProvider } from './warden-user-decoration-provider.js';
|
|
10
|
+
|
|
11
|
+
export class WardenDefaultUserDecorationProvider implements WardenUserDecorationProvider<WardenEntrySummary> {
|
|
12
|
+
public async fetchDecoration(wardenUser: WardenEntry): Promise<WardenUserDecoration<WardenEntrySummary>> {
|
|
13
|
+
// Default to 1 hour
|
|
14
|
+
const rval: WardenUserDecoration<WardenEntrySummary> = {
|
|
15
|
+
userTokenData: WardenUtils.stripWardenEntryToSummary(wardenUser),
|
|
16
|
+
proxyUserTokenData: null,
|
|
17
|
+
userTokenExpirationSeconds: 3600,
|
|
18
|
+
globalRoleIds: [],
|
|
19
|
+
teamRoleMappings: [],
|
|
20
|
+
};
|
|
21
|
+
return rval;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { WardenTeamRoleMapping } from "@bitblit/ratchet-warden-common/common/model/warden-team-role-mapping";
|
|
2
|
+
|
|
3
|
+
export class WardenDynamoStorageProviderOptions<T> {
|
|
4
|
+
tableName: string;
|
|
5
|
+
defaultTeamRoleMappings: WardenTeamRoleMapping[];
|
|
6
|
+
defaultTokenExpirationSeconds: number;
|
|
7
|
+
defaultDecoration?: T
|
|
8
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { DynamoRatchet } from '@bitblit/ratchet-aws/dynamodb/dynamo-ratchet';
|
|
2
|
+
import { WardenDynamoStorageProviderOptions } from './warden-dynamo-storage-provider-options.js';
|
|
3
|
+
import { WardenContact } from '@bitblit/ratchet-warden-common/common/model/warden-contact';
|
|
4
|
+
import { WardenEntry } from '@bitblit/ratchet-warden-common/common/model/warden-entry';
|
|
5
|
+
import { WardenEntrySummary } from '@bitblit/ratchet-warden-common/common/model/warden-entry-summary';
|
|
6
|
+
import { DeleteCommandOutput, PutCommandOutput,ScanCommandInput } from '@aws-sdk/lib-dynamodb';
|
|
7
|
+
import { Logger } from '@bitblit/ratchet-common/logger/logger';
|
|
8
|
+
import { ErrorRatchet } from '@bitblit/ratchet-common/lang/error-ratchet';
|
|
9
|
+
import { WardenUtils } from '@bitblit/ratchet-warden-common/common/util/warden-utils';
|
|
10
|
+
import { ExpiringCodeProvider } from '@bitblit/ratchet-aws/expiring-code/expiring-code-provider';
|
|
11
|
+
import { ExpiringCode } from '@bitblit/ratchet-aws/expiring-code/expiring-code';
|
|
12
|
+
import { WardenStorageProvider } from "./warden-storage-provider.js";
|
|
13
|
+
import { WardenUserDecorationProvider } from "./warden-user-decoration-provider.js";
|
|
14
|
+
import { WardenUserDecoration } from "@bitblit/ratchet-warden-common/common/model/warden-user-decoration";
|
|
15
|
+
import { WardenTeamRoleMapping } from "@bitblit/ratchet-warden-common/common/model/warden-team-role-mapping";
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
// Create a ddb table with a hashkey of userId type string
|
|
19
|
+
export class WardenDynamoStorageProvider<T> implements WardenStorageProvider, ExpiringCodeProvider, WardenUserDecorationProvider<T> {
|
|
20
|
+
|
|
21
|
+
private static readonly EXPIRING_CODE_PROVIDER_KEY: string = '__EXPIRING_CODE_DATA';
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
private ddb: DynamoRatchet,
|
|
25
|
+
private options: WardenDynamoStorageProviderOptions<T>
|
|
26
|
+
) {
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public createDecoration(decoration: T): WardenUserDecoration<T> {
|
|
30
|
+
return {
|
|
31
|
+
globalRoleIds: [],
|
|
32
|
+
teamRoleMappings: this.options.defaultTeamRoleMappings,
|
|
33
|
+
userTokenExpirationSeconds: this.options.defaultTokenExpirationSeconds,
|
|
34
|
+
userTokenData: decoration,
|
|
35
|
+
proxyUserTokenData: null
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public async updateRoles(userId: string, roles: WardenTeamRoleMapping[]): Promise<WardenUserDecoration<T>> {
|
|
40
|
+
const rval: WardenDynamoStorageDataWrapper = await this.fetchInternalByUserId(userId);
|
|
41
|
+
if (rval) {
|
|
42
|
+
rval.decoration ??= this.createDecoration(null);
|
|
43
|
+
rval.decoration.teamRoleMappings = roles;
|
|
44
|
+
await this.updateInternal(rval);
|
|
45
|
+
} else {
|
|
46
|
+
throw ErrorRatchet.fErr('Cannot update roles - no entry found for %s', userId);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return this.fetchDecorationById(userId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public async updateTokenExpirationSeconds(userId: string, newValue: number): Promise<WardenUserDecoration<T>> {
|
|
53
|
+
const rval: WardenDynamoStorageDataWrapper = await this.fetchInternalByUserId(userId);
|
|
54
|
+
if (rval) {
|
|
55
|
+
rval.decoration ??= this.createDecoration(null);
|
|
56
|
+
rval.decoration.userTokenExpirationSeconds = newValue;
|
|
57
|
+
await this.updateInternal(rval);
|
|
58
|
+
} else {
|
|
59
|
+
throw ErrorRatchet.fErr('Cannot update roles - no entry found for %s', userId);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return this.fetchDecorationById(userId);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public async updateDecoration(userId: string, decoration:T): Promise<WardenUserDecoration<T>> {
|
|
66
|
+
const rval: WardenDynamoStorageDataWrapper = await this.fetchInternalByUserId(userId);
|
|
67
|
+
if (rval) {
|
|
68
|
+
rval.decoration ??= this.createDecoration(null);
|
|
69
|
+
rval.decoration.userTokenData = decoration;
|
|
70
|
+
await this.updateInternal(rval);
|
|
71
|
+
} else {
|
|
72
|
+
throw ErrorRatchet.fErr('Cannot update roles - no entry found for %s', userId);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return this.fetchDecorationById(userId);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public async fetchDecoration(wardenUser: WardenEntry): Promise<WardenUserDecoration<T>> {
|
|
79
|
+
return this.fetchDecorationById(wardenUser.userId);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public async fetchDecorationById(userId: string): Promise<WardenUserDecoration<T>> {
|
|
83
|
+
const rval: WardenDynamoStorageDataWrapper = await this.fetchInternalByUserId(userId);
|
|
84
|
+
return rval?.decoration;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Sure, this is hackish... but DDB doesn't care, and it allows you to wrap up all the
|
|
88
|
+
// warden data in a single item
|
|
89
|
+
private async fetchExpiringCodes(): Promise<ExpiringCodeHolder> {
|
|
90
|
+
let rval: ExpiringCodeHolder = await this.ddb.simpleGet<ExpiringCodeHolder>(this.options.tableName, {
|
|
91
|
+
userId: WardenDynamoStorageProvider.EXPIRING_CODE_PROVIDER_KEY
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (!rval) {
|
|
95
|
+
rval = {
|
|
96
|
+
userId: WardenDynamoStorageProvider.EXPIRING_CODE_PROVIDER_KEY,
|
|
97
|
+
values: []
|
|
98
|
+
};
|
|
99
|
+
await this.updateInternal(rval as unknown as WardenDynamoStorageDataWrapper); // Hack to piggyback
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return rval;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private async updateInternal(val: WardenDynamoStorageDataWrapper): Promise<PutCommandOutput> {
|
|
106
|
+
return this.ddb.simplePut(this.options.tableName, val);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public async checkCode(code: string, context: string, deleteOnMatch?: boolean): Promise<boolean> {
|
|
110
|
+
const codes: ExpiringCodeHolder = await this.fetchExpiringCodes();
|
|
111
|
+
const rval: ExpiringCode = codes.values.find((c) => c.code === code && c.context === context);
|
|
112
|
+
if (rval) {
|
|
113
|
+
if (deleteOnMatch) {
|
|
114
|
+
codes.values = codes.values.filter((c) => c.code !== code || c.context !== context);
|
|
115
|
+
await this.updateInternal(codes as unknown as WardenDynamoStorageDataWrapper); // Hack to piggyback
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
} else {
|
|
119
|
+
return !!rval;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
public async storeCode(code: ExpiringCode): Promise<boolean> {
|
|
124
|
+
const codes: ExpiringCodeHolder = await this.fetchExpiringCodes();
|
|
125
|
+
codes.values.push(code);
|
|
126
|
+
const now: number = Date.now();
|
|
127
|
+
codes.values=codes.values.filter(c=>c.expiresEpochMS>now); // Always remove expired ones on storage
|
|
128
|
+
const stored: PutCommandOutput = await this.updateInternal(codes as unknown as WardenDynamoStorageDataWrapper); // Hack to piggyback
|
|
129
|
+
|
|
130
|
+
return stored ? true : false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private static contactToSearchString(contact: WardenContact): string {
|
|
134
|
+
const toFind: string = `${contact.type}:${contact.value}`;
|
|
135
|
+
return toFind;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private static thirdPartyToSearchString(thirdParty: string, thirdPartyId: string): string {
|
|
139
|
+
const toFind: string = `${thirdParty}:${thirdPartyId}`;
|
|
140
|
+
return toFind;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private async fetchInternalByUserId(userId: string): Promise<WardenDynamoStorageDataWrapper> {
|
|
144
|
+
return this.ddb.simpleGet<WardenDynamoStorageDataWrapper>(this.options.tableName, {userId: userId});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
public async fetchCurrentUserChallenge(userId: string, relyingPartyId: string): Promise<string> {
|
|
148
|
+
const rval: WardenDynamoStorageDataWrapper = await this.fetchInternalByUserId(userId);
|
|
149
|
+
const cuc: string = rval ? rval.currentUserChallenges.find((c) => c.startsWith(relyingPartyId)) : null;
|
|
150
|
+
return cuc ? cuc.substring(relyingPartyId.length+1) : null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
public async findEntryByThirdPartyId(thirdParty: string, thirdPartyId: string): Promise<WardenEntry> {
|
|
156
|
+
const toFind: string = WardenDynamoStorageProvider.thirdPartyToSearchString(thirdParty, thirdPartyId);
|
|
157
|
+
const scan: ScanCommandInput = {
|
|
158
|
+
TableName: this.options.tableName,
|
|
159
|
+
FilterExpression: 'contains(#thirdPartySearchString,:thirdPartySearchString)',
|
|
160
|
+
ExpressionAttributeNames: {
|
|
161
|
+
'#thirdPartySearchString': 'thirdPartySearchString',
|
|
162
|
+
},
|
|
163
|
+
ExpressionAttributeValues: {
|
|
164
|
+
':thirdPartySearchString': toFind
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const results: WardenDynamoStorageDataWrapper[] = await this.ddb.fullyExecuteScan<WardenDynamoStorageDataWrapper>(scan);
|
|
169
|
+
if (results && results.length > 0) {
|
|
170
|
+
const rval: WardenDynamoStorageDataWrapper = results[0];
|
|
171
|
+
return rval.entry;
|
|
172
|
+
} else {
|
|
173
|
+
Logger.info('No results found for %s', toFind);
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
public async findEntryByContact(contact: WardenContact): Promise<WardenEntry> {
|
|
180
|
+
const toFind: string = WardenDynamoStorageProvider.contactToSearchString(contact);
|
|
181
|
+
const scan: ScanCommandInput = {
|
|
182
|
+
TableName: this.options.tableName,
|
|
183
|
+
FilterExpression: 'contains(#contactSearchString,:contactSearchString)',
|
|
184
|
+
ExpressionAttributeNames: {
|
|
185
|
+
'#contactSearchString': 'contactSearchString',
|
|
186
|
+
},
|
|
187
|
+
ExpressionAttributeValues: {
|
|
188
|
+
':contactSearchString': toFind
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const results: WardenDynamoStorageDataWrapper[] = await this.ddb.fullyExecuteScan<WardenDynamoStorageDataWrapper>(scan);
|
|
193
|
+
if (results && results.length > 0) {
|
|
194
|
+
const rval: WardenDynamoStorageDataWrapper = results[0];
|
|
195
|
+
return rval.entry;
|
|
196
|
+
} else {
|
|
197
|
+
Logger.info('No results found for %s', toFind);
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
public async findEntryById(userId: string): Promise<WardenEntry> {
|
|
204
|
+
const rval: WardenDynamoStorageDataWrapper = await this.fetchInternalByUserId(userId);
|
|
205
|
+
return rval ? rval.entry : null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
public async listUserSummaries(): Promise<WardenEntrySummary[]> {
|
|
209
|
+
// TODO: This is way too slow long term
|
|
210
|
+
const scan: ScanCommandInput = {
|
|
211
|
+
TableName: this.options.tableName,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const results: WardenDynamoStorageDataWrapper[] = await this.ddb.fullyExecuteScan<WardenDynamoStorageDataWrapper>(scan);
|
|
215
|
+
const rval: WardenEntrySummary[] = results.map(wd=>{
|
|
216
|
+
return WardenUtils.stripWardenEntryToSummary(wd.entry);
|
|
217
|
+
})
|
|
218
|
+
return rval;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
public async removeEntry(userId: string): Promise<boolean> {
|
|
222
|
+
const tmp: DeleteCommandOutput = await this.ddb.simpleDelete(this.options.tableName, {userId: userId});
|
|
223
|
+
return tmp.Attributes ? true : false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
public async saveEntry(entry: WardenEntry): Promise<WardenEntry> {
|
|
227
|
+
let rval: WardenDynamoStorageDataWrapper = await this.fetchInternalByUserId(entry.userId);
|
|
228
|
+
if (!rval) {
|
|
229
|
+
rval = {
|
|
230
|
+
userId: entry.userId,
|
|
231
|
+
entry: entry,
|
|
232
|
+
currentUserChallenges: [],
|
|
233
|
+
decoration: this.createDecoration(null),
|
|
234
|
+
contactSearchString: 'ContactSearch: '+(entry.contactMethods || []).map((cm) => WardenDynamoStorageProvider.contactToSearchString(cm)).join(' '),
|
|
235
|
+
thirdPartySearchString: '3rdPartySearch: '+(entry.thirdPartyAuthenticators || []).map((item) => WardenDynamoStorageProvider.thirdPartyToSearchString(item.thirdParty, item.thirdPartyId)).join(' '),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
rval.entry = entry;
|
|
239
|
+
const now: number = Date.now();
|
|
240
|
+
rval.entry.updatedEpochMS = now;
|
|
241
|
+
rval.entry.createdEpochMS = rval.entry.createdEpochMS || now;
|
|
242
|
+
const saved: PutCommandOutput = await this.updateInternal(rval);
|
|
243
|
+
Logger.silly('Saved %j', saved);
|
|
244
|
+
|
|
245
|
+
const postSaveLookup: WardenDynamoStorageDataWrapper = await this.fetchInternalByUserId(entry.userId);
|
|
246
|
+
return postSaveLookup.entry;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
public async updateUserChallenge(userId: string, relyingPartyId: string, challenge: string): Promise<boolean> {
|
|
250
|
+
const rval: WardenDynamoStorageDataWrapper = await this.fetchInternalByUserId(userId);
|
|
251
|
+
if (!rval) {
|
|
252
|
+
throw ErrorRatchet.fErr('Cannot update user challenge - no entry found for %s', userId);
|
|
253
|
+
}
|
|
254
|
+
rval.currentUserChallenges = (rval.currentUserChallenges || []).filter((c) => !c.startsWith(relyingPartyId));
|
|
255
|
+
const cuc: string = relyingPartyId + ':' + challenge;
|
|
256
|
+
rval.currentUserChallenges.push(cuc);
|
|
257
|
+
|
|
258
|
+
const saved: PutCommandOutput = await this.updateInternal(rval);
|
|
259
|
+
Logger.silly('Saved %j', saved);
|
|
260
|
+
return saved.Attributes ? true : false;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export interface WardenDynamoStorageDataWrapper {
|
|
267
|
+
userId: string;
|
|
268
|
+
entry: WardenEntry;
|
|
269
|
+
decoration: WardenUserDecoration<any>;
|
|
270
|
+
currentUserChallenges: string[];
|
|
271
|
+
contactSearchString: string;
|
|
272
|
+
thirdPartySearchString: string;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export interface ExpiringCodeHolder {
|
|
276
|
+
userId: string;
|
|
277
|
+
values: ExpiringCode[];
|
|
278
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { WardenEntry } from '@bitblit/ratchet-warden-common/common/model/warden-entry';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Notifies the containing system when significant events happen
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface WardenEventProcessingProvider {
|
|
8
|
+
userCreated(entry: WardenEntry): Promise<void>;
|
|
9
|
+
userRemoved(entry: WardenEntry): Promise<void>;
|
|
10
|
+
}
|
package/src/server/provider/warden-mailer-and-expiring-code-ratchet-single-use-code-provider.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { WardenSingleUseCodeProvider } from './warden-single-use-code-provider.js';
|
|
2
|
+
import { Logger } from '@bitblit/ratchet-common/logger/logger';
|
|
3
|
+
import { ErrorRatchet } from '@bitblit/ratchet-common/lang/error-ratchet';
|
|
4
|
+
import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet';
|
|
5
|
+
import { Base64Ratchet } from '@bitblit/ratchet-common/lang/base64-ratchet';
|
|
6
|
+
import { WardenMailerAndExpiringCodeRatchetSingleUseCodeProviderOptions } from './warden-mailer-and-expiring-code-ratchet-single-user-provider-options.js';
|
|
7
|
+
import { ExpiringCodeRatchet } from '@bitblit/ratchet-aws/expiring-code/expiring-code-ratchet';
|
|
8
|
+
import { ExpiringCode } from '@bitblit/ratchet-aws/expiring-code/expiring-code';
|
|
9
|
+
import { Mailer } from '@bitblit/ratchet-common/mail/mailer';
|
|
10
|
+
import { ReadyToSendEmail } from '@bitblit/ratchet-common/mail/ready-to-send-email';
|
|
11
|
+
import { SendEmailResult } from '@bitblit/ratchet-common/mail/send-email-result';
|
|
12
|
+
import { WardenContactType } from '@bitblit/ratchet-warden-common/common/model/warden-contact-type';
|
|
13
|
+
import { WardenContact } from '@bitblit/ratchet-warden-common/common/model/warden-contact';
|
|
14
|
+
import { WardenCustomerMessageType } from '@bitblit/ratchet-warden-common/common/model/warden-customer-message-type';
|
|
15
|
+
import { WardenCustomTemplateDescriptor } from '@bitblit/ratchet-warden-common/common/command/warden-custom-template-descriptor';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Classes implementing WardenSingleUseCodeProvider are able to
|
|
19
|
+
* generate single-use codes for a user, and to validate a code
|
|
20
|
+
* provided by the user
|
|
21
|
+
**/
|
|
22
|
+
export class WardenMailerAndExpiringCodeRatchetSingleUseCodeProvider implements WardenSingleUseCodeProvider {
|
|
23
|
+
private static defaultOptions(): WardenMailerAndExpiringCodeRatchetSingleUseCodeProviderOptions {
|
|
24
|
+
const rval: WardenMailerAndExpiringCodeRatchetSingleUseCodeProviderOptions = {
|
|
25
|
+
emailBaseLayoutName: undefined,
|
|
26
|
+
expiringTokenHtmlTemplateName: 'expiring-token-request-email',
|
|
27
|
+
expiringTokenTxtTemplateName: undefined,
|
|
28
|
+
magicLinkHtmlTemplateName: 'magic-token-request-email',
|
|
29
|
+
magicLinkTxtTemplateName: undefined,
|
|
30
|
+
};
|
|
31
|
+
return rval;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
private mailer: Mailer<any, any>,
|
|
36
|
+
private expiringCodeRatchet: ExpiringCodeRatchet,
|
|
37
|
+
private mailerOptions: WardenMailerAndExpiringCodeRatchetSingleUseCodeProviderOptions = WardenMailerAndExpiringCodeRatchetSingleUseCodeProvider.defaultOptions(),
|
|
38
|
+
) {}
|
|
39
|
+
public handlesContactType(type: WardenContactType): boolean {
|
|
40
|
+
return type === WardenContactType.EmailAddress;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public async createAndSendNewCode(contact: WardenContact, relyingPartyName: string, origin: string): Promise<boolean> {
|
|
44
|
+
let rval: boolean = null;
|
|
45
|
+
const token: ExpiringCode = await this.expiringCodeRatchet.createNewCode({
|
|
46
|
+
context: contact.value,
|
|
47
|
+
length: 6,
|
|
48
|
+
alphabet: '0123456789',
|
|
49
|
+
timeToLiveSeconds: 300,
|
|
50
|
+
tags: ['Login'],
|
|
51
|
+
});
|
|
52
|
+
const msg: any = await this.formatMessage(contact, WardenCustomerMessageType.ExpiringCode, {
|
|
53
|
+
requestor: contact.value,
|
|
54
|
+
requestorB64: Base64Ratchet.encodeStringToBase64String(contact.value),
|
|
55
|
+
code: token.code,
|
|
56
|
+
relyingPartyName: relyingPartyName,
|
|
57
|
+
origin: origin
|
|
58
|
+
});
|
|
59
|
+
rval = await this.sendMessage(msg);
|
|
60
|
+
return rval;
|
|
61
|
+
}
|
|
62
|
+
public async checkCode(contactValue: string, code: string): Promise<boolean> {
|
|
63
|
+
const rval: boolean = await this.expiringCodeRatchet.checkCode(code, contactValue);
|
|
64
|
+
return rval;
|
|
65
|
+
}
|
|
66
|
+
public async createCodeAndSendMagicLink?(
|
|
67
|
+
loginContact: WardenContact,
|
|
68
|
+
relyingPartyName: string,
|
|
69
|
+
landingUrl: string,
|
|
70
|
+
metaIn?: Record<string, string>,
|
|
71
|
+
ttlSeconds?: number,
|
|
72
|
+
destinationContact?: WardenContact,
|
|
73
|
+
customTemplate?: WardenCustomTemplateDescriptor,
|
|
74
|
+
): Promise<boolean> {
|
|
75
|
+
let rval: boolean = null;
|
|
76
|
+
const token: ExpiringCode = await this.expiringCodeRatchet.createNewCode({
|
|
77
|
+
context: loginContact.value,
|
|
78
|
+
length: 36,
|
|
79
|
+
alphabet: StringRatchet.UPPER_CASE_LATIN,
|
|
80
|
+
timeToLiveSeconds: ttlSeconds,
|
|
81
|
+
tags: ['MagicLink'],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const meta: Record<string, any> = Object.assign({}, metaIn || {}, { contact: loginContact });
|
|
85
|
+
const encodedMeta: string = Base64Ratchet.safeObjectToBase64JSON(meta || {});
|
|
86
|
+
|
|
87
|
+
const landingUrlFilled: string = StringRatchet.simpleTemplateFill(landingUrl, { CODE: token.code, META: encodedMeta }, true, '{', '}');
|
|
88
|
+
|
|
89
|
+
const context: Record<string, string> = Object.assign({}, meta || {}, {
|
|
90
|
+
landingUrl: landingUrlFilled,
|
|
91
|
+
code: token.code,
|
|
92
|
+
relyingPartyName: relyingPartyName,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const msgType: WardenCustomerMessageType = customTemplate ? WardenCustomerMessageType.Custom : WardenCustomerMessageType.MagicLink;
|
|
96
|
+
const msg: ReadyToSendEmail = await this.formatMessage(loginContact, msgType, context, destinationContact, customTemplate);
|
|
97
|
+
rval = await this.sendMessage(msg);
|
|
98
|
+
return rval;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public async formatMessage(
|
|
102
|
+
contact: WardenContact,
|
|
103
|
+
messageType: WardenCustomerMessageType,
|
|
104
|
+
context: Record<string, any>,
|
|
105
|
+
destinationContact?: WardenContact,
|
|
106
|
+
customTemplate?: WardenCustomTemplateDescriptor,
|
|
107
|
+
): Promise<ReadyToSendEmail> {
|
|
108
|
+
const rts: ReadyToSendEmail = {
|
|
109
|
+
destinationAddresses: [destinationContact?.value || contact.value],
|
|
110
|
+
subject: customTemplate?.subjectLine || this.mailerOptions.magicLinkSubjectLine || 'Login token for '+contact.value,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
Logger.info('Formatting Message for magic link, rts: %j, messageType: %s, context: %j', rts, messageType, context);
|
|
114
|
+
|
|
115
|
+
if (messageType === WardenCustomerMessageType.ExpiringCode) {
|
|
116
|
+
await this.mailer.fillEmailBody(
|
|
117
|
+
rts,
|
|
118
|
+
context,
|
|
119
|
+
this.mailerOptions.expiringTokenHtmlTemplateName,
|
|
120
|
+
this.mailerOptions.expiringTokenTxtTemplateName,
|
|
121
|
+
this.mailerOptions.emailBaseLayoutName,
|
|
122
|
+
);
|
|
123
|
+
} else if (messageType === WardenCustomerMessageType.MagicLink) {
|
|
124
|
+
await this.mailer.fillEmailBody(
|
|
125
|
+
rts,
|
|
126
|
+
context,
|
|
127
|
+
this.mailerOptions.magicLinkHtmlTemplateName,
|
|
128
|
+
this.mailerOptions.magicLinkTxtTemplateName,
|
|
129
|
+
this.mailerOptions.emailBaseLayoutName,
|
|
130
|
+
);
|
|
131
|
+
} else if (messageType === WardenCustomerMessageType.Custom) {
|
|
132
|
+
if (!customTemplate) {
|
|
133
|
+
throw ErrorRatchet.fErr('Cannot send custom message if customTemplate not set');
|
|
134
|
+
}
|
|
135
|
+
Logger.info('Sending custom template : %j', customTemplate);
|
|
136
|
+
await this.mailer.fillEmailBody(
|
|
137
|
+
rts,
|
|
138
|
+
context,
|
|
139
|
+
customTemplate.htmlVersion,
|
|
140
|
+
customTemplate.textVersion,
|
|
141
|
+
customTemplate.baseLayout === 'DEFAULT' ? this.mailerOptions.emailBaseLayoutName : customTemplate.baseLayout,
|
|
142
|
+
);
|
|
143
|
+
} else {
|
|
144
|
+
throw ErrorRatchet.fErr('No such message type : %s', messageType);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return rts;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public async sendMessage(message: ReadyToSendEmail): Promise<boolean> {
|
|
151
|
+
const rval: SendEmailResult<any, any> = await this.mailer.sendEmail(message);
|
|
152
|
+
Logger.debug('SendRawEmailResponse was : %j', rval);
|
|
153
|
+
return !!rval;
|
|
154
|
+
}
|
|
155
|
+
}
|
package/src/server/provider/warden-mailer-and-expiring-code-ratchet-single-user-provider-options.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Service for interacting with positions for a given user
|
|
2
|
+
|
|
3
|
+
export interface WardenMailerAndExpiringCodeRatchetSingleUseCodeProviderOptions {
|
|
4
|
+
emailBaseLayoutName?: string;
|
|
5
|
+
expiringTokenHtmlTemplateName: string;
|
|
6
|
+
expiringTokenTxtTemplateName?: string;
|
|
7
|
+
magicLinkHtmlTemplateName: string;
|
|
8
|
+
magicLinkTxtTemplateName?: string;
|
|
9
|
+
magicLinkSubjectLine?: string;
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { WardenContactType } from '@bitblit/ratchet-warden-common/common/model/warden-contact-type';
|
|
2
|
+
import { WardenContact } from '@bitblit/ratchet-warden-common/common/model/warden-contact';
|
|
3
|
+
import { WardenCustomerMessageType } from '@bitblit/ratchet-warden-common/common/model/warden-customer-message-type';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Classes implementing WardenMessageSendingProvider are able to
|
|
7
|
+
* send expiring, single
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface WardenMessageSendingProvider<T> {
|
|
11
|
+
handlesContactType(type: WardenContactType): boolean;
|
|
12
|
+
sendMessage(contact: WardenContact, message: T): Promise<boolean>;
|
|
13
|
+
formatMessage(contact: WardenContact, messageType: WardenCustomerMessageType, context: Record<string, any>): Promise<T>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default implementation of the event processing provider - does nothing
|
|
3
|
+
*/
|
|
4
|
+
import { WardenEntry } from '@bitblit/ratchet-warden-common/common/model/warden-entry';
|
|
5
|
+
import { WardenEventProcessingProvider } from './warden-event-processing-provider.js';
|
|
6
|
+
|
|
7
|
+
export class WardenNoOpEventProcessingProvider implements WardenEventProcessingProvider {
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
9
|
+
public async userCreated(_entry: WardenEntry): Promise<void> {}
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
11
|
+
public async userRemoved(_entry: WardenEntry): Promise<void> {}
|
|
12
|
+
}
|