@atcute/oauth-browser-client 2.0.3 → 3.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 +18 -12
- package/dist/agents/exchange.d.ts +2 -1
- package/dist/agents/exchange.d.ts.map +1 -1
- package/dist/agents/exchange.js +3 -4
- package/dist/agents/exchange.js.map +1 -1
- package/dist/agents/server-agent.d.ts +5 -5
- package/dist/agents/server-agent.d.ts.map +1 -1
- package/dist/agents/server-agent.js +5 -9
- package/dist/agents/server-agent.js.map +1 -1
- package/dist/agents/sessions.d.ts.map +1 -1
- package/dist/agents/sessions.js +16 -1
- package/dist/agents/sessions.js.map +1 -1
- package/dist/dpop.d.ts +2 -4
- package/dist/dpop.d.ts.map +1 -1
- package/dist/dpop.js +6 -79
- package/dist/dpop.js.map +1 -1
- package/dist/environment.d.ts +3 -3
- package/dist/environment.d.ts.map +1 -1
- package/dist/environment.js.map +1 -1
- package/dist/index.d.ts +3 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -11
- package/dist/index.js.map +1 -1
- package/dist/resolvers.d.ts +92 -4
- package/dist/resolvers.d.ts.map +1 -1
- package/dist/resolvers.js +5 -5
- package/dist/resolvers.js.map +1 -1
- package/dist/store/db.d.ts +49 -6
- package/dist/store/db.d.ts.map +1 -1
- package/dist/types/client-assertion.d.ts +2 -3
- package/dist/types/client-assertion.d.ts.map +1 -1
- package/dist/types/server.d.ts +2 -56
- package/dist/types/server.d.ts.map +1 -1
- package/dist/types/token.d.ts +8 -20
- package/dist/types/token.d.ts.map +1 -1
- package/dist/utils/dpop-key.d.ts +10 -0
- package/dist/utils/dpop-key.d.ts.map +1 -0
- package/dist/utils/dpop-key.js +13 -0
- package/dist/utils/dpop-key.js.map +1 -0
- package/dist/utils/runtime.d.ts +0 -6
- package/dist/utils/runtime.d.ts.map +1 -1
- package/dist/utils/runtime.js +0 -16
- package/dist/utils/runtime.js.map +1 -1
- package/lib/agents/exchange.ts +10 -11
- package/lib/agents/server-agent.ts +14 -17
- package/lib/agents/sessions.ts +23 -2
- package/lib/dpop.ts +7 -108
- package/lib/environment.ts +3 -3
- package/lib/index.ts +12 -12
- package/lib/resolvers.ts +13 -11
- package/lib/store/db.ts +6 -6
- package/lib/types/client-assertion.ts +2 -4
- package/lib/types/server.ts +2 -57
- package/lib/types/token.ts +10 -24
- package/lib/utils/dpop-key.ts +24 -0
- package/lib/utils/runtime.ts +0 -22
- package/package.json +12 -8
- package/dist/types/client.d.ts +0 -38
- package/dist/types/client.d.ts.map +0 -1
- package/dist/types/client.js +0 -2
- package/dist/types/client.js.map +0 -1
- package/dist/types/dpop.d.ts +0 -10
- package/dist/types/dpop.d.ts.map +0 -1
- package/dist/types/dpop.js +0 -2
- package/dist/types/dpop.js.map +0 -1
- package/dist/types/identity.d.ts +0 -6
- package/dist/types/identity.d.ts.map +0 -1
- package/dist/types/identity.js +0 -2
- package/dist/types/identity.js.map +0 -1
- package/dist/types/par.d.ts +0 -5
- package/dist/types/par.d.ts.map +0 -1
- package/dist/types/par.js +0 -2
- package/dist/types/par.js.map +0 -1
- package/dist/utils/identity-resolver.d.ts +0 -7
- package/dist/utils/identity-resolver.d.ts.map +0 -1
- package/dist/utils/identity-resolver.js +0 -8
- package/dist/utils/identity-resolver.js.map +0 -1
- package/lib/types/client.ts +0 -82
- package/lib/types/dpop.ts +0 -9
- package/lib/types/identity.ts +0 -12
- package/lib/types/par.ts +0 -4
- package/lib/utils/identity-resolver.ts +0 -12
package/lib/dpop.ts
CHANGED
|
@@ -1,82 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { encodeUtf8 } from '@atcute/uint8array';
|
|
3
|
-
|
|
4
|
-
import { nanoid } from 'nanoid';
|
|
1
|
+
import { createDpopProofSigner, sha256Base64Url, type DpopPrivateJwk } from '@atcute/oauth-crypto';
|
|
5
2
|
|
|
6
3
|
import { database } from './environment.js';
|
|
7
|
-
import type { DPoPKey } from './types/dpop.js';
|
|
8
4
|
import { extractContentType } from './utils/response.js';
|
|
9
|
-
import { stringToSha256 } from './utils/runtime.js';
|
|
10
|
-
|
|
11
|
-
const ES256_ALG = { name: 'ECDSA', namedCurve: 'P-256' } as const;
|
|
12
|
-
|
|
13
|
-
export const createES256Key = async (): Promise<DPoPKey> => {
|
|
14
|
-
const pair = await crypto.subtle.generateKey(ES256_ALG, true, ['sign', 'verify']);
|
|
15
|
-
|
|
16
|
-
const key = await crypto.subtle.exportKey('pkcs8', pair.privateKey);
|
|
17
|
-
const { ext: _ext, key_ops: _key_opts, ...jwk } = await crypto.subtle.exportKey('jwk', pair.publicKey);
|
|
18
|
-
|
|
19
|
-
const canonicalJwk = JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y });
|
|
20
|
-
const jkt = await stringToSha256(canonicalJwk);
|
|
21
|
-
|
|
22
|
-
return {
|
|
23
|
-
typ: 'ES256',
|
|
24
|
-
key: toBase64Url(new Uint8Array(key)),
|
|
25
|
-
jwt: toBase64Url(encodeUtf8(JSON.stringify({ typ: 'dpop+jwt', alg: 'ES256', jwk: jwk }))),
|
|
26
|
-
jkt: jkt,
|
|
27
|
-
};
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export const createDPoPSignage = (dpopKey: DPoPKey) => {
|
|
31
|
-
const headerString = dpopKey.jwt;
|
|
32
|
-
const keyPromise = crypto.subtle.importKey(
|
|
33
|
-
'pkcs8',
|
|
34
|
-
fromBase64Url(dpopKey.key) as Uint8Array<ArrayBuffer>,
|
|
35
|
-
ES256_ALG,
|
|
36
|
-
true,
|
|
37
|
-
['sign'],
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
const constructPayload = (htm: string, htu: string, nonce: string | undefined, ath: string | undefined) => {
|
|
41
|
-
const payload = {
|
|
42
|
-
ath: ath,
|
|
43
|
-
htm: htm,
|
|
44
|
-
htu: htu,
|
|
45
|
-
iat: Math.floor(Date.now() / 1_000),
|
|
46
|
-
jti: nanoid(24),
|
|
47
|
-
nonce: nonce,
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
return toBase64Url(encodeUtf8(JSON.stringify(payload)));
|
|
51
|
-
};
|
|
52
5
|
|
|
53
|
-
|
|
54
|
-
const payloadString = constructPayload(method, htu, nonce, ath);
|
|
55
|
-
|
|
56
|
-
const signed = await crypto.subtle.sign(
|
|
57
|
-
{ name: 'ECDSA', hash: { name: 'SHA-256' } },
|
|
58
|
-
await keyPromise,
|
|
59
|
-
encodeUtf8(headerString + '.' + payloadString) as Uint8Array<ArrayBuffer>,
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
const signatureString = toBase64Url(new Uint8Array(signed));
|
|
63
|
-
|
|
64
|
-
return headerString + '.' + payloadString + '.' + signatureString;
|
|
65
|
-
};
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export const createDPoPFetch = (dpopKey: DPoPKey, isAuthServer?: boolean): typeof fetch => {
|
|
6
|
+
export const createDPoPFetch = (dpopKey: DpopPrivateJwk, isAuthServer?: boolean): typeof fetch => {
|
|
69
7
|
const nonces = database.dpopNonces;
|
|
70
8
|
const pending = database.inflightDpop;
|
|
71
9
|
|
|
72
|
-
const sign =
|
|
10
|
+
const sign = createDpopProofSigner(dpopKey);
|
|
73
11
|
|
|
74
12
|
return async (input, init) => {
|
|
75
13
|
const request = new Request(input, init);
|
|
76
14
|
|
|
77
15
|
const authorizationHeader = request.headers.get('authorization');
|
|
78
16
|
const ath = authorizationHeader?.startsWith('DPoP ')
|
|
79
|
-
? await
|
|
17
|
+
? await sha256Base64Url(authorizationHeader.slice(5))
|
|
80
18
|
: undefined;
|
|
81
19
|
|
|
82
20
|
const { method, url } = request;
|
|
@@ -84,44 +22,24 @@ export const createDPoPFetch = (dpopKey: DPoPKey, isAuthServer?: boolean): typeo
|
|
|
84
22
|
|
|
85
23
|
const htu = origin + pathname;
|
|
86
24
|
|
|
87
|
-
// See if we have a pending promise for this origin, we'll await before
|
|
88
|
-
// proceeding with this request, next comment describes what the promise
|
|
89
|
-
// is meant to be.
|
|
90
25
|
let deferred = pending.get(origin);
|
|
91
26
|
if (deferred) {
|
|
92
27
|
await deferred.promise;
|
|
93
28
|
deferred = undefined;
|
|
94
29
|
}
|
|
95
30
|
|
|
96
|
-
// Get our persisted nonce value for this origin
|
|
97
31
|
let initNonce: string | undefined;
|
|
98
32
|
let expiredOrMissing = false;
|
|
99
33
|
try {
|
|
100
34
|
const [nonce, lapsed] = nonces.getWithLapsed(origin);
|
|
101
35
|
|
|
102
36
|
initNonce = nonce;
|
|
103
|
-
|
|
104
|
-
// The problem with DPoP nonces is that we don't have insight as to when
|
|
105
|
-
// they'll expire, either we have a nonce value or we don't.
|
|
106
|
-
//
|
|
107
|
-
// Which is very unfortunate, if the client makes multiple requests at the
|
|
108
|
-
// same time, there's a chance that all of them will fail due to the nonce
|
|
109
|
-
// value having expired.
|
|
110
|
-
//
|
|
111
|
-
// To make this less painful, if it's been over 3 minutes since we last
|
|
112
|
-
// had a nonce value, or we never had one to begin with, we'll let this
|
|
113
|
-
// request through and defer everyone else until we get a possibly fresh
|
|
114
|
-
// nonce value.
|
|
115
|
-
//
|
|
116
|
-
// 3 minutes being the DPoP nonce expiration time set by the reference PDS
|
|
117
|
-
// implementation.
|
|
118
37
|
expiredOrMissing = lapsed > 3 * 60 * 1_000;
|
|
119
38
|
} catch {
|
|
120
|
-
//
|
|
39
|
+
// ignore read errors
|
|
121
40
|
}
|
|
122
41
|
|
|
123
42
|
if (expiredOrMissing) {
|
|
124
|
-
// Defer everyone else until this request finishes.
|
|
125
43
|
pending.set(origin, (deferred = Promise.withResolvers()));
|
|
126
44
|
}
|
|
127
45
|
|
|
@@ -134,44 +52,30 @@ export const createDPoPFetch = (dpopKey: DPoPKey, isAuthServer?: boolean): typeo
|
|
|
134
52
|
|
|
135
53
|
nextNonce = initResponse.headers.get('dpop-nonce');
|
|
136
54
|
if (nextNonce === null || nextNonce === initNonce) {
|
|
137
|
-
// No nonce was returned or it is the same as the one we sent. No need to
|
|
138
|
-
// update the nonce store, or retry the request.
|
|
139
|
-
|
|
140
55
|
return initResponse;
|
|
141
56
|
}
|
|
142
57
|
|
|
143
|
-
// Store the fresh nonce for future requests
|
|
144
58
|
try {
|
|
145
59
|
nonces.set(origin, nextNonce);
|
|
146
60
|
} catch {
|
|
147
|
-
//
|
|
61
|
+
// ignore write errors
|
|
148
62
|
}
|
|
149
63
|
|
|
150
64
|
const shouldRetry = await isUseDpopNonceError(initResponse, isAuthServer);
|
|
151
65
|
if (!shouldRetry) {
|
|
152
|
-
// Not a "use_dpop_nonce" error, so there is no need to retry
|
|
153
|
-
|
|
154
66
|
return initResponse;
|
|
155
67
|
}
|
|
156
68
|
|
|
157
69
|
if (input === request || init?.body instanceof ReadableStream) {
|
|
158
|
-
// If the input stream was already consumed, we cannot retry the request. A
|
|
159
|
-
// solution would be to clone() the request but that would bufferize the
|
|
160
|
-
// entire stream in memory which can lead to memory starvation. Instead, we
|
|
161
|
-
// will return the original response and let the calling code handle retries.
|
|
162
|
-
|
|
163
70
|
return initResponse;
|
|
164
71
|
}
|
|
165
72
|
} finally {
|
|
166
|
-
// Now everyone can have their turn.
|
|
167
73
|
if (deferred) {
|
|
168
74
|
pending.delete(origin);
|
|
169
75
|
deferred.resolve();
|
|
170
76
|
}
|
|
171
77
|
}
|
|
172
78
|
|
|
173
|
-
// We got here because we were asked to retry the request (due to missing
|
|
174
|
-
// nonce value in the first request), let's do just that.
|
|
175
79
|
{
|
|
176
80
|
const nextProof = await sign(method, htu, nextNonce, ath);
|
|
177
81
|
const nextRequest = new Request(input, init);
|
|
@@ -179,13 +83,12 @@ export const createDPoPFetch = (dpopKey: DPoPKey, isAuthServer?: boolean): typeo
|
|
|
179
83
|
|
|
180
84
|
const retryResponse = await fetch(nextRequest);
|
|
181
85
|
|
|
182
|
-
// Check if the server returned another new nonce in the retry response
|
|
183
86
|
const retryNonce = retryResponse.headers.get('dpop-nonce');
|
|
184
87
|
if (retryNonce !== null && retryNonce !== nextNonce) {
|
|
185
88
|
try {
|
|
186
89
|
nonces.set(origin, retryNonce);
|
|
187
90
|
} catch {
|
|
188
|
-
//
|
|
91
|
+
// ignore write errors
|
|
189
92
|
}
|
|
190
93
|
}
|
|
191
94
|
|
|
@@ -195,8 +98,6 @@ export const createDPoPFetch = (dpopKey: DPoPKey, isAuthServer?: boolean): typeo
|
|
|
195
98
|
};
|
|
196
99
|
|
|
197
100
|
const isUseDpopNonceError = async (response: Response, isAuthServer?: boolean): Promise<boolean> => {
|
|
198
|
-
// https://datatracker.ietf.org/doc/html/rfc6750#section-3
|
|
199
|
-
// https://datatracker.ietf.org/doc/html/rfc9449#name-resource-server-provided-no
|
|
200
101
|
if (isAuthServer === undefined || isAuthServer === false) {
|
|
201
102
|
if (response.status === 401) {
|
|
202
103
|
const wwwAuth = response.headers.get('www-authenticate');
|
|
@@ -206,14 +107,12 @@ const isUseDpopNonceError = async (response: Response, isAuthServer?: boolean):
|
|
|
206
107
|
}
|
|
207
108
|
}
|
|
208
109
|
|
|
209
|
-
// https://datatracker.ietf.org/doc/html/rfc9449#name-authorization-server-provid
|
|
210
110
|
if (isAuthServer === undefined || isAuthServer === true) {
|
|
211
111
|
if (response.status === 400 && extractContentType(response.headers) === 'application/json') {
|
|
212
112
|
try {
|
|
213
113
|
const json = await response.clone().json();
|
|
214
114
|
return typeof json === 'object' && json?.['error'] === 'use_dpop_nonce';
|
|
215
115
|
} catch {
|
|
216
|
-
// Response too big (to be "use_dpop_nonce" error) or invalid JSON
|
|
217
116
|
return false;
|
|
218
117
|
}
|
|
219
118
|
}
|
package/lib/environment.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ActorResolver } from '@atcute/identity-resolver';
|
|
2
2
|
|
|
3
3
|
import { createOAuthDatabase, type OAuthDatabase } from './store/db.js';
|
|
4
4
|
import type { ClientAssertionFetcher } from './types/client-assertion.js';
|
|
@@ -10,7 +10,7 @@ export let fetchClientAssertion: ClientAssertionFetcher | undefined;
|
|
|
10
10
|
|
|
11
11
|
export let database: OAuthDatabase;
|
|
12
12
|
|
|
13
|
-
export let identityResolver:
|
|
13
|
+
export let identityResolver: ActorResolver;
|
|
14
14
|
|
|
15
15
|
export interface ConfigureOAuthOptions {
|
|
16
16
|
/**
|
|
@@ -22,7 +22,7 @@ export interface ConfigureOAuthOptions {
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
/** resolves actor identifiers into identity metadata */
|
|
25
|
-
identityResolver:
|
|
25
|
+
identityResolver: ActorResolver;
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* optional function to fetch DPoP-bound client assertions from your backend.
|
package/lib/index.ts
CHANGED
|
@@ -3,17 +3,17 @@ export { configureOAuth, type ConfigureOAuthOptions } from './environment.js';
|
|
|
3
3
|
export * from './errors.js';
|
|
4
4
|
|
|
5
5
|
export * from './agents/exchange.js';
|
|
6
|
-
export
|
|
7
|
-
|
|
6
|
+
export {
|
|
7
|
+
getSession,
|
|
8
|
+
deleteStoredSession,
|
|
9
|
+
listStoredSessions,
|
|
10
|
+
type SessionGetOptions,
|
|
11
|
+
} from './agents/sessions.js';
|
|
8
12
|
export * from './agents/user-agent.js';
|
|
9
13
|
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export
|
|
16
|
-
export * from './types/store.js';
|
|
17
|
-
export * from './types/token.js';
|
|
18
|
-
|
|
19
|
-
export * from './utils/identity-resolver.js';
|
|
14
|
+
export type {
|
|
15
|
+
ClientAssertionCredentials,
|
|
16
|
+
ClientAssertionFetcher,
|
|
17
|
+
FetchClientAssertionParams,
|
|
18
|
+
} from './types/client-assertion.js';
|
|
19
|
+
export type { TokenInfo, ExchangeInfo, Session } from './types/token.js';
|
package/lib/resolvers.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
+
import type { ResolvedActor } from '@atcute/identity-resolver';
|
|
1
2
|
import type { ActorIdentifier } from '@atcute/lexicons';
|
|
3
|
+
import type { OAuthAuthorizationServerMetadata, OAuthProtectedResourceMetadata } from '@atcute/oauth-types';
|
|
2
4
|
|
|
3
5
|
import { identityResolver } from './environment.js';
|
|
4
6
|
import { ResolverError } from './errors.js';
|
|
5
|
-
import type { ResolvedIdentity } from './types/identity.js';
|
|
6
|
-
import type { AuthorizationServerMetadata, ProtectedResourceMetadata } from './types/server.js';
|
|
7
7
|
import { extractContentType } from './utils/response.js';
|
|
8
8
|
import { isValidUrl } from './utils/strings.js';
|
|
9
9
|
|
|
10
10
|
export const resolveFromIdentifier = async (
|
|
11
11
|
ident: ActorIdentifier,
|
|
12
|
-
): Promise<{ identity:
|
|
12
|
+
): Promise<{ identity: ResolvedActor; metadata: OAuthAuthorizationServerMetadata }> => {
|
|
13
13
|
const identity = await identityResolver.resolve(ident);
|
|
14
14
|
|
|
15
15
|
return {
|
|
@@ -20,14 +20,14 @@ export const resolveFromIdentifier = async (
|
|
|
20
20
|
|
|
21
21
|
export const resolveFromService = async (
|
|
22
22
|
host: string,
|
|
23
|
-
): Promise<{ metadata:
|
|
23
|
+
): Promise<{ metadata: OAuthAuthorizationServerMetadata }> => {
|
|
24
24
|
try {
|
|
25
25
|
const metadata = await getMetadataFromResourceServer(host);
|
|
26
26
|
return { metadata };
|
|
27
27
|
} catch (err) {
|
|
28
28
|
if (err instanceof ResolverError) {
|
|
29
29
|
try {
|
|
30
|
-
const metadata = await
|
|
30
|
+
const metadata = await getOAuthAuthorizationServerMetadata(host);
|
|
31
31
|
return { metadata };
|
|
32
32
|
} catch {}
|
|
33
33
|
}
|
|
@@ -36,7 +36,7 @@ export const resolveFromService = async (
|
|
|
36
36
|
}
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
const
|
|
39
|
+
const getOAuthProtectedResourceMetadata = async (host: string): Promise<OAuthProtectedResourceMetadata> => {
|
|
40
40
|
const url = new URL(`/.well-known/oauth-protected-resource`, host);
|
|
41
41
|
const response = await fetch(url.href, {
|
|
42
42
|
redirect: 'manual',
|
|
@@ -49,7 +49,7 @@ const getProtectedResourceMetadata = async (host: string): Promise<ProtectedReso
|
|
|
49
49
|
throw new ResolverError(`unexpected response`);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
const metadata = (await response.json()) as
|
|
52
|
+
const metadata = (await response.json()) as OAuthProtectedResourceMetadata;
|
|
53
53
|
if (metadata.resource !== url.origin) {
|
|
54
54
|
throw new ResolverError(`unexpected issuer`);
|
|
55
55
|
}
|
|
@@ -57,7 +57,9 @@ const getProtectedResourceMetadata = async (host: string): Promise<ProtectedReso
|
|
|
57
57
|
return metadata;
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
-
const
|
|
60
|
+
const getOAuthAuthorizationServerMetadata = async (
|
|
61
|
+
host: string,
|
|
62
|
+
): Promise<OAuthAuthorizationServerMetadata> => {
|
|
61
63
|
const url = new URL(`/.well-known/oauth-authorization-server`, host);
|
|
62
64
|
const response = await fetch(url.href, {
|
|
63
65
|
redirect: 'manual',
|
|
@@ -70,7 +72,7 @@ const getAuthorizationServerMetadata = async (host: string): Promise<Authorizati
|
|
|
70
72
|
throw new ResolverError(`unexpected response`);
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
const metadata = (await response.json()) as
|
|
75
|
+
const metadata = (await response.json()) as OAuthAuthorizationServerMetadata;
|
|
74
76
|
if (metadata.issuer !== url.origin) {
|
|
75
77
|
throw new ResolverError(`unexpected issuer`);
|
|
76
78
|
}
|
|
@@ -93,7 +95,7 @@ const getAuthorizationServerMetadata = async (host: string): Promise<Authorizati
|
|
|
93
95
|
};
|
|
94
96
|
|
|
95
97
|
const getMetadataFromResourceServer = async (input: string) => {
|
|
96
|
-
const rs_metadata = await
|
|
98
|
+
const rs_metadata = await getOAuthProtectedResourceMetadata(input);
|
|
97
99
|
|
|
98
100
|
if (rs_metadata.authorization_servers?.length !== 1) {
|
|
99
101
|
throw new ResolverError(`expected exactly one authorization server in the listing`);
|
|
@@ -101,7 +103,7 @@ const getMetadataFromResourceServer = async (input: string) => {
|
|
|
101
103
|
|
|
102
104
|
const issuer = rs_metadata.authorization_servers[0];
|
|
103
105
|
|
|
104
|
-
const as_metadata = await
|
|
106
|
+
const as_metadata = await getOAuthAuthorizationServerMetadata(issuer);
|
|
105
107
|
|
|
106
108
|
if (as_metadata.protected_resources) {
|
|
107
109
|
if (!as_metadata.protected_resources.includes(rs_metadata.resource)) {
|
package/lib/store/db.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Did } from '@atcute/lexicons';
|
|
2
|
+
import type { DpopPrivateJwk } from '@atcute/oauth-crypto';
|
|
3
|
+
import type { OAuthAuthorizationServerMetadata } from '@atcute/oauth-types';
|
|
2
4
|
|
|
3
|
-
import type { DPoPKey } from '../types/dpop.js';
|
|
4
|
-
import type { AuthorizationServerMetadata } from '../types/server.js';
|
|
5
5
|
import type { SimpleStore } from '../types/store.js';
|
|
6
|
-
import type {
|
|
6
|
+
import type { RawSession } from '../types/token.js';
|
|
7
7
|
import { locks } from '../utils/runtime.js';
|
|
8
8
|
|
|
9
9
|
export interface OAuthDatabaseOptions {
|
|
@@ -19,7 +19,7 @@ interface SchemaItem<T> {
|
|
|
19
19
|
interface Schema {
|
|
20
20
|
sessions: {
|
|
21
21
|
key: Did;
|
|
22
|
-
value:
|
|
22
|
+
value: RawSession;
|
|
23
23
|
indexes: {
|
|
24
24
|
expiresAt: number;
|
|
25
25
|
};
|
|
@@ -27,8 +27,8 @@ interface Schema {
|
|
|
27
27
|
states: {
|
|
28
28
|
key: string;
|
|
29
29
|
value: {
|
|
30
|
-
dpopKey:
|
|
31
|
-
metadata:
|
|
30
|
+
dpopKey: DpopPrivateJwk;
|
|
31
|
+
metadata: OAuthAuthorizationServerMetadata;
|
|
32
32
|
verifier?: string;
|
|
33
33
|
state?: unknown;
|
|
34
34
|
};
|
|
@@ -6,18 +6,16 @@ export interface ClientAssertionCredentials {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export interface FetchClientAssertionParams {
|
|
9
|
-
/** JWK thumbprint of the DPoP key to bind the assertion to */
|
|
10
|
-
jkt: string;
|
|
11
9
|
/** authorization server issuer (audience for the assertion) */
|
|
12
10
|
aud: string;
|
|
13
|
-
|
|
14
11
|
/**
|
|
15
12
|
* create a DPoP proof to prove you possess the key for the claimed jkt.
|
|
16
13
|
*
|
|
17
14
|
* @param htu origin and pathname to your backend
|
|
15
|
+
* @param nonce optional DPoP nonce from the server
|
|
18
16
|
* @returns DPoP proof that can be included in the assertion
|
|
19
17
|
*/
|
|
20
|
-
createDpopProof: (htu: string) => Promise<string>;
|
|
18
|
+
createDpopProof: (htu: string, nonce?: string) => Promise<string>;
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
export type ClientAssertionFetcher = (
|
package/lib/types/server.ts
CHANGED
|
@@ -1,62 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
resource: string;
|
|
3
|
-
jwks_uri?: string;
|
|
4
|
-
authorization_servers?: string[];
|
|
5
|
-
scopes_supported?: string[];
|
|
6
|
-
bearer_methods_supported?: ('header' | 'body' | 'query')[];
|
|
7
|
-
resource_signing_alg_values_supported?: string[];
|
|
8
|
-
resource_documentation?: string;
|
|
9
|
-
resource_policy_uri?: string;
|
|
10
|
-
resource_tos_uri?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface AuthorizationServerMetadata {
|
|
14
|
-
issuer: string;
|
|
15
|
-
authorization_endpoint: string;
|
|
16
|
-
token_endpoint: string;
|
|
17
|
-
jwks_uri?: string;
|
|
18
|
-
scopes_supported?: string[];
|
|
19
|
-
claims_supported?: string[];
|
|
20
|
-
claims_locales_supported?: string[];
|
|
21
|
-
claims_parameter_supported?: boolean;
|
|
22
|
-
request_parameter_supported?: boolean;
|
|
23
|
-
request_uri_parameter_supported?: boolean;
|
|
24
|
-
require_request_uri_registration?: boolean;
|
|
25
|
-
subject_types_supported?: string[];
|
|
26
|
-
response_types_supported?: string[];
|
|
27
|
-
response_modes_supported?: string[];
|
|
28
|
-
grant_types_supported?: string[];
|
|
29
|
-
code_challenge_methods_supported?: string[];
|
|
30
|
-
ui_locales_supported?: string[];
|
|
31
|
-
id_token_signing_alg_values_supported?: string[];
|
|
32
|
-
display_values_supported?: string[];
|
|
33
|
-
request_object_signing_alg_values_supported?: string[];
|
|
34
|
-
authorization_response_iss_parameter_supported?: boolean;
|
|
35
|
-
authorization_details_types_supported?: string[];
|
|
36
|
-
request_object_encryption_alg_values_supported?: string[];
|
|
37
|
-
request_object_encryption_enc_values_supported?: string[];
|
|
38
|
-
token_endpoint_auth_methods_supported?: string[];
|
|
39
|
-
token_endpoint_auth_signing_alg_values_supported?: string[];
|
|
40
|
-
revocation_endpoint?: string;
|
|
41
|
-
revocation_endpoint_auth_methods_supported?: string[];
|
|
42
|
-
revocation_endpoint_auth_signing_alg_values_supported?: string[];
|
|
43
|
-
introspection_endpoint?: string;
|
|
44
|
-
introspection_endpoint_auth_methods_supported?: string[];
|
|
45
|
-
introspection_endpoint_auth_signing_alg_values_supported?: string[];
|
|
46
|
-
pushed_authorization_request_endpoint?: string;
|
|
47
|
-
pushed_authorization_request_endpoint_auth_methods_supported?: string[];
|
|
48
|
-
pushed_authorization_request_endpoint_auth_signing_alg_values_supported?: string[];
|
|
49
|
-
require_pushed_authorization_requests?: boolean;
|
|
50
|
-
userinfo_endpoint?: string;
|
|
51
|
-
end_session_endpoint?: string;
|
|
52
|
-
registration_endpoint?: string;
|
|
53
|
-
dpop_signing_alg_values_supported?: string[];
|
|
54
|
-
protected_resources?: string[];
|
|
55
|
-
client_id_metadata_document_supported?: boolean;
|
|
56
|
-
}
|
|
1
|
+
import type { OAuthAuthorizationServerMetadata } from '@atcute/oauth-types';
|
|
57
2
|
|
|
58
3
|
export interface PersistedAuthorizationServerMetadata extends Pick<
|
|
59
|
-
|
|
4
|
+
OAuthAuthorizationServerMetadata,
|
|
60
5
|
| 'issuer'
|
|
61
6
|
| 'authorization_endpoint'
|
|
62
7
|
| 'introspection_endpoint'
|
package/lib/types/token.ts
CHANGED
|
@@ -1,29 +1,9 @@
|
|
|
1
1
|
import type { Did } from '@atcute/lexicons';
|
|
2
|
+
import type { DpopPrivateJwk } from '@atcute/oauth-crypto';
|
|
2
3
|
|
|
3
|
-
import type {
|
|
4
|
-
import type { PersistedAuthorizationServerMetadata } from './server.js';
|
|
4
|
+
import type { LegacyDpopKey } from '../utils/dpop-key.js';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
access_token: string;
|
|
8
|
-
// Can be DPoP or Bearer, normalize casing.
|
|
9
|
-
token_type: string;
|
|
10
|
-
issuer?: string;
|
|
11
|
-
sub?: string;
|
|
12
|
-
scope?: string;
|
|
13
|
-
id_token?: `${string}.${string}.${string}`;
|
|
14
|
-
refresh_token?: string;
|
|
15
|
-
expires_in?: number;
|
|
16
|
-
authorization_details?:
|
|
17
|
-
| {
|
|
18
|
-
type: string;
|
|
19
|
-
locations?: string[];
|
|
20
|
-
actions?: string[];
|
|
21
|
-
datatypes?: string[];
|
|
22
|
-
identifier?: string;
|
|
23
|
-
privileges?: string[];
|
|
24
|
-
}[]
|
|
25
|
-
| undefined;
|
|
26
|
-
}
|
|
6
|
+
import type { PersistedAuthorizationServerMetadata } from './server.js';
|
|
27
7
|
|
|
28
8
|
export interface TokenInfo {
|
|
29
9
|
scope: string;
|
|
@@ -39,8 +19,14 @@ export interface ExchangeInfo {
|
|
|
39
19
|
server: PersistedAuthorizationServerMetadata;
|
|
40
20
|
}
|
|
41
21
|
|
|
22
|
+
export interface RawSession {
|
|
23
|
+
dpopKey: DpopPrivateJwk | LegacyDpopKey;
|
|
24
|
+
info: ExchangeInfo;
|
|
25
|
+
token: TokenInfo;
|
|
26
|
+
}
|
|
27
|
+
|
|
42
28
|
export interface Session {
|
|
43
|
-
dpopKey:
|
|
29
|
+
dpopKey: DpopPrivateJwk;
|
|
44
30
|
info: ExchangeInfo;
|
|
45
31
|
token: TokenInfo;
|
|
46
32
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { fromBase64Url } from '@atcute/multibase';
|
|
2
|
+
import type { DpopPrivateJwk } from '@atcute/oauth-crypto';
|
|
3
|
+
|
|
4
|
+
export interface LegacyDpopKey {
|
|
5
|
+
typ: 'ES256';
|
|
6
|
+
key: string;
|
|
7
|
+
jwt: string;
|
|
8
|
+
jkt?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const ES256_ALG = { name: 'ECDSA', namedCurve: 'P-256' } as const;
|
|
12
|
+
|
|
13
|
+
export const isLegacyDpopKey = (key: DpopPrivateJwk | LegacyDpopKey): key is LegacyDpopKey => {
|
|
14
|
+
return typeof (key as LegacyDpopKey).key === 'string' && typeof (key as LegacyDpopKey).jwt === 'string';
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const migrateLegacyDpopKey = async (key: LegacyDpopKey): Promise<DpopPrivateJwk> => {
|
|
18
|
+
const pkcs8 = fromBase64Url(key.key);
|
|
19
|
+
const cryptoKey = await crypto.subtle.importKey('pkcs8', pkcs8, ES256_ALG, true, ['sign']);
|
|
20
|
+
const jwk = (await crypto.subtle.exportKey('jwk', cryptoKey)) as DpopPrivateJwk;
|
|
21
|
+
jwk.alg = 'ES256';
|
|
22
|
+
|
|
23
|
+
return jwk;
|
|
24
|
+
};
|
package/lib/utils/runtime.ts
CHANGED
|
@@ -1,23 +1 @@
|
|
|
1
|
-
import { nanoid } from 'nanoid';
|
|
2
|
-
|
|
3
|
-
import { toBase64Url } from '@atcute/multibase';
|
|
4
|
-
import { encodeUtf8, toSha256 } from '@atcute/uint8array';
|
|
5
|
-
|
|
6
1
|
export const locks: LockManager | undefined = typeof navigator !== 'undefined' ? navigator.locks : undefined;
|
|
7
|
-
|
|
8
|
-
export const stringToSha256 = async (input: string): Promise<string> => {
|
|
9
|
-
const bytes = encodeUtf8(input);
|
|
10
|
-
const digest = await toSha256(bytes);
|
|
11
|
-
|
|
12
|
-
return toBase64Url(digest);
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export const generatePKCE = async (): Promise<{ verifier: string; challenge: string; method: string }> => {
|
|
16
|
-
const verifier = nanoid(64);
|
|
17
|
-
|
|
18
|
-
return {
|
|
19
|
-
verifier: verifier,
|
|
20
|
-
challenge: await stringToSha256(verifier),
|
|
21
|
-
method: 'S256',
|
|
22
|
-
};
|
|
23
|
-
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"type": "module",
|
|
3
2
|
"name": "@atcute/oauth-browser-client",
|
|
4
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
5
4
|
"description": "minimal OAuth browser client implementation for AT Protocol",
|
|
6
5
|
"license": "0BSD",
|
|
7
6
|
"repository": {
|
|
@@ -14,17 +13,22 @@
|
|
|
14
13
|
"!lib/**/*.bench.ts",
|
|
15
14
|
"!lib/**/*.test.ts"
|
|
16
15
|
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"sideEffects": false,
|
|
17
18
|
"exports": {
|
|
18
19
|
".": "./dist/index.js"
|
|
19
20
|
},
|
|
20
|
-
"
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
21
24
|
"dependencies": {
|
|
22
25
|
"nanoid": "^5.1.6",
|
|
23
|
-
"@atcute/client": "^4.
|
|
24
|
-
"@atcute/identity-resolver": "^1.2.
|
|
25
|
-
"@atcute/lexicons": "^1.2.
|
|
26
|
-
"@atcute/
|
|
27
|
-
"@atcute/multibase": "^1.1.
|
|
26
|
+
"@atcute/client": "^4.2.1",
|
|
27
|
+
"@atcute/identity-resolver": "^1.2.2",
|
|
28
|
+
"@atcute/lexicons": "^1.2.7",
|
|
29
|
+
"@atcute/oauth-crypto": "^0.1.0",
|
|
30
|
+
"@atcute/multibase": "^1.1.7",
|
|
31
|
+
"@atcute/oauth-types": "^0.1.0"
|
|
28
32
|
},
|
|
29
33
|
"scripts": {
|
|
30
34
|
"build": "tsgo --project tsconfig.build.json",
|
package/dist/types/client.d.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
export interface ClientMetadata {
|
|
2
|
-
redirect_uris: string[];
|
|
3
|
-
response_types: ('code' | 'token' | 'none' | 'code id_token token' | 'code id_token' | 'code token' | 'id_token token' | 'id_token')[];
|
|
4
|
-
grant_types: ('authorization_code' | 'implicit' | 'refresh_token' | 'password' | 'client_credentials' | 'urn:ietf:params:oauth:grant-type:jwt-bearer' | 'urn:ietf:params:oauth:grant-type:saml2-bearer')[];
|
|
5
|
-
scope?: string;
|
|
6
|
-
token_endpoint_auth_method?: 'none' | 'client_secret_basic' | 'client_secret_jwt' | 'client_secret_post' | 'private_key_jwt' | 'self_signed_tls_client_auth' | 'tls_client_auth';
|
|
7
|
-
token_endpoint_auth_signing_alg?: string;
|
|
8
|
-
introspection_endpoint_auth_method?: 'none' | 'client_secret_basic' | 'client_secret_jwt' | 'client_secret_post' | 'private_key_jwt' | 'self_signed_tls_client_auth' | 'tls_client_auth';
|
|
9
|
-
introspection_endpoint_auth_signing_alg?: string;
|
|
10
|
-
revocation_endpoint_auth_method?: 'none' | 'client_secret_basic' | 'client_secret_jwt' | 'client_secret_post' | 'private_key_jwt' | 'self_signed_tls_client_auth' | 'tls_client_auth';
|
|
11
|
-
revocation_endpoint_auth_signing_alg?: string;
|
|
12
|
-
pushed_authorization_request_endpoint_auth_method?: 'none' | 'client_secret_basic' | 'client_secret_jwt' | 'client_secret_post' | 'private_key_jwt' | 'self_signed_tls_client_auth' | 'tls_client_auth';
|
|
13
|
-
pushed_authorization_request_endpoint_auth_signing_alg?: string;
|
|
14
|
-
userinfo_signed_response_alg?: string;
|
|
15
|
-
userinfo_encrypted_response_alg?: string;
|
|
16
|
-
jwks_uri?: string;
|
|
17
|
-
jwks?: unknown;
|
|
18
|
-
application_type?: 'web' | 'native';
|
|
19
|
-
subject_type?: 'public' | 'pairwise';
|
|
20
|
-
request_object_signing_alg?: string;
|
|
21
|
-
id_token_signed_response_alg?: string;
|
|
22
|
-
authorization_signed_response_alg?: string;
|
|
23
|
-
authorization_encrypted_response_enc?: 'A128CBC-HS256';
|
|
24
|
-
authorization_encrypted_response_alg?: string;
|
|
25
|
-
client_id?: string;
|
|
26
|
-
client_name?: string;
|
|
27
|
-
client_uri?: string;
|
|
28
|
-
policy_uri?: string;
|
|
29
|
-
tos_uri?: string;
|
|
30
|
-
logo_uri?: string;
|
|
31
|
-
default_max_age?: number;
|
|
32
|
-
require_auth_time?: boolean;
|
|
33
|
-
contacts?: string[];
|
|
34
|
-
tls_client_certificate_bound_access_tokens?: boolean;
|
|
35
|
-
dpop_bound_access_tokens?: boolean;
|
|
36
|
-
authorization_details_types?: string[];
|
|
37
|
-
}
|
|
38
|
-
//# sourceMappingURL=client.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../lib/types/client.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC9B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,cAAc,EAAE,CACb,MAAM,GACN,OAAO,GACP,MAAM,GACN,qBAAqB,GACrB,eAAe,GACf,YAAY,GACZ,gBAAgB,GAChB,UAAU,CACZ,EAAE,CAAC;IACJ,WAAW,EAAE,CACV,oBAAoB,GACpB,UAAU,GACV,eAAe,GACf,UAAU,GACV,oBAAoB,GACpB,6CAA6C,GAC7C,+CAA+C,CACjD,EAAE,CAAC;IACJ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0BAA0B,CAAC,EACxB,MAAM,GACN,qBAAqB,GACrB,mBAAmB,GACnB,oBAAoB,GACpB,iBAAiB,GACjB,6BAA6B,GAC7B,iBAAiB,CAAC;IACrB,+BAA+B,CAAC,EAAE,MAAM,CAAC;IACzC,kCAAkC,CAAC,EAChC,MAAM,GACN,qBAAqB,GACrB,mBAAmB,GACnB,oBAAoB,GACpB,iBAAiB,GACjB,6BAA6B,GAC7B,iBAAiB,CAAC;IACrB,uCAAuC,CAAC,EAAE,MAAM,CAAC;IACjD,+BAA+B,CAAC,EAC7B,MAAM,GACN,qBAAqB,GACrB,mBAAmB,GACnB,oBAAoB,GACpB,iBAAiB,GACjB,6BAA6B,GAC7B,iBAAiB,CAAC;IACrB,oCAAoC,CAAC,EAAE,MAAM,CAAC;IAC9C,iDAAiD,CAAC,EAC/C,MAAM,GACN,qBAAqB,GACrB,mBAAmB,GACnB,oBAAoB,GACpB,iBAAiB,GACjB,6BAA6B,GAC7B,iBAAiB,CAAC;IACrB,sDAAsD,CAAC,EAAE,MAAM,CAAC;IAChE,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC,+BAA+B,CAAC,EAAE,MAAM,CAAC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,gBAAgB,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IACpC,YAAY,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;IACrC,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC,iCAAiC,CAAC,EAAE,MAAM,CAAC;IAC3C,oCAAoC,CAAC,EAAE,eAAe,CAAC;IACvD,oCAAoC,CAAC,EAAE,MAAM,CAAC;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,0CAA0C,CAAC,EAAE,OAAO,CAAC;IACrD,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,2BAA2B,CAAC,EAAE,MAAM,EAAE,CAAC;CACvC"}
|
package/dist/types/client.js
DELETED
package/dist/types/client.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../lib/types/client.ts"],"names":[],"mappings":""}
|