@atproto/oauth-provider 0.2.0 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
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
  }