@auth/azure-tables-adapter 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +26 -0
- package/index.d.ts +70 -0
- package/index.d.ts.map +1 -0
- package/index.js +258 -0
- package/package.json +59 -0
- package/src/index.ts +303 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022-2023, Balázs Orbán
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<br/>
|
|
3
|
+
<a href="https://authjs.dev" target="_blank">
|
|
4
|
+
<img height="64px" src="https://authjs.dev/img/logo/logo-sm.png" />
|
|
5
|
+
</a>
|
|
6
|
+
<a href="https://azure.microsoft.com/en-us/products/storage/tables" target="_blank">
|
|
7
|
+
<img height="64px" src="https://authjs.dev/img/adapters/azure-tables.svg"/>
|
|
8
|
+
</a>
|
|
9
|
+
<h3 align="center"><b>Azure Table Storage Adapter</b> - NextAuth.js / Auth.js</a></h3>
|
|
10
|
+
<p align="center" style="align: center;">
|
|
11
|
+
<a href="https://npm.im/@auth/azure-tables-adapter">
|
|
12
|
+
<img src="https://img.shields.io/badge/TypeScript-blue?style=flat-square" alt="TypeScript" />
|
|
13
|
+
</a>
|
|
14
|
+
<a href="https://npm.im/@auth/azure-tables-adapter">
|
|
15
|
+
<img alt="npm" src="https://img.shields.io/npm/v/@auth/azure-tables-adapter?color=green&label=@auth/azure-tables-adapter&style=flat-square">
|
|
16
|
+
</a>
|
|
17
|
+
<a href="https://www.npmtrends.com/@auth/azure-tables-adapter">
|
|
18
|
+
<img src="https://img.shields.io/npm/dm/@auth/azure-tables-adapter?label=%20downloads&style=flat-square" alt="Downloads" />
|
|
19
|
+
</a>
|
|
20
|
+
<a href="https://github.com/nextauthjs/next-auth/stargazers">
|
|
21
|
+
<img src="https://img.shields.io/github/stars/nextauthjs/next-auth?style=flat-square" alt="Github Stars" />
|
|
22
|
+
</a>
|
|
23
|
+
</p>
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
---
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: 16}}>
|
|
3
|
+
* <p style={{fontWeight: "normal"}}>An official <a href="https://azure.microsoft.com/en-us/products/storage/tables">Azure Table Storage</a> adapter for Auth.js / NextAuth.js.</p>
|
|
4
|
+
* <a href="https://azure.microsoft.com/en-us/products/storage/tables">
|
|
5
|
+
* <img style={{display: "block"}} src="/img/adapters/azure-tables.svg" width="48" />
|
|
6
|
+
* </a>
|
|
7
|
+
* </div>
|
|
8
|
+
*
|
|
9
|
+
* ## Installation
|
|
10
|
+
*
|
|
11
|
+
* ```bash npm2yarn2pnpm
|
|
12
|
+
* npm install next-auth @auth/azure-tables-adapter
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* @module @auth/azure-tables-adapter
|
|
16
|
+
*/
|
|
17
|
+
import type { Adapter } from "@auth/core/adapters";
|
|
18
|
+
import { GetTableEntityResponse, TableClient, TableEntityResult } from "@azure/data-tables";
|
|
19
|
+
export declare const keys: {
|
|
20
|
+
user: string;
|
|
21
|
+
userByEmail: string;
|
|
22
|
+
account: string;
|
|
23
|
+
accountByUserId: string;
|
|
24
|
+
session: string;
|
|
25
|
+
sessionByUserId: string;
|
|
26
|
+
verificationToken: string;
|
|
27
|
+
};
|
|
28
|
+
export declare function withoutKeys<T>(entity: GetTableEntityResponse<TableEntityResult<T>>): T;
|
|
29
|
+
/**
|
|
30
|
+
*
|
|
31
|
+
* 1. Create a table for authentication data, `auth` in the example below.
|
|
32
|
+
*
|
|
33
|
+
* ```js title="auth.ts"
|
|
34
|
+
* import type { AuthConfig } from "next-auth"
|
|
35
|
+
* import { TableStorageAdapter } from "@next-auth/azure-tables-adapter"
|
|
36
|
+
* import { AzureNamedKeyCredential, TableClient } from "@azure/data-tables"
|
|
37
|
+
*
|
|
38
|
+
* const credential = new AzureNamedKeyCredential(
|
|
39
|
+
* process.env.AZURE_ACCOUNT,
|
|
40
|
+
* process.env.AZURE_ACCESS_KEY
|
|
41
|
+
* )
|
|
42
|
+
* const authClient = new TableClient(
|
|
43
|
+
* process.env.AZURE_TABLES_ENDPOINT,
|
|
44
|
+
* "auth",
|
|
45
|
+
* credential
|
|
46
|
+
* )
|
|
47
|
+
*
|
|
48
|
+
* // For more information on each option (and a full list of options) go to
|
|
49
|
+
* // https://authjs.dev/reference/configuration/auth-options
|
|
50
|
+
* export default const authConfig = {
|
|
51
|
+
* // https://authjs.dev/reference/providers/oauth-builtin
|
|
52
|
+
* providers: [
|
|
53
|
+
* // ...
|
|
54
|
+
* ],
|
|
55
|
+
* adapter: TableStorageAdapter(authClient),
|
|
56
|
+
* // ...
|
|
57
|
+
* } satisfies AuthConfig
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* Environment variable are as follows:
|
|
61
|
+
*
|
|
62
|
+
* ```
|
|
63
|
+
* AZURE_ACCOUNT=storageaccountname
|
|
64
|
+
* AZURE_ACCESS_KEY=longRandomKey
|
|
65
|
+
* AZURE_TABLES_ENDPOINT=https://$AZURE_ACCOUNT.table.core.windows.net
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
*/
|
|
69
|
+
export declare const TableStorageAdapter: (client: TableClient) => Adapter;
|
|
70
|
+
//# sourceMappingURL=index.d.ts.map
|
package/index.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EACV,OAAO,EAKR,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACL,sBAAsB,EACtB,WAAW,EACX,iBAAiB,EAClB,MAAM,oBAAoB,CAAA;AAI3B,eAAO,MAAM,IAAI;;;;;;;;CAQhB,CAAA;AAED,wBAAgB,WAAW,CAAC,CAAC,EAC3B,MAAM,EAAE,sBAAsB,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,GACnD,CAAC,CAUH;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,eAAO,MAAM,mBAAmB,WAAY,WAAW,KAAG,OA+MzD,CAAA"}
|
package/index.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: 16}}>
|
|
3
|
+
* <p style={{fontWeight: "normal"}}>An official <a href="https://azure.microsoft.com/en-us/products/storage/tables">Azure Table Storage</a> adapter for Auth.js / NextAuth.js.</p>
|
|
4
|
+
* <a href="https://azure.microsoft.com/en-us/products/storage/tables">
|
|
5
|
+
* <img style={{display: "block"}} src="/img/adapters/azure-tables.svg" width="48" />
|
|
6
|
+
* </a>
|
|
7
|
+
* </div>
|
|
8
|
+
*
|
|
9
|
+
* ## Installation
|
|
10
|
+
*
|
|
11
|
+
* ```bash npm2yarn2pnpm
|
|
12
|
+
* npm install next-auth @auth/azure-tables-adapter
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* @module @auth/azure-tables-adapter
|
|
16
|
+
*/
|
|
17
|
+
globalThis.crypto ?? (globalThis.crypto = require("node:crypto").webcrypto);
|
|
18
|
+
export const keys = {
|
|
19
|
+
user: "user",
|
|
20
|
+
userByEmail: "userByEmail",
|
|
21
|
+
account: "account",
|
|
22
|
+
accountByUserId: "accountByUserId",
|
|
23
|
+
session: "session",
|
|
24
|
+
sessionByUserId: "sessionByUserId",
|
|
25
|
+
verificationToken: "verificationToken",
|
|
26
|
+
};
|
|
27
|
+
export function withoutKeys(entity) {
|
|
28
|
+
delete entity.partitionKey;
|
|
29
|
+
delete entity.rowKey;
|
|
30
|
+
// @ts-expect-error
|
|
31
|
+
delete entity.etag;
|
|
32
|
+
delete entity.timestamp;
|
|
33
|
+
// @ts-expect-error
|
|
34
|
+
delete entity["odata.metadata"];
|
|
35
|
+
return entity;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
*
|
|
39
|
+
* 1. Create a table for authentication data, `auth` in the example below.
|
|
40
|
+
*
|
|
41
|
+
* ```js title="auth.ts"
|
|
42
|
+
* import type { AuthConfig } from "next-auth"
|
|
43
|
+
* import { TableStorageAdapter } from "@next-auth/azure-tables-adapter"
|
|
44
|
+
* import { AzureNamedKeyCredential, TableClient } from "@azure/data-tables"
|
|
45
|
+
*
|
|
46
|
+
* const credential = new AzureNamedKeyCredential(
|
|
47
|
+
* process.env.AZURE_ACCOUNT,
|
|
48
|
+
* process.env.AZURE_ACCESS_KEY
|
|
49
|
+
* )
|
|
50
|
+
* const authClient = new TableClient(
|
|
51
|
+
* process.env.AZURE_TABLES_ENDPOINT,
|
|
52
|
+
* "auth",
|
|
53
|
+
* credential
|
|
54
|
+
* )
|
|
55
|
+
*
|
|
56
|
+
* // For more information on each option (and a full list of options) go to
|
|
57
|
+
* // https://authjs.dev/reference/configuration/auth-options
|
|
58
|
+
* export default const authConfig = {
|
|
59
|
+
* // https://authjs.dev/reference/providers/oauth-builtin
|
|
60
|
+
* providers: [
|
|
61
|
+
* // ...
|
|
62
|
+
* ],
|
|
63
|
+
* adapter: TableStorageAdapter(authClient),
|
|
64
|
+
* // ...
|
|
65
|
+
* } satisfies AuthConfig
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* Environment variable are as follows:
|
|
69
|
+
*
|
|
70
|
+
* ```
|
|
71
|
+
* AZURE_ACCOUNT=storageaccountname
|
|
72
|
+
* AZURE_ACCESS_KEY=longRandomKey
|
|
73
|
+
* AZURE_TABLES_ENDPOINT=https://$AZURE_ACCOUNT.table.core.windows.net
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
*/
|
|
77
|
+
export const TableStorageAdapter = (client) => {
|
|
78
|
+
return {
|
|
79
|
+
async createUser(user) {
|
|
80
|
+
const id = crypto.randomUUID();
|
|
81
|
+
const newUser = {
|
|
82
|
+
...user,
|
|
83
|
+
id,
|
|
84
|
+
};
|
|
85
|
+
await Promise.all([
|
|
86
|
+
client.createEntity({
|
|
87
|
+
...newUser,
|
|
88
|
+
partitionKey: keys.userByEmail,
|
|
89
|
+
rowKey: user.email,
|
|
90
|
+
}),
|
|
91
|
+
client.createEntity({
|
|
92
|
+
...newUser,
|
|
93
|
+
partitionKey: keys.user,
|
|
94
|
+
rowKey: id,
|
|
95
|
+
}),
|
|
96
|
+
]);
|
|
97
|
+
return newUser;
|
|
98
|
+
},
|
|
99
|
+
async getUser(id) {
|
|
100
|
+
try {
|
|
101
|
+
const user = await client.getEntity(keys.user, id);
|
|
102
|
+
return withoutKeys(user);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
async getUserByEmail(email) {
|
|
109
|
+
try {
|
|
110
|
+
const user = await client.getEntity(keys.userByEmail, email);
|
|
111
|
+
return withoutKeys(user);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
async getUserByAccount({ providerAccountId, provider }) {
|
|
118
|
+
try {
|
|
119
|
+
const rowKey = `${providerAccountId}_${provider}`;
|
|
120
|
+
const account = await client.getEntity(keys.account, rowKey);
|
|
121
|
+
const user = await client.getEntity(keys.user, account.userId);
|
|
122
|
+
return withoutKeys(user);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
async updateUser(user) {
|
|
129
|
+
const _user = await client.getEntity(keys.user, user.id);
|
|
130
|
+
const updatedUser = {
|
|
131
|
+
...user,
|
|
132
|
+
partitionKey: keys.user,
|
|
133
|
+
rowKey: _user.id,
|
|
134
|
+
};
|
|
135
|
+
await client.updateEntity(updatedUser, "Merge");
|
|
136
|
+
return { ..._user, ...updatedUser };
|
|
137
|
+
},
|
|
138
|
+
async deleteUser(userId) {
|
|
139
|
+
try {
|
|
140
|
+
const user = await client.getEntity(keys.user, userId);
|
|
141
|
+
const { sessionToken } = await client.getEntity(keys.sessionByUserId, userId);
|
|
142
|
+
const accounts = withoutKeys(await client.getEntity(keys.accountByUserId, userId));
|
|
143
|
+
const deleteAccounts = Object.keys(accounts).map((property) => client.deleteEntity(keys.account, `${accounts[property]}_${property}`));
|
|
144
|
+
await Promise.allSettled([
|
|
145
|
+
client.deleteEntity(keys.userByEmail, user.email),
|
|
146
|
+
client.deleteEntity(keys.user, userId),
|
|
147
|
+
client.deleteEntity(keys.session, sessionToken),
|
|
148
|
+
client.deleteEntity(keys.sessionByUserId, userId),
|
|
149
|
+
...deleteAccounts,
|
|
150
|
+
client.deleteEntity(keys.accountByUserId, userId),
|
|
151
|
+
]);
|
|
152
|
+
return withoutKeys(user);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
async linkAccount(account) {
|
|
159
|
+
try {
|
|
160
|
+
await client.createEntity({
|
|
161
|
+
...account,
|
|
162
|
+
partitionKey: keys.account,
|
|
163
|
+
rowKey: `${account.providerAccountId}_${account.provider}`,
|
|
164
|
+
});
|
|
165
|
+
await client.upsertEntity({
|
|
166
|
+
partitionKey: keys.accountByUserId,
|
|
167
|
+
rowKey: account.userId,
|
|
168
|
+
[account.provider]: account.providerAccountId,
|
|
169
|
+
});
|
|
170
|
+
return account;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
async unlinkAccount({ providerAccountId, provider }) {
|
|
177
|
+
const rowKey = `${providerAccountId}_${provider}`;
|
|
178
|
+
const account = await client.getEntity(keys.account, rowKey);
|
|
179
|
+
await client.deleteEntity(keys.account, rowKey);
|
|
180
|
+
await client.deleteEntity(keys.accountByUserId, account.userId);
|
|
181
|
+
},
|
|
182
|
+
async createSession(session) {
|
|
183
|
+
await client.createEntity({
|
|
184
|
+
...session,
|
|
185
|
+
partitionKey: keys.session,
|
|
186
|
+
rowKey: session.sessionToken,
|
|
187
|
+
});
|
|
188
|
+
await client.upsertEntity({
|
|
189
|
+
partitionKey: keys.sessionByUserId,
|
|
190
|
+
rowKey: session.userId,
|
|
191
|
+
sessionToken: session.sessionToken,
|
|
192
|
+
});
|
|
193
|
+
return session;
|
|
194
|
+
},
|
|
195
|
+
async getSessionAndUser(sessionToken) {
|
|
196
|
+
try {
|
|
197
|
+
const session = await client.getEntity(keys.session, sessionToken);
|
|
198
|
+
if (session.expires.valueOf() < Date.now()) {
|
|
199
|
+
await client.deleteEntity(keys.session, sessionToken);
|
|
200
|
+
}
|
|
201
|
+
const user = await client.getEntity(keys.user, session.userId);
|
|
202
|
+
return {
|
|
203
|
+
session: withoutKeys(session),
|
|
204
|
+
user: withoutKeys(user),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
async updateSession(session) {
|
|
212
|
+
const _session = await client.getEntity(keys.session, session.sessionToken);
|
|
213
|
+
const newSession = {
|
|
214
|
+
expires: session.expires ?? _session.expires,
|
|
215
|
+
};
|
|
216
|
+
await client.updateEntity({
|
|
217
|
+
...newSession,
|
|
218
|
+
partitionKey: keys.session,
|
|
219
|
+
rowKey: session.sessionToken,
|
|
220
|
+
});
|
|
221
|
+
return { ...withoutKeys(_session), ...newSession };
|
|
222
|
+
},
|
|
223
|
+
async deleteSession(sessionToken) {
|
|
224
|
+
try {
|
|
225
|
+
const session = await client.getEntity(keys.session, sessionToken);
|
|
226
|
+
await Promise.allSettled([
|
|
227
|
+
client.deleteEntity(keys.session, sessionToken),
|
|
228
|
+
client.deleteEntity(keys.sessionByUserId, session.userId),
|
|
229
|
+
]);
|
|
230
|
+
return withoutKeys(session);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
async createVerificationToken(token) {
|
|
237
|
+
await client.createEntity({
|
|
238
|
+
...token,
|
|
239
|
+
partitionKey: keys.verificationToken,
|
|
240
|
+
rowKey: token.token,
|
|
241
|
+
});
|
|
242
|
+
return token;
|
|
243
|
+
},
|
|
244
|
+
async useVerificationToken({ identifier, token }) {
|
|
245
|
+
try {
|
|
246
|
+
const tokenEntity = await client.getEntity(keys.verificationToken, token);
|
|
247
|
+
if (tokenEntity.identifier !== identifier) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
await client.deleteEntity(keys.verificationToken, token);
|
|
251
|
+
return withoutKeys(tokenEntity);
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@auth/azure-tables-adapter",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Azure Tables Storage adapter for next-auth.",
|
|
5
|
+
"homepage": "https://authjs.dev",
|
|
6
|
+
"repository": "https://github.com/nextauthjs/next-auth",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/nextauthjs/next-auth/issues"
|
|
9
|
+
},
|
|
10
|
+
"author": "Nikita Dmitrijev <nikitadmitry@gmail.com>",
|
|
11
|
+
"contributors": [
|
|
12
|
+
"Thang Huu Vu <hi@thvu.dev>"
|
|
13
|
+
],
|
|
14
|
+
"license": "ISC",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"next-auth",
|
|
17
|
+
"next.js",
|
|
18
|
+
"oauth",
|
|
19
|
+
"azure-tables",
|
|
20
|
+
"adapter"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"types": "./index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./index.d.ts",
|
|
27
|
+
"import": "./index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"*.d.ts*",
|
|
32
|
+
"*.js",
|
|
33
|
+
"src"
|
|
34
|
+
],
|
|
35
|
+
"private": false,
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@auth/core": "0.15.2"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@azure/data-tables": "^13.2.1"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"jest": "^27.4.3",
|
|
47
|
+
"@azure/data-tables": "^13.2.1",
|
|
48
|
+
"@auth/adapter-test": "0.0.0",
|
|
49
|
+
"@auth/tsconfig": "0.0.0"
|
|
50
|
+
},
|
|
51
|
+
"jest": {
|
|
52
|
+
"preset": "@auth/adapter-test/jest"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"test": "./tests/test.sh",
|
|
56
|
+
"test:watch": "./tests/test.sh -w",
|
|
57
|
+
"build": "tsc"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: 16}}>
|
|
3
|
+
* <p style={{fontWeight: "normal"}}>An official <a href="https://azure.microsoft.com/en-us/products/storage/tables">Azure Table Storage</a> adapter for Auth.js / NextAuth.js.</p>
|
|
4
|
+
* <a href="https://azure.microsoft.com/en-us/products/storage/tables">
|
|
5
|
+
* <img style={{display: "block"}} src="/img/adapters/azure-tables.svg" width="48" />
|
|
6
|
+
* </a>
|
|
7
|
+
* </div>
|
|
8
|
+
*
|
|
9
|
+
* ## Installation
|
|
10
|
+
*
|
|
11
|
+
* ```bash npm2yarn2pnpm
|
|
12
|
+
* npm install next-auth @auth/azure-tables-adapter
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* @module @auth/azure-tables-adapter
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
Adapter,
|
|
20
|
+
AdapterUser,
|
|
21
|
+
AdapterAccount,
|
|
22
|
+
AdapterSession,
|
|
23
|
+
VerificationToken,
|
|
24
|
+
} from "@auth/core/adapters"
|
|
25
|
+
import {
|
|
26
|
+
GetTableEntityResponse,
|
|
27
|
+
TableClient,
|
|
28
|
+
TableEntityResult,
|
|
29
|
+
} from "@azure/data-tables"
|
|
30
|
+
|
|
31
|
+
globalThis.crypto ??= require("node:crypto").webcrypto
|
|
32
|
+
|
|
33
|
+
export const keys = {
|
|
34
|
+
user: "user",
|
|
35
|
+
userByEmail: "userByEmail",
|
|
36
|
+
account: "account",
|
|
37
|
+
accountByUserId: "accountByUserId",
|
|
38
|
+
session: "session",
|
|
39
|
+
sessionByUserId: "sessionByUserId",
|
|
40
|
+
verificationToken: "verificationToken",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function withoutKeys<T>(
|
|
44
|
+
entity: GetTableEntityResponse<TableEntityResult<T>>
|
|
45
|
+
): T {
|
|
46
|
+
delete entity.partitionKey
|
|
47
|
+
delete entity.rowKey
|
|
48
|
+
// @ts-expect-error
|
|
49
|
+
delete entity.etag
|
|
50
|
+
delete entity.timestamp
|
|
51
|
+
// @ts-expect-error
|
|
52
|
+
delete entity["odata.metadata"]
|
|
53
|
+
|
|
54
|
+
return entity
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
*
|
|
58
|
+
* 1. Create a table for authentication data, `auth` in the example below.
|
|
59
|
+
*
|
|
60
|
+
* ```js title="auth.ts"
|
|
61
|
+
* import type { AuthConfig } from "next-auth"
|
|
62
|
+
* import { TableStorageAdapter } from "@next-auth/azure-tables-adapter"
|
|
63
|
+
* import { AzureNamedKeyCredential, TableClient } from "@azure/data-tables"
|
|
64
|
+
*
|
|
65
|
+
* const credential = new AzureNamedKeyCredential(
|
|
66
|
+
* process.env.AZURE_ACCOUNT,
|
|
67
|
+
* process.env.AZURE_ACCESS_KEY
|
|
68
|
+
* )
|
|
69
|
+
* const authClient = new TableClient(
|
|
70
|
+
* process.env.AZURE_TABLES_ENDPOINT,
|
|
71
|
+
* "auth",
|
|
72
|
+
* credential
|
|
73
|
+
* )
|
|
74
|
+
*
|
|
75
|
+
* // For more information on each option (and a full list of options) go to
|
|
76
|
+
* // https://authjs.dev/reference/configuration/auth-options
|
|
77
|
+
* export default const authConfig = {
|
|
78
|
+
* // https://authjs.dev/reference/providers/oauth-builtin
|
|
79
|
+
* providers: [
|
|
80
|
+
* // ...
|
|
81
|
+
* ],
|
|
82
|
+
* adapter: TableStorageAdapter(authClient),
|
|
83
|
+
* // ...
|
|
84
|
+
* } satisfies AuthConfig
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
87
|
+
* Environment variable are as follows:
|
|
88
|
+
*
|
|
89
|
+
* ```
|
|
90
|
+
* AZURE_ACCOUNT=storageaccountname
|
|
91
|
+
* AZURE_ACCESS_KEY=longRandomKey
|
|
92
|
+
* AZURE_TABLES_ENDPOINT=https://$AZURE_ACCOUNT.table.core.windows.net
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
*/
|
|
96
|
+
export const TableStorageAdapter = (client: TableClient): Adapter => {
|
|
97
|
+
return {
|
|
98
|
+
async createUser(user) {
|
|
99
|
+
const id = crypto.randomUUID()
|
|
100
|
+
const newUser = {
|
|
101
|
+
...user,
|
|
102
|
+
id,
|
|
103
|
+
}
|
|
104
|
+
await Promise.all([
|
|
105
|
+
client.createEntity({
|
|
106
|
+
...newUser,
|
|
107
|
+
partitionKey: keys.userByEmail,
|
|
108
|
+
rowKey: user.email,
|
|
109
|
+
}),
|
|
110
|
+
client.createEntity({
|
|
111
|
+
...newUser,
|
|
112
|
+
partitionKey: keys.user,
|
|
113
|
+
rowKey: id,
|
|
114
|
+
}),
|
|
115
|
+
])
|
|
116
|
+
return newUser
|
|
117
|
+
},
|
|
118
|
+
async getUser(id: string) {
|
|
119
|
+
try {
|
|
120
|
+
const user = await client.getEntity<AdapterUser>(keys.user, id)
|
|
121
|
+
return withoutKeys(user)
|
|
122
|
+
} catch {
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
async getUserByEmail(email) {
|
|
127
|
+
try {
|
|
128
|
+
const user = await client.getEntity<AdapterUser>(
|
|
129
|
+
keys.userByEmail,
|
|
130
|
+
email
|
|
131
|
+
)
|
|
132
|
+
return withoutKeys(user)
|
|
133
|
+
} catch {
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
async getUserByAccount({ providerAccountId, provider }) {
|
|
138
|
+
try {
|
|
139
|
+
const rowKey = `${providerAccountId}_${provider}`
|
|
140
|
+
const account = await client.getEntity<AdapterAccount>(
|
|
141
|
+
keys.account,
|
|
142
|
+
rowKey
|
|
143
|
+
)
|
|
144
|
+
const user = await client.getEntity<AdapterUser>(
|
|
145
|
+
keys.user,
|
|
146
|
+
account.userId
|
|
147
|
+
)
|
|
148
|
+
return withoutKeys(user)
|
|
149
|
+
} catch {
|
|
150
|
+
return null
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
async updateUser(user) {
|
|
154
|
+
const _user = await client.getEntity<AdapterUser>(keys.user, user.id)
|
|
155
|
+
const updatedUser = {
|
|
156
|
+
...user,
|
|
157
|
+
partitionKey: keys.user,
|
|
158
|
+
rowKey: _user.id,
|
|
159
|
+
}
|
|
160
|
+
await client.updateEntity(updatedUser, "Merge")
|
|
161
|
+
return { ..._user, ...updatedUser }
|
|
162
|
+
},
|
|
163
|
+
async deleteUser(userId) {
|
|
164
|
+
try {
|
|
165
|
+
const user = await client.getEntity<AdapterUser>(keys.user, userId)
|
|
166
|
+
const { sessionToken } = await client.getEntity<AdapterSession>(
|
|
167
|
+
keys.sessionByUserId,
|
|
168
|
+
userId
|
|
169
|
+
)
|
|
170
|
+
const accounts = withoutKeys(
|
|
171
|
+
await client.getEntity<AdapterAccount>(keys.accountByUserId, userId)
|
|
172
|
+
)
|
|
173
|
+
const deleteAccounts = Object.keys(accounts).map((property) =>
|
|
174
|
+
client.deleteEntity(keys.account, `${accounts[property]}_${property}`)
|
|
175
|
+
)
|
|
176
|
+
await Promise.allSettled([
|
|
177
|
+
client.deleteEntity(keys.userByEmail, user.email),
|
|
178
|
+
client.deleteEntity(keys.user, userId),
|
|
179
|
+
client.deleteEntity(keys.session, sessionToken),
|
|
180
|
+
client.deleteEntity(keys.sessionByUserId, userId),
|
|
181
|
+
...deleteAccounts,
|
|
182
|
+
client.deleteEntity(keys.accountByUserId, userId),
|
|
183
|
+
])
|
|
184
|
+
return withoutKeys(user)
|
|
185
|
+
} catch {
|
|
186
|
+
return null
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
async linkAccount(account) {
|
|
190
|
+
try {
|
|
191
|
+
await client.createEntity({
|
|
192
|
+
...account,
|
|
193
|
+
partitionKey: keys.account,
|
|
194
|
+
rowKey: `${account.providerAccountId}_${account.provider}`,
|
|
195
|
+
})
|
|
196
|
+
await client.upsertEntity({
|
|
197
|
+
partitionKey: keys.accountByUserId,
|
|
198
|
+
rowKey: account.userId,
|
|
199
|
+
[account.provider]: account.providerAccountId,
|
|
200
|
+
})
|
|
201
|
+
return account
|
|
202
|
+
} catch {
|
|
203
|
+
return null
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
async unlinkAccount({ providerAccountId, provider }) {
|
|
207
|
+
const rowKey = `${providerAccountId}_${provider}`
|
|
208
|
+
const account = await client.getEntity<AdapterAccount>(
|
|
209
|
+
keys.account,
|
|
210
|
+
rowKey
|
|
211
|
+
)
|
|
212
|
+
await client.deleteEntity(keys.account, rowKey)
|
|
213
|
+
await client.deleteEntity(keys.accountByUserId, account.userId)
|
|
214
|
+
},
|
|
215
|
+
async createSession(session) {
|
|
216
|
+
await client.createEntity({
|
|
217
|
+
...session,
|
|
218
|
+
partitionKey: keys.session,
|
|
219
|
+
rowKey: session.sessionToken,
|
|
220
|
+
})
|
|
221
|
+
await client.upsertEntity({
|
|
222
|
+
partitionKey: keys.sessionByUserId,
|
|
223
|
+
rowKey: session.userId,
|
|
224
|
+
sessionToken: session.sessionToken,
|
|
225
|
+
})
|
|
226
|
+
return session
|
|
227
|
+
},
|
|
228
|
+
async getSessionAndUser(sessionToken) {
|
|
229
|
+
try {
|
|
230
|
+
const session = await client.getEntity<AdapterSession>(
|
|
231
|
+
keys.session,
|
|
232
|
+
sessionToken
|
|
233
|
+
)
|
|
234
|
+
if (session.expires.valueOf() < Date.now()) {
|
|
235
|
+
await client.deleteEntity(keys.session, sessionToken)
|
|
236
|
+
}
|
|
237
|
+
const user = await client.getEntity<AdapterUser>(
|
|
238
|
+
keys.user,
|
|
239
|
+
session.userId
|
|
240
|
+
)
|
|
241
|
+
return {
|
|
242
|
+
session: withoutKeys(session),
|
|
243
|
+
user: withoutKeys(user),
|
|
244
|
+
}
|
|
245
|
+
} catch {
|
|
246
|
+
return null
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
async updateSession(session) {
|
|
250
|
+
const _session = await client.getEntity<AdapterSession>(
|
|
251
|
+
keys.session,
|
|
252
|
+
session.sessionToken
|
|
253
|
+
)
|
|
254
|
+
const newSession = {
|
|
255
|
+
expires: session.expires ?? _session.expires,
|
|
256
|
+
}
|
|
257
|
+
await client.updateEntity({
|
|
258
|
+
...newSession,
|
|
259
|
+
partitionKey: keys.session,
|
|
260
|
+
rowKey: session.sessionToken,
|
|
261
|
+
})
|
|
262
|
+
return { ...withoutKeys(_session), ...newSession }
|
|
263
|
+
},
|
|
264
|
+
async deleteSession(sessionToken) {
|
|
265
|
+
try {
|
|
266
|
+
const session = await client.getEntity<AdapterSession>(
|
|
267
|
+
keys.session,
|
|
268
|
+
sessionToken
|
|
269
|
+
)
|
|
270
|
+
await Promise.allSettled([
|
|
271
|
+
client.deleteEntity(keys.session, sessionToken),
|
|
272
|
+
client.deleteEntity(keys.sessionByUserId, session.userId),
|
|
273
|
+
])
|
|
274
|
+
return withoutKeys(session)
|
|
275
|
+
} catch {
|
|
276
|
+
return null
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
async createVerificationToken(token) {
|
|
280
|
+
await client.createEntity({
|
|
281
|
+
...token,
|
|
282
|
+
partitionKey: keys.verificationToken,
|
|
283
|
+
rowKey: token.token,
|
|
284
|
+
})
|
|
285
|
+
return token
|
|
286
|
+
},
|
|
287
|
+
async useVerificationToken({ identifier, token }) {
|
|
288
|
+
try {
|
|
289
|
+
const tokenEntity = await client.getEntity<VerificationToken>(
|
|
290
|
+
keys.verificationToken,
|
|
291
|
+
token
|
|
292
|
+
)
|
|
293
|
+
if (tokenEntity.identifier !== identifier) {
|
|
294
|
+
return null
|
|
295
|
+
}
|
|
296
|
+
await client.deleteEntity(keys.verificationToken, token)
|
|
297
|
+
return withoutKeys(tokenEntity)
|
|
298
|
+
} catch {
|
|
299
|
+
return null
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
}
|
|
303
|
+
}
|