@atcute/oauth-browser-client 1.0.27 → 2.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 +122 -218
- package/dist/agents/exchange.d.ts +18 -6
- package/dist/agents/exchange.d.ts.map +1 -1
- package/dist/agents/exchange.js +35 -17
- package/dist/agents/exchange.js.map +1 -1
- package/dist/agents/server-agent.d.ts.map +1 -1
- package/dist/agents/server-agent.js +22 -5
- package/dist/agents/server-agent.js.map +1 -1
- package/dist/dpop.d.ts.map +1 -1
- package/dist/dpop.js +3 -0
- package/dist/dpop.js.map +1 -1
- package/dist/environment.d.ts +12 -2
- package/dist/environment.d.ts.map +1 -1
- package/dist/environment.js +3 -0
- package/dist/environment.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/resolvers.d.ts +5 -47
- package/dist/resolvers.d.ts.map +1 -1
- package/dist/resolvers.js +22 -122
- package/dist/resolvers.js.map +1 -1
- package/dist/store/db.d.ts +1 -0
- package/dist/store/db.d.ts.map +1 -1
- package/dist/store/db.js.map +1 -1
- package/dist/types/client-assertion.d.ts +21 -0
- package/dist/types/client-assertion.d.ts.map +1 -0
- package/dist/types/client-assertion.js +3 -0
- package/dist/types/client-assertion.js.map +1 -0
- package/dist/types/dpop.d.ts +2 -0
- package/dist/types/dpop.d.ts.map +1 -1
- package/dist/types/identity.d.ts +12 -5
- package/dist/types/identity.d.ts.map +1 -1
- package/dist/utils/identity-resolver.d.ts +8 -0
- package/dist/utils/identity-resolver.d.ts.map +1 -0
- package/dist/utils/identity-resolver.js +44 -0
- package/dist/utils/identity-resolver.js.map +1 -0
- package/lib/agents/exchange.ts +52 -25
- package/lib/agents/server-agent.ts +25 -5
- package/lib/dpop.ts +4 -0
- package/lib/environment.ts +19 -2
- package/lib/index.ts +3 -1
- package/lib/resolvers.ts +27 -142
- package/lib/store/db.ts +1 -0
- package/lib/types/client-assertion.ts +25 -0
- package/lib/types/dpop.ts +2 -0
- package/lib/types/identity.ts +14 -5
- package/lib/utils/identity-resolver.ts +59 -0
- package/package.json +5 -4
package/lib/agents/exchange.ts
CHANGED
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
import { nanoid } from 'nanoid';
|
|
2
2
|
|
|
3
|
+
import type { ActorIdentifier } from '@atcute/lexicons';
|
|
4
|
+
|
|
3
5
|
import { createES256Key } from '../dpop.js';
|
|
4
6
|
import { CLIENT_ID, database, REDIRECT_URI } from '../environment.js';
|
|
5
7
|
import { AuthorizationError, LoginError } from '../errors.js';
|
|
6
|
-
import type {
|
|
8
|
+
import type { ResolvedIdentity } from '../types/identity.js';
|
|
7
9
|
import type { AuthorizationServerMetadata } from '../types/server.js';
|
|
8
10
|
import type { Session } from '../types/token.js';
|
|
9
11
|
import { generatePKCE } from '../utils/runtime.js';
|
|
10
12
|
|
|
13
|
+
import { resolveFromIdentifier, resolveFromService } from '../resolvers.js';
|
|
11
14
|
import { OAuthServerAgent } from './server-agent.js';
|
|
12
15
|
import { storeSession } from './sessions.js';
|
|
13
16
|
|
|
17
|
+
export type AuthorizeTargetOptions =
|
|
18
|
+
| { type: 'account'; identifier: ActorIdentifier }
|
|
19
|
+
| { type: 'pds'; serviceUrl: string };
|
|
20
|
+
|
|
14
21
|
export interface AuthorizeOptions {
|
|
15
|
-
|
|
16
|
-
identity?: IdentityMetadata;
|
|
22
|
+
target: AuthorizeTargetOptions;
|
|
17
23
|
scope: string;
|
|
24
|
+
state?: unknown;
|
|
25
|
+
prompt?: 'none' | 'login' | 'consent' | 'select_account';
|
|
26
|
+
display?: 'page' | 'popup' | 'touch' | 'wap';
|
|
27
|
+
locale?: string;
|
|
18
28
|
}
|
|
19
29
|
|
|
20
30
|
/**
|
|
@@ -22,36 +32,52 @@ export interface AuthorizeOptions {
|
|
|
22
32
|
* @param options
|
|
23
33
|
* @returns URL to redirect the user for authorization
|
|
24
34
|
*/
|
|
25
|
-
export const createAuthorizationUrl = async ({
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
export const createAuthorizationUrl = async (options: AuthorizeOptions): Promise<URL> => {
|
|
36
|
+
const { target, scope, state = null, ...reqs } = options;
|
|
37
|
+
|
|
38
|
+
let resolved: { identity?: ResolvedIdentity; metadata: AuthorizationServerMetadata };
|
|
39
|
+
switch (target.type) {
|
|
40
|
+
case 'account': {
|
|
41
|
+
resolved = await resolveFromIdentifier(target.identifier);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
case 'pds': {
|
|
45
|
+
resolved = await resolveFromService(target.serviceUrl);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { identity, metadata } = resolved;
|
|
50
|
+
const loginHint = identity
|
|
51
|
+
? identity.handle !== 'handle.invalid'
|
|
52
|
+
? identity.handle
|
|
53
|
+
: identity.did
|
|
54
|
+
: undefined;
|
|
55
|
+
|
|
56
|
+
const sid = nanoid(24);
|
|
31
57
|
|
|
32
58
|
const pkce = await generatePKCE();
|
|
33
59
|
const dpopKey = await createES256Key();
|
|
34
60
|
|
|
35
61
|
const params = {
|
|
62
|
+
display: reqs.display,
|
|
63
|
+
ui_locales: reqs.locale,
|
|
64
|
+
prompt: reqs.prompt,
|
|
65
|
+
|
|
36
66
|
redirect_uri: REDIRECT_URI,
|
|
37
67
|
code_challenge: pkce.challenge,
|
|
38
68
|
code_challenge_method: pkce.method,
|
|
39
|
-
state:
|
|
40
|
-
login_hint:
|
|
69
|
+
state: sid,
|
|
70
|
+
login_hint: loginHint,
|
|
41
71
|
response_mode: 'fragment',
|
|
42
72
|
response_type: 'code',
|
|
43
|
-
display: 'page',
|
|
44
|
-
// id_token_hint: undefined,
|
|
45
|
-
// max_age: undefined,
|
|
46
|
-
// prompt: undefined,
|
|
47
73
|
scope: scope,
|
|
48
|
-
// ui_locales: undefined,
|
|
49
74
|
} satisfies Record<string, string | undefined>;
|
|
50
75
|
|
|
51
|
-
database.states.set(
|
|
76
|
+
database.states.set(sid, {
|
|
52
77
|
dpopKey: dpopKey,
|
|
53
78
|
metadata: metadata,
|
|
54
79
|
verifier: pkce.verifier,
|
|
80
|
+
state: state,
|
|
55
81
|
});
|
|
56
82
|
|
|
57
83
|
const server = new OAuthServerAgent(metadata, dpopKey);
|
|
@@ -71,25 +97,22 @@ export const createAuthorizationUrl = async ({
|
|
|
71
97
|
*/
|
|
72
98
|
export const finalizeAuthorization = async (params: URLSearchParams) => {
|
|
73
99
|
const issuer = params.get('iss');
|
|
74
|
-
const
|
|
100
|
+
const sid = params.get('state');
|
|
75
101
|
const code = params.get('code');
|
|
76
102
|
const error = params.get('error');
|
|
77
103
|
|
|
78
|
-
if (!
|
|
104
|
+
if (!sid || !(code || error)) {
|
|
79
105
|
throw new LoginError(`missing parameters`);
|
|
80
106
|
}
|
|
81
107
|
|
|
82
|
-
const stored = database.states.get(
|
|
108
|
+
const stored = database.states.get(sid);
|
|
83
109
|
if (stored) {
|
|
84
110
|
// Delete now that we've caught it
|
|
85
|
-
database.states.delete(
|
|
111
|
+
database.states.delete(sid);
|
|
86
112
|
} else {
|
|
87
113
|
throw new LoginError(`unknown state provided`);
|
|
88
114
|
}
|
|
89
115
|
|
|
90
|
-
const dpopKey = stored.dpopKey;
|
|
91
|
-
const metadata = stored.metadata;
|
|
92
|
-
|
|
93
116
|
if (error) {
|
|
94
117
|
throw new AuthorizationError(params.get('error_description') || error);
|
|
95
118
|
}
|
|
@@ -97,6 +120,10 @@ export const finalizeAuthorization = async (params: URLSearchParams) => {
|
|
|
97
120
|
throw new LoginError(`missing code parameter`);
|
|
98
121
|
}
|
|
99
122
|
|
|
123
|
+
const dpopKey = stored.dpopKey;
|
|
124
|
+
const metadata = stored.metadata;
|
|
125
|
+
const state = stored.state ?? null;
|
|
126
|
+
|
|
100
127
|
if (issuer === null) {
|
|
101
128
|
throw new LoginError(`missing issuer parameter`);
|
|
102
129
|
} else if (issuer !== metadata.issuer) {
|
|
@@ -113,5 +140,5 @@ export const finalizeAuthorization = async (params: URLSearchParams) => {
|
|
|
113
140
|
|
|
114
141
|
await storeSession(sub, session);
|
|
115
142
|
|
|
116
|
-
return session;
|
|
143
|
+
return { session, state };
|
|
117
144
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Did } from '@atcute/lexicons';
|
|
2
2
|
|
|
3
|
-
import { createDPoPFetch } from '../dpop.js';
|
|
4
|
-
import { CLIENT_ID, REDIRECT_URI } from '../environment.js';
|
|
3
|
+
import { createDPoPFetch, createDPoPSignage } from '../dpop.js';
|
|
4
|
+
import { CLIENT_ID, fetchClientAssertion, REDIRECT_URI } from '../environment.js';
|
|
5
5
|
import { FetchResponseError, OAuthResponseError, TokenRefreshError } from '../errors.js';
|
|
6
|
-
import {
|
|
6
|
+
import { resolveFromIdentifier } from '../resolvers.js';
|
|
7
7
|
import type { DPoPKey } from '../types/dpop.js';
|
|
8
8
|
import type { OAuthParResponse } from '../types/par.js';
|
|
9
9
|
import type { PersistedAuthorizationServerMetadata } from '../types/server.js';
|
|
@@ -14,9 +14,11 @@ import { extractContentType } from '../utils/response.js';
|
|
|
14
14
|
export class OAuthServerAgent {
|
|
15
15
|
#fetch: typeof fetch;
|
|
16
16
|
#metadata: PersistedAuthorizationServerMetadata;
|
|
17
|
+
#dpopKey: DPoPKey;
|
|
17
18
|
|
|
18
19
|
constructor(metadata: PersistedAuthorizationServerMetadata, dpopKey: DPoPKey) {
|
|
19
20
|
this.#metadata = metadata;
|
|
21
|
+
this.#dpopKey = dpopKey;
|
|
20
22
|
this.#fetch = createDPoPFetch(dpopKey, true);
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -33,6 +35,24 @@ export class OAuthServerAgent {
|
|
|
33
35
|
throw new Error(`no endpoint for ${endpoint}`);
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
if (endpoint === 'token' && fetchClientAssertion !== undefined) {
|
|
39
|
+
const jkt = this.#dpopKey.jkt;
|
|
40
|
+
if (jkt === undefined) {
|
|
41
|
+
throw new Error(`DPoP key missing jkt field`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const clientAssertionCredentials = await fetchClientAssertion({
|
|
45
|
+
jkt: jkt,
|
|
46
|
+
aud: this.#metadata.issuer,
|
|
47
|
+
createDpopProof: async (url) => {
|
|
48
|
+
const sign = createDPoPSignage(this.#dpopKey);
|
|
49
|
+
return await sign('POST', url, undefined, undefined);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
payload = { ...payload, ...clientAssertionCredentials };
|
|
54
|
+
}
|
|
55
|
+
|
|
36
56
|
const response = await this.#fetch(url, {
|
|
37
57
|
method: 'post',
|
|
38
58
|
headers: { 'content-type': 'application/json' },
|
|
@@ -124,7 +144,7 @@ export class OAuthServerAgent {
|
|
|
124
144
|
}
|
|
125
145
|
|
|
126
146
|
const token = this.#processTokenResponse(res);
|
|
127
|
-
const resolved = await
|
|
147
|
+
const resolved = await resolveFromIdentifier(sub as Did);
|
|
128
148
|
|
|
129
149
|
if (resolved.metadata.issuer !== this.#metadata.issuer) {
|
|
130
150
|
throw new TypeError(`issuer mismatch; got ${resolved.metadata.issuer}`);
|
|
@@ -134,7 +154,7 @@ export class OAuthServerAgent {
|
|
|
134
154
|
token: token,
|
|
135
155
|
info: {
|
|
136
156
|
sub: sub as Did,
|
|
137
|
-
aud: resolved.identity.pds
|
|
157
|
+
aud: resolved.identity.pds,
|
|
138
158
|
server: pick(resolved.metadata, [
|
|
139
159
|
'issuer',
|
|
140
160
|
'authorization_endpoint',
|
package/lib/dpop.ts
CHANGED
|
@@ -16,10 +16,14 @@ export const createES256Key = async (): Promise<DPoPKey> => {
|
|
|
16
16
|
const key = await crypto.subtle.exportKey('pkcs8', pair.privateKey);
|
|
17
17
|
const { ext: _ext, key_ops: _key_opts, ...jwk } = await crypto.subtle.exportKey('jwk', pair.publicKey);
|
|
18
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
|
+
|
|
19
22
|
return {
|
|
20
23
|
typ: 'ES256',
|
|
21
24
|
key: toBase64Url(new Uint8Array(key)),
|
|
22
25
|
jwt: toBase64Url(encodeUtf8(JSON.stringify({ typ: 'dpop+jwt', alg: 'ES256', jwk: jwk }))),
|
|
26
|
+
jkt: jkt,
|
|
23
27
|
};
|
|
24
28
|
};
|
|
25
29
|
|
package/lib/environment.ts
CHANGED
|
@@ -1,27 +1,44 @@
|
|
|
1
|
+
import type { IdentityResolver } from './types/identity.js';
|
|
2
|
+
|
|
1
3
|
import { createOAuthDatabase, type OAuthDatabase } from './store/db.js';
|
|
4
|
+
import type { ClientAssertionFetcher } from './types/client-assertion.js';
|
|
2
5
|
|
|
3
6
|
export let CLIENT_ID: string;
|
|
4
7
|
export let REDIRECT_URI: string;
|
|
5
8
|
|
|
9
|
+
export let fetchClientAssertion: ClientAssertionFetcher | undefined;
|
|
10
|
+
|
|
6
11
|
export let database: OAuthDatabase;
|
|
7
12
|
|
|
13
|
+
export let identityResolver: IdentityResolver;
|
|
14
|
+
|
|
8
15
|
export interface ConfigureOAuthOptions {
|
|
9
16
|
/**
|
|
10
|
-
*
|
|
17
|
+
* client metadata, necessary to drive the whole request
|
|
11
18
|
*/
|
|
12
19
|
metadata: {
|
|
13
20
|
client_id: string;
|
|
14
21
|
redirect_uri: string;
|
|
15
22
|
};
|
|
16
23
|
|
|
24
|
+
/** resolves actor identifiers into identity metadata */
|
|
25
|
+
identityResolver: IdentityResolver;
|
|
26
|
+
|
|
17
27
|
/**
|
|
18
|
-
*
|
|
28
|
+
* optional function to fetch DPoP-bound client assertions from your backend.
|
|
29
|
+
*/
|
|
30
|
+
fetchClientAssertion?: ClientAssertionFetcher;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* name that will be used as prefix for storage keys needed to persist authentication.
|
|
19
34
|
* @default "atcute-oauth"
|
|
20
35
|
*/
|
|
21
36
|
storageName?: string;
|
|
22
37
|
}
|
|
23
38
|
|
|
24
39
|
export const configureOAuth = (options: ConfigureOAuthOptions) => {
|
|
40
|
+
({ identityResolver, fetchClientAssertion } = options);
|
|
25
41
|
({ client_id: CLIENT_ID, redirect_uri: REDIRECT_URI } = options.metadata);
|
|
42
|
+
|
|
26
43
|
database = createOAuthDatabase({ name: options.storageName ?? 'atcute-oauth' });
|
|
27
44
|
};
|
package/lib/index.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
export { configureOAuth, type ConfigureOAuthOptions } from './environment.js';
|
|
2
2
|
|
|
3
3
|
export * from './errors.js';
|
|
4
|
-
export * from './resolvers.js';
|
|
5
4
|
|
|
6
5
|
export * from './agents/exchange.js';
|
|
7
6
|
export * from './agents/server-agent.js';
|
|
8
7
|
export * from './agents/sessions.js';
|
|
9
8
|
export * from './agents/user-agent.js';
|
|
10
9
|
|
|
10
|
+
export * from './types/client-assertion.js';
|
|
11
11
|
export * from './types/client.js';
|
|
12
12
|
export * from './types/dpop.js';
|
|
13
13
|
export * from './types/identity.js';
|
|
@@ -15,3 +15,5 @@ export * from './types/par.js';
|
|
|
15
15
|
export * from './types/server.js';
|
|
16
16
|
export * from './types/store.js';
|
|
17
17
|
export * from './types/token.js';
|
|
18
|
+
|
|
19
|
+
export * from './utils/identity-resolver.js';
|
package/lib/resolvers.ts
CHANGED
|
@@ -1,91 +1,42 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { type DidDocument, getPdsEndpoint } from '@atcute/identity';
|
|
3
|
-
import type { Did } from '@atcute/lexicons';
|
|
4
|
-
import { isDid } from '@atcute/lexicons/syntax';
|
|
1
|
+
import type { ActorIdentifier } from '@atcute/lexicons';
|
|
5
2
|
|
|
6
|
-
import {
|
|
3
|
+
import { identityResolver } from './environment.js';
|
|
7
4
|
import { ResolverError } from './errors.js';
|
|
8
|
-
import type {
|
|
5
|
+
import type { ResolvedIdentity } from './types/identity.js';
|
|
9
6
|
import type { AuthorizationServerMetadata, ProtectedResourceMetadata } from './types/server.js';
|
|
10
7
|
import { extractContentType } from './utils/response.js';
|
|
11
8
|
import { isValidUrl } from './utils/strings.js';
|
|
12
9
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* for identity resolution.
|
|
18
|
-
* @param handle Domain handle to resolve
|
|
19
|
-
* @returns DID identifier resolved from the domain handle
|
|
20
|
-
*/
|
|
21
|
-
export const resolveHandle = async (handle: string): Promise<Did> => {
|
|
22
|
-
const url = DEFAULT_APPVIEW_URL + `/xrpc/com.atproto.identity.resolveHandle` + `?handle=${handle}`;
|
|
23
|
-
|
|
24
|
-
const response = await fetch(url);
|
|
25
|
-
if (response.status === 400) {
|
|
26
|
-
throw new ResolverError(`domain handle not found`);
|
|
27
|
-
} else if (!response.ok) {
|
|
28
|
-
throw new ResolverError(`directory is unreachable`);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const json = (await response.json()) as ComAtprotoIdentityResolveHandle.$output;
|
|
10
|
+
export const resolveFromIdentifier = async (
|
|
11
|
+
ident: ActorIdentifier,
|
|
12
|
+
): Promise<{ identity: ResolvedIdentity; metadata: AuthorizationServerMetadata }> => {
|
|
13
|
+
const identity = await identityResolver.resolve(ident);
|
|
32
14
|
|
|
33
|
-
return
|
|
15
|
+
return {
|
|
16
|
+
identity: identity,
|
|
17
|
+
metadata: await getMetadataFromResourceServer(identity.pds),
|
|
18
|
+
};
|
|
34
19
|
};
|
|
35
20
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
let doc: DidDocument;
|
|
49
|
-
|
|
50
|
-
if (type === 'plc') {
|
|
51
|
-
const response = await fetch(`https://plc.directory/${did}`);
|
|
52
|
-
|
|
53
|
-
if (response.status === 404) {
|
|
54
|
-
throw new ResolverError(`did not found in directory`);
|
|
55
|
-
} else if (!response.ok) {
|
|
56
|
-
throw new ResolverError(`directory is unreachable`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const json = await response.json();
|
|
60
|
-
|
|
61
|
-
doc = json as DidDocument;
|
|
62
|
-
} else if (type === 'web') {
|
|
63
|
-
if (!DID_WEB_RE.test(ident)) {
|
|
64
|
-
throw new ResolverError(`invalid identifier`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const response = await fetch(`https://${ident}/.well-known/did.json`);
|
|
68
|
-
|
|
69
|
-
if (!response.ok) {
|
|
70
|
-
throw new ResolverError(`did document is unreachable`);
|
|
21
|
+
export const resolveFromService = async (
|
|
22
|
+
host: string,
|
|
23
|
+
): Promise<{ metadata: AuthorizationServerMetadata }> => {
|
|
24
|
+
try {
|
|
25
|
+
const metadata = await getMetadataFromResourceServer(host);
|
|
26
|
+
return { metadata };
|
|
27
|
+
} catch (err) {
|
|
28
|
+
if (err instanceof ResolverError) {
|
|
29
|
+
try {
|
|
30
|
+
const metadata = await getAuthorizationServerMetadata(host);
|
|
31
|
+
return { metadata };
|
|
32
|
+
} catch {}
|
|
71
33
|
}
|
|
72
34
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
doc = json as DidDocument;
|
|
76
|
-
} else {
|
|
77
|
-
throw new ResolverError(`unsupported did method`);
|
|
35
|
+
throw err;
|
|
78
36
|
}
|
|
79
|
-
|
|
80
|
-
return doc;
|
|
81
37
|
};
|
|
82
38
|
|
|
83
|
-
|
|
84
|
-
* Get OAuth protected resource metadata from a host
|
|
85
|
-
* @param host URL of the host
|
|
86
|
-
* @returns Retrieved protected resource metadata
|
|
87
|
-
*/
|
|
88
|
-
export const getProtectedResourceMetadata = async (host: string): Promise<ProtectedResourceMetadata> => {
|
|
39
|
+
const getProtectedResourceMetadata = async (host: string): Promise<ProtectedResourceMetadata> => {
|
|
89
40
|
const url = new URL(`/.well-known/oauth-protected-resource`, host);
|
|
90
41
|
const response = await fetch(url, {
|
|
91
42
|
redirect: 'manual',
|
|
@@ -106,12 +57,7 @@ export const getProtectedResourceMetadata = async (host: string): Promise<Protec
|
|
|
106
57
|
return metadata;
|
|
107
58
|
};
|
|
108
59
|
|
|
109
|
-
|
|
110
|
-
* Get OAuth authorization server metadata from a host
|
|
111
|
-
* @param host URL of the host
|
|
112
|
-
* @returns Retrieved authorization server metadata
|
|
113
|
-
*/
|
|
114
|
-
export const getAuthorizationServerMetadata = async (host: string): Promise<AuthorizationServerMetadata> => {
|
|
60
|
+
const getAuthorizationServerMetadata = async (host: string): Promise<AuthorizationServerMetadata> => {
|
|
115
61
|
const url = new URL(`/.well-known/oauth-authorization-server`, host);
|
|
116
62
|
const response = await fetch(url, {
|
|
117
63
|
redirect: 'manual',
|
|
@@ -146,68 +92,7 @@ export const getAuthorizationServerMetadata = async (host: string): Promise<Auth
|
|
|
146
92
|
return metadata;
|
|
147
93
|
};
|
|
148
94
|
|
|
149
|
-
|
|
150
|
-
* Resolve handle domains or DID identifiers to get their PDS and its authorization server metadata
|
|
151
|
-
* @param ident Handle domain or DID identifier to resolve
|
|
152
|
-
* @returns Resolved PDS and authorization server metadata
|
|
153
|
-
*/
|
|
154
|
-
export const resolveFromIdentity = async (
|
|
155
|
-
ident: string,
|
|
156
|
-
): Promise<{ identity: IdentityMetadata; metadata: AuthorizationServerMetadata }> => {
|
|
157
|
-
let did: Did;
|
|
158
|
-
if (isDid(ident)) {
|
|
159
|
-
did = ident;
|
|
160
|
-
} else {
|
|
161
|
-
const resolved = await resolveHandle(ident);
|
|
162
|
-
did = resolved;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const doc = await getDidDocument(did);
|
|
166
|
-
const pds = getPdsEndpoint(doc);
|
|
167
|
-
|
|
168
|
-
if (!pds) {
|
|
169
|
-
throw new ResolverError(`missing pds endpoint`);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return {
|
|
173
|
-
identity: {
|
|
174
|
-
id: did,
|
|
175
|
-
raw: ident,
|
|
176
|
-
pds: new URL(pds),
|
|
177
|
-
},
|
|
178
|
-
metadata: await getMetadataFromResourceServer(pds),
|
|
179
|
-
};
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Request authorization server metadata from a PDS
|
|
184
|
-
* @param host URL of the host
|
|
185
|
-
* @returns Resolved authorization server metadata
|
|
186
|
-
*/
|
|
187
|
-
export const resolveFromService = async (
|
|
188
|
-
host: string,
|
|
189
|
-
): Promise<{ metadata: AuthorizationServerMetadata }> => {
|
|
190
|
-
try {
|
|
191
|
-
const metadata = await getMetadataFromResourceServer(host);
|
|
192
|
-
return { metadata };
|
|
193
|
-
} catch (err) {
|
|
194
|
-
if (err instanceof ResolverError) {
|
|
195
|
-
try {
|
|
196
|
-
const metadata = await getAuthorizationServerMetadata(host);
|
|
197
|
-
return { metadata };
|
|
198
|
-
} catch {}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
throw err;
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Request authorization server metadata from its protected resource metadata
|
|
207
|
-
* @param input URL of the host whose authorization server is delegated
|
|
208
|
-
* @returns Resolved authorization server metadata
|
|
209
|
-
*/
|
|
210
|
-
export const getMetadataFromResourceServer = async (input: string) => {
|
|
95
|
+
const getMetadataFromResourceServer = async (input: string) => {
|
|
211
96
|
const rs_metadata = await getProtectedResourceMetadata(input);
|
|
212
97
|
|
|
213
98
|
if (rs_metadata.authorization_servers?.length !== 1) {
|
package/lib/store/db.ts
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const CLIENT_ASSERTION_TYPE_JWT_BEARER = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';
|
|
2
|
+
|
|
3
|
+
export interface ClientAssertionCredentials {
|
|
4
|
+
client_assertion: string;
|
|
5
|
+
client_assertion_type: typeof CLIENT_ASSERTION_TYPE_JWT_BEARER;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface FetchClientAssertionParams {
|
|
9
|
+
/** JWK thumbprint of the DPoP key to bind the assertion to */
|
|
10
|
+
jkt: string;
|
|
11
|
+
/** authorization server issuer (audience for the assertion) */
|
|
12
|
+
aud: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* create a DPoP proof to prove you possess the key for the claimed jkt.
|
|
16
|
+
*
|
|
17
|
+
* @param htu origin and pathname to your backend
|
|
18
|
+
* @returns DPoP proof that can be included in the assertion
|
|
19
|
+
*/
|
|
20
|
+
createDpopProof: (htu: string) => Promise<string>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ClientAssertionFetcher = (
|
|
24
|
+
params: FetchClientAssertionParams,
|
|
25
|
+
) => Promise<ClientAssertionCredentials>;
|
package/lib/types/dpop.ts
CHANGED
package/lib/types/identity.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
import type { Did } from '@atcute/lexicons';
|
|
1
|
+
import type { ActorIdentifier, Did, Handle } from '@atcute/lexicons';
|
|
2
2
|
|
|
3
|
-
export interface
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
pds:
|
|
3
|
+
export interface ResolvedIdentity {
|
|
4
|
+
did: Did;
|
|
5
|
+
handle: Handle;
|
|
6
|
+
pds: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ResolveIdentityOptions {
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
noCache?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface IdentityResolver {
|
|
15
|
+
resolve(actor: ActorIdentifier, options?: ResolveIdentityOptions): Promise<ResolvedIdentity>;
|
|
7
16
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { getAtprotoHandle, getPdsEndpoint } from '@atcute/identity';
|
|
2
|
+
import type { DidDocumentResolver, HandleResolver } from '@atcute/identity-resolver';
|
|
3
|
+
import type { ActorIdentifier, Did, Handle } from '@atcute/lexicons';
|
|
4
|
+
import { isDid } from '@atcute/lexicons/syntax';
|
|
5
|
+
|
|
6
|
+
import { ResolverError } from '../errors.js';
|
|
7
|
+
import type { IdentityResolver, ResolvedIdentity, ResolveIdentityOptions } from '../types/identity.js';
|
|
8
|
+
|
|
9
|
+
export interface DefaultIdentityResolverOptions {
|
|
10
|
+
handleResolver: HandleResolver;
|
|
11
|
+
didDocumentResolver: DidDocumentResolver;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const defaultIdentityResolver = ({
|
|
15
|
+
handleResolver,
|
|
16
|
+
didDocumentResolver,
|
|
17
|
+
}: DefaultIdentityResolverOptions): IdentityResolver => {
|
|
18
|
+
return {
|
|
19
|
+
async resolve(actor: ActorIdentifier, options?: ResolveIdentityOptions): Promise<ResolvedIdentity> {
|
|
20
|
+
const identifierIsDid = isDid(actor);
|
|
21
|
+
|
|
22
|
+
let did: Did;
|
|
23
|
+
if (identifierIsDid) {
|
|
24
|
+
did = actor;
|
|
25
|
+
} else {
|
|
26
|
+
did = await handleResolver.resolve(actor, options);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const doc = await didDocumentResolver.resolve(did, options);
|
|
30
|
+
|
|
31
|
+
const pds = getPdsEndpoint(doc);
|
|
32
|
+
if (!pds) {
|
|
33
|
+
throw new ResolverError(`missing pds endpoint`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let handle: Handle = 'handle.invalid';
|
|
37
|
+
if (identifierIsDid) {
|
|
38
|
+
const writtenHandle = getAtprotoHandle(doc);
|
|
39
|
+
if (writtenHandle) {
|
|
40
|
+
try {
|
|
41
|
+
const resolved = await handleResolver.resolve(writtenHandle, options);
|
|
42
|
+
|
|
43
|
+
if (resolved === did) {
|
|
44
|
+
handle = writtenHandle;
|
|
45
|
+
}
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
} else if (getAtprotoHandle(doc) === actor) {
|
|
49
|
+
handle = actor;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
did: did,
|
|
54
|
+
handle: handle,
|
|
55
|
+
pds: new URL(pds).href,
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@atcute/oauth-browser-client",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "2.0.0",
|
|
5
5
|
"description": "minimal OAuth browser client implementation for AT Protocol",
|
|
6
6
|
"license": "0BSD",
|
|
7
7
|
"repository": {
|
|
@@ -20,14 +20,15 @@
|
|
|
20
20
|
"sideEffects": false,
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"nanoid": "^5.1.5",
|
|
23
|
-
"@atcute/client": "^4.0.
|
|
23
|
+
"@atcute/client": "^4.0.5",
|
|
24
24
|
"@atcute/identity": "^1.1.1",
|
|
25
|
-
"@atcute/
|
|
25
|
+
"@atcute/identity-resolver": "^1.1.4",
|
|
26
26
|
"@atcute/multibase": "^1.1.6",
|
|
27
|
+
"@atcute/lexicons": "^1.2.2",
|
|
27
28
|
"@atcute/uint8array": "^1.0.5"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
|
-
"@atcute/atproto": "^3.1.
|
|
31
|
+
"@atcute/atproto": "^3.1.8"
|
|
31
32
|
},
|
|
32
33
|
"scripts": {
|
|
33
34
|
"build": "tsc --project tsconfig.build.json",
|