@diskominfo/sso-kukar-sdk-js 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +218 -0
- package/bun.lock +172 -0
- package/dist/sso-kukar-sdk-express.js +272 -0
- package/package.json +26 -0
- package/resource/callback.html +6 -0
- package/resource/client.js +159 -0
- package/src/SsoKukarClient.ts +98 -0
- package/src/SsoKukarFrontend.ts +135 -0
- package/src/SsoKukarSdkExpress.ts +201 -0
package/README.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# SSO Kukar SDK Express
|
|
2
|
+
|
|
3
|
+
## Instalasi
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install git+https://reporepo.kukarkab.go.id/rizkipadhil/sso-kukar-sdk-express
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Frontend
|
|
10
|
+
|
|
11
|
+
pastikan **application** dan **callback url** telah terdaftar pada Server SSO Kukar,
|
|
12
|
+
kemudian gunakan salah satu metode dibawah
|
|
13
|
+
|
|
14
|
+
### Handle Dengan Redirect
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import SsoKukarFrontend from "sso-kukar-sdk-express/client";
|
|
18
|
+
|
|
19
|
+
// konfigurasi
|
|
20
|
+
const sso = new SsoKukarFrontend({
|
|
21
|
+
client_id: "masukkan-client-id",
|
|
22
|
+
auth_url: "masukkan-url-sso-server",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// pindah ke halaman login atau callback
|
|
26
|
+
await sso.redirectLogin();
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
pada halaman callback
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import SsoKukarFrontend from "sso-kukar-sdk-express/client";
|
|
35
|
+
|
|
36
|
+
const sso = new SsoKukarFrontend({
|
|
37
|
+
client_id: "masukkan-client-id",
|
|
38
|
+
auth_url: "masukkan-url-sso-server",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// penggunaan
|
|
42
|
+
const userData = await sso.handleRedirectLogin();
|
|
43
|
+
|
|
44
|
+
/*
|
|
45
|
+
{
|
|
46
|
+
payload: {
|
|
47
|
+
user: {...}
|
|
48
|
+
...
|
|
49
|
+
},
|
|
50
|
+
tokens: {
|
|
51
|
+
access_token: ...,
|
|
52
|
+
refresh_token: ...
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
*/
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
simpan `access_token` dan `refresh_token` yang didapatkan dari `handleRedirectLogin`
|
|
61
|
+
pada local storage atau cookies
|
|
62
|
+
|
|
63
|
+
ketika mengakes endpoint ke backend, taruh `access_token` pada request header
|
|
64
|
+
|
|
65
|
+
`Authorization: Bearer {access_token}`
|
|
66
|
+
|
|
67
|
+
### Handle Dengan Popup
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import SsoKukarFrontend from "sso-kukar-sdk-express/client";
|
|
71
|
+
|
|
72
|
+
// konfigurasi
|
|
73
|
+
const sso = new SsoKukarFrontend({
|
|
74
|
+
client_id: "masukkan-client-id",
|
|
75
|
+
auth_url: "masukkan-url-sso-server",
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// penggunaan data
|
|
79
|
+
const userData = await sso.openLoginPopup();
|
|
80
|
+
|
|
81
|
+
/*
|
|
82
|
+
{
|
|
83
|
+
payload: {
|
|
84
|
+
user: {...}
|
|
85
|
+
...
|
|
86
|
+
},
|
|
87
|
+
tokens: {
|
|
88
|
+
access_token: ...,
|
|
89
|
+
refresh_token: ...
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
*/
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
pada halaman callback
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import SsoKukarFrontend from "sso-kukar-sdk-express/client";
|
|
101
|
+
|
|
102
|
+
const sso = new SsoKukarFrontend({
|
|
103
|
+
client_id: "masukkan-client-id",
|
|
104
|
+
auth_url: "masukkan-url-sso-server",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
sso.closeLoginPopup();
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
atau bisa juga dengan snippet dibawah ini
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const queryParams = new URLSearchParams(window.location.search);
|
|
114
|
+
const code = queryParams.get("code");
|
|
115
|
+
window.opener.postMessage({ code });
|
|
116
|
+
window.close();
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
simpan `access_token` dan `refresh_token` yang didapatkan dari `openLoginPopup`
|
|
122
|
+
pada local storage atau cookies
|
|
123
|
+
|
|
124
|
+
ketika mengakes endpoint ke backend, taruh `access_token` pada request header
|
|
125
|
+
|
|
126
|
+
`Authorization: Bearer {access_token}`
|
|
127
|
+
|
|
128
|
+
## Backend
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import express from "express";
|
|
132
|
+
import SsoKukarSdkExpress from "sso-kukar-sdk-express";
|
|
133
|
+
|
|
134
|
+
const app = express();
|
|
135
|
+
|
|
136
|
+
const sso = new SsoKukarSdkExpress({
|
|
137
|
+
client_id: "masukkan-client-id",
|
|
138
|
+
client_secret: "masukkan-client-secret",
|
|
139
|
+
auth_url: "masukkan-url-sso-server",
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// register callback handler, endpoint yang akan memproses code dari SSO Kukar
|
|
143
|
+
app.use("/callback", sso.callbackHandler(req, res) => {
|
|
144
|
+
// setelah callback berhasil, redirect ke halaman dashboard atau halaman lain
|
|
145
|
+
res.redirect('/dashboard');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// register middleware untuk endpoint yang memerlukan otentikasi
|
|
149
|
+
app.use('/restricted', sso.middleware(), (req, res) => {
|
|
150
|
+
// data user yang telah diverifikasi akan tersedia di req.user_sso
|
|
151
|
+
res.json({ user: req.user_sso });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// register endpoint untuk logout
|
|
155
|
+
app.use('/logout', sso.logoutHandler());
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
middleware ini memeriksa `access_token` pada header `Authorization` atau cookies
|
|
159
|
+
dan secara otomatis mengisi `req.user_sso` dengan data pengguna yang telah diverifikasi.
|
|
160
|
+
|
|
161
|
+
apabila `access_token` berasal dari cookies dan tidak valid, maka akan mencoba untuk
|
|
162
|
+
mengambil `refresh_token` dari cookies dan memperbarui `access_token` jika berhasil.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
jika callback dilakukan pada halaman popup, ganti `callbackHandler` dengan `callbackPopupHandler`
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
app.use("/callback", sso.callbackPopupHandler());
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
jika ingin agar import bisa embed pada header html, gunakan `frontendHandler`
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
app.use("/sso-client.js", sso.frontendHandler());
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
pada frontend, import seperti ini:
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
import SsoKukarFrontend from "/sso-client.js";
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Validasi
|
|
187
|
+
|
|
188
|
+
untuk validasi secara manual baik pada frontend maupun backend, gunakan `verifyAccessToken`
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
const verify_token = await sso.verifyAccessToken("masukkan-access-token");
|
|
192
|
+
/*
|
|
193
|
+
{
|
|
194
|
+
verified: true | false,
|
|
195
|
+
expired: true | false,
|
|
196
|
+
data: {
|
|
197
|
+
user: {...},
|
|
198
|
+
...
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
*/
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
apabila `access_token` tidak bisa lagi divalidasi, minta lagi `access_token` baru dengan `refreshAccessToken`
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
const response_token = await sso.refreshAccessToken("masukkan-refresh-token");
|
|
210
|
+
/*
|
|
211
|
+
{
|
|
212
|
+
success: true,
|
|
213
|
+
data: {
|
|
214
|
+
access_token: ...,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
*/
|
|
218
|
+
```
|
package/bun.lock
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 0,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "sso-kukar-sdk-express",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@types/express": "^5.0.3",
|
|
9
|
+
"express": "^5.1.0",
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
"packages": {
|
|
14
|
+
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
|
|
15
|
+
|
|
16
|
+
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
|
17
|
+
|
|
18
|
+
"@types/express": ["@types/express@5.0.3", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw=="],
|
|
19
|
+
|
|
20
|
+
"@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA=="],
|
|
21
|
+
|
|
22
|
+
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
|
|
23
|
+
|
|
24
|
+
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
|
|
25
|
+
|
|
26
|
+
"@types/node": ["@types/node@24.0.3", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg=="],
|
|
27
|
+
|
|
28
|
+
"@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
|
|
29
|
+
|
|
30
|
+
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
|
|
31
|
+
|
|
32
|
+
"@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="],
|
|
33
|
+
|
|
34
|
+
"@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="],
|
|
35
|
+
|
|
36
|
+
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
|
37
|
+
|
|
38
|
+
"body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
|
|
39
|
+
|
|
40
|
+
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
|
41
|
+
|
|
42
|
+
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
|
43
|
+
|
|
44
|
+
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
|
45
|
+
|
|
46
|
+
"content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="],
|
|
47
|
+
|
|
48
|
+
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
|
49
|
+
|
|
50
|
+
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
|
51
|
+
|
|
52
|
+
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
|
53
|
+
|
|
54
|
+
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
|
55
|
+
|
|
56
|
+
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
|
57
|
+
|
|
58
|
+
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
|
59
|
+
|
|
60
|
+
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
|
61
|
+
|
|
62
|
+
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
|
63
|
+
|
|
64
|
+
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
|
65
|
+
|
|
66
|
+
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
|
67
|
+
|
|
68
|
+
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
|
69
|
+
|
|
70
|
+
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
|
71
|
+
|
|
72
|
+
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
|
73
|
+
|
|
74
|
+
"express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
|
|
75
|
+
|
|
76
|
+
"finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="],
|
|
77
|
+
|
|
78
|
+
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
|
79
|
+
|
|
80
|
+
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
|
81
|
+
|
|
82
|
+
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
|
83
|
+
|
|
84
|
+
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
|
85
|
+
|
|
86
|
+
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
|
87
|
+
|
|
88
|
+
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
|
89
|
+
|
|
90
|
+
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
|
91
|
+
|
|
92
|
+
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
|
93
|
+
|
|
94
|
+
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
|
|
95
|
+
|
|
96
|
+
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
|
97
|
+
|
|
98
|
+
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
|
99
|
+
|
|
100
|
+
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
|
101
|
+
|
|
102
|
+
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
|
103
|
+
|
|
104
|
+
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
|
105
|
+
|
|
106
|
+
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
|
107
|
+
|
|
108
|
+
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
|
109
|
+
|
|
110
|
+
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
|
111
|
+
|
|
112
|
+
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
|
113
|
+
|
|
114
|
+
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
|
115
|
+
|
|
116
|
+
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
|
117
|
+
|
|
118
|
+
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
|
119
|
+
|
|
120
|
+
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
|
121
|
+
|
|
122
|
+
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
|
123
|
+
|
|
124
|
+
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
|
125
|
+
|
|
126
|
+
"path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="],
|
|
127
|
+
|
|
128
|
+
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
|
129
|
+
|
|
130
|
+
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
|
|
131
|
+
|
|
132
|
+
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
|
133
|
+
|
|
134
|
+
"raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="],
|
|
135
|
+
|
|
136
|
+
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
|
137
|
+
|
|
138
|
+
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
|
139
|
+
|
|
140
|
+
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
|
141
|
+
|
|
142
|
+
"send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="],
|
|
143
|
+
|
|
144
|
+
"serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="],
|
|
145
|
+
|
|
146
|
+
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
|
147
|
+
|
|
148
|
+
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
|
149
|
+
|
|
150
|
+
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
|
151
|
+
|
|
152
|
+
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
|
153
|
+
|
|
154
|
+
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
|
155
|
+
|
|
156
|
+
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
|
157
|
+
|
|
158
|
+
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
|
159
|
+
|
|
160
|
+
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
|
161
|
+
|
|
162
|
+
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
|
163
|
+
|
|
164
|
+
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
|
165
|
+
|
|
166
|
+
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
|
167
|
+
|
|
168
|
+
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
|
169
|
+
|
|
170
|
+
"http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
8
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
9
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
10
|
+
for (let key of __getOwnPropNames(mod))
|
|
11
|
+
if (!__hasOwnProp.call(to, key))
|
|
12
|
+
__defProp(to, key, {
|
|
13
|
+
get: () => mod[key],
|
|
14
|
+
enumerable: true
|
|
15
|
+
});
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __moduleCache = /* @__PURE__ */ new WeakMap;
|
|
19
|
+
var __toCommonJS = (from) => {
|
|
20
|
+
var entry = __moduleCache.get(from), desc;
|
|
21
|
+
if (entry)
|
|
22
|
+
return entry;
|
|
23
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
24
|
+
if (from && typeof from === "object" || typeof from === "function")
|
|
25
|
+
__getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
|
|
26
|
+
get: () => from[key],
|
|
27
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
28
|
+
}));
|
|
29
|
+
__moduleCache.set(from, entry);
|
|
30
|
+
return entry;
|
|
31
|
+
};
|
|
32
|
+
var __export = (target, all) => {
|
|
33
|
+
for (var name in all)
|
|
34
|
+
__defProp(target, name, {
|
|
35
|
+
get: all[name],
|
|
36
|
+
enumerable: true,
|
|
37
|
+
configurable: true,
|
|
38
|
+
set: (newValue) => all[name] = () => newValue
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// src/SsoKukarSdkExpress.ts
|
|
43
|
+
var exports_SsoKukarSdkExpress = {};
|
|
44
|
+
__export(exports_SsoKukarSdkExpress, {
|
|
45
|
+
default: () => SsoKukarSdkExpress,
|
|
46
|
+
SsoKukarSdkError: () => SsoKukarSdkError
|
|
47
|
+
});
|
|
48
|
+
module.exports = __toCommonJS(exports_SsoKukarSdkExpress);
|
|
49
|
+
var import_path = __toESM(require("path"));
|
|
50
|
+
|
|
51
|
+
// src/SsoKukarClient.ts
|
|
52
|
+
class SsoKukarClient {
|
|
53
|
+
config;
|
|
54
|
+
#pubkey;
|
|
55
|
+
constructor(config) {
|
|
56
|
+
this.config = config;
|
|
57
|
+
}
|
|
58
|
+
async fetchPublicKey() {
|
|
59
|
+
if (this.#pubkey) {
|
|
60
|
+
return this.#pubkey;
|
|
61
|
+
}
|
|
62
|
+
const pubkeyPath = `${this.config.auth_url}/.well-known/public.pem`;
|
|
63
|
+
this.#pubkey = await fetch(pubkeyPath).then((res) => res.text()).then((pem) => pem.replace(/-----(BEGIN|END) PUBLIC KEY-----/g, "").replace(/\s+/g, ""));
|
|
64
|
+
return this.#pubkey;
|
|
65
|
+
}
|
|
66
|
+
async verifyAccessToken(accessToken) {
|
|
67
|
+
const pubkey = await this.fetchPublicKey();
|
|
68
|
+
const binaryDer = Uint8Array.from(atob(pubkey), (c) => c.charCodeAt(0));
|
|
69
|
+
const cryptoKey = await crypto.subtle.importKey("spki", binaryDer.buffer, {
|
|
70
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
71
|
+
hash: "SHA-256"
|
|
72
|
+
}, false, ["verify"]);
|
|
73
|
+
const [header, payload, signature] = accessToken.split(".");
|
|
74
|
+
const base64Signature = signature.replace(/-/g, "+").replace(/_/g, "/");
|
|
75
|
+
const verified = await crypto.subtle.verify({ name: "RSASSA-PKCS1-v1_5" }, cryptoKey, Uint8Array.from(atob(base64Signature), (c) => c.charCodeAt(0)), new TextEncoder().encode([header, payload].join(".")));
|
|
76
|
+
const data = JSON.parse(atob(payload));
|
|
77
|
+
const expired = data.exp && data.exp < Date.now() / 1000;
|
|
78
|
+
return {
|
|
79
|
+
verified,
|
|
80
|
+
expired,
|
|
81
|
+
data
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async refreshAccessToken(refresh_token) {
|
|
85
|
+
const res = await fetch(this.config.auth_url + "/sso/token", {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: {
|
|
88
|
+
"content-type": "application/json"
|
|
89
|
+
},
|
|
90
|
+
body: JSON.stringify({
|
|
91
|
+
grant_type: "refresh_token",
|
|
92
|
+
client_id: this.config.client_id,
|
|
93
|
+
refresh_token
|
|
94
|
+
})
|
|
95
|
+
});
|
|
96
|
+
return res.json();
|
|
97
|
+
}
|
|
98
|
+
async generateAccessToken(code, code_verifier) {
|
|
99
|
+
const res = await fetch(this.config.auth_url + "/sso/token", {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: {
|
|
102
|
+
"content-type": "application/json"
|
|
103
|
+
},
|
|
104
|
+
body: JSON.stringify({
|
|
105
|
+
grant_type: "authorization_code",
|
|
106
|
+
client_id: this.config.client_id,
|
|
107
|
+
code,
|
|
108
|
+
code_verifier
|
|
109
|
+
})
|
|
110
|
+
});
|
|
111
|
+
return res.json();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/SsoKukarSdkExpress.ts
|
|
116
|
+
var __dirname = "/home/aldinh777/Projects/simpeg/sso-kukar-sdk-express/src";
|
|
117
|
+
|
|
118
|
+
class SsoKukarSdkError extends Error {
|
|
119
|
+
constructor(message) {
|
|
120
|
+
super(message);
|
|
121
|
+
this.name = "SsoKukarSdkError";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
class SsoKukarSdkExpress extends SsoKukarClient {
|
|
126
|
+
ssoRedirectUrl() {
|
|
127
|
+
const { client_id, auth_url } = this.config;
|
|
128
|
+
return `${auth_url}/sso/auth?response_type=code&client_id=${client_id}`;
|
|
129
|
+
}
|
|
130
|
+
obtainHeaderAccessToken(req) {
|
|
131
|
+
const authHeader = req.headers.authorization;
|
|
132
|
+
if (authHeader && authHeader.startsWith("Bearer ")) {
|
|
133
|
+
return authHeader.split(" ")[1];
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
obtainCookiesAccessToken(req) {
|
|
138
|
+
const token = req.cookies?.access_token;
|
|
139
|
+
if (token) {
|
|
140
|
+
return token;
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
obtainCookiesRefreshToken(req) {
|
|
145
|
+
const token = req.cookies?.refresh_token;
|
|
146
|
+
if (token) {
|
|
147
|
+
return token;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
setCookie(res, key, value) {
|
|
152
|
+
res.cookie(key, value, {
|
|
153
|
+
httpOnly: true,
|
|
154
|
+
secure: true,
|
|
155
|
+
sameSite: "strict",
|
|
156
|
+
maxAge: 30 * 24 * 60 * 60 * 1000
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
clearCookie(res, key) {
|
|
160
|
+
res.clearCookie(key, {
|
|
161
|
+
httpOnly: true,
|
|
162
|
+
secure: true,
|
|
163
|
+
sameSite: "strict"
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
middleware() {
|
|
167
|
+
return async (req, res, next) => {
|
|
168
|
+
try {
|
|
169
|
+
const accessToken = this.obtainHeaderAccessToken(req) || this.obtainCookiesAccessToken(req);
|
|
170
|
+
if (!accessToken) {
|
|
171
|
+
return next(new SsoKukarSdkError("Access token not found"));
|
|
172
|
+
}
|
|
173
|
+
let user = await this.verifyAccessToken(accessToken);
|
|
174
|
+
if (user.verified === false) {
|
|
175
|
+
const refresh_token = this.obtainCookiesRefreshToken(req);
|
|
176
|
+
if (!refresh_token) {
|
|
177
|
+
this.clearCookie(res, "access_token");
|
|
178
|
+
return next(new SsoKukarSdkError("Unauthorized"));
|
|
179
|
+
}
|
|
180
|
+
const tokenResponse = await this.refreshAccessToken(refresh_token);
|
|
181
|
+
const newAccessToken = tokenResponse.data?.access_token;
|
|
182
|
+
if (!newAccessToken) {
|
|
183
|
+
this.clearCookie(res, "access_token");
|
|
184
|
+
this.clearCookie(res, "refresh_token");
|
|
185
|
+
return next(new SsoKukarSdkError("Unauthorized"));
|
|
186
|
+
}
|
|
187
|
+
user = await this.verifyAccessToken(newAccessToken);
|
|
188
|
+
if (user.verified === false) {
|
|
189
|
+
this.clearCookie(res, "access_token");
|
|
190
|
+
this.clearCookie(res, "refresh_token");
|
|
191
|
+
return next(new SsoKukarSdkError("Unauthorized"));
|
|
192
|
+
}
|
|
193
|
+
this.setCookie(res, "access_token", newAccessToken);
|
|
194
|
+
}
|
|
195
|
+
req.user_sso = user.data;
|
|
196
|
+
next();
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error("Authentication error:", error);
|
|
199
|
+
next(error);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
async refreshAccessToken(refresh_token) {
|
|
204
|
+
const res = await fetch(this.config.auth_url + "/sso/token", {
|
|
205
|
+
method: "POST",
|
|
206
|
+
headers: {
|
|
207
|
+
"content-type": "application/json"
|
|
208
|
+
},
|
|
209
|
+
body: JSON.stringify({
|
|
210
|
+
grant_type: "refresh_token",
|
|
211
|
+
client_id: this.config.client_id,
|
|
212
|
+
client_secret: this.config.client_secret,
|
|
213
|
+
refresh_token
|
|
214
|
+
})
|
|
215
|
+
});
|
|
216
|
+
return res.json();
|
|
217
|
+
}
|
|
218
|
+
async generateAccessToken(code) {
|
|
219
|
+
const res = await fetch(this.config.auth_url + "/sso/token", {
|
|
220
|
+
method: "POST",
|
|
221
|
+
headers: {
|
|
222
|
+
"content-type": "application/json"
|
|
223
|
+
},
|
|
224
|
+
body: JSON.stringify({
|
|
225
|
+
grant_type: "authorization_code",
|
|
226
|
+
client_id: this.config.client_id,
|
|
227
|
+
client_secret: this.config.client_secret,
|
|
228
|
+
code
|
|
229
|
+
})
|
|
230
|
+
});
|
|
231
|
+
return res.json();
|
|
232
|
+
}
|
|
233
|
+
frontendHandler() {
|
|
234
|
+
return (_req, res) => {
|
|
235
|
+
res.sendFile(import_path.default.join(__dirname, "../resource/client.js"));
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
callbackHandler(onSuccess) {
|
|
239
|
+
return async (req, res, next) => {
|
|
240
|
+
const { code } = req.query;
|
|
241
|
+
if (!code || typeof code !== "string") {
|
|
242
|
+
res.status(400).json({ error: "Invalid code" });
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
const response = await this.generateAccessToken(code);
|
|
247
|
+
const { access_token, refresh_token } = response.data;
|
|
248
|
+
this.setCookie(res, "access_token", access_token);
|
|
249
|
+
this.setCookie(res, "refresh_token", refresh_token);
|
|
250
|
+
onSuccess(req, res, next);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
next(error);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
callbackPopupHandler() {
|
|
257
|
+
return (_req, res) => {
|
|
258
|
+
res.sendFile(import_path.default.join(__dirname, "../resource/callback.html"));
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
logoutHandler() {
|
|
262
|
+
return async (_req, res, next) => {
|
|
263
|
+
try {
|
|
264
|
+
this.clearCookie(res, "access_token");
|
|
265
|
+
this.clearCookie(res, "refresh_token");
|
|
266
|
+
res.status(204).json({ message: "Logged out successfully" });
|
|
267
|
+
} catch (error) {
|
|
268
|
+
next(error);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@diskominfo/sso-kukar-sdk-js",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "express middleware untuk sso-kukar",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build-client": "bun build src/SsoKukarFrontend.ts --outfile=resource/client.js --target=browser --format=esm",
|
|
9
|
+
"build-server": "bun build src/SsoKukarSdkExpress.ts --outfile=dist/sso-kukar-sdk-express.js --target=node --format=cjs"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./dist/sso-kukar-sdk-express.js",
|
|
13
|
+
"./client": "./resource/client.js"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/express": "^5.0.3",
|
|
17
|
+
"express": "^5.1.0"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://reporepo.kukarkab.go.id/rizkipadhil/sso-kukar-sdk-express.git"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// src/SsoKukarClient.ts
|
|
2
|
+
class SsoKukarClient {
|
|
3
|
+
config;
|
|
4
|
+
#pubkey;
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.config = config;
|
|
7
|
+
}
|
|
8
|
+
async fetchPublicKey() {
|
|
9
|
+
if (this.#pubkey) {
|
|
10
|
+
return this.#pubkey;
|
|
11
|
+
}
|
|
12
|
+
const pubkeyPath = `${this.config.auth_url}/.well-known/public.pem`;
|
|
13
|
+
this.#pubkey = await fetch(pubkeyPath).then((res) => res.text()).then((pem) => pem.replace(/-----(BEGIN|END) PUBLIC KEY-----/g, "").replace(/\s+/g, ""));
|
|
14
|
+
return this.#pubkey;
|
|
15
|
+
}
|
|
16
|
+
async verifyAccessToken(accessToken) {
|
|
17
|
+
const pubkey = await this.fetchPublicKey();
|
|
18
|
+
const binaryDer = Uint8Array.from(atob(pubkey), (c) => c.charCodeAt(0));
|
|
19
|
+
const cryptoKey = await crypto.subtle.importKey("spki", binaryDer.buffer, {
|
|
20
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
21
|
+
hash: "SHA-256"
|
|
22
|
+
}, false, ["verify"]);
|
|
23
|
+
const [header, payload, signature] = accessToken.split(".");
|
|
24
|
+
const base64Signature = signature.replace(/-/g, "+").replace(/_/g, "/");
|
|
25
|
+
const verified = await crypto.subtle.verify({ name: "RSASSA-PKCS1-v1_5" }, cryptoKey, Uint8Array.from(atob(base64Signature), (c) => c.charCodeAt(0)), new TextEncoder().encode([header, payload].join(".")));
|
|
26
|
+
const data = JSON.parse(atob(payload));
|
|
27
|
+
const expired = data.exp && data.exp < Date.now() / 1000;
|
|
28
|
+
return {
|
|
29
|
+
verified,
|
|
30
|
+
expired,
|
|
31
|
+
data
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async refreshAccessToken(refresh_token) {
|
|
35
|
+
const res = await fetch(this.config.auth_url + "/sso/token", {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: {
|
|
38
|
+
"content-type": "application/json"
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify({
|
|
41
|
+
grant_type: "refresh_token",
|
|
42
|
+
client_id: this.config.client_id,
|
|
43
|
+
refresh_token
|
|
44
|
+
})
|
|
45
|
+
});
|
|
46
|
+
return res.json();
|
|
47
|
+
}
|
|
48
|
+
async generateAccessToken(code, code_verifier) {
|
|
49
|
+
const res = await fetch(this.config.auth_url + "/sso/token", {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: {
|
|
52
|
+
"content-type": "application/json"
|
|
53
|
+
},
|
|
54
|
+
body: JSON.stringify({
|
|
55
|
+
grant_type: "authorization_code",
|
|
56
|
+
client_id: this.config.client_id,
|
|
57
|
+
code,
|
|
58
|
+
code_verifier
|
|
59
|
+
})
|
|
60
|
+
});
|
|
61
|
+
return res.json();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/SsoKukarFrontend.ts
|
|
66
|
+
class SsoKukarFrontend extends SsoKukarClient {
|
|
67
|
+
static verifier_code_name = "sso_kukar_verifier_code";
|
|
68
|
+
async generateCodeChallengePair() {
|
|
69
|
+
const code_verifier = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
70
|
+
const encoder = new TextEncoder;
|
|
71
|
+
const data = encoder.encode(code_verifier);
|
|
72
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
73
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
74
|
+
const code_challenge = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
75
|
+
return { code_challenge, code_verifier };
|
|
76
|
+
}
|
|
77
|
+
handleLoginPopup(url, width = 500, height = 600) {
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
const left = window.screenX + (window.outerWidth - width) / 2;
|
|
80
|
+
const top = window.screenY + (window.outerHeight - height) / 2;
|
|
81
|
+
const popup = window.open(url, "_blank", `width=${width},height=${height},left=${left},top=${top},resizable,scrollbars`);
|
|
82
|
+
if (!popup) {
|
|
83
|
+
return reject("Popup blocked");
|
|
84
|
+
}
|
|
85
|
+
const interval = setInterval(() => {
|
|
86
|
+
if (popup.closed) {
|
|
87
|
+
clearInterval(interval);
|
|
88
|
+
window.removeEventListener("message", msgHandler);
|
|
89
|
+
reject("Failed to authenticate");
|
|
90
|
+
}
|
|
91
|
+
}, 500);
|
|
92
|
+
const msgHandler = (event) => {
|
|
93
|
+
if (event.origin !== window.location.origin || !event.data?.code) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
clearInterval(interval);
|
|
97
|
+
window.removeEventListener("message", msgHandler);
|
|
98
|
+
resolve(event.data);
|
|
99
|
+
};
|
|
100
|
+
window.addEventListener("message", msgHandler);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
async redirectLogin() {
|
|
104
|
+
const { auth_url, client_id } = this.config;
|
|
105
|
+
const { code_challenge, code_verifier } = await this.generateCodeChallengePair();
|
|
106
|
+
const url = `${auth_url}/sso/auth?response_type=code&client_id=${client_id}&code_challenge=${code_challenge}`;
|
|
107
|
+
localStorage.setItem(SsoKukarFrontend.verifier_code_name, code_verifier);
|
|
108
|
+
window.location.href = url;
|
|
109
|
+
}
|
|
110
|
+
async handleRedirectLogin() {
|
|
111
|
+
const params = new URLSearchParams(window.location.search);
|
|
112
|
+
const code = params.get("code");
|
|
113
|
+
const code_verifier = localStorage.getItem(SsoKukarFrontend.verifier_code_name);
|
|
114
|
+
if (!code || !code_verifier) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const res_tokens = await this.generateAccessToken(code, code_verifier);
|
|
118
|
+
localStorage.removeItem(SsoKukarFrontend.verifier_code_name);
|
|
119
|
+
const access_token = res_tokens.data.access_token;
|
|
120
|
+
const refresh_token = res_tokens.data.refresh_token;
|
|
121
|
+
const verifyToken = await this.verifyAccessToken(access_token);
|
|
122
|
+
if (verifyToken.verified) {
|
|
123
|
+
return {
|
|
124
|
+
tokens: {
|
|
125
|
+
access_token,
|
|
126
|
+
refresh_token
|
|
127
|
+
},
|
|
128
|
+
payload: verifyToken.data
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async openLoginPopup() {
|
|
133
|
+
const { auth_url, client_id } = this.config;
|
|
134
|
+
const { code_challenge, code_verifier } = await this.generateCodeChallengePair();
|
|
135
|
+
const { code } = await this.handleLoginPopup(`${auth_url}/sso/auth?response_type=code&client_id=${client_id}&code_challenge=${code_challenge}`);
|
|
136
|
+
const res_tokens = await this.generateAccessToken(code, code_verifier);
|
|
137
|
+
const access_token = res_tokens.data.access_token;
|
|
138
|
+
const refresh_token = res_tokens.data.refresh_token;
|
|
139
|
+
const verifyToken = await this.verifyAccessToken(access_token);
|
|
140
|
+
if (verifyToken.verified) {
|
|
141
|
+
return {
|
|
142
|
+
tokens: {
|
|
143
|
+
access_token,
|
|
144
|
+
refresh_token
|
|
145
|
+
},
|
|
146
|
+
payload: verifyToken.data
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
closeLoginPopup() {
|
|
151
|
+
const queryParams = new URLSearchParams(window.location.search);
|
|
152
|
+
const code = queryParams.get("code");
|
|
153
|
+
window.opener.postMessage({ client_id: this.config.client_id, code });
|
|
154
|
+
window.close();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export {
|
|
158
|
+
SsoKukarFrontend as default
|
|
159
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export type SsoClientConfig = {
|
|
2
|
+
client_id: string;
|
|
3
|
+
auth_url: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export default class SsoKukarClient<
|
|
7
|
+
T extends SsoClientConfig = SsoClientConfig,
|
|
8
|
+
> {
|
|
9
|
+
config: T;
|
|
10
|
+
#pubkey: string;
|
|
11
|
+
|
|
12
|
+
constructor(config: T) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async fetchPublicKey() {
|
|
17
|
+
if (this.#pubkey) {
|
|
18
|
+
return this.#pubkey;
|
|
19
|
+
}
|
|
20
|
+
const pubkeyPath = `${this.config.auth_url}/.well-known/public.pem`;
|
|
21
|
+
this.#pubkey = await fetch(pubkeyPath)
|
|
22
|
+
.then((res) => res.text())
|
|
23
|
+
.then((pem) =>
|
|
24
|
+
pem
|
|
25
|
+
.replace(/-----(BEGIN|END) PUBLIC KEY-----/g, "")
|
|
26
|
+
.replace(/\s+/g, ""),
|
|
27
|
+
);
|
|
28
|
+
return this.#pubkey;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async verifyAccessToken(accessToken: string) {
|
|
32
|
+
const pubkey = await this.fetchPublicKey();
|
|
33
|
+
const binaryDer = Uint8Array.from(atob(pubkey), (c) => c.charCodeAt(0));
|
|
34
|
+
|
|
35
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
36
|
+
"spki",
|
|
37
|
+
binaryDer.buffer,
|
|
38
|
+
{
|
|
39
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
40
|
+
hash: "SHA-256",
|
|
41
|
+
},
|
|
42
|
+
false,
|
|
43
|
+
["verify"],
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const [header, payload, signature] = accessToken.split(".");
|
|
47
|
+
|
|
48
|
+
const base64Signature = signature.replace(/-/g, "+").replace(/_/g, "/");
|
|
49
|
+
const verified = await crypto.subtle.verify(
|
|
50
|
+
{ name: "RSASSA-PKCS1-v1_5" },
|
|
51
|
+
cryptoKey,
|
|
52
|
+
Uint8Array.from(atob(base64Signature), (c) => c.charCodeAt(0)),
|
|
53
|
+
new TextEncoder().encode([header, payload].join(".")),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const data = JSON.parse(atob(payload));
|
|
57
|
+
const expired = data.exp && data.exp < Date.now() / 1000;
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
verified,
|
|
61
|
+
expired,
|
|
62
|
+
data,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async refreshAccessToken(refresh_token: string) {
|
|
67
|
+
const res = await fetch(this.config.auth_url + "/sso/token", {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: {
|
|
70
|
+
"content-type": "application/json",
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
grant_type: "refresh_token",
|
|
74
|
+
client_id: this.config.client_id,
|
|
75
|
+
refresh_token,
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return res.json();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async generateAccessToken(code: string, code_verifier: string) {
|
|
83
|
+
const res = await fetch(this.config.auth_url + "/sso/token", {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: {
|
|
86
|
+
"content-type": "application/json",
|
|
87
|
+
},
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
grant_type: "authorization_code",
|
|
90
|
+
client_id: this.config.client_id,
|
|
91
|
+
code,
|
|
92
|
+
code_verifier,
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return res.json();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import SsoKukarClient from "./SsoKukarClient";
|
|
2
|
+
|
|
3
|
+
type PopupResponse = {
|
|
4
|
+
client_id: string;
|
|
5
|
+
code: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export default class SsoKukarFrontend extends SsoKukarClient {
|
|
9
|
+
static verifier_code_name: string = "sso_kukar_verifier_code";
|
|
10
|
+
|
|
11
|
+
async generateCodeChallengePair() {
|
|
12
|
+
const code_verifier =
|
|
13
|
+
Math.random().toString(36).substring(2, 15) +
|
|
14
|
+
Math.random().toString(36).substring(2, 15);
|
|
15
|
+
|
|
16
|
+
// Encode string to Uint8Array
|
|
17
|
+
const encoder = new TextEncoder();
|
|
18
|
+
const data = encoder.encode(code_verifier);
|
|
19
|
+
|
|
20
|
+
// Hash with SHA-256 using SubtleCrypto
|
|
21
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
22
|
+
|
|
23
|
+
// Convert ArrayBuffer to hex string
|
|
24
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
25
|
+
const code_challenge = hashArray
|
|
26
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
27
|
+
.join("");
|
|
28
|
+
|
|
29
|
+
return { code_challenge, code_verifier };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
handleLoginPopup(url: string, width = 500, height = 600) {
|
|
33
|
+
return new Promise<PopupResponse>((resolve, reject) => {
|
|
34
|
+
const left = window.screenX + (window.outerWidth - width) / 2;
|
|
35
|
+
const top = window.screenY + (window.outerHeight - height) / 2;
|
|
36
|
+
const popup = window.open(
|
|
37
|
+
url,
|
|
38
|
+
"_blank",
|
|
39
|
+
`width=${width},height=${height},left=${left},top=${top},resizable,scrollbars`,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (!popup) {
|
|
43
|
+
return reject("Popup blocked");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const interval = setInterval(() => {
|
|
47
|
+
// window closed unsuccessfully
|
|
48
|
+
if (popup.closed) {
|
|
49
|
+
clearInterval(interval);
|
|
50
|
+
window.removeEventListener("message", msgHandler);
|
|
51
|
+
reject("Failed to authenticate");
|
|
52
|
+
}
|
|
53
|
+
}, 500);
|
|
54
|
+
|
|
55
|
+
const msgHandler = (event: MessageEvent) => {
|
|
56
|
+
if (event.origin !== window.location.origin || !event.data?.code) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// window closed successfully
|
|
60
|
+
clearInterval(interval);
|
|
61
|
+
window.removeEventListener("message", msgHandler);
|
|
62
|
+
resolve(event.data);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
window.addEventListener("message", msgHandler);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async redirectLogin() {
|
|
70
|
+
const { auth_url, client_id } = this.config;
|
|
71
|
+
const { code_challenge, code_verifier } =
|
|
72
|
+
await this.generateCodeChallengePair();
|
|
73
|
+
const url = `${auth_url}/sso/auth?response_type=code&client_id=${client_id}&code_challenge=${code_challenge}`;
|
|
74
|
+
localStorage.setItem(SsoKukarFrontend.verifier_code_name, code_verifier);
|
|
75
|
+
window.location.href = url;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async handleRedirectLogin() {
|
|
79
|
+
const params = new URLSearchParams(window.location.search);
|
|
80
|
+
const code = params.get("code");
|
|
81
|
+
const code_verifier = localStorage.getItem(
|
|
82
|
+
SsoKukarFrontend.verifier_code_name,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (!code || !code_verifier) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const res_tokens = await this.generateAccessToken(code, code_verifier);
|
|
90
|
+
localStorage.removeItem(SsoKukarFrontend.verifier_code_name);
|
|
91
|
+
|
|
92
|
+
const access_token = res_tokens.data.access_token;
|
|
93
|
+
const refresh_token = res_tokens.data.refresh_token;
|
|
94
|
+
const verifyToken = await this.verifyAccessToken(access_token);
|
|
95
|
+
if (verifyToken.verified) {
|
|
96
|
+
return {
|
|
97
|
+
tokens: {
|
|
98
|
+
access_token,
|
|
99
|
+
refresh_token,
|
|
100
|
+
},
|
|
101
|
+
payload: verifyToken.data,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async openLoginPopup() {
|
|
107
|
+
const { auth_url, client_id } = this.config;
|
|
108
|
+
const { code_challenge, code_verifier } =
|
|
109
|
+
await this.generateCodeChallengePair();
|
|
110
|
+
const { code } = await this.handleLoginPopup(
|
|
111
|
+
`${auth_url}/sso/auth?response_type=code&client_id=${client_id}&code_challenge=${code_challenge}`,
|
|
112
|
+
);
|
|
113
|
+
const res_tokens = await this.generateAccessToken(code, code_verifier);
|
|
114
|
+
const access_token = res_tokens.data.access_token;
|
|
115
|
+
const refresh_token = res_tokens.data.refresh_token;
|
|
116
|
+
|
|
117
|
+
const verifyToken = await this.verifyAccessToken(access_token);
|
|
118
|
+
if (verifyToken.verified) {
|
|
119
|
+
return {
|
|
120
|
+
tokens: {
|
|
121
|
+
access_token,
|
|
122
|
+
refresh_token,
|
|
123
|
+
},
|
|
124
|
+
payload: verifyToken.data,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
closeLoginPopup() {
|
|
130
|
+
const queryParams = new URLSearchParams(window.location.search);
|
|
131
|
+
const code = queryParams.get("code");
|
|
132
|
+
window.opener.postMessage({ client_id: this.config.client_id, code });
|
|
133
|
+
window.close();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { NextFunction, Request, Response, Router } from "express";
|
|
3
|
+
import SsoKukarClient, { SsoClientConfig } from "./SsoKukarClient";
|
|
4
|
+
|
|
5
|
+
type SsoServerConfig = SsoClientConfig & {
|
|
6
|
+
client_secret: string;
|
|
7
|
+
onSuccessRedirect?: string;
|
|
8
|
+
onErrorRedirect?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
declare global {
|
|
12
|
+
namespace Express {
|
|
13
|
+
interface Request {
|
|
14
|
+
user_sso?: any;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class SsoKukarSdkError extends Error {
|
|
20
|
+
constructor(message: string) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "SsoKukarSdkError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default class SsoKukarSdkExpress extends SsoKukarClient<SsoServerConfig> {
|
|
27
|
+
ssoRedirectUrl() {
|
|
28
|
+
const { client_id, auth_url } = this.config;
|
|
29
|
+
return `${auth_url}/sso/auth?response_type=code&client_id=${client_id}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
obtainHeaderAccessToken(req: Request): string | null {
|
|
33
|
+
const authHeader = req.headers.authorization;
|
|
34
|
+
if (authHeader && authHeader.startsWith("Bearer ")) {
|
|
35
|
+
return authHeader.split(" ")[1];
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
obtainCookiesAccessToken(req: Request): string | null {
|
|
41
|
+
const token = req.cookies?.access_token;
|
|
42
|
+
if (token) {
|
|
43
|
+
return token;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
obtainCookiesRefreshToken(req: Request): string | null {
|
|
49
|
+
const token = req.cookies?.refresh_token;
|
|
50
|
+
if (token) {
|
|
51
|
+
return token;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
setCookie(res: Response, key: string, value: string) {
|
|
57
|
+
res.cookie(key, value, {
|
|
58
|
+
httpOnly: true,
|
|
59
|
+
secure: true,
|
|
60
|
+
sameSite: "strict",
|
|
61
|
+
maxAge: 30 * 24 * 60 * 60 * 1000,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
clearCookie(res: Response, key: string) {
|
|
66
|
+
res.clearCookie(key, {
|
|
67
|
+
httpOnly: true,
|
|
68
|
+
secure: true,
|
|
69
|
+
sameSite: "strict",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
middleware() {
|
|
74
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
75
|
+
try {
|
|
76
|
+
const accessToken =
|
|
77
|
+
this.obtainHeaderAccessToken(req) ||
|
|
78
|
+
this.obtainCookiesAccessToken(req);
|
|
79
|
+
|
|
80
|
+
if (!accessToken) {
|
|
81
|
+
return next(new SsoKukarSdkError("Access token not found"));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let user = await this.verifyAccessToken(accessToken);
|
|
85
|
+
if (user.verified === false) {
|
|
86
|
+
const refresh_token = this.obtainCookiesRefreshToken(req);
|
|
87
|
+
if (!refresh_token) {
|
|
88
|
+
this.clearCookie(res, "access_token");
|
|
89
|
+
return next(new SsoKukarSdkError("Unauthorized"));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const tokenResponse = await this.refreshAccessToken(refresh_token);
|
|
93
|
+
const newAccessToken = tokenResponse.data?.access_token;
|
|
94
|
+
if (!newAccessToken) {
|
|
95
|
+
this.clearCookie(res, "access_token");
|
|
96
|
+
this.clearCookie(res, "refresh_token");
|
|
97
|
+
return next(new SsoKukarSdkError("Unauthorized"));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
user = await this.verifyAccessToken(newAccessToken);
|
|
101
|
+
if (user.verified === false) {
|
|
102
|
+
this.clearCookie(res, "access_token");
|
|
103
|
+
this.clearCookie(res, "refresh_token");
|
|
104
|
+
return next(new SsoKukarSdkError("Unauthorized"));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.setCookie(res, "access_token", newAccessToken);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
req.user_sso = user.data;
|
|
111
|
+
|
|
112
|
+
next();
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error("Authentication error:", error);
|
|
115
|
+
next(error);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// override
|
|
121
|
+
async refreshAccessToken(refresh_token: string) {
|
|
122
|
+
const res = await fetch(this.config.auth_url + "/sso/token", {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers: {
|
|
125
|
+
"content-type": "application/json",
|
|
126
|
+
},
|
|
127
|
+
body: JSON.stringify({
|
|
128
|
+
grant_type: "refresh_token",
|
|
129
|
+
client_id: this.config.client_id,
|
|
130
|
+
client_secret: this.config.client_secret,
|
|
131
|
+
refresh_token,
|
|
132
|
+
}),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return res.json();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// override
|
|
139
|
+
async generateAccessToken(code: string) {
|
|
140
|
+
const res = await fetch(this.config.auth_url + "/sso/token", {
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers: {
|
|
143
|
+
"content-type": "application/json",
|
|
144
|
+
},
|
|
145
|
+
body: JSON.stringify({
|
|
146
|
+
grant_type: "authorization_code",
|
|
147
|
+
client_id: this.config.client_id,
|
|
148
|
+
client_secret: this.config.client_secret,
|
|
149
|
+
code,
|
|
150
|
+
}),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return res.json();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
frontendHandler() {
|
|
157
|
+
return (_req: Request, res: Response) => {
|
|
158
|
+
res.sendFile(path.join(__dirname, "../resource/client.js"));
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
callbackHandler(
|
|
163
|
+
onSuccess: (req: Request, res: Response, next: NextFunction) => any,
|
|
164
|
+
) {
|
|
165
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
166
|
+
const { code } = req.query;
|
|
167
|
+
if (!code || typeof code !== "string") {
|
|
168
|
+
res.status(400).json({ error: "Invalid code" });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const response = await this.generateAccessToken(code);
|
|
173
|
+
const { access_token, refresh_token } = response.data;
|
|
174
|
+
this.setCookie(res, "access_token", access_token);
|
|
175
|
+
this.setCookie(res, "refresh_token", refresh_token);
|
|
176
|
+
|
|
177
|
+
onSuccess(req, res, next);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
next(error);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
callbackPopupHandler() {
|
|
185
|
+
return (_req: Request, res: Response) => {
|
|
186
|
+
res.sendFile(path.join(__dirname, "../resource/callback.html"));
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
logoutHandler() {
|
|
191
|
+
return async (_req: Request, res: Response, next: NextFunction) => {
|
|
192
|
+
try {
|
|
193
|
+
this.clearCookie(res, "access_token");
|
|
194
|
+
this.clearCookie(res, "refresh_token");
|
|
195
|
+
res.status(204).json({ message: "Logged out successfully" });
|
|
196
|
+
} catch (error) {
|
|
197
|
+
next(error);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|