@atproto/oauth-provider 0.2.0 → 0.2.2

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.
Files changed (170) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/account/account-store.d.ts +2 -2
  3. package/dist/assets/app/bundle-manifest.json +3 -3
  4. package/dist/assets/app/main.css +1 -1
  5. package/dist/assets/app/main.js +3 -3
  6. package/dist/assets/app/main.js.map +1 -1
  7. package/dist/assets/assets-middleware.d.ts.map +1 -1
  8. package/dist/assets/assets-middleware.js +4 -2
  9. package/dist/assets/assets-middleware.js.map +1 -1
  10. package/dist/client/client-manager.d.ts.map +1 -1
  11. package/dist/client/client-manager.js +127 -118
  12. package/dist/client/client-manager.js.map +1 -1
  13. package/dist/client/client-utils.d.ts +1 -2
  14. package/dist/client/client-utils.d.ts.map +1 -1
  15. package/dist/client/client-utils.js +3 -12
  16. package/dist/client/client-utils.js.map +1 -1
  17. package/dist/client/client.d.ts +8 -3
  18. package/dist/client/client.d.ts.map +1 -1
  19. package/dist/client/client.js +70 -1
  20. package/dist/client/client.js.map +1 -1
  21. package/dist/constants.d.ts +0 -1
  22. package/dist/constants.d.ts.map +1 -1
  23. package/dist/constants.js +1 -2
  24. package/dist/constants.js.map +1 -1
  25. package/dist/errors/access-denied-error.d.ts +4 -4
  26. package/dist/errors/access-denied-error.d.ts.map +1 -1
  27. package/dist/errors/access-denied-error.js +2 -2
  28. package/dist/errors/access-denied-error.js.map +1 -1
  29. package/dist/errors/account-selection-required-error.d.ts +2 -2
  30. package/dist/errors/account-selection-required-error.d.ts.map +1 -1
  31. package/dist/errors/account-selection-required-error.js.map +1 -1
  32. package/dist/errors/consent-required-error.d.ts +2 -2
  33. package/dist/errors/consent-required-error.d.ts.map +1 -1
  34. package/dist/errors/consent-required-error.js.map +1 -1
  35. package/dist/errors/invalid-authorization-details-error.d.ts +2 -2
  36. package/dist/errors/invalid-authorization-details-error.d.ts.map +1 -1
  37. package/dist/errors/invalid-authorization-details-error.js.map +1 -1
  38. package/dist/errors/invalid-client-id-error.d.ts +1 -1
  39. package/dist/errors/invalid-client-id-error.d.ts.map +1 -1
  40. package/dist/errors/invalid-client-id-error.js +12 -6
  41. package/dist/errors/invalid-client-id-error.js.map +1 -1
  42. package/dist/errors/invalid-client-metadata-error.d.ts +1 -1
  43. package/dist/errors/invalid-client-metadata-error.d.ts.map +1 -1
  44. package/dist/errors/invalid-client-metadata-error.js +11 -3
  45. package/dist/errors/invalid-client-metadata-error.js.map +1 -1
  46. package/dist/errors/invalid-parameters-error.d.ts +2 -2
  47. package/dist/errors/invalid-parameters-error.d.ts.map +1 -1
  48. package/dist/errors/invalid-parameters-error.js.map +1 -1
  49. package/dist/errors/invalid-scope-error.d.ts +9 -0
  50. package/dist/errors/invalid-scope-error.d.ts.map +1 -0
  51. package/dist/errors/invalid-scope-error.js +14 -0
  52. package/dist/errors/invalid-scope-error.js.map +1 -0
  53. package/dist/errors/login-required-error.d.ts +2 -2
  54. package/dist/errors/login-required-error.d.ts.map +1 -1
  55. package/dist/errors/login-required-error.js.map +1 -1
  56. package/dist/lib/html/html.d.ts +1 -1
  57. package/dist/lib/html/html.d.ts.map +1 -1
  58. package/dist/lib/html/html.js +14 -11
  59. package/dist/lib/html/html.js.map +1 -1
  60. package/dist/lib/http/parser.d.ts +9 -2
  61. package/dist/lib/http/parser.d.ts.map +1 -1
  62. package/dist/lib/http/parser.js +15 -7
  63. package/dist/lib/http/parser.js.map +1 -1
  64. package/dist/lib/http/request.d.ts +0 -23
  65. package/dist/lib/http/request.d.ts.map +1 -1
  66. package/dist/lib/http/request.js +1 -11
  67. package/dist/lib/http/request.js.map +1 -1
  68. package/dist/lib/http/stream.d.ts +28 -6
  69. package/dist/lib/http/stream.d.ts.map +1 -1
  70. package/dist/lib/http/stream.js +21 -32
  71. package/dist/lib/http/stream.js.map +1 -1
  72. package/dist/lib/util/authorization-header.d.ts.map +1 -1
  73. package/dist/lib/util/authorization-header.js +1 -1
  74. package/dist/lib/util/authorization-header.js.map +1 -1
  75. package/dist/lib/util/hostname.d.ts +3 -2
  76. package/dist/lib/util/hostname.d.ts.map +1 -1
  77. package/dist/lib/util/hostname.js +12 -8
  78. package/dist/lib/util/hostname.js.map +1 -1
  79. package/dist/metadata/build-metadata.d.ts.map +1 -1
  80. package/dist/metadata/build-metadata.js +2 -1
  81. package/dist/metadata/build-metadata.js.map +1 -1
  82. package/dist/oauth-errors.d.ts +1 -0
  83. package/dist/oauth-errors.d.ts.map +1 -1
  84. package/dist/oauth-errors.js +3 -1
  85. package/dist/oauth-errors.js.map +1 -1
  86. package/dist/oauth-hooks.d.ts +3 -3
  87. package/dist/oauth-hooks.d.ts.map +1 -1
  88. package/dist/oauth-provider.d.ts +20 -22
  89. package/dist/oauth-provider.d.ts.map +1 -1
  90. package/dist/oauth-provider.js +234 -176
  91. package/dist/oauth-provider.js.map +1 -1
  92. package/dist/oauth-verifier.d.ts +2 -2
  93. package/dist/oauth-verifier.d.ts.map +1 -1
  94. package/dist/oauth-verifier.js.map +1 -1
  95. package/dist/output/build-authorize-data.d.ts +2 -2
  96. package/dist/output/build-authorize-data.d.ts.map +1 -1
  97. package/dist/output/send-authorize-redirect.d.ts +2 -4
  98. package/dist/output/send-authorize-redirect.d.ts.map +1 -1
  99. package/dist/output/send-authorize-redirect.js +5 -2
  100. package/dist/output/send-authorize-redirect.js.map +1 -1
  101. package/dist/request/request-data.d.ts +2 -2
  102. package/dist/request/request-data.d.ts.map +1 -1
  103. package/dist/request/request-info.d.ts +2 -2
  104. package/dist/request/request-info.d.ts.map +1 -1
  105. package/dist/request/request-manager.d.ts +4 -4
  106. package/dist/request/request-manager.d.ts.map +1 -1
  107. package/dist/request/request-manager.js +94 -60
  108. package/dist/request/request-manager.js.map +1 -1
  109. package/dist/signer/signed-token-payload.d.ts +122 -122
  110. package/dist/signer/signer.d.ts +41 -40
  111. package/dist/signer/signer.d.ts.map +1 -1
  112. package/dist/signer/signer.js +13 -15
  113. package/dist/signer/signer.js.map +1 -1
  114. package/dist/token/token-claims.d.ts +121 -121
  115. package/dist/token/token-data.d.ts +3 -3
  116. package/dist/token/token-data.d.ts.map +1 -1
  117. package/dist/token/token-manager.d.ts +4 -5
  118. package/dist/token/token-manager.d.ts.map +1 -1
  119. package/dist/token/token-manager.js +96 -72
  120. package/dist/token/token-manager.js.map +1 -1
  121. package/dist/token/verify-token-claims.d.ts +3 -3
  122. package/dist/token/verify-token-claims.d.ts.map +1 -1
  123. package/dist/token/verify-token-claims.js.map +1 -1
  124. package/package.json +7 -6
  125. package/src/assets/app/components/sign-in-form.tsx +31 -2
  126. package/src/assets/app/components/url-viewer.tsx +3 -3
  127. package/src/assets/assets-middleware.ts +4 -2
  128. package/src/client/client-manager.ts +163 -161
  129. package/src/client/client-utils.ts +7 -12
  130. package/src/client/client.ts +112 -3
  131. package/src/constants.ts +0 -2
  132. package/src/errors/access-denied-error.ts +10 -4
  133. package/src/errors/account-selection-required-error.ts +2 -2
  134. package/src/errors/consent-required-error.ts +2 -2
  135. package/src/errors/invalid-authorization-details-error.ts +2 -2
  136. package/src/errors/invalid-client-id-error.ts +15 -4
  137. package/src/errors/invalid-client-metadata-error.ts +15 -3
  138. package/src/errors/invalid-parameters-error.ts +2 -2
  139. package/src/errors/invalid-scope-error.ts +15 -0
  140. package/src/errors/login-required-error.ts +2 -2
  141. package/src/lib/html/html.ts +14 -12
  142. package/src/lib/http/parser.ts +21 -8
  143. package/src/lib/http/request.ts +1 -23
  144. package/src/lib/http/stream.ts +29 -60
  145. package/src/lib/util/authorization-header.ts +5 -2
  146. package/src/lib/util/hostname.ts +9 -5
  147. package/src/metadata/build-metadata.ts +3 -1
  148. package/src/oauth-errors.ts +1 -0
  149. package/src/oauth-hooks.ts +3 -3
  150. package/src/oauth-provider.ts +368 -269
  151. package/src/oauth-verifier.ts +2 -2
  152. package/src/output/build-authorize-data.ts +2 -2
  153. package/src/output/send-authorize-redirect.ts +7 -6
  154. package/src/request/request-data.ts +2 -2
  155. package/src/request/request-info.ts +2 -2
  156. package/src/request/request-manager.ts +129 -103
  157. package/src/signer/signer.ts +24 -25
  158. package/src/token/token-data.ts +3 -3
  159. package/src/token/token-manager.ts +141 -99
  160. package/src/token/verify-token-claims.ts +3 -3
  161. package/dist/request/types.d.ts +0 -328
  162. package/dist/request/types.d.ts.map +0 -1
  163. package/dist/request/types.js +0 -27
  164. package/dist/request/types.js.map +0 -1
  165. package/dist/token/types.d.ts +0 -250
  166. package/dist/token/types.d.ts.map +0 -1
  167. package/dist/token/types.js +0 -36
  168. package/dist/token/types.js.map +0 -1
  169. package/src/request/types.ts +0 -48
  170. package/src/token/types.ts +0 -86
