@atproto/oauth-client 0.7.6 → 0.7.7
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/CHANGELOG.md +20 -0
- package/package.json +19 -14
- package/src/constants.ts +0 -4
- package/src/core-js.d.ts +0 -1
- package/src/errors/auth-method-unsatisfiable-error.ts +0 -1
- package/src/errors/token-invalid-error.ts +0 -9
- package/src/errors/token-refresh-error.ts +0 -9
- package/src/errors/token-revoked-error.ts +0 -9
- package/src/fetch-dpop.ts +0 -250
- package/src/identity-resolver.ts +0 -25
- package/src/index.ts +0 -32
- package/src/lock.ts +0 -33
- package/src/oauth-authorization-server-metadata-resolver.ts +0 -120
- package/src/oauth-callback-error.ts +0 -16
- package/src/oauth-client-auth.ts +0 -180
- package/src/oauth-client.ts +0 -541
- package/src/oauth-protected-resource-metadata-resolver.ts +0 -122
- package/src/oauth-resolver-error.ts +0 -21
- package/src/oauth-resolver.ts +0 -177
- package/src/oauth-response-error.ts +0 -33
- package/src/oauth-server-agent.ts +0 -289
- package/src/oauth-server-factory.ts +0 -64
- package/src/oauth-session.ts +0 -170
- package/src/runtime-implementation.ts +0 -25
- package/src/runtime.ts +0 -114
- package/src/session-getter.ts +0 -281
- package/src/state-store.ts +0 -24
- package/src/types.ts +0 -39
- package/src/util.test.ts +0 -83
- package/src/util.ts +0 -81
- package/src/validate-client-metadata.ts +0 -116
- package/tsconfig.build.json +0 -9
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -7
- package/tsconfig.tests.json +0 -8
- package/vitest.config.ts +0 -5
package/src/oauth-resolver.ts
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import { extractPdsUrl } from '@atproto/did'
|
|
2
|
-
import {
|
|
3
|
-
OAuthAuthorizationServerMetadata,
|
|
4
|
-
oauthIssuerIdentifierSchema,
|
|
5
|
-
} from '@atproto/oauth-types'
|
|
6
|
-
import {
|
|
7
|
-
IdentityInfo,
|
|
8
|
-
IdentityResolver,
|
|
9
|
-
ResolveIdentityOptions,
|
|
10
|
-
} from '@atproto-labs/identity-resolver'
|
|
11
|
-
import {
|
|
12
|
-
GetCachedOptions,
|
|
13
|
-
OAuthAuthorizationServerMetadataResolver,
|
|
14
|
-
} from './oauth-authorization-server-metadata-resolver.js'
|
|
15
|
-
import { OAuthProtectedResourceMetadataResolver } from './oauth-protected-resource-metadata-resolver.js'
|
|
16
|
-
import { OAuthResolverError } from './oauth-resolver-error.js'
|
|
17
|
-
|
|
18
|
-
export type { GetCachedOptions }
|
|
19
|
-
export type ResolveOAuthOptions = GetCachedOptions & ResolveIdentityOptions
|
|
20
|
-
|
|
21
|
-
export class OAuthResolver {
|
|
22
|
-
constructor(
|
|
23
|
-
readonly identityResolver: IdentityResolver,
|
|
24
|
-
readonly protectedResourceMetadataResolver: OAuthProtectedResourceMetadataResolver,
|
|
25
|
-
readonly authorizationServerMetadataResolver: OAuthAuthorizationServerMetadataResolver,
|
|
26
|
-
) {}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @param input - A handle, DID, PDS URL or Entryway URL
|
|
30
|
-
*/
|
|
31
|
-
public async resolve(
|
|
32
|
-
input: string,
|
|
33
|
-
options?: ResolveOAuthOptions,
|
|
34
|
-
): Promise<{
|
|
35
|
-
identityInfo?: IdentityInfo
|
|
36
|
-
metadata: OAuthAuthorizationServerMetadata
|
|
37
|
-
}> {
|
|
38
|
-
// Allow using an entryway, or PDS url, directly as login input (e.g.
|
|
39
|
-
// when the user forgot their handle, or when the handle does not
|
|
40
|
-
// resolve to a DID)
|
|
41
|
-
return /^https?:\/\//.test(input)
|
|
42
|
-
? this.resolveFromService(input, options)
|
|
43
|
-
: this.resolveFromIdentity(input, options)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* @note this method can be used to verify if a particular uri supports OAuth
|
|
48
|
-
* based sign-in (for compatibility with legacy implementation).
|
|
49
|
-
*/
|
|
50
|
-
public async resolveFromService(
|
|
51
|
-
input: string,
|
|
52
|
-
options?: ResolveOAuthOptions,
|
|
53
|
-
): Promise<{
|
|
54
|
-
metadata: OAuthAuthorizationServerMetadata
|
|
55
|
-
}> {
|
|
56
|
-
try {
|
|
57
|
-
// Assume first that input is a PDS URL (as required by ATPROTO)
|
|
58
|
-
const metadata = await this.getResourceServerMetadata(input, options)
|
|
59
|
-
return { metadata }
|
|
60
|
-
} catch (err) {
|
|
61
|
-
if (!options?.signal?.aborted && err instanceof OAuthResolverError) {
|
|
62
|
-
try {
|
|
63
|
-
// Fallback to trying to fetch as an issuer (Entryway)
|
|
64
|
-
const result = oauthIssuerIdentifierSchema.safeParse(input)
|
|
65
|
-
if (result.success) {
|
|
66
|
-
const metadata = await this.getAuthorizationServerMetadata(
|
|
67
|
-
result.data,
|
|
68
|
-
options,
|
|
69
|
-
)
|
|
70
|
-
return { metadata }
|
|
71
|
-
}
|
|
72
|
-
} catch {
|
|
73
|
-
// Fallback failed, throw original error
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
throw err
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
public async resolveFromIdentity(
|
|
82
|
-
input: string,
|
|
83
|
-
options?: ResolveOAuthOptions,
|
|
84
|
-
): Promise<{
|
|
85
|
-
identityInfo: IdentityInfo
|
|
86
|
-
metadata: OAuthAuthorizationServerMetadata
|
|
87
|
-
pds: URL
|
|
88
|
-
}> {
|
|
89
|
-
const identityInfo = await this.resolveIdentity(input, options)
|
|
90
|
-
|
|
91
|
-
options?.signal?.throwIfAborted()
|
|
92
|
-
|
|
93
|
-
const pds = extractPdsUrl(identityInfo.didDoc)
|
|
94
|
-
|
|
95
|
-
const metadata = await this.getResourceServerMetadata(pds, options)
|
|
96
|
-
|
|
97
|
-
return { identityInfo, metadata, pds }
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
public async resolveIdentity(
|
|
101
|
-
input: string,
|
|
102
|
-
options?: ResolveIdentityOptions,
|
|
103
|
-
): Promise<IdentityInfo> {
|
|
104
|
-
try {
|
|
105
|
-
return await this.identityResolver.resolve(input, options)
|
|
106
|
-
} catch (cause) {
|
|
107
|
-
throw OAuthResolverError.from(
|
|
108
|
-
cause,
|
|
109
|
-
`Failed to resolve identity: ${input}`,
|
|
110
|
-
)
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
public async getAuthorizationServerMetadata(
|
|
115
|
-
issuer: string | URL,
|
|
116
|
-
options?: GetCachedOptions,
|
|
117
|
-
): Promise<OAuthAuthorizationServerMetadata> {
|
|
118
|
-
try {
|
|
119
|
-
return await this.authorizationServerMetadataResolver.get(issuer, options)
|
|
120
|
-
} catch (cause) {
|
|
121
|
-
throw OAuthResolverError.from(
|
|
122
|
-
cause,
|
|
123
|
-
`Failed to resolve OAuth server metadata for issuer: ${issuer}`,
|
|
124
|
-
)
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
public async getResourceServerMetadata(
|
|
129
|
-
pdsUrl: string | URL,
|
|
130
|
-
options?: GetCachedOptions,
|
|
131
|
-
) {
|
|
132
|
-
try {
|
|
133
|
-
const rsMetadata = await this.protectedResourceMetadataResolver.get(
|
|
134
|
-
pdsUrl,
|
|
135
|
-
options,
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
if (!rsMetadata) {
|
|
139
|
-
return this.getAuthorizationServerMetadata(pdsUrl, options)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// ATPROTO requires one, and only one, authorization server entry
|
|
143
|
-
if (rsMetadata.authorization_servers?.length !== 1) {
|
|
144
|
-
throw new OAuthResolverError(
|
|
145
|
-
rsMetadata.authorization_servers?.length
|
|
146
|
-
? `Unable to determine authorization server for PDS: ${pdsUrl}`
|
|
147
|
-
: `No authorization servers found for PDS: ${pdsUrl}`,
|
|
148
|
-
)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const issuer = rsMetadata.authorization_servers![0]!
|
|
152
|
-
|
|
153
|
-
options?.signal?.throwIfAborted()
|
|
154
|
-
|
|
155
|
-
const asMetadata = await this.getAuthorizationServerMetadata(
|
|
156
|
-
issuer,
|
|
157
|
-
options,
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
// https://www.rfc-editor.org/rfc/rfc9728.html#section-4
|
|
161
|
-
if (asMetadata.protected_resources) {
|
|
162
|
-
if (!asMetadata.protected_resources.includes(rsMetadata.resource)) {
|
|
163
|
-
throw new OAuthResolverError(
|
|
164
|
-
`PDS "${pdsUrl}" not protected by issuer "${issuer}"`,
|
|
165
|
-
)
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return asMetadata
|
|
170
|
-
} catch (cause) {
|
|
171
|
-
throw OAuthResolverError.from(
|
|
172
|
-
cause,
|
|
173
|
-
`Failed to resolve OAuth server metadata for resource: ${pdsUrl}`,
|
|
174
|
-
)
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { Json } from '@atproto-labs/fetch'
|
|
2
|
-
import { ifString } from './util.js'
|
|
3
|
-
|
|
4
|
-
export class OAuthResponseError extends Error {
|
|
5
|
-
readonly error?: string
|
|
6
|
-
readonly errorDescription?: string
|
|
7
|
-
|
|
8
|
-
constructor(
|
|
9
|
-
public readonly response: Response,
|
|
10
|
-
public readonly payload: Json,
|
|
11
|
-
) {
|
|
12
|
-
const objPayload = typeof payload === 'object' ? payload : undefined
|
|
13
|
-
const error = ifString(objPayload?.['error'])
|
|
14
|
-
const errorDescription = ifString(objPayload?.['error_description'])
|
|
15
|
-
|
|
16
|
-
const messageError = error ? `"${error}"` : 'unknown'
|
|
17
|
-
const messageDesc = errorDescription ? `: ${errorDescription}` : ''
|
|
18
|
-
const message = `OAuth ${messageError} error${messageDesc}`
|
|
19
|
-
|
|
20
|
-
super(message)
|
|
21
|
-
|
|
22
|
-
this.error = error
|
|
23
|
-
this.errorDescription = errorDescription
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
get status() {
|
|
27
|
-
return this.response.status
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
get headers() {
|
|
31
|
-
return this.response.headers
|
|
32
|
-
}
|
|
33
|
-
}
|
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
import { AtprotoDid } from '@atproto/did'
|
|
2
|
-
import { Key, Keyset } from '@atproto/jwk'
|
|
3
|
-
import {
|
|
4
|
-
AtprotoOAuthScope,
|
|
5
|
-
AtprotoOAuthTokenResponse,
|
|
6
|
-
OAuthAuthorizationRequestPar,
|
|
7
|
-
OAuthAuthorizationServerMetadata,
|
|
8
|
-
OAuthEndpointName,
|
|
9
|
-
OAuthParResponse,
|
|
10
|
-
OAuthRedirectUri,
|
|
11
|
-
OAuthTokenRequest,
|
|
12
|
-
atprotoOAuthTokenResponseSchema,
|
|
13
|
-
oauthParResponseSchema,
|
|
14
|
-
} from '@atproto/oauth-types'
|
|
15
|
-
import { Fetch, Json, bindFetch, fetchJsonProcessor } from '@atproto-labs/fetch'
|
|
16
|
-
import { SimpleStore } from '@atproto-labs/simple-store'
|
|
17
|
-
import { TokenRefreshError } from './errors/token-refresh-error.js'
|
|
18
|
-
import { dpopFetchWrapper } from './fetch-dpop.js'
|
|
19
|
-
import {
|
|
20
|
-
ClientAuthMethod,
|
|
21
|
-
ClientCredentialsFactory,
|
|
22
|
-
createClientCredentialsFactory,
|
|
23
|
-
} from './oauth-client-auth.js'
|
|
24
|
-
import { OAuthResolver } from './oauth-resolver.js'
|
|
25
|
-
import { OAuthResponseError } from './oauth-response-error.js'
|
|
26
|
-
import { Runtime } from './runtime.js'
|
|
27
|
-
import { ClientMetadata } from './types.js'
|
|
28
|
-
import { timeoutSignal } from './util.js'
|
|
29
|
-
|
|
30
|
-
export type { AtprotoOAuthScope, AtprotoOAuthTokenResponse }
|
|
31
|
-
|
|
32
|
-
export type TokenSet = {
|
|
33
|
-
iss: string
|
|
34
|
-
sub: AtprotoDid
|
|
35
|
-
aud: string
|
|
36
|
-
scope: AtprotoOAuthScope
|
|
37
|
-
|
|
38
|
-
refresh_token?: string
|
|
39
|
-
access_token: string
|
|
40
|
-
token_type: 'DPoP'
|
|
41
|
-
/** ISO Date */
|
|
42
|
-
expires_at?: string
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export type DpopNonceCache = SimpleStore<string, string>
|
|
46
|
-
|
|
47
|
-
export class OAuthServerAgent {
|
|
48
|
-
protected dpopFetch: Fetch<unknown>
|
|
49
|
-
protected clientCredentialsFactory: ClientCredentialsFactory
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* @throws see {@link createClientCredentialsFactory}
|
|
53
|
-
*/
|
|
54
|
-
constructor(
|
|
55
|
-
readonly authMethod: ClientAuthMethod,
|
|
56
|
-
readonly dpopKey: Key,
|
|
57
|
-
readonly serverMetadata: OAuthAuthorizationServerMetadata,
|
|
58
|
-
readonly clientMetadata: ClientMetadata,
|
|
59
|
-
readonly dpopNonces: DpopNonceCache,
|
|
60
|
-
readonly oauthResolver: OAuthResolver,
|
|
61
|
-
readonly runtime: Runtime,
|
|
62
|
-
readonly keyset?: Keyset,
|
|
63
|
-
fetch?: Fetch,
|
|
64
|
-
) {
|
|
65
|
-
this.clientCredentialsFactory = createClientCredentialsFactory(
|
|
66
|
-
authMethod,
|
|
67
|
-
serverMetadata,
|
|
68
|
-
clientMetadata,
|
|
69
|
-
runtime,
|
|
70
|
-
keyset,
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
this.dpopFetch = dpopFetchWrapper<void>({
|
|
74
|
-
fetch: bindFetch(fetch),
|
|
75
|
-
key: dpopKey,
|
|
76
|
-
supportedAlgs: serverMetadata.dpop_signing_alg_values_supported,
|
|
77
|
-
sha256: async (v) => runtime.sha256(v),
|
|
78
|
-
nonces: dpopNonces,
|
|
79
|
-
isAuthServer: true,
|
|
80
|
-
})
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
get issuer() {
|
|
84
|
-
return this.serverMetadata.issuer
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async revoke(token: string) {
|
|
88
|
-
try {
|
|
89
|
-
await this.request('revocation', { token })
|
|
90
|
-
} catch {
|
|
91
|
-
// Don't care
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async exchangeCode(
|
|
96
|
-
code: string,
|
|
97
|
-
codeVerifier?: string,
|
|
98
|
-
redirectUri?: OAuthRedirectUri,
|
|
99
|
-
): Promise<TokenSet> {
|
|
100
|
-
const now = Date.now()
|
|
101
|
-
|
|
102
|
-
const tokenResponse = await this.request('token', {
|
|
103
|
-
grant_type: 'authorization_code',
|
|
104
|
-
// redirectUri should always be passed by the calling code, but if it is
|
|
105
|
-
// not, default to the first redirect_uri registered for the client:
|
|
106
|
-
redirect_uri: redirectUri ?? this.clientMetadata.redirect_uris[0],
|
|
107
|
-
code,
|
|
108
|
-
code_verifier: codeVerifier,
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
// /!\ IMPORTANT /!\
|
|
113
|
-
//
|
|
114
|
-
// The tokenResponse MUST always be valid before the "sub" it contains
|
|
115
|
-
// can be trusted (see Atproto's OAuth spec for details).
|
|
116
|
-
const aud = await this.verifyIssuer(tokenResponse.sub)
|
|
117
|
-
|
|
118
|
-
return {
|
|
119
|
-
aud,
|
|
120
|
-
sub: tokenResponse.sub,
|
|
121
|
-
iss: this.issuer,
|
|
122
|
-
|
|
123
|
-
scope: tokenResponse.scope,
|
|
124
|
-
refresh_token: tokenResponse.refresh_token,
|
|
125
|
-
access_token: tokenResponse.access_token,
|
|
126
|
-
token_type: tokenResponse.token_type,
|
|
127
|
-
|
|
128
|
-
expires_at:
|
|
129
|
-
typeof tokenResponse.expires_in === 'number'
|
|
130
|
-
? new Date(now + tokenResponse.expires_in * 1000).toISOString()
|
|
131
|
-
: undefined,
|
|
132
|
-
}
|
|
133
|
-
} catch (err) {
|
|
134
|
-
await this.revoke(tokenResponse.access_token)
|
|
135
|
-
|
|
136
|
-
throw err
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async refresh(tokenSet: TokenSet): Promise<TokenSet> {
|
|
141
|
-
if (!tokenSet.refresh_token) {
|
|
142
|
-
throw new TokenRefreshError(tokenSet.sub, 'No refresh token available')
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// /!\ IMPORTANT /!\
|
|
146
|
-
//
|
|
147
|
-
// The "sub" MUST be a DID, whose issuer authority is indeed the server we
|
|
148
|
-
// are trying to obtain credentials from. Note that we are doing this
|
|
149
|
-
// *before* we actually try to refresh the token:
|
|
150
|
-
// 1) To avoid unnecessary refresh
|
|
151
|
-
// 2) So that the refresh is the last async operation, ensuring as few
|
|
152
|
-
// async operations happen before the result gets a chance to be stored.
|
|
153
|
-
const aud = await this.verifyIssuer(tokenSet.sub)
|
|
154
|
-
|
|
155
|
-
const now = Date.now()
|
|
156
|
-
|
|
157
|
-
const tokenResponse = await this.request('token', {
|
|
158
|
-
grant_type: 'refresh_token',
|
|
159
|
-
refresh_token: tokenSet.refresh_token,
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
return {
|
|
163
|
-
aud,
|
|
164
|
-
sub: tokenSet.sub,
|
|
165
|
-
iss: this.issuer,
|
|
166
|
-
|
|
167
|
-
scope: tokenResponse.scope,
|
|
168
|
-
refresh_token: tokenResponse.refresh_token,
|
|
169
|
-
access_token: tokenResponse.access_token,
|
|
170
|
-
token_type: tokenResponse.token_type,
|
|
171
|
-
|
|
172
|
-
expires_at:
|
|
173
|
-
typeof tokenResponse.expires_in === 'number'
|
|
174
|
-
? new Date(now + tokenResponse.expires_in * 1000).toISOString()
|
|
175
|
-
: undefined,
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* VERY IMPORTANT ! Always call this to process token responses.
|
|
181
|
-
*
|
|
182
|
-
* Whenever an OAuth token response is received, we **MUST** verify that the
|
|
183
|
-
* "sub" is a DID, whose issuer authority is indeed the server we just
|
|
184
|
-
* obtained credentials from. This check is a critical step to actually be
|
|
185
|
-
* able to use the "sub" (DID) as being the actual user's identifier.
|
|
186
|
-
*
|
|
187
|
-
* @returns The user's PDS URL (the resource server for the user)
|
|
188
|
-
*/
|
|
189
|
-
protected async verifyIssuer(sub: AtprotoDid): Promise<string> {
|
|
190
|
-
const resolved = await this.oauthResolver.resolveFromIdentity(sub, {
|
|
191
|
-
noCache: true,
|
|
192
|
-
allowStale: false,
|
|
193
|
-
signal: timeoutSignal(10e3),
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
if (this.issuer !== resolved.metadata.issuer) {
|
|
197
|
-
// Best case scenario; the user switched PDS. Worst case scenario; a bad
|
|
198
|
-
// actor is trying to impersonate a user. In any case, we must not allow
|
|
199
|
-
// this token to be used.
|
|
200
|
-
throw new TypeError('Issuer mismatch')
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return resolved.pds.href
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
async request<Endpoint extends OAuthEndpointName>(
|
|
207
|
-
endpoint: Endpoint,
|
|
208
|
-
payload: Endpoint extends 'token'
|
|
209
|
-
? OAuthTokenRequest
|
|
210
|
-
: Endpoint extends 'pushed_authorization_request'
|
|
211
|
-
? OAuthAuthorizationRequestPar
|
|
212
|
-
: Record<string, unknown>,
|
|
213
|
-
): Promise<
|
|
214
|
-
Endpoint extends 'token'
|
|
215
|
-
? AtprotoOAuthTokenResponse
|
|
216
|
-
: Endpoint extends 'pushed_authorization_request'
|
|
217
|
-
? OAuthParResponse
|
|
218
|
-
: Json
|
|
219
|
-
>
|
|
220
|
-
async request(
|
|
221
|
-
endpoint: OAuthEndpointName,
|
|
222
|
-
payload: Record<string, unknown>,
|
|
223
|
-
): Promise<unknown> {
|
|
224
|
-
const url = this.serverMetadata[`${endpoint}_endpoint`]
|
|
225
|
-
if (!url) throw new Error(`No ${endpoint} endpoint available`)
|
|
226
|
-
|
|
227
|
-
const auth = await this.clientCredentialsFactory()
|
|
228
|
-
|
|
229
|
-
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13#section-3.2.2
|
|
230
|
-
// https://datatracker.ietf.org/doc/html/rfc7009#section-2.1
|
|
231
|
-
// https://datatracker.ietf.org/doc/html/rfc7662#section-2.1
|
|
232
|
-
// https://datatracker.ietf.org/doc/html/rfc9126#section-2
|
|
233
|
-
const { response, json } = await this.dpopFetch(url, {
|
|
234
|
-
method: 'POST',
|
|
235
|
-
headers: {
|
|
236
|
-
...auth.headers,
|
|
237
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
238
|
-
},
|
|
239
|
-
body: wwwFormUrlEncode({ ...payload, ...auth.payload }),
|
|
240
|
-
}).then(fetchJsonProcessor())
|
|
241
|
-
|
|
242
|
-
if (response.ok) {
|
|
243
|
-
switch (endpoint) {
|
|
244
|
-
case 'token':
|
|
245
|
-
return atprotoOAuthTokenResponseSchema.parse(json)
|
|
246
|
-
case 'pushed_authorization_request':
|
|
247
|
-
return oauthParResponseSchema.parse(json)
|
|
248
|
-
default:
|
|
249
|
-
return json
|
|
250
|
-
}
|
|
251
|
-
} else {
|
|
252
|
-
throw new OAuthResponseError(response, json)
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function wwwFormUrlEncode(payload: Record<string, undefined | unknown>) {
|
|
258
|
-
return new URLSearchParams(
|
|
259
|
-
Object.entries(payload)
|
|
260
|
-
.filter(entryHasDefinedValue)
|
|
261
|
-
.map(stringifyEntryValue),
|
|
262
|
-
).toString()
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function entryHasDefinedValue(
|
|
266
|
-
entry: [string, unknown],
|
|
267
|
-
): entry is [string, null | NonNullable<unknown>] {
|
|
268
|
-
return entry[1] !== undefined
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function stringifyEntryValue(entry: [string, unknown]): [string, string] {
|
|
272
|
-
const name = entry[0]
|
|
273
|
-
const value = entry[1]
|
|
274
|
-
|
|
275
|
-
switch (typeof value) {
|
|
276
|
-
case 'string':
|
|
277
|
-
return [name, value]
|
|
278
|
-
case 'number':
|
|
279
|
-
case 'boolean':
|
|
280
|
-
return [name, String(value)]
|
|
281
|
-
default: {
|
|
282
|
-
const enc = JSON.stringify(value)
|
|
283
|
-
if (enc === undefined) {
|
|
284
|
-
throw new Error(`Unsupported value type for ${name}: ${String(value)}`)
|
|
285
|
-
}
|
|
286
|
-
return [name, enc]
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { Key, Keyset } from '@atproto/jwk'
|
|
2
|
-
import { OAuthAuthorizationServerMetadata } from '@atproto/oauth-types'
|
|
3
|
-
import { Fetch } from '@atproto-labs/fetch'
|
|
4
|
-
import { GetCachedOptions } from './oauth-authorization-server-metadata-resolver.js'
|
|
5
|
-
import { ClientAuthMethod } from './oauth-client-auth.js'
|
|
6
|
-
import { OAuthResolver } from './oauth-resolver.js'
|
|
7
|
-
import { DpopNonceCache, OAuthServerAgent } from './oauth-server-agent.js'
|
|
8
|
-
import { Runtime } from './runtime.js'
|
|
9
|
-
import { ClientMetadata } from './types.js'
|
|
10
|
-
|
|
11
|
-
export class OAuthServerFactory {
|
|
12
|
-
constructor(
|
|
13
|
-
readonly clientMetadata: ClientMetadata,
|
|
14
|
-
readonly runtime: Runtime,
|
|
15
|
-
readonly resolver: OAuthResolver,
|
|
16
|
-
readonly fetch: Fetch,
|
|
17
|
-
readonly keyset: Keyset | undefined,
|
|
18
|
-
readonly dpopNonceCache: DpopNonceCache,
|
|
19
|
-
) {}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @param authMethod `undefined` means that we are restoring a session that
|
|
23
|
-
* was created before we started storing the `authMethod` in the session. In
|
|
24
|
-
* that case, we will use the first key from the keyset.
|
|
25
|
-
*
|
|
26
|
-
* Support for this might be removed in the future.
|
|
27
|
-
*
|
|
28
|
-
* @throws see {@link OAuthServerFactory.fromMetadata}
|
|
29
|
-
*/
|
|
30
|
-
async fromIssuer(
|
|
31
|
-
issuer: string,
|
|
32
|
-
authMethod: ClientAuthMethod,
|
|
33
|
-
dpopKey: Key,
|
|
34
|
-
options?: GetCachedOptions,
|
|
35
|
-
) {
|
|
36
|
-
const serverMetadata = await this.resolver.getAuthorizationServerMetadata(
|
|
37
|
-
issuer,
|
|
38
|
-
options,
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
return this.fromMetadata(serverMetadata, authMethod, dpopKey)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* @throws see {@link OAuthServerAgent}
|
|
46
|
-
*/
|
|
47
|
-
async fromMetadata(
|
|
48
|
-
serverMetadata: OAuthAuthorizationServerMetadata,
|
|
49
|
-
authMethod: ClientAuthMethod,
|
|
50
|
-
dpopKey: Key,
|
|
51
|
-
) {
|
|
52
|
-
return new OAuthServerAgent(
|
|
53
|
-
authMethod,
|
|
54
|
-
dpopKey,
|
|
55
|
-
serverMetadata,
|
|
56
|
-
this.clientMetadata,
|
|
57
|
-
this.dpopNonceCache,
|
|
58
|
-
this.resolver,
|
|
59
|
-
this.runtime,
|
|
60
|
-
this.keyset,
|
|
61
|
-
this.fetch,
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
}
|