@storyteq/authnz-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/README.md +220 -0
- package/dist/cache/token-cache.d.ts +15 -0
- package/dist/cache/token-cache.d.ts.map +1 -0
- package/dist/cache/token-cache.js +48 -0
- package/dist/clients/access-api-client.d.ts +18 -0
- package/dist/clients/access-api-client.d.ts.map +1 -0
- package/dist/clients/access-api-client.js +108 -0
- package/dist/clients/keycloak-client.d.ts +15 -0
- package/dist/clients/keycloak-client.d.ts.map +1 -0
- package/dist/clients/keycloak-client.js +60 -0
- package/dist/constants.d.ts +6 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +25 -0
- package/dist/errors.d.ts +20 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +19 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/providers/service-token-provider.d.ts +3 -0
- package/dist/providers/service-token-provider.d.ts.map +1 -0
- package/dist/providers/service-token-provider.js +64 -0
- package/dist/providers/user-token-provider.d.ts +3 -0
- package/dist/providers/user-token-provider.d.ts.map +1 -0
- package/dist/providers/user-token-provider.js +23 -0
- package/dist/types.d.ts +43 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils/hash.d.ts +2 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/utils/hash.js +8 -0
- package/dist/utils/jwt.d.ts +11 -0
- package/dist/utils/jwt.d.ts.map +1 -0
- package/dist/utils/jwt.js +25 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# @storyteq/authnz-sdk
|
|
2
|
+
|
|
3
|
+
Backend SDK for Keycloak authentication and Access API authorization.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @storyteq/authnz-sdk @keycloak/keycloak-admin-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Service Token Provider (Machine-to-Machine)
|
|
14
|
+
|
|
15
|
+
Use this for backend services that need to authenticate as a service account:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import {
|
|
19
|
+
createServiceTokenProvider,
|
|
20
|
+
KEYCLOAK_URLS,
|
|
21
|
+
ACCESS_API_URLS,
|
|
22
|
+
REALMS,
|
|
23
|
+
} from "@storyteq/authnz-sdk"
|
|
24
|
+
|
|
25
|
+
const provider = createServiceTokenProvider({
|
|
26
|
+
keycloakBaseUrl: KEYCLOAK_URLS.production["europe-west1"],
|
|
27
|
+
keycloakRealm: REALMS.production,
|
|
28
|
+
accessApiBaseUrl: ACCESS_API_URLS.production["europe-west1"],
|
|
29
|
+
clientId: process.env.KEYCLOAK_CLIENT_ID,
|
|
30
|
+
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
|
|
31
|
+
// Optional: set defaults for RPT requests
|
|
32
|
+
defaultPermissions: ["workspace:*"],
|
|
33
|
+
defaultAudience: ["platform"],
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// Get a Keycloak service account token
|
|
37
|
+
const kcToken = await provider.keycloak()
|
|
38
|
+
console.log(kcToken.accessToken)
|
|
39
|
+
|
|
40
|
+
// Get an RPT (Resource Permission Token) from Access API
|
|
41
|
+
const rptToken = await provider.rpt({
|
|
42
|
+
permissions: ["workspace:123"],
|
|
43
|
+
audience: ["platform"],
|
|
44
|
+
})
|
|
45
|
+
console.log(rptToken.accessToken)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### User Token Provider (On-Behalf-Of)
|
|
49
|
+
|
|
50
|
+
Use this when you have a user's Keycloak JWT and need to exchange it for an RPT:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { createUserTokenProvider, ACCESS_API_URLS } from "@storyteq/authnz-sdk"
|
|
54
|
+
|
|
55
|
+
const provider = createUserTokenProvider({
|
|
56
|
+
accessApiBaseUrl: ACCESS_API_URLS.production["europe-west1"],
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// Exchange user's Keycloak JWT for RPT
|
|
60
|
+
const rptToken = await provider.rpt({
|
|
61
|
+
keycloakJwt: userKeycloakToken,
|
|
62
|
+
permissions: ["workspace:123"],
|
|
63
|
+
audience: ["platform"],
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// Or exchange a static API token
|
|
67
|
+
const rptFromApiToken = await provider.rpt({
|
|
68
|
+
apiToken: userApiToken,
|
|
69
|
+
permissions: ["workspace:123"],
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## API Reference
|
|
74
|
+
|
|
75
|
+
### Types
|
|
76
|
+
|
|
77
|
+
#### TokenResult
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
interface TokenResult {
|
|
81
|
+
accessToken: string
|
|
82
|
+
expiresAt: Date
|
|
83
|
+
expiresIn: number
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
#### ServiceTokenProviderConfig
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
interface ServiceTokenProviderConfig {
|
|
91
|
+
keycloakBaseUrl: string
|
|
92
|
+
keycloakRealm: string
|
|
93
|
+
accessApiBaseUrl: string
|
|
94
|
+
clientId: string
|
|
95
|
+
clientSecret: string
|
|
96
|
+
defaultPermissions?: string[]
|
|
97
|
+
defaultAudience?: string[]
|
|
98
|
+
logger?: Logger
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
#### UserTokenProviderConfig
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
interface UserTokenProviderConfig {
|
|
106
|
+
accessApiBaseUrl: string
|
|
107
|
+
logger?: Logger
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Constants
|
|
112
|
+
|
|
113
|
+
- `KEYCLOAK_URLS` - Keycloak URLs by environment and region
|
|
114
|
+
- `ACCESS_API_URLS` - Access API URLs by environment and region
|
|
115
|
+
- `REALMS` - Keycloak realm names by environment
|
|
116
|
+
|
|
117
|
+
### Error Handling
|
|
118
|
+
|
|
119
|
+
All errors are thrown as `AuthNZError` with specific error codes:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { AuthNZError, ErrorCode } from "@storyteq/authnz-sdk"
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const token = await provider.rpt()
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error instanceof AuthNZError) {
|
|
128
|
+
switch (error.code) {
|
|
129
|
+
case ErrorCode.KEYCLOAK_AUTH_FAILED:
|
|
130
|
+
// Handle Keycloak authentication failure
|
|
131
|
+
break
|
|
132
|
+
case ErrorCode.ACCESS_API_INVALID_TOKEN:
|
|
133
|
+
// Handle invalid token
|
|
134
|
+
break
|
|
135
|
+
case ErrorCode.ACCESS_API_USER_NOT_FOUND:
|
|
136
|
+
// Handle user not found
|
|
137
|
+
break
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Logging
|
|
144
|
+
|
|
145
|
+
Pass a logger to the provider configuration to enable debug logging:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
const provider = createServiceTokenProvider({
|
|
149
|
+
// ... config
|
|
150
|
+
logger: {
|
|
151
|
+
debug: (msg, ctx) => console.debug(msg, ctx),
|
|
152
|
+
info: (msg, ctx) => console.info(msg, ctx),
|
|
153
|
+
warn: (msg, ctx) => console.warn(msg, ctx),
|
|
154
|
+
error: (msg, ctx) => console.error(msg, ctx),
|
|
155
|
+
},
|
|
156
|
+
})
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Caching Behavior
|
|
160
|
+
|
|
161
|
+
### Service Token Provider
|
|
162
|
+
|
|
163
|
+
- Keycloak tokens are cached and reused until 15 seconds before expiry
|
|
164
|
+
- RPT tokens are cached per unique combination of permissions and audience
|
|
165
|
+
- On 401 errors, the cache is cleared and tokens are refreshed automatically
|
|
166
|
+
|
|
167
|
+
### User Token Provider
|
|
168
|
+
|
|
169
|
+
- User tokens are NOT cached (each call makes a fresh request)
|
|
170
|
+
- This is intentional for security - user context should not be cached
|
|
171
|
+
|
|
172
|
+
## Development
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
# Install dependencies
|
|
176
|
+
pnpm install
|
|
177
|
+
|
|
178
|
+
# Run tests
|
|
179
|
+
pnpm test
|
|
180
|
+
|
|
181
|
+
# Type check
|
|
182
|
+
pnpm type-check
|
|
183
|
+
|
|
184
|
+
# Build
|
|
185
|
+
pnpm build
|
|
186
|
+
|
|
187
|
+
# Lint
|
|
188
|
+
pnpm lint
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## CI/CD
|
|
192
|
+
|
|
193
|
+
### Static Checks
|
|
194
|
+
|
|
195
|
+
On every MR and push to main, the CI runs:
|
|
196
|
+
|
|
197
|
+
- `pnpm lint` - ESLint checks
|
|
198
|
+
- `pnpm type-check` - TypeScript type checking
|
|
199
|
+
- `pnpm build` - Build verification
|
|
200
|
+
- `pnpm test` - Unit tests
|
|
201
|
+
|
|
202
|
+
### Publishing to NPM
|
|
203
|
+
|
|
204
|
+
To publish a new version to NPM:
|
|
205
|
+
|
|
206
|
+
1. Update the version in `package.json`
|
|
207
|
+
2. Commit and merge to `main`
|
|
208
|
+
3. Create a git tag from a commit on main:
|
|
209
|
+
```bash
|
|
210
|
+
git tag authnz-sdk/1.0.0
|
|
211
|
+
```
|
|
212
|
+
4. Push the tag:
|
|
213
|
+
```bash
|
|
214
|
+
git push origin authnz-sdk/1.0.0
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The CI will automatically:
|
|
218
|
+
|
|
219
|
+
- Build the package
|
|
220
|
+
- Publish to NPM
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { TokenResult } from "../types.js";
|
|
2
|
+
export declare class TokenCache {
|
|
3
|
+
private keycloakToken;
|
|
4
|
+
private rptTokens;
|
|
5
|
+
private isTokenValid;
|
|
6
|
+
setKeycloakToken(token: TokenResult): void;
|
|
7
|
+
getKeycloakToken(bufferSeconds: number): TokenResult | null;
|
|
8
|
+
clearKeycloakToken(): void;
|
|
9
|
+
setRptToken(key: string, token: TokenResult): void;
|
|
10
|
+
getRptToken(key: string, bufferSeconds: number): TokenResult | null;
|
|
11
|
+
clearRptToken(key: string): void;
|
|
12
|
+
clearAllRptTokens(): void;
|
|
13
|
+
clearAll(): void;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=token-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-cache.d.ts","sourceRoot":"","sources":["../../src/cache/token-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAE9C,qBAAa,UAAU;IACrB,OAAO,CAAC,aAAa,CAA2B;IAChD,OAAO,CAAC,SAAS,CAAiC;IAElD,OAAO,CAAC,YAAY;IAKpB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI;IAI1C,gBAAgB,CAAC,aAAa,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI;IAU3D,kBAAkB,IAAI,IAAI;IAI1B,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI;IAIlD,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI;IAWnE,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIhC,iBAAiB,IAAI,IAAI;IAIzB,QAAQ,IAAI,IAAI;CAIjB"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export class TokenCache {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.keycloakToken = null;
|
|
4
|
+
this.rptTokens = new Map();
|
|
5
|
+
}
|
|
6
|
+
isTokenValid(token, bufferSeconds) {
|
|
7
|
+
const bufferMs = bufferSeconds * 1000;
|
|
8
|
+
return Date.now() < token.expiresAt.getTime() - bufferMs;
|
|
9
|
+
}
|
|
10
|
+
setKeycloakToken(token) {
|
|
11
|
+
this.keycloakToken = token;
|
|
12
|
+
}
|
|
13
|
+
getKeycloakToken(bufferSeconds) {
|
|
14
|
+
if (!this.keycloakToken) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
if (!this.isTokenValid(this.keycloakToken, bufferSeconds)) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return this.keycloakToken;
|
|
21
|
+
}
|
|
22
|
+
clearKeycloakToken() {
|
|
23
|
+
this.keycloakToken = null;
|
|
24
|
+
}
|
|
25
|
+
setRptToken(key, token) {
|
|
26
|
+
this.rptTokens.set(key, token);
|
|
27
|
+
}
|
|
28
|
+
getRptToken(key, bufferSeconds) {
|
|
29
|
+
const token = this.rptTokens.get(key);
|
|
30
|
+
if (!token) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (!this.isTokenValid(token, bufferSeconds)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return token;
|
|
37
|
+
}
|
|
38
|
+
clearRptToken(key) {
|
|
39
|
+
this.rptTokens.delete(key);
|
|
40
|
+
}
|
|
41
|
+
clearAllRptTokens() {
|
|
42
|
+
this.rptTokens.clear();
|
|
43
|
+
}
|
|
44
|
+
clearAll() {
|
|
45
|
+
this.keycloakToken = null;
|
|
46
|
+
this.rptTokens.clear();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Logger, RptRequestOptions, TokenResult } from "../types.js";
|
|
2
|
+
export interface AccessApiClientConfig {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
logger?: Logger | undefined;
|
|
5
|
+
}
|
|
6
|
+
export interface ExchangeOptions extends RptRequestOptions {
|
|
7
|
+
keycloakJwt?: string | undefined;
|
|
8
|
+
apiToken?: string | undefined;
|
|
9
|
+
}
|
|
10
|
+
export declare class AccessApiClient {
|
|
11
|
+
private readonly config;
|
|
12
|
+
constructor(config: AccessApiClientConfig);
|
|
13
|
+
exchangeForRpt(options: ExchangeOptions): Promise<TokenResult>;
|
|
14
|
+
getRptFromKeycloakToken(keycloakToken: string, options?: RptRequestOptions): Promise<TokenResult>;
|
|
15
|
+
private doRequest;
|
|
16
|
+
private mapStatusToErrorCode;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=access-api-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access-api-client.d.ts","sourceRoot":"","sources":["../../src/clients/access-api-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAGzE,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC5B;AAED,MAAM,WAAW,eAAgB,SAAQ,iBAAiB;IACxD,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAChC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC9B;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;gBAElC,MAAM,EAAE,qBAAqB;IAInC,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,WAAW,CAAC;IAqC9D,uBAAuB,CAC3B,aAAa,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,WAAW,CAAC;YAyBT,SAAS;IAiEvB,OAAO,CAAC,oBAAoB;CAa7B"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { AuthNZError, ErrorCode } from "../errors.js";
|
|
2
|
+
import { getTokenExpiry } from "../utils/jwt.js";
|
|
3
|
+
export class AccessApiClient {
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.config = config;
|
|
6
|
+
}
|
|
7
|
+
async exchangeForRpt(options) {
|
|
8
|
+
if (!options.keycloakJwt && !options.apiToken) {
|
|
9
|
+
throw new AuthNZError(ErrorCode.INVALID_CONFIG, "Either keycloakJwt or apiToken must be provided");
|
|
10
|
+
}
|
|
11
|
+
const body = new URLSearchParams();
|
|
12
|
+
if (options.keycloakJwt) {
|
|
13
|
+
body.append("jwt_keycloak_token", options.keycloakJwt);
|
|
14
|
+
}
|
|
15
|
+
else if (options.apiToken) {
|
|
16
|
+
body.append("api_token", options.apiToken);
|
|
17
|
+
}
|
|
18
|
+
if (options.permissions) {
|
|
19
|
+
for (const perm of options.permissions) {
|
|
20
|
+
body.append("permission", perm);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (options.audience) {
|
|
24
|
+
for (const aud of options.audience) {
|
|
25
|
+
body.append("audience", aud);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return this.doRequest(`${this.config.baseUrl}/exchange`, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: {
|
|
31
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
32
|
+
},
|
|
33
|
+
body: body.toString(),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async getRptFromKeycloakToken(keycloakToken, options) {
|
|
37
|
+
const body = new URLSearchParams();
|
|
38
|
+
if (options === null || options === void 0 ? void 0 : options.permissions) {
|
|
39
|
+
for (const perm of options.permissions) {
|
|
40
|
+
body.append("permission", perm);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (options === null || options === void 0 ? void 0 : options.audience) {
|
|
44
|
+
for (const aud of options.audience) {
|
|
45
|
+
body.append("audience", aud);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return this.doRequest(`${this.config.baseUrl}/token`, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: {
|
|
51
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
52
|
+
Authorization: `Bearer ${keycloakToken}`,
|
|
53
|
+
},
|
|
54
|
+
body: body.toString(),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async doRequest(url, init) {
|
|
58
|
+
var _a, _b, _c, _d, _e;
|
|
59
|
+
(_a = this.config.logger) === null || _a === void 0 ? void 0 : _a.debug("Calling Access API", { url });
|
|
60
|
+
let response;
|
|
61
|
+
try {
|
|
62
|
+
response = await fetch(url, init);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
(_b = this.config.logger) === null || _b === void 0 ? void 0 : _b.error("Access API unreachable", {
|
|
66
|
+
url,
|
|
67
|
+
error: error instanceof Error ? error.message : String(error),
|
|
68
|
+
});
|
|
69
|
+
throw new AuthNZError(ErrorCode.ACCESS_API_UNREACHABLE, "Failed to reach Access API", {
|
|
70
|
+
cause: error instanceof Error ? error : new Error(String(error)),
|
|
71
|
+
context: { url },
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
const errorText = await response.text();
|
|
76
|
+
(_c = this.config.logger) === null || _c === void 0 ? void 0 : _c.error("Access API request failed", {
|
|
77
|
+
url,
|
|
78
|
+
status: response.status,
|
|
79
|
+
error: errorText,
|
|
80
|
+
});
|
|
81
|
+
const errorCode = this.mapStatusToErrorCode(response.status);
|
|
82
|
+
throw new AuthNZError(errorCode, `Access API returned ${response.status}: ${errorText}`, { context: { url, status: response.status } });
|
|
83
|
+
}
|
|
84
|
+
const data = (await response.json());
|
|
85
|
+
const expiresAt = (_d = getTokenExpiry(data.access_token)) !== null && _d !== void 0 ? _d : new Date(Date.now() + data.expires_in * 1000);
|
|
86
|
+
(_e = this.config.logger) === null || _e === void 0 ? void 0 : _e.info("Access API token obtained", {
|
|
87
|
+
expiresIn: data.expires_in,
|
|
88
|
+
});
|
|
89
|
+
return {
|
|
90
|
+
accessToken: data.access_token,
|
|
91
|
+
expiresAt,
|
|
92
|
+
expiresIn: data.expires_in,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
mapStatusToErrorCode(status) {
|
|
96
|
+
switch (status) {
|
|
97
|
+
case 400:
|
|
98
|
+
case 401:
|
|
99
|
+
return ErrorCode.ACCESS_API_INVALID_TOKEN;
|
|
100
|
+
case 404:
|
|
101
|
+
return ErrorCode.ACCESS_API_USER_NOT_FOUND;
|
|
102
|
+
case 422:
|
|
103
|
+
return ErrorCode.ACCESS_API_VALIDATION_ERROR;
|
|
104
|
+
default:
|
|
105
|
+
return ErrorCode.ACCESS_API_UNREACHABLE;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Logger, TokenResult } from "../types.js";
|
|
2
|
+
export interface KeycloakClientConfig {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
realm: string;
|
|
5
|
+
clientId: string;
|
|
6
|
+
clientSecret: string;
|
|
7
|
+
logger?: Logger | undefined;
|
|
8
|
+
}
|
|
9
|
+
export declare class KeycloakClient {
|
|
10
|
+
private readonly kcClient;
|
|
11
|
+
private readonly config;
|
|
12
|
+
constructor(config: KeycloakClientConfig);
|
|
13
|
+
getToken(): Promise<TokenResult>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=keycloak-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keycloak-client.d.ts","sourceRoot":"","sources":["../../src/clients/keycloak-client.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAGtD,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC5B;AAID,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAe;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsB;gBAEjC,MAAM,EAAE,oBAAoB;IAQlC,QAAQ,IAAI,OAAO,CAAC,WAAW,CAAC;CAgEvC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import KcAdminClient from "@keycloak/keycloak-admin-client";
|
|
2
|
+
import { AuthNZError, ErrorCode } from "../errors.js";
|
|
3
|
+
import { getTokenExpiry } from "../utils/jwt.js";
|
|
4
|
+
const DEFAULT_TOKEN_LIFETIME_SECONDS = 300;
|
|
5
|
+
export class KeycloakClient {
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.config = config;
|
|
8
|
+
this.kcClient = new KcAdminClient({
|
|
9
|
+
baseUrl: config.baseUrl,
|
|
10
|
+
realmName: config.realm,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
async getToken() {
|
|
14
|
+
var _a, _b, _c, _d;
|
|
15
|
+
const credentials = {
|
|
16
|
+
grantType: "client_credentials",
|
|
17
|
+
clientId: this.config.clientId,
|
|
18
|
+
clientSecret: this.config.clientSecret,
|
|
19
|
+
};
|
|
20
|
+
try {
|
|
21
|
+
(_a = this.config.logger) === null || _a === void 0 ? void 0 : _a.debug("Authenticating with Keycloak", {
|
|
22
|
+
baseUrl: this.config.baseUrl,
|
|
23
|
+
realm: this.config.realm,
|
|
24
|
+
clientId: this.config.clientId,
|
|
25
|
+
});
|
|
26
|
+
await this.kcClient.auth(credentials);
|
|
27
|
+
const accessToken = await this.kcClient.getAccessToken();
|
|
28
|
+
if (!accessToken) {
|
|
29
|
+
throw new AuthNZError(ErrorCode.KEYCLOAK_AUTH_FAILED, "No access token returned from Keycloak");
|
|
30
|
+
}
|
|
31
|
+
const expiresAt = (_b = getTokenExpiry(accessToken)) !== null && _b !== void 0 ? _b : new Date(Date.now() + DEFAULT_TOKEN_LIFETIME_SECONDS * 1000);
|
|
32
|
+
const expiresIn = Math.floor((expiresAt.getTime() - Date.now()) / 1000);
|
|
33
|
+
(_c = this.config.logger) === null || _c === void 0 ? void 0 : _c.info("Keycloak token obtained", {
|
|
34
|
+
expiresIn,
|
|
35
|
+
expiresAt: expiresAt.toISOString(),
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
accessToken,
|
|
39
|
+
expiresAt,
|
|
40
|
+
expiresIn,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error instanceof AuthNZError) {
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
(_d = this.config.logger) === null || _d === void 0 ? void 0 : _d.error("Keycloak authentication failed", {
|
|
48
|
+
error: error instanceof Error ? error.message : String(error),
|
|
49
|
+
});
|
|
50
|
+
throw new AuthNZError(ErrorCode.KEYCLOAK_AUTH_FAILED, "Failed to authenticate with Keycloak", {
|
|
51
|
+
cause: error instanceof Error ? error : new Error(String(error)),
|
|
52
|
+
context: {
|
|
53
|
+
baseUrl: this.config.baseUrl,
|
|
54
|
+
realm: this.config.realm,
|
|
55
|
+
clientId: this.config.clientId,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Environment, Region } from "./types.js";
|
|
2
|
+
export declare const KEYCLOAK_URLS: Record<Environment, Record<Region, string>>;
|
|
3
|
+
export declare const ACCESS_API_URLS: Record<Environment, Record<Region, string>>;
|
|
4
|
+
export declare const REALMS: Record<Environment, string>;
|
|
5
|
+
export declare const TOKEN_REFRESH_BUFFER_SECONDS = 15;
|
|
6
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAErD,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASrE,CAAA;AAED,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASvE,CAAA;AAED,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAG9C,CAAA;AAED,eAAO,MAAM,4BAA4B,KAAK,CAAA"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const KEYCLOAK_URLS = {
|
|
2
|
+
staging: {
|
|
3
|
+
"europe-west1": "https://staging.auth.europe-west1.storyteq.work",
|
|
4
|
+
"us-east4": "https://staging.auth.us-east4.storyteq.work",
|
|
5
|
+
},
|
|
6
|
+
production: {
|
|
7
|
+
"europe-west1": "https://auth.storyteq.com",
|
|
8
|
+
"us-east4": "https://auth.us-east4.storyteq.com",
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
export const ACCESS_API_URLS = {
|
|
12
|
+
staging: {
|
|
13
|
+
"europe-west1": "https://access.storyteq.work",
|
|
14
|
+
"us-east4": "https://access.us-east4.storyteq.work",
|
|
15
|
+
},
|
|
16
|
+
production: {
|
|
17
|
+
"europe-west1": "https://access.storyteq.com",
|
|
18
|
+
"us-east4": "https://access.us-east4.storyteq.com",
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
export const REALMS = {
|
|
22
|
+
staging: "storyteq.work",
|
|
23
|
+
production: "storyteq.com",
|
|
24
|
+
};
|
|
25
|
+
export const TOKEN_REFRESH_BUFFER_SECONDS = 15;
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare enum ErrorCode {
|
|
2
|
+
KEYCLOAK_AUTH_FAILED = "KEYCLOAK_AUTH_FAILED",
|
|
3
|
+
KEYCLOAK_UNREACHABLE = "KEYCLOAK_UNREACHABLE",
|
|
4
|
+
ACCESS_API_UNREACHABLE = "ACCESS_API_UNREACHABLE",
|
|
5
|
+
ACCESS_API_INVALID_TOKEN = "ACCESS_API_INVALID_TOKEN",
|
|
6
|
+
ACCESS_API_USER_NOT_FOUND = "ACCESS_API_USER_NOT_FOUND",
|
|
7
|
+
ACCESS_API_VALIDATION_ERROR = "ACCESS_API_VALIDATION_ERROR",
|
|
8
|
+
INVALID_CONFIG = "INVALID_CONFIG",
|
|
9
|
+
TOKEN_REFRESH_FAILED = "TOKEN_REFRESH_FAILED"
|
|
10
|
+
}
|
|
11
|
+
export interface AuthNZErrorOptions {
|
|
12
|
+
cause?: Error;
|
|
13
|
+
context?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
export declare class AuthNZError extends Error {
|
|
16
|
+
readonly code: ErrorCode;
|
|
17
|
+
readonly context: Record<string, unknown> | undefined;
|
|
18
|
+
constructor(code: ErrorCode, message: string, options?: AuthNZErrorOptions);
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,oBAAY,SAAS;IACnB,oBAAoB,yBAAyB;IAC7C,oBAAoB,yBAAyB;IAC7C,sBAAsB,2BAA2B;IACjD,wBAAwB,6BAA6B;IACrD,yBAAyB,8BAA8B;IACvD,2BAA2B,gCAAgC;IAC3D,cAAc,mBAAmB;IACjC,oBAAoB,yBAAyB;CAC9C;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,KAAK,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAClC;AAED,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAA;gBAEzC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB;CAM3E"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export var ErrorCode;
|
|
2
|
+
(function (ErrorCode) {
|
|
3
|
+
ErrorCode["KEYCLOAK_AUTH_FAILED"] = "KEYCLOAK_AUTH_FAILED";
|
|
4
|
+
ErrorCode["KEYCLOAK_UNREACHABLE"] = "KEYCLOAK_UNREACHABLE";
|
|
5
|
+
ErrorCode["ACCESS_API_UNREACHABLE"] = "ACCESS_API_UNREACHABLE";
|
|
6
|
+
ErrorCode["ACCESS_API_INVALID_TOKEN"] = "ACCESS_API_INVALID_TOKEN";
|
|
7
|
+
ErrorCode["ACCESS_API_USER_NOT_FOUND"] = "ACCESS_API_USER_NOT_FOUND";
|
|
8
|
+
ErrorCode["ACCESS_API_VALIDATION_ERROR"] = "ACCESS_API_VALIDATION_ERROR";
|
|
9
|
+
ErrorCode["INVALID_CONFIG"] = "INVALID_CONFIG";
|
|
10
|
+
ErrorCode["TOKEN_REFRESH_FAILED"] = "TOKEN_REFRESH_FAILED";
|
|
11
|
+
})(ErrorCode || (ErrorCode = {}));
|
|
12
|
+
export class AuthNZError extends Error {
|
|
13
|
+
constructor(code, message, options) {
|
|
14
|
+
super(message, { cause: options === null || options === void 0 ? void 0 : options.cause });
|
|
15
|
+
this.name = "AuthNZError";
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.context = options === null || options === void 0 ? void 0 : options.context;
|
|
18
|
+
}
|
|
19
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const VERSION = "1.0.0";
|
|
2
|
+
export type { Environment, Logger, Region, RptRequestOptions, ServiceTokenProvider, ServiceTokenProviderConfig, TokenResult, UserTokenExchangeOptions, UserTokenProvider, UserTokenProviderConfig, } from "./types.js";
|
|
3
|
+
export { ACCESS_API_URLS, KEYCLOAK_URLS, REALMS, TOKEN_REFRESH_BUFFER_SECONDS, } from "./constants.js";
|
|
4
|
+
export type { AuthNZErrorOptions } from "./errors.js";
|
|
5
|
+
export { AuthNZError, ErrorCode } from "./errors.js";
|
|
6
|
+
export { createServiceTokenProvider } from "./providers/service-token-provider.js";
|
|
7
|
+
export { createUserTokenProvider } from "./providers/user-token-provider.js";
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,OAAO,UAAU,CAAA;AAG9B,YAAY,EACV,WAAW,EACX,MAAM,EACN,MAAM,EACN,iBAAiB,EACjB,oBAAoB,EACpB,0BAA0B,EAC1B,WAAW,EACX,wBAAwB,EACxB,iBAAiB,EACjB,uBAAuB,GACxB,MAAM,YAAY,CAAA;AAGnB,OAAO,EACL,eAAe,EACf,aAAa,EACb,MAAM,EACN,4BAA4B,GAC7B,MAAM,gBAAgB,CAAA;AAGvB,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AACrD,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAGpD,OAAO,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAA;AAClF,OAAO,EAAE,uBAAuB,EAAE,MAAM,oCAAoC,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @storyteq/authnz-sdk
|
|
2
|
+
// Unified authentication and authorization SDK
|
|
3
|
+
export const VERSION = "1.0.0";
|
|
4
|
+
// Constants
|
|
5
|
+
export { ACCESS_API_URLS, KEYCLOAK_URLS, REALMS, TOKEN_REFRESH_BUFFER_SECONDS, } from "./constants.js";
|
|
6
|
+
export { AuthNZError, ErrorCode } from "./errors.js";
|
|
7
|
+
// Providers
|
|
8
|
+
export { createServiceTokenProvider } from "./providers/service-token-provider.js";
|
|
9
|
+
export { createUserTokenProvider } from "./providers/user-token-provider.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-token-provider.d.ts","sourceRoot":"","sources":["../../src/providers/service-token-provider.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAEV,oBAAoB,EACpB,0BAA0B,EAE3B,MAAM,aAAa,CAAA;AAGpB,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,0BAA0B,GACjC,oBAAoB,CA6EtB"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { TokenCache } from "../cache/token-cache.js";
|
|
2
|
+
import { AccessApiClient } from "../clients/access-api-client.js";
|
|
3
|
+
import { KeycloakClient } from "../clients/keycloak-client.js";
|
|
4
|
+
import { TOKEN_REFRESH_BUFFER_SECONDS } from "../constants.js";
|
|
5
|
+
import { AuthNZError, ErrorCode } from "../errors.js";
|
|
6
|
+
import { generateCacheKey } from "../utils/hash.js";
|
|
7
|
+
export function createServiceTokenProvider(config) {
|
|
8
|
+
const keycloakClient = new KeycloakClient({
|
|
9
|
+
baseUrl: config.keycloakBaseUrl,
|
|
10
|
+
realm: config.keycloakRealm,
|
|
11
|
+
clientId: config.clientId,
|
|
12
|
+
clientSecret: config.clientSecret,
|
|
13
|
+
logger: config.logger,
|
|
14
|
+
});
|
|
15
|
+
const accessApiClient = new AccessApiClient({
|
|
16
|
+
baseUrl: config.accessApiBaseUrl,
|
|
17
|
+
logger: config.logger,
|
|
18
|
+
});
|
|
19
|
+
const cache = new TokenCache();
|
|
20
|
+
async function keycloak() {
|
|
21
|
+
var _a, _b;
|
|
22
|
+
const cached = cache.getKeycloakToken(TOKEN_REFRESH_BUFFER_SECONDS);
|
|
23
|
+
if (cached) {
|
|
24
|
+
(_a = config.logger) === null || _a === void 0 ? void 0 : _a.debug("Using cached Keycloak token");
|
|
25
|
+
return cached;
|
|
26
|
+
}
|
|
27
|
+
(_b = config.logger) === null || _b === void 0 ? void 0 : _b.debug("Fetching new Keycloak token");
|
|
28
|
+
const token = await keycloakClient.getToken();
|
|
29
|
+
cache.setKeycloakToken(token);
|
|
30
|
+
return token;
|
|
31
|
+
}
|
|
32
|
+
async function rpt(options) {
|
|
33
|
+
var _a, _b, _c, _d;
|
|
34
|
+
const permissions = (_a = options === null || options === void 0 ? void 0 : options.permissions) !== null && _a !== void 0 ? _a : config.defaultPermissions;
|
|
35
|
+
const audience = (_b = options === null || options === void 0 ? void 0 : options.audience) !== null && _b !== void 0 ? _b : config.defaultAudience;
|
|
36
|
+
const cacheKey = generateCacheKey(permissions, audience);
|
|
37
|
+
const cachedRpt = cache.getRptToken(cacheKey, TOKEN_REFRESH_BUFFER_SECONDS);
|
|
38
|
+
if (cachedRpt) {
|
|
39
|
+
(_c = config.logger) === null || _c === void 0 ? void 0 : _c.debug("Using cached RPT token", { cacheKey });
|
|
40
|
+
return cachedRpt;
|
|
41
|
+
}
|
|
42
|
+
const kcToken = await keycloak();
|
|
43
|
+
try {
|
|
44
|
+
const rptToken = await accessApiClient.getRptFromKeycloakToken(kcToken.accessToken, { permissions, audience });
|
|
45
|
+
cache.setRptToken(cacheKey, rptToken);
|
|
46
|
+
return rptToken;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
// Retry on 401 with fresh keycloak token
|
|
50
|
+
if (error instanceof AuthNZError &&
|
|
51
|
+
error.code === ErrorCode.ACCESS_API_INVALID_TOKEN) {
|
|
52
|
+
(_d = config.logger) === null || _d === void 0 ? void 0 : _d.info("RPT request failed with 401, retrying with fresh Keycloak token");
|
|
53
|
+
cache.clearKeycloakToken();
|
|
54
|
+
const freshKcToken = await keycloakClient.getToken();
|
|
55
|
+
cache.setKeycloakToken(freshKcToken);
|
|
56
|
+
const rptToken = await accessApiClient.getRptFromKeycloakToken(freshKcToken.accessToken, { permissions, audience });
|
|
57
|
+
cache.setRptToken(cacheKey, rptToken);
|
|
58
|
+
return rptToken;
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { keycloak, rpt };
|
|
64
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user-token-provider.d.ts","sourceRoot":"","sources":["../../src/providers/user-token-provider.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAGV,iBAAiB,EACjB,uBAAuB,EACxB,MAAM,aAAa,CAAA;AAEpB,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,uBAAuB,GAC9B,iBAAiB,CAuBnB"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AccessApiClient } from "../clients/access-api-client.js";
|
|
2
|
+
export function createUserTokenProvider(config) {
|
|
3
|
+
const accessApiClient = new AccessApiClient({
|
|
4
|
+
baseUrl: config.accessApiBaseUrl,
|
|
5
|
+
logger: config.logger,
|
|
6
|
+
});
|
|
7
|
+
async function rpt(options) {
|
|
8
|
+
var _a;
|
|
9
|
+
(_a = config.logger) === null || _a === void 0 ? void 0 : _a.debug("Exchanging user token for RPT", {
|
|
10
|
+
hasKeycloakJwt: !!options.keycloakJwt,
|
|
11
|
+
hasApiToken: !!options.apiToken,
|
|
12
|
+
permissions: options.permissions,
|
|
13
|
+
audience: options.audience,
|
|
14
|
+
});
|
|
15
|
+
return accessApiClient.exchangeForRpt({
|
|
16
|
+
keycloakJwt: options.keycloakJwt,
|
|
17
|
+
apiToken: options.apiToken,
|
|
18
|
+
permissions: options.permissions,
|
|
19
|
+
audience: options.audience,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return { rpt };
|
|
23
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type Environment = "staging" | "production";
|
|
2
|
+
export type Region = "europe-west1" | "us-east4";
|
|
3
|
+
export interface TokenResult {
|
|
4
|
+
accessToken: string;
|
|
5
|
+
expiresAt: Date;
|
|
6
|
+
expiresIn: number;
|
|
7
|
+
}
|
|
8
|
+
export interface Logger {
|
|
9
|
+
debug(message: string, context?: Record<string, unknown>): void;
|
|
10
|
+
info(message: string, context?: Record<string, unknown>): void;
|
|
11
|
+
warn(message: string, context?: Record<string, unknown>): void;
|
|
12
|
+
error(message: string, context?: Record<string, unknown>): void;
|
|
13
|
+
}
|
|
14
|
+
export interface ServiceTokenProviderConfig {
|
|
15
|
+
keycloakBaseUrl: string;
|
|
16
|
+
keycloakRealm: string;
|
|
17
|
+
accessApiBaseUrl: string;
|
|
18
|
+
clientId: string;
|
|
19
|
+
clientSecret: string;
|
|
20
|
+
defaultPermissions?: string[] | undefined;
|
|
21
|
+
defaultAudience?: string[] | undefined;
|
|
22
|
+
logger?: Logger | undefined;
|
|
23
|
+
}
|
|
24
|
+
export interface UserTokenProviderConfig {
|
|
25
|
+
accessApiBaseUrl: string;
|
|
26
|
+
logger?: Logger | undefined;
|
|
27
|
+
}
|
|
28
|
+
export interface RptRequestOptions {
|
|
29
|
+
permissions?: string[] | undefined;
|
|
30
|
+
audience?: string[] | undefined;
|
|
31
|
+
}
|
|
32
|
+
export interface UserTokenExchangeOptions extends RptRequestOptions {
|
|
33
|
+
keycloakJwt?: string | undefined;
|
|
34
|
+
apiToken?: string | undefined;
|
|
35
|
+
}
|
|
36
|
+
export interface ServiceTokenProvider {
|
|
37
|
+
keycloak(): Promise<TokenResult>;
|
|
38
|
+
rpt(options?: RptRequestOptions): Promise<TokenResult>;
|
|
39
|
+
}
|
|
40
|
+
export interface UserTokenProvider {
|
|
41
|
+
rpt(options: UserTokenExchangeOptions): Promise<TokenResult>;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,YAAY,CAAA;AAClD,MAAM,MAAM,MAAM,GAAG,cAAc,GAAG,UAAU,CAAA;AAEhD,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,IAAI,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IAC/D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IAC9D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IAC9D,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;CAChE;AAED,MAAM,WAAW,0BAA0B;IACzC,eAAe,EAAE,MAAM,CAAA;IACvB,aAAa,EAAE,MAAM,CAAA;IACrB,gBAAgB,EAAE,MAAM,CAAA;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,kBAAkB,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;IACzC,eAAe,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;IACtC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC5B;AAED,MAAM,WAAW,uBAAuB;IACtC,gBAAgB,EAAE,MAAM,CAAA;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;IAClC,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;CAChC;AAED,MAAM,WAAW,wBAAyB,SAAQ,iBAAiB;IACjE,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAChC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC9B;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,IAAI,OAAO,CAAC,WAAW,CAAC,CAAA;IAChC,GAAG,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;CACvD;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;CAC7D"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../../src/utils/hash.ts"],"names":[],"mappings":"AAAA,wBAAgB,gBAAgB,CAC9B,WAAW,CAAC,EAAE,MAAM,EAAE,EACtB,QAAQ,CAAC,EAAE,MAAM,EAAE,GAClB,MAAM,CAQR"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function generateCacheKey(permissions, audience) {
|
|
2
|
+
const sortedPermissions = [...(permissions !== null && permissions !== void 0 ? permissions : [])].sort();
|
|
3
|
+
const sortedAudience = [...(audience !== null && audience !== void 0 ? audience : [])].sort();
|
|
4
|
+
return JSON.stringify({
|
|
5
|
+
permissions: sortedPermissions,
|
|
6
|
+
audience: sortedAudience,
|
|
7
|
+
});
|
|
8
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface TokenPayload {
|
|
2
|
+
sub?: string;
|
|
3
|
+
exp?: number;
|
|
4
|
+
iat?: number;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
export declare function decodeToken(token: string): TokenPayload | null;
|
|
8
|
+
export declare function getTokenExpiry(token: string): Date | null;
|
|
9
|
+
export declare function isTokenExpired(token: string, bufferSeconds: number): boolean;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=jwt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../src/utils/jwt.ts"],"names":[],"mappings":"AAEA,UAAU,YAAY;IACpB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAO9D;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAMzD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAO5E"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import jwt from "jsonwebtoken";
|
|
2
|
+
export function decodeToken(token) {
|
|
3
|
+
try {
|
|
4
|
+
const decoded = jwt.decode(token, { json: true });
|
|
5
|
+
return decoded;
|
|
6
|
+
}
|
|
7
|
+
catch (_a) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function getTokenExpiry(token) {
|
|
12
|
+
const decoded = decodeToken(token);
|
|
13
|
+
if (!(decoded === null || decoded === void 0 ? void 0 : decoded.exp)) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return new Date(decoded.exp * 1000);
|
|
17
|
+
}
|
|
18
|
+
export function isTokenExpired(token, bufferSeconds) {
|
|
19
|
+
const expiry = getTokenExpiry(token);
|
|
20
|
+
if (!expiry) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
const bufferMs = bufferSeconds * 1000;
|
|
24
|
+
return Date.now() >= expiry.getTime() - bufferMs;
|
|
25
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@storyteq/authnz-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Backend SDK for Keycloak authentication and Access API authorization",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": "^18.0.0 || ^22.0.0"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"@keycloak/keycloak-admin-client": ">=26.0.0"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"jsonwebtoken": "^9.0.2",
|
|
25
|
+
"zod": "^3.24.2"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@eslint/js": "^9.21.0",
|
|
29
|
+
"@keycloak/keycloak-admin-client": "^26.0.7",
|
|
30
|
+
"@tsconfig/node-lts": "^22.0.1",
|
|
31
|
+
"@tsconfig/strictest": "^2.0.5",
|
|
32
|
+
"@types/jsonwebtoken": "^9.0.9",
|
|
33
|
+
"@types/node": "^22.15.21",
|
|
34
|
+
"eslint": "^9.21.0",
|
|
35
|
+
"prettier": "^3.5.3",
|
|
36
|
+
"typescript": "^5.8.3",
|
|
37
|
+
"typescript-eslint": "^8.26.0",
|
|
38
|
+
"vitest": "^3.1.4",
|
|
39
|
+
"@storyteq/prettier-config": "1.0.0"
|
|
40
|
+
},
|
|
41
|
+
"prettier": "@storyteq/prettier-config",
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc -p ./tsconfig.dist.json",
|
|
44
|
+
"dev": "tsc --watch",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:watch": "vitest watch",
|
|
47
|
+
"type-check": "tsc --noEmit",
|
|
48
|
+
"lint": "eslint . && prettier --check .",
|
|
49
|
+
"lint:fix": "eslint . --fix",
|
|
50
|
+
"format": "prettier --write ."
|
|
51
|
+
}
|
|
52
|
+
}
|