@hengkianggia/sso-erlangga 1.0.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 +119 -0
- package/dist/index.d.mts +25 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +114 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +82 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# package-sso
|
|
2
|
+
|
|
3
|
+
Modul Helper SSO (Single Sign-On) yang menyediakan fungsionalitas untuk konfigurasi login, parsing token, dan pertukaran kode otorisasi berbasis PKCE.
|
|
4
|
+
|
|
5
|
+
## Instalasi
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install package-sso
|
|
9
|
+
# atau
|
|
10
|
+
pnpm install package-sso
|
|
11
|
+
# atau
|
|
12
|
+
yarn add package-sso
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Penggunaan
|
|
16
|
+
|
|
17
|
+
Dokumentasi di bawah ini merupakan referensi fungsi-fungsi dari `sso-helper`.
|
|
18
|
+
|
|
19
|
+
### 1. `generateSSOLoginUrl(config: SSOConfig)`
|
|
20
|
+
Fungsi ini digunakan untuk menghasilkan URL halaman login SSO. Fungsi ini juga secara otomatis meng-generate `code_challenge` untuk standar PKCE dan mengamankan `code_verifier` di dalam `localStorage`.
|
|
21
|
+
|
|
22
|
+
**Tipe Data (Interface):**
|
|
23
|
+
```typescript
|
|
24
|
+
interface SSOConfig {
|
|
25
|
+
clientId: string; // Client ID aplikasi Anda
|
|
26
|
+
redirectUri: string; // Callback URI
|
|
27
|
+
ssoBaseUrl: string; // URL dasar / Base URL SSO
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Contoh Penggunaan:**
|
|
32
|
+
```typescript
|
|
33
|
+
import { generateSSOLoginUrl } from 'package-sso';
|
|
34
|
+
|
|
35
|
+
async function loginStart() {
|
|
36
|
+
const loginUrl = await generateSSOLoginUrl({
|
|
37
|
+
clientId: 'app-client-123',
|
|
38
|
+
redirectUri: 'http://localhost:3000/callback',
|
|
39
|
+
ssoBaseUrl: 'https://sso.example.com'
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Arahkan user ke halaman login SSO:
|
|
43
|
+
window.location.href = loginUrl;
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
### 2. `getSSOTokenPayload(token: string)`
|
|
50
|
+
Fungsi ini bertugas untuk mendekode dan melakukan parsing payload dari sebuah string token berformat JWT.
|
|
51
|
+
|
|
52
|
+
**Contoh Penggunaan:**
|
|
53
|
+
```typescript
|
|
54
|
+
import { getSSOTokenPayload } from 'package-sso';
|
|
55
|
+
|
|
56
|
+
const tokenUrl = "eyJhbGc.eyJzdWIiOiIxMjMifQ.sgn";
|
|
57
|
+
const payload = getSSOTokenPayload(tokenUrl);
|
|
58
|
+
|
|
59
|
+
if (payload) {
|
|
60
|
+
console.log("Data Payload User:", payload);
|
|
61
|
+
} else {
|
|
62
|
+
console.log("Token tidak valid / gagal parsing");
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
### 3. `getSSOExchangeBody(config: SSOExchangeConfig)`
|
|
69
|
+
Fungsi ini sangat penting ketika user dikembalikan dari SSO ke aplikasi Anda (Callback). Ia akan menyiapkan Object body untuk ditukar (_exchange_) dengan Access Token. Secara otomatis mengambil `code_verifier` yang dibentuk pada tahap pertama dari `localStorage`.
|
|
70
|
+
|
|
71
|
+
**Tipe Data (Interface):**
|
|
72
|
+
```typescript
|
|
73
|
+
interface SSOExchangeConfig {
|
|
74
|
+
code: string; // Kode otorisasi yg didapat dari query parameter URL Callback
|
|
75
|
+
clientId: string; // Client ID aplikasi
|
|
76
|
+
redirectUri: string; // URI Redirect yang dikonfigurasi
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Contoh Penggunaan:**
|
|
81
|
+
```typescript
|
|
82
|
+
import { getSSOExchangeBody } from 'package-sso';
|
|
83
|
+
|
|
84
|
+
// Misalnya didapatkan dari URL query `?code=xyZ123`
|
|
85
|
+
const authCode = "xyZ123";
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const requestBody = getSSOExchangeBody({
|
|
89
|
+
code: authCode,
|
|
90
|
+
clientId: 'app-client-123',
|
|
91
|
+
redirectUri: 'http://localhost:3000/callback'
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Anda dapat menggunakan `requestBody` ini untuk Post payload Axios/Fetch
|
|
95
|
+
// ke Endpoin Token SSO backend Anda.
|
|
96
|
+
/*
|
|
97
|
+
axios.post('https://sso.example.com/token', requestBody)
|
|
98
|
+
*/
|
|
99
|
+
|
|
100
|
+
} catch (error) {
|
|
101
|
+
// Menangkap error jika `code_verifier` tidak ditemukan di browser
|
|
102
|
+
console.error(error.message);
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
### 4. `clearSSOData()`
|
|
109
|
+
Membersihkan seluruh jejak local state terkait SSO (`sso_state`, `sso_code_verifier`, dan `sso_token`) di dalam `localStorage`.
|
|
110
|
+
|
|
111
|
+
**Contoh Penggunaan:**
|
|
112
|
+
```typescript
|
|
113
|
+
import { clearSSOData } from 'package-sso';
|
|
114
|
+
|
|
115
|
+
function doLogout() {
|
|
116
|
+
clearSSOData();
|
|
117
|
+
// Lanjut redirect ke home, dsb...
|
|
118
|
+
}
|
|
119
|
+
```
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
interface SSOConfig {
|
|
2
|
+
clientId: string;
|
|
3
|
+
redirectUri: string;
|
|
4
|
+
ssoBaseUrl: string;
|
|
5
|
+
}
|
|
6
|
+
declare function generateSSOLoginUrl(config: SSOConfig): Promise<string>;
|
|
7
|
+
declare function getSSOTokenPayload(token: string): Record<string, any> | null;
|
|
8
|
+
interface SSOExchangeConfig {
|
|
9
|
+
code: string;
|
|
10
|
+
clientId: string;
|
|
11
|
+
redirectUri: string;
|
|
12
|
+
}
|
|
13
|
+
declare function getSSOExchangeBody(config: SSOExchangeConfig): {
|
|
14
|
+
grant_type: string;
|
|
15
|
+
code: string;
|
|
16
|
+
redirect_uri: string;
|
|
17
|
+
client_id: string;
|
|
18
|
+
code_verifier: string;
|
|
19
|
+
};
|
|
20
|
+
declare function clearSSOData(): void;
|
|
21
|
+
|
|
22
|
+
declare function generateRandomString(length: number): string;
|
|
23
|
+
declare function generateCodeChallenge2(codeVerifier: string): Promise<string>;
|
|
24
|
+
|
|
25
|
+
export { type SSOConfig, type SSOExchangeConfig, clearSSOData, generateCodeChallenge2, generateRandomString, generateSSOLoginUrl, getSSOExchangeBody, getSSOTokenPayload };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
interface SSOConfig {
|
|
2
|
+
clientId: string;
|
|
3
|
+
redirectUri: string;
|
|
4
|
+
ssoBaseUrl: string;
|
|
5
|
+
}
|
|
6
|
+
declare function generateSSOLoginUrl(config: SSOConfig): Promise<string>;
|
|
7
|
+
declare function getSSOTokenPayload(token: string): Record<string, any> | null;
|
|
8
|
+
interface SSOExchangeConfig {
|
|
9
|
+
code: string;
|
|
10
|
+
clientId: string;
|
|
11
|
+
redirectUri: string;
|
|
12
|
+
}
|
|
13
|
+
declare function getSSOExchangeBody(config: SSOExchangeConfig): {
|
|
14
|
+
grant_type: string;
|
|
15
|
+
code: string;
|
|
16
|
+
redirect_uri: string;
|
|
17
|
+
client_id: string;
|
|
18
|
+
code_verifier: string;
|
|
19
|
+
};
|
|
20
|
+
declare function clearSSOData(): void;
|
|
21
|
+
|
|
22
|
+
declare function generateRandomString(length: number): string;
|
|
23
|
+
declare function generateCodeChallenge2(codeVerifier: string): Promise<string>;
|
|
24
|
+
|
|
25
|
+
export { type SSOConfig, type SSOExchangeConfig, clearSSOData, generateCodeChallenge2, generateRandomString, generateSSOLoginUrl, getSSOExchangeBody, getSSOTokenPayload };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
clearSSOData: () => clearSSOData,
|
|
24
|
+
generateCodeChallenge2: () => generateCodeChallenge2,
|
|
25
|
+
generateRandomString: () => generateRandomString,
|
|
26
|
+
generateSSOLoginUrl: () => generateSSOLoginUrl,
|
|
27
|
+
getSSOExchangeBody: () => getSSOExchangeBody,
|
|
28
|
+
getSSOTokenPayload: () => getSSOTokenPayload
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
|
+
|
|
32
|
+
// src/helper.ts
|
|
33
|
+
function base64UrlEncode(arrayBuffer) {
|
|
34
|
+
const bytes = new Uint8Array(arrayBuffer);
|
|
35
|
+
let binary = "";
|
|
36
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
37
|
+
binary += String.fromCharCode(bytes[i]);
|
|
38
|
+
}
|
|
39
|
+
const base64 = btoa(binary);
|
|
40
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
41
|
+
}
|
|
42
|
+
function generateRandomString(length) {
|
|
43
|
+
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
44
|
+
let result = "";
|
|
45
|
+
const values = new Uint32Array(length);
|
|
46
|
+
window.crypto.getRandomValues(values);
|
|
47
|
+
for (let i = 0; i < length; i++) {
|
|
48
|
+
result += charset[values[i] % charset.length];
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
async function generateCodeChallenge2(codeVerifier) {
|
|
53
|
+
const encoder = new TextEncoder();
|
|
54
|
+
const data = encoder.encode(codeVerifier);
|
|
55
|
+
const digest = await window.crypto.subtle.digest("SHA-256", data);
|
|
56
|
+
return base64UrlEncode(digest);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/sso-helper.ts
|
|
60
|
+
async function generateSSOLoginUrl(config) {
|
|
61
|
+
const codeVerifier = generateRandomString(64);
|
|
62
|
+
localStorage.setItem("sso_code_verifier", codeVerifier);
|
|
63
|
+
const codeChallenge = await generateCodeChallenge2(codeVerifier);
|
|
64
|
+
const params = new URLSearchParams({
|
|
65
|
+
response_type: "code",
|
|
66
|
+
client_id: config.clientId,
|
|
67
|
+
redirect_uri: config.redirectUri,
|
|
68
|
+
code_challenge: codeChallenge,
|
|
69
|
+
code_challenge_method: "S256"
|
|
70
|
+
});
|
|
71
|
+
return `${config.ssoBaseUrl}/callback?${params.toString()}`;
|
|
72
|
+
}
|
|
73
|
+
function getSSOTokenPayload(token) {
|
|
74
|
+
try {
|
|
75
|
+
const base64Url = token.split(".")[1];
|
|
76
|
+
if (!base64Url) return null;
|
|
77
|
+
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
|
78
|
+
const jsonPayload = decodeURIComponent(
|
|
79
|
+
atob(base64).split("").map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)).join("")
|
|
80
|
+
);
|
|
81
|
+
return JSON.parse(jsonPayload);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error("Failed to parse SSO token payload", error);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function getSSOExchangeBody(config) {
|
|
88
|
+
const codeVerifier = localStorage.getItem("sso_code_verifier") || "";
|
|
89
|
+
if (!codeVerifier) {
|
|
90
|
+
throw new Error("Code verifier not found");
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
grant_type: "authorization_code",
|
|
94
|
+
code: config.code,
|
|
95
|
+
redirect_uri: config.redirectUri,
|
|
96
|
+
client_id: config.clientId,
|
|
97
|
+
code_verifier: codeVerifier
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function clearSSOData() {
|
|
101
|
+
localStorage.removeItem("sso_state");
|
|
102
|
+
localStorage.removeItem("sso_code_verifier");
|
|
103
|
+
localStorage.removeItem("sso_token");
|
|
104
|
+
}
|
|
105
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
106
|
+
0 && (module.exports = {
|
|
107
|
+
clearSSOData,
|
|
108
|
+
generateCodeChallenge2,
|
|
109
|
+
generateRandomString,
|
|
110
|
+
generateSSOLoginUrl,
|
|
111
|
+
getSSOExchangeBody,
|
|
112
|
+
getSSOTokenPayload
|
|
113
|
+
});
|
|
114
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/helper.ts","../src/sso-helper.ts"],"sourcesContent":["export * from './sso-helper';\nexport * from './helper';\n","// Base64-url encoding logic\nfunction base64UrlEncode(arrayBuffer: ArrayBuffer): string {\n const bytes = new Uint8Array(arrayBuffer);\n let binary = '';\n // Convert bytes to string (chunking can help for very large arrays, but for SHA-256 (32 bytes) it's fine)\n for (let i = 0; i < bytes.byteLength; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n const base64 = btoa(binary);\n\n return base64\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '');\n}\n\nexport function generateRandomString(length: number): string {\n const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';\n let result = '';\n const values = new Uint32Array(length);\n window.crypto.getRandomValues(values);\n for (let i = 0; i < length; i++) {\n result += charset[values[i] % charset.length];\n }\n return result;\n}\n\nexport async function generateCodeChallenge2(codeVerifier: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(codeVerifier);\n // Hash the data using SHA-256\n const digest = await window.crypto.subtle.digest('SHA-256', data);\n // Base64-url encode the hash\n return base64UrlEncode(digest);\n}\n","import { generateRandomString, generateCodeChallenge2 } from './helper';\n\nexport interface SSOConfig {\n clientId: string;\n redirectUri: string;\n ssoBaseUrl: string;\n}\n\nexport async function generateSSOLoginUrl(config: SSOConfig): Promise<string> {\n const codeVerifier = generateRandomString(64);\n\n localStorage.setItem('sso_code_verifier', codeVerifier);\n\n const codeChallenge = await generateCodeChallenge2(codeVerifier);\n\n const params = new URLSearchParams({\n response_type: 'code',\n client_id: config.clientId,\n redirect_uri: config.redirectUri,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n });\n\n return `${config.ssoBaseUrl}/callback?${params.toString()}`;\n}\n\nexport function getSSOTokenPayload(token: string): Record<string, any> | null {\n try {\n const base64Url = token.split('.')[1];\n if (!base64Url) return null;\n const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');\n const jsonPayload = decodeURIComponent(\n atob(base64)\n .split('')\n .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))\n .join('')\n );\n return JSON.parse(jsonPayload);\n } catch (error) {\n console.error('Failed to parse SSO token payload', error);\n return null;\n }\n}\n\nexport interface SSOExchangeConfig {\n code: string;\n clientId: string;\n redirectUri: string;\n}\n\nexport function getSSOExchangeBody(config: SSOExchangeConfig) {\n const codeVerifier = localStorage.getItem('sso_code_verifier') || '';\n if (!codeVerifier) {\n throw new Error('Code verifier not found');\n }\n\n return {\n grant_type: 'authorization_code',\n code: config.code,\n redirect_uri: config.redirectUri,\n client_id: config.clientId,\n code_verifier: codeVerifier,\n };\n}\n\nexport function clearSSOData(): void {\n localStorage.removeItem('sso_state');\n localStorage.removeItem('sso_code_verifier');\n localStorage.removeItem('sso_token'); // Assuming the token might be stored this way, though up to the app\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACCA,SAAS,gBAAgB,aAAkC;AACzD,QAAM,QAAQ,IAAI,WAAW,WAAW;AACxC,MAAI,SAAS;AAEb,WAAS,IAAI,GAAG,IAAI,MAAM,YAAY,KAAK;AACzC,cAAU,OAAO,aAAa,MAAM,CAAC,CAAC;AAAA,EACxC;AACA,QAAM,SAAS,KAAK,MAAM;AAE1B,SAAO,OACJ,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,EAAE;AACtB;AAEO,SAAS,qBAAqB,QAAwB;AAC3D,QAAM,UAAU;AAChB,MAAI,SAAS;AACb,QAAM,SAAS,IAAI,YAAY,MAAM;AACrC,SAAO,OAAO,gBAAgB,MAAM;AACpC,WAAS,IAAI,GAAG,IAAI,QAAQ,KAAK;AAC/B,cAAU,QAAQ,OAAO,CAAC,IAAI,QAAQ,MAAM;AAAA,EAC9C;AACA,SAAO;AACT;AAEA,eAAsB,uBAAuB,cAAuC;AAClF,QAAM,UAAU,IAAI,YAAY;AAChC,QAAM,OAAO,QAAQ,OAAO,YAAY;AAExC,QAAM,SAAS,MAAM,OAAO,OAAO,OAAO,OAAO,WAAW,IAAI;AAEhE,SAAO,gBAAgB,MAAM;AAC/B;;;AC1BA,eAAsB,oBAAoB,QAAoC;AAC5E,QAAM,eAAe,qBAAqB,EAAE;AAE5C,eAAa,QAAQ,qBAAqB,YAAY;AAEtD,QAAM,gBAAgB,MAAM,uBAAuB,YAAY;AAE/D,QAAM,SAAS,IAAI,gBAAgB;AAAA,IACjC,eAAe;AAAA,IACf,WAAW,OAAO;AAAA,IAClB,cAAc,OAAO;AAAA,IACrB,gBAAgB;AAAA,IAChB,uBAAuB;AAAA,EACzB,CAAC;AAED,SAAO,GAAG,OAAO,UAAU,aAAa,OAAO,SAAS,CAAC;AAC3D;AAEO,SAAS,mBAAmB,OAA2C;AAC5E,MAAI;AACF,UAAM,YAAY,MAAM,MAAM,GAAG,EAAE,CAAC;AACpC,QAAI,CAAC,UAAW,QAAO;AACvB,UAAM,SAAS,UAAU,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG;AAC7D,UAAM,cAAc;AAAA,MAClB,KAAK,MAAM,EACR,MAAM,EAAE,EACR,IAAI,CAAC,MAAM,OAAO,OAAO,EAAE,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,MAAM,EAAE,CAAC,EAChE,KAAK,EAAE;AAAA,IACZ;AACA,WAAO,KAAK,MAAM,WAAW;AAAA,EAC/B,SAAS,OAAO;AACd,YAAQ,MAAM,qCAAqC,KAAK;AACxD,WAAO;AAAA,EACT;AACF;AAQO,SAAS,mBAAmB,QAA2B;AAC5D,QAAM,eAAe,aAAa,QAAQ,mBAAmB,KAAK;AAClE,MAAI,CAAC,cAAc;AACjB,UAAM,IAAI,MAAM,yBAAyB;AAAA,EAC3C;AAEA,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,MAAM,OAAO;AAAA,IACb,cAAc,OAAO;AAAA,IACrB,WAAW,OAAO;AAAA,IAClB,eAAe;AAAA,EACjB;AACF;AAEO,SAAS,eAAqB;AACnC,eAAa,WAAW,WAAW;AACnC,eAAa,WAAW,mBAAmB;AAC3C,eAAa,WAAW,WAAW;AACrC;","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// src/helper.ts
|
|
2
|
+
function base64UrlEncode(arrayBuffer) {
|
|
3
|
+
const bytes = new Uint8Array(arrayBuffer);
|
|
4
|
+
let binary = "";
|
|
5
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
6
|
+
binary += String.fromCharCode(bytes[i]);
|
|
7
|
+
}
|
|
8
|
+
const base64 = btoa(binary);
|
|
9
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
10
|
+
}
|
|
11
|
+
function generateRandomString(length) {
|
|
12
|
+
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
13
|
+
let result = "";
|
|
14
|
+
const values = new Uint32Array(length);
|
|
15
|
+
window.crypto.getRandomValues(values);
|
|
16
|
+
for (let i = 0; i < length; i++) {
|
|
17
|
+
result += charset[values[i] % charset.length];
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
async function generateCodeChallenge2(codeVerifier) {
|
|
22
|
+
const encoder = new TextEncoder();
|
|
23
|
+
const data = encoder.encode(codeVerifier);
|
|
24
|
+
const digest = await window.crypto.subtle.digest("SHA-256", data);
|
|
25
|
+
return base64UrlEncode(digest);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/sso-helper.ts
|
|
29
|
+
async function generateSSOLoginUrl(config) {
|
|
30
|
+
const codeVerifier = generateRandomString(64);
|
|
31
|
+
localStorage.setItem("sso_code_verifier", codeVerifier);
|
|
32
|
+
const codeChallenge = await generateCodeChallenge2(codeVerifier);
|
|
33
|
+
const params = new URLSearchParams({
|
|
34
|
+
response_type: "code",
|
|
35
|
+
client_id: config.clientId,
|
|
36
|
+
redirect_uri: config.redirectUri,
|
|
37
|
+
code_challenge: codeChallenge,
|
|
38
|
+
code_challenge_method: "S256"
|
|
39
|
+
});
|
|
40
|
+
return `${config.ssoBaseUrl}/callback?${params.toString()}`;
|
|
41
|
+
}
|
|
42
|
+
function getSSOTokenPayload(token) {
|
|
43
|
+
try {
|
|
44
|
+
const base64Url = token.split(".")[1];
|
|
45
|
+
if (!base64Url) return null;
|
|
46
|
+
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
|
47
|
+
const jsonPayload = decodeURIComponent(
|
|
48
|
+
atob(base64).split("").map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)).join("")
|
|
49
|
+
);
|
|
50
|
+
return JSON.parse(jsonPayload);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error("Failed to parse SSO token payload", error);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function getSSOExchangeBody(config) {
|
|
57
|
+
const codeVerifier = localStorage.getItem("sso_code_verifier") || "";
|
|
58
|
+
if (!codeVerifier) {
|
|
59
|
+
throw new Error("Code verifier not found");
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
grant_type: "authorization_code",
|
|
63
|
+
code: config.code,
|
|
64
|
+
redirect_uri: config.redirectUri,
|
|
65
|
+
client_id: config.clientId,
|
|
66
|
+
code_verifier: codeVerifier
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function clearSSOData() {
|
|
70
|
+
localStorage.removeItem("sso_state");
|
|
71
|
+
localStorage.removeItem("sso_code_verifier");
|
|
72
|
+
localStorage.removeItem("sso_token");
|
|
73
|
+
}
|
|
74
|
+
export {
|
|
75
|
+
clearSSOData,
|
|
76
|
+
generateCodeChallenge2,
|
|
77
|
+
generateRandomString,
|
|
78
|
+
generateSSOLoginUrl,
|
|
79
|
+
getSSOExchangeBody,
|
|
80
|
+
getSSOTokenPayload
|
|
81
|
+
};
|
|
82
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/helper.ts","../src/sso-helper.ts"],"sourcesContent":["// Base64-url encoding logic\nfunction base64UrlEncode(arrayBuffer: ArrayBuffer): string {\n const bytes = new Uint8Array(arrayBuffer);\n let binary = '';\n // Convert bytes to string (chunking can help for very large arrays, but for SHA-256 (32 bytes) it's fine)\n for (let i = 0; i < bytes.byteLength; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n const base64 = btoa(binary);\n\n return base64\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '');\n}\n\nexport function generateRandomString(length: number): string {\n const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';\n let result = '';\n const values = new Uint32Array(length);\n window.crypto.getRandomValues(values);\n for (let i = 0; i < length; i++) {\n result += charset[values[i] % charset.length];\n }\n return result;\n}\n\nexport async function generateCodeChallenge2(codeVerifier: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(codeVerifier);\n // Hash the data using SHA-256\n const digest = await window.crypto.subtle.digest('SHA-256', data);\n // Base64-url encode the hash\n return base64UrlEncode(digest);\n}\n","import { generateRandomString, generateCodeChallenge2 } from './helper';\n\nexport interface SSOConfig {\n clientId: string;\n redirectUri: string;\n ssoBaseUrl: string;\n}\n\nexport async function generateSSOLoginUrl(config: SSOConfig): Promise<string> {\n const codeVerifier = generateRandomString(64);\n\n localStorage.setItem('sso_code_verifier', codeVerifier);\n\n const codeChallenge = await generateCodeChallenge2(codeVerifier);\n\n const params = new URLSearchParams({\n response_type: 'code',\n client_id: config.clientId,\n redirect_uri: config.redirectUri,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n });\n\n return `${config.ssoBaseUrl}/callback?${params.toString()}`;\n}\n\nexport function getSSOTokenPayload(token: string): Record<string, any> | null {\n try {\n const base64Url = token.split('.')[1];\n if (!base64Url) return null;\n const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');\n const jsonPayload = decodeURIComponent(\n atob(base64)\n .split('')\n .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))\n .join('')\n );\n return JSON.parse(jsonPayload);\n } catch (error) {\n console.error('Failed to parse SSO token payload', error);\n return null;\n }\n}\n\nexport interface SSOExchangeConfig {\n code: string;\n clientId: string;\n redirectUri: string;\n}\n\nexport function getSSOExchangeBody(config: SSOExchangeConfig) {\n const codeVerifier = localStorage.getItem('sso_code_verifier') || '';\n if (!codeVerifier) {\n throw new Error('Code verifier not found');\n }\n\n return {\n grant_type: 'authorization_code',\n code: config.code,\n redirect_uri: config.redirectUri,\n client_id: config.clientId,\n code_verifier: codeVerifier,\n };\n}\n\nexport function clearSSOData(): void {\n localStorage.removeItem('sso_state');\n localStorage.removeItem('sso_code_verifier');\n localStorage.removeItem('sso_token'); // Assuming the token might be stored this way, though up to the app\n}\n"],"mappings":";AACA,SAAS,gBAAgB,aAAkC;AACzD,QAAM,QAAQ,IAAI,WAAW,WAAW;AACxC,MAAI,SAAS;AAEb,WAAS,IAAI,GAAG,IAAI,MAAM,YAAY,KAAK;AACzC,cAAU,OAAO,aAAa,MAAM,CAAC,CAAC;AAAA,EACxC;AACA,QAAM,SAAS,KAAK,MAAM;AAE1B,SAAO,OACJ,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,EAAE;AACtB;AAEO,SAAS,qBAAqB,QAAwB;AAC3D,QAAM,UAAU;AAChB,MAAI,SAAS;AACb,QAAM,SAAS,IAAI,YAAY,MAAM;AACrC,SAAO,OAAO,gBAAgB,MAAM;AACpC,WAAS,IAAI,GAAG,IAAI,QAAQ,KAAK;AAC/B,cAAU,QAAQ,OAAO,CAAC,IAAI,QAAQ,MAAM;AAAA,EAC9C;AACA,SAAO;AACT;AAEA,eAAsB,uBAAuB,cAAuC;AAClF,QAAM,UAAU,IAAI,YAAY;AAChC,QAAM,OAAO,QAAQ,OAAO,YAAY;AAExC,QAAM,SAAS,MAAM,OAAO,OAAO,OAAO,OAAO,WAAW,IAAI;AAEhE,SAAO,gBAAgB,MAAM;AAC/B;;;AC1BA,eAAsB,oBAAoB,QAAoC;AAC5E,QAAM,eAAe,qBAAqB,EAAE;AAE5C,eAAa,QAAQ,qBAAqB,YAAY;AAEtD,QAAM,gBAAgB,MAAM,uBAAuB,YAAY;AAE/D,QAAM,SAAS,IAAI,gBAAgB;AAAA,IACjC,eAAe;AAAA,IACf,WAAW,OAAO;AAAA,IAClB,cAAc,OAAO;AAAA,IACrB,gBAAgB;AAAA,IAChB,uBAAuB;AAAA,EACzB,CAAC;AAED,SAAO,GAAG,OAAO,UAAU,aAAa,OAAO,SAAS,CAAC;AAC3D;AAEO,SAAS,mBAAmB,OAA2C;AAC5E,MAAI;AACF,UAAM,YAAY,MAAM,MAAM,GAAG,EAAE,CAAC;AACpC,QAAI,CAAC,UAAW,QAAO;AACvB,UAAM,SAAS,UAAU,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG;AAC7D,UAAM,cAAc;AAAA,MAClB,KAAK,MAAM,EACR,MAAM,EAAE,EACR,IAAI,CAAC,MAAM,OAAO,OAAO,EAAE,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,MAAM,EAAE,CAAC,EAChE,KAAK,EAAE;AAAA,IACZ;AACA,WAAO,KAAK,MAAM,WAAW;AAAA,EAC/B,SAAS,OAAO;AACd,YAAQ,MAAM,qCAAqC,KAAK;AACxD,WAAO;AAAA,EACT;AACF;AAQO,SAAS,mBAAmB,QAA2B;AAC5D,QAAM,eAAe,aAAa,QAAQ,mBAAmB,KAAK;AAClE,MAAI,CAAC,cAAc;AACjB,UAAM,IAAI,MAAM,yBAAyB;AAAA,EAC3C;AAEA,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,MAAM,OAAO;AAAA,IACb,cAAc,OAAO;AAAA,IACrB,WAAW,OAAO;AAAA,IAClB,eAAe;AAAA,EACjB;AACF;AAEO,SAAS,eAAqB;AACnC,eAAa,WAAW,WAAW;AACnC,eAAa,WAAW,mBAAmB;AAC3C,eAAa,WAAW,WAAW;AACrC;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hengkianggia/sso-erlangga",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Helper package for SSO PKCE integration",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsup",
|
|
10
|
+
"dev": "tsup --watch",
|
|
11
|
+
"lint": "eslint src/",
|
|
12
|
+
"typecheck": "tsc --noEmit",
|
|
13
|
+
"prepare": "pnpm run build"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"sso",
|
|
20
|
+
"pkce",
|
|
21
|
+
"oauth2",
|
|
22
|
+
"auth"
|
|
23
|
+
],
|
|
24
|
+
"author": "",
|
|
25
|
+
"license": "ISC",
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"tsup": "^8.0.2",
|
|
28
|
+
"typescript": "^5.4.3"
|
|
29
|
+
}
|
|
30
|
+
}
|