@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
|
@@ -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
|
}
|