@@ -1,6 +1,6 @@
1
1
  import { Key, Keyset, isSignedJwt } from '@atproto/jwk'
2
2
  import {
3
- AccessToken,
3
+ OAuthAccessToken,
4
4
  OAuthTokenType,
5
5
  oauthIssuerIdentifierSchema,
6
6
  } from '@atproto/oauth-types'
@@ -154,7 +154,7 @@ export class OAuthVerifier {
154
154
 
155
155
  protected async authenticateToken(
156
156
  tokenType: OAuthTokenType,
157
- token: AccessToken,
157
+ token: OAuthAccessToken,
158
158
  dpopJkt: string | null,
159
159
  verifyOptions?: VerifyTokenClaimsOptions,
160
160
  ): Promise<VerifyTokenClaimsResult> {
@@ -1,5 +1,5 @@
1
1
  import {
2
- OAuthAuthenticationRequestParameters,
2
+ OAuthAuthorizationRequestParameters,
3
3
  OAuthClientMetadata,
4
4
  } from '@atproto/oauth-types'
5
5
 
@@ -16,7 +16,7 @@ export type ScopeDetail = {
16
16
  export type AuthorizationResultAuthorize = {
17
17
  issuer: string
18
18
  client: Client
19
- parameters: OAuthAuthenticationRequestParameters
19
+ parameters: OAuthAuthorizationRequestParameters
20
20
  authorize: {
21
21
  uri: RequestUri
22
22
  scopeDetails?: ScopeDetail[]
@@ -1,10 +1,10 @@
1
1
  import {
2
- OAuthAuthenticationRequestParameters,
2
+ OAuthAuthorizationRequestParameters,
3
3
  OAuthTokenType,
4
4
  } from '@atproto/oauth-types'
5
5
  import { ServerResponse } from 'node:http'
6
6
 
7
- import { Client } from '../client/client.js'
7
+ import { InvalidRequestError } from '../errors/invalid-request-error.js'
8
8
  import { html, js } from '../lib/html/index.js'
9
9
  import { Code } from '../request/code.js'
10
10
  import { sendWebPage } from './send-web-page.js'
@@ -35,8 +35,7 @@ export type AuthorizationResponseParameters = {
35
35
 
36
36
  export type AuthorizationResultRedirect = {
37
37
  issuer: string
38
- client: Client
39
- parameters: OAuthAuthenticationRequestParameters
38
+ parameters: OAuthAuthorizationRequestParameters
40
39
  redirect: AuthorizationResponseParameters
41
40
  }
42
41
 
@@ -44,9 +43,11 @@ export async function sendAuthorizeRedirect(
44
43
  res: ServerResponse,
45
44
  result: AuthorizationResultRedirect,
46
45
  ): Promise<void> {
47
- const { issuer, parameters, redirect, client } = result
46
+ const { issuer, parameters, redirect } = result
47
+
48
+ const uri = parameters.redirect_uri
49
+ if (!uri) throw new InvalidRequestError('No redirect_uri')
48
50
 
49
- const uri = parameters.redirect_uri || client.metadata.redirect_uris[0]
50
51
  const mode = parameters.response_mode || 'query' // @TODO: default should depend on response_type
51
52
 
52
53
  const entries: [string, string][] = Object.entries({
@@ -1,4 +1,4 @@
1
- import { OAuthAuthenticationRequestParameters } from '@atproto/oauth-types'
1
+ import { OAuthAuthorizationRequestParameters } from '@atproto/oauth-types'
2
2
 
3
3
  import { ClientAuth } from '../client/client-auth.js'
4
4
  import { ClientId } from '../client/client-id.js'
@@ -9,7 +9,7 @@ import { Code } from './code.js'
9
9
  export type RequestData = {
10
10
  clientId: ClientId
11
11
  clientAuth: ClientAuth
12
- parameters: Readonly<OAuthAuthenticationRequestParameters>
12
+ parameters: Readonly<OAuthAuthorizationRequestParameters>
13
13
  expiresAt: Date
14
14
  deviceId: DeviceId | null
15
15
  sub: Sub | null
@@ -1,4 +1,4 @@
1
- import { OAuthAuthenticationRequestParameters } from '@atproto/oauth-types'
1
+ import { OAuthAuthorizationRequestParameters } from '@atproto/oauth-types'
2
2
  import { ClientId } from '../client/client-id.js'
3
3
  import { ClientAuth } from '../client/client-auth.js'
4
4
  import { RequestId } from './request-id.js'
@@ -7,7 +7,7 @@ import { RequestUri } from './request-uri.js'
7
7
  export type RequestInfo = {
8
8
  id: RequestId
9
9
  uri: RequestUri
10
- parameters: Readonly<OAuthAuthenticationRequestParameters>
10
+ parameters: Readonly<OAuthAuthorizationRequestParameters>
11
11
  expiresAt: Date
12
12
  clientId: ClientId
13
13
  clientAuth: ClientAuth
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  CLIENT_ASSERTION_TYPE_JWT_BEARER,
3
- OAuthAuthenticationRequestParameters,
3
+ OAuthAuthorizationRequestParameters,
4
4
  OAuthAuthorizationServerMetadata,
5
5
  } from '@atproto/oauth-types'
6
6
 
@@ -20,7 +20,6 @@ import { InvalidAuthorizationDetailsError } from '../errors/invalid-authorizatio
20
20
  import { InvalidGrantError } from '../errors/invalid-grant-error.js'
21
21
  import { InvalidParametersError } from '../errors/invalid-parameters-error.js'
22
22
  import { InvalidRequestError } from '../errors/invalid-request-error.js'
23
- import { compareRedirectUri } from '../lib/util/redirect-uri.js'
24
23
  import { OAuthHooks } from '../oauth-hooks.js'
25
24
  import { Signer } from '../signer/signer.js'
26
25
  import { Code, generateCode } from './code.js'
@@ -36,6 +35,7 @@ import {
36
35
  encodeRequestUri,
37
36
  RequestUri,
38
37
  } from './request-uri.js'
38
+ import { InvalidScopeError } from '../errors/invalid-scope-error.js'
39
39
 
40
40
  export class RequestManager {
41
41
  constructor(
@@ -53,7 +53,7 @@ export class RequestManager {
53
53
  async createAuthorizationRequest(
54
54
  client: Client,
55
55
  clientAuth: ClientAuth,
56
- input: Readonly<OAuthAuthenticationRequestParameters>,
56
+ input: Readonly<OAuthAuthorizationRequestParameters>,
57
57
  deviceId: null | DeviceId,
58
58
  dpopJkt: null | string,
59
59
  ): Promise<RequestInfo> {
@@ -64,7 +64,7 @@ export class RequestManager {
64
64
  protected async create(
65
65
  client: Client,
66
66
  clientAuth: ClientAuth,
67
- parameters: Readonly<OAuthAuthenticationRequestParameters>,
67
+ parameters: Readonly<OAuthAuthorizationRequestParameters>,
68
68
  deviceId: null | DeviceId = null,
69
69
  ): Promise<RequestInfo> {
70
70
  const expiresAt = new Date(Date.now() + PAR_EXPIRES_IN)
@@ -84,19 +84,23 @@ export class RequestManager {
84
84
  return { id, uri, expiresAt, parameters, clientId: client.id, clientAuth }
85
85
  }
86
86
 
87
- async validate(
87
+ protected async validate(
88
88
  client: Client,
89
89
  clientAuth: ClientAuth,
90
- parameters: Readonly<OAuthAuthenticationRequestParameters>,
91
- dpopJkt: null | string,
92
- ): Promise<Readonly<OAuthAuthenticationRequestParameters>> {
90
+ parameters: Readonly<OAuthAuthorizationRequestParameters>,
91
+ dpop_jkt: null | string,
92
+ ): Promise<Readonly<OAuthAuthorizationRequestParameters>> {
93
+ // -------------------------------
94
+ // Validate unsupported parameters
95
+ // -------------------------------
96
+
93
97
  for (const k of [
94
98
  // Known unsupported OIDC parameters
95
99
  'claims',
96
100
  'id_token_hint',
97
101
  'nonce', // note that OIDC "nonce" is redundant with PKCE
98
102
  ] as const) {
99
- if (parameters[k]) {
103
+ if (parameters[k] !== undefined) {
100
104
  throw new InvalidParametersError(
101
105
  parameters,
102
106
  `Unsupported "${k}" parameter`,
@@ -104,73 +108,48 @@ export class RequestManager {
104
108
  }
105
109
  }
106
110
 
107
- if (parameters.response_type !== 'code') {
108
- throw new InvalidParametersError(
109
- parameters,
110
- 'Only "code" response type is allowed',
111
- )
112
- }
111
+ // -----------------------
112
+ // Validate against server
113
+ // -----------------------
113
114
 
114
- // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-10#section-1.4.1
115
- // > The authorization server MAY fully or partially ignore the scope
116
- // > requested by the client, based on the authorization server policy or
117
- // > the resource owner's instructions. If the issued access token scope is
118
- // > different from the one requested by the client, the authorization
119
- // > server MUST include the scope response parameter in the token response
120
- // > (Section 3.2.3) to inform the client of the actual scope granted.
121
-
122
- const cScopes = client.metadata.scope?.split(' ').filter(Boolean)
123
- const sScopes = this.metadata.scopes_supported
124
-
125
- const scopes = new Set(
126
- parameters.scope?.split(' ').filter(Boolean) || cScopes,
127
- )
128
-
129
- if (scopes.has('openid')) {
130
- throw new InvalidParametersError(
115
+ if (
116
+ !this.metadata.response_types_supported?.includes(
117
+ parameters.response_type,
118
+ )
119
+ ) {
120
+ throw new AccessDeniedError(
131
121
  parameters,
132
- 'OpenID Connect is not supported',
122
+ `Unsupported response_type "${parameters.response_type}"`,
123
+ 'unsupported_response_type',
133
124
  )
134
125
  }
135
126
 
136
- if (!scopes.has('atproto')) {
137
- throw new InvalidParametersError(
127
+ if (
128
+ parameters.response_type === 'code' &&
129
+ !this.metadata.grant_types_supported?.includes('authorization_code')
130
+ ) {
131
+ throw new AccessDeniedError(
138
132
  parameters,
139
- 'The "atproto" scope is required',
133
+ `Unsupported grant_type "authorization_code"`,
134
+ 'unsupported_grant_type',
140
135
  )
141
136
  }
142
137
 
143
- for (const scope of scopes) {
144
- // Loopback clients do not define any scope in their metadata
145
- if (cScopes && !cScopes.includes(scope)) {
146
- throw new InvalidParametersError(
147
- parameters,
148
- `Scope "${scope}" is not registered for this client`,
149
- )
150
- }
151
-
152
- // Currently, the implementation requires all the scopes to be statically
153
- // defined in the server metadata. In the future, we might add support
154
- // for dynamic scopes.
155
- if (!sScopes?.includes(scope)) {
156
- throw new InvalidParametersError(
157
- parameters,
158
- `Scope "${scope}" is not supported by this server`,
159
- )
138
+ if (parameters.scope) {
139
+ for (const scope of parameters.scope.split(' ')) {
140
+ // Currently, the implementation requires all the scopes to be statically
141
+ // defined in the server metadata. In the future, we might add support
142
+ // for dynamic scopes.
143
+ if (!this.metadata.scopes_supported?.includes(scope)) {
144
+ throw new InvalidParametersError(
145
+ parameters,
146
+ `Scope "${scope}" is not supported by this server`,
147
+ )
148
+ }
160
149
  }
161
150
  }
162
151
 
163
- parameters = { ...parameters, scope: [...scopes].join(' ') || undefined }
164
-
165
152
  if (parameters.authorization_details) {
166
- const clientAuthDetailsTypes = client.metadata.authorization_details_types
167
- if (!clientAuthDetailsTypes) {
168
- throw new InvalidAuthorizationDetailsError(
169
- parameters,
170
- 'Client Metadata does not declare any "authorization_details"',
171
- )
172
- }
173
-
174
153
  for (const detail of parameters.authorization_details) {
175
154
  if (
176
155
  !this.metadata.authorization_details_types_supported?.includes(
@@ -182,35 +161,45 @@ export class RequestManager {
182
161
  `Unsupported "authorization_details" type "${detail.type}"`,
183
162
  )
184
163
  }
185
- if (!clientAuthDetailsTypes?.includes(detail.type)) {
186
- throw new InvalidAuthorizationDetailsError(
187
- parameters,
188
- `Client Metadata does not declare any "authorization_details" of type "${detail.type}"`,
189
- )
190
- }
191
164
  }
192
165
  }
193
166
 
194
- const { redirect_uri } = parameters
195
- if (
196
- redirect_uri &&
197
- !client.metadata.redirect_uris.some((uri) =>
198
- compareRedirectUri(uri, redirect_uri),
199
- )
200
- ) {
201
- throw new InvalidParametersError(
202
- parameters,
203
- `Invalid redirect_uri ${redirect_uri} (allowed: ${client.metadata.redirect_uris.join(' ')})`,
204
- )
167
+ // -----------------------
168
+ // Validate against client
169
+ // -----------------------
170
+
171
+ parameters = client.validateRequest(parameters)
172
+
173
+ // -------------------
174
+ // Validate parameters
175
+ // -------------------
176
+
177
+ if (!parameters.redirect_uri) {
178
+ // Should already be ensured by client.validateRequest(). Adding here for
179
+ // clarity & extra safety.
180
+ throw new InvalidParametersError(parameters, 'Missing "redirect_uri"')
205
181
  }
206
182
 
183
+ // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-10#section-1.4.1
184
+ // > The authorization server MAY fully or partially ignore the scope
185
+ // > requested by the client, based on the authorization server policy or
186
+ // > the resource owner's instructions. If the issued access token scope is
187
+ // > different from the one requested by the client, the authorization
188
+ // > server MUST include the scope response parameter in the token response
189
+ // > (Section 3.2.3) to inform the client of the actual scope granted.
190
+
191
+ // Let's make sure the scopes are unique (to reduce the token & storage size)
192
+ const scopes = new Set(parameters.scope?.split(' '))
193
+
194
+ parameters = { ...parameters, scope: [...scopes].join(' ') || undefined }
195
+
207
196
  // https://datatracker.ietf.org/doc/html/rfc9449#section-10
208
197
  if (!parameters.dpop_jkt) {
209
- if (dpopJkt) parameters = { ...parameters, dpop_jkt: dpopJkt }
210
- } else if (parameters.dpop_jkt !== dpopJkt) {
198
+ if (dpop_jkt) parameters = { ...parameters, dpop_jkt }
199
+ } else if (parameters.dpop_jkt !== dpop_jkt) {
211
200
  throw new InvalidParametersError(
212
201
  parameters,
213
- 'DPoP header and dpop_jkt do not match',
202
+ '"dpop_jkt" parameters does not match the DPoP proof',
214
203
  )
215
204
  }
216
205
 
@@ -223,40 +212,77 @@ export class RequestManager {
223
212
  }
224
213
  }
225
214
 
226
- if (!client.metadata.response_types.includes(parameters.response_type)) {
215
+ if (parameters.code_challenge) {
216
+ switch (parameters.code_challenge_method) {
217
+ case undefined:
218
+ // https://datatracker.ietf.org/doc/html/rfc7636#section-4.3
219
+ parameters = { ...parameters, code_challenge_method: 'plain' }
220
+ // falls through
221
+ case 'plain':
222
+ case 'S256':
223
+ break
224
+ default: {
225
+ throw new InvalidParametersError(
226
+ parameters,
227
+ `Unsupported code_challenge_method "${parameters.code_challenge_method}"`,
228
+ )
229
+ }
230
+ }
231
+ } else {
232
+ if (parameters.code_challenge_method) {
233
+ // https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1
234
+ throw new InvalidParametersError(
235
+ parameters,
236
+ 'code_challenge is required when code_challenge_method is provided',
237
+ )
238
+ }
239
+
240
+ // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-4.1.2.1
241
+ //
242
+ // > An AS MUST reject requests without a code_challenge from public
243
+ // > clients, and MUST reject such requests from other clients unless
244
+ // > there is reasonable assurance that the client mitigates
245
+ // > authorization code injection in other ways. See Section 7.5.1 for
246
+ // > details.
247
+ //
248
+ // > [...] In the specific deployment and the specific request, there is
249
+ // > reasonable assurance by the authorization server that the client
250
+ // > implements the OpenID Connect nonce mechanism properly.
251
+ //
252
+ // atproto does not implement the OpenID Connect nonce mechanism, so we
253
+ // require the use of PKCE for all clients.
254
+
255
+ throw new InvalidParametersError(parameters, 'Use of PKCE is required')
256
+ }
257
+
258
+ // -----------------
259
+ // atproto extension
260
+ // -----------------
261
+
262
+ if (parameters.response_type !== 'code') {
227
263
  throw new InvalidParametersError(
228
264
  parameters,
229
- `Unsupported response_type "${parameters.response_type}"`,
230
- 'unsupported_response_type',
265
+ 'atproto only supports the "code" response_type',
231
266
  )
232
267
  }
233
268
 
234
- // https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1
235
- // PKCE is mandatory
236
- if (!parameters.code_challenge) {
237
- throw new InvalidParametersError(parameters, 'code_challenge is required')
238
- }
239
-
240
- if (
241
- parameters.code_challenge &&
242
- clientAuth.method === 'none' &&
243
- (parameters.code_challenge_method ?? 'plain') === 'plain'
244
- ) {
245
- throw new InvalidParametersError(
269
+ if (!scopes.has('atproto')) {
270
+ throw new InvalidScopeError(parameters, 'The "atproto" scope is required')
271
+ } else if (scopes.has('openid')) {
272
+ throw new InvalidScopeError(
246
273
  parameters,
247
- 'code_challenge_method=plain requires client authentication',
274
+ 'OpenID Connect is not compatible with atproto',
248
275
  )
249
276
  }
250
277
 
251
- // https://datatracker.ietf.org/doc/html/rfc7636#section-4.3
252
- if (parameters.code_challenge_method && !parameters.code_challenge) {
278
+ if (parameters.code_challenge_method !== 'S256') {
253
279
  throw new InvalidParametersError(
254
280
  parameters,
255
- 'code_challenge_method requires code_challenge',
281
+ 'atproto requires use of "S256" code_challenge_method',
256
282
  )
257
283
  }
258
284
 
259
- // ATPROTO extension: if the client is not trusted, and not authenticated,
285
+ // atproto extension: if the client is not trusted, and not authenticated,
260
286
  // force users to consent to authorization requests. We do this to avoid
261
287
  // unauthenticated clients from being able to silently re-authenticate
262
288
  // users.
@@ -7,11 +7,10 @@ import {
7
7
  VerifyOptions,
8
8
  } from '@atproto/jwk'
9
9
  import {
10
- OAuthAuthenticationRequestParameters,
10
+ OAuthAuthorizationRequestParameters,
11
11
  OAuthAuthorizationDetails,
12
12
  } from '@atproto/oauth-types'
13
13
 
14
- import { Account } from '../account/account.js'
15
14
  import { Client } from '../client/client.js'
16
15
  import { dateToEpoch } from '../lib/util/date.js'
17
16
  import { TokenId } from '../token/token-id.js'
@@ -52,9 +51,10 @@ export class Signer {
52
51
 
53
52
  async accessToken(
54
53
  client: Client,
55
- parameters: OAuthAuthenticationRequestParameters,
56
- account: Account,
57
- extra: {
54
+ parameters: OAuthAuthorizationRequestParameters,
55
+ options: {
56
+ aud: string | [string, ...string[]]
57
+ sub: string
58
58
  jti: TokenId
59
59
  exp: Date
60
60
  iat?: Date
@@ -63,26 +63,25 @@ export class Signer {
63
63
  authorization_details?: OAuthAuthorizationDetails
64
64
  },
65
65
  ): Promise<SignedJwt> {
66
- const header: JwtSignHeader = {
67
- // https://datatracker.ietf.org/doc/html/rfc9068#section-2.1
68
- alg: extra.alg,
69
- typ: 'at+jwt',
70
- }
71
-
72
- const payload: Omit<SignedTokenPayload, 'iss'> = {
73
- aud: account.aud,
74
- iat: dateToEpoch(extra?.iat),
75
- exp: dateToEpoch(extra.exp),
76
- sub: account.sub,
77
- jti: extra.jti,
78
- cnf: extra.cnf,
79
- // https://datatracker.ietf.org/doc/html/rfc8693#section-4.3
80
- client_id: client.id,
81
- scope: parameters.scope || client.metadata.scope,
82
- authorization_details: extra.authorization_details,
83
- }
84
-
85
- return this.sign(header, payload)
66
+ return this.sign(
67
+ {
68
+ // https://datatracker.ietf.org/doc/html/rfc9068#section-2.1
69
+ alg: options.alg,
70
+ typ: 'at+jwt',
71
+ },
72
+ {
73
+ aud: options.aud,
74
+ iat: dateToEpoch(options?.iat),
75
+ exp: dateToEpoch(options.exp),
76
+ sub: options.sub,
77
+ jti: options.jti,
78
+ cnf: options.cnf,
79
+ // https://datatracker.ietf.org/doc/html/rfc8693#section-4.3
80
+ client_id: client.id,
81
+ scope: parameters.scope,
82
+ authorization_details: options.authorization_details,
83
+ },
84
+ )
86
85
  }
87
86
 
88
87
  async verifyAccessToken(token: SignedJwt) {
@@ -1,6 +1,6 @@
1
1
  import {
2
- OAuthAuthenticationRequestParameters,
3
2
  OAuthAuthorizationDetails,
3
+ OAuthAuthorizationRequestParameters,
4
4
  } from '@atproto/oauth-types'
5
5
 
6
6
  import { ClientAuth } from '../client/client-auth.js'
@@ -14,8 +14,8 @@ export type {
14
14
  ClientId,
15
15
  Code,
16
16
  DeviceId,
17
- OAuthAuthenticationRequestParameters,
18
17
  OAuthAuthorizationDetails,
18
+ OAuthAuthorizationRequestParameters,
19
19
  Sub,
20
20
  }
21
21
 
@@ -27,7 +27,7 @@ export type TokenData = {
27
27
  clientAuth: ClientAuth
28
28
  deviceId: DeviceId | null
29
29
  sub: Sub
30
- parameters: OAuthAuthenticationRequestParameters
30
+ parameters: OAuthAuthorizationRequestParameters
31
31
  details: OAuthAuthorizationDetails | null
32
32
  code: Code | null
33
33
  }