@atcute/oauth-browser-client 1.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/LICENSE +17 -0
- package/README.md +159 -0
- package/dist/agents/exchange.d.ts +20 -0
- package/dist/agents/exchange.js +87 -0
- package/dist/agents/exchange.js.map +1 -0
- package/dist/agents/server-agent.d.ts +22 -0
- package/dist/agents/server-agent.js +119 -0
- package/dist/agents/server-agent.js.map +1 -0
- package/dist/agents/sessions.d.ts +11 -0
- package/dist/agents/sessions.js +107 -0
- package/dist/agents/sessions.js.map +1 -0
- package/dist/agents/user-agent.d.ts +13 -0
- package/dist/agents/user-agent.js +77 -0
- package/dist/agents/user-agent.js.map +1 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +2 -0
- package/dist/constants.js.map +1 -0
- package/dist/dpop.d.ts +4 -0
- package/dist/dpop.js +118 -0
- package/dist/dpop.js.map +1 -0
- package/dist/environment.d.ts +19 -0
- package/dist/environment.js +9 -0
- package/dist/environment.js.map +1 -0
- package/dist/errors.d.ts +31 -0
- package/dist/errors.js +59 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/resolvers.d.ts +52 -0
- package/dist/resolvers.js +186 -0
- package/dist/resolvers.js.map +1 -0
- package/dist/store/db.d.ts +18 -0
- package/dist/store/db.js +106 -0
- package/dist/store/db.js.map +1 -0
- package/dist/types/client.d.ts +37 -0
- package/dist/types/client.js +2 -0
- package/dist/types/client.js.map +1 -0
- package/dist/types/dpop.d.ts +7 -0
- package/dist/types/dpop.js +2 -0
- package/dist/types/dpop.js.map +1 -0
- package/dist/types/identity.d.ts +6 -0
- package/dist/types/identity.js +2 -0
- package/dist/types/identity.js.map +1 -0
- package/dist/types/par.d.ts +4 -0
- package/dist/types/par.js +2 -0
- package/dist/types/par.js.map +1 -0
- package/dist/types/server.d.ts +57 -0
- package/dist/types/server.js +2 -0
- package/dist/types/server.js.map +1 -0
- package/dist/types/store.d.ts +6 -0
- package/dist/types/store.js +2 -0
- package/dist/types/store.js.map +1 -0
- package/dist/types/token.d.ts +38 -0
- package/dist/types/token.js +2 -0
- package/dist/types/token.js.map +1 -0
- package/dist/utils/misc.d.ts +3 -0
- package/dist/utils/misc.js +10 -0
- package/dist/utils/misc.js.map +1 -0
- package/dist/utils/response.d.ts +1 -0
- package/dist/utils/response.js +4 -0
- package/dist/utils/response.js.map +1 -0
- package/dist/utils/runtime.d.ts +12 -0
- package/dist/utils/runtime.js +44 -0
- package/dist/utils/runtime.js.map +1 -0
- package/dist/utils/strings.d.ts +2 -0
- package/dist/utils/strings.js +4 -0
- package/dist/utils/strings.js.map +1 -0
- package/package.json +29 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
2
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
3
|
+
in the Software without restriction, including without limitation the rights
|
|
4
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
5
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
6
|
+
furnished to do so, subject to the following conditions:
|
|
7
|
+
|
|
8
|
+
The above copyright notice and this permission notice shall be included in all
|
|
9
|
+
copies or substantial portions of the Software.
|
|
10
|
+
|
|
11
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
12
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
13
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
14
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
15
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
16
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
17
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# @atcute/oauth-browser-client
|
|
2
|
+
|
|
3
|
+
minimal OAuth browser client implementation for AT Protocol.
|
|
4
|
+
|
|
5
|
+
- **only the bare minimum**: enough code to get authentication reasonably working, with only one
|
|
6
|
+
happy path is supported (only ES256 keys for DPoP. PKCE and DPoP-bound PAR is required.)
|
|
7
|
+
- **does not use IndexedDB**: makes the library work under Safari's lockdown mode, and has less
|
|
8
|
+
[maintenance headache][indexeddb-woes] overall, but it also means this is "less secure" (it won't
|
|
9
|
+
be able to use non-exportable keys as recommended by [DPoP specification][idb-dpop-spec].)
|
|
10
|
+
- **no independent DNS/HTTP handle checks**: the default handle resolver makes use of Bluesky's
|
|
11
|
+
AppView to retrieve the correct DID identifier. you should be able to write your own resolver
|
|
12
|
+
function that'll resolve via DNS-over-HTTPS or via other PDSes.
|
|
13
|
+
- **not well-tested**: it has been used in personal projects for quite some time, but hasn't seen
|
|
14
|
+
any use outside of that. using the [reference implementation][oauth-atproto-lib] is recommended if
|
|
15
|
+
you are unsure about the implications presented here.
|
|
16
|
+
|
|
17
|
+
[indexeddb-woes]: https://gist.github.com/pesterhazy/4de96193af89a6dd5ce682ce2adff49a
|
|
18
|
+
[idb-dpop-spec]: https://datatracker.ietf.org/doc/html/rfc9449#section-2-4
|
|
19
|
+
[oauth-atproto-lib]: https://npm.im/@atproto/oauth-client-browser
|
|
20
|
+
|
|
21
|
+
## usage
|
|
22
|
+
|
|
23
|
+
### setup
|
|
24
|
+
|
|
25
|
+
initialize the client by importing and calling `configureOAuth` with the client ID and redirect URL.
|
|
26
|
+
this call should be placed before any other calls you make with this library.
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { configureOAuth } from '@atcute/oauth-browser-client';
|
|
30
|
+
|
|
31
|
+
configureOAuth({
|
|
32
|
+
metadata: {
|
|
33
|
+
client_id: 'https://example.com/oauth/client-metadata.json',
|
|
34
|
+
redirect_uri: 'https://example.com/oauth/callback',
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### starting an authorization flow
|
|
40
|
+
|
|
41
|
+
if your application involves asking for the user's handle or DID, you can use `resolveFromIdentity`
|
|
42
|
+
which resolves the user's identity to get its PDS, and the metadata of its authorization server.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { resolveFromIdentity } from '@atcute/oauth-browser-client';
|
|
46
|
+
|
|
47
|
+
const { identity, metadata } = await resolveFromIdentity('mary.my.id');
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
alternatively, if it involves asking for the user's PDS, then you can use `resolveFromService` which
|
|
51
|
+
just grabs the authorization server metadata.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { resolveFromService } from '@atcute/oauth-browser-client';
|
|
55
|
+
|
|
56
|
+
const { metadata } = await resolveFromService('bsky.social');
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
we can then proceed with authorization by calling `createAuthorizationUrl` with the resolved
|
|
60
|
+
`metadata` (and `identity`, if using `resolveFromIdentity`) along with the scope of the
|
|
61
|
+
authorization, which should either match the one in your client metadata, or a reduced set of it.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { createAuthorizationUrl } from '@atcute/oauth-browser-client';
|
|
65
|
+
|
|
66
|
+
// passing `identity` is optional,
|
|
67
|
+
// it allows for the login form to be autofilled with the user's handle or DID
|
|
68
|
+
const authUrl = await createAuthorizationUrl({
|
|
69
|
+
metadata: metadata,
|
|
70
|
+
identity: identity,
|
|
71
|
+
scope: 'atproto transition:generic transition:chat.bsky',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// recommended to wait for the browser to persist local storage before proceeding
|
|
75
|
+
await sleep(200);
|
|
76
|
+
|
|
77
|
+
// redirect the user to sign in and authorize the app
|
|
78
|
+
window.location.assign(authUrl);
|
|
79
|
+
|
|
80
|
+
// if this is on an async function, ideally the function should never ever resolve.
|
|
81
|
+
// the only way it should resolve at this point is if the user aborted the authorization
|
|
82
|
+
// by returning back to this page (thanks to back-forward page caching)
|
|
83
|
+
await new Promise((_resolve, reject) => {
|
|
84
|
+
const listener = () => {
|
|
85
|
+
reject(new Error(`user aborted the login request`));
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
window.addEventListener('pageshow', listener, { once: true });
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### finalizing authorization
|
|
93
|
+
|
|
94
|
+
once the user has been redirected to your redirect URL, we can call `finalizeAuthorization` with the
|
|
95
|
+
parameters that have been provided.
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { XRPC } from '@atcute/client';
|
|
99
|
+
import { OAuthUserAgent, finalizeAuthorization } from '@atcute/oauth-browser-client';
|
|
100
|
+
|
|
101
|
+
// `createAuthorizationUrl` asks for the server to redirect here with the
|
|
102
|
+
// parameters assigned in the hash, not the search string.
|
|
103
|
+
const params = new URLSearchParams(location.hash.slice(1));
|
|
104
|
+
|
|
105
|
+
// this is optional, but after retrieving the parameters, we should ideally
|
|
106
|
+
// scrub it from history to prevent this authorization state to be replayed,
|
|
107
|
+
// just for good measure.
|
|
108
|
+
history.replaceState(null, '', location.pathname + location.search);
|
|
109
|
+
|
|
110
|
+
// you'd be given a session object that you can then pass to OAuthUserAgent!
|
|
111
|
+
const session = await finalizeAuthorization(params);
|
|
112
|
+
|
|
113
|
+
// now you can start making requests!
|
|
114
|
+
const agent = new OAuthUserAgent(session);
|
|
115
|
+
|
|
116
|
+
// pass it onto the XRPC so you can make RPC calls with the PDS.
|
|
117
|
+
const rpc = new XRPC({ handler: agent });
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
the `session` object returned by `finalizeAuthorization` should not be stored anywhere else, as it
|
|
121
|
+
is already persisted in the internal database. you are expected to keep track of who's signed in and
|
|
122
|
+
who was last signed in for your own UI, as the sessions stored by the database is not guaranteed to
|
|
123
|
+
be permanent (mostly if they don't come with a refresh token.)
|
|
124
|
+
|
|
125
|
+
### resuming existing sessions
|
|
126
|
+
|
|
127
|
+
you can resume existing sessions by calling `getSession` with the DID identifier you intend to
|
|
128
|
+
resume.
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { XRPC } from '@atcute/client';
|
|
132
|
+
import { OAuthUserAgent, getSession } from '@atcute/oauth-browser-client';
|
|
133
|
+
|
|
134
|
+
const session = await getSession('did:plc:ia76kvnndjutgedggx2ibrem', { allowStale: true });
|
|
135
|
+
|
|
136
|
+
const agent = new OAuthUserAgent(session);
|
|
137
|
+
const rpc = new XRPC({ handler: agent });
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### removing sessions
|
|
141
|
+
|
|
142
|
+
you can manually remove sessions via `deleteStoredSession`, but ideally, you should revoke the token
|
|
143
|
+
first before doing so.
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { OAuthUserAgent, deleteStoredSession, getSession } from '@atcute/oauth-browser-client';
|
|
147
|
+
|
|
148
|
+
const did = 'did:plc:ia76kvnndjutgedggx2ibrem';
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const session = await getSession(did, { allowStale: true });
|
|
152
|
+
const agent = new OAuthUserAgent(session);
|
|
153
|
+
|
|
154
|
+
await agent.signOut();
|
|
155
|
+
} catch (err) {
|
|
156
|
+
// `signOut` also deletes the session, we only serve as fallback if it fails.
|
|
157
|
+
deleteStoredSession(did);
|
|
158
|
+
}
|
|
159
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { IdentityMetadata } from '../types/identity.js';
|
|
2
|
+
import type { AuthorizationServerMetadata } from '../types/server.js';
|
|
3
|
+
import type { Session } from '../types/token.js';
|
|
4
|
+
export interface AuthorizeOptions {
|
|
5
|
+
metadata: AuthorizationServerMetadata;
|
|
6
|
+
identity?: IdentityMetadata;
|
|
7
|
+
scope: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Create authentication URL for authorization
|
|
11
|
+
* @param options
|
|
12
|
+
* @returns URL to redirect the user for authorization
|
|
13
|
+
*/
|
|
14
|
+
export declare const createAuthorizationUrl: ({ metadata, identity, scope, }: AuthorizeOptions) => Promise<URL>;
|
|
15
|
+
/**
|
|
16
|
+
* Finalize authorization
|
|
17
|
+
* @param params Search params
|
|
18
|
+
* @returns Session object, which you can use to instantiate user agents
|
|
19
|
+
*/
|
|
20
|
+
export declare const finalizeAuthorization: (params: URLSearchParams) => Promise<Session>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { createES256Key } from '../dpop.js';
|
|
2
|
+
import { CLIENT_ID, database, REDIRECT_URI } from '../environment.js';
|
|
3
|
+
import { AuthorizationError, LoginError } from '../errors.js';
|
|
4
|
+
import { generatePKCE, generateState } from '../utils/runtime.js';
|
|
5
|
+
import { OAuthServerAgent } from './server-agent.js';
|
|
6
|
+
import { storeSession } from './sessions.js';
|
|
7
|
+
/**
|
|
8
|
+
* Create authentication URL for authorization
|
|
9
|
+
* @param options
|
|
10
|
+
* @returns URL to redirect the user for authorization
|
|
11
|
+
*/
|
|
12
|
+
export const createAuthorizationUrl = async ({ metadata, identity, scope, }) => {
|
|
13
|
+
const state = generateState();
|
|
14
|
+
const pkce = await generatePKCE();
|
|
15
|
+
const dpopKey = await createES256Key();
|
|
16
|
+
const params = {
|
|
17
|
+
redirect_uri: REDIRECT_URI,
|
|
18
|
+
code_challenge: pkce.challenge,
|
|
19
|
+
code_challenge_method: pkce.method,
|
|
20
|
+
state: state,
|
|
21
|
+
login_hint: identity?.raw,
|
|
22
|
+
response_mode: 'fragment',
|
|
23
|
+
response_type: 'code',
|
|
24
|
+
display: 'page',
|
|
25
|
+
// id_token_hint: undefined,
|
|
26
|
+
// max_age: undefined,
|
|
27
|
+
// prompt: undefined,
|
|
28
|
+
scope: scope,
|
|
29
|
+
// ui_locales: undefined,
|
|
30
|
+
};
|
|
31
|
+
database.states.set(state, {
|
|
32
|
+
dpopKey: dpopKey,
|
|
33
|
+
metadata: metadata,
|
|
34
|
+
verifier: pkce.verifier,
|
|
35
|
+
});
|
|
36
|
+
const server = new OAuthServerAgent(metadata, dpopKey);
|
|
37
|
+
const response = await server.request('pushed_authorization_request', params);
|
|
38
|
+
const authUrl = new URL(metadata.authorization_endpoint);
|
|
39
|
+
authUrl.searchParams.set('client_id', CLIENT_ID);
|
|
40
|
+
authUrl.searchParams.set('request_uri', response.request_uri);
|
|
41
|
+
return authUrl;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Finalize authorization
|
|
45
|
+
* @param params Search params
|
|
46
|
+
* @returns Session object, which you can use to instantiate user agents
|
|
47
|
+
*/
|
|
48
|
+
export const finalizeAuthorization = async (params) => {
|
|
49
|
+
const issuer = params.get('iss');
|
|
50
|
+
const state = params.get('state');
|
|
51
|
+
const code = params.get('code');
|
|
52
|
+
const error = params.get('error');
|
|
53
|
+
if (!state || !(code || error)) {
|
|
54
|
+
throw new LoginError(`missing parameters`);
|
|
55
|
+
}
|
|
56
|
+
const stored = database.states.get(state);
|
|
57
|
+
if (stored) {
|
|
58
|
+
// Delete now that we've caught it
|
|
59
|
+
database.states.delete(state);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
throw new LoginError(`unknown state provided`);
|
|
63
|
+
}
|
|
64
|
+
const dpopKey = stored.dpopKey;
|
|
65
|
+
const metadata = stored.metadata;
|
|
66
|
+
if (error) {
|
|
67
|
+
throw new AuthorizationError(params.get('error_description') || error);
|
|
68
|
+
}
|
|
69
|
+
if (!code) {
|
|
70
|
+
throw new LoginError(`missing code parameter`);
|
|
71
|
+
}
|
|
72
|
+
if (issuer === null) {
|
|
73
|
+
throw new LoginError(`missing issuer parameter`);
|
|
74
|
+
}
|
|
75
|
+
else if (issuer !== metadata.issuer) {
|
|
76
|
+
throw new LoginError(`issuer mismatch`);
|
|
77
|
+
}
|
|
78
|
+
// Retrieve authentication tokens
|
|
79
|
+
const server = new OAuthServerAgent(metadata, dpopKey);
|
|
80
|
+
const { info, token } = await server.exchangeCode(code, stored.verifier);
|
|
81
|
+
// We're finished!
|
|
82
|
+
const sub = info.sub;
|
|
83
|
+
const session = { dpopKey, info, token };
|
|
84
|
+
await storeSession(sub, session);
|
|
85
|
+
return session;
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=exchange.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exchange.js","sourceRoot":"","sources":["../../lib/agents/exchange.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAI9D,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAElE,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAQ7C;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,KAAK,EAAE,EAC5C,QAAQ,EACR,QAAQ,EACR,KAAK,GACa,EAAgB,EAAE;IACpC,MAAM,KAAK,GAAG,aAAa,EAAE,CAAC;IAE9B,MAAM,IAAI,GAAG,MAAM,YAAY,EAAE,CAAC;IAClC,MAAM,OAAO,GAAG,MAAM,cAAc,EAAE,CAAC;IAEvC,MAAM,MAAM,GAAG;QACd,YAAY,EAAE,YAAY;QAC1B,cAAc,EAAE,IAAI,CAAC,SAAS;QAC9B,qBAAqB,EAAE,IAAI,CAAC,MAAM;QAClC,KAAK,EAAE,KAAK;QACZ,UAAU,EAAE,QAAQ,EAAE,GAAG;QACzB,aAAa,EAAE,UAAU;QACzB,aAAa,EAAE,MAAM;QACrB,OAAO,EAAE,MAAM;QACf,4BAA4B;QAC5B,sBAAsB;QACtB,qBAAqB;QACrB,KAAK,EAAE,KAAK;QACZ,yBAAyB;KACoB,CAAC;IAE/C,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE;QAC1B,OAAO,EAAE,OAAO;QAChB,QAAQ,EAAE,QAAQ;QAClB,QAAQ,EAAE,IAAI,CAAC,QAAQ;KACvB,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,IAAI,gBAAgB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACvD,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,8BAA8B,EAAE,MAAM,CAAC,CAAC;IAE9E,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,sBAAsB,CAAC,CAAC;IACzD,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;IACjD,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,aAAa,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;IAE9D,OAAO,OAAO,CAAC;AAChB,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,EAAE,MAAuB,EAAE,EAAE;IACtE,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACjC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAClC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAChC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAElC,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,UAAU,CAAC,oBAAoB,CAAC,CAAC;IAC5C,CAAC;IAED,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC1C,IAAI,MAAM,EAAE,CAAC;QACZ,kCAAkC;QAClC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;SAAM,CAAC;QACP,MAAM,IAAI,UAAU,CAAC,wBAAwB,CAAC,CAAC;IAChD,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;IAC/B,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;IAEjC,IAAI,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,kBAAkB,CAAC,MAAM,CAAC,GAAG,CAAC,mBAAmB,CAAC,IAAI,KAAK,CAAC,CAAC;IACxE,CAAC;IACD,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,MAAM,IAAI,UAAU,CAAC,wBAAwB,CAAC,CAAC;IAChD,CAAC;IAED,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACrB,MAAM,IAAI,UAAU,CAAC,0BAA0B,CAAC,CAAC;IAClD,CAAC;SAAM,IAAI,MAAM,KAAK,QAAQ,CAAC,MAAM,EAAE,CAAC;QACvC,MAAM,IAAI,UAAU,CAAC,iBAAiB,CAAC,CAAC;IACzC,CAAC;IAED,iCAAiC;IACjC,MAAM,MAAM,GAAG,IAAI,gBAAgB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACvD,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IAEzE,kBAAkB;IAClB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC;IACrB,MAAM,OAAO,GAAY,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAElD,MAAM,YAAY,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAEjC,OAAO,OAAO,CAAC;AAChB,CAAC,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { At } from '@atcute/client/lexicons';
|
|
2
|
+
import type { DPoPKey } from '../types/dpop.js';
|
|
3
|
+
import type { OAuthParResponse } from '../types/par.js';
|
|
4
|
+
import type { PersistedAuthorizationServerMetadata } from '../types/server.js';
|
|
5
|
+
import type { ExchangeInfo, OAuthTokenResponse, TokenInfo } from '../types/token.js';
|
|
6
|
+
export declare class OAuthServerAgent {
|
|
7
|
+
#private;
|
|
8
|
+
constructor(metadata: PersistedAuthorizationServerMetadata, dpopKey: DPoPKey);
|
|
9
|
+
request(endpoint: 'pushed_authorization_request', payload: Record<string, unknown>): Promise<OAuthParResponse>;
|
|
10
|
+
request(endpoint: 'token', payload: Record<string, unknown>): Promise<OAuthTokenResponse>;
|
|
11
|
+
request(endpoint: 'revocation', payload: Record<string, unknown>): Promise<any>;
|
|
12
|
+
request(endpoint: 'introspection', payload: Record<string, unknown>): Promise<any>;
|
|
13
|
+
revoke(token: string): Promise<void>;
|
|
14
|
+
exchangeCode(code: string, verifier?: string): Promise<{
|
|
15
|
+
info: ExchangeInfo;
|
|
16
|
+
token: TokenInfo;
|
|
17
|
+
}>;
|
|
18
|
+
refresh({ sub, token }: {
|
|
19
|
+
sub: At.DID;
|
|
20
|
+
token: TokenInfo;
|
|
21
|
+
}): Promise<TokenInfo>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { createDPoPFetch } from '../dpop.js';
|
|
2
|
+
import { CLIENT_ID, REDIRECT_URI } from '../environment.js';
|
|
3
|
+
import { FetchResponseError, OAuthResponseError, TokenRefreshError } from '../errors.js';
|
|
4
|
+
import { resolveFromIdentity } from '../resolvers.js';
|
|
5
|
+
import { pick } from '../utils/misc.js';
|
|
6
|
+
import { extractContentType } from '../utils/response.js';
|
|
7
|
+
export class OAuthServerAgent {
|
|
8
|
+
#fetch;
|
|
9
|
+
#metadata;
|
|
10
|
+
constructor(metadata, dpopKey) {
|
|
11
|
+
this.#metadata = metadata;
|
|
12
|
+
this.#fetch = createDPoPFetch(CLIENT_ID, dpopKey, true);
|
|
13
|
+
}
|
|
14
|
+
async request(endpoint, payload) {
|
|
15
|
+
const url = this.#metadata[`${endpoint}_endpoint`];
|
|
16
|
+
if (!url) {
|
|
17
|
+
throw new Error(`no endpoint for ${endpoint}`);
|
|
18
|
+
}
|
|
19
|
+
const response = await this.#fetch(url, {
|
|
20
|
+
method: 'post',
|
|
21
|
+
headers: { 'content-type': 'application/json' },
|
|
22
|
+
body: JSON.stringify({ ...payload, client_id: CLIENT_ID }),
|
|
23
|
+
});
|
|
24
|
+
if (extractContentType(response.headers) !== 'application/json') {
|
|
25
|
+
throw new FetchResponseError(response, 2, `unexpected content-type`);
|
|
26
|
+
}
|
|
27
|
+
const json = await response.json();
|
|
28
|
+
if (response.ok) {
|
|
29
|
+
return json;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
throw new OAuthResponseError(response, json);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async revoke(token) {
|
|
36
|
+
try {
|
|
37
|
+
await this.request('revocation', { token: token });
|
|
38
|
+
}
|
|
39
|
+
catch { }
|
|
40
|
+
}
|
|
41
|
+
async exchangeCode(code, verifier) {
|
|
42
|
+
const response = await this.request('token', {
|
|
43
|
+
grant_type: 'authorization_code',
|
|
44
|
+
redirect_uri: REDIRECT_URI,
|
|
45
|
+
code: code,
|
|
46
|
+
code_verifier: verifier,
|
|
47
|
+
});
|
|
48
|
+
try {
|
|
49
|
+
return await this.#processExchangeResponse(response);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
await this.revoke(response.access_token);
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async refresh({ sub, token }) {
|
|
57
|
+
if (!token.refresh) {
|
|
58
|
+
throw new TokenRefreshError(sub, 'no refresh token available');
|
|
59
|
+
}
|
|
60
|
+
const response = await this.request('token', {
|
|
61
|
+
grant_type: 'refresh_token',
|
|
62
|
+
refresh_token: token.refresh,
|
|
63
|
+
});
|
|
64
|
+
try {
|
|
65
|
+
if (sub !== response.sub) {
|
|
66
|
+
throw new TokenRefreshError(sub, `sub mismatch in token response; got ${response.sub}`);
|
|
67
|
+
}
|
|
68
|
+
return this.#processTokenResponse(response);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
await this.revoke(response.access_token);
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
#processTokenResponse(res) {
|
|
76
|
+
const sub = res.sub;
|
|
77
|
+
const scope = res.scope;
|
|
78
|
+
if (!sub) {
|
|
79
|
+
throw new TypeError(`missing sub field in token response`);
|
|
80
|
+
}
|
|
81
|
+
if (!scope) {
|
|
82
|
+
throw new TypeError(`missing scope field in token response`);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
scope: scope,
|
|
86
|
+
refresh: res.refresh_token,
|
|
87
|
+
access: res.access_token,
|
|
88
|
+
type: res.token_type ?? 'Bearer',
|
|
89
|
+
expires_at: typeof res.expires_in === 'number' ? Date.now() + res.expires_in * 1_000 : undefined,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async #processExchangeResponse(res) {
|
|
93
|
+
const sub = res.sub;
|
|
94
|
+
if (!sub) {
|
|
95
|
+
throw new TypeError(`missing sub field in token response`);
|
|
96
|
+
}
|
|
97
|
+
const token = this.#processTokenResponse(res);
|
|
98
|
+
const resolved = await resolveFromIdentity(sub);
|
|
99
|
+
if (resolved.metadata.issuer !== this.#metadata.issuer) {
|
|
100
|
+
throw new TypeError(`issuer mismatch; got ${resolved.metadata.issuer}`);
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
token: token,
|
|
104
|
+
info: {
|
|
105
|
+
sub: sub,
|
|
106
|
+
aud: resolved.identity.pds.href,
|
|
107
|
+
server: pick(resolved.metadata, [
|
|
108
|
+
'issuer',
|
|
109
|
+
'authorization_endpoint',
|
|
110
|
+
'introspection_endpoint',
|
|
111
|
+
'pushed_authorization_request_endpoint',
|
|
112
|
+
'revocation_endpoint',
|
|
113
|
+
'token_endpoint',
|
|
114
|
+
]),
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=server-agent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-agent.js","sourceRoot":"","sources":["../../lib/agents/server-agent.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { At } from '@atcute/client/lexicons';
|
|
2
|
+
import type { Session } from '../types/token.js';
|
|
3
|
+
export interface SessionGetOptions {
|
|
4
|
+
signal?: AbortSignal;
|
|
5
|
+
noCache?: boolean;
|
|
6
|
+
allowStale?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare const getSession: (sub: At.DID, options?: SessionGetOptions) => Promise<Session>;
|
|
9
|
+
export declare const storeSession: (sub: At.DID, newSession: Session) => Promise<void>;
|
|
10
|
+
export declare const deleteStoredSession: (sub: At.DID) => void;
|
|
11
|
+
export declare const listStoredSessions: () => At.DID[];
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { database } from '../environment.js';
|
|
2
|
+
import { OAuthResponseError, TokenRefreshError } from '../errors.js';
|
|
3
|
+
import { OAuthServerAgent } from './server-agent.js';
|
|
4
|
+
const pending = new Map();
|
|
5
|
+
export const getSession = async (sub, options) => {
|
|
6
|
+
options?.signal?.throwIfAborted();
|
|
7
|
+
let allowStored = isTokenUsable;
|
|
8
|
+
if (options?.noCache) {
|
|
9
|
+
allowStored = returnFalse;
|
|
10
|
+
}
|
|
11
|
+
else if (options?.allowStale) {
|
|
12
|
+
allowStored = returnTrue;
|
|
13
|
+
}
|
|
14
|
+
// As long as concurrent requests are made for the same key, only one
|
|
15
|
+
// request will be made to the cache & getter function at a time. This works
|
|
16
|
+
// because there is no async operation between the while() loop and the
|
|
17
|
+
// pending.set() call. Because of the "single threaded" nature of
|
|
18
|
+
// JavaScript, the pending item will be set before the next iteration of the
|
|
19
|
+
// while loop.
|
|
20
|
+
let previousExecutionFlow;
|
|
21
|
+
while ((previousExecutionFlow = pending.get(sub))) {
|
|
22
|
+
try {
|
|
23
|
+
const { isFresh, value } = await previousExecutionFlow;
|
|
24
|
+
if (isFresh || allowStored(value)) {
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Ignore errors from previous execution flows (they will have been
|
|
30
|
+
// propagated by that flow).
|
|
31
|
+
}
|
|
32
|
+
options?.signal?.throwIfAborted();
|
|
33
|
+
}
|
|
34
|
+
const lockKey = `atcute-oauth:${sub}`;
|
|
35
|
+
let promise;
|
|
36
|
+
promise = navigator.locks.request(lockKey, async () => {
|
|
37
|
+
const storedSession = database.sessions.get(sub);
|
|
38
|
+
console.log(storedSession, allowStored);
|
|
39
|
+
if (storedSession && allowStored(storedSession)) {
|
|
40
|
+
console.log('true');
|
|
41
|
+
// Use the stored value as return value for the current execution
|
|
42
|
+
// flow. Notify other concurrent execution flows (that should be
|
|
43
|
+
// "stuck" in the loop before until this promise resolves) that we got
|
|
44
|
+
// a value, but that it came from the store (isFresh = false).
|
|
45
|
+
return { isFresh: false, value: storedSession };
|
|
46
|
+
}
|
|
47
|
+
console.log('false');
|
|
48
|
+
const newSession = await refreshToken(sub, storedSession);
|
|
49
|
+
await storeSession(sub, newSession);
|
|
50
|
+
return { isFresh: true, value: newSession };
|
|
51
|
+
});
|
|
52
|
+
promise = promise.finally(() => pending.delete(sub));
|
|
53
|
+
if (pending.has(sub)) {
|
|
54
|
+
// This should never happen. Indeed, there must not be any 'await'
|
|
55
|
+
// statement between this and the loop iteration check meaning that
|
|
56
|
+
// this.pending.get returned undefined. It is there to catch bugs that
|
|
57
|
+
// would occur in future changes to the code.
|
|
58
|
+
throw new Error('concurrent request for the same key');
|
|
59
|
+
}
|
|
60
|
+
pending.set(sub, promise);
|
|
61
|
+
const { value } = await promise;
|
|
62
|
+
return value;
|
|
63
|
+
};
|
|
64
|
+
export const storeSession = async (sub, newSession) => {
|
|
65
|
+
try {
|
|
66
|
+
database.sessions.set(sub, newSession);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
await onRefreshError(newSession);
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
export const deleteStoredSession = (sub) => {
|
|
74
|
+
database.sessions.delete(sub);
|
|
75
|
+
};
|
|
76
|
+
export const listStoredSessions = () => {
|
|
77
|
+
return database.sessions.keys();
|
|
78
|
+
};
|
|
79
|
+
const returnTrue = () => true;
|
|
80
|
+
const returnFalse = () => false;
|
|
81
|
+
const refreshToken = async (sub, storedSession) => {
|
|
82
|
+
if (storedSession === undefined) {
|
|
83
|
+
throw new TokenRefreshError(sub, `session deleted by another tab`);
|
|
84
|
+
}
|
|
85
|
+
const { dpopKey, info, token } = storedSession;
|
|
86
|
+
const server = new OAuthServerAgent(info.server, dpopKey);
|
|
87
|
+
try {
|
|
88
|
+
const newToken = await server.refresh({ sub: info.sub, token });
|
|
89
|
+
return { dpopKey, info, token: newToken };
|
|
90
|
+
}
|
|
91
|
+
catch (cause) {
|
|
92
|
+
if (cause instanceof OAuthResponseError && cause.status === 400 && cause.error === 'invalid_grant') {
|
|
93
|
+
throw new TokenRefreshError(sub, `session was revoked`, { cause });
|
|
94
|
+
}
|
|
95
|
+
throw cause;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const onRefreshError = async ({ dpopKey, info, token }) => {
|
|
99
|
+
// If the token data cannot be stored, let's revoke it
|
|
100
|
+
const server = new OAuthServerAgent(info.server, dpopKey);
|
|
101
|
+
await server.revoke(token.refresh ?? token.access);
|
|
102
|
+
};
|
|
103
|
+
const isTokenUsable = ({ token }) => {
|
|
104
|
+
const expires = token.expires_at;
|
|
105
|
+
return expires == null || Date.now() + 60_000 <= expires;
|
|
106
|
+
};
|
|
107
|
+
//# sourceMappingURL=sessions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessions.js","sourceRoot":"","sources":["../../lib/agents/sessions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FetchHandlerObject } from '@atcute/client';
|
|
2
|
+
import type { At } from '@atcute/client/lexicons';
|
|
3
|
+
import type { Session } from '../types/token.js';
|
|
4
|
+
import { type SessionGetOptions } from './sessions.js';
|
|
5
|
+
export declare class OAuthUserAgent implements FetchHandlerObject {
|
|
6
|
+
#private;
|
|
7
|
+
session: Session;
|
|
8
|
+
constructor(session: Session);
|
|
9
|
+
get sub(): At.DID;
|
|
10
|
+
getSession(options?: SessionGetOptions): Promise<Session>;
|
|
11
|
+
signOut(): Promise<void>;
|
|
12
|
+
handle(pathname: string, init?: RequestInit): Promise<Response>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createDPoPFetch } from '../dpop.js';
|
|
2
|
+
import { CLIENT_ID } from '../environment.js';
|
|
3
|
+
import { OAuthServerAgent } from './server-agent.js';
|
|
4
|
+
import { deleteStoredSession, getSession } from './sessions.js';
|
|
5
|
+
export class OAuthUserAgent {
|
|
6
|
+
session;
|
|
7
|
+
#fetch;
|
|
8
|
+
#getSessionPromise;
|
|
9
|
+
constructor(session) {
|
|
10
|
+
this.session = session;
|
|
11
|
+
this.#fetch = createDPoPFetch(CLIENT_ID, session.dpopKey, false);
|
|
12
|
+
}
|
|
13
|
+
get sub() {
|
|
14
|
+
return this.session.info.sub;
|
|
15
|
+
}
|
|
16
|
+
getSession(options) {
|
|
17
|
+
const promise = getSession(this.session.info.sub, options);
|
|
18
|
+
promise
|
|
19
|
+
.then((session) => {
|
|
20
|
+
this.session = session;
|
|
21
|
+
})
|
|
22
|
+
.finally(() => {
|
|
23
|
+
this.#getSessionPromise = undefined;
|
|
24
|
+
});
|
|
25
|
+
return (this.#getSessionPromise = promise);
|
|
26
|
+
}
|
|
27
|
+
async signOut() {
|
|
28
|
+
const sub = this.session.info.sub;
|
|
29
|
+
try {
|
|
30
|
+
const { dpopKey, info, token } = await getSession(sub, { allowStale: true });
|
|
31
|
+
const server = new OAuthServerAgent(info.server, dpopKey);
|
|
32
|
+
await server.revoke(token.refresh ?? token.access);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
deleteStoredSession(sub);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async handle(pathname, init) {
|
|
39
|
+
await this.#getSessionPromise;
|
|
40
|
+
const headers = new Headers(init?.headers);
|
|
41
|
+
let session = this.session;
|
|
42
|
+
let url = new URL(pathname, session.info.aud);
|
|
43
|
+
headers.set('authorization', `${session.token.type} ${session.token.access}`);
|
|
44
|
+
let response = await this.#fetch(url, { ...init, headers });
|
|
45
|
+
if (!isInvalidTokenResponse(response)) {
|
|
46
|
+
return response;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
if (this.#getSessionPromise) {
|
|
50
|
+
session = await this.#getSessionPromise;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
session = await this.getSession();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return response;
|
|
58
|
+
}
|
|
59
|
+
// Stream already consumed, can't retry.
|
|
60
|
+
if (init?.body instanceof ReadableStream) {
|
|
61
|
+
return response;
|
|
62
|
+
}
|
|
63
|
+
url = new URL(pathname, session.info.aud);
|
|
64
|
+
headers.set('authorization', `${session.token.type} ${session.token.access}`);
|
|
65
|
+
return await this.#fetch(url, { ...init, headers });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const isInvalidTokenResponse = (response) => {
|
|
69
|
+
if (response.status !== 401) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const auth = response.headers.get('www-authenticate');
|
|
73
|
+
return (auth != null &&
|
|
74
|
+
(auth.startsWith('Bearer ') || auth.startsWith('DPoP ')) &&
|
|
75
|
+
auth.includes('error="invalid_token"'));
|
|
76
|
+
};
|
|
77
|
+
//# sourceMappingURL=user-agent.js.map
|