@zskj-idaas/sdk 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/LICENSE +21 -0
- package/README.md +197 -0
- package/dist/index.d.mts +89 -0
- package/dist/index.d.ts +89 -0
- package/dist/index.js +344 -0
- package/dist/index.mjs +313 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 IDaaS Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# @zskj-idaas/sdk
|
|
2
|
+
|
|
3
|
+
IDaaS OIDC Client SDK — 适用于浏览器应用的 OpenID Connect 客户端,支持 Authorization Code Flow with PKCE。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- **零运行时依赖** — 轻量、无外部依赖
|
|
8
|
+
- **PKCE 支持** — 自动生成 code_verifier / code_challenge
|
|
9
|
+
- **双模块输出** — 同时支持 ESM 和 CommonJS
|
|
10
|
+
- **TypeScript** — 完整的类型定义
|
|
11
|
+
- **OIDC Discovery** — 自动发现 OpenID 配置端点
|
|
12
|
+
- **Token 管理** — 自动存储、过期检测、静默刷新
|
|
13
|
+
- **认证 Fetch** — 内置带 Bearer Token 的 fetch 封装
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @zskj-idaas/sdk
|
|
19
|
+
# 或
|
|
20
|
+
pnpm add @zskj-idaas/sdk
|
|
21
|
+
# 或
|
|
22
|
+
yarn add @zskj-idaas/sdk
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 快速开始
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { IdaasClient } from '@zskj-idaas/sdk';
|
|
29
|
+
|
|
30
|
+
const client = new IdaasClient({
|
|
31
|
+
issuer: 'https://your-idaas-server.com',
|
|
32
|
+
clientId: 'your-client-id',
|
|
33
|
+
redirectUri: 'http://localhost:3000/callback',
|
|
34
|
+
scope: 'openid profile email',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// 初始化(自动发现 OIDC 端点)
|
|
38
|
+
await client.init();
|
|
39
|
+
|
|
40
|
+
// 发起登录
|
|
41
|
+
await client.login();
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## API 参考
|
|
45
|
+
|
|
46
|
+
### `new IdaasClient(config)`
|
|
47
|
+
|
|
48
|
+
创建 SDK 客户端实例。
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
interface IdaasConfig {
|
|
52
|
+
issuer: string; // OIDC 提供者地址
|
|
53
|
+
clientId: string; // 客户端 ID
|
|
54
|
+
clientSecret?: string; // 客户端密钥(机密客户端)
|
|
55
|
+
redirectUri: string; // 登录回调地址
|
|
56
|
+
postLogoutRedirectUri?: string; // 登出后跳转地址
|
|
57
|
+
scope?: string; // 请求的 scope(默认: 'openid profile email')
|
|
58
|
+
autoDiscovery?: boolean; // 是否自动发现端点(默认: true)
|
|
59
|
+
storagePrefix?: string; // 存储 key 前缀(默认: 'idaas')
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### `client.init(): Promise<AuthState>`
|
|
64
|
+
|
|
65
|
+
初始化客户端,执行 OIDC Discovery 并返回当前认证状态。
|
|
66
|
+
|
|
67
|
+
### `client.login(extraParams?): Promise<void>`
|
|
68
|
+
|
|
69
|
+
发起 OIDC 授权码登录流程,跳转到授权端点。
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// 附加额外参数
|
|
73
|
+
await client.login({ prompt: 'login', login_hint: 'user@example.com' });
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `client.handleCallback(callbackUrl?): Promise<TokenSet>`
|
|
77
|
+
|
|
78
|
+
处理授权回调,用授权码交换 Token。
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// 在回调页面调用
|
|
82
|
+
const tokens = await client.handleCallback();
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### `client.getUserInfo(forceRefresh?): Promise<UserInfo | null>`
|
|
86
|
+
|
|
87
|
+
获取当前用户信息。
|
|
88
|
+
|
|
89
|
+
### `client.getAccessToken(): Promise<string | null>`
|
|
90
|
+
|
|
91
|
+
获取有效的 Access Token(过期时自动刷新)。
|
|
92
|
+
|
|
93
|
+
### `client.getIdToken(): string | null`
|
|
94
|
+
|
|
95
|
+
获取 ID Token。
|
|
96
|
+
|
|
97
|
+
### `client.getAuthState(): AuthState`
|
|
98
|
+
|
|
99
|
+
获取当前认证状态。
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
interface AuthState {
|
|
103
|
+
isAuthenticated: boolean;
|
|
104
|
+
user: UserInfo | null;
|
|
105
|
+
accessToken: string | null;
|
|
106
|
+
idToken: string | null;
|
|
107
|
+
expiresAt: number | null;
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### `client.isAuthenticated(): boolean`
|
|
112
|
+
|
|
113
|
+
检查是否已认证。
|
|
114
|
+
|
|
115
|
+
### `client.logout(): Promise<void>`
|
|
116
|
+
|
|
117
|
+
执行 OIDC 登出流程。
|
|
118
|
+
|
|
119
|
+
### `client.revokeToken(): Promise<void>`
|
|
120
|
+
|
|
121
|
+
撤销 Refresh Token。
|
|
122
|
+
|
|
123
|
+
### `client.fetch(url, init?): Promise<Response>`
|
|
124
|
+
|
|
125
|
+
带认证的 fetch 封装,自动附加 Authorization 头。
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
const res = await client.fetch('/api/protected-resource');
|
|
129
|
+
const data = await res.json();
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## 完整示例
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import { IdaasClient } from '@zskj-idaas/sdk';
|
|
136
|
+
import type { AuthState, UserInfo } from '@zskj-idaas/sdk';
|
|
137
|
+
|
|
138
|
+
const client = new IdaasClient({
|
|
139
|
+
issuer: 'https://idaas.example.com',
|
|
140
|
+
clientId: 'my-app',
|
|
141
|
+
redirectUri: `${window.location.origin}/callback`,
|
|
142
|
+
postLogoutRedirectUri: window.location.origin,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// 应用启动时初始化
|
|
146
|
+
async function bootstrap() {
|
|
147
|
+
const state: AuthState = await client.init();
|
|
148
|
+
|
|
149
|
+
if (state.isAuthenticated) {
|
|
150
|
+
const user: UserInfo | null = await client.getUserInfo();
|
|
151
|
+
console.log('已登录:', user?.name);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 回调页面处理
|
|
156
|
+
async function handleCallback() {
|
|
157
|
+
try {
|
|
158
|
+
await client.handleCallback();
|
|
159
|
+
window.location.href = '/';
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.error('登录失败:', err);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 调用受保护的 API
|
|
166
|
+
async function fetchData() {
|
|
167
|
+
const res = await client.fetch('https://api.example.com/data');
|
|
168
|
+
return res.json();
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## 导出内容
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// 类
|
|
176
|
+
export { IdaasClient } from './client';
|
|
177
|
+
export { TokenStorage } from './storage';
|
|
178
|
+
|
|
179
|
+
// 工具函数
|
|
180
|
+
export { createPKCE, generateState, generateNonce } from './crypto';
|
|
181
|
+
|
|
182
|
+
// 类型
|
|
183
|
+
export type { IdaasConfig, TokenSet, UserInfo, AuthState } from './types';
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## 浏览器兼容性
|
|
187
|
+
|
|
188
|
+
需要支持 Web Crypto API 的现代浏览器:
|
|
189
|
+
|
|
190
|
+
- Chrome 63+
|
|
191
|
+
- Firefox 57+
|
|
192
|
+
- Safari 11.1+
|
|
193
|
+
- Edge 79+
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
interface IdaasConfig {
|
|
2
|
+
issuer: string;
|
|
3
|
+
clientId: string;
|
|
4
|
+
clientSecret?: string;
|
|
5
|
+
redirectUri: string;
|
|
6
|
+
postLogoutRedirectUri?: string;
|
|
7
|
+
scope?: string;
|
|
8
|
+
/** Auto-discover OIDC endpoints from issuer */
|
|
9
|
+
autoDiscovery?: boolean;
|
|
10
|
+
/** Storage key prefix */
|
|
11
|
+
storagePrefix?: string;
|
|
12
|
+
}
|
|
13
|
+
interface TokenSet {
|
|
14
|
+
access_token: string;
|
|
15
|
+
token_type: string;
|
|
16
|
+
expires_in: number;
|
|
17
|
+
id_token?: string;
|
|
18
|
+
refresh_token?: string;
|
|
19
|
+
scope: string;
|
|
20
|
+
}
|
|
21
|
+
interface UserInfo {
|
|
22
|
+
sub: string;
|
|
23
|
+
preferred_username?: string;
|
|
24
|
+
name?: string;
|
|
25
|
+
email?: string;
|
|
26
|
+
phone_number?: string;
|
|
27
|
+
picture?: string;
|
|
28
|
+
roles?: string[];
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
}
|
|
31
|
+
interface AuthState {
|
|
32
|
+
isAuthenticated: boolean;
|
|
33
|
+
user: UserInfo | null;
|
|
34
|
+
accessToken: string | null;
|
|
35
|
+
idToken: string | null;
|
|
36
|
+
expiresAt: number | null;
|
|
37
|
+
}
|
|
38
|
+
interface PKCEPair {
|
|
39
|
+
codeVerifier: string;
|
|
40
|
+
codeChallenge: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
declare class IdaasClient {
|
|
44
|
+
private config;
|
|
45
|
+
private endpoints;
|
|
46
|
+
private storage;
|
|
47
|
+
private userInfo;
|
|
48
|
+
constructor(config: IdaasConfig);
|
|
49
|
+
init(): Promise<AuthState>;
|
|
50
|
+
private discover;
|
|
51
|
+
login(extraParams?: Record<string, string>): Promise<void>;
|
|
52
|
+
handleCallback(callbackUrl?: string): Promise<TokenSet>;
|
|
53
|
+
refreshToken(): Promise<TokenSet | null>;
|
|
54
|
+
getUserInfo(forceRefresh?: boolean): Promise<UserInfo | null>;
|
|
55
|
+
getAccessToken(): Promise<string | null>;
|
|
56
|
+
getIdToken(): string | null;
|
|
57
|
+
getAuthState(): AuthState;
|
|
58
|
+
isAuthenticated(): boolean;
|
|
59
|
+
logout(): Promise<void>;
|
|
60
|
+
revokeToken(): Promise<void>;
|
|
61
|
+
/** Create an authenticated fetch wrapper */
|
|
62
|
+
fetch(url: string, init?: RequestInit): Promise<Response>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
declare function createPKCE(): Promise<PKCEPair>;
|
|
66
|
+
declare function generateState(): string;
|
|
67
|
+
declare function generateNonce(): string;
|
|
68
|
+
|
|
69
|
+
declare class TokenStorage {
|
|
70
|
+
private prefix;
|
|
71
|
+
constructor(prefix?: string);
|
|
72
|
+
private key;
|
|
73
|
+
saveTokens(tokens: TokenSet): void;
|
|
74
|
+
getAccessToken(): string | null;
|
|
75
|
+
getIdToken(): string | null;
|
|
76
|
+
getRefreshToken(): string | null;
|
|
77
|
+
getExpiresAt(): number | null;
|
|
78
|
+
isTokenExpired(): boolean;
|
|
79
|
+
clear(): void;
|
|
80
|
+
saveState(state: string): void;
|
|
81
|
+
getState(): string | null;
|
|
82
|
+
saveCodeVerifier(verifier: string): void;
|
|
83
|
+
getCodeVerifier(): string | null;
|
|
84
|
+
saveNonce(nonce: string): void;
|
|
85
|
+
getNonce(): string | null;
|
|
86
|
+
clearAuthState(): void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { type AuthState, IdaasClient, type IdaasConfig, type TokenSet, TokenStorage, type UserInfo, createPKCE, generateNonce, generateState };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
interface IdaasConfig {
|
|
2
|
+
issuer: string;
|
|
3
|
+
clientId: string;
|
|
4
|
+
clientSecret?: string;
|
|
5
|
+
redirectUri: string;
|
|
6
|
+
postLogoutRedirectUri?: string;
|
|
7
|
+
scope?: string;
|
|
8
|
+
/** Auto-discover OIDC endpoints from issuer */
|
|
9
|
+
autoDiscovery?: boolean;
|
|
10
|
+
/** Storage key prefix */
|
|
11
|
+
storagePrefix?: string;
|
|
12
|
+
}
|
|
13
|
+
interface TokenSet {
|
|
14
|
+
access_token: string;
|
|
15
|
+
token_type: string;
|
|
16
|
+
expires_in: number;
|
|
17
|
+
id_token?: string;
|
|
18
|
+
refresh_token?: string;
|
|
19
|
+
scope: string;
|
|
20
|
+
}
|
|
21
|
+
interface UserInfo {
|
|
22
|
+
sub: string;
|
|
23
|
+
preferred_username?: string;
|
|
24
|
+
name?: string;
|
|
25
|
+
email?: string;
|
|
26
|
+
phone_number?: string;
|
|
27
|
+
picture?: string;
|
|
28
|
+
roles?: string[];
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
}
|
|
31
|
+
interface AuthState {
|
|
32
|
+
isAuthenticated: boolean;
|
|
33
|
+
user: UserInfo | null;
|
|
34
|
+
accessToken: string | null;
|
|
35
|
+
idToken: string | null;
|
|
36
|
+
expiresAt: number | null;
|
|
37
|
+
}
|
|
38
|
+
interface PKCEPair {
|
|
39
|
+
codeVerifier: string;
|
|
40
|
+
codeChallenge: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
declare class IdaasClient {
|
|
44
|
+
private config;
|
|
45
|
+
private endpoints;
|
|
46
|
+
private storage;
|
|
47
|
+
private userInfo;
|
|
48
|
+
constructor(config: IdaasConfig);
|
|
49
|
+
init(): Promise<AuthState>;
|
|
50
|
+
private discover;
|
|
51
|
+
login(extraParams?: Record<string, string>): Promise<void>;
|
|
52
|
+
handleCallback(callbackUrl?: string): Promise<TokenSet>;
|
|
53
|
+
refreshToken(): Promise<TokenSet | null>;
|
|
54
|
+
getUserInfo(forceRefresh?: boolean): Promise<UserInfo | null>;
|
|
55
|
+
getAccessToken(): Promise<string | null>;
|
|
56
|
+
getIdToken(): string | null;
|
|
57
|
+
getAuthState(): AuthState;
|
|
58
|
+
isAuthenticated(): boolean;
|
|
59
|
+
logout(): Promise<void>;
|
|
60
|
+
revokeToken(): Promise<void>;
|
|
61
|
+
/** Create an authenticated fetch wrapper */
|
|
62
|
+
fetch(url: string, init?: RequestInit): Promise<Response>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
declare function createPKCE(): Promise<PKCEPair>;
|
|
66
|
+
declare function generateState(): string;
|
|
67
|
+
declare function generateNonce(): string;
|
|
68
|
+
|
|
69
|
+
declare class TokenStorage {
|
|
70
|
+
private prefix;
|
|
71
|
+
constructor(prefix?: string);
|
|
72
|
+
private key;
|
|
73
|
+
saveTokens(tokens: TokenSet): void;
|
|
74
|
+
getAccessToken(): string | null;
|
|
75
|
+
getIdToken(): string | null;
|
|
76
|
+
getRefreshToken(): string | null;
|
|
77
|
+
getExpiresAt(): number | null;
|
|
78
|
+
isTokenExpired(): boolean;
|
|
79
|
+
clear(): void;
|
|
80
|
+
saveState(state: string): void;
|
|
81
|
+
getState(): string | null;
|
|
82
|
+
saveCodeVerifier(verifier: string): void;
|
|
83
|
+
getCodeVerifier(): string | null;
|
|
84
|
+
saveNonce(nonce: string): void;
|
|
85
|
+
getNonce(): string | null;
|
|
86
|
+
clearAuthState(): void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { type AuthState, IdaasClient, type IdaasConfig, type TokenSet, TokenStorage, type UserInfo, createPKCE, generateNonce, generateState };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
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
|
+
IdaasClient: () => IdaasClient,
|
|
24
|
+
TokenStorage: () => TokenStorage,
|
|
25
|
+
createPKCE: () => createPKCE,
|
|
26
|
+
generateNonce: () => generateNonce,
|
|
27
|
+
generateState: () => generateState
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// src/crypto.ts
|
|
32
|
+
function generateRandomString(length) {
|
|
33
|
+
const array = new Uint8Array(length);
|
|
34
|
+
crypto.getRandomValues(array);
|
|
35
|
+
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("").substring(0, length);
|
|
36
|
+
}
|
|
37
|
+
function base64UrlEncode(buffer) {
|
|
38
|
+
const bytes = new Uint8Array(buffer);
|
|
39
|
+
let binary = "";
|
|
40
|
+
bytes.forEach((b) => {
|
|
41
|
+
binary += String.fromCharCode(b);
|
|
42
|
+
});
|
|
43
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
44
|
+
}
|
|
45
|
+
async function createPKCE() {
|
|
46
|
+
const codeVerifier = generateRandomString(64);
|
|
47
|
+
const encoder = new TextEncoder();
|
|
48
|
+
const data = encoder.encode(codeVerifier);
|
|
49
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
50
|
+
const codeChallenge = base64UrlEncode(digest);
|
|
51
|
+
return { codeVerifier, codeChallenge };
|
|
52
|
+
}
|
|
53
|
+
function generateState() {
|
|
54
|
+
return generateRandomString(32);
|
|
55
|
+
}
|
|
56
|
+
function generateNonce() {
|
|
57
|
+
return generateRandomString(32);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/storage.ts
|
|
61
|
+
var TokenStorage = class {
|
|
62
|
+
constructor(prefix = "idaas") {
|
|
63
|
+
this.prefix = prefix;
|
|
64
|
+
}
|
|
65
|
+
key(name) {
|
|
66
|
+
return `${this.prefix}:${name}`;
|
|
67
|
+
}
|
|
68
|
+
saveTokens(tokens) {
|
|
69
|
+
const expiresAt = Date.now() + tokens.expires_in * 1e3;
|
|
70
|
+
sessionStorage.setItem(this.key("access_token"), tokens.access_token);
|
|
71
|
+
sessionStorage.setItem(this.key("expires_at"), expiresAt.toString());
|
|
72
|
+
if (tokens.id_token) {
|
|
73
|
+
sessionStorage.setItem(this.key("id_token"), tokens.id_token);
|
|
74
|
+
}
|
|
75
|
+
if (tokens.refresh_token) {
|
|
76
|
+
localStorage.setItem(this.key("refresh_token"), tokens.refresh_token);
|
|
77
|
+
}
|
|
78
|
+
sessionStorage.setItem(this.key("scope"), tokens.scope);
|
|
79
|
+
}
|
|
80
|
+
getAccessToken() {
|
|
81
|
+
const token = sessionStorage.getItem(this.key("access_token"));
|
|
82
|
+
const expiresAt = sessionStorage.getItem(this.key("expires_at"));
|
|
83
|
+
if (!token || !expiresAt) return null;
|
|
84
|
+
if (Date.now() > parseInt(expiresAt, 10)) return null;
|
|
85
|
+
return token;
|
|
86
|
+
}
|
|
87
|
+
getIdToken() {
|
|
88
|
+
return sessionStorage.getItem(this.key("id_token"));
|
|
89
|
+
}
|
|
90
|
+
getRefreshToken() {
|
|
91
|
+
return localStorage.getItem(this.key("refresh_token"));
|
|
92
|
+
}
|
|
93
|
+
getExpiresAt() {
|
|
94
|
+
const val = sessionStorage.getItem(this.key("expires_at"));
|
|
95
|
+
return val ? parseInt(val, 10) : null;
|
|
96
|
+
}
|
|
97
|
+
isTokenExpired() {
|
|
98
|
+
const expiresAt = this.getExpiresAt();
|
|
99
|
+
if (!expiresAt) return true;
|
|
100
|
+
return Date.now() > expiresAt - 6e4;
|
|
101
|
+
}
|
|
102
|
+
clear() {
|
|
103
|
+
sessionStorage.removeItem(this.key("access_token"));
|
|
104
|
+
sessionStorage.removeItem(this.key("id_token"));
|
|
105
|
+
sessionStorage.removeItem(this.key("expires_at"));
|
|
106
|
+
sessionStorage.removeItem(this.key("scope"));
|
|
107
|
+
localStorage.removeItem(this.key("refresh_token"));
|
|
108
|
+
}
|
|
109
|
+
saveState(state) {
|
|
110
|
+
sessionStorage.setItem(this.key("state"), state);
|
|
111
|
+
}
|
|
112
|
+
getState() {
|
|
113
|
+
return sessionStorage.getItem(this.key("state"));
|
|
114
|
+
}
|
|
115
|
+
saveCodeVerifier(verifier) {
|
|
116
|
+
sessionStorage.setItem(this.key("code_verifier"), verifier);
|
|
117
|
+
}
|
|
118
|
+
getCodeVerifier() {
|
|
119
|
+
return sessionStorage.getItem(this.key("code_verifier"));
|
|
120
|
+
}
|
|
121
|
+
saveNonce(nonce) {
|
|
122
|
+
sessionStorage.setItem(this.key("nonce"), nonce);
|
|
123
|
+
}
|
|
124
|
+
getNonce() {
|
|
125
|
+
return sessionStorage.getItem(this.key("nonce"));
|
|
126
|
+
}
|
|
127
|
+
clearAuthState() {
|
|
128
|
+
sessionStorage.removeItem(this.key("state"));
|
|
129
|
+
sessionStorage.removeItem(this.key("code_verifier"));
|
|
130
|
+
sessionStorage.removeItem(this.key("nonce"));
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// src/client.ts
|
|
135
|
+
var IdaasClient = class {
|
|
136
|
+
constructor(config) {
|
|
137
|
+
this.endpoints = null;
|
|
138
|
+
this.userInfo = null;
|
|
139
|
+
this.config = {
|
|
140
|
+
scope: "openid profile email",
|
|
141
|
+
autoDiscovery: true,
|
|
142
|
+
storagePrefix: "idaas",
|
|
143
|
+
...config
|
|
144
|
+
};
|
|
145
|
+
this.storage = new TokenStorage(this.config.storagePrefix);
|
|
146
|
+
}
|
|
147
|
+
async init() {
|
|
148
|
+
if (this.config.autoDiscovery) {
|
|
149
|
+
await this.discover();
|
|
150
|
+
} else {
|
|
151
|
+
this.endpoints = {
|
|
152
|
+
authorization_endpoint: `${this.config.issuer}/oauth2/authorize`,
|
|
153
|
+
token_endpoint: `${this.config.issuer}/oauth2/token`,
|
|
154
|
+
userinfo_endpoint: `${this.config.issuer}/oauth2/userinfo`,
|
|
155
|
+
end_session_endpoint: `${this.config.issuer}/oauth2/logout`,
|
|
156
|
+
jwks_uri: `${this.config.issuer}/.well-known/jwks.json`,
|
|
157
|
+
revocation_endpoint: `${this.config.issuer}/oauth2/revoke`,
|
|
158
|
+
introspection_endpoint: `${this.config.issuer}/oauth2/introspect`
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return this.getAuthState();
|
|
162
|
+
}
|
|
163
|
+
async discover() {
|
|
164
|
+
const res = await fetch(`${this.config.issuer}/.well-known/openid-configuration`);
|
|
165
|
+
if (!res.ok) throw new Error("Failed to fetch OIDC discovery document");
|
|
166
|
+
const doc = await res.json();
|
|
167
|
+
this.endpoints = {
|
|
168
|
+
authorization_endpoint: doc.authorization_endpoint,
|
|
169
|
+
token_endpoint: doc.token_endpoint,
|
|
170
|
+
userinfo_endpoint: doc.userinfo_endpoint,
|
|
171
|
+
end_session_endpoint: doc.end_session_endpoint,
|
|
172
|
+
jwks_uri: doc.jwks_uri,
|
|
173
|
+
revocation_endpoint: doc.revocation_endpoint,
|
|
174
|
+
introspection_endpoint: doc.introspection_endpoint
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
async login(extraParams) {
|
|
178
|
+
if (!this.endpoints) await this.init();
|
|
179
|
+
const { codeVerifier, codeChallenge } = await createPKCE();
|
|
180
|
+
const state = generateState();
|
|
181
|
+
const nonce = generateNonce();
|
|
182
|
+
this.storage.saveCodeVerifier(codeVerifier);
|
|
183
|
+
this.storage.saveState(state);
|
|
184
|
+
this.storage.saveNonce(nonce);
|
|
185
|
+
const params = new URLSearchParams({
|
|
186
|
+
response_type: "code",
|
|
187
|
+
client_id: this.config.clientId,
|
|
188
|
+
redirect_uri: this.config.redirectUri,
|
|
189
|
+
scope: this.config.scope,
|
|
190
|
+
state,
|
|
191
|
+
nonce,
|
|
192
|
+
code_challenge: codeChallenge,
|
|
193
|
+
code_challenge_method: "S256",
|
|
194
|
+
...extraParams
|
|
195
|
+
});
|
|
196
|
+
window.location.href = `${this.endpoints.authorization_endpoint}?${params.toString()}`;
|
|
197
|
+
}
|
|
198
|
+
async handleCallback(callbackUrl) {
|
|
199
|
+
if (!this.endpoints) await this.init();
|
|
200
|
+
const url = new URL(callbackUrl || window.location.href);
|
|
201
|
+
const code = url.searchParams.get("code");
|
|
202
|
+
const state = url.searchParams.get("state");
|
|
203
|
+
const error = url.searchParams.get("error");
|
|
204
|
+
if (error) {
|
|
205
|
+
const desc = url.searchParams.get("error_description") || error;
|
|
206
|
+
throw new Error(desc);
|
|
207
|
+
}
|
|
208
|
+
if (!code) throw new Error("No authorization code in callback URL");
|
|
209
|
+
const savedState = this.storage.getState();
|
|
210
|
+
if (state !== savedState) {
|
|
211
|
+
throw new Error("State mismatch - possible CSRF attack");
|
|
212
|
+
}
|
|
213
|
+
const codeVerifier = this.storage.getCodeVerifier();
|
|
214
|
+
if (!codeVerifier) throw new Error("No code verifier found");
|
|
215
|
+
const body = new URLSearchParams({
|
|
216
|
+
grant_type: "authorization_code",
|
|
217
|
+
code,
|
|
218
|
+
redirect_uri: this.config.redirectUri,
|
|
219
|
+
client_id: this.config.clientId,
|
|
220
|
+
code_verifier: codeVerifier
|
|
221
|
+
});
|
|
222
|
+
if (this.config.clientSecret) {
|
|
223
|
+
body.set("client_secret", this.config.clientSecret);
|
|
224
|
+
}
|
|
225
|
+
const res = await fetch(this.endpoints.token_endpoint, {
|
|
226
|
+
method: "POST",
|
|
227
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
228
|
+
body: body.toString()
|
|
229
|
+
});
|
|
230
|
+
if (!res.ok) {
|
|
231
|
+
const err = await res.json().catch(() => ({ error: "token_exchange_failed" }));
|
|
232
|
+
throw new Error(err.error_description || err.error || "Token exchange failed");
|
|
233
|
+
}
|
|
234
|
+
const tokens = await res.json();
|
|
235
|
+
this.storage.saveTokens(tokens);
|
|
236
|
+
this.storage.clearAuthState();
|
|
237
|
+
return tokens;
|
|
238
|
+
}
|
|
239
|
+
async refreshToken() {
|
|
240
|
+
if (!this.endpoints) await this.init();
|
|
241
|
+
const refreshToken = this.storage.getRefreshToken();
|
|
242
|
+
if (!refreshToken) return null;
|
|
243
|
+
const body = new URLSearchParams({
|
|
244
|
+
grant_type: "refresh_token",
|
|
245
|
+
refresh_token: refreshToken,
|
|
246
|
+
client_id: this.config.clientId
|
|
247
|
+
});
|
|
248
|
+
if (this.config.clientSecret) {
|
|
249
|
+
body.set("client_secret", this.config.clientSecret);
|
|
250
|
+
}
|
|
251
|
+
const res = await fetch(this.endpoints.token_endpoint, {
|
|
252
|
+
method: "POST",
|
|
253
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
254
|
+
body: body.toString()
|
|
255
|
+
});
|
|
256
|
+
if (!res.ok) {
|
|
257
|
+
this.storage.clear();
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
const tokens = await res.json();
|
|
261
|
+
this.storage.saveTokens(tokens);
|
|
262
|
+
return tokens;
|
|
263
|
+
}
|
|
264
|
+
async getUserInfo(forceRefresh = false) {
|
|
265
|
+
if (!this.endpoints) await this.init();
|
|
266
|
+
if (this.userInfo && !forceRefresh) return this.userInfo;
|
|
267
|
+
const accessToken = await this.getAccessToken();
|
|
268
|
+
if (!accessToken) return null;
|
|
269
|
+
const res = await fetch(this.endpoints.userinfo_endpoint, {
|
|
270
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
271
|
+
});
|
|
272
|
+
if (!res.ok) return null;
|
|
273
|
+
this.userInfo = await res.json();
|
|
274
|
+
return this.userInfo;
|
|
275
|
+
}
|
|
276
|
+
async getAccessToken() {
|
|
277
|
+
let token = this.storage.getAccessToken();
|
|
278
|
+
if (token) return token;
|
|
279
|
+
if (this.storage.isTokenExpired()) {
|
|
280
|
+
const tokens = await this.refreshToken();
|
|
281
|
+
if (tokens) return tokens.access_token;
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
getIdToken() {
|
|
286
|
+
return this.storage.getIdToken();
|
|
287
|
+
}
|
|
288
|
+
getAuthState() {
|
|
289
|
+
const accessToken = this.storage.getAccessToken();
|
|
290
|
+
return {
|
|
291
|
+
isAuthenticated: !!accessToken,
|
|
292
|
+
user: this.userInfo,
|
|
293
|
+
accessToken,
|
|
294
|
+
idToken: this.storage.getIdToken(),
|
|
295
|
+
expiresAt: this.storage.getExpiresAt()
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
isAuthenticated() {
|
|
299
|
+
return !!this.storage.getAccessToken();
|
|
300
|
+
}
|
|
301
|
+
async logout() {
|
|
302
|
+
if (!this.endpoints) await this.init();
|
|
303
|
+
const idToken = this.storage.getIdToken();
|
|
304
|
+
this.storage.clear();
|
|
305
|
+
this.userInfo = null;
|
|
306
|
+
const params = new URLSearchParams();
|
|
307
|
+
if (idToken) params.set("id_token_hint", idToken);
|
|
308
|
+
if (this.config.postLogoutRedirectUri) {
|
|
309
|
+
params.set("post_logout_redirect_uri", this.config.postLogoutRedirectUri);
|
|
310
|
+
}
|
|
311
|
+
window.location.href = `${this.endpoints.end_session_endpoint}?${params.toString()}`;
|
|
312
|
+
}
|
|
313
|
+
async revokeToken() {
|
|
314
|
+
if (!this.endpoints) await this.init();
|
|
315
|
+
const refreshToken = this.storage.getRefreshToken();
|
|
316
|
+
if (refreshToken) {
|
|
317
|
+
await fetch(this.endpoints.revocation_endpoint, {
|
|
318
|
+
method: "POST",
|
|
319
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
320
|
+
body: new URLSearchParams({
|
|
321
|
+
token: refreshToken,
|
|
322
|
+
token_type_hint: "refresh_token"
|
|
323
|
+
}).toString()
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/** Create an authenticated fetch wrapper */
|
|
328
|
+
async fetch(url, init) {
|
|
329
|
+
const token = await this.getAccessToken();
|
|
330
|
+
const headers = new Headers(init?.headers);
|
|
331
|
+
if (token) {
|
|
332
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
333
|
+
}
|
|
334
|
+
return fetch(url, { ...init, headers });
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
338
|
+
0 && (module.exports = {
|
|
339
|
+
IdaasClient,
|
|
340
|
+
TokenStorage,
|
|
341
|
+
createPKCE,
|
|
342
|
+
generateNonce,
|
|
343
|
+
generateState
|
|
344
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// src/crypto.ts
|
|
2
|
+
function generateRandomString(length) {
|
|
3
|
+
const array = new Uint8Array(length);
|
|
4
|
+
crypto.getRandomValues(array);
|
|
5
|
+
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("").substring(0, length);
|
|
6
|
+
}
|
|
7
|
+
function base64UrlEncode(buffer) {
|
|
8
|
+
const bytes = new Uint8Array(buffer);
|
|
9
|
+
let binary = "";
|
|
10
|
+
bytes.forEach((b) => {
|
|
11
|
+
binary += String.fromCharCode(b);
|
|
12
|
+
});
|
|
13
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
14
|
+
}
|
|
15
|
+
async function createPKCE() {
|
|
16
|
+
const codeVerifier = generateRandomString(64);
|
|
17
|
+
const encoder = new TextEncoder();
|
|
18
|
+
const data = encoder.encode(codeVerifier);
|
|
19
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
20
|
+
const codeChallenge = base64UrlEncode(digest);
|
|
21
|
+
return { codeVerifier, codeChallenge };
|
|
22
|
+
}
|
|
23
|
+
function generateState() {
|
|
24
|
+
return generateRandomString(32);
|
|
25
|
+
}
|
|
26
|
+
function generateNonce() {
|
|
27
|
+
return generateRandomString(32);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/storage.ts
|
|
31
|
+
var TokenStorage = class {
|
|
32
|
+
constructor(prefix = "idaas") {
|
|
33
|
+
this.prefix = prefix;
|
|
34
|
+
}
|
|
35
|
+
key(name) {
|
|
36
|
+
return `${this.prefix}:${name}`;
|
|
37
|
+
}
|
|
38
|
+
saveTokens(tokens) {
|
|
39
|
+
const expiresAt = Date.now() + tokens.expires_in * 1e3;
|
|
40
|
+
sessionStorage.setItem(this.key("access_token"), tokens.access_token);
|
|
41
|
+
sessionStorage.setItem(this.key("expires_at"), expiresAt.toString());
|
|
42
|
+
if (tokens.id_token) {
|
|
43
|
+
sessionStorage.setItem(this.key("id_token"), tokens.id_token);
|
|
44
|
+
}
|
|
45
|
+
if (tokens.refresh_token) {
|
|
46
|
+
localStorage.setItem(this.key("refresh_token"), tokens.refresh_token);
|
|
47
|
+
}
|
|
48
|
+
sessionStorage.setItem(this.key("scope"), tokens.scope);
|
|
49
|
+
}
|
|
50
|
+
getAccessToken() {
|
|
51
|
+
const token = sessionStorage.getItem(this.key("access_token"));
|
|
52
|
+
const expiresAt = sessionStorage.getItem(this.key("expires_at"));
|
|
53
|
+
if (!token || !expiresAt) return null;
|
|
54
|
+
if (Date.now() > parseInt(expiresAt, 10)) return null;
|
|
55
|
+
return token;
|
|
56
|
+
}
|
|
57
|
+
getIdToken() {
|
|
58
|
+
return sessionStorage.getItem(this.key("id_token"));
|
|
59
|
+
}
|
|
60
|
+
getRefreshToken() {
|
|
61
|
+
return localStorage.getItem(this.key("refresh_token"));
|
|
62
|
+
}
|
|
63
|
+
getExpiresAt() {
|
|
64
|
+
const val = sessionStorage.getItem(this.key("expires_at"));
|
|
65
|
+
return val ? parseInt(val, 10) : null;
|
|
66
|
+
}
|
|
67
|
+
isTokenExpired() {
|
|
68
|
+
const expiresAt = this.getExpiresAt();
|
|
69
|
+
if (!expiresAt) return true;
|
|
70
|
+
return Date.now() > expiresAt - 6e4;
|
|
71
|
+
}
|
|
72
|
+
clear() {
|
|
73
|
+
sessionStorage.removeItem(this.key("access_token"));
|
|
74
|
+
sessionStorage.removeItem(this.key("id_token"));
|
|
75
|
+
sessionStorage.removeItem(this.key("expires_at"));
|
|
76
|
+
sessionStorage.removeItem(this.key("scope"));
|
|
77
|
+
localStorage.removeItem(this.key("refresh_token"));
|
|
78
|
+
}
|
|
79
|
+
saveState(state) {
|
|
80
|
+
sessionStorage.setItem(this.key("state"), state);
|
|
81
|
+
}
|
|
82
|
+
getState() {
|
|
83
|
+
return sessionStorage.getItem(this.key("state"));
|
|
84
|
+
}
|
|
85
|
+
saveCodeVerifier(verifier) {
|
|
86
|
+
sessionStorage.setItem(this.key("code_verifier"), verifier);
|
|
87
|
+
}
|
|
88
|
+
getCodeVerifier() {
|
|
89
|
+
return sessionStorage.getItem(this.key("code_verifier"));
|
|
90
|
+
}
|
|
91
|
+
saveNonce(nonce) {
|
|
92
|
+
sessionStorage.setItem(this.key("nonce"), nonce);
|
|
93
|
+
}
|
|
94
|
+
getNonce() {
|
|
95
|
+
return sessionStorage.getItem(this.key("nonce"));
|
|
96
|
+
}
|
|
97
|
+
clearAuthState() {
|
|
98
|
+
sessionStorage.removeItem(this.key("state"));
|
|
99
|
+
sessionStorage.removeItem(this.key("code_verifier"));
|
|
100
|
+
sessionStorage.removeItem(this.key("nonce"));
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// src/client.ts
|
|
105
|
+
var IdaasClient = class {
|
|
106
|
+
constructor(config) {
|
|
107
|
+
this.endpoints = null;
|
|
108
|
+
this.userInfo = null;
|
|
109
|
+
this.config = {
|
|
110
|
+
scope: "openid profile email",
|
|
111
|
+
autoDiscovery: true,
|
|
112
|
+
storagePrefix: "idaas",
|
|
113
|
+
...config
|
|
114
|
+
};
|
|
115
|
+
this.storage = new TokenStorage(this.config.storagePrefix);
|
|
116
|
+
}
|
|
117
|
+
async init() {
|
|
118
|
+
if (this.config.autoDiscovery) {
|
|
119
|
+
await this.discover();
|
|
120
|
+
} else {
|
|
121
|
+
this.endpoints = {
|
|
122
|
+
authorization_endpoint: `${this.config.issuer}/oauth2/authorize`,
|
|
123
|
+
token_endpoint: `${this.config.issuer}/oauth2/token`,
|
|
124
|
+
userinfo_endpoint: `${this.config.issuer}/oauth2/userinfo`,
|
|
125
|
+
end_session_endpoint: `${this.config.issuer}/oauth2/logout`,
|
|
126
|
+
jwks_uri: `${this.config.issuer}/.well-known/jwks.json`,
|
|
127
|
+
revocation_endpoint: `${this.config.issuer}/oauth2/revoke`,
|
|
128
|
+
introspection_endpoint: `${this.config.issuer}/oauth2/introspect`
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return this.getAuthState();
|
|
132
|
+
}
|
|
133
|
+
async discover() {
|
|
134
|
+
const res = await fetch(`${this.config.issuer}/.well-known/openid-configuration`);
|
|
135
|
+
if (!res.ok) throw new Error("Failed to fetch OIDC discovery document");
|
|
136
|
+
const doc = await res.json();
|
|
137
|
+
this.endpoints = {
|
|
138
|
+
authorization_endpoint: doc.authorization_endpoint,
|
|
139
|
+
token_endpoint: doc.token_endpoint,
|
|
140
|
+
userinfo_endpoint: doc.userinfo_endpoint,
|
|
141
|
+
end_session_endpoint: doc.end_session_endpoint,
|
|
142
|
+
jwks_uri: doc.jwks_uri,
|
|
143
|
+
revocation_endpoint: doc.revocation_endpoint,
|
|
144
|
+
introspection_endpoint: doc.introspection_endpoint
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
async login(extraParams) {
|
|
148
|
+
if (!this.endpoints) await this.init();
|
|
149
|
+
const { codeVerifier, codeChallenge } = await createPKCE();
|
|
150
|
+
const state = generateState();
|
|
151
|
+
const nonce = generateNonce();
|
|
152
|
+
this.storage.saveCodeVerifier(codeVerifier);
|
|
153
|
+
this.storage.saveState(state);
|
|
154
|
+
this.storage.saveNonce(nonce);
|
|
155
|
+
const params = new URLSearchParams({
|
|
156
|
+
response_type: "code",
|
|
157
|
+
client_id: this.config.clientId,
|
|
158
|
+
redirect_uri: this.config.redirectUri,
|
|
159
|
+
scope: this.config.scope,
|
|
160
|
+
state,
|
|
161
|
+
nonce,
|
|
162
|
+
code_challenge: codeChallenge,
|
|
163
|
+
code_challenge_method: "S256",
|
|
164
|
+
...extraParams
|
|
165
|
+
});
|
|
166
|
+
window.location.href = `${this.endpoints.authorization_endpoint}?${params.toString()}`;
|
|
167
|
+
}
|
|
168
|
+
async handleCallback(callbackUrl) {
|
|
169
|
+
if (!this.endpoints) await this.init();
|
|
170
|
+
const url = new URL(callbackUrl || window.location.href);
|
|
171
|
+
const code = url.searchParams.get("code");
|
|
172
|
+
const state = url.searchParams.get("state");
|
|
173
|
+
const error = url.searchParams.get("error");
|
|
174
|
+
if (error) {
|
|
175
|
+
const desc = url.searchParams.get("error_description") || error;
|
|
176
|
+
throw new Error(desc);
|
|
177
|
+
}
|
|
178
|
+
if (!code) throw new Error("No authorization code in callback URL");
|
|
179
|
+
const savedState = this.storage.getState();
|
|
180
|
+
if (state !== savedState) {
|
|
181
|
+
throw new Error("State mismatch - possible CSRF attack");
|
|
182
|
+
}
|
|
183
|
+
const codeVerifier = this.storage.getCodeVerifier();
|
|
184
|
+
if (!codeVerifier) throw new Error("No code verifier found");
|
|
185
|
+
const body = new URLSearchParams({
|
|
186
|
+
grant_type: "authorization_code",
|
|
187
|
+
code,
|
|
188
|
+
redirect_uri: this.config.redirectUri,
|
|
189
|
+
client_id: this.config.clientId,
|
|
190
|
+
code_verifier: codeVerifier
|
|
191
|
+
});
|
|
192
|
+
if (this.config.clientSecret) {
|
|
193
|
+
body.set("client_secret", this.config.clientSecret);
|
|
194
|
+
}
|
|
195
|
+
const res = await fetch(this.endpoints.token_endpoint, {
|
|
196
|
+
method: "POST",
|
|
197
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
198
|
+
body: body.toString()
|
|
199
|
+
});
|
|
200
|
+
if (!res.ok) {
|
|
201
|
+
const err = await res.json().catch(() => ({ error: "token_exchange_failed" }));
|
|
202
|
+
throw new Error(err.error_description || err.error || "Token exchange failed");
|
|
203
|
+
}
|
|
204
|
+
const tokens = await res.json();
|
|
205
|
+
this.storage.saveTokens(tokens);
|
|
206
|
+
this.storage.clearAuthState();
|
|
207
|
+
return tokens;
|
|
208
|
+
}
|
|
209
|
+
async refreshToken() {
|
|
210
|
+
if (!this.endpoints) await this.init();
|
|
211
|
+
const refreshToken = this.storage.getRefreshToken();
|
|
212
|
+
if (!refreshToken) return null;
|
|
213
|
+
const body = new URLSearchParams({
|
|
214
|
+
grant_type: "refresh_token",
|
|
215
|
+
refresh_token: refreshToken,
|
|
216
|
+
client_id: this.config.clientId
|
|
217
|
+
});
|
|
218
|
+
if (this.config.clientSecret) {
|
|
219
|
+
body.set("client_secret", this.config.clientSecret);
|
|
220
|
+
}
|
|
221
|
+
const res = await fetch(this.endpoints.token_endpoint, {
|
|
222
|
+
method: "POST",
|
|
223
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
224
|
+
body: body.toString()
|
|
225
|
+
});
|
|
226
|
+
if (!res.ok) {
|
|
227
|
+
this.storage.clear();
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
const tokens = await res.json();
|
|
231
|
+
this.storage.saveTokens(tokens);
|
|
232
|
+
return tokens;
|
|
233
|
+
}
|
|
234
|
+
async getUserInfo(forceRefresh = false) {
|
|
235
|
+
if (!this.endpoints) await this.init();
|
|
236
|
+
if (this.userInfo && !forceRefresh) return this.userInfo;
|
|
237
|
+
const accessToken = await this.getAccessToken();
|
|
238
|
+
if (!accessToken) return null;
|
|
239
|
+
const res = await fetch(this.endpoints.userinfo_endpoint, {
|
|
240
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
241
|
+
});
|
|
242
|
+
if (!res.ok) return null;
|
|
243
|
+
this.userInfo = await res.json();
|
|
244
|
+
return this.userInfo;
|
|
245
|
+
}
|
|
246
|
+
async getAccessToken() {
|
|
247
|
+
let token = this.storage.getAccessToken();
|
|
248
|
+
if (token) return token;
|
|
249
|
+
if (this.storage.isTokenExpired()) {
|
|
250
|
+
const tokens = await this.refreshToken();
|
|
251
|
+
if (tokens) return tokens.access_token;
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
getIdToken() {
|
|
256
|
+
return this.storage.getIdToken();
|
|
257
|
+
}
|
|
258
|
+
getAuthState() {
|
|
259
|
+
const accessToken = this.storage.getAccessToken();
|
|
260
|
+
return {
|
|
261
|
+
isAuthenticated: !!accessToken,
|
|
262
|
+
user: this.userInfo,
|
|
263
|
+
accessToken,
|
|
264
|
+
idToken: this.storage.getIdToken(),
|
|
265
|
+
expiresAt: this.storage.getExpiresAt()
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
isAuthenticated() {
|
|
269
|
+
return !!this.storage.getAccessToken();
|
|
270
|
+
}
|
|
271
|
+
async logout() {
|
|
272
|
+
if (!this.endpoints) await this.init();
|
|
273
|
+
const idToken = this.storage.getIdToken();
|
|
274
|
+
this.storage.clear();
|
|
275
|
+
this.userInfo = null;
|
|
276
|
+
const params = new URLSearchParams();
|
|
277
|
+
if (idToken) params.set("id_token_hint", idToken);
|
|
278
|
+
if (this.config.postLogoutRedirectUri) {
|
|
279
|
+
params.set("post_logout_redirect_uri", this.config.postLogoutRedirectUri);
|
|
280
|
+
}
|
|
281
|
+
window.location.href = `${this.endpoints.end_session_endpoint}?${params.toString()}`;
|
|
282
|
+
}
|
|
283
|
+
async revokeToken() {
|
|
284
|
+
if (!this.endpoints) await this.init();
|
|
285
|
+
const refreshToken = this.storage.getRefreshToken();
|
|
286
|
+
if (refreshToken) {
|
|
287
|
+
await fetch(this.endpoints.revocation_endpoint, {
|
|
288
|
+
method: "POST",
|
|
289
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
290
|
+
body: new URLSearchParams({
|
|
291
|
+
token: refreshToken,
|
|
292
|
+
token_type_hint: "refresh_token"
|
|
293
|
+
}).toString()
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/** Create an authenticated fetch wrapper */
|
|
298
|
+
async fetch(url, init) {
|
|
299
|
+
const token = await this.getAccessToken();
|
|
300
|
+
const headers = new Headers(init?.headers);
|
|
301
|
+
if (token) {
|
|
302
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
303
|
+
}
|
|
304
|
+
return fetch(url, { ...init, headers });
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
export {
|
|
308
|
+
IdaasClient,
|
|
309
|
+
TokenStorage,
|
|
310
|
+
createPKCE,
|
|
311
|
+
generateNonce,
|
|
312
|
+
generateState
|
|
313
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zskj-idaas/sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "IDaaS OIDC Client SDK for browser applications — supports Authorization Code Flow with PKCE, token management, and authenticated fetch.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": {
|
|
11
|
+
"types": "./dist/index.d.mts",
|
|
12
|
+
"default": "./dist/index.mjs"
|
|
13
|
+
},
|
|
14
|
+
"require": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
28
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
29
|
+
"prepublishOnly": "npm run build"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"idaas",
|
|
33
|
+
"oidc",
|
|
34
|
+
"oauth2",
|
|
35
|
+
"pkce",
|
|
36
|
+
"sso",
|
|
37
|
+
"authentication",
|
|
38
|
+
"identity",
|
|
39
|
+
"openid-connect",
|
|
40
|
+
"browser-sdk"
|
|
41
|
+
],
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"author": "IDaaS Team",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": ""
|
|
47
|
+
},
|
|
48
|
+
"homepage": "",
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": ""
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=16.0.0"
|
|
54
|
+
},
|
|
55
|
+
"browserslist": [
|
|
56
|
+
"> 0.5%",
|
|
57
|
+
"not dead",
|
|
58
|
+
"not ie 11"
|
|
59
|
+
],
|
|
60
|
+
"dependencies": {},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"tsup": "^8.2.0",
|
|
63
|
+
"typescript": "^5.6.0"
|
|
64
|
+
}
|
|
65
|
+
}
|