@c9up/visa 0.1.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/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/VisaManager.d.ts +94 -0
- package/dist/VisaManager.d.ts.map +1 -0
- package/dist/VisaManager.js +216 -0
- package/dist/VisaManager.js.map +1 -0
- package/dist/VisaProvider.d.ts +41 -0
- package/dist/VisaProvider.d.ts.map +1 -0
- package/dist/VisaProvider.js +153 -0
- package/dist/VisaProvider.js.map +1 -0
- package/dist/augmentations.d.ts +25 -0
- package/dist/augmentations.d.ts.map +1 -0
- package/dist/augmentations.js +19 -0
- package/dist/augmentations.js.map +1 -0
- package/dist/authorize.d.ts +78 -0
- package/dist/authorize.d.ts.map +1 -0
- package/dist/authorize.js +149 -0
- package/dist/authorize.js.map +1 -0
- package/dist/clients.d.ts +70 -0
- package/dist/clients.d.ts.map +1 -0
- package/dist/clients.js +177 -0
- package/dist/clients.js.map +1 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -0
- package/dist/configure.d.ts +28 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/configure.js +24 -0
- package/dist/configure.js.map +1 -0
- package/dist/crypto.d.ts +35 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +56 -0
- package/dist/crypto.js.map +1 -0
- package/dist/errors.d.ts +47 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +50 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/introspect.d.ts +32 -0
- package/dist/introspect.d.ts.map +1 -0
- package/dist/introspect.js +110 -0
- package/dist/introspect.js.map +1 -0
- package/dist/services/main.d.ts +29 -0
- package/dist/services/main.d.ts.map +1 -0
- package/dist/services/main.js +48 -0
- package/dist/services/main.js.map +1 -0
- package/dist/store.d.ts +44 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +12 -0
- package/dist/store.js.map +1 -0
- package/dist/stores/memory.d.ts +34 -0
- package/dist/stores/memory.d.ts.map +1 -0
- package/dist/stores/memory.js +100 -0
- package/dist/stores/memory.js.map +1 -0
- package/dist/stubs.d.ts +14 -0
- package/dist/stubs.d.ts.map +1 -0
- package/dist/stubs.js +15 -0
- package/dist/stubs.js.map +1 -0
- package/dist/testing/main.d.ts +49 -0
- package/dist/testing/main.d.ts.map +1 -0
- package/dist/testing/main.js +77 -0
- package/dist/testing/main.js.map +1 -0
- package/dist/token.d.ts +20 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +212 -0
- package/dist/token.js.map +1 -0
- package/dist/types.d.ts +123 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/package.json +83 -0
- package/src/VisaManager.ts +346 -0
- package/src/VisaProvider.ts +197 -0
- package/src/augmentations.ts +29 -0
- package/src/authorize.ts +250 -0
- package/src/clients.ts +231 -0
- package/src/config.ts +34 -0
- package/src/configure.ts +42 -0
- package/src/crypto.ts +59 -0
- package/src/errors.ts +79 -0
- package/src/index.ts +67 -0
- package/src/introspect.ts +139 -0
- package/src/services/main.ts +57 -0
- package/src/store.ts +56 -0
- package/src/stores/memory.ts +118 -0
- package/src/stubs.ts +16 -0
- package/src/testing/main.ts +119 -0
- package/src/token.ts +325 -0
- package/src/types.ts +134 -0
- package/stubs/config/visa.stub +25 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 C9up
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# @c9up/visa
|
|
2
|
+
|
|
3
|
+
An OAuth 2.1 authorization server for the Ream ecosystem. It issues tokens to
|
|
4
|
+
**other** applications on a user's behalf.
|
|
5
|
+
|
|
6
|
+
It does not authenticate anyone — that stays warden's job. Visa asks your
|
|
7
|
+
application who is signed in, and everything else is its business.
|
|
8
|
+
|
|
9
|
+
No dependencies: `node:crypto` only. A package in the path that mints and
|
|
10
|
+
compares credentials is a package that can replace them.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
ream configure @c9up/visa
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## What it implements
|
|
17
|
+
|
|
18
|
+
| | |
|
|
19
|
+
|---|---|
|
|
20
|
+
| Grants | `authorization_code` (PKCE required), `refresh_token`, `client_credentials` |
|
|
21
|
+
| Endpoints | `/oauth/token`, `/oauth/revoke`, `/oauth/introspect`, `/.well-known/oauth-authorization-server` |
|
|
22
|
+
| Removed | `implicit` and `password` — gone from OAuth 2.1, and not available under any option |
|
|
23
|
+
|
|
24
|
+
`/authorize` is **not** mounted, deliberately: it needs a signed-in user and a
|
|
25
|
+
consent screen, and both belong to your application. See below.
|
|
26
|
+
|
|
27
|
+
## Security decisions, and why
|
|
28
|
+
|
|
29
|
+
- **PKCE on every client**, confidential ones included — a code on the front
|
|
30
|
+
channel is interceptable whoever asked for it.
|
|
31
|
+
- **`S256` only.** `plain` puts the verifier in the authorization request, so
|
|
32
|
+
anything that can read that request can complete the exchange, which is what
|
|
33
|
+
PKCE exists to stop. `allowPlainChallenge: true` brings it back; don't.
|
|
34
|
+
- **Exact redirect URI matching**, with the one exception the spec names (a
|
|
35
|
+
loopback port). A prefix match is how an open redirect on the client's own
|
|
36
|
+
domain becomes a stolen code.
|
|
37
|
+
- **An error is never redirected to an unverified URI.** A bad `redirect_uri`
|
|
38
|
+
is shown to the user, not bounced.
|
|
39
|
+
- **Everything is hashed at rest** — codes, access tokens, refresh tokens,
|
|
40
|
+
client secrets. A database dump is not a set of working credentials.
|
|
41
|
+
- **Refresh rotation with replay detection.** Every use mints a new token; a
|
|
42
|
+
spent one coming back proves a leak, and since there is no telling the thief
|
|
43
|
+
from the victim, the whole family goes.
|
|
44
|
+
- **One sentence per failure.** "No such code", "expired", "already used" and
|
|
45
|
+
"wrong client" are `invalid_grant: The code is not valid.` — telling them
|
|
46
|
+
apart is how a code space gets probed.
|
|
47
|
+
|
|
48
|
+
## Wiring `/authorize`
|
|
49
|
+
|
|
50
|
+
The decision is visa's; the page is yours.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// start/routes.ts
|
|
54
|
+
import visa from '@c9up/visa/services/main'
|
|
55
|
+
|
|
56
|
+
router.get('/oauth/authorize', async (ctx) => {
|
|
57
|
+
const outcome = await visa.authorize(ctx.request.qs(), ctx.auth.user?.id)
|
|
58
|
+
|
|
59
|
+
if (outcome.type === 'redirect') return ctx.response.redirect(outcome.url)
|
|
60
|
+
if (outcome.type === 'error') {
|
|
61
|
+
// NOT a redirect: the redirect URI is what failed validation.
|
|
62
|
+
return ctx.view.render('oauth/error', { error: outcome.error.toResponse() })
|
|
63
|
+
}
|
|
64
|
+
if (!ctx.auth.user) return ctx.response.redirect('/login?next=' + encodeURIComponent(ctx.request.url(true)))
|
|
65
|
+
|
|
66
|
+
return ctx.view.render('oauth/consent', {
|
|
67
|
+
client: outcome.request.client,
|
|
68
|
+
scopes: outcome.request.scopes,
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
router.post('/oauth/consent', async (ctx) => {
|
|
73
|
+
const outcome = await visa.authorize(ctx.request.all(), ctx.auth.user.id)
|
|
74
|
+
if (outcome.type !== 'consent') return ctx.response.redirect('/')
|
|
75
|
+
const url = ctx.request.input('approve')
|
|
76
|
+
? await visa.grant(outcome.request, ctx.auth.user.id)
|
|
77
|
+
: visa.deny(outcome.request)
|
|
78
|
+
return ctx.response.redirect(url)
|
|
79
|
+
})
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Registering a client
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const { client, secret } = await visa.registerClient({
|
|
86
|
+
id: 'invoices',
|
|
87
|
+
name: 'Invoices',
|
|
88
|
+
redirectUris: ['https://invoices.example.com/callback'],
|
|
89
|
+
scopes: ['profile', 'invoices:read'],
|
|
90
|
+
})
|
|
91
|
+
// `secret` is shown once and never stored in plaintext. A client that loses
|
|
92
|
+
// it gets a new one.
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
A public client — a SPA, a native app — registers with
|
|
96
|
+
`tokenEndpointAuthMethod: 'none'` and gets no secret. It may not use
|
|
97
|
+
`client_credentials`: "the client itself" means nothing when anyone can read
|
|
98
|
+
its id out of a browser.
|
|
99
|
+
|
|
100
|
+
## Protecting a resource
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const grant = await visa.verify(bearerToken)
|
|
104
|
+
if (!grant) return ctx.response.unauthorized({ error: 'invalid_token' })
|
|
105
|
+
if (!grant.scopes.includes('invoices:read')) {
|
|
106
|
+
return ctx.response.forbidden({ error: 'insufficient_scope' })
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Testing
|
|
111
|
+
|
|
112
|
+
`@c9up/visa/testing` hands back the REAL server on a memory store, with a
|
|
113
|
+
client already registered:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { testVisa } from '@c9up/visa/testing'
|
|
117
|
+
|
|
118
|
+
const t = await testVisa({ scopes: ['profile', 'invoices:read'] })
|
|
119
|
+
const tokens = await t.tokensFor('user-7', 'invoices:read')
|
|
120
|
+
const grant = await t.visa.verify(tokens.access_token)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`tokensFor()` walks authorize → consent → code → exchange rather than minting a
|
|
124
|
+
token directly. Not a lenient double on purpose: a helper that granted whatever
|
|
125
|
+
was asked would teach applications to ship a consent screen nobody has seen
|
|
126
|
+
refuse.
|
|
127
|
+
|
|
128
|
+
## The issuer
|
|
129
|
+
|
|
130
|
+
It identifies the SERVER, and that is all it does here. An access token is
|
|
131
|
+
opaque — a random string with no claims inside — so nothing is "bound" to an
|
|
132
|
+
issuer cryptographically; the only thing that can vouch for such a token is the
|
|
133
|
+
server that minted it. That is why `/oauth/introspect` answers with `iss`, and
|
|
134
|
+
why the metadata document names it.
|
|
135
|
+
|
|
136
|
+
It is validated at boot rather than at the first request: it must be a URL,
|
|
137
|
+
`https` (localhost excepted), with no query string and no fragment, and a
|
|
138
|
+
trailing slash is dropped — every endpoint is built by concatenation, so
|
|
139
|
+
`https://auth.test/` would otherwise produce `https://auth.test//oauth/token`.
|
|
140
|
+
|
|
141
|
+
## The store
|
|
142
|
+
|
|
143
|
+
`MemoryStore` is for tests and a single development process. Everything else
|
|
144
|
+
implements `VisaStore` — twelve methods. Two of them, `consumeAuthorizationCode`
|
|
145
|
+
and `consumeRefreshToken`, **must be atomic**: two requests racing with the same
|
|
146
|
+
code must not both succeed, and that single-use guarantee is what replay
|
|
147
|
+
detection is built on.
|
|
148
|
+
|
|
149
|
+
## What is not here yet
|
|
150
|
+
|
|
151
|
+
OpenID Connect — `id_token`, discovery, JWKS, `/userinfo`. The `nonce` is
|
|
152
|
+
already carried through the authorization code for it.
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server, assembled.
|
|
3
|
+
*
|
|
4
|
+
* Transport-agnostic on purpose: every method takes plain values and returns
|
|
5
|
+
* plain values, so the same object is driven by a ream route, by a test, or by
|
|
6
|
+
* a host that is not ream at all. The HTTP binding lives in the provider.
|
|
7
|
+
*/
|
|
8
|
+
import { type AuthorizationRequest, type AuthorizeOptions, type ValidatedRequest } from "./authorize.js";
|
|
9
|
+
import { type ClientCredentials } from "./clients.js";
|
|
10
|
+
import { OAuthError } from "./errors.js";
|
|
11
|
+
import type { VisaStore } from "./store.js";
|
|
12
|
+
import { type TokenOptions } from "./token.js";
|
|
13
|
+
import type { Client, ClientAuthMethod, GrantType, IntrospectionResponse, TokenResponse } from "./types.js";
|
|
14
|
+
export interface VisaConfig extends AuthorizeOptions, TokenOptions {
|
|
15
|
+
/** Where this server lives — `https://auth.example.com`. */
|
|
16
|
+
issuer: string;
|
|
17
|
+
store: VisaStore;
|
|
18
|
+
}
|
|
19
|
+
/** What `authorize()` decided. */
|
|
20
|
+
export type AuthorizationOutcome =
|
|
21
|
+
/** Send the user here. */
|
|
22
|
+
{
|
|
23
|
+
type: "redirect";
|
|
24
|
+
url: string;
|
|
25
|
+
}
|
|
26
|
+
/** Ask first: this user has not agreed to these scopes for this client. */
|
|
27
|
+
| {
|
|
28
|
+
type: "consent";
|
|
29
|
+
request: ValidatedRequest;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Show the user this error. It must NOT be redirected — the redirect URI
|
|
33
|
+
* is the thing that failed validation.
|
|
34
|
+
*/
|
|
35
|
+
| {
|
|
36
|
+
type: "error";
|
|
37
|
+
error: OAuthError;
|
|
38
|
+
};
|
|
39
|
+
export declare class VisaManager {
|
|
40
|
+
#private;
|
|
41
|
+
constructor(config: VisaConfig);
|
|
42
|
+
get issuer(): string;
|
|
43
|
+
get store(): VisaStore;
|
|
44
|
+
/**
|
|
45
|
+
* Register a client and hand back its secret ONCE.
|
|
46
|
+
*
|
|
47
|
+
* The plaintext is never stored and cannot be recovered; a client that
|
|
48
|
+
* loses it gets a new one. That is not an inconvenience to work around —
|
|
49
|
+
* it is the reason a database dump is not a set of working credentials.
|
|
50
|
+
*/
|
|
51
|
+
registerClient(input: {
|
|
52
|
+
id: string;
|
|
53
|
+
name: string;
|
|
54
|
+
redirectUris: string[];
|
|
55
|
+
grantTypes?: GrantType[];
|
|
56
|
+
scopes?: string[];
|
|
57
|
+
tokenEndpointAuthMethod?: ClientAuthMethod;
|
|
58
|
+
trusted?: boolean;
|
|
59
|
+
}): Promise<{
|
|
60
|
+
client: Client;
|
|
61
|
+
secret?: string;
|
|
62
|
+
}>;
|
|
63
|
+
/**
|
|
64
|
+
* Step one: check the request, and decide what happens to the user.
|
|
65
|
+
*
|
|
66
|
+
* `userId` is whoever is signed in — warden's business, not visa's. Pass
|
|
67
|
+
* `undefined` and the caller is told to authenticate first.
|
|
68
|
+
*/
|
|
69
|
+
authorize(request: AuthorizationRequest, userId: string | undefined): Promise<AuthorizationOutcome>;
|
|
70
|
+
/**
|
|
71
|
+
* Step two: the user said yes. Record it and build the redirect.
|
|
72
|
+
*
|
|
73
|
+
* Split from `authorize` because the consent screen sits between them, and
|
|
74
|
+
* the answer to it is a POST the application owns.
|
|
75
|
+
*/
|
|
76
|
+
grant(validated: ValidatedRequest, userId: string, now?: Date): Promise<string>;
|
|
77
|
+
/** The user said no. */
|
|
78
|
+
deny(validated: ValidatedRequest): string;
|
|
79
|
+
token(body: Record<string, unknown>, credentials: ClientCredentials, now?: Date): Promise<TokenResponse>;
|
|
80
|
+
introspect(body: Record<string, unknown>, credentials: ClientCredentials, now?: Date): Promise<IntrospectionResponse>;
|
|
81
|
+
revoke(body: Record<string, unknown>, credentials: ClientCredentials, now?: Date): Promise<void>;
|
|
82
|
+
/** What a resource server calls on every request. */
|
|
83
|
+
verify(presented: string, now?: Date): Promise<{
|
|
84
|
+
clientId: string;
|
|
85
|
+
userId?: string;
|
|
86
|
+
scopes: string[];
|
|
87
|
+
} | null>;
|
|
88
|
+
/** Read credentials out of a request, both places the spec allows. */
|
|
89
|
+
readCredentials(input: {
|
|
90
|
+
authorization?: string;
|
|
91
|
+
body: Record<string, unknown>;
|
|
92
|
+
}): ClientCredentials;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=VisaManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VisaManager.d.ts","sourceRoot":"","sources":["../src/VisaManager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server, assembled.
|
|
3
|
+
*
|
|
4
|
+
* Transport-agnostic on purpose: every method takes plain values and returns
|
|
5
|
+
* plain values, so the same object is driven by a ream route, by a test, or by
|
|
6
|
+
* a host that is not ream at all. The HTTP binding lives in the provider.
|
|
7
|
+
*/
|
|
8
|
+
import { errorRedirect, issueAuthorizationCode, successRedirect, UnredirectableError, validateAuthorizationRequest, } from "./authorize.js";
|
|
9
|
+
import { readCredentials, redirectUriMatches, } from "./clients.js";
|
|
10
|
+
import { hashSecret, randomToken } from "./crypto.js";
|
|
11
|
+
import { OAuthError, VisaError } from "./errors.js";
|
|
12
|
+
import { introspect, revoke, verifyAccessToken } from "./introspect.js";
|
|
13
|
+
import { token } from "./token.js";
|
|
14
|
+
export class VisaManager {
|
|
15
|
+
#config;
|
|
16
|
+
constructor(config) {
|
|
17
|
+
// Validated here rather than at the first request: a server that boots
|
|
18
|
+
// with a broken issuer answers every metadata fetch with something no
|
|
19
|
+
// client can use, and says so nowhere.
|
|
20
|
+
this.#config = { ...config, issuer: normalizeIssuer(config.issuer) };
|
|
21
|
+
}
|
|
22
|
+
get issuer() {
|
|
23
|
+
return this.#config.issuer;
|
|
24
|
+
}
|
|
25
|
+
get store() {
|
|
26
|
+
return this.#config.store;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Register a client and hand back its secret ONCE.
|
|
30
|
+
*
|
|
31
|
+
* The plaintext is never stored and cannot be recovered; a client that
|
|
32
|
+
* loses it gets a new one. That is not an inconvenience to work around —
|
|
33
|
+
* it is the reason a database dump is not a set of working credentials.
|
|
34
|
+
*/
|
|
35
|
+
async registerClient(input) {
|
|
36
|
+
// Refused at registration, not at the first authorization request: a
|
|
37
|
+
// URI that cannot be parsed would otherwise throw out of
|
|
38
|
+
// `successRedirect`, turning a client's mistake into a 500 on a path
|
|
39
|
+
// an attacker can reach.
|
|
40
|
+
assertRedirectUris(input.redirectUris);
|
|
41
|
+
const method = input.tokenEndpointAuthMethod ?? "client_secret_basic";
|
|
42
|
+
const secret = method === "none" ? undefined : randomToken();
|
|
43
|
+
const client = {
|
|
44
|
+
id: input.id,
|
|
45
|
+
name: input.name,
|
|
46
|
+
redirectUris: input.redirectUris,
|
|
47
|
+
grantTypes: input.grantTypes ?? ["authorization_code", "refresh_token"],
|
|
48
|
+
scopes: input.scopes ?? [],
|
|
49
|
+
tokenEndpointAuthMethod: method,
|
|
50
|
+
...(input.trusted === undefined ? {} : { trusted: input.trusted }),
|
|
51
|
+
...(secret === undefined ? {} : { secretHash: hashSecret(secret) }),
|
|
52
|
+
};
|
|
53
|
+
const store = this.#config.store;
|
|
54
|
+
if (!("addClient" in store) || typeof store.addClient !== "function") {
|
|
55
|
+
throw new VisaError("E_VISA_STORE_READ_ONLY", "This store cannot register clients; create them where they live.", {
|
|
56
|
+
hint: "MemoryStore.addClient exists; a database store registers rows.",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
store.addClient(client);
|
|
60
|
+
return secret === undefined ? { client } : { client, secret };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Step one: check the request, and decide what happens to the user.
|
|
64
|
+
*
|
|
65
|
+
* `userId` is whoever is signed in — warden's business, not visa's. Pass
|
|
66
|
+
* `undefined` and the caller is told to authenticate first.
|
|
67
|
+
*/
|
|
68
|
+
async authorize(request, userId) {
|
|
69
|
+
let validated;
|
|
70
|
+
try {
|
|
71
|
+
validated = await validateAuthorizationRequest(request, this.#config.store, this.#config);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (error instanceof UnredirectableError) {
|
|
75
|
+
return { type: "error", error: error.error };
|
|
76
|
+
}
|
|
77
|
+
if (error instanceof OAuthError) {
|
|
78
|
+
// Redirectable: the URI was verified before this check ran.
|
|
79
|
+
const uri = await this.#redirectUriFor(request);
|
|
80
|
+
if (uri === null)
|
|
81
|
+
return { type: "error", error };
|
|
82
|
+
return {
|
|
83
|
+
type: "redirect",
|
|
84
|
+
url: errorRedirect(uri, error, request.state),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
if (userId === undefined) {
|
|
90
|
+
return { type: "consent", request: validated };
|
|
91
|
+
}
|
|
92
|
+
const consent = await this.#config.store.findConsent(userId, validated.client.id);
|
|
93
|
+
const covered = consent !== null &&
|
|
94
|
+
validated.scopes.every((scope) => consent.scopes.includes(scope));
|
|
95
|
+
const forced = request.prompt === "consent";
|
|
96
|
+
if (validated.client.trusted !== true && (!covered || forced)) {
|
|
97
|
+
return { type: "consent", request: validated };
|
|
98
|
+
}
|
|
99
|
+
return { type: "redirect", url: await this.grant(validated, userId) };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Step two: the user said yes. Record it and build the redirect.
|
|
103
|
+
*
|
|
104
|
+
* Split from `authorize` because the consent screen sits between them, and
|
|
105
|
+
* the answer to it is a POST the application owns.
|
|
106
|
+
*/
|
|
107
|
+
async grant(validated, userId, now = new Date()) {
|
|
108
|
+
await this.#config.store.saveConsent({
|
|
109
|
+
userId,
|
|
110
|
+
clientId: validated.client.id,
|
|
111
|
+
scopes: validated.scopes,
|
|
112
|
+
grantedAt: now,
|
|
113
|
+
});
|
|
114
|
+
const code = await issueAuthorizationCode(validated, userId, this.#config.store, this.#config, now);
|
|
115
|
+
return successRedirect(validated, code);
|
|
116
|
+
}
|
|
117
|
+
/** The user said no. */
|
|
118
|
+
deny(validated) {
|
|
119
|
+
return errorRedirect(validated.redirectUri, new OAuthError("access_denied", "The user refused the request."), validated.state);
|
|
120
|
+
}
|
|
121
|
+
async token(body, credentials, now = new Date()) {
|
|
122
|
+
return token(body, credentials, this.#config.store, this.#config, now);
|
|
123
|
+
}
|
|
124
|
+
async introspect(body, credentials, now = new Date()) {
|
|
125
|
+
return introspect(body, credentials, this.#config.store, now, this.issuer);
|
|
126
|
+
}
|
|
127
|
+
async revoke(body, credentials, now = new Date()) {
|
|
128
|
+
return revoke(body, credentials, this.#config.store, now);
|
|
129
|
+
}
|
|
130
|
+
/** What a resource server calls on every request. */
|
|
131
|
+
async verify(presented, now = new Date()) {
|
|
132
|
+
return verifyAccessToken(presented, this.#config.store, now);
|
|
133
|
+
}
|
|
134
|
+
/** Read credentials out of a request, both places the spec allows. */
|
|
135
|
+
readCredentials(input) {
|
|
136
|
+
return readCredentials(input);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The redirect URI an error may travel to, or `null` when there is none to
|
|
140
|
+
* trust. Re-resolved rather than remembered: the validation that failed is
|
|
141
|
+
* the one that would have produced it.
|
|
142
|
+
*/
|
|
143
|
+
async #redirectUriFor(request) {
|
|
144
|
+
if (request.client_id === undefined)
|
|
145
|
+
return null;
|
|
146
|
+
const client = await this.#config.store.findClient(request.client_id);
|
|
147
|
+
if (client === null)
|
|
148
|
+
return null;
|
|
149
|
+
if (request.redirect_uri === undefined) {
|
|
150
|
+
return client.redirectUris.length === 1
|
|
151
|
+
? (client.redirectUris[0] ?? null)
|
|
152
|
+
: null;
|
|
153
|
+
}
|
|
154
|
+
return redirectUriMatches(request.redirect_uri, client.redirectUris)
|
|
155
|
+
? request.redirect_uri
|
|
156
|
+
: null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The issuer, checked and trimmed.
|
|
161
|
+
*
|
|
162
|
+
* A trailing slash is not cosmetic here: every endpoint is built by
|
|
163
|
+
* concatenation, so `https://auth.test/` yields `https://auth.test//oauth/token`
|
|
164
|
+
* — a URL some clients normalise and others do not.
|
|
165
|
+
*/
|
|
166
|
+
function normalizeIssuer(issuer) {
|
|
167
|
+
if (issuer === undefined || issuer === "") {
|
|
168
|
+
throw new VisaError("E_VISA_MISSING_ISSUER", "visa needs an issuer — the public URL this authorization server answers on.", { hint: "Set `issuer` in config/visa.ts, e.g. https://auth.example.com" });
|
|
169
|
+
}
|
|
170
|
+
let url;
|
|
171
|
+
try {
|
|
172
|
+
url = new URL(issuer);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
throw new VisaError("E_VISA_INVALID_ISSUER", `visa's issuer is not a URL: ${issuer}`, { hint: "It is the public origin, e.g. https://auth.example.com" });
|
|
176
|
+
}
|
|
177
|
+
if (url.protocol !== "https:" && url.hostname !== "localhost") {
|
|
178
|
+
// Tokens travel to it. `http://` is for a laptop, and the exception is
|
|
179
|
+
// named so nobody has to guess whether it applies in production.
|
|
180
|
+
throw new VisaError("E_VISA_INSECURE_ISSUER", `visa's issuer must be https (localhost excepted): ${issuer}`);
|
|
181
|
+
}
|
|
182
|
+
if (url.hash !== "" || url.search !== "") {
|
|
183
|
+
throw new VisaError("E_VISA_INVALID_ISSUER", "visa's issuer must carry no query string and no fragment.");
|
|
184
|
+
}
|
|
185
|
+
return issuer.replace(/\/+$/, "");
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Every redirect URI a client registers, checked once.
|
|
189
|
+
*
|
|
190
|
+
* A fragment is refused because the authorization response appends its own
|
|
191
|
+
* query and a fragment would swallow it; a wildcard because this server
|
|
192
|
+
* compares exactly and `*` would simply never match, silently.
|
|
193
|
+
*/
|
|
194
|
+
function assertRedirectUris(uris) {
|
|
195
|
+
if (uris.length === 0) {
|
|
196
|
+
throw new VisaError("E_VISA_NO_REDIRECT_URI", "A client needs at least one redirect URI.");
|
|
197
|
+
}
|
|
198
|
+
for (const uri of uris) {
|
|
199
|
+
let url;
|
|
200
|
+
try {
|
|
201
|
+
url = new URL(uri);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
throw new VisaError("E_VISA_INVALID_REDIRECT_URI", `Not a URL: ${uri}`, {
|
|
205
|
+
hint: "Register the complete URI, scheme and path included.",
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (url.hash !== "") {
|
|
209
|
+
throw new VisaError("E_VISA_INVALID_REDIRECT_URI", `A redirect URI must carry no fragment: ${uri}`);
|
|
210
|
+
}
|
|
211
|
+
if (uri.includes("*")) {
|
|
212
|
+
throw new VisaError("E_VISA_INVALID_REDIRECT_URI", `Wildcards are not matched, so this would never accept anything: ${uri}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=VisaManager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VisaManager.js","sourceRoot":"","sources":["../src/VisaManager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wires `config/visa.ts` into the container, and mounts the endpoints that
|
|
3
|
+
* need no human.
|
|
4
|
+
*
|
|
5
|
+
* `/token`, `/revoke` and `/introspect` are machine-to-machine: the spec says
|
|
6
|
+
* exactly what goes in and what comes out, so the provider owns them.
|
|
7
|
+
*
|
|
8
|
+
* `/authorize` is NOT mounted, and that is deliberate rather than unfinished.
|
|
9
|
+
* It needs two things visa has no opinion about: who is signed in — warden's
|
|
10
|
+
* business — and what a consent screen looks like, which is the application's.
|
|
11
|
+
* A route that guessed either would be wrong in a way that is hard to notice:
|
|
12
|
+
* a server that shows no consent screen grants silently, and one that assumes
|
|
13
|
+
* a session shape signs the wrong user in. `visa.authorize()` gives you the
|
|
14
|
+
* decision; the page around it is yours. The README shows the ten lines.
|
|
15
|
+
*
|
|
16
|
+
* Duck-typed throughout: visa must not import its host's HTTP types.
|
|
17
|
+
*/
|
|
18
|
+
import "./augmentations.js";
|
|
19
|
+
interface VisaContainer {
|
|
20
|
+
singleton(token: unknown, factory: () => unknown): void;
|
|
21
|
+
resolve<T = unknown>(token: unknown): Promise<T>;
|
|
22
|
+
has(token: unknown): boolean;
|
|
23
|
+
}
|
|
24
|
+
interface VisaConfigStore {
|
|
25
|
+
get<T = unknown>(key: string): T | undefined;
|
|
26
|
+
}
|
|
27
|
+
export interface VisaAppContext {
|
|
28
|
+
container: VisaContainer;
|
|
29
|
+
config: VisaConfigStore;
|
|
30
|
+
}
|
|
31
|
+
export default class VisaProvider {
|
|
32
|
+
#private;
|
|
33
|
+
protected app: VisaAppContext;
|
|
34
|
+
constructor(app: VisaAppContext);
|
|
35
|
+
register(): void;
|
|
36
|
+
boot(): Promise<void>;
|
|
37
|
+
start(): Promise<void>;
|
|
38
|
+
shutdown(): Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=VisaProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VisaProvider.d.ts","sourceRoot":"","sources":["../src/VisaProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,oBAAoB,CAAC;AAM5B,UAAU,aAAa;IACtB,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,GAAG,IAAI,CAAC;IACxD,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACjD,GAAG,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;CAC7B;AACD,UAAU,eAAe;IACxB,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAAC;CAC7C;AACD,MAAM,WAAW,cAAc;IAC9B,SAAS,EAAE,aAAa,CAAC;IACzB,MAAM,EAAE,eAAe,CAAC;CACxB;AAqBD,MAAM,CAAC,OAAO,OAAO,YAAY;;IAGpB,SAAS,CAAC,GAAG,EAAE,cAAc;gBAAnB,GAAG,EAAE,cAAc;IAEzC,QAAQ,IAAI,IAAI;IAiBV,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAMrB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA6EtB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CA8B/B"}
|