trustflows-client 0.1.0-alpha.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 +115 -0
- package/dist/auth.d.ts +69 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +467 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/types.d.ts +39 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/uma/claims/accessToken.d.ts +8 -0
- package/dist/uma/claims/accessToken.d.ts.map +1 -0
- package/dist/uma/claims/accessToken.js +42 -0
- package/dist/uma/claims/idToken.d.ts +8 -0
- package/dist/uma/claims/idToken.d.ts.map +1 -0
- package/dist/uma/claims/idToken.js +21 -0
- package/dist/uma/claims/registry.d.ts +7 -0
- package/dist/uma/claims/registry.d.ts.map +1 -0
- package/dist/uma/claims/registry.js +82 -0
- package/dist/uma/claims/types.d.ts +59 -0
- package/dist/uma/claims/types.d.ts.map +1 -0
- package/dist/uma/claims/types.js +1 -0
- package/dist/uma/types.d.ts +161 -0
- package/dist/uma/types.d.ts.map +1 -0
- package/dist/uma/types.js +1 -0
- package/dist/uma/utils.d.ts +16 -0
- package/dist/uma/utils.d.ts.map +1 -0
- package/dist/uma/utils.js +194 -0
- package/dist/utils.d.ts +9 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +44 -0
- package/package.json +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SolidLab
|
|
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,115 @@
|
|
|
1
|
+
# trustflows-client
|
|
2
|
+
|
|
3
|
+
Trustflows client helpers for Solid applications. This package provides a lightweight browser-first auth helper plus
|
|
4
|
+
UMA utilities for Trustflows-compatible services.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install trustflows-client
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Auth basics
|
|
13
|
+
|
|
14
|
+
Make sure a dereferenceable Client ID exists for this application. Make sure the resource server can get this JSON-LD
|
|
15
|
+
file, and that client_id is the same as the URL where this file is hosted.
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"@context": "https://www.w3.org/ns/solid/oidc-context.jsonld",
|
|
20
|
+
"client_id": "http://localhost:8080/app/client-id.jsonld",
|
|
21
|
+
"client_name": "App Name",
|
|
22
|
+
"redirect_uris": [ "http://localhost:8080/app/logged-in-screen" ],
|
|
23
|
+
"post_logout_redirect_uris": [ "http://localhost:8080/app/logged-out-screen" ]
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This client ID file can then be used to log in a user, make sure to use the same redirect URI as in the file.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import {
|
|
31
|
+
getDefaultAuth,
|
|
32
|
+
configureDefaultAuth
|
|
33
|
+
} from "trustflows-client";
|
|
34
|
+
|
|
35
|
+
configureDefaultAuth({
|
|
36
|
+
persistTokens: false,
|
|
37
|
+
});
|
|
38
|
+
const auth = getDefaultAuth();
|
|
39
|
+
await auth.login(
|
|
40
|
+
"https://idp.example",
|
|
41
|
+
"http://localhost:8080/app/client-id.jsonld",
|
|
42
|
+
"http://localhost:8080/app/logged-in-screen"
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`configureDefaultAuth()` and the `Auth` constructor accept these options:
|
|
48
|
+
|
|
49
|
+
- `fetch`: custom `fetch` implementation (useful for tests or custom networking).
|
|
50
|
+
- `storage`: storage provider (defaults to `sessionStorage` in the browser).
|
|
51
|
+
- `claimResolvers`: add or override UMA claim resolvers.
|
|
52
|
+
- `persistTokens`: whether to persist OIDC tokens in storage. Defaults to `true`.
|
|
53
|
+
|
|
54
|
+
Notes:
|
|
55
|
+
- `configureDefaultAuth()` must be called **before** `getDefaultAuth()`; after the default instance is created,
|
|
56
|
+
calling `configureDefaultAuth()` will throw.
|
|
57
|
+
- Token persistence stores an `oidc_tokens` JSON blob in `storage` and hydrates it on startup. Set
|
|
58
|
+
`persistTokens: false` to opt out.
|
|
59
|
+
|
|
60
|
+
After redirecting back to your application, you can handle the incoming redirect and create an authenticated fetch
|
|
61
|
+
function.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import {
|
|
65
|
+
getDefaultAuth,
|
|
66
|
+
configureDefaultAuth
|
|
67
|
+
} from "trustflows-client";
|
|
68
|
+
|
|
69
|
+
configureDefaultAuth({
|
|
70
|
+
persistTokens: false,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const auth = getDefaultAuth();
|
|
74
|
+
|
|
75
|
+
await auth.handleIncomingRedirect();
|
|
76
|
+
|
|
77
|
+
const authFetch = auth.createAuthFetch();
|
|
78
|
+
|
|
79
|
+
const loggedIn = await auth.isLoggedIn();
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Custom UMA claim resolvers
|
|
83
|
+
|
|
84
|
+
You can add custom UMA claim resolvers in your application without modifying this package.
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import {
|
|
88
|
+
Auth,
|
|
89
|
+
type ClaimResolverDefinition,
|
|
90
|
+
} from "trustflows-client";
|
|
91
|
+
|
|
92
|
+
const myResolver: ClaimResolverDefinition = {
|
|
93
|
+
id: "custom-claim",
|
|
94
|
+
match: {
|
|
95
|
+
claim_type: "my-custom-claim",
|
|
96
|
+
issuer: "https://idp.example",
|
|
97
|
+
},
|
|
98
|
+
priority: 10,
|
|
99
|
+
resolve: async (requiredClaim, auth) => {
|
|
100
|
+
// Custom logic to resolve the claim
|
|
101
|
+
return {
|
|
102
|
+
claim_token: "custom-token",
|
|
103
|
+
claim_token_format: "custom-format",
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
auth.addClaimResolver(myResolver);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The `id` field must be unique among all registered claim resolvers and is used for logging and debugging purposes.
|
|
112
|
+
`match` can use any of: `claim_token_format`, `claim_type`, `issuer`, `name`, `friendly_name`. Each field can be a
|
|
113
|
+
string, RegExp, or a predicate function. The `priority` field is used to determine the order in which resolvers are
|
|
114
|
+
tried (higher priority first). The `resolve` function is called when the resolver matches a claim request. It receives
|
|
115
|
+
the required claim and the auth entry that can be used to create the claim.
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ClaimResolver, ClaimResolverDefinition, ClaimResolverRegistry } from './uma/claims/types';
|
|
2
|
+
export interface AuthOptions {
|
|
3
|
+
fetch?: typeof fetch;
|
|
4
|
+
storage?: Storage;
|
|
5
|
+
claimResolvers?: ClaimResolverRegistry;
|
|
6
|
+
persistTokens?: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface OidcConfiguration {
|
|
9
|
+
authorization_endpoint?: string;
|
|
10
|
+
token_endpoint?: string;
|
|
11
|
+
end_session_endpoint?: string;
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
interface UmaTokenCacheEntry {
|
|
15
|
+
token_type: string;
|
|
16
|
+
access_token: string;
|
|
17
|
+
expires_at?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare class Auth {
|
|
20
|
+
oidcAccessToken?: string;
|
|
21
|
+
oidcToken?: string;
|
|
22
|
+
oidcRefreshToken?: string;
|
|
23
|
+
oidcTokenExpiry?: number;
|
|
24
|
+
webId?: string;
|
|
25
|
+
umaPermissionTokens: Map<string, UmaTokenCacheEntry>;
|
|
26
|
+
private readonly fetchFn;
|
|
27
|
+
private readonly storage?;
|
|
28
|
+
private readonly claimResolvers;
|
|
29
|
+
private readonly persistTokens;
|
|
30
|
+
private oidcIssuer?;
|
|
31
|
+
constructor(options?: AuthOptions);
|
|
32
|
+
addClaimResolver(format: string | ClaimResolverDefinition, resolver?: ClaimResolver): void;
|
|
33
|
+
get accessToken(): string | undefined;
|
|
34
|
+
set accessToken(value: string | undefined);
|
|
35
|
+
login(issuer: string, clientId: string, redirectUri: string, scope?: string): Promise<void>;
|
|
36
|
+
logout(postLogoutRedirectUri: string): Promise<void>;
|
|
37
|
+
handleIncomingRedirect(): Promise<boolean>;
|
|
38
|
+
isLoggedIn(): Promise<boolean>;
|
|
39
|
+
refreshTokens(tokenEndpoint: string, clientId: string): Promise<void>;
|
|
40
|
+
ensureValidToken(): Promise<void>;
|
|
41
|
+
getOidcConfig(issuer: string): Promise<OidcConfiguration>;
|
|
42
|
+
generateRandomString(bytes?: number): string;
|
|
43
|
+
pkceChallenge(verifier: string): Promise<string>;
|
|
44
|
+
createClaimToken(): Promise<string>;
|
|
45
|
+
createAuthFetch(): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
46
|
+
clearUmaCache(): void;
|
|
47
|
+
clearOidcTokens(): void;
|
|
48
|
+
clearCache(): void;
|
|
49
|
+
extractWebId(idToken: string): string | undefined;
|
|
50
|
+
private buildUmaTokenKey;
|
|
51
|
+
private hydrateOidcTokens;
|
|
52
|
+
private persistOidcTokens;
|
|
53
|
+
private hydrateUmaTokens;
|
|
54
|
+
private persistUmaTokens;
|
|
55
|
+
getStoredUmaToken(resourceUrl: string, method?: string): UmaTokenCacheEntry | undefined;
|
|
56
|
+
storeUmaToken(resourceUrl: string, method: string, token: {
|
|
57
|
+
token_type: string;
|
|
58
|
+
access_token: string;
|
|
59
|
+
expires_in?: number;
|
|
60
|
+
}): void;
|
|
61
|
+
getClaimResolvers(): ClaimResolverRegistry;
|
|
62
|
+
getFetch(): typeof fetch;
|
|
63
|
+
private getStorage;
|
|
64
|
+
private getCrypto;
|
|
65
|
+
}
|
|
66
|
+
export declare function configureDefaultAuth(options: AuthOptions): void;
|
|
67
|
+
export declare function getDefaultAuth(): Auth;
|
|
68
|
+
export {};
|
|
69
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import { createDefaultClaimResolvers } from './uma/claims/registry';
|
|
2
|
+
import { fetchWithUma, parseUmaAuthenticateHeader, } from './uma/utils';
|
|
3
|
+
export class Auth {
|
|
4
|
+
oidcAccessToken;
|
|
5
|
+
oidcToken;
|
|
6
|
+
oidcRefreshToken;
|
|
7
|
+
oidcTokenExpiry;
|
|
8
|
+
webId;
|
|
9
|
+
umaPermissionTokens = new Map();
|
|
10
|
+
fetchFn;
|
|
11
|
+
storage;
|
|
12
|
+
claimResolvers;
|
|
13
|
+
persistTokens;
|
|
14
|
+
oidcIssuer;
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.fetchFn = options.fetch ?? fetch;
|
|
17
|
+
this.storage =
|
|
18
|
+
options.storage ??
|
|
19
|
+
(typeof sessionStorage === 'undefined' ? undefined : sessionStorage);
|
|
20
|
+
this.persistTokens = options.persistTokens ?? true;
|
|
21
|
+
this.claimResolvers = [
|
|
22
|
+
...createDefaultClaimResolvers(),
|
|
23
|
+
...options.claimResolvers ?? [],
|
|
24
|
+
];
|
|
25
|
+
this.hydrateOidcTokens();
|
|
26
|
+
this.hydrateUmaTokens();
|
|
27
|
+
}
|
|
28
|
+
addClaimResolver(format, resolver) {
|
|
29
|
+
if (typeof format === 'string') {
|
|
30
|
+
if (!resolver) {
|
|
31
|
+
throw new Error('Claim resolver function is required.');
|
|
32
|
+
}
|
|
33
|
+
this.claimResolvers.push({
|
|
34
|
+
id: `custom:${format}`,
|
|
35
|
+
match: { claim_token_format: format },
|
|
36
|
+
resolve: resolver,
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.claimResolvers.push(format);
|
|
41
|
+
}
|
|
42
|
+
get accessToken() {
|
|
43
|
+
return this.oidcAccessToken;
|
|
44
|
+
}
|
|
45
|
+
set accessToken(value) {
|
|
46
|
+
this.oidcAccessToken = value;
|
|
47
|
+
}
|
|
48
|
+
async login(issuer, clientId, redirectUri, scope = 'openid webid offline_access') {
|
|
49
|
+
const config = await this.getOidcConfig(issuer);
|
|
50
|
+
if (!config.authorization_endpoint) {
|
|
51
|
+
throw new Error('Missing authorization_endpoint in OIDC configuration');
|
|
52
|
+
}
|
|
53
|
+
const state = this.generateRandomString();
|
|
54
|
+
const codeVerifier = this.generateRandomString();
|
|
55
|
+
const codeChallenge = await this.pkceChallenge(codeVerifier);
|
|
56
|
+
const storage = this.getStorage();
|
|
57
|
+
storage.setItem('oidc_state', state);
|
|
58
|
+
storage.setItem('oidc_code_verifier', codeVerifier);
|
|
59
|
+
storage.setItem('oidc_issuer', issuer);
|
|
60
|
+
storage.setItem('oidc_client_id', clientId);
|
|
61
|
+
storage.setItem('oidc_redirect_uri', redirectUri);
|
|
62
|
+
this.oidcIssuer = issuer;
|
|
63
|
+
const params = new URLSearchParams({
|
|
64
|
+
response_type: 'code',
|
|
65
|
+
client_id: clientId,
|
|
66
|
+
redirect_uri: redirectUri,
|
|
67
|
+
scope,
|
|
68
|
+
state,
|
|
69
|
+
code_challenge: codeChallenge,
|
|
70
|
+
code_challenge_method: 'S256',
|
|
71
|
+
prompt: 'consent',
|
|
72
|
+
response_mode: 'query',
|
|
73
|
+
});
|
|
74
|
+
window.location.href = `${config.authorization_endpoint}?${params.toString()}`;
|
|
75
|
+
}
|
|
76
|
+
async logout(postLogoutRedirectUri) {
|
|
77
|
+
const resolvedIssuer = this.oidcIssuer ?? this.storage?.getItem('oidc_issuer') ?? undefined;
|
|
78
|
+
const idTokenHint = this.oidcToken;
|
|
79
|
+
this.clearCache();
|
|
80
|
+
if (!resolvedIssuer) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
let config;
|
|
84
|
+
try {
|
|
85
|
+
config = await this.getOidcConfig(resolvedIssuer);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!config.end_session_endpoint) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const params = new URLSearchParams();
|
|
94
|
+
if (idTokenHint) {
|
|
95
|
+
params.set('id_token_hint', idTokenHint);
|
|
96
|
+
}
|
|
97
|
+
params.set('post_logout_redirect_uri', postLogoutRedirectUri);
|
|
98
|
+
const logoutUrl = params.toString() ?
|
|
99
|
+
`${config.end_session_endpoint}?${params.toString()}` :
|
|
100
|
+
config.end_session_endpoint;
|
|
101
|
+
window.location.href = logoutUrl;
|
|
102
|
+
}
|
|
103
|
+
async handleIncomingRedirect() {
|
|
104
|
+
const url = new URL(window.location.href);
|
|
105
|
+
const code = url.searchParams.get('code');
|
|
106
|
+
const state = url.searchParams.get('state');
|
|
107
|
+
if (!code) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
const storage = this.getStorage();
|
|
111
|
+
const storedState = storage.getItem('oidc_state');
|
|
112
|
+
if (!state || state !== storedState) {
|
|
113
|
+
throw new Error('OIDC state mismatch');
|
|
114
|
+
}
|
|
115
|
+
const issuer = storage.getItem('oidc_issuer');
|
|
116
|
+
const clientId = storage.getItem('oidc_client_id');
|
|
117
|
+
const redirectUri = storage.getItem('oidc_redirect_uri');
|
|
118
|
+
const codeVerifier = storage.getItem('oidc_code_verifier');
|
|
119
|
+
if (!issuer || !clientId || !redirectUri || !codeVerifier) {
|
|
120
|
+
throw new Error('Missing stored OIDC parameters');
|
|
121
|
+
}
|
|
122
|
+
this.oidcIssuer = issuer;
|
|
123
|
+
const config = await this.getOidcConfig(issuer);
|
|
124
|
+
if (!config.token_endpoint) {
|
|
125
|
+
throw new Error('Missing token_endpoint in OIDC configuration');
|
|
126
|
+
}
|
|
127
|
+
const bodyParams = new URLSearchParams({
|
|
128
|
+
grant_type: 'authorization_code',
|
|
129
|
+
code,
|
|
130
|
+
redirect_uri: redirectUri,
|
|
131
|
+
client_id: clientId,
|
|
132
|
+
code_verifier: codeVerifier,
|
|
133
|
+
});
|
|
134
|
+
const tokenResp = await this.fetchFn(config.token_endpoint, {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: {
|
|
137
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
138
|
+
},
|
|
139
|
+
body: bodyParams.toString(),
|
|
140
|
+
});
|
|
141
|
+
if (!tokenResp.ok) {
|
|
142
|
+
throw new Error(`Token endpoint error ${tokenResp.status}`);
|
|
143
|
+
}
|
|
144
|
+
const tokenJson = (await tokenResp.json());
|
|
145
|
+
this.oidcAccessToken = tokenJson.access_token;
|
|
146
|
+
this.oidcToken = tokenJson.id_token;
|
|
147
|
+
this.oidcRefreshToken = tokenJson.refresh_token;
|
|
148
|
+
if (tokenJson.expires_in) {
|
|
149
|
+
this.oidcTokenExpiry = Date.now() + tokenJson.expires_in * 1000;
|
|
150
|
+
}
|
|
151
|
+
if (tokenJson.id_token) {
|
|
152
|
+
this.webId = this.extractWebId(tokenJson.id_token);
|
|
153
|
+
}
|
|
154
|
+
this.persistOidcTokens();
|
|
155
|
+
window.history.replaceState({}, document.title, redirectUri);
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
async isLoggedIn() {
|
|
159
|
+
try {
|
|
160
|
+
await this.ensureValidToken();
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
return Boolean(this.oidcAccessToken ?? this.oidcToken);
|
|
166
|
+
}
|
|
167
|
+
async refreshTokens(tokenEndpoint, clientId) {
|
|
168
|
+
if (!this.oidcRefreshToken) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const bodyParams = new URLSearchParams({
|
|
172
|
+
grant_type: 'refresh_token',
|
|
173
|
+
refresh_token: this.oidcRefreshToken,
|
|
174
|
+
client_id: clientId,
|
|
175
|
+
});
|
|
176
|
+
const resp = await this.fetchFn(tokenEndpoint, {
|
|
177
|
+
method: 'POST',
|
|
178
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
179
|
+
body: bodyParams.toString(),
|
|
180
|
+
});
|
|
181
|
+
if (!resp.ok) {
|
|
182
|
+
throw new Error(`Refresh token endpoint error ${resp.status}`);
|
|
183
|
+
}
|
|
184
|
+
const json = (await resp.json());
|
|
185
|
+
if (json.access_token) {
|
|
186
|
+
this.oidcAccessToken = json.access_token;
|
|
187
|
+
}
|
|
188
|
+
if (json.id_token) {
|
|
189
|
+
this.oidcToken = json.id_token;
|
|
190
|
+
this.webId = this.extractWebId(json.id_token);
|
|
191
|
+
}
|
|
192
|
+
if (json.refresh_token) {
|
|
193
|
+
this.oidcRefreshToken = json.refresh_token;
|
|
194
|
+
}
|
|
195
|
+
if (json.expires_in) {
|
|
196
|
+
this.oidcTokenExpiry = Date.now() + json.expires_in * 1000;
|
|
197
|
+
}
|
|
198
|
+
this.persistOidcTokens();
|
|
199
|
+
}
|
|
200
|
+
async ensureValidToken() {
|
|
201
|
+
if (!this.oidcTokenExpiry || !this.oidcRefreshToken) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (Date.now() < this.oidcTokenExpiry - 60_000) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const storage = this.getStorage();
|
|
208
|
+
const issuer = storage.getItem('oidc_issuer');
|
|
209
|
+
const clientId = storage.getItem('oidc_client_id');
|
|
210
|
+
if (!issuer || !clientId) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const config = await this.getOidcConfig(issuer);
|
|
214
|
+
if (!config.token_endpoint) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
await this.refreshTokens(config.token_endpoint, clientId);
|
|
218
|
+
}
|
|
219
|
+
async getOidcConfig(issuer) {
|
|
220
|
+
const url = `${issuer.replace(/\/$/u, '')}/.well-known/openid-configuration`;
|
|
221
|
+
const res = await this.fetchFn(url);
|
|
222
|
+
if (!res.ok) {
|
|
223
|
+
throw new Error('Failed fetching OIDC configuration');
|
|
224
|
+
}
|
|
225
|
+
return (await res.json());
|
|
226
|
+
}
|
|
227
|
+
generateRandomString(bytes = 64) {
|
|
228
|
+
const cryptoObj = this.getCrypto();
|
|
229
|
+
const arr = new Uint8Array(bytes);
|
|
230
|
+
cryptoObj.getRandomValues(arr);
|
|
231
|
+
return [...arr]
|
|
232
|
+
.map((b) => `0${b.toString(16)}`.slice(-2))
|
|
233
|
+
.join('');
|
|
234
|
+
}
|
|
235
|
+
async pkceChallenge(verifier) {
|
|
236
|
+
const data = new TextEncoder().encode(verifier);
|
|
237
|
+
const digest = await this.getCrypto().subtle.digest('SHA-256', data);
|
|
238
|
+
const arr = new Uint8Array(digest);
|
|
239
|
+
const base64 = btoa(String.fromCodePoint(...arr));
|
|
240
|
+
return base64.replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/u, '');
|
|
241
|
+
}
|
|
242
|
+
async createClaimToken() {
|
|
243
|
+
await this.ensureValidToken();
|
|
244
|
+
if (!this.oidcToken) {
|
|
245
|
+
throw new Error('No OIDC ID token available for UMA claims.');
|
|
246
|
+
}
|
|
247
|
+
return this.oidcToken;
|
|
248
|
+
}
|
|
249
|
+
createAuthFetch() {
|
|
250
|
+
return async (input, init = {}) => {
|
|
251
|
+
const noTokenResponse = await this.fetchFn(input, init);
|
|
252
|
+
if (noTokenResponse.ok) {
|
|
253
|
+
return noTokenResponse;
|
|
254
|
+
}
|
|
255
|
+
if (noTokenResponse.status !== 401) {
|
|
256
|
+
return noTokenResponse;
|
|
257
|
+
}
|
|
258
|
+
const wwwAuthenticateHeader = noTokenResponse.headers.get('WWW-Authenticate');
|
|
259
|
+
const isUmaChallenge = wwwAuthenticateHeader
|
|
260
|
+
?.trim()
|
|
261
|
+
.toLowerCase()
|
|
262
|
+
.startsWith('uma');
|
|
263
|
+
if (isUmaChallenge) {
|
|
264
|
+
const challenge = parseUmaAuthenticateHeader(noTokenResponse.headers);
|
|
265
|
+
if (!challenge) {
|
|
266
|
+
return noTokenResponse;
|
|
267
|
+
}
|
|
268
|
+
return fetchWithUma(input, init, {
|
|
269
|
+
auth: this,
|
|
270
|
+
challenge,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
await this.ensureValidToken();
|
|
274
|
+
if (!this.oidcAccessToken) {
|
|
275
|
+
return noTokenResponse;
|
|
276
|
+
}
|
|
277
|
+
const headers = new Headers(init.headers);
|
|
278
|
+
headers.set('Authorization', `Bearer ${this.oidcAccessToken}`);
|
|
279
|
+
return this.fetchFn(input, { ...init, headers });
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
clearUmaCache() {
|
|
283
|
+
try {
|
|
284
|
+
this.umaPermissionTokens.clear();
|
|
285
|
+
this.storage?.removeItem('uma_permission_tokens');
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
/* Ignore */
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
clearOidcTokens() {
|
|
292
|
+
try {
|
|
293
|
+
this.oidcAccessToken = undefined;
|
|
294
|
+
this.oidcToken = undefined;
|
|
295
|
+
this.oidcRefreshToken = undefined;
|
|
296
|
+
this.oidcTokenExpiry = undefined;
|
|
297
|
+
this.webId = undefined;
|
|
298
|
+
this.oidcIssuer = undefined;
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
/* Ignore */
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
const storage = this.storage;
|
|
305
|
+
storage?.removeItem('oidc_state');
|
|
306
|
+
storage?.removeItem('oidc_code_verifier');
|
|
307
|
+
storage?.removeItem('oidc_issuer');
|
|
308
|
+
storage?.removeItem('oidc_client_id');
|
|
309
|
+
storage?.removeItem('oidc_redirect_uri');
|
|
310
|
+
storage?.removeItem('oidc_tokens');
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
/* Ignore */
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
clearCache() {
|
|
317
|
+
this.clearUmaCache();
|
|
318
|
+
this.clearOidcTokens();
|
|
319
|
+
}
|
|
320
|
+
extractWebId(idToken) {
|
|
321
|
+
try {
|
|
322
|
+
const [, payload] = idToken.split('.');
|
|
323
|
+
if (!payload) {
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
const decoded = JSON.parse(atob(payload.replaceAll('-', '+').replaceAll('_', '/')));
|
|
327
|
+
return decoded.webid ?? decoded.sub;
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
buildUmaTokenKey(resourceUrl, method = 'GET') {
|
|
334
|
+
return `${method.toUpperCase()} ${resourceUrl}`;
|
|
335
|
+
}
|
|
336
|
+
hydrateOidcTokens() {
|
|
337
|
+
if (!this.persistTokens || !this.storage) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
341
|
+
const raw = this.storage.getItem('oidc_tokens');
|
|
342
|
+
if (!raw) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const data = JSON.parse(raw);
|
|
346
|
+
this.oidcAccessToken = data.access_token;
|
|
347
|
+
this.oidcToken = data.id_token;
|
|
348
|
+
this.oidcRefreshToken = data.refresh_token;
|
|
349
|
+
this.oidcTokenExpiry = data.expires_at;
|
|
350
|
+
this.webId = data.web_id ?? (data.id_token ? this.extractWebId(data.id_token) : undefined);
|
|
351
|
+
this.oidcIssuer = this.storage.getItem('oidc_issuer') ?? undefined;
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
/* Ignore */
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
persistOidcTokens() {
|
|
358
|
+
if (!this.persistTokens || !this.storage) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
const payload = {
|
|
363
|
+
access_token: this.oidcAccessToken,
|
|
364
|
+
id_token: this.oidcToken,
|
|
365
|
+
refresh_token: this.oidcRefreshToken,
|
|
366
|
+
expires_at: this.oidcTokenExpiry,
|
|
367
|
+
web_id: this.webId,
|
|
368
|
+
};
|
|
369
|
+
this.storage.setItem('oidc_tokens', JSON.stringify(payload));
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
/* Ignore */
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
hydrateUmaTokens() {
|
|
376
|
+
try {
|
|
377
|
+
const raw = this.storage?.getItem('uma_permission_tokens');
|
|
378
|
+
if (!raw) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const parsed = JSON.parse(raw);
|
|
382
|
+
const now = Date.now();
|
|
383
|
+
for (const [key, entry] of Object.entries(parsed)) {
|
|
384
|
+
if (!entry?.access_token) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (entry.expires_at && now > entry.expires_at) {
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
this.umaPermissionTokens.set(key, entry);
|
|
391
|
+
}
|
|
392
|
+
this.persistUmaTokens();
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
/* Ignore */
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
persistUmaTokens() {
|
|
399
|
+
const obj = {};
|
|
400
|
+
for (const [key, entry] of this.umaPermissionTokens.entries()) {
|
|
401
|
+
obj[key] = entry;
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
this.storage?.setItem('uma_permission_tokens', JSON.stringify(obj));
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
/* Ignore */
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
getStoredUmaToken(resourceUrl, method = 'GET') {
|
|
411
|
+
const key = this.buildUmaTokenKey(resourceUrl, method);
|
|
412
|
+
const entry = this.umaPermissionTokens.get(key);
|
|
413
|
+
if (!entry) {
|
|
414
|
+
return undefined;
|
|
415
|
+
}
|
|
416
|
+
if (entry.expires_at && Date.now() > entry.expires_at) {
|
|
417
|
+
this.umaPermissionTokens.delete(key);
|
|
418
|
+
this.persistUmaTokens();
|
|
419
|
+
return undefined;
|
|
420
|
+
}
|
|
421
|
+
return entry;
|
|
422
|
+
}
|
|
423
|
+
storeUmaToken(resourceUrl, method, token) {
|
|
424
|
+
const key = this.buildUmaTokenKey(resourceUrl, method);
|
|
425
|
+
const expires_at = token.expires_in ?
|
|
426
|
+
Date.now() + token.expires_in * 1000 :
|
|
427
|
+
undefined;
|
|
428
|
+
this.umaPermissionTokens.set(key, {
|
|
429
|
+
token_type: token.token_type,
|
|
430
|
+
access_token: token.access_token,
|
|
431
|
+
expires_at,
|
|
432
|
+
});
|
|
433
|
+
this.persistUmaTokens();
|
|
434
|
+
}
|
|
435
|
+
getClaimResolvers() {
|
|
436
|
+
return [...this.claimResolvers];
|
|
437
|
+
}
|
|
438
|
+
getFetch() {
|
|
439
|
+
return this.fetchFn;
|
|
440
|
+
}
|
|
441
|
+
getStorage() {
|
|
442
|
+
if (!this.storage) {
|
|
443
|
+
throw new Error('Session storage is not available in this environment.');
|
|
444
|
+
}
|
|
445
|
+
return this.storage;
|
|
446
|
+
}
|
|
447
|
+
getCrypto() {
|
|
448
|
+
if (!globalThis.crypto) {
|
|
449
|
+
throw new Error('Web Crypto is not available in this environment.');
|
|
450
|
+
}
|
|
451
|
+
return globalThis.crypto;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
let defaultAuth;
|
|
455
|
+
let defaultAuthOptions;
|
|
456
|
+
export function configureDefaultAuth(options) {
|
|
457
|
+
if (defaultAuth) {
|
|
458
|
+
throw new Error('Default Auth has already been created.');
|
|
459
|
+
}
|
|
460
|
+
defaultAuthOptions = options;
|
|
461
|
+
}
|
|
462
|
+
export function getDefaultAuth() {
|
|
463
|
+
if (!defaultAuth) {
|
|
464
|
+
defaultAuth = new Auth(defaultAuthOptions);
|
|
465
|
+
}
|
|
466
|
+
return defaultAuth;
|
|
467
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './auth';
|
|
2
|
+
export * from './types';
|
|
3
|
+
export * from './utils';
|
|
4
|
+
export * from './uma/claims/accessToken';
|
|
5
|
+
export * from './uma/claims/idToken';
|
|
6
|
+
export * from './uma/claims/registry';
|
|
7
|
+
export * from './uma/claims/types';
|
|
8
|
+
export * from './uma/types';
|
|
9
|
+
export * from './uma/utils';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AAExB,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC"}
|