@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 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,6 @@
1
+ <script>
2
+ const queryParams = new URLSearchParams(window.location.search);
3
+ const code = queryParams.get("code");
4
+ window.opener.postMessage({ code });
5
+ window.close();
6
+ </script>
@@ -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
+ }