@atcute/oauth-browser-client 1.0.3 → 1.0.5
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 +296 -6
- package/dist/agents/server-agent.js +7 -6
- package/dist/agents/server-agent.js.map +1 -1
- package/dist/store/db.js +1 -1
- package/dist/store/db.js.map +1 -1
- package/lib/agents/exchange.ts +115 -0
- package/lib/agents/server-agent.ts +149 -0
- package/lib/agents/sessions.ts +142 -0
- package/lib/agents/user-agent.ts +99 -0
- package/lib/constants.ts +1 -0
- package/lib/dpop.ts +154 -0
- package/lib/environment.ts +27 -0
- package/lib/errors.ts +76 -0
- package/lib/index.ts +17 -0
- package/lib/resolvers.ts +222 -0
- package/lib/store/db.ts +184 -0
- package/lib/types/client.ts +82 -0
- package/lib/types/dpop.ts +7 -0
- package/lib/types/identity.ts +7 -0
- package/lib/types/par.ts +4 -0
- package/lib/types/server.ts +67 -0
- package/lib/types/store.ts +6 -0
- package/lib/types/token.ts +46 -0
- package/lib/utils/misc.ts +14 -0
- package/lib/utils/response.ts +3 -0
- package/lib/utils/runtime.ts +55 -0
- package/lib/utils/strings.ts +5 -0
- package/package.json +9 -5
package/lib/resolvers.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type { At, ComAtprotoIdentityResolveHandle } from '@atcute/client/lexicons';
|
|
2
|
+
import { type DidDocument, getPdsEndpoint } from '@atcute/client/utils/did';
|
|
3
|
+
|
|
4
|
+
import { DEFAULT_APPVIEW_URL } from './constants.js';
|
|
5
|
+
import { ResolverError } from './errors.js';
|
|
6
|
+
import type { IdentityMetadata } from './types/identity.js';
|
|
7
|
+
import type { AuthorizationServerMetadata, ProtectedResourceMetadata } from './types/server.js';
|
|
8
|
+
import { extractContentType } from './utils/response.js';
|
|
9
|
+
import { isDid } from './utils/strings.js';
|
|
10
|
+
|
|
11
|
+
const DID_WEB_RE = /^([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*(?:\.[a-zA-Z]{2,}))$/;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolves domain handles into DID identifiers, by requesting Bluesky's AppView
|
|
15
|
+
* for identity resolution.
|
|
16
|
+
* @param handle Domain handle to resolve
|
|
17
|
+
* @returns DID identifier resolved from the domain handle
|
|
18
|
+
*/
|
|
19
|
+
export const resolveHandle = async (handle: string): Promise<At.DID> => {
|
|
20
|
+
const url = DEFAULT_APPVIEW_URL + `/xrpc/com.atproto.identity.resolveHandle` + `?handle=${handle}`;
|
|
21
|
+
|
|
22
|
+
const response = await fetch(url);
|
|
23
|
+
if (response.status === 400) {
|
|
24
|
+
throw new ResolverError(`domain handle not found`);
|
|
25
|
+
} else if (!response.ok) {
|
|
26
|
+
throw new ResolverError(`directory is unreachable`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const json = (await response.json()) as ComAtprotoIdentityResolveHandle.Output;
|
|
30
|
+
return json.did;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get DID documents of did:plc (via plc.directory) and did:web identifiers
|
|
35
|
+
* @param did DID identifier we're seeking DID doc from
|
|
36
|
+
* @returns Retrieved DID document
|
|
37
|
+
*/
|
|
38
|
+
export const getDidDocument = async (did: At.DID): Promise<DidDocument> => {
|
|
39
|
+
const colon_index = did.indexOf(':', 4);
|
|
40
|
+
|
|
41
|
+
const type = did.slice(4, colon_index);
|
|
42
|
+
const ident = did.slice(colon_index + 1);
|
|
43
|
+
|
|
44
|
+
// 2. retrieve their DID documents
|
|
45
|
+
let doc: DidDocument;
|
|
46
|
+
|
|
47
|
+
if (type === 'plc') {
|
|
48
|
+
const response = await fetch(`https://plc.directory/${did}`);
|
|
49
|
+
|
|
50
|
+
if (response.status === 404) {
|
|
51
|
+
throw new ResolverError(`did not found in directory`);
|
|
52
|
+
} else if (!response.ok) {
|
|
53
|
+
throw new ResolverError(`directory is unreachable`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const json = await response.json();
|
|
57
|
+
|
|
58
|
+
doc = json as DidDocument;
|
|
59
|
+
} else if (type === 'web') {
|
|
60
|
+
if (!DID_WEB_RE.test(ident)) {
|
|
61
|
+
throw new ResolverError(`invalid identifier`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const response = await fetch(`https://${ident}/.well-known/did.json`);
|
|
65
|
+
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new ResolverError(`did document is unreachable`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const json = await response.json();
|
|
71
|
+
|
|
72
|
+
doc = json as DidDocument;
|
|
73
|
+
} else {
|
|
74
|
+
throw new ResolverError(`unsupported did method`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return doc;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get OAuth protected resource metadata from a host
|
|
82
|
+
* @param host URL of the host
|
|
83
|
+
* @returns Retrieved protected resource metadata
|
|
84
|
+
*/
|
|
85
|
+
export const getProtectedResourceMetadata = async (host: string): Promise<ProtectedResourceMetadata> => {
|
|
86
|
+
const url = new URL(`/.well-known/oauth-protected-resource`, host);
|
|
87
|
+
const response = await fetch(url, {
|
|
88
|
+
redirect: 'manual',
|
|
89
|
+
headers: {
|
|
90
|
+
accept: 'application/json',
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (response.status !== 200 || extractContentType(response.headers) !== 'application/json') {
|
|
95
|
+
throw new ResolverError(`unexpected response`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const metadata = (await response.json()) as ProtectedResourceMetadata;
|
|
99
|
+
if (metadata.resource !== url.origin) {
|
|
100
|
+
throw new ResolverError(`unexpected issuer`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return metadata;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get OAuth authorization server metadata from a host
|
|
108
|
+
* @param host URL of the host
|
|
109
|
+
* @returns Retrieved authorization server metadata
|
|
110
|
+
*/
|
|
111
|
+
export const getAuthorizationServerMetadata = async (host: string): Promise<AuthorizationServerMetadata> => {
|
|
112
|
+
const url = new URL(`/.well-known/oauth-authorization-server`, host);
|
|
113
|
+
const response = await fetch(url, {
|
|
114
|
+
redirect: 'manual',
|
|
115
|
+
headers: {
|
|
116
|
+
accept: 'application/json',
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (response.status !== 200 || extractContentType(response.headers) !== 'application/json') {
|
|
121
|
+
throw new ResolverError(`unexpected response`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const metadata = (await response.json()) as AuthorizationServerMetadata;
|
|
125
|
+
if (metadata.issuer !== url.origin) {
|
|
126
|
+
throw new ResolverError(`unexpected issuer`);
|
|
127
|
+
}
|
|
128
|
+
if (!metadata.client_id_metadata_document_supported) {
|
|
129
|
+
throw new ResolverError(`authorization server does not support 'client_id_metadata_document'`);
|
|
130
|
+
}
|
|
131
|
+
if (!metadata.pushed_authorization_request_endpoint) {
|
|
132
|
+
throw new ResolverError(`authorization server does not support 'pushed_authorization request'`);
|
|
133
|
+
}
|
|
134
|
+
if (metadata.response_types_supported) {
|
|
135
|
+
if (!metadata.response_types_supported.includes('code')) {
|
|
136
|
+
throw new ResolverError(`authorization server does not support 'code' response type`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return metadata;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Resolve handle domains or DID identifiers to get their PDS and its authorization server metadata
|
|
145
|
+
* @param ident Handle domain or DID identifier to resolve
|
|
146
|
+
* @returns Resolved PDS and authorization server metadata
|
|
147
|
+
*/
|
|
148
|
+
export const resolveFromIdentity = async (
|
|
149
|
+
ident: string,
|
|
150
|
+
): Promise<{ identity: IdentityMetadata; metadata: AuthorizationServerMetadata }> => {
|
|
151
|
+
let did: At.DID;
|
|
152
|
+
if (isDid(ident)) {
|
|
153
|
+
did = ident;
|
|
154
|
+
} else {
|
|
155
|
+
const resolved = await resolveHandle(ident);
|
|
156
|
+
did = resolved;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const doc = await getDidDocument(did);
|
|
160
|
+
const pds = getPdsEndpoint(doc);
|
|
161
|
+
|
|
162
|
+
if (!pds) {
|
|
163
|
+
throw new ResolverError(`missing pds endpoint`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
identity: {
|
|
168
|
+
id: did,
|
|
169
|
+
raw: ident,
|
|
170
|
+
pds: new URL(pds),
|
|
171
|
+
},
|
|
172
|
+
metadata: await getMetadataFromResourceServer(pds),
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Request authorization server metadata from a PDS
|
|
178
|
+
* @param host URL of the host
|
|
179
|
+
* @returns Resolved authorization server metadata
|
|
180
|
+
*/
|
|
181
|
+
export const resolveFromService = async (
|
|
182
|
+
host: string,
|
|
183
|
+
): Promise<{ metadata: AuthorizationServerMetadata }> => {
|
|
184
|
+
try {
|
|
185
|
+
const metadata = await getMetadataFromResourceServer(host);
|
|
186
|
+
return { metadata };
|
|
187
|
+
} catch (err) {
|
|
188
|
+
if (err instanceof ResolverError) {
|
|
189
|
+
try {
|
|
190
|
+
const metadata = await getAuthorizationServerMetadata(host);
|
|
191
|
+
return { metadata };
|
|
192
|
+
} catch {}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
throw err;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Request authorization server metadata from its protected resource metadata
|
|
201
|
+
* @param input URL of the host whose authorization server is delegated
|
|
202
|
+
* @returns Resolved authorization server metadata
|
|
203
|
+
*/
|
|
204
|
+
export const getMetadataFromResourceServer = async (input: string) => {
|
|
205
|
+
const rs_metadata = await getProtectedResourceMetadata(input);
|
|
206
|
+
|
|
207
|
+
if (rs_metadata.authorization_servers?.length !== 1) {
|
|
208
|
+
throw new ResolverError(`expected exactly one authorization server in the listing`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const issuer = rs_metadata.authorization_servers[0];
|
|
212
|
+
|
|
213
|
+
const as_metadata = await getAuthorizationServerMetadata(issuer);
|
|
214
|
+
|
|
215
|
+
if (as_metadata.protected_resources) {
|
|
216
|
+
if (!as_metadata.protected_resources.includes(rs_metadata.resource)) {
|
|
217
|
+
throw new ResolverError(`server is not in authorization server's jurisdiction`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return as_metadata;
|
|
222
|
+
};
|
package/lib/store/db.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { At } from '@atcute/client/lexicons';
|
|
2
|
+
|
|
3
|
+
import type { DPoPKey } from '../types/dpop.js';
|
|
4
|
+
import type { AuthorizationServerMetadata } from '../types/server.js';
|
|
5
|
+
import type { SimpleStore } from '../types/store.js';
|
|
6
|
+
import type { Session } from '../types/token.js';
|
|
7
|
+
import { locks } from '../utils/runtime.js';
|
|
8
|
+
|
|
9
|
+
export interface OAuthDatabaseOptions {
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface SchemaItem<T> {
|
|
14
|
+
value: T;
|
|
15
|
+
expiresAt: number | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Schema {
|
|
19
|
+
sessions: {
|
|
20
|
+
key: At.DID;
|
|
21
|
+
value: Session;
|
|
22
|
+
indexes: {
|
|
23
|
+
expiresAt: number;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
states: {
|
|
27
|
+
key: string;
|
|
28
|
+
value: {
|
|
29
|
+
dpopKey: DPoPKey;
|
|
30
|
+
metadata: AuthorizationServerMetadata;
|
|
31
|
+
verifier?: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
dpopNonces: {
|
|
36
|
+
key: string;
|
|
37
|
+
value: string;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const parse = (raw: string | null) => {
|
|
42
|
+
if (raw != null) {
|
|
43
|
+
const parsed = JSON.parse(raw);
|
|
44
|
+
if (parsed != null) {
|
|
45
|
+
return parsed;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type OAuthDatabase = ReturnType<typeof createOAuthDatabase>;
|
|
53
|
+
|
|
54
|
+
export const createOAuthDatabase = ({ name }: OAuthDatabaseOptions) => {
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const signal = controller.signal;
|
|
57
|
+
|
|
58
|
+
const createStore = <N extends keyof Schema>(
|
|
59
|
+
subname: N,
|
|
60
|
+
expiresAt: (item: Schema[N]['value']) => null | number,
|
|
61
|
+
): SimpleStore<Schema[N]['key'], Schema[N]['value']> => {
|
|
62
|
+
let store: any;
|
|
63
|
+
|
|
64
|
+
const storageKey = `${name}:${subname}`;
|
|
65
|
+
|
|
66
|
+
const persist = () => store && localStorage.setItem(storageKey, JSON.stringify(store));
|
|
67
|
+
const read = () => {
|
|
68
|
+
if (signal.aborted) {
|
|
69
|
+
throw new Error(`store closed`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (store ??= parse(localStorage.getItem(storageKey)));
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
{
|
|
76
|
+
const listener = (ev: StorageEvent) => {
|
|
77
|
+
if (ev.key === storageKey) {
|
|
78
|
+
store = undefined;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
globalThis.addEventListener('storage', listener, { signal });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
{
|
|
86
|
+
const cleanup = async (lock: Lock | true | null) => {
|
|
87
|
+
if (!lock || signal.aborted) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await new Promise((resolve) => setTimeout(resolve, 10_000));
|
|
92
|
+
if (signal.aborted) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let now = Date.now();
|
|
97
|
+
let changed = false;
|
|
98
|
+
|
|
99
|
+
read();
|
|
100
|
+
|
|
101
|
+
for (const key in store) {
|
|
102
|
+
const item = store[key];
|
|
103
|
+
const expiresAt = item.expiresAt;
|
|
104
|
+
|
|
105
|
+
if (expiresAt !== null && now > expiresAt) {
|
|
106
|
+
changed = true;
|
|
107
|
+
delete store[key];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (changed) {
|
|
112
|
+
persist();
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
if (locks) {
|
|
117
|
+
locks.request(`${storageKey}:cleanup`, { ifAvailable: true }, cleanup);
|
|
118
|
+
} else {
|
|
119
|
+
cleanup(true);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
get(key) {
|
|
125
|
+
read();
|
|
126
|
+
|
|
127
|
+
const item: SchemaItem<Schema[N]['value']> = store[key];
|
|
128
|
+
if (!item) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const expiresAt = item.expiresAt;
|
|
133
|
+
if (expiresAt !== null && Date.now() > expiresAt) {
|
|
134
|
+
delete store[key];
|
|
135
|
+
persist();
|
|
136
|
+
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return item.value;
|
|
141
|
+
},
|
|
142
|
+
set(key, value) {
|
|
143
|
+
read();
|
|
144
|
+
|
|
145
|
+
const item: SchemaItem<Schema[N]['value']> = {
|
|
146
|
+
expiresAt: expiresAt(value),
|
|
147
|
+
value: value,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
store[key] = item;
|
|
151
|
+
persist();
|
|
152
|
+
},
|
|
153
|
+
delete(key) {
|
|
154
|
+
read();
|
|
155
|
+
|
|
156
|
+
if (store[key] !== undefined) {
|
|
157
|
+
delete store[key];
|
|
158
|
+
persist();
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
keys() {
|
|
162
|
+
read();
|
|
163
|
+
|
|
164
|
+
return Object.keys(store);
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
dispose: () => {
|
|
171
|
+
controller.abort();
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
sessions: createStore('sessions', ({ token }) => {
|
|
175
|
+
if (token.refresh) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return token.expires_at ?? null;
|
|
180
|
+
}),
|
|
181
|
+
states: createStore('states', (_item) => Date.now() + 10 * 60 * 1_000),
|
|
182
|
+
dpopNonces: createStore('dpopNonces', (_item) => Date.now() + 10 * 60 * 1_000),
|
|
183
|
+
};
|
|
184
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export interface ClientMetadata {
|
|
2
|
+
redirect_uris: string[];
|
|
3
|
+
response_types: (
|
|
4
|
+
| 'code'
|
|
5
|
+
| 'token'
|
|
6
|
+
| 'none'
|
|
7
|
+
| 'code id_token token'
|
|
8
|
+
| 'code id_token'
|
|
9
|
+
| 'code token'
|
|
10
|
+
| 'id_token token'
|
|
11
|
+
| 'id_token'
|
|
12
|
+
)[];
|
|
13
|
+
grant_types: (
|
|
14
|
+
| 'authorization_code'
|
|
15
|
+
| 'implicit'
|
|
16
|
+
| 'refresh_token'
|
|
17
|
+
| 'password'
|
|
18
|
+
| 'client_credentials'
|
|
19
|
+
| 'urn:ietf:params:oauth:grant-type:jwt-bearer'
|
|
20
|
+
| 'urn:ietf:params:oauth:grant-type:saml2-bearer'
|
|
21
|
+
)[];
|
|
22
|
+
scope?: string;
|
|
23
|
+
token_endpoint_auth_method?:
|
|
24
|
+
| 'none'
|
|
25
|
+
| 'client_secret_basic'
|
|
26
|
+
| 'client_secret_jwt'
|
|
27
|
+
| 'client_secret_post'
|
|
28
|
+
| 'private_key_jwt'
|
|
29
|
+
| 'self_signed_tls_client_auth'
|
|
30
|
+
| 'tls_client_auth';
|
|
31
|
+
token_endpoint_auth_signing_alg?: string;
|
|
32
|
+
introspection_endpoint_auth_method?:
|
|
33
|
+
| 'none'
|
|
34
|
+
| 'client_secret_basic'
|
|
35
|
+
| 'client_secret_jwt'
|
|
36
|
+
| 'client_secret_post'
|
|
37
|
+
| 'private_key_jwt'
|
|
38
|
+
| 'self_signed_tls_client_auth'
|
|
39
|
+
| 'tls_client_auth';
|
|
40
|
+
introspection_endpoint_auth_signing_alg?: string;
|
|
41
|
+
revocation_endpoint_auth_method?:
|
|
42
|
+
| 'none'
|
|
43
|
+
| 'client_secret_basic'
|
|
44
|
+
| 'client_secret_jwt'
|
|
45
|
+
| 'client_secret_post'
|
|
46
|
+
| 'private_key_jwt'
|
|
47
|
+
| 'self_signed_tls_client_auth'
|
|
48
|
+
| 'tls_client_auth';
|
|
49
|
+
revocation_endpoint_auth_signing_alg?: string;
|
|
50
|
+
pushed_authorization_request_endpoint_auth_method?:
|
|
51
|
+
| 'none'
|
|
52
|
+
| 'client_secret_basic'
|
|
53
|
+
| 'client_secret_jwt'
|
|
54
|
+
| 'client_secret_post'
|
|
55
|
+
| 'private_key_jwt'
|
|
56
|
+
| 'self_signed_tls_client_auth'
|
|
57
|
+
| 'tls_client_auth';
|
|
58
|
+
pushed_authorization_request_endpoint_auth_signing_alg?: string;
|
|
59
|
+
userinfo_signed_response_alg?: string;
|
|
60
|
+
userinfo_encrypted_response_alg?: string;
|
|
61
|
+
jwks_uri?: string;
|
|
62
|
+
jwks?: unknown;
|
|
63
|
+
application_type?: 'web' | 'native';
|
|
64
|
+
subject_type?: 'public' | 'pairwise';
|
|
65
|
+
request_object_signing_alg?: string;
|
|
66
|
+
id_token_signed_response_alg?: string;
|
|
67
|
+
authorization_signed_response_alg?: string;
|
|
68
|
+
authorization_encrypted_response_enc?: 'A128CBC-HS256';
|
|
69
|
+
authorization_encrypted_response_alg?: string;
|
|
70
|
+
client_id?: string;
|
|
71
|
+
client_name?: string;
|
|
72
|
+
client_uri?: string;
|
|
73
|
+
policy_uri?: string;
|
|
74
|
+
tos_uri?: string;
|
|
75
|
+
logo_uri?: string;
|
|
76
|
+
default_max_age?: number;
|
|
77
|
+
require_auth_time?: boolean;
|
|
78
|
+
contacts?: string[];
|
|
79
|
+
tls_client_certificate_bound_access_tokens?: boolean;
|
|
80
|
+
dpop_bound_access_tokens?: boolean;
|
|
81
|
+
authorization_details_types?: string[];
|
|
82
|
+
}
|
package/lib/types/par.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export interface ProtectedResourceMetadata {
|
|
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
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface PersistedAuthorizationServerMetadata
|
|
59
|
+
extends Pick<
|
|
60
|
+
AuthorizationServerMetadata,
|
|
61
|
+
| 'issuer'
|
|
62
|
+
| 'authorization_endpoint'
|
|
63
|
+
| 'introspection_endpoint'
|
|
64
|
+
| 'pushed_authorization_request_endpoint'
|
|
65
|
+
| 'revocation_endpoint'
|
|
66
|
+
| 'token_endpoint'
|
|
67
|
+
> {}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { At } from '@atcute/client/lexicons';
|
|
2
|
+
|
|
3
|
+
import type { DPoPKey } from './dpop.js';
|
|
4
|
+
import type { PersistedAuthorizationServerMetadata } from './server.js';
|
|
5
|
+
|
|
6
|
+
export interface OAuthTokenResponse {
|
|
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
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TokenInfo {
|
|
29
|
+
scope: string;
|
|
30
|
+
type: string;
|
|
31
|
+
expires_at?: number;
|
|
32
|
+
refresh?: string;
|
|
33
|
+
access: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ExchangeInfo {
|
|
37
|
+
sub: At.DID;
|
|
38
|
+
aud: string;
|
|
39
|
+
server: PersistedAuthorizationServerMetadata;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface Session {
|
|
43
|
+
dpopKey: DPoPKey;
|
|
44
|
+
info: ExchangeInfo;
|
|
45
|
+
token: TokenInfo;
|
|
46
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type UnwrapArray<T> = T extends (infer V)[] ? V : never;
|
|
2
|
+
|
|
3
|
+
export const pick = <T, K extends (keyof T)[]>(obj: T, keys: K): Pick<T, UnwrapArray<K>> => {
|
|
4
|
+
const cloned = {};
|
|
5
|
+
|
|
6
|
+
for (let idx = 0, len = keys.length; idx < len; idx++) {
|
|
7
|
+
const key = keys[idx];
|
|
8
|
+
|
|
9
|
+
// @ts-expect-error
|
|
10
|
+
cloned[key] = obj[key];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return cloned as Pick<T, UnwrapArray<K>>;
|
|
14
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const encoder = new TextEncoder();
|
|
2
|
+
|
|
3
|
+
export const locks = navigator.locks as LockManager | undefined;
|
|
4
|
+
|
|
5
|
+
export const toBase64Url = (input: Uint8Array): string => {
|
|
6
|
+
const CHUNK_SIZE = 0x8000;
|
|
7
|
+
const arr = [];
|
|
8
|
+
|
|
9
|
+
for (let i = 0; i < input.byteLength; i += CHUNK_SIZE) {
|
|
10
|
+
// @ts-expect-error
|
|
11
|
+
arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE)));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return btoa(arr.join('')).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const fromBase64Url = (input: string): Uint8Array => {
|
|
18
|
+
try {
|
|
19
|
+
const binary = atob(input.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, ''));
|
|
20
|
+
const bytes = new Uint8Array(binary.length);
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < binary.length; i++) {
|
|
23
|
+
bytes[i] = binary.charCodeAt(i);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return bytes;
|
|
27
|
+
} catch (err) {
|
|
28
|
+
throw new TypeError(`invalid base64url`, { cause: err });
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const toSha256 = async (input: string): Promise<string> => {
|
|
33
|
+
const bytes = encoder.encode(input);
|
|
34
|
+
const digest = await crypto.subtle.digest('SHA-256', bytes);
|
|
35
|
+
|
|
36
|
+
return toBase64Url(new Uint8Array(digest));
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const randomBytes = (length: number): string => {
|
|
40
|
+
return toBase64Url(crypto.getRandomValues(new Uint8Array(length)));
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const generateState = (): string => {
|
|
44
|
+
return randomBytes(16);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const generatePKCE = async (): Promise<{ verifier: string; challenge: string; method: string }> => {
|
|
48
|
+
const verifier = randomBytes(32);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
verifier: verifier,
|
|
52
|
+
challenge: await toSha256(verifier),
|
|
53
|
+
method: 'S256',
|
|
54
|
+
};
|
|
55
|
+
};
|