mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
856 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0eb4159737 | ||
|
|
9f0008375f | ||
|
|
0cf1823e70 | ||
|
|
7f39669053 | ||
|
|
7b82d6e985 | ||
|
|
53b0a7aa74 | ||
|
|
fbb09303af | ||
|
|
ff05ac1e41 | ||
|
|
a6f6c1590d | ||
|
|
52c2466b9e | ||
|
|
fb04ab4e76 | ||
|
|
07e2a83ccb | ||
|
|
065d9eb310 | ||
|
|
5da19f3c9a | ||
|
|
88ec3bad71 | ||
|
|
5ab7868533 | ||
|
|
835dda0899 | ||
|
|
ad4709764a | ||
|
|
55a2932973 | ||
|
|
49cb7e5bd7 | ||
|
|
b95182ded7 | ||
|
|
be28672fd4 | ||
|
|
e26c5fc905 | ||
|
|
543f812eb3 | ||
|
|
0c9f9777c5 | ||
|
|
34f334a71d | ||
|
|
172ad02f8c | ||
|
|
eed0001524 | ||
|
|
a2705fb5b9 | ||
|
|
cb1e5a7174 | ||
|
|
8cba5d06b5 | ||
|
|
c52ce57296 | ||
|
|
4dae822806 | ||
|
|
901f6fb189 | ||
|
|
bb2237d0f9 | ||
|
|
fab7ce8f94 | ||
|
|
2becdad990 | ||
|
|
e3c2c7756d | ||
|
|
718f2537cb | ||
|
|
ae26df091d | ||
|
|
1cbf73b2f6 | ||
|
|
46b62d723c | ||
|
|
457952bb5a | ||
|
|
17b789822d | ||
|
|
fd12194c0c | ||
|
|
1c662e9ddc | ||
|
|
968903d227 | ||
|
|
3dedf6c26c | ||
|
|
d1dbfe1023 | ||
|
|
63171a0271 | ||
|
|
872e180339 | ||
|
|
a7709df796 | ||
|
|
dbe283f0fa | ||
|
|
727426bbec | ||
|
|
5a3ee47337 | ||
|
|
8dd8f7c48a | ||
|
|
072c59d85a | ||
|
|
d0e8147a48 | ||
|
|
5bc8f8b986 | ||
|
|
136361e1f4 | ||
|
|
cc9869592c | ||
|
|
073da60c3d | ||
|
|
aacc34bbfd | ||
|
|
074688d10e | ||
|
|
b3ffe50c03 | ||
|
|
e6d063825d | ||
|
|
985f7b3431 | ||
|
|
237b016378 | ||
|
|
776b9480da | ||
|
|
07a3f76cb3 | ||
|
|
3726d68c49 | ||
|
|
e31db1726a | ||
|
|
a241199c11 | ||
|
|
5385ec20a9 | ||
|
|
810d02e671 | ||
|
|
e5535734f8 | ||
|
|
ba7aed1057 | ||
|
|
a7e08e2a32 | ||
|
|
0d13040264 | ||
|
|
582520f8ef | ||
|
|
95942519a5 | ||
|
|
f3e64f04cc | ||
|
|
ed5cc4aa65 | ||
|
|
0e20b60229 | ||
|
|
3aee24b5dc | ||
|
|
960ca85907 | ||
|
|
f960cc0f6f | ||
|
|
0f64f3eea7 | ||
|
|
71c78e8e24 | ||
|
|
d86609a2dc | ||
|
|
d0c3400d30 | ||
|
|
172e79cb04 | ||
|
|
46d5c76605 | ||
|
|
438efd8a9b | ||
|
|
d8d497cc91 | ||
|
|
6152c8afbb | ||
|
|
5ae6f6118c | ||
|
|
96ff048b59 | ||
|
|
e80f6e936d | ||
|
|
6b5a215fb2 | ||
|
|
782482b9f4 | ||
|
|
2d364f246a | ||
|
|
564b342f69 | ||
|
|
63638d81dc | ||
|
|
28683015f1 | ||
|
|
726c49603d | ||
|
|
a7113c6d3e | ||
|
|
910514c6e2 | ||
|
|
b7cca484cf | ||
|
|
e293e786a8 | ||
|
|
82dd6ba3e4 | ||
|
|
6e28a07746 | ||
|
|
61047e3c14 | ||
|
|
dc5f3f481d | ||
|
|
0343344802 | ||
|
|
134a95a4bd | ||
|
|
52a4bd97cd | ||
|
|
87d43e4038 | ||
|
|
68695af1f3 | ||
|
|
76df2b5e70 | ||
|
|
8bd9d87633 | ||
|
|
6af40e3fe2 | ||
|
|
cdc1ac52b2 | ||
|
|
f2a7ee0b34 | ||
|
|
d67f1b7718 | ||
|
|
396f5d8bbc | ||
|
|
d7e78d5996 | ||
|
|
ad3b0b6a7d | ||
|
|
f4a954ccbb | ||
|
|
93f051ce08 | ||
|
|
645b53ee49 | ||
|
|
9c9744f30a | ||
|
|
9860ad8c8c | ||
|
|
23ada52f97 | ||
|
|
65e4910618 | ||
|
|
30c6d6360f | ||
|
|
214b22ecbb | ||
|
|
f50ac194aa | ||
|
|
b40e1441ec | ||
|
|
90a8f7c890 | ||
|
|
a7bae0395b | ||
|
|
71b3122fd1 | ||
|
|
1caa9bb813 | ||
|
|
daed68aee2 | ||
|
|
2f3ed7507b | ||
|
|
a15bdc191b | ||
|
|
ea71a1fb2f | ||
|
|
0069095bce | ||
|
|
42a822c407 | ||
|
|
2cfe5ad879 | ||
|
|
cba149f4b6 | ||
|
|
31bb2c342c | ||
|
|
af30be1fa4 | ||
|
|
ebaa28f04e | ||
|
|
e26297b901 | ||
|
|
2562b3c5d8 | ||
|
|
f8e5a79ce1 | ||
|
|
be53ef0f71 | ||
|
|
fbb5a12cbd | ||
|
|
70a186c183 | ||
|
|
cff9d3e294 | ||
|
|
2865b8cc2d | ||
|
|
b84f1b681c | ||
|
|
ea1d09bf83 | ||
|
|
a18ec09307 | ||
|
|
37cb81094f | ||
|
|
040d7c5017 | ||
|
|
f53ea6c9a9 | ||
|
|
7f670c5222 | ||
|
|
b1a99ec32f | ||
|
|
ca065604a3 | ||
|
|
77de2abd14 | ||
|
|
f6d6c4344c | ||
|
|
e8b1513899 | ||
|
|
505efc8a5d | ||
|
|
76b983229a | ||
|
|
ecddaf696b | ||
|
|
b43e7dca43 | ||
|
|
e7c34fd74b | ||
|
|
e0dd8e400b | ||
|
|
21d22a7e08 | ||
|
|
cb2fe0cae6 | ||
|
|
9ed75c7788 | ||
|
|
8e8713755a | ||
|
|
7fdde6268e | ||
|
|
5e949a3b97 | ||
|
|
a979e040cd | ||
|
|
2205cfa754 | ||
|
|
0989ef6171 | ||
|
|
7979b1069e | ||
|
|
71b50082f8 | ||
|
|
f3cc4d1018 | ||
|
|
15570b7479 | ||
|
|
a5187b69e8 | ||
|
|
751fd7bb0e | ||
|
|
94054db3f3 | ||
|
|
f93dbbbfee | ||
|
|
e3fd0ad450 | ||
|
|
e06816a374 | ||
|
|
284118e708 | ||
|
|
84bcecbec1 | ||
|
|
5060bd7f9b | ||
|
|
16a8720b1d | ||
|
|
3c056d7ff5 | ||
|
|
49dd7a807d | ||
|
|
42596fbca5 | ||
|
|
68d0f9465a | ||
|
|
71b4af0894 | ||
|
|
8bbb0ec344 | ||
|
|
b2770d5a1f | ||
|
|
192e5bf07e | ||
|
|
9abdbb57eb | ||
|
|
989d23e827 | ||
|
|
025f33a91f | ||
|
|
6e2fc11d64 | ||
|
|
6b1b8613d0 | ||
|
|
6d023aa533 | ||
|
|
080dd5f569 | ||
|
|
a6867b3564 | ||
|
|
6750accc0a | ||
|
|
9aae7bbc54 | ||
|
|
a84fe596af | ||
|
|
18840ead40 | ||
|
|
f72ee5ec06 | ||
|
|
958c31a4ee | ||
|
|
f47f5c6c62 | ||
|
|
d5f5157366 | ||
|
|
9fbfd90bb2 | ||
|
|
97d6f19fab | ||
|
|
fdcc62bd26 | ||
|
|
4764d60268 | ||
|
|
5f51c975c9 | ||
|
|
0c9104c13a | ||
|
|
8a6f0944ef | ||
|
|
672cedc712 | ||
|
|
05b275956b | ||
|
|
219b01724b | ||
|
|
94b0c68c8f | ||
|
|
81ebff897f | ||
|
|
8ac14ed298 | ||
|
|
51cfec92ba | ||
|
|
78fd783bac | ||
|
|
cec46b0d67 | ||
|
|
7bedd4afa9 | ||
|
|
6e6a24a7af | ||
|
|
6f067be7b0 | ||
|
|
e4cc3a92e5 | ||
|
|
610ab39b40 | ||
|
|
7d1168637d | ||
|
|
b2c1f32057 | ||
|
|
9247495fee | ||
|
|
341fae28d4 | ||
|
|
b86ffa5dd5 | ||
|
|
5415a9c3ab | ||
|
|
dc516e8be8 | ||
|
|
29a0d9d295 | ||
|
|
5f5174f6e2 | ||
|
|
424b4ee257 | ||
|
|
545a7e752e | ||
|
|
c564b84182 | ||
|
|
0db233d208 | ||
|
|
5126f4e342 | ||
|
|
e09dfc6a7f | ||
|
|
ccfa1d55bb | ||
|
|
d7dc7b0753 | ||
|
|
0407e130c4 | ||
|
|
64084d634b | ||
|
|
438a737837 | ||
|
|
a482a64f10 | ||
|
|
2227d34725 | ||
|
|
9c6ef951a1 | ||
|
|
01c897f23e | ||
|
|
ea65d87d07 | ||
|
|
8d1e479d12 | ||
|
|
435b630849 | ||
|
|
773c74a756 | ||
|
|
6867bc92c8 | ||
|
|
eb6a4c46d9 | ||
|
|
cd3d2a138b | ||
|
|
0c356456bb | ||
|
|
6d44a34f7d | ||
|
|
7bda639361 | ||
|
|
40e453076e | ||
|
|
e065552784 | ||
|
|
a3104a009c | ||
|
|
e9eb6bc57e | ||
|
|
95e31b46af | ||
|
|
d5e70323f0 | ||
|
|
4e4d1eac28 | ||
|
|
15316f069e | ||
|
|
e6995d21cd | ||
|
|
433f096a63 | ||
|
|
9f487593fa | ||
|
|
65caaa6c4c | ||
|
|
0adfba8c5c | ||
|
|
2f0f738e2e | ||
|
|
1777a87be3 | ||
|
|
e94fd3b484 | ||
|
|
3b40335202 | ||
|
|
6d63b74db9 | ||
|
|
eb26722833 | ||
|
|
4937047d19 | ||
|
|
4305964864 | ||
|
|
91d93fb8fd | ||
|
|
e2e28fcfd0 | ||
|
|
66afc69a57 | ||
|
|
3046691119 | ||
|
|
88b87a53ff | ||
|
|
f1ae26efb6 | ||
|
|
ba83685a86 | ||
|
|
d514733f13 | ||
|
|
15cd608b19 | ||
|
|
08d7f5d778 | ||
|
|
a2ba7e9229 | ||
|
|
7c71a15699 | ||
|
|
351b804606 | ||
|
|
8f0501b7fe | ||
|
|
73d21e66dd | ||
|
|
6310311d52 | ||
|
|
d0caba1933 | ||
|
|
2f3291e48f | ||
|
|
43d8e3b894 | ||
|
|
5d4eb5d4e0 | ||
|
|
7ccdec22cb | ||
|
|
2ea64045cb | ||
|
|
daf97d298d | ||
|
|
ababc7ecdb | ||
|
|
33e72b2ae1 | ||
|
|
bf5716c674 | ||
|
|
c17a3b94f5 | ||
|
|
19a9c313e0 | ||
|
|
68043e65e4 | ||
|
|
a6ec60284d | ||
|
|
ff79c4b95b | ||
|
|
9c4e41a4c6 | ||
|
|
07ef3d59c5 | ||
|
|
4fe7162652 | ||
|
|
950a937633 | ||
|
|
1cc31def3e | ||
|
|
92f53c532b | ||
|
|
c6d6d9c002 | ||
|
|
85b859231c | ||
|
|
ea093dc0fc | ||
|
|
cd61178f44 | ||
|
|
eb53219cbd | ||
|
|
18d70ffbe9 | ||
|
|
bdcf823d26 | ||
|
|
3aeba2aa09 | ||
|
|
0793e2c8d8 | ||
|
|
0f01279c91 | ||
|
|
8fa9d00958 | ||
|
|
ab6ef8a19c | ||
|
|
8d68807bfe | ||
|
|
35fc38c328 | ||
|
|
85eeda5755 | ||
|
|
2e52c500a1 | ||
|
|
5886f9bea8 | ||
|
|
c497dcba26 | ||
|
|
493c45a864 | ||
|
|
b243b26a3d | ||
|
|
1d0749970a | ||
|
|
3474d3e250 | ||
|
|
a35c3a424c | ||
|
|
6e65ba87a6 | ||
|
|
ae7247f14f | ||
|
|
12a5d6b1f4 | ||
|
|
19da066b04 | ||
|
|
8115a7c66c | ||
|
|
1ab029c60a | ||
|
|
d0dbacfc4b | ||
|
|
f6b7e0aad9 | ||
|
|
4a23f88180 | ||
|
|
f8dbd67a16 | ||
|
|
9406f8b332 | ||
|
|
b0410ed9d4 | ||
|
|
8c0d0c4dea | ||
|
|
9446c26419 | ||
|
|
cdfa6008c7 | ||
|
|
b4bb8bda26 | ||
|
|
2c32504cc9 | ||
|
|
bd188ff410 | ||
|
|
89aedb1285 | ||
|
|
db8c0820b6 | ||
|
|
d86464c822 | ||
|
|
fcfeb0ce88 | ||
|
|
01e472912e | ||
|
|
bbfeac408e | ||
|
|
364de1fc6c | ||
|
|
52af06cd33 | ||
|
|
8f472c5987 | ||
|
|
dcbd7a6703 | ||
|
|
e6fd4c2edc | ||
|
|
e19ca19a82 | ||
|
|
7b1b68e1c4 | ||
|
|
56d848c868 | ||
|
|
100eece7a2 | ||
|
|
278ecc1e48 | ||
|
|
a3d379554b | ||
|
|
983dd98a66 | ||
|
|
ca3f26b8d2 | ||
|
|
d2a2352e9a | ||
|
|
3043a9525a | ||
|
|
e1c6632b6f | ||
|
|
56e64e322e | ||
|
|
cbd056f225 | ||
|
|
22ab66f9d8 | ||
|
|
3597733dae | ||
|
|
cb9ce69ba3 | ||
|
|
c19a79cbca | ||
|
|
e97e090b65 | ||
|
|
eda4a6d18b | ||
|
|
94f66b60d8 | ||
|
|
9a85e27c0c | ||
|
|
7fb7e3d1bc | ||
|
|
90066fdbec | ||
|
|
475f0e7b51 | ||
|
|
a9131724d6 | ||
|
|
55bfb6d9dc | ||
|
|
af3da3abf8 | ||
|
|
339d9f2d03 | ||
|
|
a24fb8b380 | ||
|
|
65319e3927 | ||
|
|
19917972ef | ||
|
|
c1b412814a | ||
|
|
53ea8407ea | ||
|
|
66f46e8cc7 | ||
|
|
fec69a21be | ||
|
|
505ebb8ae1 | ||
|
|
fb4381d8eb | ||
|
|
4772f5b571 | ||
|
|
481db425d6 | ||
|
|
b886729bb8 | ||
|
|
3a21a9c9f1 | ||
|
|
9e4a6fec59 | ||
|
|
86921022dc | ||
|
|
f57f11e6ff | ||
|
|
77ad6bd97e | ||
|
|
78c7041b3f | ||
|
|
99edead0f2 | ||
|
|
b0b3dbc0fc | ||
|
|
8b5af54e1c | ||
|
|
0b5b04a22f | ||
|
|
890be1de0d | ||
|
|
40ae747bc1 | ||
|
|
5a8022e9a2 | ||
|
|
3e512b5cf5 | ||
|
|
81071d7776 | ||
|
|
fc05140c1f | ||
|
|
faec6824ba | ||
|
|
b91bfef16d | ||
|
|
ba9dc17e44 | ||
|
|
c220bcc57e | ||
|
|
f8a4808aa7 | ||
|
|
495d0a47db | ||
|
|
8cda627fe6 | ||
|
|
d0a0ccc6bc | ||
|
|
999222cd97 | ||
|
|
72eb7fda3f | ||
|
|
3c94940ae6 | ||
|
|
1a8ed2aec1 | ||
|
|
0e2321dc14 | ||
|
|
78d1983f9a | ||
|
|
5435df110c | ||
|
|
32853b8d1e | ||
|
|
9737b4c6ab | ||
|
|
e9bdd5c355 | ||
|
|
9728567296 | ||
|
|
ef6579a7ee | ||
|
|
8e810aa765 | ||
|
|
37596edf2b | ||
|
|
229a3e430e | ||
|
|
1d80f595c5 | ||
|
|
189a2c8e0e | ||
|
|
97096fb811 | ||
|
|
e8b75e40b1 | ||
|
|
d41c38e002 | ||
|
|
966bc7b433 | ||
|
|
e7b06d3362 | ||
|
|
d5d8eb8d7c | ||
|
|
8ec07f0224 | ||
|
|
558536db1e | ||
|
|
0c2fe054d1 | ||
|
|
b5a69fd787 | ||
|
|
9b29ed347d | ||
|
|
c5c4ff4d51 | ||
|
|
008b1a9f8d | ||
|
|
4a6f153aa6 | ||
|
|
9eccc78e3a | ||
|
|
09938cc368 | ||
|
|
5db05e1031 | ||
|
|
f6ba72b4fa | ||
|
|
bf7e555cfa | ||
|
|
26abc70a99 | ||
|
|
d38cd54dee | ||
|
|
200690ad6c | ||
|
|
52b69a6d68 | ||
|
|
f319b2af05 | ||
|
|
b80a005733 | ||
|
|
34936aecc0 | ||
|
|
b021f26f03 | ||
|
|
fcf7197120 | ||
|
|
bec8d8dff1 | ||
|
|
781c63e966 | ||
|
|
2da1883726 | ||
|
|
83ffac7cd2 | ||
|
|
6198903cdf | ||
|
|
bd98f8188c | ||
|
|
73ea402b1c | ||
|
|
4284684a3b | ||
|
|
b5d522410a | ||
|
|
284cb8e2a7 | ||
|
|
079aab2315 | ||
|
|
645ee382cf | ||
|
|
e947a772ce | ||
|
|
5d63adf7df | ||
|
|
f1a872f861 | ||
|
|
02b1d02f09 | ||
|
|
a3479b3503 | ||
|
|
740535a8f2 | ||
|
|
19ed684a52 | ||
|
|
bd72949fa7 | ||
|
|
a277cd5b0c | ||
|
|
fd6e7e94df | ||
|
|
2f6403478d | ||
|
|
a4372ffc61 | ||
|
|
d6ce92811e | ||
|
|
e5aecdf315 | ||
|
|
6d1c457a75 | ||
|
|
6e16aec6d3 | ||
|
|
f899d7bb04 | ||
|
|
e36646ce7f | ||
|
|
f3d36a74c9 | ||
|
|
4e11c9c36e | ||
|
|
0a7ac36584 | ||
|
|
fc4850f354 | ||
|
|
6e9a8d2074 | ||
|
|
c712d7da07 | ||
|
|
5183181d1c | ||
|
|
b024f89ba8 | ||
|
|
fbbe516b9a | ||
|
|
d48a3fd948 | ||
|
|
86f0c53bd3 | ||
|
|
7f6cc2048b | ||
|
|
b2829f6384 | ||
|
|
67c5041860 | ||
|
|
33df9e3132 | ||
|
|
602bc28a45 | ||
|
|
5a7a494701 | ||
|
|
9fa82cedbd | ||
|
|
b0408284b8 | ||
|
|
388a1c4393 | ||
|
|
5c9aaeae43 | ||
|
|
0eaec78399 | ||
|
|
9349ca3b34 | ||
|
|
52f2dd5c32 | ||
|
|
6eeed21872 | ||
|
|
929a7e5840 | ||
|
|
758b3a88d0 | ||
|
|
4477bd6c80 | ||
|
|
370d2cc121 | ||
|
|
e6c4d6e737 | ||
|
|
5bd2936b90 | ||
|
|
41b6bb7000 | ||
|
|
fd8818c400 | ||
|
|
5929de4249 | ||
|
|
07c6cbccc0 | ||
|
|
f20843fcb1 | ||
|
|
eb6a7a45a5 | ||
|
|
75638db676 | ||
|
|
76fcfa53f4 | ||
|
|
708ec9fbe7 | ||
|
|
ca98750604 | ||
|
|
eb49a47b0a | ||
|
|
ac2fc85d18 | ||
|
|
4ee2b4453d | ||
|
|
0542b6a24a | ||
|
|
ca0053b6cd | ||
|
|
a902b98cfd | ||
|
|
99ff0ffd3d | ||
|
|
5bb1830c9b | ||
|
|
6055ecda24 | ||
|
|
8229a3b420 | ||
|
|
20723fd2b4 | ||
|
|
31d9363954 | ||
|
|
515facf39f | ||
|
|
173ce2aae6 | ||
|
|
935c4f2f82 | ||
|
|
b893b6485b | ||
|
|
6f5b3bd213 | ||
|
|
14fd1c9b40 | ||
|
|
c695ca98e5 | ||
|
|
7649eb7aed | ||
|
|
0119622d18 | ||
|
|
15c6781083 | ||
|
|
574387e09f | ||
|
|
aa1f29dc53 | ||
|
|
bb9a26d4fc | ||
|
|
4b036d3beb | ||
|
|
6dd8dc325e | ||
|
|
22005c7465 | ||
|
|
0686b5ff32 | ||
|
|
f495ecda3a | ||
|
|
1f4bc91d87 | ||
|
|
e54bf254cb | ||
|
|
fc2d3adc1f | ||
|
|
18c99616b3 | ||
|
|
70e3ab7e89 | ||
|
|
9b25a2d245 | ||
|
|
966aa8245d | ||
|
|
d220587018 | ||
|
|
c665631191 | ||
|
|
6032a99a90 | ||
|
|
d130251b41 | ||
|
|
4dd8d3160b | ||
|
|
667fe8cf50 | ||
|
|
554c32c6f1 | ||
|
|
bdc0e8e16f | ||
|
|
3b0527add8 | ||
|
|
2b494357e5 | ||
|
|
7a0624b8db | ||
|
|
bb8a2c94cc | ||
|
|
f3532ebef2 | ||
|
|
c5fad1b933 | ||
|
|
5e9f392ba8 | ||
|
|
f1ed5c1e97 | ||
|
|
5946710fe8 | ||
|
|
5cf0056e69 | ||
|
|
ac12d6a6e2 | ||
|
|
cc0c15e37c | ||
|
|
9a630dcb01 | ||
|
|
d30b112d71 | ||
|
|
5fded4256d | ||
|
|
d2fdfa7528 | ||
|
|
55c3acab9a | ||
|
|
dc903f8059 | ||
|
|
156c8e1e97 | ||
|
|
78ba85e74d | ||
|
|
6d41089d48 | ||
|
|
64b23d484d | ||
|
|
5f65e8c30d | ||
|
|
49d560fa24 | ||
|
|
36c469660e | ||
|
|
416785941b | ||
|
|
799bd2dfaa | ||
|
|
c0ccbc9274 | ||
|
|
0918cdbfa0 | ||
|
|
077f60e7c4 | ||
|
|
96900e77f6 | ||
|
|
f43343bd2c | ||
|
|
8e69940ae6 | ||
|
|
0d825bbc39 | ||
|
|
35123f005a | ||
|
|
b25730fbd5 | ||
|
|
585be4ce4a | ||
|
|
3b5d4b6925 | ||
|
|
39471e9bae | ||
|
|
50039e5a6b | ||
|
|
cb31f9e554 | ||
|
|
f21fb0f46d | ||
|
|
13c6801c45 | ||
|
|
1a1b0ffdc6 | ||
|
|
a638e2b27a | ||
|
|
3ce64da78f | ||
|
|
5537514b4f | ||
|
|
42363101f8 | ||
|
|
bdafc4a2f7 | ||
|
|
bdb1216119 | ||
|
|
e509c28a4f | ||
|
|
4f5954938a | ||
|
|
267a166895 | ||
|
|
e655e3c550 | ||
|
|
6ea00f44dd | ||
|
|
843c258dd3 | ||
|
|
c6c94b1805 | ||
|
|
315d75e40b | ||
|
|
f8bfe0c613 | ||
|
|
50b9743bb6 | ||
|
|
9c9abf19a3 | ||
|
|
51f9f6fe6f | ||
|
|
0c2c4cab69 | ||
|
|
ceb35cd036 | ||
|
|
f9d0719ec4 | ||
|
|
40f36e5ee9 | ||
|
|
e9903d5391 | ||
|
|
a5bc38e61c | ||
|
|
6df7322493 | ||
|
|
0495057458 | ||
|
|
8a2ee7cbce | ||
|
|
a465e2cda8 | ||
|
|
81c22f81ca | ||
|
|
4e4457f3ce | ||
|
|
e993bc4f2a | ||
|
|
c4fe49b0af | ||
|
|
5f211c8d0a | ||
|
|
3e41381a52 | ||
|
|
93488846e2 | ||
|
|
9609d44638 | ||
|
|
59403ec607 | ||
|
|
5f0f403b50 | ||
|
|
fdae191116 | ||
|
|
15424d2d03 | ||
|
|
9d2d7133a1 | ||
|
|
f50013899a | ||
|
|
beb2d08260 | ||
|
|
b39d491df3 | ||
|
|
7560d4ba80 | ||
|
|
e90244b167 | ||
|
|
a72aef7a86 | ||
|
|
39e97c3b96 | ||
|
|
e7d7a7ccab | ||
|
|
8b173efe96 | ||
|
|
f53a7f3b85 | ||
|
|
62f5d7ebe1 | ||
|
|
fd6fceb884 | ||
|
|
74a5f459f5 | ||
|
|
7b38af81cf | ||
|
|
401df2c177 | ||
|
|
ffd9691cd0 | ||
|
|
e7ae32f618 | ||
|
|
97fadb0d9f | ||
|
|
86f072bf4b | ||
|
|
981984b562 | ||
|
|
1e9053d879 | ||
|
|
cb1ce73c92 | ||
|
|
93054578c9 | ||
|
|
d112800b98 | ||
|
|
c8bf342d8b | ||
|
|
63ceb1a260 | ||
|
|
ca519b69ce | ||
|
|
2f16d8448d | ||
|
|
74b334f7ad | ||
|
|
d5a231f51b | ||
|
|
9b24e216fa | ||
|
|
a944870eb2 | ||
|
|
bc6fd4aa32 | ||
|
|
4a00d5aca5 | ||
|
|
c55cb526f7 | ||
|
|
70a728f15b | ||
|
|
e7c9c844dc | ||
|
|
cf8e6980be | ||
|
|
7cd537d58d | ||
|
|
7ad11f73cd | ||
|
|
82ac943e3e | ||
|
|
420bb9a74c | ||
|
|
4c32727b37 | ||
|
|
339f618685 | ||
|
|
2a8337e67c | ||
|
|
bd50714759 | ||
|
|
db9ef09d1d | ||
|
|
3bb4e0ca6f | ||
|
|
b4886295ac | ||
|
|
ef455dcf06 | ||
|
|
5afa4f6e2b | ||
|
|
50678d73bd | ||
|
|
6d7066e4db | ||
|
|
52eb11b385 | ||
|
|
b176c15405 | ||
|
|
021fdbcf1b | ||
|
|
d7d9988cd8 | ||
|
|
e8baee1774 | ||
|
|
79179dad71 | ||
|
|
c8de8a1182 | ||
|
|
a2cfcef0aa | ||
|
|
28d220a42b | ||
|
|
26a8b20459 | ||
|
|
84e0ddf241 | ||
|
|
6e3a6ba287 | ||
|
|
d6e7b09ff7 | ||
|
|
daca296df4 | ||
|
|
dbab5a3505 | ||
|
|
8aa4045651 | ||
|
|
eb9561edab | ||
|
|
332182a67f | ||
|
|
d7a2cde57e | ||
|
|
bb04645a93 | ||
|
|
d25493ae79 | ||
|
|
8522628a11 | ||
|
|
875ecaeb06 | ||
|
|
25c83b2914 | ||
|
|
8a516904b8 | ||
|
|
df4c71496b | ||
|
|
026bef6f60 | ||
|
|
2b168e183b | ||
|
|
c86ea5e9dc | ||
|
|
966577fc02 | ||
|
|
d0d3af5f12 | ||
|
|
c62617532f | ||
|
|
fc28374f88 | ||
|
|
6ec9d8e9d0 | ||
|
|
26d41d4a2b | ||
|
|
b6c2befba7 | ||
|
|
0d96a7e9e5 | ||
|
|
3006161bce | ||
|
|
c653a1cc72 | ||
|
|
301f048ce3 | ||
|
|
3ac6666bee | ||
|
|
73a5be5d6c | ||
|
|
ed6328679a | ||
|
|
8eb9c4822e | ||
|
|
8a9e2305c8 | ||
|
|
7ef2a2ec93 | ||
|
|
67d49fe483 | ||
|
|
cc2753efd5 | ||
|
|
d0a403e56a | ||
|
|
ab9d1d0a91 | ||
|
|
c85ad74508 | ||
|
|
2dca9308e9 | ||
|
|
494a267527 | ||
|
|
4c163d54ca | ||
|
|
b9853b362b | ||
|
|
121e978d76 | ||
|
|
b9142217a9 | ||
|
|
74d67dd801 | ||
|
|
121ed4a58e | ||
|
|
cf903ca82e | ||
|
|
2f61795697 | ||
|
|
d5257fe1db | ||
|
|
822fbee0c4 | ||
|
|
937f9cdfda | ||
|
|
2bb9355933 | ||
|
|
57a9021107 | ||
|
|
71fecfb1f2 | ||
|
|
1b374817f0 | ||
|
|
eee927a6cd | ||
|
|
0fabfa4ef9 | ||
|
|
57bf54c28d | ||
|
|
9bbc9100ab | ||
|
|
e6cd78d71b | ||
|
|
3d66b90cf8 | ||
|
|
ebfb02bd12 | ||
|
|
2032ff1276 | ||
|
|
08582aad83 | ||
|
|
c9944820c6 | ||
|
|
0697609dd0 | ||
|
|
39d3689c22 | ||
|
|
43023293ea | ||
|
|
91f319bc5f | ||
|
|
f847488643 | ||
|
|
731e227cb6 | ||
|
|
f2aafac40c | ||
|
|
5bff4cb07f | ||
|
|
06ef47cc40 | ||
|
|
3e0e4ecb5d | ||
|
|
651f3c9887 | ||
|
|
cfbe24fc24 | ||
|
|
9432cfda90 | ||
|
|
981adaae24 | ||
|
|
ec3da81887 | ||
|
|
d150a7911c | ||
|
|
018738bcc0 | ||
|
|
e37e20faf5 | ||
|
|
4bf13394f1 | ||
|
|
3dad0cc849 | ||
|
|
ea69d1e904 | ||
|
|
b666cde7a7 | ||
|
|
e3784bba9d |
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/types/ @balazsorban44 @lluia
|
||||
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://docs.github.com/en/github/administering-a-repository/displaying-a-sponsor-button-in-your-repository
|
||||
|
||||
github: [balazsorban44]
|
||||
43
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
43
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report a defect with NextAuth.js
|
||||
labels: bug
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
## Description 🐜
|
||||
|
||||
Please provide a clear and concise description of the bug in NextAuth.js.
|
||||
|
||||
🚧 – _Do not report bugs with your own project here; ask for help [by raising a question instead](https://github.com/nextauthjs/next-auth/issues/new?assignees=&labels=question&template=question.md) - this helps us a lot with administration overhead._
|
||||
|
||||
## How to reproduce ☕️
|
||||
|
||||
We encourage you to use one of the templates set up on **CodeSandbox** to reproduce your issue:
|
||||
|
||||
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
|
||||
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
|
||||
|
||||
🚧 – _If you don't provide any way to reproduce the bug, the issue is at risk of being closed._
|
||||
|
||||
## Screenshots / Logs 📽
|
||||
|
||||
**Help us help you**. We can address the bug you found much faster if you provide contextual screenshots or screen recordings showcasing the issue.
|
||||
|
||||
See [Kap](https://getkap.co/) for a good, easy-to-use, cross-platform screen recording tool.
|
||||
|
||||
## Environment 🖥
|
||||
|
||||
Please run this command:
|
||||
|
||||
```
|
||||
$ npx envinfo --system --binaries --browsers --npmPackages "{next-auth}"
|
||||
```
|
||||
|
||||
and paste the output here.
|
||||
|
||||
## Contributing 🙌🏽
|
||||
|
||||
It takes a lot of work 🏋🏻♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
|
||||
|
||||
In case you're willing to help fix this bug, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
39
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
39
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for NextAuth.js
|
||||
labels: enhancement
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
## Summary 💭
|
||||
|
||||
A clear and concise summary of the feature being proposed.
|
||||
|
||||
## Description 📓
|
||||
|
||||
Please provide a more in-depth description of the feature proposed.
|
||||
|
||||
Make sure you provide plenty of [links]() to external documentation and inline code examples like so:
|
||||
|
||||
```js
|
||||
function myAwesomeNextAuthFeature() {
|
||||
return 💚
|
||||
}
|
||||
```
|
||||
|
||||
Take time thinking about what you want to say and help us understand your proposal making sure that this description contains:
|
||||
|
||||
- **purpose of the feature**
|
||||
- **potential problems**
|
||||
- **potential alternatives**
|
||||
|
||||
You can use one of the templates set up on **CodeSandbox** to better illustrate your idea:
|
||||
|
||||
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
|
||||
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
|
||||
|
||||
## Contributing 🙌🏽
|
||||
|
||||
It takes a lot of work 🏋🏻♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
|
||||
|
||||
In case you're willing to help implement this feature, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.
|
||||
32
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
32
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: Question
|
||||
about: Ask a question about NextAuth.js or for help using it
|
||||
labels: question
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
## Question 💬
|
||||
|
||||
Please provide an in-depth description of the question you have.
|
||||
|
||||
Make sure you [link]() to external documentation if necessary and provide inline code examples like so:
|
||||
|
||||
```js
|
||||
function myAwesomeNextAuthFeature() {
|
||||
return 💚
|
||||
}
|
||||
```
|
||||
|
||||
**NOTE:** Questions will be converted to Discussions. You can find them [here](https://github.com/nextauthjs/next-auth/discussions)!
|
||||
|
||||
## How to reproduce ☕️
|
||||
|
||||
We encourage you to use the template set-up on **CodeSandbox** as a playground to represent your question or doubt:
|
||||
|
||||
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
|
||||
|
||||
## Contributing 🙌🏽
|
||||
|
||||
It takes a lot of work 🏋🏻♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
|
||||
|
||||
In case you're willing to help answer this question, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.
|
||||
36
.github/ISSUE_TEMPLATE/typescript.md
vendored
Normal file
36
.github/ISSUE_TEMPLATE/typescript.md
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: TypeScript
|
||||
about: Ask a question about NextAuth.js TypeScript integration
|
||||
labels:
|
||||
- question
|
||||
- TypeScript
|
||||
assignees:
|
||||
- lluia
|
||||
- balazsorban44
|
||||
---
|
||||
|
||||
## Question 💬
|
||||
|
||||
Please provide an in-depth description of the question you have when using NextAuth.js on a Typescript project or when consuming the built-in types for `next-auth`.
|
||||
|
||||
Make sure you [link]() to external documentation if necessary and provide inline code examples like so:
|
||||
|
||||
```js
|
||||
function myAwesomeNextAuthFeature() {
|
||||
return 💚
|
||||
}
|
||||
```
|
||||
|
||||
**NOTE:** Questions will be converted to Discussions. You can find them [here](https://github.com/nextauthjs/next-auth/discussions)!
|
||||
|
||||
## How to reproduce ☕️
|
||||
|
||||
We encourage you to use the template set-up on **CodeSandbox** as a playground to represent your question or doubt:
|
||||
|
||||
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
|
||||
|
||||
## Contributing 🙌🏽
|
||||
|
||||
It takes a lot of work 🏋🏻♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
|
||||
|
||||
In case you're willing to help answer this TypeScript question, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.
|
||||
44
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
44
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
<!--
|
||||
Thanks for your interest in the project. Bugs filed and PRs submitted are appreciated!
|
||||
|
||||
Please make sure that you are familiar with and follow the Code of Conduct for
|
||||
this project (found in the CODE_OF_CONDUCT.md file).
|
||||
|
||||
Also, please make sure you're familiar with and follow the instructions in the
|
||||
contributing guidelines (found in the CONTRIBUTING.md file).
|
||||
|
||||
If you're new to contributing to open source projects, you might find this free
|
||||
video course helpful: https://kcd.im/pull-request
|
||||
|
||||
Please fill out the information below to expedite the review and (hopefully)
|
||||
merge of your pull request!
|
||||
-->
|
||||
|
||||
<!-- What changes are being made? (What feature/bug is being fixed here?) -->
|
||||
|
||||
## Reasoning 💡
|
||||
|
||||
What changes are being made? What feature/bug is being fixed here?
|
||||
|
||||
## Checklist 🧢
|
||||
|
||||
Feel free cross items ( like this `~[] item~` ) if they're irrelevant to your changes.
|
||||
|
||||
To check an item, place an `x` in the box like so: `- [x] Documentation`.
|
||||
|
||||
- [ ] Documentation
|
||||
- [ ] Tests
|
||||
- [ ] Ready to be merged
|
||||
<!-- In your opinion, is this ready to be merged as soon as it's reviewed? -->
|
||||
|
||||
## Affected issues 🎟
|
||||
|
||||
Please [scout and link issues](https://github.com/nextauthjs/next-auth/issues) that might be solved by this PR.
|
||||
|
||||
If you write `"Fixes"` or `"Closes"` before the issue link like so:
|
||||
|
||||
```
|
||||
Fixes #359
|
||||
```
|
||||
|
||||
the connected issue will be automatically closed once the PR is merged and hence help with maintenance of the library 😊
|
||||
39
.github/labeler.yml
vendored
Normal file
39
.github/labeler.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
test:
|
||||
- test/**/*
|
||||
- types/tests/**/*
|
||||
|
||||
documentation:
|
||||
- www/**/*
|
||||
- ./**/*.md
|
||||
|
||||
providers:
|
||||
- src/providers/**/*
|
||||
- www/docs/configuration/providers.md
|
||||
- test/integration/**/*
|
||||
|
||||
adapters:
|
||||
- src/adapters/**/*
|
||||
- www/docs/schemas/adapters.md
|
||||
|
||||
databases:
|
||||
- www/docs/schemas/*.md
|
||||
- test/docker/databases/**/*
|
||||
- www/docs/configuration/databases.md
|
||||
- test/fixtures/**/*
|
||||
|
||||
core:
|
||||
- src/**/*
|
||||
|
||||
style:
|
||||
- src/css/**/*
|
||||
|
||||
client:
|
||||
- src/client/**/*
|
||||
- www/docs/getting-started/client.md
|
||||
|
||||
pages:
|
||||
- src/server/pages/**/*
|
||||
- www/docs/configuration/pages.md
|
||||
|
||||
TypeScript:
|
||||
- types/**/*
|
||||
25
.github/stale.yml
vendored
Normal file
25
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 60
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
- priority
|
||||
- bug
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
Hi there! It looks like this issue hasn't had any activity for a while.
|
||||
It will be closed if no further activity occurs. If you think your issue
|
||||
is still relevant, feel free to comment on it to keep it open. (Read more at #912)
|
||||
Thanks!
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: >
|
||||
Hi there! It looks like this issue hasn't had any activity for a while.
|
||||
To keep things tidy, I am going to close this issue for now.
|
||||
If you think your issue is still relevant, just leave a comment
|
||||
and I will reopen it. (Read more at #912)
|
||||
Thanks!
|
||||
32
.github/workflows/build.yml
vendored
Normal file
32
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# Simple check that the build is valid and no linting errors.
|
||||
# Currently is run as a seperate workflow as it's fast to fail.
|
||||
name: Lint/Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
|
||||
jobs:
|
||||
lint-and-build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12, 14, 16]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- run: npm run lint
|
||||
- run: npm run build
|
||||
67
.github/workflows/codeql-analysis.yml
vendored
Normal file
67
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, beta, next ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
schedule:
|
||||
- cron: '43 17 * * 2'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
57
.github/workflows/integration.yml
vendored
Normal file
57
.github/workflows/integration.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
name: Integration Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
# Only run tests integration against Pull Requests from branches in
|
||||
# this repository. We do this as integration tests require access to
|
||||
# secrets in GitHub and they are not exposed to tests run against
|
||||
# forks (for security reasons), so integration test against
|
||||
# Pull Requests from external repos just fail and generate noise.
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
|
||||
# We use self-hosted runners as cloud based runnners (e.g. AWS, GPC)
|
||||
# fail due to IP Address checks done by providers, which enforce
|
||||
# CAPTCHA checks on login request from cloud compute IP addresses to
|
||||
# prevent abuse.
|
||||
runs-on: self-hosted
|
||||
|
||||
# Target time is under 5 minutes to run all tests. If it takes longer than
|
||||
# 10 minutes should look at running tests in parallel. No individual flow
|
||||
# should take longer than 5 minutes to build and run.
|
||||
timeout-minutes: 10
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12, 14, 16]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
|
||||
# Run tests (build library, build + start test app in Docker, run tests)
|
||||
- run: npm test
|
||||
# TODO Tests should exit out if env vars not set (currently hangs)
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
NEXTAUTH_TWITTER_ID: ${{secrets.NEXTAUTH_TWITTER_ID}}
|
||||
NEXTAUTH_TWITTER_SECRET: ${{secrets.NEXTAUTH_TWITTER_SECRET}}
|
||||
NEXTAUTH_TWITTER_USERNAME: ${{secrets.NEXTAUTH_TWITTER_USERNAME}}
|
||||
NEXTAUTH_TWITTER_PASSWORD: ${{secrets.NEXTAUTH_TWITTER_PASSWORD}}
|
||||
NEXTAUTH_GITHUB_ID: ${{secrets.NEXTAUTH_GITHUB_ID}}
|
||||
NEXTAUTH_GITHUB_SECRET: ${{secrets.NEXTAUTH_GITHUB_SECRET}}
|
||||
NEXTAUTH_GITHUB_USERNAME: ${{secrets.NEXTAUTH_GITHUB_USERNAME}}
|
||||
NEXTAUTH_GITHUB_PASSWORD: ${{secrets.NEXTAUTH_GITHUB_PASSWORD}}
|
||||
11
.github/workflows/labeler.yml
vendored
Normal file
11
.github/workflows/labeler.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
name: "Pull Request Labeler"
|
||||
on:
|
||||
- pull_request_target
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@main
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
26
.github/workflows/release.yml
vendored
Normal file
26
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "beta"
|
||||
- "next"
|
||||
- "3.x"
|
||||
pull_request:
|
||||
jobs:
|
||||
release:
|
||||
name: "Release"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- run: npx semantic-release@17
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
NPM_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
27
.github/workflows/types.yml
vendored
Normal file
27
.github/workflows/types.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Types
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
|
||||
jobs:
|
||||
lint-and-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- name: Check types
|
||||
run: npm run test:types
|
||||
62
.gitignore
vendored
62
.gitignore
vendored
@@ -1,3 +1,61 @@
|
||||
.next
|
||||
# Misc
|
||||
.DS_Store
|
||||
|
||||
.env
|
||||
node_modules
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
yarn.lock
|
||||
|
||||
# Dependencies
|
||||
node_modules
|
||||
|
||||
# Build dirs
|
||||
.next
|
||||
/build
|
||||
/dist
|
||||
/www/build
|
||||
|
||||
# Generated files
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
www/providers.json
|
||||
src/providers/index.js
|
||||
/internals
|
||||
/adapters.d.ts
|
||||
/adapters.js
|
||||
/client.d.ts
|
||||
/client.js
|
||||
/index.d.ts
|
||||
/index.js
|
||||
/jwt.d.ts
|
||||
/jwt.js
|
||||
/providers.d.ts
|
||||
/providers.js
|
||||
/errors.js
|
||||
/errors.d.ts
|
||||
|
||||
# Development app
|
||||
app/next-auth
|
||||
app/dist/css
|
||||
app/package-lock.json
|
||||
app/yarn.lock
|
||||
|
||||
# VS
|
||||
/.vs/slnx.sqlite-journal
|
||||
/.vs/slnx.sqlite
|
||||
/.vs
|
||||
.vscode
|
||||
|
||||
# GitHub Actions runner
|
||||
/actions-runner
|
||||
/_work
|
||||
|
||||
# Prisma migrations
|
||||
/prisma/migrations
|
||||
@@ -1,85 +0,0 @@
|
||||
# Authentication
|
||||
|
||||
To use oAuth you need to configure environment variables with ID and secret and configure the service accordingly.
|
||||
|
||||
This project comes pre-configured to handle Facebook, Google and Twitter oAuth if you provide the ID and sercret for the service via environment variables.
|
||||
|
||||
You can pass them on the command line or put them in `.env` file which will be loaded at startup (see [.env.example](https://github.com/iaincollins/next-auth/blob/master/example/.env.example) for an example).
|
||||
|
||||
If you want to add new oAuth providers (such as GitHub), you will need to:
|
||||
|
||||
* Add the oauth provider configuration in next-auth.providers.js
|
||||
* Add a field to your User model (in 'index.js') with the name of the provider
|
||||
* Configure the service to point to your website (as in the examples below)
|
||||
* Specify the environment variables at run time
|
||||
|
||||
## Configuring your account
|
||||
|
||||
These guides are approximate as exactly how to configure oAuth varies for each provider and tends to change when they update their developer portals, which can be quite often. If you can't see the options mentioned, try exploring the UI in the developer portal or configuration pages.
|
||||
|
||||
Due to the volume of requests I'm not usually able to help with specific problems but pull requests with improved or extended instructions are very welcome.
|
||||
|
||||
Tip: Twitter's oAuth implementation is the most permissive and easiest to configure, you may want to start with it. If you run into problems, you might
|
||||
want to check email sign-in is working as baseline.
|
||||
|
||||
Please note that Facebook oAuth cannot be used to sign in to 'localhost' and that if you want to sign in to `localhost` with Google+ you must specifically add something like `http://localhost:3000/auth/oauth/google/callback` as a authorized redirect URI for your application.
|
||||
|
||||
### Facebook Login
|
||||
|
||||
Environment variables:
|
||||
|
||||
* FACEBOOK_ID
|
||||
* FACEBOOK_SECRET
|
||||
|
||||
Configuration steps:
|
||||
|
||||
1. Go to [Facebook Developers](https://developers.facebook.com/)
|
||||
2. Click **Apps > Create a New App** in the navigation bar
|
||||
3. Enter *Display Name*, then choose a category, then click **Create app**
|
||||
5. Specify *App ID* as the **FACEBOOK_ID** Config Variable
|
||||
6. Specify *App Secret* as the **FACEBOOK_SECRET** Config Variable
|
||||
7. Click on *Settings* on the sidebar, then click **+ Add Platform**
|
||||
8. Select **Website**
|
||||
9. Enable 'Client OAuth Login' and 'Web OAuth Login'
|
||||
10. list`http://your-server.example.com/auth/oauth/facebook/callback` under 'Valid OAuth redirect URIs'
|
||||
11. List your sites domain under 'domains'
|
||||
|
||||
### Google+
|
||||
|
||||
Environment variables:
|
||||
|
||||
* GOOGLE_ID
|
||||
* GOOGLE_SECRET
|
||||
|
||||
Configuration steps:
|
||||
|
||||
1. Visit [Google Cloud Console](https://cloud.google.com/console/project)
|
||||
2. Click the **CREATE PROJECT** button, enter a *Project Name* and click **CREATE**
|
||||
3. Then select *APIs* then *Credentials*
|
||||
4. Select **Create new oAuth Client ID** and enter the following:
|
||||
- **Application Type**: Web Application
|
||||
- **Authorized Javascript origins**: `http://your-server.example.com/`
|
||||
- **Authorized redirect URI**: `http://your-server.example.com/auth/oauth/google/callback`
|
||||
5. Specify *Client ID* as the **GOOGLE_ID** Config Variable
|
||||
6. Specify *Client Secret* as the **GOOGLE_SECRET** Config Variable
|
||||
7. Enable Google+ on the project - if you don't, sign in with Google+ will fail!
|
||||
|
||||
### Twitter
|
||||
|
||||
Environment variables:
|
||||
|
||||
* TWITTER_KEY
|
||||
* TWITTER_SECRET
|
||||
|
||||
Configuration steps:
|
||||
|
||||
1. Sign in at [https://apps.twitter.com](https://apps.twitter.com/)
|
||||
2. Click **Create a new application**
|
||||
3. Enter your application name, website and description
|
||||
4. For **Callback URL**: `http://your-server.example.com/auth/oauth/twitter/callback`
|
||||
5. Go to **Settings** tab
|
||||
6. Under *Application Type* select **Read and Write** access
|
||||
7. Check the box **Allow this application to be used to Sign in with Twitter**
|
||||
8. Click **Update this Twitter's applications settings**
|
||||
9. Specify *Consumer Key* as the **TWITTER_KEY** Config Variable
|
||||
10. Specify *Consumer Secret* as the **TWITTER_SECRET** Config Variable
|
||||
5
CHANGELOG.md
Normal file
5
CHANGELOG.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# CHANGELOG
|
||||
|
||||
The changelog is automatically updated using
|
||||
[semantic-release](https://github.com/semantic-release/semantic-release). You
|
||||
can see it on the [releases page](../../releases).
|
||||
76
CODE_OF_CONDUCT.md
Normal file
76
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting me@iaincollins.com. All complaints will be reviewed and
|
||||
investigated and will result in a response that is deemed necessary and
|
||||
appropriate to the circumstances. The project team is obligated to maintain
|
||||
confidentiality with regard to the reporter of an incident. Further details of
|
||||
specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
130
CONTRIBUTING.md
Normal file
130
CONTRIBUTING.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# Contributing guide
|
||||
|
||||
Contributions and feedback on your experience of using this software are welcome.
|
||||
|
||||
This includes bug reports, feature requests, ideas, pull requests, and examples of how you have used this software.
|
||||
|
||||
Please see the [Code of Conduct](CODE_OF_CONDUCT.md) and follow any templates configured in GitHub when reporting bugs, requesting enhancements, or contributing code.
|
||||
|
||||
Please raise any significant new functionality or breaking change an issue for discussion before raising a Pull Request for it.
|
||||
|
||||
## For contributors
|
||||
|
||||
Anyone can be a contributor. Either you found a typo, or you have an awesome feature request you could implement, we encourage you to create a Pull Request.
|
||||
### Pull Requests
|
||||
|
||||
* The latest changes are always in `main`, so please make your Pull Request against that branch.
|
||||
* Pull Requests should be raised for any change
|
||||
* Pull Requests need approval of a [core contributor](https://next-auth.js.org/contributors#core-team) before merging
|
||||
* We use ESLint/Prettier for linting/formatting, so please run `npm run lint:fix` before committing to make resolving conflicts easier (VSCode users, check out [this ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [this Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) to fix lint and formatting issues in development)
|
||||
* We encourage you to test your changes, and if you have the opportunity, please make those tests part of the Pull Request
|
||||
* If you add new functionality, please provide the corresponding documentation as well and make it part of the Pull Request
|
||||
|
||||
### Setting up local environment
|
||||
|
||||
A quick guide on how to setup *next-auth* locally to work on it and test out any changes:
|
||||
|
||||
1. Clone the repo:
|
||||
```sh
|
||||
git clone git@github.com:nextauthjs/next-auth.git
|
||||
cd next-auth
|
||||
```
|
||||
|
||||
2. Install packages:
|
||||
```sh
|
||||
npm i && npm dev:setup
|
||||
```
|
||||
|
||||
3. Populate `.env.local`:
|
||||
|
||||
Copy `app/.env.local.example` to `app/.env.local`, and add your env variables for each provider you want to test.
|
||||
|
||||
> NOTE: You can add any environment variables to .env.local that you would like to use in your dev app.
|
||||
> You can find the next-auth config under`app/pages/api/auth/[...nextauth].js`.
|
||||
|
||||
1. Start the dev application/server:
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Your dev application will be available on ```http://localhost:3000```
|
||||
|
||||
That's it! 🎉
|
||||
|
||||
If you need an example project to link to, you can use [next-auth-example](https://github.com/iaincollins/next-auth-example).
|
||||
|
||||
#### Hot reloading
|
||||
|
||||
When running `npm run dev`, you start a Next.js dev server on `http://localhost:3000`, which includes hot reloading out of the box. Make changes on any of the files in `src` and see the changes immediately.
|
||||
|
||||
> NOTE: When working on CSS, you will have to manually refresh the page after changes. The reason for this is our pages using CSS are server-side rendered. (Improving this through a PR is very welcome!)
|
||||
|
||||
> NOTE: The setup is as follows: The development application lives inside the `app` folder, and whenever you make a change to the `src` folder in the root (where next-auth is), it gets copied into `app` every time (gitignored), so Next.js can pick them up and apply hot reloading. This is to avoid some annoying issues with how symlinks are working with different React builds, and also to provide a super-fast feedback loop while developing core features.
|
||||
|
||||
#### Providers
|
||||
|
||||
If you think your custom provider might be useful to others, we encourage you to open a PR and add it to the built-in list so others can discover it much more easily! You only need to add two changes:
|
||||
1. Add your config: [`src/providers/{provider}.js`](https://github.com/nextauthjs/next-auth/tree/main/src/providers) (Make sure you use a named default export, like `export default function YourProvider`!)
|
||||
2. Add provider documentation: [`www/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/www/docs/providers)
|
||||
|
||||
That's it! 🎉 Others will be able to discover this provider much more easily now!
|
||||
|
||||
You can look at the existing built-in providers for inspiration.
|
||||
|
||||
#### Databases
|
||||
|
||||
Included is a Docker Compose file that starts up MySQL, PostgreSQL, and MongoDB databases on localhost.
|
||||
|
||||
It will use port `3306`, `5432`, and `27017` on localhost respectively; please make sure those ports are not used by other services on localhost.
|
||||
|
||||
You can start them with `npm run db:start` and stop them with `npm run db:stop`.
|
||||
|
||||
You will need Docker and Docker Compose installed to be able to start / stop the databases.
|
||||
|
||||
When stopping the databases, it will reset their contents.
|
||||
|
||||
#### Testing
|
||||
|
||||
Tests can be run with `npm run test`.
|
||||
|
||||
Automated tests are currently crude and limited in functionality, but improvements are in development.
|
||||
|
||||
Currently, to run tests you need to first have started local test databases (e.g. using `npm run db:start`).
|
||||
|
||||
The databases can take a few seconds to start up, so you might need to give it a minute before running the tests.
|
||||
|
||||
## For maintainers
|
||||
|
||||
We use [semantic-release](https://github.com/semantic-release/semantic-release) together with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0) to automate releases. This makes the maintainenance process easier and less error-prone to human error. Please study the "Conventional Commits" site to understand how to write a good commit message.
|
||||
|
||||
When accepting Pull Requests, make sure the following:
|
||||
|
||||
* Use "Squash and merge"
|
||||
* Make sure you merge contributor PRs into `main`
|
||||
* Rewrite the commit message to conform to the `Conventional Commits` style. Check the "Recommended Scopes" section for further advice.
|
||||
* Optionally link issues the PR will resolve (You can add "close" in front of the issue numbers to close the issues automatically, when the PR is merged. `semantic-release` will also comment back to connected issues and PRs, notifying the users that a feature is added/bug fixed, etc.)
|
||||
|
||||
### Recommended Scopes
|
||||
|
||||
A typical conventional commit looks like this:
|
||||
```
|
||||
type(scope): title
|
||||
|
||||
body
|
||||
```
|
||||
|
||||
Scope is the part that will help groupping the different commit types in the release notes.
|
||||
|
||||
Some recommened scopes are:
|
||||
|
||||
- **provider** - Provider related changes. (eg.: "feat(provider): add X provider", "docs(provider): fix typo in X documentation"
|
||||
- **adapter** - Adapter related changes. (eg.: "feat(adapter): add X provider", "docs(provider): fix typo in X documentation"
|
||||
- **db** - Database related changes. (eg.: "feat(db): add X database", "docs(db): fix typo in X documentation"
|
||||
- **deps** - Adding/removing/updating a dependency (eg.: "chore(deps): add X")
|
||||
|
||||
> NOTE: If you are not sure which scope to use, you can simply ignore it. (eg.: "feat: add something"). Adding the correct type already helps a lot when analyzing the commit messages.
|
||||
|
||||
|
||||
### Skipping a release
|
||||
|
||||
Every commit that contains [skip release] or [release skip] in their message will be excluded from the commit analysis and won't participate in the release type determination. This is useful, if the PR being merged should not trigger a new `npm` release.
|
||||
@@ -1,6 +1,6 @@
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2018, Iain Collins
|
||||
Copyright (c) 2018-2021, Iain Collins
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
@@ -1,95 +0,0 @@
|
||||
# NextAuthClient
|
||||
|
||||
## About NextAuthClient
|
||||
|
||||
NextAuthClient is session library for the [next-auth](https://www.npmjs.com/package/next-auth) module.
|
||||
|
||||
## Methods
|
||||
|
||||
It provides the following methods, all of which return a promise.
|
||||
|
||||
### NextAuthClient.init({ req, force })
|
||||
|
||||
Isometric (can be used in server side rendering when passed optional `req` object).
|
||||
|
||||
Return the current session.
|
||||
|
||||
When using Server Side Rendering and passed `req` object from **getInitialProps({req})** it will read the data from it.
|
||||
|
||||
When using Client Side Rendering it will use localStorage (if avalible) to check for cached session data and if not found or expired it call the `/auth/session` end point.
|
||||
|
||||
### NextAuthClient.signin(string or object)
|
||||
|
||||
Client side only method.
|
||||
|
||||
If passed a string treats it as an email address, generates an email sign in token and makes POST request to `/auth/email/signin`.
|
||||
|
||||
If passed an object treats it as a form to be handled by a custom signIn() function and makes a POST request to `/auth/signin`.
|
||||
|
||||
### NextAuthClient.signout()
|
||||
|
||||
Client side only method. Triggers the current session to be destroyed.
|
||||
|
||||
Makes POST request to `/auth/signout`.
|
||||
|
||||
### NextAuthClient.csrfToken()
|
||||
|
||||
Client side only method. Returns the latest CSRF Token.
|
||||
|
||||
Note: When rendering server side, this is accessible from NextAuthClient.init().
|
||||
|
||||
Makes GET request to `/auth/csrf`.
|
||||
|
||||
### NextAuthClient.linked({ req })
|
||||
|
||||
Isometric method (can be used in server side rendering when passed optional `req` object).
|
||||
|
||||
Returns a list of linked/unlinked oAuth providers.
|
||||
|
||||
This is useful on account management pages where you want to display buttons to link / unlink accounts.
|
||||
|
||||
Makes GET request to `/auth/linked`.
|
||||
|
||||
### NextAuthClient.providers({ req })
|
||||
|
||||
Isometric method (can be used in server side rendering when passed optional `req` object).
|
||||
|
||||
Returns a list of all configured oAuth providers.
|
||||
|
||||
It includes their names, sign in URLs and callback URLs.
|
||||
|
||||
This is useful on sign in pages (e.g. to render sign in links for all configured providers).
|
||||
|
||||
Makes GET request to `/auth/providers`.
|
||||
|
||||
## Example
|
||||
|
||||
````javascript
|
||||
import React from 'react'
|
||||
import { NextAuth } from 'next-auth/client'
|
||||
|
||||
export default class extends React.Component {
|
||||
static async getInitialProps({req}) {
|
||||
return {
|
||||
session: await NextAuth.init({req})
|
||||
}
|
||||
}
|
||||
render() {
|
||||
if (this.props.session.user) {
|
||||
return(
|
||||
<div>
|
||||
<p>You are logged in as {this.props.session.user.name || this.props.session.user.email}.</p>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return(
|
||||
<div>
|
||||
<p>You are not logged in.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
````
|
||||
|
||||
See [next-auth](https://www.npmjs.com/package/next-auth) for more information or [nextjs-starter](https://nextjs-starter.now.sh) for a working demo.
|
||||
344
README.md
344
README.md
@@ -1,221 +1,165 @@
|
||||
# NextAuth
|
||||
<p align="center">
|
||||
<br/>
|
||||
<a href="https://next-auth.js.org" target="_blank"><img width="150px" src="https://next-auth.js.org/img/logo/logo-sm.png" /></a>
|
||||
<h3 align="center">NextAuth.js</h3>
|
||||
<p align="center">Authentication for Next.js</p>
|
||||
<p align="center">
|
||||
Open Source. Full Stack. Own Your Data.
|
||||
</p>
|
||||
<p align="center" style="align: center;">
|
||||
<a href="https://github.com/nextauthjs/next-auth/actions?query=workflow%3ARelease">
|
||||
<img src="https://github.com/nextauthjs/next-auth/workflows/Release/badge.svg" alt="Release" />
|
||||
</a>
|
||||
<a href="https://github.com/nextauthjs/next-auth/actions?query=workflow%3A%22Integration+Test%22">
|
||||
<img src="https://github.com/nextauthjs/next-auth/workflows/Integration%20Test/badge.svg" alt="Integration Test" />
|
||||
</a>
|
||||
<a href="https://bundlephobia.com/result?p=next-auth">
|
||||
<img src="https://img.shields.io/bundlephobia/minzip/next-auth" alt="Bundle Size"/>
|
||||
</a>
|
||||
<a href="https://www.npmtrends.com/next-auth">
|
||||
<img src="https://img.shields.io/npm/dm/next-auth" alt="Downloads" />
|
||||
</a>
|
||||
<a href="https://github.com/nextauthjs/next-auth/stargazers">
|
||||
<img src="https://img.shields.io/github/stars/nextauthjs/next-auth" alt="Github Stars" />
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/next-auth">
|
||||
<img src="https://img.shields.io/github/v/release/nextauthjs/next-auth?label=latest" alt="Github Stable Release" />
|
||||
</a>
|
||||
<img src="https://img.shields.io/github/v/release/nextauthjs/next-auth?include_prereleases&label=prerelease&sort=semver" alt="Github Prelease" />
|
||||
</p>
|
||||
</p>
|
||||
|
||||
## About NextAuth
|
||||
## Overview
|
||||
|
||||
NextAuth is an authentication library for Next.js projects.
|
||||
NextAuth.js is a complete open source authentication solution for [Next.js](http://nextjs.org/) applications.
|
||||
|
||||
The NextAuth library uses Express and Passport, the most commonly used authentication library for Node.js, to provide support for signing in with email and with services like Facebook, Google and Twitter.
|
||||
|
||||
NextAuth adds Cross Site Request Forgery (CSRF) tokens and HTTP Only cookies, supports universal rendering and does not require client side JavaScript.
|
||||
|
||||
It adds session support without using client side accessible session tokens, providing protection against Cross Site Scripting (XSS) and session hijacking, while leveraging localStorage where available to cache non-critical session state for optimal performance in Single Page Apps.
|
||||
|
||||
The NextAuth comes with a client library, designed to work with React pages powered by Next.js to easily add universal session support to sites.
|
||||
|
||||
It contains an [example site](https://github.com/iaincollins/next-auth/tree/master/example) that shows how to use it in a simple project. It's also used in the [nextjs-starter.now.sh](https://nextjs-starter.now.sh) project, which provides a more complete example with a live demo.
|
||||
|
||||
Note: As of version 1.5 NextAuth is also compatible non-Next.js React projects, just pass `null` instead of a nextApp instance when calling `nextAuth()`.
|
||||
|
||||
You will need to handle setting up routes before and after initialising NextAuth if you are not using Next.js. NextAuth lets you pass an instance of express as 'expressApp' option (and returns it in the response).
|
||||
|
||||
## Example Client Usage
|
||||
|
||||
````javascript
|
||||
import React from 'react'
|
||||
import { NextAuth } from 'next-auth/client'
|
||||
|
||||
export default class extends React.Component {
|
||||
static async getInitialProps({req}) {
|
||||
return {
|
||||
session: await NextAuth.init({req})
|
||||
}
|
||||
}
|
||||
render() {
|
||||
if (this.props.session.user) {
|
||||
return(
|
||||
<div>
|
||||
<p>You are logged in as {this.props.session.user.name || this.props.session.user.email}.</p>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return(
|
||||
<div>
|
||||
<p>You are not logged in.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
````
|
||||
|
||||
See [Documentation for the NextAuth Client](https://github.com/iaincollins/next-auth/blob/master/README-CLIENT.md) for more information on how to interact with the client.
|
||||
|
||||
## Routes
|
||||
|
||||
NextAuth adds a number of routes under `/auth':
|
||||
|
||||
* POST `/auth/email/signin` - Request Sign In Token
|
||||
* GET `/auth/email/signin/:token` - Validate Sign In Token
|
||||
* POST `/auth/signout` - Sign Out
|
||||
* GET `/auth/csrf` - CSRF endpoint for Single Page Apps
|
||||
* GET `/auth/session` - Session endpoint for Single Page Apps
|
||||
* GET `/auth/linked` - View linked accounts for Single Page Apps
|
||||
|
||||
All POST routes request must include a CSRF token.
|
||||
|
||||
CSRF, Session and Linked Account endpoints are provided for Single Page Apps.
|
||||
|
||||
Note: Session Tokens are stored in HTTP Only cookies to prevent session hijacking and protect against Cross Site Scripting (XSS) attacks. Only HTTP requests that originate from the original domain are able to read from them.
|
||||
|
||||
In addition, it will add the following routes for each oAuth provider currently configured:
|
||||
|
||||
* GET `/auth/oauth/${provider}`
|
||||
* GET `/auth/oauth/${provider}/callback`
|
||||
* POST `/auth/oauth/${provider}/unlink`
|
||||
|
||||
You can see which routes are configured and the callback URLs defined for them via this route:
|
||||
|
||||
* GET `/auth/providers`
|
||||
|
||||
It will return a JSON object with the current oAuth provider configuration:
|
||||
|
||||
````json
|
||||
{
|
||||
"Facebook": {
|
||||
"signin": "/auth/oauth/facebook",
|
||||
"callback": "/auth/oauth/facebook/callback"
|
||||
},
|
||||
"Google": {
|
||||
"signin": "/auth/oauth/google",
|
||||
"callback": "/auth/oauth/google/callback"
|
||||
},
|
||||
"Twitter": {
|
||||
"signin": "/auth/oauth/twitter",
|
||||
"callback": "/auth/oauth/twitter/callback"
|
||||
}
|
||||
}
|
||||
````
|
||||
|
||||
Note: The `/auth` prefix is configurable via an option in the module, but it is currently hard coded in the client component, so you probably don't want to change it. It will be configurable in future releases.
|
||||
It is designed from the ground up to support Next.js and Serverless.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Create an `index.js` file in the root of your Next.js project containing the following:
|
||||
```
|
||||
npm install --save next-auth
|
||||
```
|
||||
|
||||
````javascript
|
||||
const next = require('next')
|
||||
const nextAuth = require('next-auth')
|
||||
const nextAuthConfig = require('./next-auth.config')
|
||||
The easiest way to continue getting started, is to follow the [getting started](https://next-auth.js.org/getting-started/example) section in our docs.
|
||||
|
||||
require('dotenv').load()
|
||||
We also have a section of [tutorials](https://next-auth.js.org/tutorials) for those looking for more specific examples.
|
||||
|
||||
const nextApp = next({
|
||||
dir: '.',
|
||||
dev: (process.env.NODE_ENV === 'development')
|
||||
See [next-auth.js.org](https://next-auth.js.org) for more information and documentation.
|
||||
|
||||
## Features
|
||||
|
||||
### Flexible and easy to use
|
||||
|
||||
* Designed to work with any OAuth service, it supports OAuth 1.0, 1.0A and 2.0
|
||||
* Built-in support for [many popular sign-in services](https://next-auth.js.org/configuration/providers)
|
||||
* Supports email / passwordless authentication
|
||||
* Supports stateless authentication with any backend (Active Directory, LDAP, etc)
|
||||
* Supports both JSON Web Tokens and database sessions
|
||||
* Designed for Serverless but runs anywhere (AWS Lambda, Docker, Heroku, etc…)
|
||||
|
||||
### Own your own data
|
||||
|
||||
NextAuth.js can be used with or without a database.
|
||||
|
||||
* An open source solution that allows you to keep control of your data
|
||||
* Supports Bring Your Own Database (BYOD) and can be used with any database
|
||||
* Built-in support for [MySQL, MariaDB, Postgres, Microsoft SQL Server, MongoDB and SQLite](https://next-auth.js.org/configuration/databases)
|
||||
* Works great with databases from popular hosting providers
|
||||
* Can also be used *without a database* (e.g. OAuth + JWT)
|
||||
|
||||
### Secure by default
|
||||
|
||||
* Promotes the use of passwordless sign in mechanisms
|
||||
* Designed to be secure by default and encourage best practice for safeguarding user data
|
||||
* Uses Cross Site Request Forgery Tokens on POST routes (sign in, sign out)
|
||||
* Default cookie policy aims for the most restrictive policy appropriate for each cookie
|
||||
* When JSON Web Tokens are enabled, they are signed by default (JWS) with HS512
|
||||
* Use JWT encryption (JWE) by setting the option `encryption: true` (defaults to A256GCM)
|
||||
* Auto-generates symmetric signing and encryption keys for developer convenience
|
||||
* Features tab/window syncing and keepalive messages to support short lived sessions
|
||||
* Attempts to implement the latest guidance published by [Open Web Application Security Project](https://owasp.org/)
|
||||
|
||||
Advanced options allow you to define your own routines to handle controlling what accounts are allowed to sign in, for encoding and decoding JSON Web Tokens and to set custom cookie security policies and session properties, so you can control who is able to sign in and how often sessions have to be re-validated.
|
||||
|
||||
### TypeScript
|
||||
|
||||
NextAuth.js comes with built-in types. For more information and usage, check out the [TypeScript section](https://next-auth.js.org/getting-started/typescript) in the documentaion.
|
||||
|
||||
The package at `@types/next-auth` is now deprecated.
|
||||
|
||||
## Example
|
||||
|
||||
### Add API Route
|
||||
|
||||
```javascript
|
||||
import NextAuth from 'next-auth'
|
||||
import Providers from 'next-auth/providers'
|
||||
|
||||
export default NextAuth({
|
||||
providers: [
|
||||
// OAuth authentication providers
|
||||
Providers.Apple({
|
||||
clientId: process.env.APPLE_ID,
|
||||
clientSecret: process.env.APPLE_SECRET
|
||||
}),
|
||||
Providers.Google({
|
||||
clientId: process.env.GOOGLE_ID,
|
||||
clientSecret: process.env.GOOGLE_SECRET
|
||||
}),
|
||||
// Sign in with passwordless email link
|
||||
Providers.Email({
|
||||
server: process.env.MAIL_SERVER,
|
||||
from: '<no-reply@example.com>'
|
||||
}),
|
||||
],
|
||||
// SQL or MongoDB database (or leave empty)
|
||||
database: process.env.DATABASE_URL
|
||||
})
|
||||
```
|
||||
|
||||
nextApp.prepare()
|
||||
.then(async () => {
|
||||
const nextAuthOptions = await nextAuthConfig()
|
||||
const nextAuthApp = await nextAuth(nextApp, nextAuthOptions)
|
||||
console.log(`Ready on http://localhost:${process.env.PORT || 3000}`)
|
||||
})
|
||||
.catch(err => {
|
||||
console.log('An error occurred, unable to start the server')
|
||||
console.log(err)
|
||||
})
|
||||
````
|
||||
### Add React Component
|
||||
|
||||
You can add the following to your `package.json` file to start the project:
|
||||
```javascript
|
||||
import {
|
||||
useSession, signIn, signOut
|
||||
} from 'next-auth/client'
|
||||
|
||||
````json
|
||||
"scripts": {
|
||||
"dev": "NODE_ENV=development node index.js",
|
||||
"build": "next build",
|
||||
"start": "node index.js"
|
||||
export default function Component() {
|
||||
const [ session, loading ] = useSession()
|
||||
if(session) {
|
||||
return <>
|
||||
Signed in as {session.user.email} <br/>
|
||||
<button onClick={() => signOut()}>Sign out</button>
|
||||
</>
|
||||
}
|
||||
return <>
|
||||
Not signed in <br/>
|
||||
<button onClick={() => signIn()}>Sign in</button>
|
||||
</>
|
||||
}
|
||||
````
|
||||
```
|
||||
|
||||
## Pages
|
||||
## Acknowledgements
|
||||
|
||||
You will need to create following pages under `./pages/auth` in your project:
|
||||
[NextAuth.js is made possible thanks to all of its contributors.](https://next-auth.js.org/contributors)
|
||||
|
||||
* index.js – Sign In and Link/Unlink accounts
|
||||
* error.js – If an authentication error occurs
|
||||
* check-email.js – 'Check your email' messsage
|
||||
* callback.js – Callback page; updates local session on sign in / sign out
|
||||
<a href="https://github.com/nextauthjs/next-auth/graphs/contributors">
|
||||
<img width="500px" src="https://contrib.rocks/image?repo=nextauthjs/next-auth" />
|
||||
</a>
|
||||
<div>
|
||||
<a href="https://vercel.com?utm_source=nextauthjs&utm_campaign=oss">
|
||||
<img width="170px" src="https://raw.githubusercontent.com/nextauthjs/next-auth/canary/www/static/img/powered-by-vercel.svg" alt="Powered By Vercel" />
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<p align="left">Thanks to Vercel sponsoring this project by allowing it to be deployed for free for the entire NextAuth.js Team</p>
|
||||
</div>
|
||||
|
||||
You can [find examples of these](https://github.com/iaincollins/next-auth/tree/master/example) included which you can copy and paste into your project.
|
||||
## Contributing
|
||||
|
||||
## Configuration
|
||||
We're open to all community contributions! If you'd like to contribute in any way, please first read our [Contributing Guide](https://github.com/nextauthjs/next-auth/blob/canary/CONTRIBUTING.md).
|
||||
|
||||
Configuration can be split across three files to make it easier to understand and manage.
|
||||
## License
|
||||
|
||||
You can copy over the following configuration files into the root of your project to get started:
|
||||
|
||||
* [next-auth.config.js](https://github.com/iaincollins/next-auth/tree/master/example/next-auth.config.js)
|
||||
* [next-auth.functions.js](https://github.com/iaincollins/next-auth/tree/master/example/next-auth.functions.js)
|
||||
* [next-auth.providers.js](https://github.com/iaincollins/next-auth/tree/master/example/next-auth.providers.js)
|
||||
|
||||
|
||||
You can also add a **.env** file to the root of the project as a place to specify configuration options. The provided example files for NextAuth will use one if there is is one.
|
||||
|
||||
````
|
||||
SERVER_URL=http://localhost:3000
|
||||
MONGO_URI=mongodb://localhost:27017/my-database
|
||||
FACEBOOK_ID=
|
||||
FACEBOOK_SECRET=
|
||||
GOOGLE_ID=
|
||||
GOOGLE_SECRET=
|
||||
TWITTER_KEY=
|
||||
TWITTER_SECRET=
|
||||
EMAIL_FROM=username@gmail.com
|
||||
EMAIL_SERVER=smtp.gmail.com
|
||||
EMAIL_PORT=465
|
||||
EMAIL_USERNAME=username@gmail.com
|
||||
EMAIL_PASSWORD=
|
||||
````
|
||||
|
||||
### next-auth.config.js
|
||||
|
||||
Basic configuration of NextAuth is handled in **next-auth.config.js**.
|
||||
|
||||
It is also where the **next-auth.functions.js** and **next-auth.providers.js** files are loaded.
|
||||
|
||||
### next-auth.functions.js
|
||||
|
||||
Methods for user management and sending email are defined in **next-auth.functions.js**
|
||||
|
||||
The example configuration provided is for Mongo DB. By defining the behaviour in these functions you can use NextAuth with any database, including a relational database that uses SQL.
|
||||
|
||||
#### Required
|
||||
|
||||
* find({id,email,emailToken,provider})
|
||||
* insert(user, oAuthProfile, providerParams)
|
||||
* update(user, oAuthProfile, providerParams)
|
||||
* remove(id)
|
||||
* serialize(user)
|
||||
* deserialize(id)
|
||||
|
||||
#### Optional
|
||||
|
||||
* sendSigninEmail({email, url, req})
|
||||
* signIn({form, req})
|
||||
|
||||
The `sendSigninEmail()` method is used to send an email for email token based sign in (one time use passwords). Omit it or set it to null to disable email based sign in.
|
||||
|
||||
The `signIn()` method is used to handle authenticating with custom credentials (e.g. username and password, 2FA token, etc). Omit it or leave it undefined unless you need it.
|
||||
|
||||
You can use any combination of authentication methods (email, credentials, oAuth providers).
|
||||
|
||||
### next-auth.providers.js
|
||||
|
||||
Configuration for oAuth providers are defined in **next-auth.providers.js**
|
||||
|
||||
It includes configuration examples for Facebook, Google and Twitter oAuth, which can easily be copied and replicated to add support for signing in other oAuth providers.
|
||||
|
||||
For tips on configuring oAuth see [AUTHENTICATION.md](https://github.com/iaincollins/next-auth/tree/master/AUTHENTICATION.md).
|
||||
|
||||
----
|
||||
|
||||
See the included [example site](https://github.com/iaincollins/next-auth/tree/master/example) and the expanded example at [nextjs-starter.now.sh](https://nextjs-starter.now.sh/examples/authentication).
|
||||
ISC
|
||||
|
||||
24
SECURITY.md
Normal file
24
SECURITY.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Security Policy
|
||||
|
||||
NextAuth.js practices responsible disclosure.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Security updates are only released for the current version.
|
||||
|
||||
Old releases are not maintained and do not receive updates.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
We request that you contact us directly to report serious issues that might impact the security of sites using NextAuth.js.
|
||||
|
||||
If you contact us regarding a serious issue:
|
||||
|
||||
* We will endeavor to get back to you within 72 hours.
|
||||
* We will aim to publish a fix within 30 days.
|
||||
* We will disclose the issue (and credit you, with your consent) once a fix to resolve the issue has been released.
|
||||
* If 90 days has elapsed and we still don't have a fix, we will disclose the issue publically.
|
||||
|
||||
Currently, the best way to report an issue is by emailing me@iaincollins.com
|
||||
|
||||
For less serious issues (e.g. RFC compliance for unsupported flows or potential issues that may cause a problem future or default behaviour / options) it is appropriate to submit these these publically as bug reports or feature requests or to raise a question to open a discussion around them.
|
||||
33
app/.env.local.example
Normal file
33
app/.env.local.example
Normal file
@@ -0,0 +1,33 @@
|
||||
# Rename file to .env.local (or .env) and populate values
|
||||
# to be able to run the dev app
|
||||
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
|
||||
# You can use `openssl rand -hex 32` or
|
||||
# https://generate-secret.now.sh/32 to generate a secret.
|
||||
# Note: Changing a secret may invalidate existing sessions
|
||||
# and/or verificaion tokens.
|
||||
SECRET=
|
||||
|
||||
AUTH0_ID=
|
||||
AUTH0_DOMAIN=
|
||||
AUTH0_SECRET=
|
||||
|
||||
GITHUB_ID=
|
||||
GITHUB_SECRET=
|
||||
|
||||
TWITTER_ID=
|
||||
TWITTER_SECRET=
|
||||
|
||||
# Example configuration for a Gmail account (will need SMTP enabled)
|
||||
EMAIL_SERVER=smtps://user@gmail.com:password@smtp.gmail.com:465
|
||||
EMAIL_FROM=user@gmail.com
|
||||
|
||||
# You can use any of these as the "DATABASE_URL" for
|
||||
# databases started with Docker using `npm run db:start`.
|
||||
# Note: If using with Prisma adapter, you need to use a `.env`
|
||||
# file rather than a `.env.local` file to configure env vars.
|
||||
# Postgres: DATABASE_URL=postgres://nextauth:password@127.0.0.1:5432/nextauth?synchronize=true
|
||||
# MySQL: DATABASE_URL=mysql://nextauth:password@127.0.0.1:3306/nextauth?synchronize=true
|
||||
# MongoDB: DATABASE_URL=mongodb://nextauth:password@127.0.0.1:27017/nextauth?synchronize=true
|
||||
DATABASE_URL=
|
||||
6
app/README.md
Normal file
6
app/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# NextAuth.js Development App
|
||||
|
||||
This folder contains a Next.js app using NextAuth.js for local development. See the following section on how to start:
|
||||
|
||||
[Setting up local environment
|
||||
](https://github.com/nextauthjs/next-auth/blob/main/CONTRIBUTING.md#setting-up-local-environment)
|
||||
19
app/components/access-denied.js
Normal file
19
app/components/access-denied.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { signIn } from 'next-auth/client'
|
||||
|
||||
export default function AccessDenied () {
|
||||
return (
|
||||
<>
|
||||
<h1>Access Denied</h1>
|
||||
<p>
|
||||
<a
|
||||
href='/api/auth/signin'
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
signIn()
|
||||
}}
|
||||
>You must be signed in to view this page
|
||||
</a>
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
18
app/components/footer.js
Normal file
18
app/components/footer.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import Link from 'next/link'
|
||||
import styles from './footer.module.css'
|
||||
import { version } from 'package.json'
|
||||
|
||||
export default function Footer () {
|
||||
return (
|
||||
<footer className={styles.footer}>
|
||||
<hr />
|
||||
<ul className={styles.navItems}>
|
||||
<li className={styles.navItem}><a href='https://next-auth.js.org'>Documentation</a></li>
|
||||
<li className={styles.navItem}><a href='https://www.npmjs.com/package/next-auth'>NPM</a></li>
|
||||
<li className={styles.navItem}><a href='https://github.com/nextauthjs/next-auth-example'>GitHub</a></li>
|
||||
<li className={styles.navItem}><Link href='/policy'><a>Policy</a></Link></li>
|
||||
<li className={styles.navItem}><em>{version}</em></li>
|
||||
</ul>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
14
app/components/footer.module.css
Normal file
14
app/components/footer.module.css
Normal file
@@ -0,0 +1,14 @@
|
||||
.footer {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.navItems {
|
||||
margin-bottom: 1rem;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.navItem {
|
||||
display: inline-block;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
112
app/components/header.js
Normal file
112
app/components/header.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import Link from 'next/link'
|
||||
import { signIn, signOut, useSession } from 'next-auth/client'
|
||||
import styles from './header.module.css'
|
||||
|
||||
// The approach used in this component shows how to built a sign in and sign out
|
||||
// component that works on pages which support both client and server side
|
||||
// rendering, and avoids any flash incorrect content on initial page load.
|
||||
export default function Header () {
|
||||
const [session, loading] = useSession()
|
||||
|
||||
return (
|
||||
<header>
|
||||
<noscript>
|
||||
<style>{'.nojs-show { opacity: 1; top: 0; }'}</style>
|
||||
</noscript>
|
||||
<div className={styles.signedInStatus}>
|
||||
<p
|
||||
className={`nojs-show ${
|
||||
!session && loading ? styles.loading : styles.loaded
|
||||
}`}
|
||||
>
|
||||
{!session && (
|
||||
<>
|
||||
<span className={styles.notSignedInText}>
|
||||
You are not signed in
|
||||
</span>
|
||||
<a
|
||||
href='/api/auth/signin'
|
||||
className={styles.buttonPrimary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
signIn()
|
||||
}}
|
||||
>
|
||||
Sign in
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
{session && (
|
||||
<>
|
||||
{session.user.image && (
|
||||
<span
|
||||
style={{ backgroundImage: `url(${session.user.image})` }}
|
||||
className={styles.avatar}
|
||||
/>
|
||||
)}
|
||||
<span className={styles.signedInText}>
|
||||
<small>Signed in as</small>
|
||||
<br />
|
||||
<strong>{session.user.email || session.user.name}</strong>
|
||||
</span>
|
||||
<a
|
||||
href='/api/auth/signout'
|
||||
className={styles.button}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
signOut()
|
||||
}}
|
||||
>
|
||||
Sign out
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<nav>
|
||||
<ul className={styles.navItems}>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/'>
|
||||
<a>Home</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/client'>
|
||||
<a>Client</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/server'>
|
||||
<a>Server</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/protected'>
|
||||
<a>Protected</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/protected-ssr'>
|
||||
<a>Protected(SSR)</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/api-example'>
|
||||
<a>API</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/credentials'>
|
||||
<a>Credentials</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/email'>
|
||||
<a>Email</a>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
92
app/components/header.module.css
Normal file
92
app/components/header.module.css
Normal file
@@ -0,0 +1,92 @@
|
||||
/* Set min-height to avoid page reflow while session loading */
|
||||
.signedInStatus {
|
||||
display: block;
|
||||
min-height: 4rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.loaded {
|
||||
position: relative;
|
||||
top: 0;
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
border-radius: 0 0 .6rem .6rem;
|
||||
padding: .6rem 1rem;
|
||||
margin: 0;
|
||||
background-color: rgba(0,0,0,.05);
|
||||
transition: all 0.2s ease-in;
|
||||
}
|
||||
|
||||
.loading {
|
||||
top: -2rem;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.signedInText,
|
||||
.notSignedInText {
|
||||
position: absolute;
|
||||
padding-top: .8rem;
|
||||
left: 1rem;
|
||||
right: 6.5rem;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
display: inherit;
|
||||
z-index: 1;
|
||||
line-height: 1.3rem;
|
||||
}
|
||||
|
||||
.signedInText {
|
||||
padding-top: 0rem;
|
||||
left: 4.6rem;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border-radius: 2rem;
|
||||
float: left;
|
||||
height: 2.8rem;
|
||||
width: 2.8rem;
|
||||
background-color: white;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.button,
|
||||
.buttonPrimary {
|
||||
float: right;
|
||||
margin-right: -.4rem;
|
||||
font-weight: 500;
|
||||
border-radius: .3rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
line-height: 1.4rem;
|
||||
padding: .7rem .8rem;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
background-color: transparent;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.buttonPrimary {
|
||||
background-color: #346df1;
|
||||
border-color: #346df1;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
padding: .7rem 1.4rem;
|
||||
}
|
||||
|
||||
.buttonPrimary:hover {
|
||||
box-shadow: inset 0 0 5rem rgba(0,0,0,0.2)
|
||||
}
|
||||
|
||||
.navItems {
|
||||
margin-bottom: 2rem;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.navItem {
|
||||
display: inline-block;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
14
app/components/layout.js
Normal file
14
app/components/layout.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import Header from 'components/header'
|
||||
import Footer from 'components/footer'
|
||||
|
||||
export default function Layout ({ children }) {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main>
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
}
|
||||
5
app/jsconfig.json
Normal file
5
app/jsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
}
|
||||
}
|
||||
2
app/next-env.d.ts
vendored
Normal file
2
app/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
19
app/next.config.js
Normal file
19
app/next.config.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const path = require("path")
|
||||
|
||||
module.exports = {
|
||||
webpack(config) {
|
||||
config.resolve = {
|
||||
...config.resolve,
|
||||
alias: {
|
||||
...config.resolve.alias,
|
||||
"next-auth$": path.join(process.cwd(), "next-auth/server"),
|
||||
"next-auth/client$": path.join(process.cwd(), "next-auth/client"),
|
||||
"next-auth/jwt$": path.join(process.cwd(), "next-auth/lib/jwt"),
|
||||
"next-auth/adapters": path.join(process.cwd(), "next-auth/adapters"),
|
||||
"next-auth/providers": path.join(process.cwd(), "next-auth/providers"),
|
||||
},
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
}
|
||||
25
app/package.json
Normal file
25
app/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "next-auth-app",
|
||||
"version": "1.0.0",
|
||||
"description": "NextAuth.js Developer app",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npm-run-all --parallel copy:app dev:css dev:next",
|
||||
"dev:next": "next dev",
|
||||
"copy:app": "cpx \"../src/**/*\" next-auth --watch",
|
||||
"copy:css": "cpx \"../dist/css/**/*\" dist/css --watch",
|
||||
"watch:css": "cd .. && npm run watch:css",
|
||||
"dev:css": "npm-run-all --parallel watch:css copy:css",
|
||||
"start": "next start"
|
||||
},
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"next": "^10.1.3",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cpx": "^1.5.0",
|
||||
"npm-run-all": "^4.1.5"
|
||||
}
|
||||
}
|
||||
31
app/pages/_app.js
Normal file
31
app/pages/_app.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Provider } from "next-auth/client"
|
||||
import "./styles.css"
|
||||
|
||||
// Use the <Provider> to improve performance and allow components that call
|
||||
// `useSession()` anywhere in your application to access the `session` object.
|
||||
export default function App({ Component, pageProps }) {
|
||||
return (
|
||||
<Provider
|
||||
// Provider options are not required but can be useful in situations where
|
||||
// you have a short session maxAge time. Shown here with default values.
|
||||
options={{
|
||||
// Client Max Age controls how often the useSession in the client should
|
||||
// contact the server to sync the session state. Value in seconds.
|
||||
// e.g.
|
||||
// * 0 - Disabled (always use cache value)
|
||||
// * 60 - Sync session state with server if it's older than 60 seconds
|
||||
clientMaxAge: 0,
|
||||
// Keep Alive tells windows / tabs that are signed in to keep sending
|
||||
// a keep alive request (which extends the current session expiry) to
|
||||
// prevent sessions in open windows from expiring. Value in seconds.
|
||||
//
|
||||
// Note: If a session has expired when keep alive is triggered, all open
|
||||
// windows / tabs will be updated to reflect the user is signed out.
|
||||
keepAlive: 0,
|
||||
}}
|
||||
session={pageProps.session}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
17
app/pages/api-example.js
Normal file
17
app/pages/api-example.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import Layout from '../components/layout'
|
||||
|
||||
export default function Page () {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>API Example</h1>
|
||||
<p>The examples below show responses from the example API endpoints.</p>
|
||||
<p><em>You must be signed in to see responses.</em></p>
|
||||
<h2>Session</h2>
|
||||
<p>/api/examples/session</p>
|
||||
<iframe src='/api/examples/session' />
|
||||
<h2>JSON Web Token</h2>
|
||||
<p>/api/examples/jwt</p>
|
||||
<iframe src='/api/examples/jwt' />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
87
app/pages/api/auth/[...nextauth].js
Normal file
87
app/pages/api/auth/[...nextauth].js
Normal file
@@ -0,0 +1,87 @@
|
||||
import NextAuth from 'next-auth'
|
||||
import Providers from 'next-auth/providers'
|
||||
|
||||
// import Adapters from 'next-auth/adapters'
|
||||
// import { PrismaClient } from '@prisma/client'
|
||||
// const prisma = new PrismaClient()
|
||||
|
||||
export default NextAuth({
|
||||
// Used to debug https://github.com/nextauthjs/next-auth/issues/1664
|
||||
// cookies: {
|
||||
// csrfToken: {
|
||||
// name: 'next-auth.csrf-token',
|
||||
// options: {
|
||||
// httpOnly: true,
|
||||
// sameSite: 'none',
|
||||
// path: '/',
|
||||
// secure: true
|
||||
// }
|
||||
// },
|
||||
// pkceCodeVerifier: {
|
||||
// name: 'next-auth.pkce.code_verifier',
|
||||
// options: {
|
||||
// httpOnly: true,
|
||||
// sameSite: 'none',
|
||||
// path: '/',
|
||||
// secure: true
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
providers: [
|
||||
Providers.Email({
|
||||
server: process.env.EMAIL_SERVER,
|
||||
from: process.env.EMAIL_FROM
|
||||
}),
|
||||
Providers.GitHub({
|
||||
clientId: process.env.GITHUB_ID,
|
||||
clientSecret: process.env.GITHUB_SECRET
|
||||
}),
|
||||
Providers.Auth0({
|
||||
clientId: process.env.AUTH0_ID,
|
||||
clientSecret: process.env.AUTH0_SECRET,
|
||||
domain: process.env.AUTH0_DOMAIN,
|
||||
// Used to debug https://github.com/nextauthjs/next-auth/issues/1664
|
||||
// protection: ["pkce", "state"],
|
||||
// authorizationParams: {
|
||||
// response_mode: 'form_post'
|
||||
// }
|
||||
protection: 'pkce'
|
||||
}),
|
||||
Providers.Twitter({
|
||||
clientId: process.env.TWITTER_ID,
|
||||
clientSecret: process.env.TWITTER_SECRET
|
||||
}),
|
||||
Providers.Credentials({
|
||||
name: 'Credentials',
|
||||
credentials: {
|
||||
password: { label: 'Password', type: 'password' }
|
||||
},
|
||||
async authorize (credentials) {
|
||||
if (credentials.password === 'password') {
|
||||
return {
|
||||
id: 1,
|
||||
name: 'Fill Murray',
|
||||
email: 'bill@fillmurray.com',
|
||||
image: 'https://www.fillmurray.com/64/64'
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
})
|
||||
],
|
||||
jwt: {
|
||||
encryption: true,
|
||||
secret: process.env.SECRET
|
||||
},
|
||||
debug: false,
|
||||
theme: 'auto'
|
||||
|
||||
// Default Database Adapter (TypeORM)
|
||||
// database: process.env.DATABASE_URL
|
||||
|
||||
// Prisma Database Adapter
|
||||
// To configure this app to use the schema in `prisma/schema.prisma` run:
|
||||
// npx prisma generate
|
||||
// npx prisma migrate dev
|
||||
// adapter: Adapters.Prisma.Adapter({ prisma })
|
||||
})
|
||||
9
app/pages/api/examples/jwt.js
Normal file
9
app/pages/api/examples/jwt.js
Normal file
@@ -0,0 +1,9 @@
|
||||
// This is an example of how to read a JSON Web Token from an API route
|
||||
import jwt from 'next-auth/jwt'
|
||||
|
||||
const secret = process.env.SECRET
|
||||
|
||||
export default async (req, res) => {
|
||||
const token = await jwt.getToken({ req, secret })
|
||||
res.send(JSON.stringify(token, null, 2))
|
||||
}
|
||||
12
app/pages/api/examples/protected.js
Normal file
12
app/pages/api/examples/protected.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// This is an example of to protect an API route
|
||||
import { getSession } from 'next-auth/client'
|
||||
|
||||
export default async (req, res) => {
|
||||
const session = await getSession({ req })
|
||||
|
||||
if (session) {
|
||||
res.send({ content: 'This is protected content. You can access this content because you are signed in.' })
|
||||
} else {
|
||||
res.send({ error: 'You must be sign in to view the protected content on this page.' })
|
||||
}
|
||||
}
|
||||
7
app/pages/api/examples/session.js
Normal file
7
app/pages/api/examples/session.js
Normal file
@@ -0,0 +1,7 @@
|
||||
// This is an example of how to access a session from an API route
|
||||
import { getSession } from 'next-auth/client'
|
||||
|
||||
export default async (req, res) => {
|
||||
const session = await getSession({ req })
|
||||
res.send(JSON.stringify(session, null, 2))
|
||||
}
|
||||
22
app/pages/client.js
Normal file
22
app/pages/client.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import Layout from '../components/layout'
|
||||
|
||||
export default function Page () {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Client Side Rendering</h1>
|
||||
<p>
|
||||
This page uses the <strong>useSession()</strong> React Hook in the <strong></Header></strong> component.
|
||||
</p>
|
||||
<p>
|
||||
The <strong>useSession()</strong> React Hook easy to use and allows pages to render very quickly.
|
||||
</p>
|
||||
<p>
|
||||
The advantage of this approach is that session state is shared between pages by using the <strong>Provider</strong> in <strong>_app.js</strong> so
|
||||
that navigation between pages using <strong>useSession()</strong> is very fast.
|
||||
</p>
|
||||
<p>
|
||||
The disadvantage of <strong>useSession()</strong> is that it requires client side JavaScript.
|
||||
</p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
53
app/pages/credentials.js
Normal file
53
app/pages/credentials.js
Normal file
@@ -0,0 +1,53 @@
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
import * as React from 'react'
|
||||
import { signIn, signOut, useSession } from 'next-auth/client'
|
||||
import Layout from 'components/layout'
|
||||
|
||||
export default function Page () {
|
||||
const [response, setResponse] = React.useState(null)
|
||||
const handleLogin = (options) => async () => {
|
||||
if (options.redirect) {
|
||||
return signIn('credentials', options)
|
||||
}
|
||||
const response = await signIn('credentials', options)
|
||||
setResponse(response)
|
||||
}
|
||||
|
||||
const handleLogout = (options) => async () => {
|
||||
if (options.redirect) {
|
||||
return signOut(options)
|
||||
}
|
||||
const response = await signOut(options)
|
||||
setResponse(response)
|
||||
}
|
||||
|
||||
const [session] = useSession()
|
||||
|
||||
if (session) {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Test different flows for Credentials logout</h1>
|
||||
<span className='spacing'>Default:</span>
|
||||
<button onClick={handleLogout({ redirect: true })}>Logout</button><br />
|
||||
<span className='spacing'>No redirect:</span>
|
||||
<button onClick={handleLogout({ redirect: false })}>Logout</button><br />
|
||||
<p>Response:</p>
|
||||
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Test different flows for Credentials login</h1>
|
||||
<span className='spacing'>Default:</span>
|
||||
<button onClick={handleLogin({ redirect: true, password: 'password' })}>Login</button><br />
|
||||
<span className='spacing'>No redirect:</span>
|
||||
<button onClick={handleLogin({ redirect: false, password: 'password' })}>Login</button><br />
|
||||
<span className='spacing'>No redirect, wrong password:</span>
|
||||
<button onClick={handleLogin({ redirect: false, password: '' })}>Login</button>
|
||||
<p>Response:</p>
|
||||
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
67
app/pages/email.js
Normal file
67
app/pages/email.js
Normal file
@@ -0,0 +1,67 @@
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
import * as React from 'react'
|
||||
import { signIn, signOut, useSession } from 'next-auth/client'
|
||||
import Layout from 'components/layout'
|
||||
|
||||
export default function Page () {
|
||||
const [response, setResponse] = React.useState(null)
|
||||
const [email, setEmail] = React.useState('')
|
||||
|
||||
const handleChange = (event) => {
|
||||
setEmail(event.target.value)
|
||||
}
|
||||
|
||||
const handleLogin = (options) => async (event) => {
|
||||
event.preventDefault()
|
||||
|
||||
if (options.redirect) {
|
||||
return signIn('email', options)
|
||||
}
|
||||
const response = await signIn('email', options)
|
||||
setResponse(response)
|
||||
}
|
||||
|
||||
const handleLogout = (options) => async (event) => {
|
||||
if (options.redirect) {
|
||||
return signOut(options)
|
||||
}
|
||||
const response = await signOut(options)
|
||||
setResponse(response)
|
||||
}
|
||||
|
||||
const [session] = useSession()
|
||||
|
||||
if (session) {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Test different flows for Email logout</h1>
|
||||
<span className='spacing'>Default:</span>
|
||||
<button onClick={handleLogout({ redirect: true })}>Logout</button><br />
|
||||
<span className='spacing'>No redirect:</span>
|
||||
<button onClick={handleLogout({ redirect: false })}>Logout</button><br />
|
||||
<p>Response:</p>
|
||||
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Test different flows for Email login</h1>
|
||||
<label className='spacing'>
|
||||
Email address:{' '}
|
||||
<input type='text' id='email' name='email' value={email} onChange={handleChange} />
|
||||
</label><br />
|
||||
<form onSubmit={handleLogin({ redirect: true, email })}>
|
||||
<span className='spacing'>Default:</span>
|
||||
<button type='submit'>Sign in with Email</button>
|
||||
</form>
|
||||
<form onSubmit={handleLogin({ redirect: false, email })}>
|
||||
<span className='spacing'>No redirect:</span>
|
||||
<button type='submit'>Sign in with Email</button>
|
||||
</form>
|
||||
<p>Response:</p>
|
||||
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
12
app/pages/index.js
Normal file
12
app/pages/index.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import Layout from 'components/layout'
|
||||
|
||||
export default function Page () {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>NextAuth.js Example</h1>
|
||||
<p>
|
||||
This is an example site to demonstrate how to use <a href='https://next-auth.js.org'>NextAuth.js</a> for authentication.
|
||||
</p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
30
app/pages/policy.js
Normal file
30
app/pages/policy.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import Layout from '../components/layout'
|
||||
|
||||
export default function Page () {
|
||||
return (
|
||||
<Layout>
|
||||
<p>
|
||||
This is an example site to demonstrate how to use <a href='https://next-auth.js.org'>NextAuth.js</a> for authentication.
|
||||
</p>
|
||||
<h2>Terms of Service</h2>
|
||||
<p>
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
</p>
|
||||
<h2>Privacy Policy</h2>
|
||||
<p>
|
||||
This site uses JSON Web Tokens and an in-memory database which resets every ~2 hours.
|
||||
</p>
|
||||
<p>
|
||||
Data provided to this site is exclusively used to support signing in
|
||||
and is not passed to any third party services, other than via SMTP or OAuth for the
|
||||
purposes of authentication.
|
||||
</p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
37
app/pages/protected-ssr.js
Normal file
37
app/pages/protected-ssr.js
Normal file
@@ -0,0 +1,37 @@
|
||||
// This is an example of how to protect content using server rendering
|
||||
import { getSession } from 'next-auth/client'
|
||||
import Layout from '../components/layout'
|
||||
import AccessDenied from '../components/access-denied'
|
||||
|
||||
export default function Page ({ content, session }) {
|
||||
// If no session exists, display access denied message
|
||||
if (!session) { return <Layout><AccessDenied /></Layout> }
|
||||
|
||||
// If session exists, display content
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Protected Page</h1>
|
||||
<p><strong>{content}</strong></p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getServerSideProps (context) {
|
||||
const session = await getSession(context)
|
||||
let content = null
|
||||
|
||||
if (session) {
|
||||
const hostname = process.env.NEXTAUTH_URL || 'http://localhost:3000'
|
||||
const options = { headers: { cookie: context.req.headers.cookie } }
|
||||
const res = await fetch(`${hostname}/api/examples/protected`, options)
|
||||
const json = await res.json()
|
||||
if (json.content) { content = json.content }
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
session,
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
33
app/pages/protected.js
Normal file
33
app/pages/protected.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useSession } from 'next-auth/client'
|
||||
import Layout from '../components/layout'
|
||||
import AccessDenied from '../components/access-denied'
|
||||
|
||||
export default function Page () {
|
||||
const [session, loading] = useSession()
|
||||
const [content, setContent] = useState()
|
||||
|
||||
// Fetch content from protected route
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const res = await fetch('/api/examples/protected')
|
||||
const json = await res.json()
|
||||
if (json.content) { setContent(json.content) }
|
||||
}
|
||||
fetchData()
|
||||
}, [session])
|
||||
|
||||
// When rendering client side don't display anything until loading is complete
|
||||
if (typeof window !== 'undefined' && loading) return null
|
||||
|
||||
// If no session exists, display access denied message
|
||||
if (!session) { return <Layout><AccessDenied /></Layout> }
|
||||
|
||||
// If session exists, display content
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Protected Page</h1>
|
||||
<p><strong>{content}</strong></p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
37
app/pages/server.js
Normal file
37
app/pages/server.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { getSession } from 'next-auth/client'
|
||||
import Layout from '../components/layout'
|
||||
|
||||
export default function Page () {
|
||||
// As this page uses Server Side Rendering, the `session` will be already
|
||||
// populated on render without needing to go through a loading stage.
|
||||
// This is possible because of the shared context configured in `_app.js` that
|
||||
// is used by `useSession()`.
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Server Side Rendering</h1>
|
||||
<p>
|
||||
This page uses the universal <strong>getSession()</strong> method in <strong>getServerSideProps()</strong>.
|
||||
</p>
|
||||
<p>
|
||||
Using <strong>getSession()</strong> in <strong>getServerSideProps()</strong> is the recommended approach if you need to
|
||||
support Server Side Rendering with authentication.
|
||||
</p>
|
||||
<p>
|
||||
The advantage of Server Side Rendering is this page does not require client side JavaScript.
|
||||
</p>
|
||||
<p>
|
||||
The disadvantage of Server Side Rendering is that this page is slower to render.
|
||||
</p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
// Export the `session` prop to use sessions with Server Side Rendering
|
||||
export async function getServerSideProps (context) {
|
||||
return {
|
||||
props: {
|
||||
session: await getSession(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
30
app/pages/styles.css
Normal file
30
app/pages/styles.css
Normal file
@@ -0,0 +1,30 @@
|
||||
body {
|
||||
font-family: -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
padding: 0 1rem 1rem 1rem;
|
||||
max-width: 680px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
li,
|
||||
p {
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
iframe {
|
||||
background: #ccc;
|
||||
border: 1px solid #ccc;
|
||||
height: 10rem;
|
||||
width: 100%;
|
||||
border-radius: .5rem;
|
||||
filter: invert(1);
|
||||
}
|
||||
63
app/prisma/schema.prisma
Normal file
63
app/prisma/schema.prisma
Normal file
@@ -0,0 +1,63 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Account {
|
||||
id Int @default(autoincrement()) @id
|
||||
compoundId String @unique @map(name: "compound_id")
|
||||
userId Int @map(name: "user_id")
|
||||
providerType String @map(name: "provider_type")
|
||||
providerId String @map(name: "provider_id")
|
||||
providerAccountId String @map(name: "provider_account_id")
|
||||
refreshToken String? @map(name: "refresh_token")
|
||||
accessToken String? @map(name: "access_token")
|
||||
accessTokenExpires DateTime? @map(name: "access_token_expires")
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
|
||||
@@index([providerAccountId], name: "providerAccountId")
|
||||
@@index([providerId], name: "providerId")
|
||||
@@index([userId], name: "userId")
|
||||
|
||||
@@map(name: "accounts")
|
||||
}
|
||||
|
||||
model Session {
|
||||
id Int @default(autoincrement()) @id
|
||||
userId Int @map(name: "user_id")
|
||||
expires DateTime
|
||||
sessionToken String @unique @map(name: "session_token")
|
||||
accessToken String @unique @map(name: "access_token")
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
|
||||
@@map(name: "sessions")
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @default(autoincrement()) @id
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime? @map(name: "email_verified")
|
||||
image String?
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
|
||||
@@map(name: "users")
|
||||
}
|
||||
|
||||
model VerificationRequest {
|
||||
id Int @default(autoincrement()) @id
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
|
||||
@@map(name: "verification_requests")
|
||||
}
|
||||
52
client.d.ts
vendored
52
client.d.ts
vendored
@@ -1,52 +0,0 @@
|
||||
/// <reference path="./index.d.ts" />
|
||||
import { Store } from "express-session";
|
||||
import { RequestHandler } from "express";
|
||||
import { IpcNetConnectOpts } from "net";
|
||||
|
||||
declare namespace nextAuth {
|
||||
interface ILinkedAccounts {
|
||||
[name: string]: boolean;
|
||||
}
|
||||
interface IProviders {
|
||||
[name: string]: {
|
||||
signin: string;
|
||||
callback: string;
|
||||
};
|
||||
}
|
||||
interface NextAuthRequest<SessionType = nextAuth.NextAuthSession>
|
||||
extends Express.Request {
|
||||
session: NextAuthSession;
|
||||
linked: () => Promise<ILinkedAccounts | Error>;
|
||||
providers: () => Promise<IProviders | Error>;
|
||||
}
|
||||
interface IInitOptions {
|
||||
req?: NextAuthRequest;
|
||||
force: boolean;
|
||||
}
|
||||
interface IClientOptions {
|
||||
req?: NextAuthRequest;
|
||||
}
|
||||
}
|
||||
|
||||
declare class Client {
|
||||
static init(
|
||||
opts: nextAuth.IInitOptions
|
||||
): Promise<Partial<nextAuth.INextAuthSessionData>>;
|
||||
static csrfToken(): Promise<string | Error>;
|
||||
static linked(
|
||||
opts: nextAuth.IClientOptions
|
||||
): Promise<nextAuth.ILinkedAccounts | Error>;
|
||||
static providers(
|
||||
opts: nextAuth.IClientOptions
|
||||
): Promise<nextAuth.IProviders | Error>;
|
||||
static signin(
|
||||
params: string | { [k: string]: string }
|
||||
): Promise<boolean | Error>;
|
||||
static signout(): Promise<true | Error>;
|
||||
|
||||
static _getLocalStore(): Promise<nextAuth.INextAuthSessionData | null>;
|
||||
static _saveLocalStore(): Promise<boolean>;
|
||||
static _removeLocalStore(): Promise<boolean>;
|
||||
}
|
||||
|
||||
export = Client;
|
||||
490
client.js
490
client.js
@@ -1,490 +0,0 @@
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('babel-polyfill'), require('isomorphic-fetch')) :
|
||||
typeof define === 'function' && define.amd ? define(['exports', 'babel-polyfill', 'isomorphic-fetch'], factory) :
|
||||
(factory((global['next-auth-client'] = {}),null,global.fetch));
|
||||
}(this, (function (exports,babelPolyfill,fetch) { 'use strict';
|
||||
|
||||
fetch = fetch && fetch.hasOwnProperty('default') ? fetch['default'] : fetch;
|
||||
|
||||
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
|
||||
|
||||
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
|
||||
|
||||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
||||
|
||||
var _class = function () {
|
||||
function _class() {
|
||||
_classCallCheck(this, _class);
|
||||
}
|
||||
|
||||
_createClass(_class, null, [{
|
||||
key: 'init',
|
||||
|
||||
/**
|
||||
* This is an async, isometric method which returns a session object -
|
||||
* either by looking up the current express session object when run on the
|
||||
* server, or by using fetch (and optionally caching the result in local
|
||||
* storage) when run on the client.
|
||||
*
|
||||
* Note that actual session tokens are not stored in local storage, they are
|
||||
* kept in an HTTP Only cookie as protection against session hi-jacking by
|
||||
* malicious JavaScript.
|
||||
**/
|
||||
value: function () {
|
||||
var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
|
||||
var _this = this;
|
||||
|
||||
var _ref2 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
|
||||
_ref2$req = _ref2.req,
|
||||
req = _ref2$req === undefined ? null : _ref2$req,
|
||||
_ref2$force = _ref2.force,
|
||||
force = _ref2$force === undefined ? false : _ref2$force;
|
||||
|
||||
var session;
|
||||
return regeneratorRuntime.wrap(function _callee$(_context) {
|
||||
while (1) {
|
||||
switch (_context.prev = _context.next) {
|
||||
case 0:
|
||||
session = {};
|
||||
|
||||
if (req) {
|
||||
if (req.session) {
|
||||
// If running on the server session data should be in the req object
|
||||
session.csrfToken = req.connection._httpMessage.locals._csrf;
|
||||
session.expires = req.session.cookie._expires;
|
||||
// If the user is logged in, add the user to the session object
|
||||
if (req.user) {
|
||||
session.user = req.user;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If running in the browser attempt to load session from sessionStore
|
||||
if (force === true) {
|
||||
// If force update is set, reset data store
|
||||
this._removeLocalStore('session');
|
||||
} else {
|
||||
session = this._getLocalStore('session');
|
||||
}
|
||||
}
|
||||
|
||||
// If session data exists, has not expired AND force is not set then
|
||||
// return the stored session we already have.
|
||||
|
||||
if (!(session && Object.keys(session).length > 0 && session.expires && session.expires > Date.now())) {
|
||||
_context.next = 6;
|
||||
break;
|
||||
}
|
||||
|
||||
return _context.abrupt('return', new Promise(function (resolve) {
|
||||
resolve(session);
|
||||
}));
|
||||
|
||||
case 6:
|
||||
if (!(typeof window === 'undefined')) {
|
||||
_context.next = 8;
|
||||
break;
|
||||
}
|
||||
|
||||
return _context.abrupt('return', new Promise(function (resolve) {
|
||||
resolve({});
|
||||
}));
|
||||
|
||||
case 8:
|
||||
return _context.abrupt('return', fetch('/auth/session', {
|
||||
credentials: 'same-origin'
|
||||
}).then(function (response) {
|
||||
if (response.ok) {
|
||||
return response;
|
||||
} else {
|
||||
return Promise.reject(Error('HTTP error when trying to get session'));
|
||||
}
|
||||
}).then(function (response) {
|
||||
return response.json();
|
||||
}).then(function (data) {
|
||||
// Update session with session info
|
||||
session = data;
|
||||
|
||||
// Set a value we will use to check this client should silently
|
||||
// revalidate, using the value for revalidateAge returned by the server.
|
||||
session.expires = Date.now() + session.revalidateAge;
|
||||
|
||||
// Save changes to session
|
||||
_this._saveLocalStore('session', session);
|
||||
|
||||
return session;
|
||||
}).catch(function () {
|
||||
return Error('Unable to get session');
|
||||
}));
|
||||
|
||||
case 9:
|
||||
case 'end':
|
||||
return _context.stop();
|
||||
}
|
||||
}
|
||||
}, _callee, this);
|
||||
}));
|
||||
|
||||
function init() {
|
||||
return _ref.apply(this, arguments);
|
||||
}
|
||||
|
||||
return init;
|
||||
}()
|
||||
|
||||
/**
|
||||
* A simple static method to get the CSRF Token is provided for convenience
|
||||
**/
|
||||
|
||||
}, {
|
||||
key: 'csrfToken',
|
||||
value: function () {
|
||||
var _ref3 = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee2() {
|
||||
return regeneratorRuntime.wrap(function _callee2$(_context2) {
|
||||
while (1) {
|
||||
switch (_context2.prev = _context2.next) {
|
||||
case 0:
|
||||
return _context2.abrupt('return', fetch('/auth/csrf', {
|
||||
credentials: 'same-origin'
|
||||
}).then(function (response) {
|
||||
if (response.ok) {
|
||||
return response;
|
||||
} else {
|
||||
return Promise.reject(Error('Unexpected response when trying to get CSRF token'));
|
||||
}
|
||||
}).then(function (response) {
|
||||
return response.json();
|
||||
}).then(function (data) {
|
||||
return data.csrfToken;
|
||||
}).catch(function () {
|
||||
return Error('Unable to get CSRF token');
|
||||
}));
|
||||
|
||||
case 1:
|
||||
case 'end':
|
||||
return _context2.stop();
|
||||
}
|
||||
}
|
||||
}, _callee2, this);
|
||||
}));
|
||||
|
||||
function csrfToken() {
|
||||
return _ref3.apply(this, arguments);
|
||||
}
|
||||
|
||||
return csrfToken;
|
||||
}()
|
||||
|
||||
/**
|
||||
* A static method to get list of currently linked oAuth accounts
|
||||
**/
|
||||
|
||||
}, {
|
||||
key: 'linked',
|
||||
value: function () {
|
||||
var _ref4 = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee3() {
|
||||
var _ref5 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
|
||||
_ref5$req = _ref5.req,
|
||||
req = _ref5$req === undefined ? null : _ref5$req;
|
||||
|
||||
return regeneratorRuntime.wrap(function _callee3$(_context3) {
|
||||
while (1) {
|
||||
switch (_context3.prev = _context3.next) {
|
||||
case 0:
|
||||
if (!req) {
|
||||
_context3.next = 2;
|
||||
break;
|
||||
}
|
||||
|
||||
return _context3.abrupt('return', req.linked());
|
||||
|
||||
case 2:
|
||||
return _context3.abrupt('return', fetch('/auth/linked', {
|
||||
credentials: 'same-origin'
|
||||
}).then(function (response) {
|
||||
if (response.ok) {
|
||||
return response;
|
||||
} else {
|
||||
return Promise.reject(Error('Unexpected response when trying to get linked accounts'));
|
||||
}
|
||||
}).then(function (response) {
|
||||
return response.json();
|
||||
}).then(function (data) {
|
||||
return data;
|
||||
}).catch(function () {
|
||||
return Error('Unable to get linked accounts');
|
||||
}));
|
||||
|
||||
case 3:
|
||||
case 'end':
|
||||
return _context3.stop();
|
||||
}
|
||||
}
|
||||
}, _callee3, this);
|
||||
}));
|
||||
|
||||
function linked() {
|
||||
return _ref4.apply(this, arguments);
|
||||
}
|
||||
|
||||
return linked;
|
||||
}()
|
||||
|
||||
/**
|
||||
* A static method to get list of currently configured oAuth providers
|
||||
**/
|
||||
|
||||
}, {
|
||||
key: 'providers',
|
||||
value: function () {
|
||||
var _ref6 = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee4() {
|
||||
var _ref7 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
|
||||
_ref7$req = _ref7.req,
|
||||
req = _ref7$req === undefined ? null : _ref7$req;
|
||||
|
||||
return regeneratorRuntime.wrap(function _callee4$(_context4) {
|
||||
while (1) {
|
||||
switch (_context4.prev = _context4.next) {
|
||||
case 0:
|
||||
if (!req) {
|
||||
_context4.next = 2;
|
||||
break;
|
||||
}
|
||||
|
||||
return _context4.abrupt('return', req.providers());
|
||||
|
||||
case 2:
|
||||
return _context4.abrupt('return', fetch('/auth/providers', {
|
||||
credentials: 'same-origin'
|
||||
}).then(function (response) {
|
||||
if (response.ok) {
|
||||
return response;
|
||||
} else {
|
||||
console.log("NextAuth Error Fetching Providers");
|
||||
return null;
|
||||
}
|
||||
}).then(function (response) {
|
||||
return response.json();
|
||||
}).then(function (data) {
|
||||
return data;
|
||||
}).catch(function (e) {
|
||||
console.log("NextAuth Error Loading Providers");
|
||||
console.log(e);
|
||||
return null;
|
||||
}));
|
||||
|
||||
case 3:
|
||||
case 'end':
|
||||
return _context4.stop();
|
||||
}
|
||||
}
|
||||
}, _callee4, this);
|
||||
}));
|
||||
|
||||
function providers() {
|
||||
return _ref6.apply(this, arguments);
|
||||
}
|
||||
|
||||
return providers;
|
||||
}()
|
||||
|
||||
/*
|
||||
* Sign in
|
||||
*
|
||||
* Will post a form to /auth/signin auth route if an object is passed.
|
||||
* If the details are valid a session will be created and you should redirect
|
||||
* to your callback page so the session is loaded in the client.
|
||||
*
|
||||
* If just a string containing an email address is specififed will generate a
|
||||
* a one-time use sign in link and send it via email; you should redirect to a
|
||||
* page telling the user to check their inbox for an email with the link.
|
||||
*/
|
||||
|
||||
}, {
|
||||
key: 'signin',
|
||||
value: function () {
|
||||
var _ref8 = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee6(params) {
|
||||
var _this2 = this;
|
||||
|
||||
var formData, route, encodedForm;
|
||||
return regeneratorRuntime.wrap(function _callee6$(_context6) {
|
||||
while (1) {
|
||||
switch (_context6.prev = _context6.next) {
|
||||
case 0:
|
||||
// Params can be just string (an email address) or an object (form fields)
|
||||
formData = typeof params === 'string' ? { email: params } : params;
|
||||
|
||||
// Use either the email token generation route or the custom form auth route
|
||||
|
||||
route = typeof params === 'string' ? '/auth/email/signin' : '/auth/signin';
|
||||
|
||||
// Add latest CSRF Token to request
|
||||
|
||||
_context6.next = 4;
|
||||
return this.csrfToken();
|
||||
|
||||
case 4:
|
||||
formData._csrf = _context6.sent;
|
||||
|
||||
|
||||
// Encoded form parser for sending data in the body
|
||||
encodedForm = Object.keys(formData).map(function (key) {
|
||||
return encodeURIComponent(key) + '=' + encodeURIComponent(formData[key]);
|
||||
}).join('&');
|
||||
return _context6.abrupt('return', fetch(route, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest' // So Express can detect AJAX post
|
||||
},
|
||||
body: encodedForm,
|
||||
credentials: 'same-origin'
|
||||
}).then(function () {
|
||||
var _ref9 = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee5(response) {
|
||||
return regeneratorRuntime.wrap(function _callee5$(_context5) {
|
||||
while (1) {
|
||||
switch (_context5.prev = _context5.next) {
|
||||
case 0:
|
||||
if (!response.ok) {
|
||||
_context5.next = 6;
|
||||
break;
|
||||
}
|
||||
|
||||
_context5.next = 3;
|
||||
return response.json();
|
||||
|
||||
case 3:
|
||||
return _context5.abrupt('return', _context5.sent);
|
||||
|
||||
case 6:
|
||||
throw new Error('HTTP error while attempting to sign in');
|
||||
|
||||
case 7:
|
||||
case 'end':
|
||||
return _context5.stop();
|
||||
}
|
||||
}
|
||||
}, _callee5, _this2);
|
||||
}));
|
||||
|
||||
return function (_x5) {
|
||||
return _ref9.apply(this, arguments);
|
||||
};
|
||||
}()).then(function (data) {
|
||||
if (data.success && data.success === true) {
|
||||
return Promise.resolve(true);
|
||||
} else {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
}));
|
||||
|
||||
case 7:
|
||||
case 'end':
|
||||
return _context6.stop();
|
||||
}
|
||||
}
|
||||
}, _callee6, this);
|
||||
}));
|
||||
|
||||
function signin(_x4) {
|
||||
return _ref8.apply(this, arguments);
|
||||
}
|
||||
|
||||
return signin;
|
||||
}()
|
||||
}, {
|
||||
key: 'signout',
|
||||
value: function () {
|
||||
var _ref10 = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee7() {
|
||||
var csrfToken, formData, encodedForm;
|
||||
return regeneratorRuntime.wrap(function _callee7$(_context7) {
|
||||
while (1) {
|
||||
switch (_context7.prev = _context7.next) {
|
||||
case 0:
|
||||
_context7.next = 2;
|
||||
return this.csrfToken();
|
||||
|
||||
case 2:
|
||||
csrfToken = _context7.sent;
|
||||
formData = { _csrf: csrfToken
|
||||
|
||||
// Encoded form parser for sending data in the body
|
||||
};
|
||||
encodedForm = Object.keys(formData).map(function (key) {
|
||||
return encodeURIComponent(key) + '=' + encodeURIComponent(formData[key]);
|
||||
}).join('&');
|
||||
|
||||
// Remove cached session data
|
||||
|
||||
this._removeLocalStore('session');
|
||||
|
||||
return _context7.abrupt('return', fetch('/auth/signout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: encodedForm,
|
||||
credentials: 'same-origin'
|
||||
}).then(function () {
|
||||
return true;
|
||||
}).catch(function () {
|
||||
return Error('Unable to sign out');
|
||||
}));
|
||||
|
||||
case 7:
|
||||
case 'end':
|
||||
return _context7.stop();
|
||||
}
|
||||
}
|
||||
}, _callee7, this);
|
||||
}));
|
||||
|
||||
function signout() {
|
||||
return _ref10.apply(this, arguments);
|
||||
}
|
||||
|
||||
return signout;
|
||||
}()
|
||||
|
||||
// The Web Storage API is widely supported, but not always available (e.g.
|
||||
// it can be restricted in private browsing mode, triggering an exception).
|
||||
// We handle that silently by just returning null here.
|
||||
|
||||
}, {
|
||||
key: '_getLocalStore',
|
||||
value: function _getLocalStore(name) {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(name));
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: '_saveLocalStore',
|
||||
value: function _saveLocalStore(name, data) {
|
||||
try {
|
||||
localStorage.setItem(name, JSON.stringify(data));
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: '_removeLocalStore',
|
||||
value: function _removeLocalStore(name) {
|
||||
try {
|
||||
localStorage.removeItem(name);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}]);
|
||||
|
||||
return _class;
|
||||
}();
|
||||
|
||||
exports.NextAuth = _class;
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
|
||||
})));
|
||||
15
config/babel.config.json
Normal file
15
config/babel.config.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"presets": [
|
||||
["@babel/preset-env", { "targets": { "esmodules": true } }]
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties"
|
||||
],
|
||||
"comments": false,
|
||||
"overrides": [
|
||||
{
|
||||
"test": ["../src/server/pages/**"],
|
||||
"presets": ["preact"]
|
||||
}
|
||||
]
|
||||
}
|
||||
91
config/build.js
Normal file
91
config/build.js
Normal file
@@ -0,0 +1,91 @@
|
||||
const fs = require("fs-extra")
|
||||
const path = require("path")
|
||||
|
||||
const MODULE_ENTRIES = {
|
||||
SERVER: "index",
|
||||
CLIENT: "client",
|
||||
PROVIDERS: "providers",
|
||||
ADAPTERS: "adapters",
|
||||
JWT: "jwt",
|
||||
ERRORS: "errors",
|
||||
}
|
||||
|
||||
// Building submodule entries
|
||||
|
||||
const BUILD_TARGETS = {
|
||||
[`${MODULE_ENTRIES.SERVER}.js`]: "module.exports = require('./dist/server').default\n",
|
||||
[`${MODULE_ENTRIES.CLIENT}.js`]: "module.exports = require('./dist/client').default\n",
|
||||
[`${MODULE_ENTRIES.ADAPTERS}.js`]: "module.exports = require('./dist/adapters').default\n",
|
||||
[`${MODULE_ENTRIES.PROVIDERS}.js`]: "module.exports = require('./dist/providers').default\n",
|
||||
[`${MODULE_ENTRIES.JWT}.js`]: "module.exports = require('./dist/lib/jwt').default\n",
|
||||
[`${MODULE_ENTRIES.ERRORS}.js`]: "module.exports = require('./dist/lib/errors').default\n",
|
||||
}
|
||||
|
||||
Object.entries(BUILD_TARGETS).forEach(([target, content]) => {
|
||||
fs.writeFile(path.join(process.cwd(), target), content, (err) => {
|
||||
if (err) throw err
|
||||
console.log(`[build] created "${target}" in root folder`)
|
||||
})
|
||||
})
|
||||
|
||||
// Building types
|
||||
|
||||
const TYPES_TARGETS = [
|
||||
`${MODULE_ENTRIES.SERVER}.d.ts`,
|
||||
`${MODULE_ENTRIES.CLIENT}.d.ts`,
|
||||
`${MODULE_ENTRIES.ADAPTERS}.d.ts`,
|
||||
`${MODULE_ENTRIES.PROVIDERS}.d.ts`,
|
||||
`${MODULE_ENTRIES.JWT}.d.ts`,
|
||||
`${MODULE_ENTRIES.ERRORS}.d.ts`,
|
||||
"internals",
|
||||
]
|
||||
|
||||
TYPES_TARGETS.forEach((target) => {
|
||||
fs.copy(
|
||||
path.resolve("types", target),
|
||||
path.join(process.cwd(), target),
|
||||
(err) => {
|
||||
if (err) throw err
|
||||
console.log(`[build-types] copying "${target}" to root folder`)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
// Building providers
|
||||
|
||||
const providersDir = path.join(process.cwd(), "/src/providers")
|
||||
|
||||
const files = fs
|
||||
.readdirSync(providersDir, "utf8")
|
||||
.filter((file) => file !== "index.js")
|
||||
|
||||
let importLines = ""
|
||||
let exportLines = `export default {\n`
|
||||
files.forEach((file) => {
|
||||
const provider = fs.readFileSync(path.join(providersDir, file), "utf8")
|
||||
try {
|
||||
// NOTE: If this fails, the default export probably wasn't a named function.
|
||||
// Always use a named function as default export.
|
||||
// Eg.: export default function YourProvider ...
|
||||
const { functionName } = provider.match(
|
||||
/export default function (?<functionName>.+)\s?\(/
|
||||
).groups
|
||||
|
||||
importLines += `import ${functionName} from "./${file}"\n`
|
||||
exportLines += ` ${functionName},\n`
|
||||
} catch (error) {
|
||||
console.error(
|
||||
[
|
||||
`\nThe provider file '${file}' should have a single named default export`,
|
||||
"Example: 'export default function YourProvider'\n\n",
|
||||
].join("\n")
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
exportLines += `}\n`
|
||||
|
||||
fs.writeFile(
|
||||
path.join(process.cwd(), "src/providers/index.js"),
|
||||
[importLines, exportLines].join("\n")
|
||||
)
|
||||
7
config/postcss.config.js
Normal file
7
config/postcss.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('autoprefixer'),
|
||||
require('postcss-nested'),
|
||||
require('cssnano')({ preset: 'default' })
|
||||
]
|
||||
}
|
||||
18
config/wrap-css.js
Normal file
18
config/wrap-css.js
Normal file
@@ -0,0 +1,18 @@
|
||||
// Serverless target in Next.js does not work if you try to read in files at runtime
|
||||
// that are not JavaScript or JSON (e.g. CSS files).
|
||||
// https://github.com/nextauthjs/next-auth/issues/281
|
||||
//
|
||||
// To work around this issue, this script is a manual step that wraps CSS in a
|
||||
// JavaScript file that has the compiled CSS embedded in it, and exports only
|
||||
// a function that returns the CSS as a string.
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const pathToCssJs = path.join(__dirname, '../dist/css/index.js')
|
||||
const pathToCss = path.join(__dirname, '../dist/css/index.css')
|
||||
|
||||
const css = fs.readFileSync(pathToCss, 'utf8')
|
||||
const cssWithEscapedQuotes = css.replace(/"/gm, '\\"')
|
||||
const js = `module.exports = function() { return "${cssWithEscapedQuotes}" }`
|
||||
|
||||
fs.writeFileSync(pathToCssJs, js)
|
||||
@@ -1,13 +0,0 @@
|
||||
SERVER_URL=http://localhost:3000
|
||||
MONGO_URI=mongodb://localhost:27017/my-database
|
||||
FACEBOOK_ID=
|
||||
FACEBOOK_SECRET=
|
||||
GOOGLE_ID=
|
||||
GOOGLE_SECRET=
|
||||
TWITTER_KEY=
|
||||
TWITTER_SECRET=
|
||||
EMAIL_FROM=username@gmail.com
|
||||
EMAIL_SERVER=smtp.gmail.com
|
||||
EMAIL_PORT=465
|
||||
EMAIL_USERNAME=username@gmail.com
|
||||
EMAIL_PASSWORD=
|
||||
3
example/.gitignore
vendored
3
example/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
.env
|
||||
/.env.production
|
||||
node_modules
|
||||
@@ -1,68 +0,0 @@
|
||||
# NextAuth Example
|
||||
|
||||
## About NextAuth Example
|
||||
|
||||
This is an example of how to use the [NextAuth](https://www.npmjs.com/package/next-auth) module.
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project as is run the same way as any Next.js project.
|
||||
|
||||
To run it locally, just use:
|
||||
|
||||
npm run dev
|
||||
|
||||
To run it it production mode, use:
|
||||
|
||||
npm build
|
||||
npm start
|
||||
|
||||
## Using NextAuth
|
||||
|
||||
NextAuth is included in this project here:
|
||||
|
||||
* index.js
|
||||
|
||||
## Pages
|
||||
|
||||
This example includes the following pages:
|
||||
|
||||
* pages/index.js
|
||||
* pages/auth/index.js
|
||||
* pages/auth/error.js
|
||||
* pages/auth/check-email.js
|
||||
* pages/auth/callback.js
|
||||
|
||||
The file `pages/auth/credentials.js` provides an additional example of how to use a custom authentication handler defined in `next-auth.functions.js`.
|
||||
|
||||
## Configuration
|
||||
|
||||
It also includes the following configuration files:
|
||||
|
||||
* next-auth.config.js
|
||||
* next-auth.functions.js
|
||||
* next-auth.providers.js
|
||||
|
||||
An example **.env** file is provided in **.env.example** which you can copy over to use for simple configuration:
|
||||
|
||||
````
|
||||
SERVER_URL=http://localhost:3000
|
||||
MONGO_URI=mongodb://localhost:27017/my-database
|
||||
FACEBOOK_ID=
|
||||
FACEBOOK_SECRET=
|
||||
GOOGLE_ID=
|
||||
GOOGLE_SECRET=
|
||||
TWITTER_KEY=
|
||||
TWITTER_SECRET=
|
||||
EMAIL_FROM=username@gmail.com
|
||||
EMAIL_SERVER=smtp.gmail.com
|
||||
EMAIL_PORT=465
|
||||
EMAIL_USERNAME=username@gmail.com
|
||||
EMAIL_PASSWORD=
|
||||
````
|
||||
|
||||
If you don't specify a MONGO_URI it will use an in-memory data store for user and session data.
|
||||
|
||||
If you don't specify oAuth or SMTP email details you will not be able to log in.
|
||||
|
||||
For a more complete example with live demo see [nextjs-starter.now.sh](https://nextjs-starter.now.sh/examples/authentication).
|
||||
@@ -1,40 +0,0 @@
|
||||
/**
|
||||
* An example of how to use the NextAuth module.
|
||||
*
|
||||
* To invoke next-auth you will need to define a configuration block for your
|
||||
* site. You can create a next-auth.config.js file and put all your options
|
||||
* in it and pass it to next-auth when calling init().
|
||||
*
|
||||
* A number of sample configuration files for various databases and
|
||||
* authentication options are provided.
|
||||
**/
|
||||
|
||||
// Include Next.js, Next Auth and a Next Auth config
|
||||
const next = require('next')
|
||||
const nextAuth = require('next-auth')
|
||||
const nextAuthConfig = require('./next-auth.config')
|
||||
|
||||
// Load environment variables from .env
|
||||
require('dotenv').config({ path: './.env' })
|
||||
|
||||
// Initialize Next.js
|
||||
const nextApp = next({
|
||||
dir: '.',
|
||||
dev: (process.env.NODE_ENV === 'development')
|
||||
})
|
||||
|
||||
// Add next-auth to next app
|
||||
nextApp.prepare()
|
||||
.then(async () => {
|
||||
// Load configuration and return config object
|
||||
const nextAuthOptions = await nextAuthConfig()
|
||||
|
||||
// Pass Next.js App instance and NextAuth options to NextAuth
|
||||
const nextAuthApp = await nextAuth(nextApp, nextAuthOptions)
|
||||
|
||||
console.log(`Ready on http://localhost:${process.env.PORT || 3000}`)
|
||||
})
|
||||
.catch(err => {
|
||||
console.log('An error occurred, unable to start the server')
|
||||
console.log(err)
|
||||
})
|
||||
@@ -1,80 +0,0 @@
|
||||
/**
|
||||
* next-auth.config.js Example
|
||||
*
|
||||
* Environment variables for this example:
|
||||
*
|
||||
* PORT=3000
|
||||
* SERVER_URL=http://localhost:3000
|
||||
* MONGO_URI=mongodb://localhost:27017/my-database
|
||||
*
|
||||
* If you wish, you can put these in a `.env` to seperate your environment
|
||||
* specific configuration from your code.
|
||||
**/
|
||||
|
||||
// Load environment variables from a .env file if one exists
|
||||
require('dotenv').config({ path: './.env' })
|
||||
|
||||
const nextAuthProviders = require('./next-auth.providers')
|
||||
const nextAuthFunctions = require('./next-auth.functions')
|
||||
|
||||
// If we want to pass a custom session store then we also need to pass an
|
||||
// instance of Express Session along with it.
|
||||
const expressSession = require('express-session')
|
||||
const MongoStore = require('connect-mongo')(expressSession)
|
||||
|
||||
// If no store set, NextAuth defaults to using Express Sessions in-memory
|
||||
// session store (the fallback is intended as fallback for testing only).
|
||||
let sessionStore
|
||||
if (process.env.MONGO_URI) {
|
||||
sessionStore = new MongoStore({
|
||||
url: process.env.MONGO_URI,
|
||||
autoRemove: 'interval',
|
||||
autoRemoveInterval: 10, // Removes expired sessions every 10 minutes
|
||||
collection: 'sessions',
|
||||
stringify: false
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = () => {
|
||||
// We connect to the User DB before we define our functions.
|
||||
// next-auth.functions.js returns an async method that does that and returns
|
||||
// an object with the functions needed for authentication.
|
||||
return nextAuthFunctions()
|
||||
.then(functions => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// This is the config block we return, ready to be passed to NextAuth
|
||||
resolve({
|
||||
// Define a port (if none passed, will not start Express)
|
||||
port: process.env.PORT || 3000,
|
||||
// Secret used to encrypt session data on the server.
|
||||
sessionSecret: 'change-me',
|
||||
// Maximum Session Age in ms (optional, default is 7 days).
|
||||
// The expiry time for a session is reset every time a user revisits
|
||||
// the site or revalidates their session token. This is the maximum
|
||||
// idle time value.
|
||||
sessionMaxAge: 60000 * 60 * 24 * 7,
|
||||
// Session Revalidation in X ms (optional, default is 60 seconds).
|
||||
// Specifies how often a Single Page App should revalidate a session.
|
||||
// Does not impact the session life on the server, but causes clients
|
||||
// to refetch session info (even if it is in a local cache) after N
|
||||
// seconds has elapsed since it was last checked so they always display
|
||||
// state correctly.
|
||||
// If set to 0 will revalidate a session before rendering every page.
|
||||
sessionRevalidateAge: 60000,
|
||||
// Canonical URL of the server (optional, but recommended).
|
||||
// e.g. 'http://localhost:3000' or 'https://www.example.com'
|
||||
// Used in callbak URLs and email sign in links. It will be auto
|
||||
// generated if not specified, which may cause problems if your site
|
||||
// uses multiple aliases (e.g. 'example.com and 'www.examples.com').
|
||||
serverUrl: process.env.SERVER_URL || null,
|
||||
// Add an Express Session store.
|
||||
expressSession: expressSession,
|
||||
sessionStore: sessionStore,
|
||||
// Define oAuth Providers
|
||||
providers: nextAuthProviders(),
|
||||
// Define functions for manging users and sending email.
|
||||
functions: functions
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
/**
|
||||
* next-auth.functions.js Example
|
||||
*
|
||||
* This file defines functions NextAuth to look up, add and update users.
|
||||
*
|
||||
* It returns a Promise with the functions matching these signatures:
|
||||
*
|
||||
* {
|
||||
* find: ({
|
||||
* id,
|
||||
* email,
|
||||
* emailToken,
|
||||
* provider,
|
||||
* poviderToken
|
||||
* } = {}) => {},
|
||||
* update: (user) => {},
|
||||
* insert: (user) => {},
|
||||
* remove: (id) => {},
|
||||
* serialize: (user) => {},
|
||||
* deserialize: (id) => {}
|
||||
* }
|
||||
*
|
||||
* Each function returns Promise.resolve() - or Promise.reject() on error.
|
||||
*
|
||||
* This specific example supports both MongoDB and NeDB, but can be refactored
|
||||
* to work with any database.
|
||||
*
|
||||
* Environment variables for this example:
|
||||
*
|
||||
* MONGO_URI=mongodb://localhost:27017/my-database
|
||||
* EMAIL_FROM=username@gmail.com
|
||||
* EMAIL_SERVER=smtp.gmail.com
|
||||
* EMAIL_PORT=465
|
||||
* EMAIL_USERNAME=username@gmail.com
|
||||
* EMAIL_PASSWORD=p4ssw0rd
|
||||
*
|
||||
* If you wish, you can put these in a `.env` to seperate your environment
|
||||
* specific configuration from your code.
|
||||
**/
|
||||
|
||||
// Load environment variables from a .env file if one exists
|
||||
require('dotenv').config({ path: './.env' })
|
||||
|
||||
// This config file uses MongoDB for User accounts, as well as session storage.
|
||||
// This config includes options for NeDB, which it defaults to if no DB URI
|
||||
// is specified. NeDB is an in-memory only database intended here for testing.
|
||||
const MongoClient = require('mongodb').MongoClient
|
||||
const NeDB = require('nedb')
|
||||
const MongoObjectId = (process.env.MONGO_URI) ? require('mongodb').ObjectId : (id) => { return id }
|
||||
|
||||
// Use Node Mailer for email sign in
|
||||
const nodemailer = require('nodemailer')
|
||||
const nodemailerSmtpTransport = require('nodemailer-smtp-transport')
|
||||
const nodemailerDirectTransport = require('nodemailer-direct-transport')
|
||||
|
||||
// Send email direct from localhost if no mail server configured
|
||||
let nodemailerTransport = nodemailerDirectTransport()
|
||||
if (process.env.EMAIL_SERVER && process.env.EMAIL_USERNAME && process.env.EMAIL_PASSWORD) {
|
||||
nodemailerTransport = nodemailerSmtpTransport({
|
||||
host: process.env.EMAIL_SERVER,
|
||||
port: process.env.EMAIL_PORT || 25,
|
||||
secure: process.env.EMAIL_SECURE || true,
|
||||
auth: {
|
||||
user: process.env.EMAIL_USERNAME,
|
||||
pass: process.env.EMAIL_PASSWORD
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (process.env.MONGO_URI) {
|
||||
// Connect to MongoDB Database and return user connection
|
||||
MongoClient.connect(process.env.MONGO_URI, (err, mongoClient) => {
|
||||
if (err) return reject(err)
|
||||
const dbName = process.env.MONGO_URI.split('/').pop().split('?').shift()
|
||||
const db = mongoClient.db(dbName)
|
||||
return resolve(db.collection('users'))
|
||||
})
|
||||
} else {
|
||||
// If no MongoDB URI string specified, use NeDB, an in-memory work-a-like.
|
||||
// NeDB is not persistant and is intended for testing only.
|
||||
let collection = new NeDB({ autoload: true })
|
||||
collection.loadDatabase(err => {
|
||||
if (err) return reject(err)
|
||||
resolve(collection)
|
||||
})
|
||||
}
|
||||
})
|
||||
.then(usersCollection => {
|
||||
return Promise.resolve({
|
||||
// If a user is not found find() should return null (with no error).
|
||||
find: ({id, email, emailToken, provider} = {}) => {
|
||||
let query = {}
|
||||
|
||||
// Find needs to support looking up a user by ID, Email, Email Token,
|
||||
// and Provider Name + Users ID for that Provider
|
||||
if (id) {
|
||||
query = { _id: MongoObjectId(id) }
|
||||
} else if (email) {
|
||||
query = { email: email }
|
||||
} else if (emailToken) {
|
||||
query = { emailToken: emailToken }
|
||||
} else if (provider) {
|
||||
query = { [`${provider.name}.id`]: provider.id }
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
usersCollection.findOne(query, (err, user) => {
|
||||
if (err) return reject(err)
|
||||
return resolve(user)
|
||||
})
|
||||
})
|
||||
},
|
||||
// The user parameter contains a basic user object to be added to the DB.
|
||||
// The oAuthProfile parameter is passed when signing in via oAuth.
|
||||
//
|
||||
// The optional oAuthProfile parameter contains all properties associated
|
||||
// with the users account on the oAuth service they are signing in with.
|
||||
//
|
||||
// You can use this to capture profile.avatar, profile.location, etc.
|
||||
insert: (user, oAuthProfile) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
usersCollection.insert(user, (err, response) => {
|
||||
if (err) return reject(err)
|
||||
|
||||
// Mongo Client automatically adds an id to an inserted object, but
|
||||
// if using a work-a-like we may need to add it from the response.
|
||||
if (!user._id && response._id) user._id = response._id
|
||||
|
||||
return resolve(user)
|
||||
})
|
||||
})
|
||||
},
|
||||
// The user parameter contains a basic user object to be added to the DB.
|
||||
// The oAuthProfile parameter is passed when signing in via oAuth.
|
||||
//
|
||||
// The optional oAuthProfile parameter contains all properties associated
|
||||
// with the users account on the oAuth service they are signing in with.
|
||||
//
|
||||
// You can use this to capture profile.avatar, profile.location, etc.
|
||||
update: (user, profile) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
usersCollection.update({_id: MongoObjectId(user._id)}, user, {}, (err) => {
|
||||
if (err) return reject(err)
|
||||
return resolve(user)
|
||||
})
|
||||
})
|
||||
},
|
||||
// The remove parameter is passed the ID of a user account to delete.
|
||||
//
|
||||
// This method is not used in the current version of next-auth but will
|
||||
// be in a future release, to provide an endpoint for account deletion.
|
||||
remove: (id) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
usersCollection.remove({_id: MongoObjectId(id)}, (err) => {
|
||||
if (err) return reject(err)
|
||||
return resolve(true)
|
||||
})
|
||||
})
|
||||
},
|
||||
// Seralize turns the value of the ID key from a User object
|
||||
serialize: (user) => {
|
||||
// Supports serialization from Mongo Object *and* deserialize() object
|
||||
if (user.id) {
|
||||
// Handle responses from deserialize()
|
||||
return Promise.resolve(user.id)
|
||||
} else if (user._id) {
|
||||
// Handle responses from find(), insert(), update()
|
||||
return Promise.resolve(user._id)
|
||||
} else {
|
||||
return Promise.reject(new Error("Unable to serialise user"))
|
||||
}
|
||||
},
|
||||
// Deseralize turns a User ID into a normalized User object that is
|
||||
// exported to clients. It should not return private/sensitive fields,
|
||||
// only fields you want to expose via the user interface.
|
||||
deserialize: (id) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
usersCollection.findOne({ _id: MongoObjectId(id) }, (err, user) => {
|
||||
if (err) return reject(err)
|
||||
|
||||
// If user not found (e.g. account deleted) return null object
|
||||
if (!user) return resolve(null)
|
||||
|
||||
return resolve({
|
||||
id: user._id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
emailVerified: user.emailVerified,
|
||||
admin: user.admin || false
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
// Email Sign In
|
||||
//
|
||||
// Accounts are created automatically, as when signing in via oAuth.
|
||||
// Users are sent one-time use sign in tokens in links. This avoids
|
||||
// storing user supplied passwords anywhere, preventing password re-use.
|
||||
//
|
||||
// To disable this option, do not set sendSignInEmail (or set it to null).
|
||||
sendSignInEmail: ({email, url, req}) => {
|
||||
nodemailer
|
||||
.createTransport(nodemailerTransport)
|
||||
.sendMail({
|
||||
to: email,
|
||||
from: process.env.EMAIL_FROM,
|
||||
subject: 'Sign in link',
|
||||
text: `Use the link below to sign in:\n\n${url}\n\n`,
|
||||
html: `<p>Use the link below to sign in:</p><p>${url}</p>`
|
||||
}, (err) => {
|
||||
if (err) {
|
||||
console.error('Error sending email to ' + email, err)
|
||||
}
|
||||
})
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('Generated sign in link ' + url + ' for ' + email)
|
||||
}
|
||||
},
|
||||
// Credentials Sign In
|
||||
//
|
||||
// If you use this you will need to define your own way to validate
|
||||
// credentials. Unlike with oAuth or Email Sign In, accounts are not
|
||||
// created automatically so you will need to provide a way to create them.
|
||||
//
|
||||
// This feature is intended for strategies like Two Factor Authentication.
|
||||
//
|
||||
// To disable this option, do not set signin (or set it to null).
|
||||
/*
|
||||
signIn: ({form, req}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Should validate credentials (e.g. hash password, compare 2FA token
|
||||
// etc) and return a valid user object from a database.
|
||||
return usersCollection.findOne({
|
||||
email: form.email
|
||||
}, (err, user) => {
|
||||
if (err) return reject(err)
|
||||
if (!user) return resolve(null)
|
||||
|
||||
// Check credentials - e.g. compare bcrypt password hashes
|
||||
if (form.password === "test1234") {
|
||||
// If valid, return user object - e.g. { id, name, email }
|
||||
return resolve(user)
|
||||
} else {
|
||||
// If invalid, return null
|
||||
return resolve(null)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
*/
|
||||
// Session Object (optional)
|
||||
//
|
||||
// The session object that gets returned to the client. You don't need to
|
||||
// specify this function here unless you want to override or extend the
|
||||
// default (e.g. with any other properties you have added to req.session)
|
||||
//
|
||||
// Note: The object returned will be stored in localStorage and visible
|
||||
// client side so do not return data you would not want the user to see.
|
||||
/*
|
||||
session: (session, req) => {
|
||||
if (req.session && req.session.someCustomProperty)
|
||||
session.someCustomProperty = req.session.someCustomProperty
|
||||
|
||||
session.someOtherCustomProperty = "Example custom property"
|
||||
|
||||
return session
|
||||
}
|
||||
*/
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
/**
|
||||
* next-auth.providers.js Example
|
||||
*
|
||||
* This file returns a simple array of oAuth Provider objects for NextAuth.
|
||||
*
|
||||
* This example returns an array based on what environment variables are set,
|
||||
* with explicit support for Facebook, Google and Twitter, but it can be used
|
||||
* to add strategies for other oAuth providers.
|
||||
*
|
||||
* Environment variables for this example:
|
||||
*
|
||||
* FACEBOOK_ID=
|
||||
* FACEBOOK_SECRET=
|
||||
* GOOGLE_ID=
|
||||
* GOOGLE_SECRET=
|
||||
* TWITTER_KEY=
|
||||
* TWITTER_SECRET=
|
||||
*
|
||||
* If you wish, you can put these in a `.env` to seperate your environment
|
||||
* specific configuration from your code.
|
||||
**/
|
||||
|
||||
// Load environment variables from a .env file if one exists
|
||||
require('dotenv').config({ path: './.env' })
|
||||
|
||||
module.exports = () => {
|
||||
let providers = []
|
||||
|
||||
if (process.env.FACEBOOK_ID && process.env.FACEBOOK_SECRET) {
|
||||
providers.push({
|
||||
providerName: 'Facebook',
|
||||
providerOptions: {
|
||||
scope: ['email', 'public_profile']
|
||||
},
|
||||
Strategy: require('passport-facebook').Strategy,
|
||||
strategyOptions: {
|
||||
clientID: process.env.FACEBOOK_ID,
|
||||
clientSecret: process.env.FACEBOOK_SECRET,
|
||||
profileFields: ['id', 'displayName', 'email', 'link']
|
||||
},
|
||||
getProfile(profile) {
|
||||
// Normalize profile into one with {id, name, email} keys
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.displayName,
|
||||
email: profile._json.email
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (process.env.GOOGLE_ID && process.env.GOOGLE_SECRET) {
|
||||
providers.push({
|
||||
providerName: 'Google',
|
||||
providerOptions: {
|
||||
scope: ['profile', 'email']
|
||||
},
|
||||
Strategy: require('passport-google-oauth').OAuth2Strategy,
|
||||
strategyOptions: {
|
||||
clientID: process.env.GOOGLE_ID,
|
||||
clientSecret: process.env.GOOGLE_SECRET
|
||||
},
|
||||
getProfile(profile) {
|
||||
// Normalize profile into one with {id, name, email} keys
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.displayName,
|
||||
email: profile.emails[0].value
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: Twitter doesn't expose emails by default.
|
||||
* If we don't get one, Passport-stategies.js will create a placeholder.
|
||||
*
|
||||
*
|
||||
* To have your Twitter oAuth return emails go to apps.twitter.com and add
|
||||
* links to your Terms and Conditions and Privacy Policy under the "Settings"
|
||||
* tab, then check the "Request email addresses" from users box under the
|
||||
* "Permissions" tab.
|
||||
**/
|
||||
if (process.env.TWITTER_KEY && process.env.TWITTER_SECRET) {
|
||||
providers.push({
|
||||
providerName: 'Twitter',
|
||||
providerOptions: {
|
||||
scope: []
|
||||
},
|
||||
Strategy: require('passport-twitter').Strategy,
|
||||
strategyOptions: {
|
||||
consumerKey: process.env.TWITTER_KEY,
|
||||
consumerSecret: process.env.TWITTER_SECRET,
|
||||
userProfileURL: 'https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true'
|
||||
},
|
||||
getProfile(profile) {
|
||||
// Normalize profile into one with {id, name, email} keys
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.displayName,
|
||||
email: (profile.emails && profile.emails[0].value) ? profile.emails[0].value : ''
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return providers
|
||||
}
|
||||
6888
example/package-lock.json
generated
6888
example/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "next-auth-examples",
|
||||
"version": "1.12.1",
|
||||
"description": "An example project for next-auth",
|
||||
"repository": "https://github.com/iaincollins/next-auth.git",
|
||||
"main": "",
|
||||
"scripts": {
|
||||
"dev": "NODE_ENV=development node index.js",
|
||||
"build": "next build",
|
||||
"start": "node index.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"connect-mongo": "^2.0.3",
|
||||
"dotenv": "^6.1.0",
|
||||
"express-session": "^1.15.6",
|
||||
"mongodb": "^3.1.10",
|
||||
"nedb": "^1.8.0",
|
||||
"next": "^7.0.2",
|
||||
"next-auth": "^1.12.1",
|
||||
"nodemailer": "^4.7.0",
|
||||
"nodemailer-direct-transport": "^3.3.2",
|
||||
"nodemailer-smtp-transport": "^2.7.4",
|
||||
"passport-facebook": "^2.1.1",
|
||||
"passport-google-oauth": "^1.0.0",
|
||||
"passport-twitter": "^1.0.4",
|
||||
"react": "^16.6.3",
|
||||
"react-dom": "^16.6.3"
|
||||
},
|
||||
"now": {
|
||||
"name": "next-auth-demo",
|
||||
"alias": "next-auth-demo.now.sh"
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import Document, { Head, Main, NextScript } from 'next/document'
|
||||
|
||||
export default class DefaultDocument extends Document {
|
||||
static async getInitialProps(props) {
|
||||
return await Document.getInitialProps(props)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<html lang={this.props.__NEXT_DATA__.props.lang || 'en'}>
|
||||
<Head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossOrigin="anonymous"/>
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import Router from 'next/router'
|
||||
import { NextAuth } from 'next-auth/client'
|
||||
|
||||
export default class extends React.Component {
|
||||
|
||||
static async getInitialProps({req}) {
|
||||
return {
|
||||
session: await NextAuth.init({force: true, req: req})
|
||||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
// Get latest session data after rendering on client then redirect.
|
||||
// The ensures client state is always updated after signing in or out.
|
||||
const session = await NextAuth.init({force: true})
|
||||
Router.push('/')
|
||||
}
|
||||
|
||||
render() {
|
||||
// Provide a link for clients without JavaScript as a fallback.
|
||||
return (
|
||||
<React.Fragment>
|
||||
<style jsx global>{`
|
||||
body{
|
||||
background-color: #fff;
|
||||
}
|
||||
.circle-loader {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 50%;
|
||||
z-index: 100;
|
||||
text-align: center;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
.circle-loader .circle {
|
||||
fill: transparent;
|
||||
stroke: rgba(0,0,0,0.2);
|
||||
stroke-width: 4px;
|
||||
animation: dash 2s ease infinite, rotate 2s linear infinite;
|
||||
}
|
||||
@keyframes dash {
|
||||
0% {
|
||||
stroke-dasharray: 1,95;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
50% {
|
||||
stroke-dasharray: 85,95;
|
||||
stroke-dashoffset: -25;
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 85,95;
|
||||
stroke-dashoffset: -93;
|
||||
}
|
||||
}
|
||||
@keyframes rotate {
|
||||
0% {transform: rotate(0deg); }
|
||||
100% {transform: rotate(360deg); }
|
||||
}
|
||||
`}</style>
|
||||
<noscript>
|
||||
<style>{`
|
||||
svg {
|
||||
display: none;
|
||||
}
|
||||
a {
|
||||
font-weight: bold;
|
||||
}
|
||||
`}</style>
|
||||
</noscript>
|
||||
<a href="/" className="circle-loader">
|
||||
<svg className="circle" width="60" height="60" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="30" cy="30" r="15"/>
|
||||
</svg>
|
||||
<noscript>
|
||||
Click here to continue
|
||||
</noscript>
|
||||
</a>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default class extends React.Component {
|
||||
|
||||
static async getInitialProps({query}) {
|
||||
return {
|
||||
email: query.email
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return(
|
||||
<div className="container">
|
||||
<div className="text-center">
|
||||
<h1 className="display-4 mt-5 mb-3">Check your email</h1>
|
||||
<p className="lead">
|
||||
A sign in link has been sent to { (this.props.email) ? <span className="font-weight-bold">{this.props.email}</span> : <span>your inbox</span> }.
|
||||
</p>
|
||||
<p>
|
||||
<Link href="/"><a>Home</a></Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import React from 'react'
|
||||
import Router from 'next/router'
|
||||
import Link from 'next/link'
|
||||
import { NextAuth } from 'next-auth/client'
|
||||
|
||||
export default class extends React.Component {
|
||||
|
||||
static async getInitialProps({req}) {
|
||||
return {
|
||||
session: await NextAuth.init({req}),
|
||||
linkedAccounts: await NextAuth.linked({req}),
|
||||
providers: await NextAuth.providers({req})
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
email: '',
|
||||
password: '',
|
||||
session: this.props.session
|
||||
}
|
||||
this.handleEmailChange = this.handleEmailChange.bind(this)
|
||||
this.handlePasswordChange = this.handlePasswordChange.bind(this)
|
||||
this.handleSignInSubmit = this.handleSignInSubmit.bind(this)
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
if (this.props.session.user) {
|
||||
Router.push(`/auth/`)
|
||||
}
|
||||
}
|
||||
|
||||
handleEmailChange(event) {
|
||||
this.setState({
|
||||
email: event.target.value
|
||||
})
|
||||
}
|
||||
|
||||
handlePasswordChange(event) {
|
||||
this.setState({
|
||||
password: event.target.value
|
||||
})
|
||||
}
|
||||
|
||||
handleSignInSubmit(event) {
|
||||
event.preventDefault()
|
||||
|
||||
// An object passed NextAuth.signin will be passed to your signin() function
|
||||
NextAuth.signin({
|
||||
email: this.state.email,
|
||||
password: this.state.password
|
||||
})
|
||||
.then(authenticated => {
|
||||
Router.push(`/auth/callback`)
|
||||
})
|
||||
.catch(() => {
|
||||
alert("Authentication failed.")
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.session.user) {
|
||||
return null
|
||||
} else {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="text-center">
|
||||
<h1 className="display-5 mt-4 mb-2">NextAuth - Custom Sign In</h1>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-sm-12 col-md-10 col-lg-8 col-xl-7 mr-auto ml-auto">
|
||||
<p className="mt-3 mb-4 text-center">
|
||||
If you want to support password based sign in, two factor authentication
|
||||
or another sign in method, define a signin() function
|
||||
in <strong>next-auth.functions.js</strong>.
|
||||
</p>
|
||||
<div className="card mt-3 mb-3">
|
||||
<h4 className="card-header">Sign In</h4>
|
||||
<div className="card-body pb-0">
|
||||
<form id="signin" method="post" action="/auth/signin" onSubmit={this.handleSignInSubmit}>
|
||||
<input name="_csrf" type="hidden" value={this.state.session.csrfToken}/>
|
||||
<p>
|
||||
<label htmlFor="email">Email address</label><br/>
|
||||
<input name="email" type="text" placeholder="j.smith@example.com" id="email" className="form-control" value={this.state.email} onChange={this.handleEmailChange}/>
|
||||
</p>
|
||||
<p>
|
||||
<label htmlFor="password">Password</label><br/>
|
||||
<input name="password" type="password" placeholder="" id="password" className="form-control" value={this.state.password} onChange={this.handlePasswordChange}/>
|
||||
</p>
|
||||
<p className="text-right">
|
||||
<button id="submitButton" type="submit" className="btn btn-outline-primary">Sign in</button>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-italic text-muted text-center small">
|
||||
For this to work, you will need enable the signin() function in <strong>next-auth.functions.js</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-center">
|
||||
<Link href="/auth"><a>Back</a></Link>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default class extends React.Component {
|
||||
|
||||
static async getInitialProps({query}) {
|
||||
return {
|
||||
action: query.action || null,
|
||||
type: query.type || null,
|
||||
service: query.service || null
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.action == 'signin' && this.props.type == 'oauth') {
|
||||
return(
|
||||
<div className="container">
|
||||
<div className="text-center mb-5">
|
||||
<h1 className="display-4 mt-5 mb-3">Unable to sign in</h1>
|
||||
<p className="lead">An account associated with your email address already exists.</p>
|
||||
<p className="lead"><Link href="/auth"><a>Sign in with email or another service</a></Link></p>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-sm-8 mr-auto ml-auto mb-5 mt-5">
|
||||
<div className="text-muted">
|
||||
<h4 className="mb-2">Why am I seeing this?</h4>
|
||||
<p className="mb-3">
|
||||
It looks like you might have already signed up using another service to sign in.
|
||||
</p>
|
||||
<p className="mb-3">
|
||||
If you have previously signed up using another service you must link accounts before you
|
||||
can use a different service to sign in.
|
||||
</p>
|
||||
<p className="mb-5">
|
||||
This is to prevent people from signing up to another service using your email address
|
||||
to try and access your account.
|
||||
</p>
|
||||
<h4 className="mb-2">How do I fix this?</h4>
|
||||
<p className="mb-0">
|
||||
First sign in using your email address then link your account to the service you want
|
||||
to use to sign in with in future. You only need to do this once.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
} else if (this.props.action == 'signin' && this.props.type == 'token-invalid') {
|
||||
return(
|
||||
<div className="container">
|
||||
<div className="text-center">
|
||||
<h1 className="display-4 mt-5 mb-2">Link not valid</h1>
|
||||
<p className="lead">This sign in link is no longer valid.</p>
|
||||
<p className="lead"><Link href="/auth"><a>Get a new sign in link</a></Link></p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return(
|
||||
<div className="container">
|
||||
<div className="text-center">
|
||||
<h1 className="display-4 mt-5">Error signing in</h1>
|
||||
<p className="lead">An error occured while trying to sign in.</p>
|
||||
<p className="lead"><Link href="/auth"><a>Sign in with email or another service</a></Link></p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
import React from 'react'
|
||||
import Router from 'next/router'
|
||||
import Link from 'next/link'
|
||||
import { NextAuth } from 'next-auth/client'
|
||||
|
||||
export default class extends React.Component {
|
||||
|
||||
static async getInitialProps({req}) {
|
||||
return {
|
||||
session: await NextAuth.init({req}),
|
||||
linkedAccounts: await NextAuth.linked({req}),
|
||||
providers: await NextAuth.providers({req})
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
email: '',
|
||||
session: this.props.session
|
||||
}
|
||||
this.handleEmailChange = this.handleEmailChange.bind(this)
|
||||
this.handleSignInSubmit = this.handleSignInSubmit.bind(this)
|
||||
}
|
||||
|
||||
handleEmailChange(event) {
|
||||
this.setState({
|
||||
email: event.target.value
|
||||
})
|
||||
}
|
||||
|
||||
handleSignInSubmit(event) {
|
||||
event.preventDefault()
|
||||
|
||||
if (!this.state.email) return
|
||||
|
||||
NextAuth.signin(this.state.email)
|
||||
.then(() => {
|
||||
Router.push(`/auth/check-email?email=${this.state.email}`)
|
||||
})
|
||||
.catch(() => {
|
||||
Router.push(`/auth/error?action=signin&type=email&email=${this.state.email}`)
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.session.user) {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="text-center">
|
||||
<h1 className="display-4 mt-3">NextAuth Example</h1>
|
||||
<p className="lead mt-3 mb-1">You are signed in as <span className="font-weight-bold">{this.props.session.user.email}</span>.</p>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-sm-5 mr-auto ml-auto">
|
||||
<LinkAccounts
|
||||
session={this.props.session}
|
||||
linkedAccounts={this.props.linkedAccounts}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-center">
|
||||
<Link href="/"><a>Home</a></Link>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="text-center">
|
||||
<h1 className="display-4 mt-3 mb-3">NextAuth Example</h1>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-sm-6 mr-auto ml-auto">
|
||||
<div className="card mt-3 mb-3">
|
||||
<h4 className="card-header">Sign In</h4>
|
||||
<div className="card-body pb-0">
|
||||
<SignInButtons providers={this.props.providers}/>
|
||||
<form id="signin" method="post" action="/auth/email/signin" onSubmit={this.handleSignInSubmit}>
|
||||
<input name="_csrf" type="hidden" value={this.state.session.csrfToken}/>
|
||||
<p>
|
||||
<label htmlFor="email">Email address</label><br/>
|
||||
<input name="email" type="text" placeholder="j.smith@example.com" id="email" className="form-control" value={this.state.email} onChange={this.handleEmailChange}/>
|
||||
</p>
|
||||
<p className="text-right">
|
||||
<button id="submitButton" type="submit" className="btn btn-outline-primary">Sign in with email</button>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-center small">
|
||||
<Link href="/auth/credentials"><a>Sign in with credentials</a></Link>
|
||||
</p>
|
||||
<p className="text-center">
|
||||
<Link href="/"><a>Home</a></Link>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class LinkAccounts extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="card mt-3 mb-3">
|
||||
<h4 className="card-header">Link Accounts</h4>
|
||||
<div className="card-body pb-0">
|
||||
{
|
||||
Object.keys(this.props.linkedAccounts).map((provider, i) => {
|
||||
return <LinkAccount key={i} provider={provider} session={this.props.session} linked={this.props.linkedAccounts[provider]}/>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class LinkAccount extends React.Component {
|
||||
render() {
|
||||
if (this.props.linked === true) {
|
||||
return (
|
||||
<form method="post" action={`/auth/oauth/${this.props.provider.toLowerCase()}/unlink`}>
|
||||
<input name="_csrf" type="hidden" value={this.props.session.csrfToken}/>
|
||||
<p>
|
||||
<button className="btn btn-block btn-outline-danger" type="submit">
|
||||
Unlink from {this.props.provider}
|
||||
</button>
|
||||
</p>
|
||||
</form>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<p>
|
||||
<a className="btn btn-block btn-outline-primary" href={`/auth/oauth/${this.props.provider.toLowerCase()}`}>
|
||||
Link with {this.props.provider}
|
||||
</a>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SignInButtons extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{
|
||||
Object.keys(this.props.providers).map((provider, i) => {
|
||||
return (
|
||||
<p key={i}>
|
||||
<a className="btn btn-block btn-outline-secondary" href={this.props.providers[provider].signin}>
|
||||
Sign in with {provider}
|
||||
</a>
|
||||
</p>
|
||||
)
|
||||
})
|
||||
}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import React from 'react'
|
||||
import Router from 'next/router'
|
||||
import Link from 'next/link'
|
||||
import { NextAuth } from 'next-auth/client'
|
||||
|
||||
export default class extends React.Component {
|
||||
static async getInitialProps({req}) {
|
||||
return {
|
||||
session: await NextAuth.init({req})
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.handleSignOutSubmit = this.handleSignOutSubmit.bind(this)
|
||||
}
|
||||
|
||||
handleSignOutSubmit(event) {
|
||||
event.preventDefault()
|
||||
NextAuth.signout()
|
||||
.then(() => {
|
||||
Router.push('/auth/callback')
|
||||
})
|
||||
.catch(err => {
|
||||
Router.push('/auth/error?action=signout')
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="text-center">
|
||||
<h1 className="display-4 mt-3 mb-3">NextAuth Example</h1>
|
||||
<p className="lead mt-3 mb-3">An example of how to use the <a href="https://www.npmjs.com/package/next-auth">NextAuth</a> module.</p>
|
||||
<SignInMessage {...this.props}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class SignInMessage extends React.Component {
|
||||
render() {
|
||||
if (this.props.session.user) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<p><Link href="/auth"><a className="btn btn-secondary">Manage Account</a></Link></p>
|
||||
<form id="signout" method="post" action="/auth/signout" onSubmit={this.handleSignOutSubmit}>
|
||||
<input name="_csrf" type="hidden" value={this.props.session.csrfToken}/>
|
||||
<button type="submit" className="btn btn-outline-secondary">Sign out</button>
|
||||
</form>
|
||||
</React.Fragment>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<p><Link href="/auth"><a className="btn btn-primary">Sign in</a></Link></p>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
84
index.d.ts
vendored
84
index.d.ts
vendored
@@ -1,84 +0,0 @@
|
||||
import { Store } from "express-session";
|
||||
import { RequestHandler } from "express";
|
||||
import { IpcNetConnectOpts } from "net";
|
||||
|
||||
declare namespace nextAuth {
|
||||
interface IOptions {
|
||||
bodyParser: boolean;
|
||||
csrf: boolean;
|
||||
pathPrefix: string;
|
||||
expressApp?: Express.Application;
|
||||
expressSession: RequestHandler;
|
||||
sessionSecret: string;
|
||||
sessionStore: Store;
|
||||
sessionMaxAge: number;
|
||||
sessionRevalidateAge: number;
|
||||
sessionResave: boolean;
|
||||
sessionRolling: boolean;
|
||||
sessionSaveUninitialized: boolean;
|
||||
serverUrl?: string;
|
||||
trustProxy: boolean;
|
||||
providers: any[];
|
||||
port?: number;
|
||||
functions: IFunctions;
|
||||
}
|
||||
|
||||
interface IUserProvider {
|
||||
name: string;
|
||||
id: string;
|
||||
}
|
||||
interface ISendSignInEmailOpts {
|
||||
email?: string;
|
||||
url?: string;
|
||||
req?: Express.Request;
|
||||
}
|
||||
interface ISignInOpts {
|
||||
email?: string;
|
||||
password?: string;
|
||||
}
|
||||
interface INextAuthSessionData<UserType = {}> extends Session {
|
||||
maxAge: number;
|
||||
revalidateAge: number;
|
||||
csrfToken: string;
|
||||
user?: UserType;
|
||||
expires?: number;
|
||||
}
|
||||
interface IFunctions<
|
||||
UserType = {},
|
||||
IDType = string,
|
||||
SessionType extends INextAuthSessionData = INextAuthSessionData
|
||||
> {
|
||||
find(
|
||||
id: IDType,
|
||||
email: string,
|
||||
emailToken: string,
|
||||
provider: IUserProvider
|
||||
): Promise<UserType>;
|
||||
update: (user: UserType, profile: any) => Promise<UserType>;
|
||||
insert: (user: UserType, profile: any) => Promise<UserType>;
|
||||
remove: (id: IDType) => Promise<boolean>;
|
||||
serialize: (user: UserType) => Promise<IDType>;
|
||||
deserialize: (id: IDType) => Promise<UserType>;
|
||||
session?: (
|
||||
session: INextAuthSessionData,
|
||||
req: Express.Request
|
||||
) => SessionType;
|
||||
sendSignInEmail?: (opts: ISendSignInEmailOpts) => Promise<boolean>;
|
||||
signIn?: (opts: ISignInOpts) => Promise<UserType>;
|
||||
}
|
||||
|
||||
interface INextAuthResult {
|
||||
next?: nextApp;
|
||||
express: Express;
|
||||
expressApp: Express.Application;
|
||||
function: IFunctions;
|
||||
providers: any;
|
||||
port?: number;
|
||||
}
|
||||
}
|
||||
|
||||
declare function NextAuth(
|
||||
nextApp?: NextApp,
|
||||
options?: nextAuth.IOptions
|
||||
): Promise<nextAuth.INextAuthResult>;
|
||||
export = NextAuth;
|
||||
478
index.js
478
index.js
@@ -1,478 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const BodyParser = require('body-parser')
|
||||
const Express = require('express')
|
||||
const ExpressSession = require('express-session')
|
||||
const lusca = require('lusca')
|
||||
const passportStrategies = require('./src/passport-strategies')
|
||||
const uuid = require('uuid/v4')
|
||||
|
||||
module.exports = (nextApp, {
|
||||
bodyParser = true,
|
||||
// Optional, allows you to set e.g. 'limit' (maximum request body size, default 100kb)
|
||||
bodyParserJsonOptions = {},
|
||||
bodyParserUrlEncodedOptions = { extended: true },
|
||||
csrf = true,
|
||||
// URL base path for authentication routes (optional).
|
||||
// Note: The prefix value of '/auth' is currently hard coded in
|
||||
// next-auth-client so you should not change this unless you also modify it.
|
||||
pathPrefix = '/auth',
|
||||
// Express Server (optional).
|
||||
expressApp = null,
|
||||
// Express Session (optional).
|
||||
expressSession = ExpressSession,
|
||||
// Secret used to encrypt session data on the server.
|
||||
sessionSecret = 'change-me',
|
||||
// Session store for express-session.
|
||||
// Defaults to an in memory store, which is not recommended for production.
|
||||
sessionStore = expressSession.MemoryStore(),
|
||||
// The name of the session ID cookie to set in the response (and read from in
|
||||
// the request). The default value is 'connect.sid'.
|
||||
sessionCookie = 'connect.sid',
|
||||
// Maximum Session Age in ms (optional, default is 7 days).
|
||||
// The expiry time for a session is reset every time a user revisits the site
|
||||
// or revalidates their session token - this is the maximum idle time value.
|
||||
sessionMaxAge = 60000 * 60 * 24 * 7,
|
||||
// The session cookie name. Useful for adding cookie prefixes. E.g. setting
|
||||
// '__HOST-' and '__SECURE-' prefixes on cookie names prevents them from being
|
||||
// overwritten by insecure origins.
|
||||
sessionName = null,
|
||||
// Session Revalidation in X ms (optional, default is 60 seconds).
|
||||
// Specifies how often a Single Page App should revalidate a session.
|
||||
// Does not impact the session life on the server, but causes clients to
|
||||
// refetch session info (even if it is in a local cache) after N seconds has
|
||||
// elapsed since it was last checked so they always display state correctly.
|
||||
// If set to 0 will revalidate a session before rendering every page.
|
||||
sessionRevalidateAge = 60000,
|
||||
// Force the session to be saved back to the session store, even if the
|
||||
// session was not modified during the request.
|
||||
// Note: If this is false, session expiry will not rotate and will expire
|
||||
// after sessionMaxAge unless you write you own code to rotate the session.
|
||||
// This is an option exposed for advanced use cases on people with specific
|
||||
// databases that have session store drivers that do not work well with
|
||||
// the express-session resave option.
|
||||
// https://www.npmjs.com/package/express-session#resave
|
||||
sessionResave = true,
|
||||
// Force a session identifier cookie to be set on every response. The expire
|
||||
// time is reset to the original maxAge, resetting the expiration time.
|
||||
// Note When this option is set to true but the saveUninitialized option
|
||||
// is set to false, the cookie will not be set on a response with an
|
||||
// uninitialized session https://www.npmjs.com/package/express-session#rolling
|
||||
sessionRolling = true,
|
||||
// Prevent cookies from being sent cross-site, protecting against CSRF
|
||||
// attacks.
|
||||
sessionSameSite = null,
|
||||
// Forces a session that is "uninitialized" to be saved to the store.
|
||||
// A session is uninitialized when it is new but not modified. Choosing false
|
||||
// is useful for implementing login sessions, reducing server storage usage,
|
||||
// or complying with laws that require permission before setting a cookie.
|
||||
//
|
||||
// Choosing false will also help with race conditions where a client makes
|
||||
// multiple parallel requests without a session.
|
||||
//
|
||||
// Note that if the build in CSRF protection is enabled (the default) then
|
||||
// sessions will ALWAYS be 'initialized' as it saves to the session.
|
||||
// https://www.npmjs.com/package/express-session#saveuninitialized
|
||||
sessionSaveUninitialized = false,
|
||||
// Canonical URL of the server (optional, but recommended).
|
||||
// e.g. 'http://localhost:3000' or 'https://www.example.com'
|
||||
// Used in callbak URLs and email sign in links. It will be auto generated
|
||||
// if not specified, which may cause problems if your site uses multiple
|
||||
// aliases (e.g. 'example.com and 'www.examples.com').
|
||||
serverUrl = null,
|
||||
// If we are behind a proxy server and it says we are running SSL, trust it.
|
||||
// All this does is make sure we use HTTPS for callback URLs and email links.
|
||||
// You should never need to turn this off.
|
||||
trustProxy = true,
|
||||
// An array of oAuth Provider config blocks (optional).
|
||||
providers = [],
|
||||
// Port to start listening on
|
||||
port = null,
|
||||
// Functions for find, update, insert, serialize and deserialize methods.
|
||||
// They should all return a Promise with resolve() or reject().
|
||||
// find() should return resolve(null) if no matching user found.
|
||||
functions = {
|
||||
find: ({
|
||||
id,
|
||||
email,
|
||||
emailToken,
|
||||
provider // provider = { name: 'twitter', id: '123456' }
|
||||
} = {}) => { Promise.resolve(user) },
|
||||
update: (user, profile) => { Promise.resolve(user) },
|
||||
insert: (user, profile) => { Promise.resolve(user) },
|
||||
remove: (id) => { Promise.resolve(id) },
|
||||
serialize: (user) => { Promise.resolve(id) },
|
||||
deserialize: (id) => { Promise.resolve(user) },
|
||||
session: null,
|
||||
sendSignInEmail: null, /* ({
|
||||
email = null,
|
||||
url = null,
|
||||
req = null
|
||||
} = {}) => { Promise.resolve(true) }
|
||||
*/
|
||||
signIn: null /* ({
|
||||
email = null,
|
||||
password = null
|
||||
} = {}) => { Promise.resolve(user) }
|
||||
*/
|
||||
}
|
||||
} = {}) => {
|
||||
|
||||
if (typeof(functions) !== 'object') {
|
||||
throw new Error('functions must be a an object')
|
||||
}
|
||||
|
||||
if (expressApp === null) {
|
||||
expressApp = Express()
|
||||
}
|
||||
|
||||
// If an instance of nextApp was passed, let all requests to /_next/* pass
|
||||
// to it *before* Express Session and other middleware is called.
|
||||
if (nextApp) {
|
||||
expressApp.all('/_next/*', (req, res) => {
|
||||
let nextRequestHandler = nextApp.getRequestHandler()
|
||||
return nextRequestHandler(req, res)
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Set up body parsing, express sessions and add CSRF tokens.
|
||||
*
|
||||
* You can set bodyParser to false and pass an Express instance if you want
|
||||
* to customise how you invoke Body Parser.
|
||||
*/
|
||||
if (bodyParser === true) {
|
||||
expressApp.use(BodyParser.json(bodyParserJsonOptions))
|
||||
expressApp.use(BodyParser.urlencoded(bodyParserUrlEncodedOptions))
|
||||
}
|
||||
expressApp.use(expressSession({
|
||||
name: sessionName,
|
||||
secret: sessionSecret,
|
||||
store: sessionStore,
|
||||
resave: sessionResave,
|
||||
rolling: sessionRolling,
|
||||
saveUninitialized: sessionSaveUninitialized,
|
||||
cookie: {
|
||||
name: sessionCookie,
|
||||
httpOnly: true,
|
||||
secure: 'auto',
|
||||
maxAge: sessionMaxAge,
|
||||
sameSite: sessionSameSite,
|
||||
}
|
||||
}))
|
||||
|
||||
if (csrf === true) {
|
||||
// If csrf is true (default) apply to all routes
|
||||
expressApp.use(lusca.csrf())
|
||||
} else if (csrf !== false) {
|
||||
// If csrf is anything else (except false) then pass it as a config option
|
||||
expressApp.use(lusca.csrf(csrf))
|
||||
} // if csrf is explicitly set to false then doesn't apply CSRF at all
|
||||
|
||||
if (trustProxy === true) {
|
||||
expressApp.set('trust proxy', 1)
|
||||
}
|
||||
|
||||
/*
|
||||
* With sessions configured we need to configure Passport and trigger
|
||||
* passport.initialize() before we add any other routes.
|
||||
*/
|
||||
passportStrategies({
|
||||
expressApp: expressApp,
|
||||
serverUrl: serverUrl,
|
||||
providers: providers,
|
||||
functions: functions
|
||||
})
|
||||
|
||||
/*
|
||||
* Add route to get CSRF token via AJAX
|
||||
*/
|
||||
expressApp.get(`${pathPrefix}/csrf`, (req, res) => {
|
||||
return res.json({
|
||||
csrfToken: res.locals._csrf
|
||||
})
|
||||
})
|
||||
|
||||
/*
|
||||
* Return session info to client
|
||||
*
|
||||
* Will be stored in local storage, so should not return sensitive data that
|
||||
* could be captured in a Cross Site Scripting attack (i.e. so not the session
|
||||
* token) – or anything you don't want users to see (like private IDs) but is
|
||||
* is okay to return things like access tokens for acessing remote services.
|
||||
*/
|
||||
expressApp.get(`${pathPrefix}/session`, (req, res) => {
|
||||
let session = {
|
||||
maxAge: sessionMaxAge,
|
||||
revalidateAge: sessionRevalidateAge,
|
||||
csrfToken: res.locals._csrf
|
||||
}
|
||||
|
||||
if (req.user)
|
||||
session.user = req.user
|
||||
|
||||
if (functions.session)
|
||||
session = functions.session(session, req)
|
||||
|
||||
return res.json(session)
|
||||
})
|
||||
|
||||
// Server side function for returning list of accounts user has linked.
|
||||
// Called when pages are rendered in on the server (instead of /auth/linked).
|
||||
// Returns all accounts the user has linked (e.g. Twitter, Facebook, Google…)
|
||||
expressApp.use((req, res, next) => {
|
||||
req.linked = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!req.user) return resolve({})
|
||||
|
||||
functions.serialize(req.user)
|
||||
.then(id => {
|
||||
if (!id) throw new Error("Unable to serialize user")
|
||||
return functions.find({ id: id })
|
||||
})
|
||||
.then(user => {
|
||||
if (!user) return resolve({})
|
||||
|
||||
let linkedAccounts = {}
|
||||
providers.forEach(provider => {
|
||||
linkedAccounts[provider.providerName] = (user[provider.providerName.toLowerCase()]) ? true : false
|
||||
})
|
||||
|
||||
return resolve(linkedAccounts)
|
||||
})
|
||||
.catch(err => {
|
||||
return reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
// Client side REST endpoint for returning list of accounts user has linked.
|
||||
// Called when pages are rendered in the browser (instead of req.linked()).
|
||||
// Returns all accounts the user has linked (e.g. Twitter, Facebook, Google…)
|
||||
expressApp.get(`${pathPrefix}/linked`, (req, res) => {
|
||||
if (!req.user) return res.json({})
|
||||
|
||||
// First get the User ID from the User, then look up the user details.
|
||||
// Note: We don't use the User object in req.user directly as it is a
|
||||
// a simplified set of properties set by functions.deserialize().
|
||||
functions.serialize(req.user)
|
||||
.then(id => {
|
||||
return functions.find({ id: id })
|
||||
})
|
||||
.then(user => {
|
||||
if (!user) return res.json({})
|
||||
|
||||
let linkedAccounts = {}
|
||||
providers.forEach(provider => {
|
||||
linkedAccounts[provider.providerName] = (user[provider.providerName.toLowerCase()]) ? true : false
|
||||
})
|
||||
|
||||
return res.json(linkedAccounts)
|
||||
})
|
||||
.catch(err => {
|
||||
return res.status(500).end()
|
||||
})
|
||||
})
|
||||
|
||||
/*
|
||||
* Return list of configured oAuth Providers
|
||||
*
|
||||
* We define this both as a server side function and a RESTful endpoint so
|
||||
* that it can be used rendering a page both server side and client side.
|
||||
*/
|
||||
// Server side function
|
||||
expressApp.use((req, res, next) => {
|
||||
req.providers = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let configuredProviders = {}
|
||||
providers.forEach(provider => {
|
||||
configuredProviders[provider.providerName] = {
|
||||
signin: (serverUrl || '') + `${pathPrefix}/oauth/${provider.providerName.toLowerCase()}`,
|
||||
callback: (serverUrl || '') + `${pathPrefix}/oauth/${provider.providerName.toLowerCase()}/callback`
|
||||
}
|
||||
})
|
||||
return resolve(configuredProviders)
|
||||
})
|
||||
}
|
||||
next()
|
||||
})
|
||||
// RESTful endpoint
|
||||
expressApp.get(`${pathPrefix}/providers`, (req, res) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let configuredProviders = {}
|
||||
providers.forEach(provider => {
|
||||
configuredProviders[provider.providerName] = {
|
||||
signin: (serverUrl || '') + `${pathPrefix}/oauth/${provider.providerName.toLowerCase()}`,
|
||||
callback: (serverUrl || '') + `${pathPrefix}/oauth/${provider.providerName.toLowerCase()}/callback`
|
||||
}
|
||||
})
|
||||
return res.json(configuredProviders)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
/*
|
||||
* Enable /auth/signin routes if signIn() function is passed
|
||||
*/
|
||||
if (functions.signIn) {
|
||||
expressApp.post(`${pathPrefix}/signin`, (req, res) => {
|
||||
// Passes all supplied credentials to the signIn function
|
||||
functions.signIn({
|
||||
form: req.body,
|
||||
req: req
|
||||
})
|
||||
.then(user => {
|
||||
if (user) {
|
||||
// If signIn() returns a user, sign in as them
|
||||
req.logIn(user, (err) => {
|
||||
if (err) return res.redirect(`${pathPrefix}/error?action=signin&type=credentials`)
|
||||
if (req.xhr) {
|
||||
// If AJAX request (from client with JS), return JSON response
|
||||
return res.json({success: true})
|
||||
} else {
|
||||
// If normal form POST (from client without JS) return redirect
|
||||
return res.redirect(`${pathPrefix}/callback?action=signin&service=credentials`)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// If no user object is returned, bounce back to the sign in page
|
||||
return res.redirect(`${pathPrefix}`)
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
return res.redirect(`${pathPrefix}/error?action=signin&type=credentials`)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Enable /auth/email/signin routes if sendSignInEmail() function is passed
|
||||
*/
|
||||
if (functions.sendSignInEmail) {
|
||||
/*
|
||||
* Generate a one time use sign in link and email it to the user
|
||||
*/
|
||||
expressApp.post(`${pathPrefix}/email/signin`, (req, res) => {
|
||||
const email = req.body.email || null
|
||||
|
||||
if (!email || email.trim() === '') {
|
||||
res.redirect(`${pathPrefix}`)
|
||||
}
|
||||
|
||||
const token = uuid()
|
||||
const url = (serverUrl || `${req.protocol}://${req.headers.host}`) + `${pathPrefix}/email/signin/${token}`
|
||||
|
||||
// Create verification token save it to database
|
||||
functions.find({ email: email })
|
||||
.then(user => {
|
||||
if (user) {
|
||||
// If a user with that email address exists already, update token.
|
||||
user.emailToken = token
|
||||
return functions.update(user)
|
||||
} else {
|
||||
// If the user does not exist, create a new account with the token.
|
||||
return functions.insert({
|
||||
email: email,
|
||||
emailToken: token
|
||||
})
|
||||
}
|
||||
})
|
||||
.then(user => {
|
||||
functions.sendSignInEmail({
|
||||
email: user.email,
|
||||
url: url,
|
||||
req: req
|
||||
})
|
||||
if (req.xhr) {
|
||||
// If AJAX request (from client with JS), return JSON response
|
||||
return res.json({success: true})
|
||||
} else {
|
||||
// If normal form POST (from client without JS) return redirect
|
||||
return res.redirect(`${pathPrefix}/check-email?email=${email}`)
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
return res.redirect(`${pathPrefix}/error?action=signin&type=email&email=${email}`)
|
||||
})
|
||||
})
|
||||
|
||||
/*
|
||||
* Verify token in callback URL for email sign in
|
||||
*/
|
||||
expressApp.get(`${pathPrefix}/email/signin/:token`, (req, res) => {
|
||||
if (!req.params.token) {
|
||||
return res.redirect(`${pathPrefix}/error?action=signin&type=token-missing`)
|
||||
}
|
||||
|
||||
functions.find({ emailToken: req.params.token })
|
||||
.then(user => {
|
||||
if (user) {
|
||||
// Delete current token so it cannot be used again
|
||||
delete user.emailToken
|
||||
// Mark email as verified now we know they have access to it
|
||||
user.emailVerified = true
|
||||
return functions.update(user, null, { delete: 'emailToken' })
|
||||
} else {
|
||||
return Promise.reject(new Error("Token not valid"))
|
||||
}
|
||||
})
|
||||
.then(user => {
|
||||
// If the user object is valid, sign the user in
|
||||
req.logIn(user, (err) => {
|
||||
if (err) return res.redirect(`${pathPrefix}/error?action=signin&type=token-invalid`)
|
||||
if (req.xhr) {
|
||||
// If AJAX request (from client with JS), return JSON response
|
||||
return res.json({success: true})
|
||||
} else {
|
||||
// If normal form POST (from client without JS) return redirect
|
||||
return res.redirect(`${pathPrefix}/callback?action=signin&service=email`)
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
return res.redirect(`${pathPrefix}/error?action=signin&type=token-invalid`)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Sign a user out
|
||||
*/
|
||||
expressApp.post(`${pathPrefix}/signout`, (req, res) => {
|
||||
// Log user out with Passport and remove their Express session
|
||||
req.logout()
|
||||
req.session.destroy(() => {
|
||||
return res.redirect(`${pathPrefix}/callback?action=signout`)
|
||||
})
|
||||
})
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let response = {
|
||||
next: nextApp,
|
||||
express: Express,
|
||||
expressApp: expressApp,
|
||||
functions: functions,
|
||||
providers: providers,
|
||||
port: port
|
||||
}
|
||||
|
||||
// If no port specified, don't start Express automatically
|
||||
if (!port) return resolve(response)
|
||||
|
||||
// If an instance of nextApp was passed, have it handle all other routes
|
||||
if (nextApp) {
|
||||
expressApp.all('*', (req, res) => {
|
||||
let nextRequestHandler = nextApp.getRequestHandler()
|
||||
return nextRequestHandler(req, res)
|
||||
})
|
||||
}
|
||||
|
||||
// Start Express
|
||||
return expressApp.listen(port, err => {
|
||||
if (err) reject(err)
|
||||
return resolve(response)
|
||||
})
|
||||
})
|
||||
}
|
||||
20344
package-lock.json
generated
20344
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
211
package.json
211
package.json
@@ -1,37 +1,192 @@
|
||||
{
|
||||
"name": "next-auth",
|
||||
"version": "1.13.0",
|
||||
"description": "An authentication library for Next.js",
|
||||
"repository": "https://github.com/iaincollins/next-auth.git",
|
||||
"version": "0.0.0-semantically-released",
|
||||
"description": "Authentication for Next.js",
|
||||
"homepage": "https://next-auth.js.org",
|
||||
"repository": "https://github.com/nextauthjs/next-auth.git",
|
||||
"author": "Iain Collins <me@iaincollins.com>",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "rollup --config",
|
||||
"prepare": "rollup --config"
|
||||
"types": "./index.d.ts",
|
||||
"keywords": [
|
||||
"react",
|
||||
"nodejs",
|
||||
"oauth",
|
||||
"jwt",
|
||||
"oauth2",
|
||||
"authentication",
|
||||
"nextjs",
|
||||
"csrf",
|
||||
"oidc",
|
||||
"nextauth"
|
||||
],
|
||||
"exports": {
|
||||
".": "./dist/server/index.js",
|
||||
"./jwt": "./dist/lib/jwt.js",
|
||||
"./adapters": "./dist/adapters/index.js",
|
||||
"./client": "./dist/client/index.js",
|
||||
"./providers": "./dist/providers/index.js",
|
||||
"./providers/*": "./dist/providers/*.js",
|
||||
"./errors": "./dist/lib/errors.js"
|
||||
},
|
||||
"author": "",
|
||||
"scripts": {
|
||||
"build": "npm run build:js && npm run build:css",
|
||||
"build:js": "node ./config/build.js && babel --config-file ./config/babel.config.json src --out-dir dist",
|
||||
"build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir dist && node config/wrap-css.js",
|
||||
"dev:setup": "npm run build:css && cd app && npm i",
|
||||
"dev": "cd app && npm run dev",
|
||||
"watch": "npm run watch:js | npm run watch:css",
|
||||
"watch:js": "babel --config-file ./config/babel.config.json --watch src --out-dir dist",
|
||||
"watch:css": "postcss --config config/postcss.config.js --watch src/**/*.css --base src --dir dist",
|
||||
"test": "echo \"Write some tests...\"; npm run test:types",
|
||||
"test:types": "dtslint types",
|
||||
"prepublishOnly": "npm run build",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"index.js",
|
||||
"index.d.ts",
|
||||
"providers.js",
|
||||
"providers.d.ts",
|
||||
"adapters.js",
|
||||
"adapters.d.ts",
|
||||
"client.js",
|
||||
"client.d.ts",
|
||||
"errors.js",
|
||||
"errors.d.ts",
|
||||
"jwt.js",
|
||||
"jwt.d.ts",
|
||||
"internals"
|
||||
],
|
||||
"license": "ISC",
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"body-parser": "^1.18.2",
|
||||
"express": "^4.16.3",
|
||||
"express-session": "^1.15.6",
|
||||
"isomorphic-fetch": "^2.2.1",
|
||||
"lusca": "^1.6.0",
|
||||
"passport": "^0.4.0",
|
||||
"uuid": "^3.2.1"
|
||||
"@next-auth/prisma-legacy-adapter": "canary",
|
||||
"@next-auth/typeorm-legacy-adapter": "canary",
|
||||
"crypto-js": "^4.0.0",
|
||||
"futoin-hkdf": "^1.3.2",
|
||||
"jose": "^1.27.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"nodemailer": "^6.4.16",
|
||||
"oauth": "^0.9.15",
|
||||
"pkce-challenge": "^2.1.0",
|
||||
"preact": "^10.4.1",
|
||||
"preact-render-to-string": "^5.1.14",
|
||||
"querystring": "^0.2.0",
|
||||
"require_optional": "^1.0.1",
|
||||
"typeorm": "^0.2.30"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1 || ^17",
|
||||
"react-dom": "16.13.1 || ^17"
|
||||
},
|
||||
"peerOptionalDependencies": {
|
||||
"mongodb": "^3.5.9",
|
||||
"mysql": "^2.18.1",
|
||||
"mssql": "^6.2.1",
|
||||
"pg": "^8.2.1",
|
||||
"@prisma/client": "^2.16.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.26.3",
|
||||
"rollup-plugin-babel": "^3.0.7",
|
||||
"babel-plugin-external-helpers": "^6.22.0",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"bl": "^2.1.2",
|
||||
"rollup": "^0.67.4",
|
||||
"rollup-plugin-commonjs": "^9.1.3",
|
||||
"rollup-plugin-json": "^3.0.0",
|
||||
"rollup-plugin-node-builtins": "^2.1.2",
|
||||
"rollup-plugin-node-resolve": "^3.3.0",
|
||||
"semver": "^5.6.0"
|
||||
}
|
||||
"@babel/cli": "^7.8.4",
|
||||
"@babel/core": "^7.9.6",
|
||||
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
||||
"@babel/preset-env": "^7.9.6",
|
||||
"@prisma/client": "^2.16.1",
|
||||
"@semantic-release/commit-analyzer": "^8.0.1",
|
||||
"@semantic-release/github": "^7.2.0",
|
||||
"@semantic-release/npm": "7.0.8",
|
||||
"@semantic-release/release-notes-generator": "^9.0.1",
|
||||
"@types/react": "^17.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
||||
"@typescript-eslint/parser": "^4.22.0",
|
||||
"autoprefixer": "^9.7.6",
|
||||
"babel-preset-preact": "^2.0.0",
|
||||
"conventional-changelog-conventionalcommits": "4.4.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"dotenv": "^8.2.0",
|
||||
"dtslint": "^4.0.8",
|
||||
"eslint": "^7.19.0",
|
||||
"eslint-config-prettier": "^8.2.0",
|
||||
"eslint-config-standard-with-typescript": "^19.0.1",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.3.1",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"mocha": "^8.1.3",
|
||||
"mongodb": "^3.5.9",
|
||||
"mssql": "^6.2.1",
|
||||
"mysql": "^2.18.1",
|
||||
"next": "^10.0.5",
|
||||
"pg": "^8.2.1",
|
||||
"postcss-cli": "^7.1.1",
|
||||
"postcss-nested": "^4.2.1",
|
||||
"prettier": "^2.2.1",
|
||||
"prisma": "^2.16.1",
|
||||
"puppeteer": "^5.2.1",
|
||||
"puppeteer-extra": "^3.1.15",
|
||||
"puppeteer-extra-plugin-stealth": "^2.6.1",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"typescript": "^4.1.3"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false
|
||||
},
|
||||
"eslintConfig": {
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"extends": [
|
||||
"standard-with-typescript",
|
||||
"prettier"
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"node_modules",
|
||||
"test",
|
||||
"next-env.d.ts",
|
||||
"types",
|
||||
"www",
|
||||
".next",
|
||||
"dist"
|
||||
],
|
||||
"globals": {
|
||||
"localStorage": "readonly",
|
||||
"location": "readonly",
|
||||
"fetch": "readonly"
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"branches": [
|
||||
"+([0-9])?(.{+([0-9]),x}).x",
|
||||
"main",
|
||||
{
|
||||
"name": "beta",
|
||||
"prerelease": true
|
||||
},
|
||||
{
|
||||
"name": "next",
|
||||
"prerelease": true
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/npm",
|
||||
[
|
||||
"@semantic-release/github",
|
||||
{
|
||||
"releasedLabels": false,
|
||||
"successComment": false
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/balazsorban44"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import babel from 'rollup-plugin-babel'
|
||||
|
||||
export default {
|
||||
input: 'src/client/index.js',
|
||||
output: {
|
||||
name: 'next-auth-client',
|
||||
file: 'client.js',
|
||||
format: 'umd',
|
||||
globals: {
|
||||
'fetch': 'isomorphic-fetch'
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
babel({
|
||||
babelrc: false,
|
||||
exclude: [ 'node_modules/**' ],
|
||||
presets: [['env', { modules: false }]]
|
||||
})
|
||||
],
|
||||
|
||||
}
|
||||
8
src/adapters/index.js
Normal file
8
src/adapters/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import TypeORM from './typeorm'
|
||||
import Prisma from './prisma'
|
||||
|
||||
export default {
|
||||
Default: TypeORM.Adapter,
|
||||
TypeORM,
|
||||
Prisma
|
||||
}
|
||||
8
src/adapters/prisma.js
Normal file
8
src/adapters/prisma.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
* Source code is now at:
|
||||
* https://github.com/nextauthjs/adapters/tree/canary/packages/prisma-legacy
|
||||
*/
|
||||
|
||||
import PrismaLegacyAdapter from "@next-auth/prisma-legacy-adapter"
|
||||
|
||||
export default PrismaLegacyAdapter
|
||||
8
src/adapters/typeorm.js
Normal file
8
src/adapters/typeorm.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
* Source code is now at:
|
||||
* https://github.com/nextauthjs/adapters/tree/canary/packages/typeorm-legacy
|
||||
*/
|
||||
|
||||
import TypeORMLegacyAdapter from "@next-auth/typeorm-legacy-adapter"
|
||||
|
||||
export default TypeORMLegacyAdapter
|
||||
@@ -1,8 +1,378 @@
|
||||
'use strict'
|
||||
// Note about signIn() and signOut() methods:
|
||||
//
|
||||
// On signIn() and signOut() we pass 'json: true' to request a response in JSON
|
||||
// instead of HTTP as redirect URLs on other domains are not returned to
|
||||
// requests made using the fetch API in the browser, and we need to ask the API
|
||||
// to return the response as a JSON object (the end point still defaults to
|
||||
// returning an HTTP response with a redirect for non-JavaScript clients).
|
||||
//
|
||||
// We use HTTP POST requests with CSRF Tokens to protect against CSRF attacks.
|
||||
|
||||
import "babel-polyfill"
|
||||
import NextAuth from './next-auth-client'
|
||||
import { useState, useEffect, useContext, createContext, createElement } from 'react'
|
||||
import _logger, { proxyLogger } from '../lib/logger'
|
||||
import parseUrl from '../lib/parse-url'
|
||||
|
||||
export {
|
||||
NextAuth
|
||||
// This behaviour mirrors the default behaviour for getting the site name that
|
||||
// happens server side in server/index.js
|
||||
// 1. An empty value is legitimate when the code is being invoked client side as
|
||||
// relative URLs are valid in that context and so defaults to empty.
|
||||
// 2. When invoked server side the value is picked up from an environment
|
||||
// variable and defaults to 'http://localhost:3000'.
|
||||
/** @type {import("types/internals/client").NextAuthConfig} */
|
||||
const __NEXTAUTH = {
|
||||
baseUrl: parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
|
||||
basePath: parseUrl(process.env.NEXTAUTH_URL).basePath,
|
||||
baseUrlServer: parseUrl(process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
|
||||
basePathServer: parseUrl(process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL).basePath,
|
||||
keepAlive: 0,
|
||||
clientMaxAge: 0,
|
||||
// Properties starting with _ are used for tracking internal app state
|
||||
_clientLastSync: 0,
|
||||
_clientSyncTimer: null,
|
||||
_eventListenersAdded: false,
|
||||
_clientSession: undefined,
|
||||
_getSession: () => {}
|
||||
}
|
||||
|
||||
const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
|
||||
|
||||
const broadcast = BroadcastChannel()
|
||||
|
||||
// Add event listners on load
|
||||
if (typeof window !== 'undefined' && !__NEXTAUTH._eventListenersAdded) {
|
||||
__NEXTAUTH._eventListenersAdded = true
|
||||
// Listen for storage events and update session if event fired from
|
||||
// another window (but suppress firing another event to avoid a loop)
|
||||
// Fetch new session data but tell it to not to fire another event to
|
||||
// avoid an infinite loop.
|
||||
// Note: We could pass session data through and do something like
|
||||
// `setData(message.data)` but that can cause problems depending
|
||||
// on how the session object is being used in the client; it is
|
||||
// more robust to have each window/tab fetch it's own copy of the
|
||||
// session object rather than share it across instances.
|
||||
broadcast.receive(() => __NEXTAUTH._getSession({ event: 'storage' }))
|
||||
|
||||
// Listen for document visibility change events and
|
||||
// if visibility of the document changes, re-fetch the session.
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
!document.hidden && __NEXTAUTH._getSession({ event: 'visibilitychange' })
|
||||
}, false)
|
||||
}
|
||||
|
||||
// Context to store session data globally
|
||||
/** @type {import("types/internals/client").SessionContext} */
|
||||
const SessionContext = createContext()
|
||||
|
||||
export function useSession (session) {
|
||||
const context = useContext(SessionContext)
|
||||
if (context) return context
|
||||
return _useSessionHook(session)
|
||||
}
|
||||
|
||||
function _useSessionHook (session) {
|
||||
const [data, setData] = useState(session)
|
||||
const [loading, setLoading] = useState(!data)
|
||||
|
||||
useEffect(() => {
|
||||
__NEXTAUTH._getSession = async ({ event = null } = {}) => {
|
||||
try {
|
||||
const triggredByEvent = event !== null
|
||||
const triggeredByStorageEvent = event === 'storage'
|
||||
|
||||
const clientMaxAge = __NEXTAUTH.clientMaxAge
|
||||
const clientLastSync = parseInt(__NEXTAUTH._clientLastSync)
|
||||
const currentTime = _now()
|
||||
const clientSession = __NEXTAUTH._clientSession
|
||||
|
||||
// Updates triggered by a storage event *always* trigger an update and we
|
||||
// always update if we don't have any value for the current session state.
|
||||
if (!triggeredByStorageEvent && clientSession !== undefined) {
|
||||
if (clientMaxAge === 0 && triggredByEvent !== true) {
|
||||
// If there is no time defined for when a session should be considered
|
||||
// stale, then it's okay to use the value we have until an event is
|
||||
// triggered which updates it.
|
||||
return
|
||||
} else if (clientMaxAge > 0 && clientSession === null) {
|
||||
// If the client doesn't have a session then we don't need to call
|
||||
// the server to check if it does (if they have signed in via another
|
||||
// tab or window that will come through as a triggeredByStorageEvent
|
||||
// event and will skip this logic)
|
||||
return
|
||||
} else if (clientMaxAge > 0 && currentTime < (clientLastSync + clientMaxAge)) {
|
||||
// If the session freshness is within clientMaxAge then don't request
|
||||
// it again on this call (avoids too many invokations).
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (clientSession === undefined) { __NEXTAUTH._clientSession = null }
|
||||
|
||||
// Update clientLastSync before making response to avoid repeated
|
||||
// invokations that would otherwise be triggered while we are still
|
||||
// waiting for a response.
|
||||
__NEXTAUTH._clientLastSync = _now()
|
||||
|
||||
// If this call was invoked via a storage event (i.e. another window) then
|
||||
// tell getSession not to trigger an event when it calls to avoid an
|
||||
// infinate loop.
|
||||
const newClientSessionData = await getSession({
|
||||
triggerEvent: !triggeredByStorageEvent
|
||||
})
|
||||
|
||||
// Save session state internally, just so we can track that we've checked
|
||||
// if a session exists at least once.
|
||||
__NEXTAUTH._clientSession = newClientSessionData
|
||||
|
||||
setData(newClientSessionData)
|
||||
setLoading(false)
|
||||
} catch (error) {
|
||||
logger.error('CLIENT_USE_SESSION_ERROR', error)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
__NEXTAUTH._getSession()
|
||||
})
|
||||
|
||||
return [data, loading]
|
||||
}
|
||||
|
||||
export async function getSession (ctx) {
|
||||
const session = await _fetchData('session', ctx)
|
||||
if (ctx?.triggerEvent ?? true) {
|
||||
broadcast.post({ event: 'session', data: { trigger: 'getSession' } })
|
||||
}
|
||||
return session
|
||||
}
|
||||
|
||||
export async function getCsrfToken (ctx) {
|
||||
return (await _fetchData('csrf', ctx))?.csrfToken
|
||||
}
|
||||
|
||||
export async function getProviders () {
|
||||
return _fetchData('providers')
|
||||
}
|
||||
|
||||
export async function signIn (provider, options = {}, authorizationParams = {}) {
|
||||
const {
|
||||
callbackUrl = window.location,
|
||||
redirect = true
|
||||
} = options
|
||||
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const providers = await getProviders()
|
||||
|
||||
// Redirect to sign in page if no valid provider specified
|
||||
if (!(provider in providers)) {
|
||||
// If Provider not recognized, redirect to sign in page
|
||||
window.location = `${baseUrl}/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
|
||||
return
|
||||
}
|
||||
const isCredentials = providers[provider].type === 'credentials'
|
||||
const isEmail = providers[provider].type === 'email'
|
||||
const canRedirectBeDisabled = isCredentials || isEmail
|
||||
|
||||
const signInUrl = isCredentials
|
||||
? `${baseUrl}/callback/${provider}`
|
||||
: `${baseUrl}/signin/${provider}`
|
||||
|
||||
// If is any other provider type, POST to provider URL with CSRF Token,
|
||||
// callback URL and any other parameters supplied.
|
||||
const fetchOptions = {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
...options,
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl,
|
||||
json: true
|
||||
})
|
||||
}
|
||||
const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`
|
||||
const res = await fetch(_signInUrl, fetchOptions)
|
||||
const data = await res.json()
|
||||
if (redirect || !canRedirectBeDisabled) {
|
||||
const url = data.url ?? callbackUrl
|
||||
window.location = url
|
||||
// If url contains a hash, the browser does not reload the page. We reload manually
|
||||
if (url.includes('#')) window.location.reload()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const error = new URL(data.url).searchParams.get('error')
|
||||
|
||||
if (res.ok) {
|
||||
await __NEXTAUTH._getSession({ event: 'storage' })
|
||||
}
|
||||
|
||||
return {
|
||||
error,
|
||||
status: res.status,
|
||||
ok: res.ok,
|
||||
url: error ? null : data.url
|
||||
}
|
||||
}
|
||||
|
||||
export async function signOut (options = {}) {
|
||||
const {
|
||||
callbackUrl = window.location,
|
||||
redirect = true
|
||||
} = options
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const fetchOptions = {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl,
|
||||
json: true
|
||||
})
|
||||
}
|
||||
const res = await fetch(`${baseUrl}/signout`, fetchOptions)
|
||||
const data = await res.json()
|
||||
broadcast.post({ event: 'session', data: { trigger: 'signout' } })
|
||||
if (redirect) {
|
||||
const url = data.url ?? callbackUrl
|
||||
window.location = url
|
||||
// If url contains a hash, the browser does not reload the page. We reload manually
|
||||
if (url.includes('#')) window.location.reload()
|
||||
return
|
||||
}
|
||||
|
||||
await __NEXTAUTH._getSession({ event: 'storage' })
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// Method to set options. The documented way is to use the provider, but this
|
||||
// method is being left in as an alternative, that will be helpful if/when we
|
||||
// expose a vanilla JavaScript version that doesn't depend on React.
|
||||
export function setOptions ({ baseUrl, basePath, clientMaxAge, keepAlive } = {}) {
|
||||
if (baseUrl) __NEXTAUTH.baseUrl = baseUrl
|
||||
if (basePath) __NEXTAUTH.basePath = basePath
|
||||
if (clientMaxAge) __NEXTAUTH.clientMaxAge = clientMaxAge
|
||||
if (keepAlive) {
|
||||
__NEXTAUTH.keepAlive = keepAlive
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
// Clear existing timer (if there is one)
|
||||
if (__NEXTAUTH._clientSyncTimer !== null) {
|
||||
clearTimeout(__NEXTAUTH._clientSyncTimer)
|
||||
}
|
||||
|
||||
// Set next timer to trigger in number of seconds
|
||||
__NEXTAUTH._clientSyncTimer = setTimeout(async () => {
|
||||
// Only invoke keepalive when a session exists
|
||||
if (!__NEXTAUTH._clientSession) return
|
||||
await __NEXTAUTH._getSession({ event: 'timer' })
|
||||
}, keepAlive * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
export function Provider ({ children, session, options }) {
|
||||
setOptions(options)
|
||||
return createElement(
|
||||
SessionContext.Provider,
|
||||
{ value: useSession(session) },
|
||||
children
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* If passed 'appContext' via getInitialProps() in _app.js
|
||||
* then get the req object from ctx and use that for the
|
||||
* req value to allow _fetchData to
|
||||
* work seemlessly in getInitialProps() on server side
|
||||
* pages *and* in _app.js.
|
||||
*/
|
||||
async function _fetchData (path, { ctx, req = ctx?.req } = {}) {
|
||||
try {
|
||||
const baseUrl = await _apiBaseUrl()
|
||||
const options = req ? { headers: { cookie: req.headers.cookie } } : {}
|
||||
const res = await fetch(`${baseUrl}/${path}`, options)
|
||||
const data = await res.json()
|
||||
return Object.keys(data).length > 0 ? data : null // Return null if data empty
|
||||
} catch (error) {
|
||||
logger.error('CLIENT_FETCH_ERROR', path, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function _apiBaseUrl () {
|
||||
if (typeof window === 'undefined') {
|
||||
// NEXTAUTH_URL should always be set explicitly to support server side calls - log warning if not set
|
||||
if (!process.env.NEXTAUTH_URL) {
|
||||
logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set')
|
||||
}
|
||||
|
||||
// Return absolute path when called server side
|
||||
return `${__NEXTAUTH.baseUrlServer}${__NEXTAUTH.basePathServer}`
|
||||
}
|
||||
// Return relative path when called client side
|
||||
return __NEXTAUTH.basePath
|
||||
}
|
||||
|
||||
/** Returns the number of seconds elapsed since January 1, 1970 00:00:00 UTC. */
|
||||
function _now () {
|
||||
return Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspired by [Broadcast Channel API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API)
|
||||
* Only not using it directly, because Safari does not support it.
|
||||
*
|
||||
* https://caniuse.com/?search=broadcastchannel
|
||||
*/
|
||||
function BroadcastChannel (name = 'nextauth.message') {
|
||||
return {
|
||||
/**
|
||||
* Get notified by other tabs/windows.
|
||||
* @param {(message: import("types/internals/client").BroadcastMessage) => void} onReceive
|
||||
*/
|
||||
receive (onReceive) {
|
||||
if (typeof window === 'undefined') return
|
||||
window.addEventListener('storage', async (event) => {
|
||||
if (event.key !== name) return
|
||||
/** @type {import("types/internals/client").BroadcastMessage} */
|
||||
const message = JSON.parse(event.newValue)
|
||||
if (message?.event !== 'session' || !message?.data) return
|
||||
|
||||
onReceive(message)
|
||||
})
|
||||
},
|
||||
/** Notify other tabs/windows. */
|
||||
post (message) {
|
||||
if (typeof localStorage === 'undefined') return
|
||||
localStorage.setItem(name,
|
||||
JSON.stringify({ ...message, timestamp: _now() })
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getSession,
|
||||
getCsrfToken,
|
||||
getProviders,
|
||||
useSession,
|
||||
signIn,
|
||||
signOut,
|
||||
Provider,
|
||||
/* Deprecated / unsupported features below this line */
|
||||
// Use setOptions() set options globally in the app.
|
||||
setOptions,
|
||||
// Some methods are exported with more than one name. This provides some
|
||||
// flexibility over how they can be invoked and backwards compatibility
|
||||
// with earlier releases.
|
||||
options: setOptions,
|
||||
session: getSession,
|
||||
providers: getProviders,
|
||||
csrfToken: getCsrfToken,
|
||||
signin: signIn,
|
||||
signout: signOut
|
||||
}
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
import fetch from 'isomorphic-fetch'
|
||||
|
||||
export default class {
|
||||
/**
|
||||
* This is an async, isometric method which returns a session object -
|
||||
* either by looking up the current express session object when run on the
|
||||
* server, or by using fetch (and optionally caching the result in local
|
||||
* storage) when run on the client.
|
||||
*
|
||||
* Note that actual session tokens are not stored in local storage, they are
|
||||
* kept in an HTTP Only cookie as protection against session hi-jacking by
|
||||
* malicious JavaScript.
|
||||
**/
|
||||
static async init({
|
||||
req = null,
|
||||
force = false
|
||||
} = {}) {
|
||||
let session = {}
|
||||
if (req) {
|
||||
if (req.session) {
|
||||
// If running on the server session data should be in the req object
|
||||
session.csrfToken = req.connection._httpMessage.locals._csrf
|
||||
session.expires = req.session.cookie._expires
|
||||
// If the user is logged in, add the user to the session object
|
||||
if (req.user) {
|
||||
session.user = req.user
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If running in the browser attempt to load session from sessionStore
|
||||
if (force === true) {
|
||||
// If force update is set, reset data store
|
||||
this._removeLocalStore('session')
|
||||
} else {
|
||||
session = this._getLocalStore('session')
|
||||
}
|
||||
}
|
||||
|
||||
// If session data exists, has not expired AND force is not set then
|
||||
// return the stored session we already have.
|
||||
if (session && Object.keys(session).length > 0 && session.expires && session.expires > Date.now()) {
|
||||
return new Promise(resolve => {
|
||||
resolve(session)
|
||||
})
|
||||
} else {
|
||||
// If running on server, but session has expired return empty object
|
||||
// (no valid session)
|
||||
if (typeof window === 'undefined') {
|
||||
return new Promise(resolve => {
|
||||
resolve({})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't have session data, or it's expired, or force is set
|
||||
// to true then revalidate it by fetching it again from the server.
|
||||
return fetch('/auth/session', {
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response
|
||||
} else {
|
||||
return Promise.reject(Error('HTTP error when trying to get session'))
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Update session with session info
|
||||
session = data
|
||||
|
||||
// Set a value we will use to check this client should silently
|
||||
// revalidate, using the value for revalidateAge returned by the server.
|
||||
session.expires = Date.now() + session.revalidateAge
|
||||
|
||||
// Save changes to session
|
||||
this._saveLocalStore('session', session)
|
||||
|
||||
return session
|
||||
})
|
||||
.catch(() => Error('Unable to get session'))
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple static method to get the CSRF Token is provided for convenience
|
||||
**/
|
||||
static async csrfToken() {
|
||||
return fetch('/auth/csrf', {
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response
|
||||
} else {
|
||||
return Promise.reject(Error('Unexpected response when trying to get CSRF token'))
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => data.csrfToken)
|
||||
.catch(() => Error('Unable to get CSRF token'))
|
||||
}
|
||||
|
||||
/**
|
||||
* A static method to get list of currently linked oAuth accounts
|
||||
**/
|
||||
static async linked({
|
||||
req = null
|
||||
} = {}) {
|
||||
// If running server side, uses server side method
|
||||
if (req) return req.linked()
|
||||
|
||||
// If running client side, use RESTful endpoint
|
||||
return fetch('/auth/linked', {
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response
|
||||
} else {
|
||||
return Promise.reject(Error('Unexpected response when trying to get linked accounts'))
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => data)
|
||||
.catch(() => Error('Unable to get linked accounts'))
|
||||
}
|
||||
|
||||
/**
|
||||
* A static method to get list of currently configured oAuth providers
|
||||
**/
|
||||
static async providers({
|
||||
req = null
|
||||
} = {}) {
|
||||
// If running server side, uses server side method
|
||||
if (req) return req.providers()
|
||||
|
||||
// If running client side, use RESTful endpoint
|
||||
return fetch('/auth/providers', {
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response
|
||||
} else {
|
||||
console.log("NextAuth Error Fetching Providers")
|
||||
return null
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => data)
|
||||
.catch((e) => {
|
||||
console.log("NextAuth Error Loading Providers")
|
||||
console.log(e)
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Sign in
|
||||
*
|
||||
* Will post a form to /auth/signin auth route if an object is passed.
|
||||
* If the details are valid a session will be created and you should redirect
|
||||
* to your callback page so the session is loaded in the client.
|
||||
*
|
||||
* If just a string containing an email address is specififed will generate a
|
||||
* a one-time use sign in link and send it via email; you should redirect to a
|
||||
* page telling the user to check their inbox for an email with the link.
|
||||
*/
|
||||
static async signin(params) {
|
||||
// Params can be just string (an email address) or an object (form fields)
|
||||
const formData = (typeof params === 'string') ? { email: params } : params
|
||||
|
||||
// Use either the email token generation route or the custom form auth route
|
||||
const route = (typeof params === 'string') ? '/auth/email/signin' : '/auth/signin'
|
||||
|
||||
// Add latest CSRF Token to request
|
||||
formData._csrf = await this.csrfToken()
|
||||
|
||||
// Encoded form parser for sending data in the body
|
||||
const encodedForm = Object.keys(formData).map((key) => {
|
||||
return encodeURIComponent(key) + '=' + encodeURIComponent(formData[key])
|
||||
}).join('&')
|
||||
|
||||
return fetch(route, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest' // So Express can detect AJAX post
|
||||
},
|
||||
body: encodedForm,
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
.then(async response => {
|
||||
if (response.ok) {
|
||||
return await response.json()
|
||||
} else {
|
||||
throw new Error('HTTP error while attempting to sign in')
|
||||
}
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success && data.success === true) {
|
||||
return Promise.resolve(true)
|
||||
} else {
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static async signout() {
|
||||
// Signout from the server
|
||||
const csrfToken = await this.csrfToken()
|
||||
const formData = { _csrf: csrfToken }
|
||||
|
||||
// Encoded form parser for sending data in the body
|
||||
const encodedForm = Object.keys(formData).map((key) => {
|
||||
return encodeURIComponent(key) + '=' + encodeURIComponent(formData[key])
|
||||
}).join('&')
|
||||
|
||||
// Remove cached session data
|
||||
this._removeLocalStore('session')
|
||||
|
||||
return fetch('/auth/signout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: encodedForm,
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
.then(() => {
|
||||
return true
|
||||
})
|
||||
.catch(() => Error('Unable to sign out'))
|
||||
}
|
||||
|
||||
// The Web Storage API is widely supported, but not always available (e.g.
|
||||
// it can be restricted in private browsing mode, triggering an exception).
|
||||
// We handle that silently by just returning null here.
|
||||
static _getLocalStore(name) {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(name))
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
static _saveLocalStore(name, data) {
|
||||
try {
|
||||
localStorage.setItem(name, JSON.stringify(data))
|
||||
return true
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
static _removeLocalStore(name) {
|
||||
try {
|
||||
localStorage.removeItem(name)
|
||||
return true
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
231
src/css/index.css
Normal file
231
src/css/index.css
Normal file
@@ -0,0 +1,231 @@
|
||||
:root {
|
||||
--border-width: 1px;
|
||||
--border-radius: .3rem;
|
||||
--color-error: #c94b4b;
|
||||
--color-info: #157efb;
|
||||
--color-info-text: #fff;
|
||||
}
|
||||
|
||||
.__next-auth-theme-auto,
|
||||
.__next-auth-theme-light {
|
||||
--color-background: #fff;
|
||||
--color-text: #000;
|
||||
--color-primary: #444;
|
||||
--color-control-border: #bbb;
|
||||
--color-button-active-background: #f9f9f9;
|
||||
--color-button-active-border: #aaa;
|
||||
--color-seperator: #ccc;
|
||||
}
|
||||
|
||||
.__next-auth-theme-dark {
|
||||
--color-background: #000;
|
||||
--color-text: #fff;
|
||||
--color-primary: #ccc;
|
||||
--color-control-border: #555;
|
||||
--color-button-active-background: #060606;
|
||||
--color-button-active-border: #666;
|
||||
--color-seperator: #444;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.__next-auth-theme-auto {
|
||||
--color-background: #000;
|
||||
--color-text: #fff;
|
||||
--color-primary: #ccc;
|
||||
--color-control-border: #555;
|
||||
--color-button-active-background: #060606;
|
||||
--color-button-active-border: #666;
|
||||
--color-seperator: #444;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--color-background);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: 400;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 0 1rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--color-text)
|
||||
}
|
||||
|
||||
form {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
margin-bottom: 0.25rem;
|
||||
display: block;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
input[type] {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: .5rem 1rem;
|
||||
border: var(--border-width) solid var(--color-control-border);
|
||||
background: var(--color-background);
|
||||
font-size: 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: inset 0 .1rem .2rem rgba(0, 0, 0, .2);
|
||||
color: var(--color-text);
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 1.5rem 0;
|
||||
padding: 0 1rem;
|
||||
font-size: 1.1rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
a.button {
|
||||
text-decoration: none;
|
||||
line-height: 1rem;
|
||||
|
||||
&:link,
|
||||
&:visited {
|
||||
background-color: var(--color-background);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
button,
|
||||
a.button {
|
||||
margin: 0 0 .75rem 0;
|
||||
padding: .75rem 1rem;
|
||||
border: var(--border-width) solid var(--color-control-border);
|
||||
color: var(--color-primary);
|
||||
background-color: var(--color-background);
|
||||
font-size: 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
transition: all .1s ease-in-out;
|
||||
box-shadow: 0 0.15rem 0.3rem rgba(0, 0, 0, .15), inset 0 .1rem .2rem var(--color-background), inset 0 -.1rem .1rem rgba(0, 0, 0, .05);
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: 0 0.15rem 0.3rem rgba(0, 0, 0, .15), inset 0 .1rem .2rem var(--color-background), inset 0 -.1rem .1rem rgba(0, 0, 0, .1);
|
||||
background-color: var(--color-button-active-background);
|
||||
border-color: var(--color-button-active-border);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
a.site {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
font-size: 1rem;
|
||||
line-height: 2rem;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.page {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: table;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
>div {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
a.button {
|
||||
display: inline-block;
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.signin {
|
||||
|
||||
button,
|
||||
a.button,
|
||||
input[type="text"] {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
hr {
|
||||
display: block;
|
||||
border: 0;
|
||||
border-top: 1px solid var(--color-seperator);
|
||||
margin: 1.5em auto 0 auto;
|
||||
overflow: visible;
|
||||
|
||||
&::before {
|
||||
content: "or";
|
||||
background: var(--color-background);
|
||||
color: #888;
|
||||
padding: 0 .4rem;
|
||||
position: relative;
|
||||
top: -.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #f5f5f5;
|
||||
font-weight: 500;
|
||||
border-radius: 0.3rem;
|
||||
background: var(--color-info);
|
||||
|
||||
p {
|
||||
text-align: left;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.2rem;
|
||||
color: var(--color-info-text);
|
||||
}
|
||||
}
|
||||
|
||||
>div,
|
||||
form {
|
||||
display: block;
|
||||
margin: 0 auto 0.5rem auto;
|
||||
|
||||
input[type] {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
11
src/css/index.js
Normal file
11
src/css/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// To support serverless targets (which don't work if you try to read in things
|
||||
// like CSS files at run time) this file is replaced in production builds with
|
||||
// a function that returns compiled CSS (embedded as a string in the function).
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
const pathToCss = path.join(process.cwd(), '/dist/css/index.css')
|
||||
|
||||
export default function css () {
|
||||
return fs.readFileSync(pathToCss, 'utf8')
|
||||
}
|
||||
98
src/lib/errors.js
Normal file
98
src/lib/errors.js
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Same as the default `Error`, but it is JSON serializable.
|
||||
* @source https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af
|
||||
*/
|
||||
export class UnknownError extends Error {
|
||||
constructor(error) {
|
||||
// Support passing error or string
|
||||
super(error?.message ?? error)
|
||||
this.name = "UnknownError"
|
||||
if (error instanceof Error) {
|
||||
this.stack = error.stack
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
name: this.name,
|
||||
message: this.message,
|
||||
stack: this.stack,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class OAuthCallbackError extends UnknownError {
|
||||
name = "OAuthCallbackError"
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when an Email address is already associated with an account
|
||||
* but the user is trying an OAuth account that is not linked to it.
|
||||
*/
|
||||
export class AccountNotLinkedError extends UnknownError {
|
||||
name = "AccountNotLinkedError"
|
||||
}
|
||||
|
||||
export class CreateUserError extends UnknownError {
|
||||
name = "CreateUserError"
|
||||
}
|
||||
|
||||
export class GetUserError extends UnknownError {
|
||||
name = "GetUserError"
|
||||
}
|
||||
|
||||
export class GetUserByEmailError extends UnknownError {
|
||||
name = "GetUserByEmailError"
|
||||
}
|
||||
|
||||
export class GetUserByIdError extends UnknownError {
|
||||
name = "GetUserByIdError"
|
||||
}
|
||||
|
||||
export class GetUserByProviderAccountIdError extends UnknownError {
|
||||
name = "GetUserByProviderAccountIdError"
|
||||
}
|
||||
|
||||
export class UpdateUserError extends UnknownError {
|
||||
name = "UpdateUserError"
|
||||
}
|
||||
|
||||
export class DeleteUserError extends UnknownError {
|
||||
name = "DeleteUserError"
|
||||
}
|
||||
|
||||
export class LinkAccountError extends UnknownError {
|
||||
name = "LinkAccountError"
|
||||
}
|
||||
|
||||
export class UnlinkAccountError extends UnknownError {
|
||||
name = "UnlinkAccountError"
|
||||
}
|
||||
|
||||
export class CreateSessionError extends UnknownError {
|
||||
name = "CreateSessionError"
|
||||
}
|
||||
|
||||
export class GetSessionError extends UnknownError {
|
||||
name = "GetSessionError"
|
||||
}
|
||||
|
||||
export class UpdateSessionError extends UnknownError {
|
||||
name = "UpdateSessionError"
|
||||
}
|
||||
|
||||
export class DeleteSessionError extends UnknownError {
|
||||
name = "DeleteSessionError"
|
||||
}
|
||||
|
||||
export class CreateVerificationRequestError extends UnknownError {
|
||||
name = "CreateVerificationRequestError"
|
||||
}
|
||||
|
||||
export class GetVerificationRequestError extends UnknownError {
|
||||
name = "GetVerificationRequestError"
|
||||
}
|
||||
|
||||
export class DeleteVerificationRequestError extends UnknownError {
|
||||
name = "DeleteVerificationRequestError"
|
||||
}
|
||||
208
src/lib/jwt.js
Normal file
208
src/lib/jwt.js
Normal file
@@ -0,0 +1,208 @@
|
||||
import crypto from "crypto"
|
||||
import jose from "jose"
|
||||
import logger from "./logger"
|
||||
|
||||
// Set default algorithm to use for auto-generated signing key
|
||||
const DEFAULT_SIGNATURE_ALGORITHM = "HS512"
|
||||
|
||||
// Set default algorithm for auto-generated symmetric encryption key
|
||||
const DEFAULT_ENCRYPTION_ALGORITHM = "A256GCM"
|
||||
|
||||
// Use encryption or not by default
|
||||
const DEFAULT_ENCRYPTION_ENABLED = false
|
||||
|
||||
const DEFAULT_MAX_AGE = 30 * 24 * 60 * 60 // 30 days
|
||||
|
||||
export async function encode({
|
||||
token = {},
|
||||
maxAge = DEFAULT_MAX_AGE,
|
||||
secret,
|
||||
signingKey,
|
||||
signingOptions = {
|
||||
expiresIn: `${maxAge}s`,
|
||||
},
|
||||
encryptionKey,
|
||||
encryptionOptions = {
|
||||
alg: "dir",
|
||||
enc: DEFAULT_ENCRYPTION_ALGORITHM,
|
||||
zip: "DEF",
|
||||
},
|
||||
encryption = DEFAULT_ENCRYPTION_ENABLED,
|
||||
} = {}) {
|
||||
// Signing Key
|
||||
const _signingKey = signingKey
|
||||
? jose.JWK.asKey(JSON.parse(signingKey))
|
||||
: getDerivedSigningKey(secret)
|
||||
|
||||
// Sign token
|
||||
const signedToken = jose.JWT.sign(token, _signingKey, signingOptions)
|
||||
|
||||
if (encryption) {
|
||||
// Encryption Key
|
||||
const _encryptionKey = encryptionKey
|
||||
? jose.JWK.asKey(JSON.parse(encryptionKey))
|
||||
: getDerivedEncryptionKey(secret)
|
||||
|
||||
// Encrypt token
|
||||
return jose.JWE.encrypt(signedToken, _encryptionKey, encryptionOptions)
|
||||
}
|
||||
return signedToken
|
||||
}
|
||||
|
||||
export async function decode({
|
||||
secret,
|
||||
token,
|
||||
maxAge = DEFAULT_MAX_AGE,
|
||||
signingKey,
|
||||
verificationKey = signingKey, // Optional (defaults to encryptionKey)
|
||||
verificationOptions = {
|
||||
maxTokenAge: `${maxAge}s`,
|
||||
algorithms: [DEFAULT_SIGNATURE_ALGORITHM],
|
||||
},
|
||||
encryptionKey,
|
||||
decryptionKey = encryptionKey, // Optional (defaults to encryptionKey)
|
||||
decryptionOptions = {
|
||||
algorithms: [DEFAULT_ENCRYPTION_ALGORITHM],
|
||||
},
|
||||
encryption = DEFAULT_ENCRYPTION_ENABLED,
|
||||
} = {}) {
|
||||
if (!token) return null
|
||||
|
||||
let tokenToVerify = token
|
||||
|
||||
if (encryption) {
|
||||
// Encryption Key
|
||||
const _encryptionKey = decryptionKey
|
||||
? jose.JWK.asKey(JSON.parse(decryptionKey))
|
||||
: getDerivedEncryptionKey(secret)
|
||||
|
||||
// Decrypt token
|
||||
const decryptedToken = jose.JWE.decrypt(
|
||||
token,
|
||||
_encryptionKey,
|
||||
decryptionOptions
|
||||
)
|
||||
tokenToVerify = decryptedToken.toString("utf8")
|
||||
}
|
||||
|
||||
// Signing Key
|
||||
const _signingKey = verificationKey
|
||||
? jose.JWK.asKey(JSON.parse(verificationKey))
|
||||
: getDerivedSigningKey(secret)
|
||||
|
||||
// Verify token
|
||||
return jose.JWT.verify(tokenToVerify, _signingKey, verificationOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Server-side method to retrieve the JWT from `req`.
|
||||
* @param {{
|
||||
* req: NextApiRequest
|
||||
* secureCookie?: boolean
|
||||
* cookieName?: string
|
||||
* raw?: boolean
|
||||
* }} params
|
||||
*/
|
||||
export async function getToken(params) {
|
||||
const {
|
||||
req,
|
||||
// Use secure prefix for cookie name, unless URL is NEXTAUTH_URL is http://
|
||||
// or not set (e.g. development or test instance) case use unprefixed name
|
||||
secureCookie = !(
|
||||
!process.env.NEXTAUTH_URL ||
|
||||
process.env.NEXTAUTH_URL.startsWith("http://")
|
||||
),
|
||||
cookieName = secureCookie
|
||||
? "__Secure-next-auth.session-token"
|
||||
: "next-auth.session-token",
|
||||
raw = false,
|
||||
decode: _decode = decode,
|
||||
} = params
|
||||
if (!req) throw new Error("Must pass `req` to JWT getToken()")
|
||||
|
||||
// Try to get token from cookie
|
||||
let token = req.cookies[cookieName]
|
||||
|
||||
// If cookie not found in cookie look for bearer token in authorization header.
|
||||
// This allows clients that pass through tokens in headers rather than as
|
||||
// cookies to use this helper function.
|
||||
if (!token && req.headers.authorization?.split(" ")[0] === "Bearer") {
|
||||
const urlEncodedToken = req.headers.authorization.split(" ")[1]
|
||||
token = decodeURIComponent(urlEncodedToken)
|
||||
}
|
||||
|
||||
if (raw) {
|
||||
return token
|
||||
}
|
||||
|
||||
try {
|
||||
return _decode({ token, ...params })
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Generate warning (but only once at startup) when auto-generated keys are used
|
||||
let DERIVED_SIGNING_KEY_WARNING = false
|
||||
let DERIVED_ENCRYPTION_KEY_WARNING = false
|
||||
|
||||
// Do the better hkdf of Node.js one added in `v15.0.0` and Third Party one
|
||||
function hkdf(secret, { byteLength, encryptionInfo, digest = "sha256" }) {
|
||||
if (crypto.hkdfSync) {
|
||||
return Buffer.from(
|
||||
crypto.hkdfSync(
|
||||
digest,
|
||||
secret,
|
||||
Buffer.alloc(0),
|
||||
encryptionInfo,
|
||||
byteLength
|
||||
)
|
||||
)
|
||||
}
|
||||
return require("futoin-hkdf")(secret, byteLength, {
|
||||
info: encryptionInfo,
|
||||
hash: digest,
|
||||
})
|
||||
}
|
||||
|
||||
function getDerivedSigningKey(secret) {
|
||||
if (!DERIVED_SIGNING_KEY_WARNING) {
|
||||
logger.warn("JWT_AUTO_GENERATED_SIGNING_KEY")
|
||||
DERIVED_SIGNING_KEY_WARNING = true
|
||||
}
|
||||
|
||||
const buffer = hkdf(secret, {
|
||||
byteLength: 64,
|
||||
encryptionInfo: "NextAuth.js Generated Signing Key",
|
||||
})
|
||||
const key = jose.JWK.asKey(buffer, {
|
||||
alg: DEFAULT_SIGNATURE_ALGORITHM,
|
||||
use: "sig",
|
||||
kid: "nextauth-auto-generated-signing-key",
|
||||
})
|
||||
return key
|
||||
}
|
||||
|
||||
function getDerivedEncryptionKey(secret) {
|
||||
if (!DERIVED_ENCRYPTION_KEY_WARNING) {
|
||||
logger.warn("JWT_AUTO_GENERATED_ENCRYPTION_KEY")
|
||||
DERIVED_ENCRYPTION_KEY_WARNING = true
|
||||
}
|
||||
|
||||
const buffer = hkdf(secret, {
|
||||
byteLength: 32,
|
||||
encryptionInfo: "NextAuth.js Generated Encryption Key",
|
||||
})
|
||||
const key = jose.JWK.asKey(buffer, {
|
||||
alg: DEFAULT_ENCRYPTION_ALGORITHM,
|
||||
use: "enc",
|
||||
kid: "nextauth-auto-generated-encryption-key",
|
||||
})
|
||||
return key
|
||||
}
|
||||
|
||||
export default {
|
||||
encode,
|
||||
decode,
|
||||
getToken,
|
||||
}
|
||||
81
src/lib/logger.js
Normal file
81
src/lib/logger.js
Normal file
@@ -0,0 +1,81 @@
|
||||
/** @type {import("types").LoggerInstance} */
|
||||
const _logger = {
|
||||
error(code, ...message) {
|
||||
console.error(
|
||||
`[next-auth][error][${code.toLowerCase()}]`,
|
||||
`\nhttps://next-auth.js.org/errors#${code.toLowerCase()}`,
|
||||
...message
|
||||
)
|
||||
},
|
||||
warn(code, ...message) {
|
||||
console.warn(
|
||||
`[next-auth][warn][${code.toLowerCase()}]`,
|
||||
`\nhttps://next-auth.js.org/warnings#${code.toLowerCase()}`,
|
||||
...message
|
||||
)
|
||||
},
|
||||
debug(code, ...message) {
|
||||
if (!process?.env?._NEXTAUTH_DEBUG) return
|
||||
console.log(`[next-auth][debug][${code.toLowerCase()}]`, ...message)
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the built-in logger.
|
||||
* Any `undefined` level will use the default logger.
|
||||
* @param {Partial<import("types").LoggerInstance>} newLogger
|
||||
*/
|
||||
export function setLogger(newLogger = {}) {
|
||||
if (newLogger.error) _logger.error = newLogger.error
|
||||
if (newLogger.warn) _logger.warn = newLogger.warn
|
||||
if (newLogger.debug) _logger.debug = newLogger.debug
|
||||
}
|
||||
|
||||
export default _logger
|
||||
|
||||
/**
|
||||
* Serializes client-side log messages and sends them to the server
|
||||
* @param {import("types").LoggerInstance} logger
|
||||
* @param {string} basePath
|
||||
* @return {import("types").LoggerInstance}
|
||||
*/
|
||||
export function proxyLogger(logger = _logger, basePath) {
|
||||
try {
|
||||
if (typeof window === "undefined") {
|
||||
return logger
|
||||
}
|
||||
|
||||
const clientLogger = {}
|
||||
for (const level in logger) {
|
||||
clientLogger[level] = (code, ...message) => {
|
||||
_logger[level](code, ...message) // Log on client as usual
|
||||
|
||||
const url = `${basePath}/_log`
|
||||
const body = new URLSearchParams({
|
||||
level,
|
||||
code,
|
||||
message: JSON.stringify(
|
||||
message.map((m) => {
|
||||
if (m instanceof Error) {
|
||||
// Serializing errors: https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af
|
||||
return { name: m.name, message: m.message, stack: m.stack }
|
||||
}
|
||||
return m
|
||||
})
|
||||
),
|
||||
})
|
||||
if (navigator.sendBeacon) {
|
||||
return navigator.sendBeacon(url, body)
|
||||
}
|
||||
return fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body,
|
||||
})
|
||||
}
|
||||
}
|
||||
return clientLogger
|
||||
} catch {
|
||||
return _logger
|
||||
}
|
||||
}
|
||||
27
src/lib/parse-url.js
Normal file
27
src/lib/parse-url.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Simple universal (client/server) function to split host and path
|
||||
* We use this rather than a library because we need to use the same logic both
|
||||
* client and server side and we only need to parse out the host and path, while
|
||||
* supporting a default value, so a simple split is sufficent.
|
||||
* @param {string} url
|
||||
*/
|
||||
export default function parseUrl (url) {
|
||||
// Default values
|
||||
const defaultHost = 'http://localhost:3000'
|
||||
const defaultPath = '/api/auth'
|
||||
|
||||
if (!url) { url = `${defaultHost}${defaultPath}` }
|
||||
|
||||
// Default to HTTPS if no protocol explictly specified
|
||||
const protocol = url.startsWith('http:') ? 'http' : 'https'
|
||||
|
||||
// Normalize URLs by stripping protocol and no trailing slash
|
||||
url = url.replace(/^https?:\/\//, '').replace(/\/$/, '')
|
||||
|
||||
// Simple split based on first /
|
||||
const [_host, ..._path] = url.split('/')
|
||||
const baseUrl = _host ? `${protocol}://${_host}` : defaultHost
|
||||
const basePath = _path.length > 0 ? `/${_path.join('/')}` : defaultPath
|
||||
|
||||
return { baseUrl, basePath }
|
||||
}
|
||||
@@ -1,347 +0,0 @@
|
||||
/*
|
||||
* Configures Passport Strategies
|
||||
*/
|
||||
'use strict'
|
||||
|
||||
const passport = require('passport')
|
||||
|
||||
module.exports = ({
|
||||
expressApp = null, // Express Server
|
||||
pathPrefix = '/auth', // URL base path for authentication routes
|
||||
providers = [],
|
||||
serverUrl = null,
|
||||
functions = {
|
||||
find: ({
|
||||
id,
|
||||
email,
|
||||
emailToken,
|
||||
provider
|
||||
} = {}) => {},
|
||||
update: (user, profile) => {},
|
||||
insert: (user, profile) => {},
|
||||
serialize: (user) => {},
|
||||
deserialize: (id) => {}
|
||||
}
|
||||
} = {}) => {
|
||||
if (expressApp === null) {
|
||||
throw new Error('expressApp must be an instance of an express server')
|
||||
}
|
||||
|
||||
if (typeof(functions) !== 'object') {
|
||||
throw new Error('functions must be a an object')
|
||||
}
|
||||
|
||||
/*
|
||||
* Return functions ID property from a functions object
|
||||
*/
|
||||
passport.serializeUser((user, next) => {
|
||||
functions.serialize(user)
|
||||
.then(id => {
|
||||
next(null, id)
|
||||
})
|
||||
.catch(err => {
|
||||
next(err, false)
|
||||
})
|
||||
})
|
||||
|
||||
/*
|
||||
* Return functions from a functions ID
|
||||
*/
|
||||
passport.deserializeUser((id, next) => {
|
||||
functions.deserialize(id)
|
||||
.then(user => {
|
||||
if (!user) return next(null, false)
|
||||
next(null, user)
|
||||
})
|
||||
.catch(err => {
|
||||
next(err, false)
|
||||
})
|
||||
})
|
||||
|
||||
// Define a Passport strategy for provider
|
||||
providers.forEach(({
|
||||
providerName,
|
||||
Strategy,
|
||||
strategyOptions,
|
||||
getProfile
|
||||
}) => {
|
||||
|
||||
strategyOptions.callbackURL = (strategyOptions.callbackURL || (serverUrl || '') + `${pathPrefix}/oauth/${providerName.toLowerCase()}/callback`)
|
||||
strategyOptions.passReqToCallback = true
|
||||
|
||||
passport.use(providerName, new Strategy(strategyOptions, (req, accessToken, refreshToken, _params, _profile, next) => {
|
||||
|
||||
try {
|
||||
// Normalise the provider specific profile into a standard basic
|
||||
// profile object with just { id, name, email } properties.
|
||||
let profile = getProfile(_profile)
|
||||
|
||||
const linkToExistingAccount = user => {
|
||||
const nextUser = Object.assign({}, user)
|
||||
|
||||
// If we don't already have a name for the user, use value the
|
||||
// name value specfied in their profile on the remote service.
|
||||
nextUser.name = user.name || profile.name
|
||||
|
||||
// If we don't have a real email address for the user, use the
|
||||
// email value specified in their profile on the remote service.
|
||||
if (
|
||||
user.email &&
|
||||
user.email.match(/.*@localhost\.localdomain$/) &&
|
||||
profile.email &&
|
||||
!profile.email.match(/.*@localhost\.localdomain$/)
|
||||
) {
|
||||
nextUser.emailVerified = false
|
||||
nextUser.email = profile.email
|
||||
}
|
||||
|
||||
// Save Profile ID, Access Token and Refresh Token values
|
||||
// to the users local account, which links the accounts.
|
||||
nextUser[providerName.toLowerCase()] = {
|
||||
id: profile.id,
|
||||
accessToken: accessToken,
|
||||
refreshToken: refreshToken
|
||||
};
|
||||
|
||||
// Update details for the new provider for this user.
|
||||
return functions.update(nextUser, _profile, _params)
|
||||
}
|
||||
|
||||
// Save the Access Token to the current session.
|
||||
req.session[providerName.toLowerCase()] = {
|
||||
accessToken: accessToken
|
||||
}
|
||||
|
||||
// If we didn't get an email address from the oAuth provider then
|
||||
// generate a unique one as placeholder, using Provider name and ID.
|
||||
//
|
||||
// If you want users to specify a valid email address after signing in,
|
||||
// you can check for email addresses ending "@localhost.localdomain"
|
||||
// and prompt those users to supply a valid address.
|
||||
if (!profile.email) {
|
||||
profile.email = `${providerName.toLowerCase()}-${profile.id}@localhost.localdomain`
|
||||
}
|
||||
|
||||
// Look for a user in the database associated with this account.
|
||||
functions.find({
|
||||
provider: {
|
||||
name: providerName.toLowerCase(),
|
||||
id: profile.id
|
||||
}
|
||||
})
|
||||
.then(user => {
|
||||
if (req.user) {
|
||||
// This section handles scenarios when a user is already signed in.
|
||||
|
||||
if (user) {
|
||||
// This section handles if the user is already logged in
|
||||
if (req.user.id === user.id) {
|
||||
// This section handles if the user is already logged in and is
|
||||
// already linked to local account they are signed in with.
|
||||
// If they are, all we need to do is update the Refresh Token
|
||||
// value if we got one.
|
||||
if (refreshToken) {
|
||||
user[providerName.toLowerCase()] = {
|
||||
id: profile.id,
|
||||
accessToken: accessToken,
|
||||
refreshToken: refreshToken
|
||||
}
|
||||
|
||||
functions.update(user, _profile, _params)
|
||||
.then(user => {
|
||||
return next(null, user)
|
||||
})
|
||||
.catch(err => {
|
||||
next(err)
|
||||
})
|
||||
} else {
|
||||
return next(null, user)
|
||||
}
|
||||
} else {
|
||||
// This section handles if a user is logged in but the oAuth
|
||||
// account they are trying to link to is already linked to a
|
||||
// different local account.
|
||||
|
||||
// This prevents users from linking an oAuth account to more
|
||||
// than one local account at the same time.
|
||||
return next(null, false)
|
||||
}
|
||||
} else {
|
||||
// This secion handles if a user is already logged in and is
|
||||
// trying to link a new account to the existing local account.
|
||||
|
||||
// First get the User ID from the User, then look up the user
|
||||
// details. Note: We don't use the User object in req.user
|
||||
// directly as it is a simplified set of properties set by
|
||||
// functions.deserialize().
|
||||
return functions.serialize(req.user)
|
||||
.then(id => {
|
||||
if (!id) throw new Error("Unable to serialize user")
|
||||
return functions.find({ id: id })
|
||||
})
|
||||
.then(user => {
|
||||
// This error should not happen, unless the currently signed in
|
||||
// user has been deleted deleted from the database since
|
||||
// signing in (or there is a problem talking to the database).
|
||||
if (!user) return next(new Error("Unable to look up account for current user"), false);
|
||||
return linkToExistingAccount(user)
|
||||
})
|
||||
.then(user => {
|
||||
next(null, user)
|
||||
})
|
||||
.catch(err => next(err, false));
|
||||
}
|
||||
|
||||
} else {
|
||||
// This section handles scenarios when a user is not logged in.
|
||||
|
||||
if (user) {
|
||||
// This section handles senarios where the user is not logged in
|
||||
// but they seem to have an account already, so we sign them in
|
||||
// as that user.
|
||||
|
||||
// Update Access and Refresh Tokens for the user if we got them.
|
||||
if (accessToken || refreshToken) {
|
||||
if (accessToken) user[providerName.toLowerCase()].accessToken = accessToken
|
||||
if (refreshToken) user[providerName.toLowerCase()].refreshToken = refreshToken
|
||||
return functions.update(user, _profile, _params)
|
||||
.then(user => {
|
||||
return next(null, user)
|
||||
})
|
||||
.catch(err => {
|
||||
return next(err, false)
|
||||
})
|
||||
} else {
|
||||
return next(null, user)
|
||||
}
|
||||
} else {
|
||||
// This section handles senarios where the user is not logged in
|
||||
// and they don't have a local account already.
|
||||
|
||||
// First we check to see if a local account with the same email
|
||||
// address as the one associated with their oAuth profile exists.
|
||||
//
|
||||
// This is so they can't accidentally end up with two accounts
|
||||
// linked to the same email address.
|
||||
return functions.find({email: profile.email})
|
||||
.then(user => {
|
||||
|
||||
if (user) {
|
||||
// We already have a local account associated with their
|
||||
// email address.
|
||||
|
||||
// If the provider is trustworthy and doesn't allow to create accounts
|
||||
// for another user emails, then we allow to link this new account to
|
||||
// the corresponding local account.
|
||||
//
|
||||
// Note: By default, we treat all providers as not trustworthy. Enable
|
||||
// the `trustedIdentity` flag at your own risk and only to those providers,
|
||||
// who you are sure will guarantee that identities can not be spoofed.
|
||||
const provider = providers.find(provider => provider.providerName === providerName)
|
||||
if (provider && provider.trustedIdentity) {
|
||||
return linkToExistingAccount(user)
|
||||
.then(user => {
|
||||
next(null, user)
|
||||
})
|
||||
.catch(err => next(err, false))
|
||||
}
|
||||
|
||||
// Otherwise, the user should sign in with his/her local account first -
|
||||
// and then they can link accounts if they wish.
|
||||
//
|
||||
// Note: Automatically linking insufficiently trusted accounts here could
|
||||
// expose a potential security exploit allowing someone to pre-register
|
||||
// or create an account elsewhere for another users email
|
||||
// address then trying to sign in from it, so don't do that.
|
||||
return next(null, false)
|
||||
}
|
||||
|
||||
// If an account does not exist, create one for them and return
|
||||
// a user object to passport, which will sign them in.
|
||||
return functions.insert({
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
[providerName.toLowerCase()]: {
|
||||
id: profile.id,
|
||||
accessToken: accessToken,
|
||||
refreshToken: refreshToken
|
||||
}
|
||||
}, _profile, _params)
|
||||
.then(user => {
|
||||
return next(null, user)
|
||||
})
|
||||
.catch(err => {
|
||||
return next(err, false)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
next(err, false)
|
||||
})
|
||||
} catch (err) {
|
||||
return next(err, false)
|
||||
}
|
||||
|
||||
}))
|
||||
})
|
||||
|
||||
// Initialise Passport
|
||||
expressApp.use(passport.initialize())
|
||||
expressApp.use(passport.session())
|
||||
|
||||
// Add routes for each provider
|
||||
providers.forEach(({
|
||||
providerName,
|
||||
providerOptions
|
||||
}) => {
|
||||
// Route to start sign in
|
||||
expressApp.get(`${pathPrefix}/oauth/${providerName.toLowerCase()}`, passport.authenticate(providerName, providerOptions))
|
||||
|
||||
// Route to call back to after signing in
|
||||
expressApp.get(`${pathPrefix}/oauth/${providerName.toLowerCase()}/callback`,
|
||||
passport.authenticate(providerName, {
|
||||
successRedirect: `${pathPrefix}/callback?action=signin&service=${providerName}`,
|
||||
failureRedirect: `${pathPrefix}/error?action=signin&type=oauth&service=${providerName}`
|
||||
})
|
||||
)
|
||||
|
||||
// Route to post to unlink accounts
|
||||
expressApp.post(`${pathPrefix}/oauth/${providerName.toLowerCase()}/unlink`, (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return next(new Error('Not signed in'))
|
||||
}
|
||||
|
||||
// First get the User ID from the User, then look up the user details.
|
||||
// Note: We don't use the User object in req.user directly as it is a
|
||||
// a simplified set of properties set by functions.deserialize().
|
||||
functions.serialize(req.user)
|
||||
.then(id => {
|
||||
if (!id) throw new Error("Unable to serialize user")
|
||||
return functions.find({ id: id })
|
||||
})
|
||||
.then(user => {
|
||||
if (!user) return next(new Error('Unable to look up account for current user'))
|
||||
|
||||
// Remove connection between user account and oauth provider
|
||||
if (user[providerName.toLowerCase()]) {
|
||||
delete user[providerName.toLowerCase()]
|
||||
}
|
||||
|
||||
return functions.update(user, null, { delete: providerName.toLowerCase() })
|
||||
.then(user => {
|
||||
return res.redirect(`${pathPrefix}/callback?action=unlink&service=${providerName.toLowerCase()}`)
|
||||
})
|
||||
.catch(err => {
|
||||
return next(err, false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// A catch all for providers that are not configured
|
||||
expressApp.get(`${pathPrefix}/oauth/:provider`, (req, res) => {
|
||||
return res.redirect(`${pathPrefix}/error?action=signin&type=unsupported`)
|
||||
})
|
||||
}
|
||||
20
src/providers/42.js
Normal file
20
src/providers/42.js
Normal file
@@ -0,0 +1,20 @@
|
||||
export default function FortyTwo(options) {
|
||||
return {
|
||||
id: '42-school',
|
||||
name: '42 School',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://api.intra.42.fr/oauth/token',
|
||||
authorizationUrl:
|
||||
'https://api.intra.42.fr/oauth/authorize?response_type=code',
|
||||
profileUrl: 'https://api.intra.42.fr/v2/me',
|
||||
profile: (profile) => ({
|
||||
id: profile.id,
|
||||
email: profile.email,
|
||||
image: profile.image_url,
|
||||
name: profile.usual_full_name,
|
||||
}),
|
||||
...options,
|
||||
}
|
||||
}
|
||||
34
src/providers/apple.js
Normal file
34
src/providers/apple.js
Normal file
@@ -0,0 +1,34 @@
|
||||
export default function Apple(options) {
|
||||
return {
|
||||
id: "apple",
|
||||
name: "Apple",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "name email",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://appleid.apple.com/auth/token",
|
||||
authorizationUrl:
|
||||
"https://appleid.apple.com/auth/authorize?response_type=code&id_token&response_mode=form_post",
|
||||
profileUrl: null,
|
||||
idToken: true,
|
||||
profile(profile) {
|
||||
// The name of the user will only return on first login
|
||||
return {
|
||||
id: profile.sub,
|
||||
name:
|
||||
profile.user != null
|
||||
? profile.user.name.firstName + " " + profile.user.name.lastName
|
||||
: null,
|
||||
email: profile.email,
|
||||
}
|
||||
},
|
||||
clientId: null,
|
||||
clientSecret: {
|
||||
teamId: null,
|
||||
privateKey: null,
|
||||
keyId: null,
|
||||
},
|
||||
protection: "none", // REVIEW: Apple does not support state, as far as I know. Can we use "pkce" then?
|
||||
...options,
|
||||
}
|
||||
}
|
||||
24
src/providers/atlassian.js
Normal file
24
src/providers/atlassian.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export default function Atlassian(options) {
|
||||
return {
|
||||
id: "atlassian",
|
||||
name: "Atlassian",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
params: {
|
||||
grant_type: "authorization_code",
|
||||
},
|
||||
accessTokenUrl: "https://auth.atlassian.com/oauth/token",
|
||||
authorizationUrl:
|
||||
"https://auth.atlassian.com/authorize?audience=api.atlassian.com&response_type=code&prompt=consent",
|
||||
profileUrl: "https://api.atlassian.com/me",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.account_id,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: profile.picture,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
}
|
||||
22
src/providers/auth0.js
Normal file
22
src/providers/auth0.js
Normal file
@@ -0,0 +1,22 @@
|
||||
export default function Auth0(options) {
|
||||
return {
|
||||
id: "auth0",
|
||||
name: "Auth0",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
params: { grant_type: "authorization_code" },
|
||||
scope: "openid email profile",
|
||||
accessTokenUrl: `https://${options.domain}/oauth/token`,
|
||||
authorizationUrl: `https://${options.domain}/authorize?response_type=code`,
|
||||
profileUrl: `https://${options.domain}/userinfo`,
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.nickname,
|
||||
email: profile.email,
|
||||
image: profile.picture,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
}
|
||||
24
src/providers/azure-ad-b2c.js
Normal file
24
src/providers/azure-ad-b2c.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export default function AzureADB2C(options) {
|
||||
const tenant = options.tenantId ? options.tenantId : "common"
|
||||
|
||||
return {
|
||||
id: "azure-ad-b2c",
|
||||
name: "Azure Active Directory B2C",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
params: {
|
||||
grant_type: "authorization_code",
|
||||
},
|
||||
accessTokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
|
||||
authorizationUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize?response_type=code&response_mode=query`,
|
||||
profileUrl: "https://graph.microsoft.com/v1.0/me/",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.displayName,
|
||||
email: profile.userPrincipalName,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
}
|
||||
22
src/providers/basecamp.js
Normal file
22
src/providers/basecamp.js
Normal file
@@ -0,0 +1,22 @@
|
||||
export default function Basecamp(options) {
|
||||
return {
|
||||
id: "basecamp",
|
||||
name: "Basecamp",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
accessTokenUrl:
|
||||
"https://launchpad.37signals.com/authorization/token?type=web_server",
|
||||
authorizationUrl:
|
||||
"https://launchpad.37signals.com/authorization/new?type=web_server",
|
||||
profileUrl: "https://launchpad.37signals.com/authorization.json",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.identity.id,
|
||||
name: `${profile.identity.first_name} ${profile.identity.last_name}`,
|
||||
email: profile.identity.email_address,
|
||||
image: null,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
}
|
||||
29
src/providers/battlenet.js
Normal file
29
src/providers/battlenet.js
Normal file
@@ -0,0 +1,29 @@
|
||||
export default function BattleNet(options) {
|
||||
const { region } = options
|
||||
return {
|
||||
id: "battlenet",
|
||||
name: "Battle.net",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "openid",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl:
|
||||
region === "CN"
|
||||
? "https://www.battlenet.com.cn/oauth/token"
|
||||
: `https://${region}.battle.net/oauth/token`,
|
||||
authorizationUrl:
|
||||
region === "CN"
|
||||
? "https://www.battlenet.com.cn/oauth/authorize?response_type=code"
|
||||
: `https://${region}.battle.net/oauth/authorize?response_type=code`,
|
||||
profileUrl: "https://us.battle.net/oauth/userinfo",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.battletag,
|
||||
email: null,
|
||||
image: null,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
}
|
||||
23
src/providers/box.js
Normal file
23
src/providers/box.js
Normal file
@@ -0,0 +1,23 @@
|
||||
export default function Box(options) {
|
||||
return {
|
||||
id: "box",
|
||||
name: "Box",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://api.box.com/oauth2/token",
|
||||
authorizationUrl:
|
||||
"https://account.box.com/api/oauth2/authorize?response_type=code",
|
||||
profileUrl: "https://api.box.com/2.0/users/me",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
email: profile.login,
|
||||
image: profile.avatar_url,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user