@atproto/oauth-provider 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -1,22 +1,23 @@
|
|
1
1
|
import { OAuthClientMetadata } from '@atproto/oauth-types'
|
2
2
|
import { FormEvent } from 'react'
|
3
3
|
|
4
|
-
import { Account } from '../backend-data'
|
4
|
+
import { Account, ScopeDetail } from '../backend-data'
|
5
5
|
import { Override } from '../lib/util'
|
6
6
|
import { AccountIdentifier } from './account-identifier'
|
7
7
|
import { Button } from './button'
|
8
|
-
import { ClientIdentifier } from './client-identifier'
|
9
8
|
import { ClientName } from './client-name'
|
10
9
|
import { FormCard, FormCardProps } from './form-card'
|
11
|
-
import { Fieldset } from './fieldset'
|
12
10
|
|
13
11
|
export type AcceptFormProps = Override<
|
14
12
|
FormCardProps,
|
15
13
|
{
|
16
|
-
account: Account
|
17
14
|
clientId: string
|
18
15
|
clientMetadata: OAuthClientMetadata
|
19
16
|
clientTrusted: boolean
|
17
|
+
|
18
|
+
account: Account
|
19
|
+
scopeDetails?: ScopeDetail[]
|
20
|
+
|
20
21
|
onAccept: () => void
|
21
22
|
acceptLabel?: string
|
22
23
|
|
@@ -29,10 +30,13 @@ export type AcceptFormProps = Override<
|
|
29
30
|
>
|
30
31
|
|
31
32
|
export function AcceptForm({
|
32
|
-
account,
|
33
33
|
clientId,
|
34
34
|
clientMetadata,
|
35
35
|
clientTrusted,
|
36
|
+
|
37
|
+
account,
|
38
|
+
scopeDetails,
|
39
|
+
|
36
40
|
onAccept,
|
37
41
|
acceptLabel = 'Accept',
|
38
42
|
onReject,
|
@@ -62,54 +66,64 @@ export function AcceptForm({
|
|
62
66
|
}
|
63
67
|
{...props}
|
64
68
|
>
|
65
|
-
|
66
|
-
|
67
|
-
<
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
)
|
69
|
+
{clientTrusted && clientMetadata.logo_uri && (
|
70
|
+
<div key="logo" className="flex items-center justify-center">
|
71
|
+
<img
|
72
|
+
crossOrigin="anonymous"
|
73
|
+
src={clientMetadata.logo_uri}
|
74
|
+
alt={clientMetadata.client_name}
|
75
|
+
className="w-16 h-16 rounded-full"
|
76
|
+
/>
|
77
|
+
</div>
|
78
|
+
)}
|
79
|
+
<p>
|
80
|
+
<ClientName clientId={clientId} clientMetadata={clientMetadata} /> is
|
81
|
+
asking for permission to access your account (
|
82
|
+
<AccountIdentifier account={account} />
|
83
|
+
).
|
84
|
+
</p>
|
80
85
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
86
|
+
<p>
|
87
|
+
By clicking <b>{acceptLabel}</b>, you allow this application to perform
|
88
|
+
the following actions in accordance to their{' '}
|
89
|
+
<a
|
90
|
+
href={clientMetadata.tos_uri}
|
91
|
+
rel="nofollow noopener"
|
92
|
+
target="_blank"
|
93
|
+
className="text-brand underline"
|
94
|
+
>
|
95
|
+
terms of service
|
96
|
+
</a>
|
97
|
+
{' and '}
|
98
|
+
<a
|
99
|
+
href={clientMetadata.policy_uri}
|
100
|
+
rel="nofollow noopener"
|
101
|
+
target="_blank"
|
102
|
+
className="text-brand underline"
|
103
|
+
>
|
104
|
+
privacy policy
|
105
|
+
</a>
|
106
|
+
:
|
107
|
+
</p>
|
89
108
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
terms of service
|
100
|
-
</a>
|
101
|
-
{' and '}
|
102
|
-
<a
|
103
|
-
href={clientMetadata.policy_uri}
|
104
|
-
rel="nofollow noopener"
|
105
|
-
target="_blank"
|
106
|
-
className="text-brand underline"
|
107
|
-
>
|
108
|
-
privacy policy
|
109
|
-
</a>
|
110
|
-
.
|
111
|
-
</p>
|
112
|
-
</Fieldset>
|
109
|
+
{scopeDetails?.length ? (
|
110
|
+
<ul className="list-disc list-inside">
|
111
|
+
{scopeDetails.map(
|
112
|
+
({ scope, description = getScopeDescription(scope) }) => (
|
113
|
+
<li key={scope}>{description}</li>
|
114
|
+
),
|
115
|
+
)}
|
116
|
+
</ul>
|
117
|
+
) : null}
|
113
118
|
</FormCard>
|
114
119
|
)
|
115
120
|
}
|
121
|
+
|
122
|
+
function getScopeDescription(scope: string): string {
|
123
|
+
switch (scope) {
|
124
|
+
case 'atproto':
|
125
|
+
return 'Uniquely identify you'
|
126
|
+
default:
|
127
|
+
return scope
|
128
|
+
}
|
129
|
+
}
|
@@ -1,30 +1,38 @@
|
|
1
|
-
import {
|
1
|
+
import {
|
2
|
+
isOAuthClientIdDiscoverable,
|
3
|
+
isOAuthClientIdLoopback,
|
4
|
+
OAuthClientMetadata,
|
5
|
+
} from '@atproto/oauth-types'
|
2
6
|
import { HTMLAttributes } from 'react'
|
3
7
|
|
4
|
-
import {
|
8
|
+
import { UrlViewer } from './url-viewer'
|
5
9
|
|
6
10
|
export type ClientNameProps = {
|
7
11
|
clientId: string
|
8
12
|
clientMetadata: OAuthClientMetadata
|
9
|
-
|
10
|
-
}
|
13
|
+
} & HTMLAttributes<Element>
|
11
14
|
|
12
15
|
export function ClientName({
|
13
16
|
clientId,
|
14
17
|
clientMetadata,
|
15
|
-
as: As = 'span',
|
16
18
|
...attrs
|
17
|
-
}: ClientNameProps
|
18
|
-
if (
|
19
|
-
return <
|
19
|
+
}: ClientNameProps) {
|
20
|
+
if (isOAuthClientIdLoopback(clientId)) {
|
21
|
+
return <span {...attrs}>An application on your device</span>
|
22
|
+
}
|
23
|
+
|
24
|
+
if (isOAuthClientIdDiscoverable(clientId)) {
|
25
|
+
if (clientMetadata.client_name) {
|
26
|
+
return (
|
27
|
+
<span {...attrs}>
|
28
|
+
{clientMetadata.client_name} (
|
29
|
+
<UrlViewer url={clientId} path />)
|
30
|
+
</span>
|
31
|
+
)
|
32
|
+
}
|
33
|
+
|
34
|
+
return <UrlViewer {...attrs} url={clientId} path />
|
20
35
|
}
|
21
36
|
|
22
|
-
return
|
23
|
-
<ClientIdentifier
|
24
|
-
clientId={clientId}
|
25
|
-
clientMetadata={clientMetadata}
|
26
|
-
as={As}
|
27
|
-
{...attrs}
|
28
|
-
/>
|
29
|
-
)
|
37
|
+
return <span {...attrs}>{clientMetadata.client_name || clientId}</span>
|
30
38
|
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { HTMLAttributes, useMemo } from 'react'
|
1
|
+
import { Component, HTMLAttributes, useMemo } from 'react'
|
2
2
|
|
3
3
|
export type UrlPartRenderingOptions = {
|
4
4
|
faded?: boolean
|
@@ -28,7 +28,7 @@ export function UrlViewer({
|
|
28
28
|
const urlObj = useMemo(() => new URL(url), [url])
|
29
29
|
|
30
30
|
return (
|
31
|
-
<As {...attrs}>
|
31
|
+
<Component as={As} {...attrs}>
|
32
32
|
{proto && (
|
33
33
|
<UrlPartViewer
|
34
34
|
value={`${urlObj.protocol}//`}
|
@@ -56,7 +56,7 @@ export function UrlViewer({
|
|
56
56
|
{hash && (
|
57
57
|
<UrlPartViewer value={urlObj.hash} {...(hash === true ? null : hash)} />
|
58
58
|
)}
|
59
|
-
</
|
59
|
+
</Component>
|
60
60
|
)
|
61
61
|
}
|
62
62
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import { OAuthClientMetadata } from '@atproto/oauth-types'
|
2
2
|
|
3
|
-
import {
|
3
|
+
import { Account, ScopeDetail } from '../backend-data'
|
4
4
|
import { AcceptForm } from '../components/accept-form'
|
5
5
|
import { LayoutTitlePage } from '../components/layout-title-page'
|
6
6
|
|
@@ -8,7 +8,9 @@ export type AcceptViewProps = {
|
|
8
8
|
clientId: string
|
9
9
|
clientMetadata: OAuthClientMetadata
|
10
10
|
clientTrusted: boolean
|
11
|
-
|
11
|
+
|
12
|
+
account: Account
|
13
|
+
scopeDetails?: ScopeDetail[]
|
12
14
|
|
13
15
|
onAccept: () => void
|
14
16
|
onReject: () => void
|
@@ -19,12 +21,12 @@ export function AcceptView({
|
|
19
21
|
clientId,
|
20
22
|
clientMetadata,
|
21
23
|
clientTrusted,
|
22
|
-
|
24
|
+
account,
|
25
|
+
scopeDetails,
|
23
26
|
onAccept,
|
24
27
|
onReject,
|
25
28
|
onBack,
|
26
29
|
}: AcceptViewProps) {
|
27
|
-
const { account } = session
|
28
30
|
return (
|
29
31
|
<LayoutTitlePage
|
30
32
|
title="Authorize"
|
@@ -43,6 +45,7 @@ export function AcceptView({
|
|
43
45
|
clientMetadata={clientMetadata}
|
44
46
|
clientTrusted={clientTrusted}
|
45
47
|
account={account}
|
48
|
+
scopeDetails={scopeDetails}
|
46
49
|
onBack={onBack}
|
47
50
|
onAccept={onAccept}
|
48
51
|
onReject={onReject}
|
@@ -79,10 +79,11 @@ export function AuthorizeView({
|
|
79
79
|
if (view === 'accept' && session) {
|
80
80
|
return (
|
81
81
|
<AcceptView
|
82
|
-
session={session}
|
83
82
|
clientId={authorizeData.clientId}
|
84
83
|
clientMetadata={authorizeData.clientMetadata}
|
85
84
|
clientTrusted={authorizeData.clientTrusted}
|
85
|
+
account={session.account}
|
86
|
+
scopeDetails={authorizeData.scopeDetails}
|
86
87
|
onAccept={() => doAccept(session.account)}
|
87
88
|
onReject={doReject}
|
88
89
|
onBack={
|
@@ -1,8 +1,13 @@
|
|
1
|
-
import {
|
1
|
+
import {
|
2
|
+
Middleware,
|
3
|
+
validateFetchDest,
|
4
|
+
validateFetchSite,
|
5
|
+
writeStream,
|
6
|
+
} from '../lib/http/index.js'
|
2
7
|
|
3
8
|
import { ASSETS_URL_PREFIX, getAsset } from './index.js'
|
4
9
|
|
5
|
-
export function authorizeAssetsMiddleware() {
|
10
|
+
export function authorizeAssetsMiddleware(): Middleware {
|
6
11
|
return async function assetsMiddleware(req, res, next): Promise<void> {
|
7
12
|
if (req.method !== 'GET' && req.method !== 'HEAD') return next()
|
8
13
|
if (!req.url?.startsWith(ASSETS_URL_PREFIX)) return next()
|
@@ -17,6 +22,13 @@ export function authorizeAssetsMiddleware() {
|
|
17
22
|
const asset = await getAsset(filename).catch(() => null)
|
18
23
|
if (!asset) return next()
|
19
24
|
|
25
|
+
try {
|
26
|
+
validateFetchSite(req, res, ['same-origin'])
|
27
|
+
validateFetchDest(req, res, ['style', 'script'])
|
28
|
+
} catch (err) {
|
29
|
+
return next(err)
|
30
|
+
}
|
31
|
+
|
20
32
|
if (req.headers['if-none-match'] === asset.sha256) {
|
21
33
|
return void res.writeHead(304).end()
|
22
34
|
}
|
@@ -17,7 +17,7 @@ import {
|
|
17
17
|
isLoopbackUrl,
|
18
18
|
isOAuthClientIdDiscoverable,
|
19
19
|
isOAuthClientIdLoopback,
|
20
|
-
|
20
|
+
OAuthAuthorizationServerMetadata,
|
21
21
|
OAuthClientIdDiscoverable,
|
22
22
|
OAuthClientIdLoopback,
|
23
23
|
OAuthClientMetadata,
|
@@ -56,9 +56,10 @@ export type LoopbackMetadataGetter = (
|
|
56
56
|
|
57
57
|
export class ClientManager {
|
58
58
|
protected readonly jwks: CachedGetter<string, Jwks>
|
59
|
-
protected readonly
|
59
|
+
protected readonly metadataGetter: CachedGetter<string, OAuthClientMetadata>
|
60
60
|
|
61
61
|
constructor(
|
62
|
+
protected readonly serverMetadata: OAuthAuthorizationServerMetadata,
|
62
63
|
protected readonly keyset: Keyset,
|
63
64
|
protected readonly hooks: OAuthHooks,
|
64
65
|
protected readonly store: ClientStore | null,
|
@@ -77,7 +78,7 @@ export class ClientManager {
|
|
77
78
|
return jwks
|
78
79
|
}, clientJwksCache)
|
79
80
|
|
80
|
-
this.
|
81
|
+
this.metadataGetter = new CachedGetter(async (uri, options) => {
|
81
82
|
const metadata = await fetch(buildJsonGetRequest(uri, options)).then(
|
82
83
|
fetchMetadataHandler,
|
83
84
|
)
|
@@ -160,7 +161,7 @@ export class ClientManager {
|
|
160
161
|
): Promise<OAuthClientMetadata> {
|
161
162
|
const metadataUrl = parseDiscoverableClientId(clientId)
|
162
163
|
|
163
|
-
const metadata = await this.
|
164
|
+
const metadata = await this.metadataGetter.get(metadataUrl.href)
|
164
165
|
|
165
166
|
// Note: we do *not* re-validate the metadata here, as the metadata is
|
166
167
|
// validated within the getter. This is to avoid double validation.
|
@@ -196,6 +197,18 @@ export class ClientManager {
|
|
196
197
|
)
|
197
198
|
}
|
198
199
|
|
200
|
+
// Known OIDC specific parameters
|
201
|
+
for (const k of [
|
202
|
+
'default_max_age',
|
203
|
+
'userinfo_signed_response_alg',
|
204
|
+
'id_token_signed_response_alg',
|
205
|
+
'userinfo_encrypted_response_alg',
|
206
|
+
] as const) {
|
207
|
+
if (metadata[k] != null) {
|
208
|
+
throw new InvalidClientMetadataError(`Unsupported "${k}" parameter`)
|
209
|
+
}
|
210
|
+
}
|
211
|
+
|
199
212
|
const clientUriUrl = metadata.client_uri
|
200
213
|
? new URL(metadata.client_uri)
|
201
214
|
: null
|
@@ -205,13 +218,27 @@ export class ClientManager {
|
|
205
218
|
throw new InvalidClientMetadataError('client_uri must be a valid URL')
|
206
219
|
}
|
207
220
|
|
208
|
-
const scopes = metadata.scope?.split(' ')
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
221
|
+
const scopes = metadata.scope?.split(' ').filter(Boolean)
|
222
|
+
|
223
|
+
const dupScope = scopes?.find(isDuplicate)
|
224
|
+
if (dupScope) {
|
225
|
+
throw new InvalidClientMetadataError(`Duplicate scope "${dupScope}"`)
|
226
|
+
}
|
227
|
+
|
228
|
+
if (scopes) {
|
229
|
+
for (const scope of scopes) {
|
230
|
+
// Note, once we have dynamic scopes, this check will need to be
|
231
|
+
// updated to check against the server's supported scopes.
|
232
|
+
if (!this.serverMetadata.scopes_supported?.includes(scope)) {
|
233
|
+
throw new InvalidClientMetadataError(`Unsupported scope "${scope}"`)
|
234
|
+
}
|
235
|
+
}
|
236
|
+
}
|
237
|
+
|
238
|
+
const dupGrantType = metadata.grant_types.find(isDuplicate)
|
239
|
+
if (dupGrantType) {
|
213
240
|
throw new InvalidClientMetadataError(
|
214
|
-
|
241
|
+
`Duplicate grant type "${dupGrantType}"`,
|
215
242
|
)
|
216
243
|
}
|
217
244
|
|
@@ -219,8 +246,8 @@ export class ClientManager {
|
|
219
246
|
switch (grantType) {
|
220
247
|
case 'authorization_code':
|
221
248
|
case 'refresh_token':
|
222
|
-
case 'implicit': // Required by OIDC (for id_token)
|
223
249
|
continue
|
250
|
+
case 'implicit':
|
224
251
|
case 'password':
|
225
252
|
throw new InvalidClientMetadataError(
|
226
253
|
`Grant type "${grantType}" is not allowed`,
|
@@ -242,78 +269,43 @@ export class ClientManager {
|
|
242
269
|
)
|
243
270
|
}
|
244
271
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
throw new InvalidClientMetadataError(
|
252
|
-
`Unsupported "userinfo_signed_response_alg" ${metadata.userinfo_signed_response_alg}`,
|
253
|
-
)
|
254
|
-
}
|
255
|
-
|
256
|
-
if (
|
257
|
-
metadata.id_token_signed_response_alg &&
|
258
|
-
!this.keyset.signAlgorithms.includes(
|
259
|
-
metadata.id_token_signed_response_alg,
|
260
|
-
)
|
261
|
-
) {
|
262
|
-
throw new InvalidClientMetadataError(
|
263
|
-
`Unsupported "id_token_signed_response_alg" ${metadata.id_token_signed_response_alg}`,
|
264
|
-
)
|
265
|
-
}
|
266
|
-
|
267
|
-
if (metadata.userinfo_encrypted_response_alg) {
|
268
|
-
// We only support signature for now.
|
269
|
-
throw new InvalidClientMetadataError(
|
270
|
-
'Encrypted userinfo response is not supported',
|
271
|
-
)
|
272
|
-
}
|
273
|
-
|
274
|
-
if (!metadata[`token_endpoint_auth_method`]) {
|
275
|
-
throw new InvalidClientMetadataError(
|
276
|
-
'Missing token_endpoint_auth_method client metadata',
|
277
|
-
)
|
278
|
-
}
|
279
|
-
|
280
|
-
for (const endpoint of OAUTH_AUTHENTICATED_ENDPOINT_NAMES) {
|
281
|
-
const method =
|
282
|
-
metadata[`${endpoint}_endpoint_auth_method`] ||
|
283
|
-
metadata[`token_endpoint_auth_method`]
|
284
|
-
|
285
|
-
switch (method) {
|
286
|
-
case 'none':
|
287
|
-
if (metadata.token_endpoint_auth_signing_alg) {
|
288
|
-
throw new InvalidClientMetadataError(
|
289
|
-
`${endpoint}_endpoint_auth_method "none" must not have ${endpoint}_endpoint_auth_signing_alg`,
|
290
|
-
)
|
291
|
-
}
|
292
|
-
break
|
272
|
+
const method = metadata[`token_endpoint_auth_method`]
|
273
|
+
switch (method) {
|
274
|
+
case undefined:
|
275
|
+
throw new InvalidClientMetadataError(
|
276
|
+
'Missing token_endpoint_auth_method client metadata',
|
277
|
+
)
|
293
278
|
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
throw new InvalidClientMetadataError(
|
302
|
-
`private_key_jwt auth method requires at least one key in jwks`,
|
303
|
-
)
|
304
|
-
}
|
305
|
-
if (!metadata.token_endpoint_auth_signing_alg) {
|
306
|
-
throw new InvalidClientMetadataError(
|
307
|
-
`Missing token_endpoint_auth_signing_alg client metadata`,
|
308
|
-
)
|
309
|
-
}
|
310
|
-
break
|
279
|
+
case 'none':
|
280
|
+
if (metadata.token_endpoint_auth_signing_alg) {
|
281
|
+
throw new InvalidClientMetadataError(
|
282
|
+
`token_endpoint_auth_method "none" must not have token_endpoint_auth_signing_alg`,
|
283
|
+
)
|
284
|
+
}
|
285
|
+
break
|
311
286
|
|
312
|
-
|
287
|
+
case 'private_key_jwt':
|
288
|
+
if (!metadata.jwks && !metadata.jwks_uri) {
|
313
289
|
throw new InvalidClientMetadataError(
|
314
|
-
|
290
|
+
`private_key_jwt auth method requires jwks or jwks_uri`,
|
315
291
|
)
|
316
|
-
|
292
|
+
}
|
293
|
+
if (metadata.jwks?.keys.length === 0) {
|
294
|
+
throw new InvalidClientMetadataError(
|
295
|
+
`private_key_jwt auth method requires at least one key in jwks`,
|
296
|
+
)
|
297
|
+
}
|
298
|
+
if (!metadata.token_endpoint_auth_signing_alg) {
|
299
|
+
throw new InvalidClientMetadataError(
|
300
|
+
`Missing token_endpoint_auth_signing_alg client metadata`,
|
301
|
+
)
|
302
|
+
}
|
303
|
+
break
|
304
|
+
|
305
|
+
default:
|
306
|
+
throw new InvalidClientMetadataError(
|
307
|
+
`${method} is not a supported "token_endpoint_auth_method". Use "private_key_jwt" or "none".`,
|
308
|
+
)
|
317
309
|
}
|
318
310
|
|
319
311
|
if (metadata.authorization_encrypted_response_enc) {
|
@@ -345,37 +337,28 @@ export class ClientManager {
|
|
345
337
|
}
|
346
338
|
|
347
339
|
for (const responseType of metadata.response_types) {
|
348
|
-
|
340
|
+
if (responseType.includes('id_token')) {
|
341
|
+
throw new InvalidClientMetadataError(
|
342
|
+
`OpenID Connect response type "${responseType}" is not supported`,
|
343
|
+
)
|
344
|
+
}
|
349
345
|
|
350
346
|
// ATPROTO spec requires the use of PKCE
|
351
|
-
if (
|
347
|
+
if (responseType !== 'code') {
|
352
348
|
throw new InvalidClientMetadataError(
|
353
|
-
|
349
|
+
`Unsupported response type "${responseType}"`,
|
354
350
|
)
|
355
351
|
}
|
356
352
|
|
357
353
|
// Consistency check
|
358
354
|
if (
|
359
|
-
|
355
|
+
responseType === 'code' &&
|
360
356
|
!metadata.grant_types.includes('authorization_code')
|
361
357
|
) {
|
362
358
|
throw new InvalidClientMetadataError(
|
363
359
|
`Response type "${responseType}" requires the "authorization_code" grant type`,
|
364
360
|
)
|
365
361
|
}
|
366
|
-
|
367
|
-
// Asking for "code token" or "code id_token" is fine (as long as the
|
368
|
-
// grant_types includes "authorization_code" and the scope includes
|
369
|
-
// "openid"). Asking for "token" or "id_token" (without "code") requires
|
370
|
-
// the "implicit" grant type.
|
371
|
-
if (
|
372
|
-
(rt.includes('token') || rt.includes('id_token')) &&
|
373
|
-
!metadata.grant_types.includes('implicit')
|
374
|
-
) {
|
375
|
-
throw new InvalidClientMetadataError(
|
376
|
-
`Response type "${responseType}" requires the "implicit" grant type`,
|
377
|
-
)
|
378
|
-
}
|
379
362
|
}
|
380
363
|
|
381
364
|
if (metadata.application_type === 'native') {
|
@@ -390,11 +373,33 @@ export class ClientManager {
|
|
390
373
|
// > accordingly.
|
391
374
|
}
|
392
375
|
|
376
|
+
if (metadata.authorization_details_types?.length) {
|
377
|
+
const dupAuthDetailsType =
|
378
|
+
metadata.authorization_details_types.find(isDuplicate)
|
379
|
+
if (dupAuthDetailsType) {
|
380
|
+
throw new InvalidClientMetadataError(
|
381
|
+
`Duplicate authorization_details_type "${dupAuthDetailsType}"`,
|
382
|
+
)
|
383
|
+
}
|
384
|
+
|
385
|
+
const authorizationDetailsTypesSupported =
|
386
|
+
this.serverMetadata.authorization_details_types_supported
|
387
|
+
if (!authorizationDetailsTypesSupported) {
|
388
|
+
throw new InvalidClientMetadataError(
|
389
|
+
'authorization_details_types are not supported',
|
390
|
+
)
|
391
|
+
}
|
392
|
+
for (const type of metadata.authorization_details_types) {
|
393
|
+
if (!authorizationDetailsTypesSupported.includes(type)) {
|
394
|
+
throw new InvalidClientMetadataError(
|
395
|
+
`Unsupported authorization_details_type "${type}"`,
|
396
|
+
)
|
397
|
+
}
|
398
|
+
}
|
399
|
+
}
|
400
|
+
|
393
401
|
if (!metadata.redirect_uris?.length) {
|
394
|
-
//
|
395
|
-
//
|
396
|
-
// > OPs can require that request_uri values used be pre-registered with
|
397
|
-
// > the require_request_uri_registration discovery parameter.
|
402
|
+
// ATPROTO spec requires that at least one redirect URI is provided
|
398
403
|
|
399
404
|
throw new InvalidClientMetadataError(
|
400
405
|
'At least one redirect_uri is required',
|
@@ -693,16 +698,11 @@ export class ClientManager {
|
|
693
698
|
)
|
694
699
|
}
|
695
700
|
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
if (method !== 'none') {
|
702
|
-
throw new InvalidClientMetadataError(
|
703
|
-
`Loopback clients are not allowed to use "${endpoint}_endpoint_auth_method" ${method}`,
|
704
|
-
)
|
705
|
-
}
|
701
|
+
const method = metadata[`token_endpoint_auth_method`]
|
702
|
+
if (method !== 'none') {
|
703
|
+
throw new InvalidClientMetadataError(
|
704
|
+
`Loopback clients are not allowed to use "token_endpoint_auth_method" ${method}`,
|
705
|
+
)
|
706
706
|
}
|
707
707
|
|
708
708
|
for (const redirectUri of metadata.redirect_uris) {
|
@@ -766,16 +766,14 @@ export class ClientManager {
|
|
766
766
|
}
|
767
767
|
}
|
768
768
|
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
)
|
778
|
-
}
|
769
|
+
const method = metadata[`token_endpoint_auth_method`]
|
770
|
+
switch (method) {
|
771
|
+
case 'client_secret_post':
|
772
|
+
case 'client_secret_basic':
|
773
|
+
case 'client_secret_jwt':
|
774
|
+
throw new InvalidClientMetadataError(
|
775
|
+
`Client authentication method "${method}" is not allowed for discoverable clients`,
|
776
|
+
)
|
779
777
|
}
|
780
778
|
|
781
779
|
for (const redirectUri of metadata.redirect_uris) {
|
@@ -800,6 +798,12 @@ export class ClientManager {
|
|
800
798
|
}
|
801
799
|
}
|
802
800
|
|
801
|
+
function isDuplicate<
|
802
|
+
T extends string | number | boolean | null | undefined | symbol,
|
803
|
+
>(value: T, index: number, array: T[]) {
|
804
|
+
return array.includes(value, index + 1)
|
805
|
+
}
|
806
|
+
|
803
807
|
function reverseDomain(domain: string) {
|
804
808
|
return domain.split('.').reverse().join('.')
|
805
809
|
}
|