@atproto/oauth-provider 0.1.2 → 0.2.0
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 +46 -0
- package/dist/account/account.d.ts +6 -2
- package/dist/account/account.d.ts.map +1 -1
- package/dist/assets/app/bundle-manifest.json +3 -3
- package/dist/assets/app/main.css +1 -1
- package/dist/assets/app/main.js +3 -3
- package/dist/assets/app/main.js.map +1 -1
- package/dist/assets/assets-middleware.d.ts +2 -1
- package/dist/assets/assets-middleware.d.ts.map +1 -1
- package/dist/assets/assets-middleware.js +7 -0
- package/dist/assets/assets-middleware.js.map +1 -1
- package/dist/client/client-manager.d.ts +4 -3
- package/dist/client/client-manager.d.ts.map +1 -1
- package/dist/client/client-manager.js +91 -77
- package/dist/client/client-manager.js.map +1 -1
- package/dist/client/client.d.ts +2 -3
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +6 -12
- package/dist/client/client.js.map +1 -1
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +3 -1
- package/dist/constants.js.map +1 -1
- package/dist/device/device-manager.d.ts +1 -1
- package/dist/device/device-manager.d.ts.map +1 -1
- package/dist/device/device-manager.js +2 -2
- package/dist/device/device-manager.js.map +1 -1
- package/dist/dpop/dpop-manager.d.ts +0 -1
- package/dist/dpop/dpop-manager.d.ts.map +1 -1
- package/dist/dpop/dpop-manager.js +1 -4
- package/dist/dpop/dpop-manager.js.map +1 -1
- package/dist/errors/invalid-authorization-details-error.d.ts +4 -3
- package/dist/errors/invalid-authorization-details-error.d.ts.map +1 -1
- package/dist/errors/invalid-authorization-details-error.js +4 -4
- package/dist/errors/invalid-authorization-details-error.js.map +1 -1
- package/dist/lib/http/parser.d.ts +13 -7
- package/dist/lib/http/parser.d.ts.map +1 -1
- package/dist/lib/http/parser.js +29 -9
- package/dist/lib/http/parser.js.map +1 -1
- package/dist/lib/http/request.d.ts +8 -5
- package/dist/lib/http/request.d.ts.map +1 -1
- package/dist/lib/http/request.js +24 -12
- package/dist/lib/http/request.js.map +1 -1
- package/dist/lib/http/stream.d.ts.map +1 -1
- package/dist/lib/http/stream.js +3 -2
- package/dist/lib/http/stream.js.map +1 -1
- package/dist/metadata/build-metadata.d.ts +0 -1
- package/dist/metadata/build-metadata.d.ts.map +1 -1
- package/dist/metadata/build-metadata.js +9 -49
- package/dist/metadata/build-metadata.js.map +1 -1
- package/dist/oauth-hooks.d.ts +3 -10
- package/dist/oauth-hooks.d.ts.map +1 -1
- package/dist/oauth-provider.d.ts +10 -15
- package/dist/oauth-provider.d.ts.map +1 -1
- package/dist/oauth-provider.js +176 -114
- package/dist/oauth-provider.js.map +1 -1
- package/dist/oauth-verifier.d.ts +1 -2
- package/dist/oauth-verifier.d.ts.map +1 -1
- package/dist/oauth-verifier.js.map +1 -1
- package/dist/output/build-authorize-data.d.ts +6 -0
- package/dist/output/build-authorize-data.d.ts.map +1 -1
- package/dist/output/build-authorize-data.js +1 -0
- package/dist/output/build-authorize-data.js.map +1 -1
- package/dist/replay/replay-manager.d.ts +1 -0
- package/dist/replay/replay-manager.d.ts.map +1 -1
- package/dist/replay/replay-manager.js +3 -0
- package/dist/replay/replay-manager.js.map +1 -1
- package/dist/replay/replay-store.d.ts +1 -1
- package/dist/request/request-info.d.ts +2 -0
- package/dist/request/request-info.d.ts.map +1 -1
- package/dist/request/request-manager.d.ts +3 -9
- package/dist/request/request-manager.d.ts.map +1 -1
- package/dist/request/request-manager.js +52 -77
- package/dist/request/request-manager.js.map +1 -1
- package/dist/request/types.d.ts +10 -10
- package/dist/signer/signed-token-payload.d.ts +88 -88
- package/dist/signer/signer.d.ts +24 -31
- package/dist/signer/signer.d.ts.map +1 -1
- package/dist/signer/signer.js +0 -40
- package/dist/signer/signer.js.map +1 -1
- package/dist/token/token-claims.d.ts +84 -84
- package/dist/token/token-manager.d.ts +1 -2
- package/dist/token/token-manager.d.ts.map +1 -1
- package/dist/token/token-manager.js +10 -37
- package/dist/token/token-manager.js.map +1 -1
- package/dist/token/types.d.ts +10 -10
- package/package.json +3 -3
- package/src/account/account.ts +11 -7
- package/src/assets/app/backend-data.ts +9 -2
- package/src/assets/app/components/accept-form.tsx +65 -51
- package/src/assets/app/components/client-name.tsx +24 -16
- package/src/assets/app/components/url-viewer.tsx +3 -3
- package/src/assets/app/views/accept-view.tsx +7 -4
- package/src/assets/app/views/authorize-view.tsx +2 -1
- package/src/assets/assets-middleware.ts +14 -2
- package/src/client/client-manager.ts +124 -120
- package/src/client/client.ts +5 -17
- package/src/constants.ts +3 -0
- package/src/device/device-manager.ts +7 -1
- package/src/dpop/dpop-manager.ts +1 -6
- package/src/errors/invalid-authorization-details-error.ts +9 -4
- package/src/lib/http/parser.ts +37 -13
- package/src/lib/http/request.ts +61 -15
- package/src/lib/http/stream.ts +5 -2
- package/src/metadata/build-metadata.ts +9 -56
- package/src/oauth-hooks.ts +3 -13
- package/src/oauth-provider.ts +187 -177
- package/src/oauth-verifier.ts +1 -2
- package/src/output/build-authorize-data.ts +8 -0
- package/src/replay/replay-manager.ts +9 -0
- package/src/replay/replay-store.ts +1 -1
- package/src/request/request-info.ts +2 -0
- package/src/request/request-manager.ts +81 -107
- package/src/signer/signer.ts +0 -63
- package/src/token/token-manager.ts +8 -41
- package/dist/oidc/claims.d.ts +0 -16
- package/dist/oidc/claims.d.ts.map +0 -1
- package/dist/oidc/claims.js +0 -29
- package/dist/oidc/claims.js.map +0 -1
- package/dist/oidc/userinfo.d.ts +0 -7
- package/dist/oidc/userinfo.d.ts.map +0 -1
- package/dist/oidc/userinfo.js +0 -3
- package/dist/oidc/userinfo.js.map +0 -1
- package/dist/parameters/claims-requested.d.ts +0 -3
- package/dist/parameters/claims-requested.d.ts.map +0 -1
- package/dist/parameters/claims-requested.js +0 -77
- package/dist/parameters/claims-requested.js.map +0 -1
- package/dist/parameters/oidc-payload.d.ts +0 -31
- package/dist/parameters/oidc-payload.d.ts.map +0 -1
- package/dist/parameters/oidc-payload.js +0 -25
- package/dist/parameters/oidc-payload.js.map +0 -1
- package/src/assets/app/components/client-identifier.tsx +0 -31
- package/src/oidc/claims.ts +0 -35
- package/src/oidc/userinfo.ts +0 -11
- package/src/parameters/claims-requested.ts +0 -106
- package/src/parameters/oidc-payload.ts +0 -28
|
@@ -8,12 +8,18 @@ import { Account } from '../account/account.js'
|
|
|
8
8
|
import { Client } from '../client/client.js'
|
|
9
9
|
import { RequestUri } from '../request/request-uri.js'
|
|
10
10
|
|
|
11
|
+
export type ScopeDetail = {
|
|
12
|
+
scope: string
|
|
13
|
+
description?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
export type AuthorizationResultAuthorize = {
|
|
12
17
|
issuer: string
|
|
13
18
|
client: Client
|
|
14
19
|
parameters: OAuthAuthenticationRequestParameters
|
|
15
20
|
authorize: {
|
|
16
21
|
uri: RequestUri
|
|
22
|
+
scopeDetails?: ScopeDetail[]
|
|
17
23
|
sessions: readonly {
|
|
18
24
|
account: Account
|
|
19
25
|
info: DeviceAccountInfo
|
|
@@ -44,6 +50,7 @@ export type AuthorizeData = {
|
|
|
44
50
|
requestUri: string
|
|
45
51
|
csrfCookie: string
|
|
46
52
|
loginHint?: string
|
|
53
|
+
scopeDetails?: ScopeDetail[]
|
|
47
54
|
newSessionsRequireConsent: boolean
|
|
48
55
|
sessions: Session[]
|
|
49
56
|
}
|
|
@@ -59,6 +66,7 @@ export function buildAuthorizeData(
|
|
|
59
66
|
csrfCookie: `csrf-${data.authorize.uri}`,
|
|
60
67
|
loginHint: data.parameters.login_hint,
|
|
61
68
|
newSessionsRequireConsent: data.parameters.prompt === 'consent',
|
|
69
|
+
scopeDetails: data.authorize.scopeDetails,
|
|
62
70
|
sessions: data.authorize.sessions.map(
|
|
63
71
|
(session): Session => ({
|
|
64
72
|
account: session.account,
|
|
@@ -2,6 +2,7 @@ import { ClientId } from '../client/client-id.js'
|
|
|
2
2
|
import {
|
|
3
3
|
CLIENT_ASSERTION_MAX_AGE,
|
|
4
4
|
DPOP_NONCE_MAX_AGE,
|
|
5
|
+
CODE_CHALLENGE_REPLAY_TIMEFRAME,
|
|
5
6
|
JAR_MAX_AGE,
|
|
6
7
|
} from '../constants.js'
|
|
7
8
|
import { ReplayStore } from './replay-store.js'
|
|
@@ -35,4 +36,12 @@ export class ReplayManager {
|
|
|
35
36
|
asTimeFrame(DPOP_NONCE_MAX_AGE),
|
|
36
37
|
)
|
|
37
38
|
}
|
|
39
|
+
|
|
40
|
+
async uniqueCodeChallenge(challenge: string): Promise<boolean> {
|
|
41
|
+
return this.replayStore.unique(
|
|
42
|
+
'CodeChallenge',
|
|
43
|
+
challenge,
|
|
44
|
+
asTimeFrame(CODE_CHALLENGE_REPLAY_TIMEFRAME),
|
|
45
|
+
)
|
|
46
|
+
}
|
|
38
47
|
}
|
|
@@ -9,7 +9,7 @@ export interface ReplayStore {
|
|
|
9
9
|
* strictly necessary for security purposes, the namespace should be used to
|
|
10
10
|
* mitigate denial of service attacks from one client to the other.
|
|
11
11
|
*
|
|
12
|
-
* @param timeFrame expressed in milliseconds.
|
|
12
|
+
* @param timeFrame expressed in milliseconds.
|
|
13
13
|
*/
|
|
14
14
|
unique(
|
|
15
15
|
namespace: string,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { OAuthAuthenticationRequestParameters } from '@atproto/oauth-types'
|
|
2
|
+
import { ClientId } from '../client/client-id.js'
|
|
2
3
|
import { ClientAuth } from '../client/client-auth.js'
|
|
3
4
|
import { RequestId } from './request-id.js'
|
|
4
5
|
import { RequestUri } from './request-uri.js'
|
|
@@ -8,5 +9,6 @@ export type RequestInfo = {
|
|
|
8
9
|
uri: RequestUri
|
|
9
10
|
parameters: Readonly<OAuthAuthenticationRequestParameters>
|
|
10
11
|
expiresAt: Date
|
|
12
|
+
clientId: ClientId
|
|
11
13
|
clientAuth: ClientAuth
|
|
12
14
|
}
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
OAuthAuthorizationServerMetadata,
|
|
5
5
|
} from '@atproto/oauth-types'
|
|
6
6
|
|
|
7
|
-
import { DeviceAccountInfo } from '../account/account-store.js'
|
|
8
7
|
import { Account } from '../account/account.js'
|
|
9
8
|
import { ClientAuth } from '../client/client-auth.js'
|
|
10
9
|
import { ClientId } from '../client/client-id.js'
|
|
@@ -17,12 +16,12 @@ import {
|
|
|
17
16
|
import { DeviceId } from '../device/device-id.js'
|
|
18
17
|
import { AccessDeniedError } from '../errors/access-denied-error.js'
|
|
19
18
|
import { ConsentRequiredError } from '../errors/consent-required-error.js'
|
|
19
|
+
import { InvalidAuthorizationDetailsError } from '../errors/invalid-authorization-details-error.js'
|
|
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
23
|
import { compareRedirectUri } from '../lib/util/redirect-uri.js'
|
|
24
24
|
import { OAuthHooks } from '../oauth-hooks.js'
|
|
25
|
-
import { OIDC_SCOPE_CLAIMS } from '../oidc/claims.js'
|
|
26
25
|
import { Signer } from '../signer/signer.js'
|
|
27
26
|
import { Code, generateCode } from './code.js'
|
|
28
27
|
import {
|
|
@@ -44,7 +43,6 @@ export class RequestManager {
|
|
|
44
43
|
protected readonly signer: Signer,
|
|
45
44
|
protected readonly metadata: OAuthAuthorizationServerMetadata,
|
|
46
45
|
protected readonly hooks: OAuthHooks,
|
|
47
|
-
protected readonly pkceRequired = true,
|
|
48
46
|
protected readonly tokenMaxAge = TOKEN_MAX_AGE,
|
|
49
47
|
) {}
|
|
50
48
|
|
|
@@ -83,7 +81,7 @@ export class RequestManager {
|
|
|
83
81
|
})
|
|
84
82
|
|
|
85
83
|
const uri = encodeRequestUri(id)
|
|
86
|
-
return { id, uri, expiresAt, parameters, clientAuth }
|
|
84
|
+
return { id, uri, expiresAt, parameters, clientId: client.id, clientAuth }
|
|
87
85
|
}
|
|
88
86
|
|
|
89
87
|
async validate(
|
|
@@ -91,8 +89,28 @@ export class RequestManager {
|
|
|
91
89
|
clientAuth: ClientAuth,
|
|
92
90
|
parameters: Readonly<OAuthAuthenticationRequestParameters>,
|
|
93
91
|
dpopJkt: null | string,
|
|
94
|
-
pkceRequired = this.pkceRequired,
|
|
95
92
|
): Promise<Readonly<OAuthAuthenticationRequestParameters>> {
|
|
93
|
+
for (const k of [
|
|
94
|
+
// Known unsupported OIDC parameters
|
|
95
|
+
'claims',
|
|
96
|
+
'id_token_hint',
|
|
97
|
+
'nonce', // note that OIDC "nonce" is redundant with PKCE
|
|
98
|
+
] as const) {
|
|
99
|
+
if (parameters[k]) {
|
|
100
|
+
throw new InvalidParametersError(
|
|
101
|
+
parameters,
|
|
102
|
+
`Unsupported "${k}" parameter`,
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (parameters.response_type !== 'code') {
|
|
108
|
+
throw new InvalidParametersError(
|
|
109
|
+
parameters,
|
|
110
|
+
'Only "code" response type is allowed',
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
96
114
|
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-10#section-1.4.1
|
|
97
115
|
// > The authorization server MAY fully or partially ignore the scope
|
|
98
116
|
// > requested by the client, based on the authorization server policy or
|
|
@@ -101,59 +119,75 @@ export class RequestManager {
|
|
|
101
119
|
// > server MUST include the scope response parameter in the token response
|
|
102
120
|
// > (Section 3.2.3) to inform the client of the actual scope granted.
|
|
103
121
|
|
|
104
|
-
const cScopes = client.metadata.scope?.split(' ')
|
|
122
|
+
const cScopes = client.metadata.scope?.split(' ').filter(Boolean)
|
|
105
123
|
const sScopes = this.metadata.scopes_supported
|
|
106
124
|
|
|
107
|
-
const scopes =
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
125
|
+
const scopes = new Set(
|
|
126
|
+
parameters.scope?.split(' ').filter(Boolean) || cScopes,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if (scopes.has('openid')) {
|
|
130
|
+
throw new InvalidParametersError(
|
|
131
|
+
parameters,
|
|
132
|
+
'OpenID Connect is not supported',
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!scopes.has('atproto')) {
|
|
137
|
+
throw new InvalidParametersError(
|
|
138
|
+
parameters,
|
|
139
|
+
'The "atproto" scope is required',
|
|
140
|
+
)
|
|
141
|
+
}
|
|
111
142
|
|
|
112
143
|
for (const scope of scopes) {
|
|
113
|
-
|
|
144
|
+
// Loopback clients do not define any scope in their metadata
|
|
145
|
+
if (cScopes && !cScopes.includes(scope)) {
|
|
114
146
|
throw new InvalidParametersError(
|
|
115
147
|
parameters,
|
|
116
148
|
`Scope "${scope}" is not registered for this client`,
|
|
117
149
|
)
|
|
118
150
|
}
|
|
119
|
-
}
|
|
120
151
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
parameters,
|
|
130
|
-
`Essential ${claim} claim requires "${scope}" scope`,
|
|
131
|
-
)
|
|
132
|
-
}
|
|
133
|
-
}
|
|
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
|
+
)
|
|
134
160
|
}
|
|
135
161
|
}
|
|
136
162
|
|
|
137
|
-
parameters = { ...parameters, scope: scopes.join(' ') }
|
|
138
|
-
|
|
139
|
-
const responseTypes = parameters.response_type.split(' ')
|
|
163
|
+
parameters = { ...parameters, scope: [...scopes].join(' ') || undefined }
|
|
140
164
|
|
|
141
165
|
if (parameters.authorization_details) {
|
|
142
166
|
const clientAuthDetailsTypes = client.metadata.authorization_details_types
|
|
143
167
|
if (!clientAuthDetailsTypes) {
|
|
144
|
-
throw new
|
|
168
|
+
throw new InvalidAuthorizationDetailsError(
|
|
145
169
|
parameters,
|
|
146
170
|
'Client Metadata does not declare any "authorization_details"',
|
|
147
171
|
)
|
|
148
172
|
}
|
|
149
173
|
|
|
150
174
|
for (const detail of parameters.authorization_details) {
|
|
151
|
-
if (
|
|
152
|
-
|
|
175
|
+
if (
|
|
176
|
+
!this.metadata.authorization_details_types_supported?.includes(
|
|
177
|
+
detail.type,
|
|
178
|
+
)
|
|
179
|
+
) {
|
|
180
|
+
throw new InvalidAuthorizationDetailsError(
|
|
153
181
|
parameters,
|
|
154
182
|
`Unsupported "authorization_details" type "${detail.type}"`,
|
|
155
183
|
)
|
|
156
184
|
}
|
|
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
|
+
}
|
|
157
191
|
}
|
|
158
192
|
}
|
|
159
193
|
|
|
@@ -197,16 +231,9 @@ export class RequestManager {
|
|
|
197
231
|
)
|
|
198
232
|
}
|
|
199
233
|
|
|
200
|
-
if (pkceRequired && responseTypes.includes('token')) {
|
|
201
|
-
throw new InvalidParametersError(
|
|
202
|
-
parameters,
|
|
203
|
-
`Response type "${parameters.response_type}" is incompatible with PKCE`,
|
|
204
|
-
'unsupported_response_type',
|
|
205
|
-
)
|
|
206
|
-
}
|
|
207
|
-
|
|
208
234
|
// https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1
|
|
209
|
-
|
|
235
|
+
// PKCE is mandatory
|
|
236
|
+
if (!parameters.code_challenge) {
|
|
210
237
|
throw new InvalidParametersError(parameters, 'code_challenge is required')
|
|
211
238
|
}
|
|
212
239
|
|
|
@@ -229,50 +256,15 @@ export class RequestManager {
|
|
|
229
256
|
)
|
|
230
257
|
}
|
|
231
258
|
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
// > notes, see Section 15.5.2.
|
|
242
|
-
if (responseTypes.includes('id_token') && !parameters.nonce) {
|
|
243
|
-
throw new InvalidParametersError(
|
|
244
|
-
parameters,
|
|
245
|
-
'nonce is required for implicit and hybrid flows',
|
|
246
|
-
)
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Make "expensive" checks after the "cheaper" checks
|
|
250
|
-
|
|
251
|
-
if (parameters.id_token_hint != null) {
|
|
252
|
-
const { payload } = await this.signer.verify(parameters.id_token_hint, {
|
|
253
|
-
// these are meant to be outdated when used as a hint
|
|
254
|
-
clockTolerance: Infinity,
|
|
255
|
-
})
|
|
256
|
-
|
|
257
|
-
if (!payload.sub) {
|
|
258
|
-
throw new InvalidParametersError(
|
|
259
|
-
parameters,
|
|
260
|
-
`Unexpected empty id_token_hint "sub"`,
|
|
261
|
-
)
|
|
262
|
-
} else if (parameters.login_hint == null) {
|
|
263
|
-
parameters = { ...parameters, login_hint: payload.sub }
|
|
264
|
-
} else if (parameters.login_hint !== payload.sub) {
|
|
265
|
-
throw new InvalidParametersError(
|
|
266
|
-
parameters,
|
|
267
|
-
'login_hint does not match "sub" of id_token_hint',
|
|
268
|
-
)
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// ATPROTO extension: if the client is not trusted, force users to consent
|
|
273
|
-
// to authorization requests. We do this to avoid unauthenticated clients
|
|
274
|
-
// from being able to silently re-authenticate users.
|
|
275
|
-
if (clientAuth.method === 'none' && !client.info.isFirstParty) {
|
|
259
|
+
// ATPROTO extension: if the client is not trusted, and not authenticated,
|
|
260
|
+
// force users to consent to authorization requests. We do this to avoid
|
|
261
|
+
// unauthenticated clients from being able to silently re-authenticate
|
|
262
|
+
// users.
|
|
263
|
+
if (
|
|
264
|
+
!client.info.isTrusted &&
|
|
265
|
+
!client.info.isFirstParty &&
|
|
266
|
+
clientAuth.method === 'none'
|
|
267
|
+
) {
|
|
276
268
|
if (parameters.prompt === 'none') {
|
|
277
269
|
throw new ConsentRequiredError(
|
|
278
270
|
parameters,
|
|
@@ -346,6 +338,7 @@ export class RequestManager {
|
|
|
346
338
|
uri,
|
|
347
339
|
expiresAt: updates.expiresAt || data.expiresAt,
|
|
348
340
|
parameters: data.parameters,
|
|
341
|
+
clientId: data.clientId,
|
|
349
342
|
clientAuth: data.clientAuth,
|
|
350
343
|
}
|
|
351
344
|
}
|
|
@@ -355,8 +348,7 @@ export class RequestManager {
|
|
|
355
348
|
uri: RequestUri,
|
|
356
349
|
deviceId: DeviceId,
|
|
357
350
|
account: Account,
|
|
358
|
-
|
|
359
|
-
): Promise<{ code?: Code; token?: string; id_token?: string }> {
|
|
351
|
+
): Promise<Code> {
|
|
360
352
|
const id = decodeRequestUri(uri)
|
|
361
353
|
|
|
362
354
|
const data = await this.store.readRequest(id)
|
|
@@ -385,18 +377,8 @@ export class RequestManager {
|
|
|
385
377
|
)
|
|
386
378
|
}
|
|
387
379
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (responseType.includes('token')) {
|
|
391
|
-
throw new AccessDeniedError(
|
|
392
|
-
data.parameters,
|
|
393
|
-
'Implicit "token" forbidden (use "code" with PKCE instead)',
|
|
394
|
-
)
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const code = responseType.includes('code')
|
|
398
|
-
? await generateCode()
|
|
399
|
-
: undefined
|
|
380
|
+
// Only response_type=code is supported
|
|
381
|
+
const code = await generateCode()
|
|
400
382
|
|
|
401
383
|
// Bind the request to the account, preventing it from being used again.
|
|
402
384
|
await this.store.updateRequest(id, {
|
|
@@ -406,15 +388,7 @@ export class RequestManager {
|
|
|
406
388
|
expiresAt: new Date(Date.now() + AUTHORIZATION_INACTIVITY_TIMEOUT),
|
|
407
389
|
})
|
|
408
390
|
|
|
409
|
-
|
|
410
|
-
? await this.signer.idToken(client, data.parameters, account, {
|
|
411
|
-
auth_time: info.authenticatedAt,
|
|
412
|
-
exp: this.createTokenExpiry(),
|
|
413
|
-
code,
|
|
414
|
-
})
|
|
415
|
-
: undefined
|
|
416
|
-
|
|
417
|
-
return { code, id_token }
|
|
391
|
+
return code
|
|
418
392
|
} catch (err) {
|
|
419
393
|
await this.store.deleteRequest(id)
|
|
420
394
|
throw err
|
package/src/signer/signer.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto'
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
JwtPayload,
|
|
5
3
|
JwtPayloadGetter,
|
|
@@ -12,14 +10,10 @@ import {
|
|
|
12
10
|
OAuthAuthenticationRequestParameters,
|
|
13
11
|
OAuthAuthorizationDetails,
|
|
14
12
|
} from '@atproto/oauth-types'
|
|
15
|
-
import { generate as hash } from 'oidc-token-hash'
|
|
16
13
|
|
|
17
14
|
import { Account } from '../account/account.js'
|
|
18
15
|
import { Client } from '../client/client.js'
|
|
19
|
-
import { InvalidClientMetadataError } from '../errors/invalid-client-metadata-error.js'
|
|
20
16
|
import { dateToEpoch } from '../lib/util/date.js'
|
|
21
|
-
import { claimRequested } from '../parameters/claims-requested.js'
|
|
22
|
-
import { oidcPayload } from '../parameters/oidc-payload.js'
|
|
23
17
|
import { TokenId } from '../token/token-id.js'
|
|
24
18
|
import {
|
|
25
19
|
SignedTokenPayload,
|
|
@@ -105,61 +99,4 @@ export class Signer {
|
|
|
105
99
|
|
|
106
100
|
return result
|
|
107
101
|
}
|
|
108
|
-
|
|
109
|
-
async idToken(
|
|
110
|
-
client: Client,
|
|
111
|
-
params: OAuthAuthenticationRequestParameters,
|
|
112
|
-
account: Account,
|
|
113
|
-
extra: {
|
|
114
|
-
exp: Date
|
|
115
|
-
iat?: Date
|
|
116
|
-
auth_time?: Date
|
|
117
|
-
code?: string
|
|
118
|
-
access_token?: string
|
|
119
|
-
},
|
|
120
|
-
): Promise<SignedJwt> {
|
|
121
|
-
// This can happen when a client is using password_grant. If a client is
|
|
122
|
-
// using password_grant, it should not set "require_auth_time" to true.
|
|
123
|
-
if (client.metadata.require_auth_time && extra.auth_time == null) {
|
|
124
|
-
throw new InvalidClientMetadataError(
|
|
125
|
-
'"require_auth_time" metadata is not compatible with "password_grant" flow',
|
|
126
|
-
)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return this.sign(
|
|
130
|
-
{
|
|
131
|
-
alg: client.metadata.id_token_signed_response_alg,
|
|
132
|
-
typ: 'JWT',
|
|
133
|
-
},
|
|
134
|
-
async ({ alg }, key) => ({
|
|
135
|
-
...oidcPayload(params, account),
|
|
136
|
-
|
|
137
|
-
aud: client.id,
|
|
138
|
-
iat: dateToEpoch(extra.iat),
|
|
139
|
-
exp: dateToEpoch(extra.exp),
|
|
140
|
-
sub: account.sub,
|
|
141
|
-
jti: randomBytes(16).toString('hex'),
|
|
142
|
-
scope: params.scope,
|
|
143
|
-
nonce: params.nonce,
|
|
144
|
-
|
|
145
|
-
s_hash: params.state //
|
|
146
|
-
? await hash(params.state, alg, key.crv)
|
|
147
|
-
: undefined,
|
|
148
|
-
c_hash: extra.code //
|
|
149
|
-
? await hash(extra.code, alg, key.crv)
|
|
150
|
-
: undefined,
|
|
151
|
-
at_hash: extra.access_token //
|
|
152
|
-
? await hash(extra.access_token, alg, key.crv)
|
|
153
|
-
: undefined,
|
|
154
|
-
|
|
155
|
-
// https://openid.net/specs/openid-provider-authentication-policy-extension-1_0.html#rfc.section.5.2
|
|
156
|
-
auth_time:
|
|
157
|
-
client.metadata.require_auth_time ||
|
|
158
|
-
(extra.auth_time != null && params.max_age != null) ||
|
|
159
|
-
claimRequested(params, 'id_token', 'auth_time', extra.auth_time)
|
|
160
|
-
? dateToEpoch(extra.auth_time!)
|
|
161
|
-
: undefined,
|
|
162
|
-
}),
|
|
163
|
-
)
|
|
164
|
-
}
|
|
165
102
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isSignedJwt
|
|
1
|
+
import { isSignedJwt } from '@atproto/jwk'
|
|
2
2
|
import {
|
|
3
3
|
AccessToken,
|
|
4
4
|
CLIENT_ASSERTION_TYPE_JWT_BEARER,
|
|
@@ -140,6 +140,10 @@ export class TokenManager {
|
|
|
140
140
|
if (!('code_verifier' in input) || !input.code_verifier) {
|
|
141
141
|
throw new InvalidGrantError('code_verifier is required')
|
|
142
142
|
}
|
|
143
|
+
// Prevent client from generating too short code_verifiers
|
|
144
|
+
if (input.code_verifier.length < 43) {
|
|
145
|
+
throw new InvalidGrantError('code_verifier too short')
|
|
146
|
+
}
|
|
143
147
|
switch (parameters.code_challenge_method) {
|
|
144
148
|
case undefined: // Default is "plain" (per spec)
|
|
145
149
|
case 'plain': {
|
|
@@ -181,8 +185,7 @@ export class TokenManager {
|
|
|
181
185
|
}
|
|
182
186
|
|
|
183
187
|
const tokenId = await generateTokenId()
|
|
184
|
-
const
|
|
185
|
-
const refreshToken = scopes?.includes('offline_access')
|
|
188
|
+
const refreshToken = client.metadata.grant_types.includes('refresh_token')
|
|
186
189
|
? await generateRefreshToken()
|
|
187
190
|
: undefined
|
|
188
191
|
|
|
@@ -222,22 +225,10 @@ export class TokenManager {
|
|
|
222
225
|
authorization_details: authorizationDetails,
|
|
223
226
|
})
|
|
224
227
|
|
|
225
|
-
const idToken = scopes?.includes('openid')
|
|
226
|
-
? await this.signer.idToken(client, parameters, account, {
|
|
227
|
-
exp: expiresAt,
|
|
228
|
-
iat: now,
|
|
229
|
-
// If there is no deviceInfo, we are in a "password_grant" context
|
|
230
|
-
auth_time: device?.info.authenticatedAt || new Date(),
|
|
231
|
-
access_token: accessToken,
|
|
232
|
-
code,
|
|
233
|
-
})
|
|
234
|
-
: undefined
|
|
235
|
-
|
|
236
228
|
return this.buildTokenResponse(
|
|
237
229
|
client,
|
|
238
230
|
accessToken,
|
|
239
231
|
refreshToken,
|
|
240
|
-
idToken,
|
|
241
232
|
expiresAt,
|
|
242
233
|
parameters,
|
|
243
234
|
account,
|
|
@@ -249,7 +240,6 @@ export class TokenManager {
|
|
|
249
240
|
client: Client,
|
|
250
241
|
accessToken: AccessToken,
|
|
251
242
|
refreshToken: string | undefined,
|
|
252
|
-
idToken: SignedJwt | undefined,
|
|
253
243
|
expiresAt: Date,
|
|
254
244
|
parameters: OAuthAuthenticationRequestParameters,
|
|
255
245
|
account: Account,
|
|
@@ -259,8 +249,7 @@ export class TokenManager {
|
|
|
259
249
|
access_token: accessToken,
|
|
260
250
|
token_type: parameters.dpop_jkt ? 'DPoP' : 'Bearer',
|
|
261
251
|
refresh_token: refreshToken,
|
|
262
|
-
|
|
263
|
-
scope: parameters.scope ?? '',
|
|
252
|
+
scope: parameters.scope,
|
|
264
253
|
authorization_details: authorizationDetails,
|
|
265
254
|
get expires_in() {
|
|
266
255
|
return dateToRelativeSeconds(expiresAt)
|
|
@@ -272,12 +261,6 @@ export class TokenManager {
|
|
|
272
261
|
sub: account.sub,
|
|
273
262
|
}
|
|
274
263
|
|
|
275
|
-
await this.hooks.onTokenResponse?.call(null, tokenResponse, {
|
|
276
|
-
client,
|
|
277
|
-
parameters,
|
|
278
|
-
account,
|
|
279
|
-
})
|
|
280
|
-
|
|
281
264
|
return tokenResponse
|
|
282
265
|
}
|
|
283
266
|
|
|
@@ -316,7 +299,7 @@ export class TokenManager {
|
|
|
316
299
|
throw new InvalidGrantError(`Invalid refresh token`)
|
|
317
300
|
}
|
|
318
301
|
|
|
319
|
-
const { account,
|
|
302
|
+
const { account, data } = tokenInfo
|
|
320
303
|
const { parameters } = data
|
|
321
304
|
|
|
322
305
|
try {
|
|
@@ -400,26 +383,10 @@ export class TokenManager {
|
|
|
400
383
|
authorization_details,
|
|
401
384
|
})
|
|
402
385
|
|
|
403
|
-
// https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.1.3.3
|
|
404
|
-
//
|
|
405
|
-
// > In addition to the response parameters specified by OAuth 2.0, the
|
|
406
|
-
// > following parameters MUST be included in the response:
|
|
407
|
-
// > - id_token: ID Token value associated with the authenticated session.
|
|
408
|
-
const scopes = parameters.scope?.split(' ')
|
|
409
|
-
const idToken = scopes?.includes('openid')
|
|
410
|
-
? await this.signer.idToken(client, parameters, account, {
|
|
411
|
-
exp: expiresAt,
|
|
412
|
-
iat: now,
|
|
413
|
-
auth_time: info?.authenticatedAt,
|
|
414
|
-
access_token: accessToken,
|
|
415
|
-
})
|
|
416
|
-
: undefined
|
|
417
|
-
|
|
418
386
|
return this.buildTokenResponse(
|
|
419
387
|
client,
|
|
420
388
|
accessToken,
|
|
421
389
|
nextRefreshToken,
|
|
422
|
-
idToken,
|
|
423
390
|
expiresAt,
|
|
424
391
|
parameters,
|
|
425
392
|
account,
|
package/dist/oidc/claims.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { JwtPayload } from '@atproto/jwk';
|
|
2
|
-
/**
|
|
3
|
-
* @see {@link https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims | OpenID Connect Core 1.0, 5.4. Requesting Claims using Scope Values}
|
|
4
|
-
*/
|
|
5
|
-
export declare const OIDC_SCOPE_CLAIMS: Readonly<{
|
|
6
|
-
email: readonly ["email", "email_verified"];
|
|
7
|
-
phone: readonly ["phone_number", "phone_number_verified"];
|
|
8
|
-
address: readonly ["address"];
|
|
9
|
-
profile: readonly ["name", "family_name", "given_name", "middle_name", "nickname", "preferred_username", "gender", "picture", "profile", "website", "birthdate", "zoneinfo", "locale", "updated_at"];
|
|
10
|
-
}>;
|
|
11
|
-
export declare const OIDC_STANDARD_CLAIMS: readonly ("name" | "email" | "email_verified" | "phone_number" | "phone_number_verified" | "address" | "profile" | "family_name" | "given_name" | "middle_name" | "nickname" | "preferred_username" | "gender" | "picture" | "website" | "birthdate" | "zoneinfo" | "locale" | "updated_at")[];
|
|
12
|
-
export type OIDCStandardClaim = (typeof OIDC_STANDARD_CLAIMS)[number];
|
|
13
|
-
export type OIDCStandardPayload = Partial<{
|
|
14
|
-
[K in OIDCStandardClaim]?: JwtPayload[K];
|
|
15
|
-
}>;
|
|
16
|
-
//# sourceMappingURL=claims.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"claims.d.ts","sourceRoot":"","sources":["../../src/oidc/claims.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAEzC;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;EAoB5B,CAAA;AAEF,eAAO,MAAM,oBAAoB,gSAEhC,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAA;AACrE,MAAM,MAAM,mBAAmB,GAAG,OAAO,CAAC;KACvC,CAAC,IAAI,iBAAiB,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC;CACzC,CAAC,CAAA"}
|
package/dist/oidc/claims.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.OIDC_STANDARD_CLAIMS = exports.OIDC_SCOPE_CLAIMS = void 0;
|
|
4
|
-
/**
|
|
5
|
-
* @see {@link https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims | OpenID Connect Core 1.0, 5.4. Requesting Claims using Scope Values}
|
|
6
|
-
*/
|
|
7
|
-
exports.OIDC_SCOPE_CLAIMS = Object.freeze({
|
|
8
|
-
email: Object.freeze(['email', 'email_verified']),
|
|
9
|
-
phone: Object.freeze(['phone_number', 'phone_number_verified']),
|
|
10
|
-
address: Object.freeze(['address']),
|
|
11
|
-
profile: Object.freeze([
|
|
12
|
-
'name',
|
|
13
|
-
'family_name',
|
|
14
|
-
'given_name',
|
|
15
|
-
'middle_name',
|
|
16
|
-
'nickname',
|
|
17
|
-
'preferred_username',
|
|
18
|
-
'gender',
|
|
19
|
-
'picture',
|
|
20
|
-
'profile',
|
|
21
|
-
'website',
|
|
22
|
-
'birthdate',
|
|
23
|
-
'zoneinfo',
|
|
24
|
-
'locale',
|
|
25
|
-
'updated_at',
|
|
26
|
-
]),
|
|
27
|
-
});
|
|
28
|
-
exports.OIDC_STANDARD_CLAIMS = Object.freeze(Object.values(exports.OIDC_SCOPE_CLAIMS).flat());
|
|
29
|
-
//# sourceMappingURL=claims.js.map
|
package/dist/oidc/claims.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"claims.js","sourceRoot":"","sources":["../../src/oidc/claims.ts"],"names":[],"mappings":";;;AAEA;;GAEG;AACU,QAAA,iBAAiB,GAAG,MAAM,CAAC,MAAM,CAAC;IAC7C,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,gBAAgB,CAAU,CAAC;IAC1D,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,cAAc,EAAE,uBAAuB,CAAU,CAAC;IACxE,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAU,CAAC;IAC5C,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC;QACrB,MAAM;QACN,aAAa;QACb,YAAY;QACZ,aAAa;QACb,UAAU;QACV,oBAAoB;QACpB,QAAQ;QACR,SAAS;QACT,SAAS;QACT,SAAS;QACT,WAAW;QACX,UAAU;QACV,QAAQ;QACR,YAAY;KACJ,CAAC;CACZ,CAAC,CAAA;AAEW,QAAA,oBAAoB,GAAG,MAAM,CAAC,MAAM,CAC/C,MAAM,CAAC,MAAM,CAAC,yBAAiB,CAAC,CAAC,IAAI,EAAE,CACxC,CAAA"}
|
package/dist/oidc/userinfo.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"userinfo.d.ts","sourceRoot":"","sources":["../../src/oidc/userinfo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAEjD,MAAM,MAAM,QAAQ,GAAG,mBAAmB,GAAG;IAE3C,GAAG,EAAE,MAAM,CAAA;IAGX,SAAS,EAAE,MAAM,CAAA;IAEjB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA"}
|
package/dist/oidc/userinfo.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"userinfo.js","sourceRoot":"","sources":["../../src/oidc/userinfo.ts"],"names":[],"mappings":""}
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
import { OAuthAuthenticationRequestParameters, OidcClaimsParameter, OidcEntityType } from '@atproto/oauth-types';
|
|
2
|
-
export declare function claimRequested(parameters: OAuthAuthenticationRequestParameters, entityType: OidcEntityType, claimName: OidcClaimsParameter, value: unknown): boolean;
|
|
3
|
-
//# sourceMappingURL=claims-requested.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"claims-requested.d.ts","sourceRoot":"","sources":["../../src/parameters/claims-requested.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oCAAoC,EACpC,mBAAmB,EACnB,cAAc,EACf,MAAM,sBAAsB,CAAA;AAG7B,wBAAgB,cAAc,CAC5B,UAAU,EAAE,oCAAoC,EAChD,UAAU,EAAE,cAAc,EAC1B,SAAS,EAAE,mBAAmB,EAC9B,KAAK,EAAE,OAAO,GACb,OAAO,CA+BT"}
|