@atcute/oauth-browser-client 2.0.3 → 3.0.1
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 +19 -12
- package/dist/agents/exchange.d.ts +3 -2
- 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 +6 -6
- 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 +6 -5
- 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/agents/user-agent.d.ts +2 -2
- package/dist/agents/user-agent.d.ts.map +1 -1
- package/dist/agents/user-agent.js +2 -2
- package/dist/agents/user-agent.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 +5 -5
- package/dist/environment.d.ts.map +1 -1
- package/dist/environment.js.map +1 -1
- package/dist/errors.d.ts +3 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +3 -3
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +7 -15
- 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 +5 -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 +8 -8
- package/dist/store/db.d.ts.map +1 -1
- package/dist/store/db.js.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 +9 -21
- 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/misc.d.ts.map +1 -1
- package/dist/utils/misc.js.map +1 -1
- package/dist/utils/response.d.ts.map +1 -1
- package/dist/utils/response.js.map +1 -1
- 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/dist/utils/strings.d.ts.map +1 -1
- package/dist/utils/strings.js.map +1 -1
- package/lib/agents/exchange.ts +15 -16
- package/lib/agents/server-agent.ts +21 -24
- package/lib/agents/sessions.ts +28 -7
- package/lib/agents/user-agent.ts +14 -8
- package/lib/dpop.ts +9 -110
- package/lib/environment.ts +5 -5
- package/lib/errors.ts +15 -14
- package/lib/index.ts +16 -16
- package/lib/resolvers.ts +17 -15
- package/lib/store/db.ts +8 -8
- 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 +20 -11
- 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/agents/exchange.ts
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import type { ResolvedActor } from '@atcute/identity-resolver';
|
|
3
2
|
import type { ActorIdentifier } from '@atcute/lexicons';
|
|
3
|
+
import { generateDpopKey, generatePkce } from '@atcute/oauth-crypto';
|
|
4
|
+
import type { OAuthAuthorizationServerMetadata, OAuthPrompt } from '@atcute/oauth-types';
|
|
5
|
+
|
|
6
|
+
import { nanoid } from 'nanoid';
|
|
4
7
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import type {
|
|
9
|
-
import type { AuthorizationServerMetadata } from '../types/server.js';
|
|
10
|
-
import type { Session } from '../types/token.js';
|
|
11
|
-
import { generatePKCE } from '../utils/runtime.js';
|
|
8
|
+
import { CLIENT_ID, database, REDIRECT_URI } from '../environment.ts';
|
|
9
|
+
import { AuthorizationError, LoginError } from '../errors.ts';
|
|
10
|
+
import { resolveFromIdentifier, resolveFromService } from '../resolvers.ts';
|
|
11
|
+
import type { Session } from '../types/token.ts';
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { storeSession } from './sessions.js';
|
|
13
|
+
import { OAuthServerAgent } from './server-agent.ts';
|
|
14
|
+
import { storeSession } from './sessions.ts';
|
|
16
15
|
|
|
17
16
|
export type AuthorizeTargetOptions =
|
|
18
17
|
| { type: 'account'; identifier: ActorIdentifier }
|
|
@@ -22,7 +21,7 @@ export interface AuthorizeOptions {
|
|
|
22
21
|
target: AuthorizeTargetOptions;
|
|
23
22
|
scope: string;
|
|
24
23
|
state?: unknown;
|
|
25
|
-
prompt?:
|
|
24
|
+
prompt?: OAuthPrompt | (string & {});
|
|
26
25
|
display?: 'page' | 'popup' | 'touch' | 'wap';
|
|
27
26
|
locale?: string;
|
|
28
27
|
}
|
|
@@ -35,7 +34,7 @@ export interface AuthorizeOptions {
|
|
|
35
34
|
export const createAuthorizationUrl = async (options: AuthorizeOptions): Promise<URL> => {
|
|
36
35
|
const { target, scope, state = null, ...reqs } = options;
|
|
37
36
|
|
|
38
|
-
let resolved: { identity?:
|
|
37
|
+
let resolved: { identity?: ResolvedActor; metadata: OAuthAuthorizationServerMetadata };
|
|
39
38
|
switch (target.type) {
|
|
40
39
|
case 'account': {
|
|
41
40
|
resolved = await resolveFromIdentifier(target.identifier);
|
|
@@ -55,8 +54,8 @@ export const createAuthorizationUrl = async (options: AuthorizeOptions): Promise
|
|
|
55
54
|
|
|
56
55
|
const sid = nanoid(24);
|
|
57
56
|
|
|
58
|
-
const pkce = await
|
|
59
|
-
const dpopKey = await
|
|
57
|
+
const pkce = await generatePkce();
|
|
58
|
+
const dpopKey = await generateDpopKey(['ES256']);
|
|
60
59
|
|
|
61
60
|
const params = {
|
|
62
61
|
display: reqs.display,
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import type { Did } from '@atcute/lexicons';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import type { PersistedAuthorizationServerMetadata } from '../types/server.
|
|
10
|
-
import type { ExchangeInfo,
|
|
11
|
-
import { pick } from '../utils/misc.
|
|
12
|
-
import { extractContentType } from '../utils/response.
|
|
2
|
+
import { createDpopProofSigner, type DpopPrivateJwk } from '@atcute/oauth-crypto';
|
|
3
|
+
import type { AtprotoOAuthTokenResponse, OAuthParResponse } from '@atcute/oauth-types';
|
|
4
|
+
|
|
5
|
+
import { createDPoPFetch } from '../dpop.ts';
|
|
6
|
+
import { CLIENT_ID, fetchClientAssertion, REDIRECT_URI } from '../environment.ts';
|
|
7
|
+
import { FetchResponseError, OAuthResponseError, TokenRefreshError } from '../errors.ts';
|
|
8
|
+
import { resolveFromIdentifier } from '../resolvers.ts';
|
|
9
|
+
import type { PersistedAuthorizationServerMetadata } from '../types/server.ts';
|
|
10
|
+
import type { ExchangeInfo, TokenInfo } from '../types/token.ts';
|
|
11
|
+
import { pick } from '../utils/misc.ts';
|
|
12
|
+
import { extractContentType } from '../utils/response.ts';
|
|
13
13
|
|
|
14
14
|
export class OAuthServerAgent {
|
|
15
15
|
#fetch: typeof fetch;
|
|
16
16
|
#metadata: PersistedAuthorizationServerMetadata;
|
|
17
|
-
#dpopKey:
|
|
17
|
+
#dpopKey: DpopPrivateJwk;
|
|
18
18
|
|
|
19
|
-
constructor(metadata: PersistedAuthorizationServerMetadata, dpopKey:
|
|
19
|
+
constructor(metadata: PersistedAuthorizationServerMetadata, dpopKey: DpopPrivateJwk) {
|
|
20
20
|
this.#metadata = metadata;
|
|
21
21
|
this.#dpopKey = dpopKey;
|
|
22
22
|
this.#fetch = createDPoPFetch(dpopKey, true);
|
|
@@ -26,7 +26,7 @@ export class OAuthServerAgent {
|
|
|
26
26
|
endpoint: 'pushed_authorization_request',
|
|
27
27
|
payload: Record<string, unknown>,
|
|
28
28
|
): Promise<OAuthParResponse>;
|
|
29
|
-
async request(endpoint: 'token', payload: Record<string, unknown>): Promise<
|
|
29
|
+
async request(endpoint: 'token', payload: Record<string, unknown>): Promise<AtprotoOAuthTokenResponse>;
|
|
30
30
|
async request(endpoint: 'revocation', payload: Record<string, unknown>): Promise<any>;
|
|
31
31
|
async request(endpoint: 'introspection', payload: Record<string, unknown>): Promise<any>;
|
|
32
32
|
async request(endpoint: string, payload: Record<string, unknown>): Promise<any> {
|
|
@@ -39,17 +39,12 @@ export class OAuthServerAgent {
|
|
|
39
39
|
(endpoint === 'token' || endpoint === 'pushed_authorization_request') &&
|
|
40
40
|
fetchClientAssertion !== undefined
|
|
41
41
|
) {
|
|
42
|
-
const
|
|
43
|
-
if (jkt === undefined) {
|
|
44
|
-
throw new Error(`DPoP key missing jkt field`);
|
|
45
|
-
}
|
|
42
|
+
const sign = createDpopProofSigner(this.#dpopKey);
|
|
46
43
|
|
|
47
44
|
const assertion = await fetchClientAssertion({
|
|
48
|
-
jkt: jkt,
|
|
49
45
|
aud: this.#metadata.issuer,
|
|
50
|
-
createDpopProof: async (url) => {
|
|
51
|
-
|
|
52
|
-
return await sign('POST', url, undefined, undefined);
|
|
46
|
+
createDpopProof: async (url, nonce) => {
|
|
47
|
+
return await sign('POST', url, nonce, undefined);
|
|
53
48
|
},
|
|
54
49
|
});
|
|
55
50
|
|
|
@@ -120,7 +115,7 @@ export class OAuthServerAgent {
|
|
|
120
115
|
}
|
|
121
116
|
}
|
|
122
117
|
|
|
123
|
-
#processTokenResponse(res:
|
|
118
|
+
#processTokenResponse(res: AtprotoOAuthTokenResponse): TokenInfo {
|
|
124
119
|
if (!res.sub) {
|
|
125
120
|
throw new TypeError(`missing sub field in token response`);
|
|
126
121
|
}
|
|
@@ -140,7 +135,9 @@ export class OAuthServerAgent {
|
|
|
140
135
|
};
|
|
141
136
|
}
|
|
142
137
|
|
|
143
|
-
async #processExchangeResponse(
|
|
138
|
+
async #processExchangeResponse(
|
|
139
|
+
res: AtprotoOAuthTokenResponse,
|
|
140
|
+
): Promise<{ info: ExchangeInfo; token: TokenInfo }> {
|
|
144
141
|
const sub = res.sub;
|
|
145
142
|
if (!sub) {
|
|
146
143
|
throw new TypeError(`missing sub field in token response`);
|
package/lib/agents/sessions.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { Did } from '@atcute/lexicons';
|
|
2
2
|
|
|
3
|
-
import { database } from '../environment.
|
|
4
|
-
import { OAuthResponseError, TokenRefreshError } from '../errors.
|
|
5
|
-
import type { Session } from '../types/token.
|
|
6
|
-
import {
|
|
3
|
+
import { database } from '../environment.ts';
|
|
4
|
+
import { OAuthResponseError, TokenRefreshError } from '../errors.ts';
|
|
5
|
+
import type { RawSession, Session } from '../types/token.ts';
|
|
6
|
+
import { isLegacyDpopKey, migrateLegacyDpopKey } from '../utils/dpop-key.ts';
|
|
7
|
+
import { locks } from '../utils/runtime.ts';
|
|
7
8
|
|
|
8
|
-
import { OAuthServerAgent } from './server-agent.
|
|
9
|
+
import { OAuthServerAgent } from './server-agent.ts';
|
|
9
10
|
|
|
10
11
|
export interface SessionGetOptions {
|
|
11
12
|
signal?: AbortSignal;
|
|
@@ -13,7 +14,7 @@ export interface SessionGetOptions {
|
|
|
13
14
|
allowStale?: boolean;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
type PendingItem<V> =
|
|
17
|
+
type PendingItem<V> = { value: V; isFresh: boolean };
|
|
17
18
|
const pending = new Map<Did, Promise<PendingItem<Session>>>();
|
|
18
19
|
|
|
19
20
|
export const getSession = async (sub: Did, options?: SessionGetOptions): Promise<Session> => {
|
|
@@ -49,7 +50,7 @@ export const getSession = async (sub: Did, options?: SessionGetOptions): Promise
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
const run = async (): Promise<PendingItem<Session>> => {
|
|
52
|
-
const storedSession = database.sessions.get(sub);
|
|
53
|
+
const storedSession = await migrateSessionIfNeeded(sub, database.sessions.get(sub));
|
|
53
54
|
|
|
54
55
|
if (storedSession && allowStored(storedSession)) {
|
|
55
56
|
// Use the stored value as return value for the current execution
|
|
@@ -140,3 +141,23 @@ const isTokenUsable = ({ token }: Session): boolean => {
|
|
|
140
141
|
const expires = token.expires_at;
|
|
141
142
|
return expires == null || Date.now() + 60_000 <= expires;
|
|
142
143
|
};
|
|
144
|
+
|
|
145
|
+
const migrateSessionIfNeeded = async (
|
|
146
|
+
sub: Did,
|
|
147
|
+
session: RawSession | undefined,
|
|
148
|
+
): Promise<Session | undefined> => {
|
|
149
|
+
if (!session || !isLegacyDpopKey(session.dpopKey)) {
|
|
150
|
+
return session as Session | undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const dpopKey = await migrateLegacyDpopKey(session.dpopKey);
|
|
154
|
+
const migrated = { ...session, dpopKey };
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
database.sessions.set(sub, migrated);
|
|
158
|
+
} catch {
|
|
159
|
+
// ignore persistence errors
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return migrated;
|
|
163
|
+
};
|
package/lib/agents/user-agent.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import type { FetchHandlerObject } from '@atcute/client';
|
|
2
2
|
import type { Did } from '@atcute/lexicons';
|
|
3
3
|
|
|
4
|
-
import { createDPoPFetch } from '../dpop.
|
|
5
|
-
import type { Session } from '../types/token.
|
|
4
|
+
import { createDPoPFetch } from '../dpop.ts';
|
|
5
|
+
import type { Session } from '../types/token.ts';
|
|
6
6
|
|
|
7
|
-
import { OAuthServerAgent } from './server-agent.
|
|
8
|
-
import { type SessionGetOptions, deleteStoredSession, getSession } from './sessions.
|
|
7
|
+
import { OAuthServerAgent } from './server-agent.ts';
|
|
8
|
+
import { type SessionGetOptions, deleteStoredSession, getSession } from './sessions.ts';
|
|
9
9
|
|
|
10
10
|
export class OAuthUserAgent implements FetchHandlerObject {
|
|
11
11
|
#fetch: typeof fetch;
|
|
12
12
|
#getSessionPromise: Promise<Session> | undefined;
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
session: Session;
|
|
15
|
+
|
|
16
|
+
constructor(session: Session) {
|
|
17
|
+
this.session = session;
|
|
15
18
|
this.#fetch = createDPoPFetch(session.dpopKey, false);
|
|
16
19
|
}
|
|
17
20
|
|
|
@@ -23,9 +26,12 @@ export class OAuthUserAgent implements FetchHandlerObject {
|
|
|
23
26
|
const promise = getSession(this.session.info.sub, options);
|
|
24
27
|
|
|
25
28
|
promise
|
|
26
|
-
.then(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
.then(
|
|
30
|
+
(session) => {
|
|
31
|
+
this.session = session;
|
|
32
|
+
},
|
|
33
|
+
() => {},
|
|
34
|
+
)
|
|
29
35
|
.finally(() => {
|
|
30
36
|
this.#getSessionPromise = undefined;
|
|
31
37
|
});
|
package/lib/dpop.ts
CHANGED
|
@@ -1,82 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { encodeUtf8 } from '@atcute/uint8array';
|
|
1
|
+
import { createDpopProofSigner, sha256Base64Url, type DpopPrivateJwk } from '@atcute/oauth-crypto';
|
|
3
2
|
|
|
4
|
-
import {
|
|
3
|
+
import { database } from './environment.ts';
|
|
4
|
+
import { extractContentType } from './utils/response.ts';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
import type { DPoPKey } from './types/dpop.js';
|
|
8
|
-
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
|
-
|
|
53
|
-
return async (method: string, htu: string, nonce: string | undefined, ath: string | undefined) => {
|
|
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,7 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ActorResolver } from '@atcute/identity-resolver';
|
|
2
2
|
|
|
3
|
-
import { createOAuthDatabase, type OAuthDatabase } from './store/db.
|
|
4
|
-
import type { ClientAssertionFetcher } from './types/client-assertion.
|
|
3
|
+
import { createOAuthDatabase, type OAuthDatabase } from './store/db.ts';
|
|
4
|
+
import type { ClientAssertionFetcher } from './types/client-assertion.ts';
|
|
5
5
|
|
|
6
6
|
export let CLIENT_ID: string;
|
|
7
7
|
export let REDIRECT_URI: string;
|
|
@@ -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/errors.ts
CHANGED
|
@@ -15,25 +15,23 @@ export class ResolverError extends Error {
|
|
|
15
15
|
export class TokenRefreshError extends Error {
|
|
16
16
|
override name = 'TokenRefreshError';
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
options?: ErrorOptions,
|
|
22
|
-
) {
|
|
18
|
+
readonly sub: Did;
|
|
19
|
+
|
|
20
|
+
constructor(sub: Did, message: string, options?: ErrorOptions) {
|
|
23
21
|
super(message, options);
|
|
22
|
+
this.sub = sub;
|
|
24
23
|
}
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
export class OAuthResponseError extends Error {
|
|
28
27
|
override name = 'OAuthResponseError';
|
|
29
28
|
|
|
29
|
+
readonly response: Response;
|
|
30
|
+
readonly data: any;
|
|
30
31
|
readonly error: string | undefined;
|
|
31
32
|
readonly description: string | undefined;
|
|
32
33
|
|
|
33
|
-
constructor(
|
|
34
|
-
public readonly response: Response,
|
|
35
|
-
public readonly data: any,
|
|
36
|
-
) {
|
|
34
|
+
constructor(response: Response, data: any) {
|
|
37
35
|
const error = ifString(ifObject(data)?.['error']);
|
|
38
36
|
const errorDescription = ifString(ifObject(data)?.['error_description']);
|
|
39
37
|
|
|
@@ -43,6 +41,8 @@ export class OAuthResponseError extends Error {
|
|
|
43
41
|
|
|
44
42
|
super(message);
|
|
45
43
|
|
|
44
|
+
this.response = response;
|
|
45
|
+
this.data = data;
|
|
46
46
|
this.error = error;
|
|
47
47
|
this.description = errorDescription;
|
|
48
48
|
}
|
|
@@ -59,12 +59,13 @@ export class OAuthResponseError extends Error {
|
|
|
59
59
|
export class FetchResponseError extends Error {
|
|
60
60
|
override name = 'FetchResponseError';
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
) {
|
|
62
|
+
readonly response: Response;
|
|
63
|
+
status: number;
|
|
64
|
+
|
|
65
|
+
constructor(response: Response, status: number, message: string) {
|
|
67
66
|
super(message);
|
|
67
|
+
this.response = response;
|
|
68
|
+
this.status = status;
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
|
package/lib/index.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
export { configureOAuth, type ConfigureOAuthOptions } from './environment.
|
|
1
|
+
export { configureOAuth, type ConfigureOAuthOptions } from './environment.ts';
|
|
2
2
|
|
|
3
|
-
export * from './errors.
|
|
3
|
+
export * from './errors.ts';
|
|
4
4
|
|
|
5
|
-
export * from './agents/exchange.
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
export * from './agents/exchange.ts';
|
|
6
|
+
export {
|
|
7
|
+
getSession,
|
|
8
|
+
deleteStoredSession,
|
|
9
|
+
listStoredSessions,
|
|
10
|
+
type SessionGetOptions,
|
|
11
|
+
} from './agents/sessions.ts';
|
|
12
|
+
export * from './agents/user-agent.ts';
|
|
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.ts';
|
|
19
|
+
export type { TokenInfo, ExchangeInfo, Session } from './types/token.ts';
|