initial commit. phase 1 complete

This commit is contained in:
2026-05-05 20:45:19 +02:00
parent d9c68313a0
commit 89e058ffac
20631 changed files with 3224610 additions and 43 deletions
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Filip Skokan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+320
View File
@@ -0,0 +1,320 @@
# openid-client
openid-client is a server side [OpenID][openid-connect] Relying Party (RP, Client) implementation for
Node.js runtime, supports [passport][passport-url].
## Implemented specs & features
The following client/RP features from OpenID Connect/OAuth2.0 specifications are implemented by
openid-client.
- [OpenID Connect Core 1.0][feature-core]
- Authorization Callback
- Authorization Code Flow
- Implicit Flow
- Hybrid Flow
- UserInfo Request
- Offline Access / Refresh Token Grant
- Client Credentials Grant
- Client Authentication
- none
- client_secret_basic
- client_secret_post
- client_secret_jwt
- private_key_jwt
- Consuming Self-Issued OpenID Provider ID Token response
- [OpenID Connect Discovery 1.0][feature-discovery]
- Discovery of OpenID Provider (Issuer) Metadata
- Discovery of OpenID Provider (Issuer) Metadata via user provided inputs (via [webfinger][documentation-webfinger])
- [OpenID Connect Dynamic Client Registration 1.0][feature-registration]
- Dynamic Client Registration request
- Client initialization via registration client uri
- [RFC7009 - OAuth 2.0 Token revocation][feature-revocation]
- Client Authenticated request to token revocation
- [RFC7662 - OAuth 2.0 Token introspection][feature-introspection]
- Client Authenticated request to token introspection
- [RFC8628 - OAuth 2.0 Device Authorization Grant (Device Flow)][feature-device-flow]
- [RFC8705 - OAuth 2.0 Mutual TLS Client Authentication and Certificate-Bound Access Tokens][feature-mtls]
- Mutual TLS Client Certificate-Bound Access Tokens
- Metadata for Mutual TLS Endpoint Aliases
- Client Authentication
- tls_client_auth
- self_signed_tls_client_auth
- [RFC9101 - OAuth 2.0 JWT-Secured Authorization Request (JAR)][feature-jar]
- [RFC9126 - OAuth 2.0 Pushed Authorization Requests (PAR)][feature-par]
- [RFC9449 - OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP)][feature-dpop]
- [OpenID Connect RP-Initiated Logout 1.0][feature-rp-logout]
- [Financial-grade API Security Profile 1.0 - Part 2: Advanced (FAPI)][feature-fapi]
- [JWT Secured Authorization Response Mode for OAuth 2.0 (JARM)][feature-jarm]
- [OAuth 2.0 Authorization Server Issuer Identification][feature-iss]
Updates to draft specifications are released as MINOR library versions,
if you utilize these specification implementations consider using the tilde `~` operator in your
package.json since breaking changes may be introduced as part of these version updates.
## [Certification](https://openid.net/certification/faq/)
[<img width="96" height="50" align="right" src="https://user-images.githubusercontent.com/241506/166977513-7cd710a9-7f60-4944-aebe-a658e9f36375.png" alt="OpenID Certification">](#certification)
[Filip Skokan](https://github.com/panva) has [certified](https://openid.net/certification) that [this software](https://github.com/panva/node-openid-client) conforms to the Basic, Implicit, Hybrid, Config, Dynamic, FAPI 1.0, and FAPI 2.0 Relying Party Conformance Profiles of the OpenID Connect™ protocol.
## Sponsor
[<img height="65" align="left" src="https://cdn.auth0.com/blog/github-sponsorships/brand-evolution-logo-Auth0-horizontal-Indigo.png" alt="auth0-logo">][sponsor-auth0] If you want to quickly add OpenID Connect authentication to Node.js apps, feel free to check out Auth0's Node.js SDK and free plan. [Create an Auth0 account; it's free!][sponsor-auth0]<br><br>
## Support
If you or your business use openid-client, please consider becoming a [sponsor][support-sponsor] so I can continue maintaining it and adding new features carefree.
## Documentation
The library exposes what are essentially steps necessary to be done by a relying party consuming
OpenID Connect Authorization Server responses or wrappers around requests to its endpoints. Aside
from a generic OpenID Connect [passport][passport-url] strategy it does not expose any framework
specific middlewares. Those can however be built using the exposed API, one such example is [express-openid-connect][]
- [openid-client API Documentation][documentation]
- [Issuer][documentation-issuer]
- [Client][documentation-client]
- [Customizing][documentation-customizing]
- [TokenSet][documentation-tokenset]
- [Strategy][documentation-strategy]
- [generators][documentation-generators]
- [errors][documentation-errors]
## Install
Node.js LTS releases Codename Erbium and newer LTS releases are supported.
```console
npm install openid-client
```
Note: Other javascript runtimes are not supported.
I recommend [panva/oauth4webapi][oauth4webapi], or a derivate thereof, if you're
looking for a similarly compliant and certified client software that's not dependent
on the Node.js runtime builtins.
## Quick start
Discover an Issuer configuration using its published .well-known endpoints
```js
import { Issuer } from 'openid-client';
const googleIssuer = await Issuer.discover('https://accounts.google.com');
console.log('Discovered issuer %s %O', googleIssuer.issuer, googleIssuer.metadata);
```
### Authorization Code Flow
Authorization Code flow is for obtaining Access Tokens (and optionally Refresh Tokens) to use with
third party APIs securely as well as Refresh Tokens. In this quick start your application also uses
PKCE instead of `state` parameter for CSRF protection.
Create a Client instance for that issuer's authorization server intended for Authorization Code
flow.
**See the [documentation][] for full API details.**
```js
const client = new googleIssuer.Client({
client_id: 'zELcpfANLqY7Oqas',
client_secret: 'TQV5U29k1gHibH5bx1layBo0OSAvAbRT3UYW3EWrSYBB5swxjVfWUa1BS8lqzxG/0v9wruMcrGadany3',
redirect_uris: ['http://localhost:3000/cb'],
response_types: ['code'],
// id_token_signed_response_alg (default "RS256")
// token_endpoint_auth_method (default "client_secret_basic")
}); // => Client
```
When you want to have your end-users authorize you need to send them to the issuer's
`authorization_endpoint`. Consult the web framework of your choice on how to redirect but here's how
to get the authorization endpoint's URL with parameters already encoded in the query to redirect
to.
```js
import { generators } from 'openid-client';
const code_verifier = generators.codeVerifier();
// store the code_verifier in your framework's session mechanism, if it is a cookie based solution
// it should be httpOnly (not readable by javascript) and encrypted.
const code_challenge = generators.codeChallenge(code_verifier);
client.authorizationUrl({
scope: 'openid email profile',
resource: 'https://my.api.example.com/resource/32178',
code_challenge,
code_challenge_method: 'S256',
});
```
When end-users are redirected back to your `redirect_uri` your application consumes the callback and
passes in the `code_verifier` to include it in the authorization code grant token exchange.
```js
const params = client.callbackParams(req);
const tokenSet = await client.callback('https://client.example.com/callback', params, { code_verifier });
console.log('received and validated tokens %j', tokenSet);
console.log('validated ID Token claims %j', tokenSet.claims());
```
You can then call the `userinfo_endpoint`.
```js
const userinfo = await client.userinfo(access_token);
console.log('userinfo %j', userinfo);
```
And later refresh the tokenSet if it had a `refresh_token`.
```js
const tokenSet = await client.refresh(refresh_token);
console.log('refreshed and validated tokens %j', tokenSet);
console.log('refreshed ID Token claims %j', tokenSet.claims());
```
### Implicit ID Token Flow
Implicit `response_type=id_token` flow is perfect for simply authenticating your end-users, assuming
the only job you want done is authenticating the user and then relying on your own session mechanism
with no need for accessing any third party APIs with an Access Token from the Authorization Server.
Create a Client instance for that issuer's authorization server intended for ID Token implicit flow.
**See the [documentation][] for full API details.**
```js
const client = new googleIssuer.Client({
client_id: 'zELcpfANLqY7Oqas',
redirect_uris: ['http://localhost:3000/cb'],
response_types: ['id_token'],
// id_token_signed_response_alg (default "RS256")
}); // => Client
```
When you want to have your end-users authorize you need to send them to the issuer's
`authorization_endpoint`. Consult the web framework of your choice on how to redirect but here's how
to get the authorization endpoint's URL with parameters already encoded in the query to redirect
to.
```js
import { generators } from 'openid-client';
const nonce = generators.nonce();
// store the nonce in your framework's session mechanism, if it is a cookie based solution
// it should be httpOnly (not readable by javascript) and encrypted.
client.authorizationUrl({
scope: 'openid email profile',
response_mode: 'form_post',
nonce,
});
```
When end-users hit back your `redirect_uri` with a POST (authorization request included `form_post`
response mode) your application consumes the callback and passes the `nonce` in to include it in the
ID Token verification steps.
```js
// assumes req.body is populated from your web framework's body parser
const params = client.callbackParams(req);
const tokenSet = await client.callback('https://client.example.com/callback', params, { nonce });
console.log('received and validated tokens %j', tokenSet);
console.log('validated ID Token claims %j', tokenSet.claims());
```
### Device Authorization Grant (Device Flow)
[RFC8628 - OAuth 2.0 Device Authorization Grant (Device Flow)](https://tools.ietf.org/html/rfc8628)
is started by starting a Device Authorization Request.
```js
const handle = await client.deviceAuthorization();
console.log('User Code: ', handle.user_code);
console.log('Verification URI: ', handle.verification_uri);
console.log('Verification URI (complete): ', handle.verification_uri_complete);
```
The handle represents a Device Authorization Response with the `verification_uri`, `user_code` and
other defined response properties.
You will display the instructions to the end-user and have him directed at `verification_uri` or
`verification_uri_complete`, afterwards you can start polling for the Device Access Token Response.
```js
const tokenSet = await handle.poll();
console.log('received tokens %j', tokenSet);
```
This will poll in the defined interval and only resolve with a TokenSet once one is received. This
will handle the defined `authorization_pending` and `slow_down` "soft" errors and continue polling
but upon any other error it will reject. With tokenSet received you can throw away the handle.
### Client Credentials Grant Flow
Client Credentials flow is for obtaining Access Tokens to use with third party APIs on behalf of your application, rather than an end-user which was the case in previous examples.
**See the [documentation](./docs/README.md#clientgrantbody-extras) for full API details.**
```js
const client = new issuer.Client({
client_id: 'zELcpfANLqY7Oqas',
client_secret: 'TQV5U29k1gHibH5bx1layBo0OSAvAbRT3UYW3EWrSYBB5swxjVfWUa1BS8lqzxG/0v9wruMcrGadany3',
});
const tokenSet = await client.grant({
resource: 'urn:example:third-party-api',
grant_type: 'client_credentials'
});
```
## FAQ
#### Semver?
**Yes.** Everything that's either exported in the TypeScript definitions file or
[documented][documentation] is subject to
[Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html). The rest is to be considered
private API and is subject to change between any versions.
#### How do I use it outside of Node.js
It is **only built for Node.js**. Other javascript runtimes are not supported.
I recommend [panva/oauth4webapi][oauth4webapi], or a derivate thereof, if you're
looking for a similarly compliant and certified client software that's not dependent
on the Node.js runtime builtins.
#### How to make the client send client_id and client_secret in the body?
See [Client Authentication Methods (docs)][documentation-methods].
#### Can I adjust the HTTP timeout?
See [Customizing (docs)][documentation-customizing].
[openid-connect]: https://openid.net/connect/
[feature-core]: https://openid.net/specs/openid-connect-core-1_0.html
[feature-discovery]: https://openid.net/specs/openid-connect-discovery-1_0.html
[feature-registration]: https://openid.net/specs/openid-connect-registration-1_0.html
[feature-revocation]: https://tools.ietf.org/html/rfc7009
[feature-introspection]: https://tools.ietf.org/html/rfc7662
[feature-mtls]: https://tools.ietf.org/html/rfc8705
[feature-device-flow]: https://tools.ietf.org/html/rfc8628
[feature-rp-logout]: https://openid.net/specs/openid-connect-rpinitiated-1_0.html
[feature-jarm]: https://openid.net/specs/oauth-v2-jarm.html
[feature-fapi]: https://openid.net/specs/openid-financial-api-part-2-1_0.html
[feature-dpop]: https://www.rfc-editor.org/rfc/rfc9449.html
[feature-par]: https://www.rfc-editor.org/rfc/rfc9126.html
[feature-jar]: https://www.rfc-editor.org/rfc/rfc9101.html
[feature-iss]: https://www.rfc-editor.org/rfc/rfc9207.html
[passport-url]: http://passportjs.org
[npm-url]: https://www.npmjs.com/package/openid-client
[sponsor-auth0]: https://a0.to/try-auth0
[support-sponsor]: https://github.com/sponsors/panva
[documentation]: https://github.com/panva/node-openid-client/blob/main/docs/README.md
[documentation-issuer]: https://github.com/panva/node-openid-client/blob/main/docs/README.md#issuer
[documentation-client]: https://github.com/panva/node-openid-client/blob/main/docs/README.md#client
[documentation-customizing]: https://github.com/panva/node-openid-client/blob/main/docs/README.md#customizing
[documentation-tokenset]: https://github.com/panva/node-openid-client/blob/main/docs/README.md#tokenset
[documentation-strategy]: https://github.com/panva/node-openid-client/blob/main/docs/README.md#strategy
[documentation-errors]: https://github.com/panva/node-openid-client/blob/main/docs/README.md#errors
[documentation-generators]: https://github.com/panva/node-openid-client/blob/main/docs/README.md#generators
[documentation-methods]: https://github.com/panva/node-openid-client/blob/main/docs/README.md#client-authentication-methods
[documentation-webfinger]: https://github.com/panva/node-openid-client/blob/main/docs/README.md#issuerwebfingerinput
[express-openid-connect]: https://www.npmjs.com/package/express-openid-connect
[oauth4webapi]: https://github.com/panva/oauth4webapi#readme
+1884
View File
File diff suppressed because it is too large Load Diff
+125
View File
@@ -0,0 +1,125 @@
const { inspect } = require('util');
const { RPError, OPError } = require('./errors');
const now = require('./helpers/unix_timestamp');
class DeviceFlowHandle {
#aborted;
#client;
#clientAssertionPayload;
#DPoP;
#exchangeBody;
#expires_at;
#interval;
#maxAge;
#response;
constructor({ client, exchangeBody, clientAssertionPayload, response, maxAge, DPoP }) {
['verification_uri', 'user_code', 'device_code'].forEach((prop) => {
if (typeof response[prop] !== 'string' || !response[prop]) {
throw new RPError(
`expected ${prop} string to be returned by Device Authorization Response, got %j`,
response[prop],
);
}
});
if (!Number.isSafeInteger(response.expires_in)) {
throw new RPError(
'expected expires_in number to be returned by Device Authorization Response, got %j',
response.expires_in,
);
}
this.#expires_at = now() + response.expires_in;
this.#client = client;
this.#DPoP = DPoP;
this.#maxAge = maxAge;
this.#exchangeBody = exchangeBody;
this.#clientAssertionPayload = clientAssertionPayload;
this.#response = response;
this.#interval = response.interval * 1000 || 5000;
}
abort() {
this.#aborted = true;
}
async poll({ signal } = {}) {
if ((signal && signal.aborted) || this.#aborted) {
throw new RPError('polling aborted');
}
if (this.expired()) {
throw new RPError(
'the device code %j has expired and the device authorization session has concluded',
this.device_code,
);
}
await new Promise((resolve) => setTimeout(resolve, this.#interval));
let tokenset;
try {
tokenset = await this.#client.grant(
{
...this.#exchangeBody,
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
device_code: this.device_code,
},
{ clientAssertionPayload: this.#clientAssertionPayload, DPoP: this.#DPoP },
);
} catch (err) {
switch (err instanceof OPError && err.error) {
case 'slow_down':
this.#interval += 5000;
case 'authorization_pending':
return this.poll({ signal });
default:
throw err;
}
}
if ('id_token' in tokenset) {
await this.#client.decryptIdToken(tokenset);
await this.#client.validateIdToken(tokenset, undefined, 'token', this.#maxAge);
}
return tokenset;
}
get device_code() {
return this.#response.device_code;
}
get user_code() {
return this.#response.user_code;
}
get verification_uri() {
return this.#response.verification_uri;
}
get verification_uri_complete() {
return this.#response.verification_uri_complete;
}
get expires_in() {
return Math.max.apply(null, [this.#expires_at - now(), 0]);
}
expired() {
return this.expires_in === 0;
}
/* istanbul ignore next */
[inspect.custom]() {
return `${this.constructor.name} ${inspect(this.#response, {
depth: Infinity,
colors: process.stdout.isTTY,
compact: false,
sorted: true,
})}`;
}
}
module.exports = DeviceFlowHandle;
+55
View File
@@ -0,0 +1,55 @@
const { format } = require('util');
class OPError extends Error {
constructor({ error_description, error, error_uri, session_state, state, scope }, response) {
super(!error_description ? error : `${error} (${error_description})`);
Object.assign(
this,
{ error },
error_description && { error_description },
error_uri && { error_uri },
state && { state },
scope && { scope },
session_state && { session_state },
);
if (response) {
Object.defineProperty(this, 'response', {
value: response,
});
}
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
class RPError extends Error {
constructor(...args) {
if (typeof args[0] === 'string') {
super(format(...args));
} else {
const { message, printf, response, ...rest } = args[0];
if (printf) {
super(format(...printf));
} else {
super(message);
}
Object.assign(this, rest);
if (response) {
Object.defineProperty(this, 'response', {
value: response,
});
}
}
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = {
OPError,
RPError,
};
+24
View File
@@ -0,0 +1,24 @@
function assertSigningAlgValuesSupport(endpoint, issuer, properties) {
if (!issuer[`${endpoint}_endpoint`]) return;
const eam = `${endpoint}_endpoint_auth_method`;
const easa = `${endpoint}_endpoint_auth_signing_alg`;
const easavs = `${endpoint}_endpoint_auth_signing_alg_values_supported`;
if (properties[eam] && properties[eam].endsWith('_jwt') && !properties[easa] && !issuer[easavs]) {
throw new TypeError(
`${easavs} must be configured on the issuer if ${easa} is not defined on a client`,
);
}
}
function assertIssuerConfiguration(issuer, endpoint) {
if (!issuer[endpoint]) {
throw new TypeError(`${endpoint} must be configured on the issuer`);
}
}
module.exports = {
assertSigningAlgValuesSupport,
assertIssuerConfiguration,
};
+13
View File
@@ -0,0 +1,13 @@
let encode;
if (Buffer.isEncoding('base64url')) {
encode = (input, encoding = 'utf8') => Buffer.from(input, encoding).toString('base64url');
} else {
const fromBase64 = (base64) => base64.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
encode = (input, encoding = 'utf8') =>
fromBase64(Buffer.from(input, encoding).toString('base64'));
}
const decode = (input) => Buffer.from(input, 'base64');
module.exports.decode = decode;
module.exports.encode = encode;
+208
View File
@@ -0,0 +1,208 @@
const jose = require('jose');
const { RPError } = require('../errors');
const { assertIssuerConfiguration } = require('./assert');
const { random } = require('./generators');
const now = require('./unix_timestamp');
const request = require('./request');
const { keystores } = require('./weak_cache');
const merge = require('./merge');
// TODO: in v6.x additionally encode the `- _ . ! ~ * ' ( )` characters
// https://github.com/panva/node-openid-client/commit/5a2ea80ef5e59ec0c03dbd97d82f551e24a9d348
const formUrlEncode = (value) => encodeURIComponent(value).replace(/%20/g, '+');
async function clientAssertion(endpoint, payload) {
let alg = this[`${endpoint}_endpoint_auth_signing_alg`];
if (!alg) {
assertIssuerConfiguration(
this.issuer,
`${endpoint}_endpoint_auth_signing_alg_values_supported`,
);
}
if (this[`${endpoint}_endpoint_auth_method`] === 'client_secret_jwt') {
if (!alg) {
const supported = this.issuer[`${endpoint}_endpoint_auth_signing_alg_values_supported`];
alg =
Array.isArray(supported) && supported.find((signAlg) => /^HS(?:256|384|512)/.test(signAlg));
}
if (!alg) {
throw new RPError(
`failed to determine a JWS Algorithm to use for ${
this[`${endpoint}_endpoint_auth_method`]
} Client Assertion`,
);
}
return new jose.CompactSign(Buffer.from(JSON.stringify(payload)))
.setProtectedHeader({ alg })
.sign(this.secretForAlg(alg));
}
const keystore = await keystores.get(this);
if (!keystore) {
throw new TypeError('no client jwks provided for signing a client assertion with');
}
if (!alg) {
const supported = this.issuer[`${endpoint}_endpoint_auth_signing_alg_values_supported`];
alg =
Array.isArray(supported) &&
supported.find((signAlg) => keystore.get({ alg: signAlg, use: 'sig' }));
}
if (!alg) {
throw new RPError(
`failed to determine a JWS Algorithm to use for ${
this[`${endpoint}_endpoint_auth_method`]
} Client Assertion`,
);
}
const key = keystore.get({ alg, use: 'sig' });
if (!key) {
throw new RPError(
`no key found in client jwks to sign a client assertion with using alg ${alg}`,
);
}
return new jose.CompactSign(Buffer.from(JSON.stringify(payload)))
.setProtectedHeader({ alg, kid: key.jwk && key.jwk.kid })
.sign(await key.keyObject(alg));
}
async function authFor(endpoint, { clientAssertionPayload } = {}) {
const authMethod = this[`${endpoint}_endpoint_auth_method`];
switch (authMethod) {
case 'self_signed_tls_client_auth':
case 'tls_client_auth':
case 'none':
return { form: { client_id: this.client_id } };
case 'client_secret_post':
if (typeof this.client_secret !== 'string') {
throw new TypeError(
'client_secret_post client authentication method requires a client_secret',
);
}
return { form: { client_id: this.client_id, client_secret: this.client_secret } };
case 'private_key_jwt':
case 'client_secret_jwt': {
const timestamp = now();
const assertion = await clientAssertion.call(this, endpoint, {
iat: timestamp,
exp: timestamp + 60,
jti: random(),
iss: this.client_id,
sub: this.client_id,
aud: this.issuer.issuer,
...clientAssertionPayload,
});
return {
form: {
client_id: this.client_id,
client_assertion: assertion,
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
},
};
}
case 'client_secret_basic': {
// This is correct behaviour, see https://tools.ietf.org/html/rfc6749#section-2.3.1 and the
// related appendix. (also https://github.com/panva/node-openid-client/pull/91)
// > The client identifier is encoded using the
// > "application/x-www-form-urlencoded" encoding algorithm per
// > Appendix B, and the encoded value is used as the username; the client
// > password is encoded using the same algorithm and used as the
// > password.
if (typeof this.client_secret !== 'string') {
throw new TypeError(
'client_secret_basic client authentication method requires a client_secret',
);
}
const encoded = `${formUrlEncode(this.client_id)}:${formUrlEncode(this.client_secret)}`;
const value = Buffer.from(encoded).toString('base64');
return { headers: { Authorization: `Basic ${value}` } };
}
default: {
throw new TypeError(`missing, or unsupported, ${endpoint}_endpoint_auth_method`);
}
}
}
function resolveResponseType() {
const { length, 0: value } = this.response_types;
if (length === 1) {
return value;
}
return undefined;
}
function resolveRedirectUri() {
const { length, 0: value } = this.redirect_uris || [];
if (length === 1) {
return value;
}
return undefined;
}
async function authenticatedPost(
endpoint,
opts,
{ clientAssertionPayload, endpointAuthMethod = endpoint, DPoP } = {},
) {
const auth = await authFor.call(this, endpointAuthMethod, { clientAssertionPayload });
const requestOpts = merge(opts, auth);
const mTLS =
this[`${endpointAuthMethod}_endpoint_auth_method`].includes('tls_client_auth') ||
(endpoint === 'token' && this.tls_client_certificate_bound_access_tokens);
let targetUrl;
if (mTLS && this.issuer.mtls_endpoint_aliases) {
targetUrl = this.issuer.mtls_endpoint_aliases[`${endpoint}_endpoint`];
}
targetUrl = targetUrl || this.issuer[`${endpoint}_endpoint`];
if ('form' in requestOpts) {
for (const [key, value] of Object.entries(requestOpts.form)) {
if (typeof value === 'undefined') {
delete requestOpts.form[key];
}
}
}
return request.call(
this,
{
...requestOpts,
method: 'POST',
url: targetUrl,
headers: {
...(endpoint !== 'revocation'
? {
Accept: 'application/json',
}
: undefined),
...requestOpts.headers,
},
},
{ mTLS, DPoP },
);
}
module.exports = {
resolveResponseType,
resolveRedirectUri,
authFor,
authenticatedPost,
};
+7
View File
@@ -0,0 +1,7 @@
const HTTP_OPTIONS = Symbol();
const CLOCK_TOLERANCE = Symbol();
module.exports = {
CLOCK_TOLERANCE,
HTTP_OPTIONS,
};
+27
View File
@@ -0,0 +1,27 @@
const base64url = require('./base64url');
module.exports = (token) => {
if (typeof token !== 'string' || !token) {
throw new TypeError('JWT must be a string');
}
const { 0: header, 1: payload, 2: signature, length } = token.split('.');
if (length === 5) {
throw new TypeError('encrypted JWTs cannot be decoded');
}
if (length !== 3) {
throw new Error('JWTs must have three components');
}
try {
return {
header: JSON.parse(base64url.decode(header)),
payload: JSON.parse(base64url.decode(payload)),
signature,
};
} catch (err) {
throw new Error('JWT is malformed');
}
};
+1
View File
@@ -0,0 +1 @@
module.exports = globalThis.structuredClone || ((obj) => JSON.parse(JSON.stringify(obj)));
+27
View File
@@ -0,0 +1,27 @@
const isPlainObject = require('./is_plain_object');
function defaults(deep, target, ...sources) {
for (const source of sources) {
if (!isPlainObject(source)) {
continue;
}
for (const [key, value] of Object.entries(source)) {
/* istanbul ignore if */
if (key === '__proto__' || key === 'constructor') {
continue;
}
if (typeof target[key] === 'undefined' && typeof value !== 'undefined') {
target[key] = value;
}
if (deep && isPlainObject(target[key]) && isPlainObject(value)) {
defaults(true, target[key], value);
}
}
}
return target;
}
module.exports = defaults.bind(undefined, false);
module.exports.deep = defaults.bind(undefined, true);
+14
View File
@@ -0,0 +1,14 @@
const { createHash, randomBytes } = require('crypto');
const base64url = require('./base64url');
const random = (bytes = 32) => base64url.encode(randomBytes(bytes));
module.exports = {
random,
state: random,
nonce: random,
codeVerifier: random,
codeChallenge: (codeVerifier) =>
base64url.encode(createHash('sha256').update(codeVerifier).digest()),
};
+4
View File
@@ -0,0 +1,4 @@
const util = require('util');
const crypto = require('crypto');
module.exports = util.types.isKeyObject || ((obj) => obj && obj instanceof crypto.KeyObject);
+1
View File
@@ -0,0 +1 @@
module.exports = (a) => !!a && a.constructor === Object;
+111
View File
@@ -0,0 +1,111 @@
const objectHash = require('object-hash');
const LRU = require('lru-cache');
const { RPError } = require('../errors');
const { assertIssuerConfiguration } = require('./assert');
const KeyStore = require('./keystore');
const { keystores } = require('./weak_cache');
const processResponse = require('./process_response');
const request = require('./request');
const inFlight = new WeakMap();
const caches = new WeakMap();
const lrus = (ctx) => {
if (!caches.has(ctx)) {
caches.set(ctx, new LRU({ max: 100 }));
}
return caches.get(ctx);
};
async function getKeyStore(reload = false) {
assertIssuerConfiguration(this, 'jwks_uri');
const keystore = keystores.get(this);
const cache = lrus(this);
if (reload || !keystore) {
if (inFlight.has(this)) {
return inFlight.get(this);
}
cache.reset();
inFlight.set(
this,
(async () => {
const response = await request
.call(this, {
method: 'GET',
responseType: 'json',
url: this.jwks_uri,
headers: {
Accept: 'application/json, application/jwk-set+json',
},
})
.finally(() => {
inFlight.delete(this);
});
const jwks = processResponse(response);
const joseKeyStore = KeyStore.fromJWKS(jwks, { onlyPublic: true });
cache.set('throttle', true, 60 * 1000);
keystores.set(this, joseKeyStore);
return joseKeyStore;
})(),
);
return inFlight.get(this);
}
return keystore;
}
async function queryKeyStore({ kid, kty, alg, use }, { allowMulti = false } = {}) {
const cache = lrus(this);
const def = {
kid,
kty,
alg,
use,
};
const defHash = objectHash(def, {
algorithm: 'sha256',
ignoreUnknown: true,
unorderedArrays: true,
unorderedSets: true,
respectType: false,
});
// refresh keystore on every unknown key but also only upto once every minute
const freshJwksUri = cache.get(defHash) || cache.get('throttle');
const keystore = await getKeyStore.call(this, !freshJwksUri);
const keys = keystore.all(def);
delete def.use;
if (keys.length === 0) {
throw new RPError({
printf: ["no valid key found in issuer's jwks_uri for key parameters %j", def],
jwks: keystore,
});
}
if (!allowMulti && keys.length > 1 && !kid) {
throw new RPError({
printf: [
"multiple matching keys found in issuer's jwks_uri for key parameters %j, kid must be provided in this case",
def,
],
jwks: keystore,
});
}
cache.set(defHash, true);
return keys;
}
module.exports.queryKeyStore = queryKeyStore;
module.exports.keystore = getKeyStore;
+298
View File
@@ -0,0 +1,298 @@
const jose = require('jose');
const clone = require('./deep_clone');
const isPlainObject = require('./is_plain_object');
const internal = Symbol();
const keyscore = (key, { alg, use }) => {
let score = 0;
if (alg && key.alg) {
score++;
}
if (use && key.use) {
score++;
}
return score;
};
function getKtyFromAlg(alg) {
switch (typeof alg === 'string' && alg.slice(0, 2)) {
case 'RS':
case 'PS':
return 'RSA';
case 'ES':
return 'EC';
case 'Ed':
return 'OKP';
default:
return undefined;
}
}
function getAlgorithms(use, alg, kty, crv) {
// Ed25519, Ed448, and secp256k1 always have "alg"
// OKP always has "use"
if (alg) {
return new Set([alg]);
}
switch (kty) {
case 'EC': {
let algs = [];
if (use === 'enc' || use === undefined) {
algs = algs.concat(['ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW']);
}
if (use === 'sig' || use === undefined) {
switch (crv) {
case 'P-256':
case 'P-384':
algs = algs.concat([`ES${crv.slice(-3)}`]);
break;
case 'P-521':
algs = algs.concat(['ES512']);
break;
case 'secp256k1':
if (jose.cryptoRuntime === 'node:crypto') {
algs = algs.concat(['ES256K']);
}
break;
}
}
return new Set(algs);
}
case 'OKP': {
return new Set(['ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW']);
}
case 'RSA': {
let algs = [];
if (use === 'enc' || use === undefined) {
algs = algs.concat(['RSA-OAEP', 'RSA-OAEP-256', 'RSA-OAEP-384', 'RSA-OAEP-512']);
if (jose.cryptoRuntime === 'node:crypto') {
algs = algs.concat(['RSA1_5']);
}
}
if (use === 'sig' || use === undefined) {
algs = algs.concat(['PS256', 'PS384', 'PS512', 'RS256', 'RS384', 'RS512']);
}
return new Set(algs);
}
default:
throw new Error('unreachable');
}
}
module.exports = class KeyStore {
#keys;
constructor(i, keys) {
if (i !== internal) throw new Error('invalid constructor call');
this.#keys = keys;
}
toJWKS() {
return {
keys: this.map(({ jwk: { d, p, q, dp, dq, qi, ...jwk } }) => jwk),
};
}
all({ alg, kid, use } = {}) {
if (!use || !alg) {
throw new Error();
}
const kty = getKtyFromAlg(alg);
const search = { alg, use };
return this.filter((key) => {
let candidate = true;
if (candidate && kty !== undefined && key.jwk.kty !== kty) {
candidate = false;
}
if (candidate && kid !== undefined && key.jwk.kid !== kid) {
candidate = false;
}
if (candidate && use !== undefined && key.jwk.use !== undefined && key.jwk.use !== use) {
candidate = false;
}
if (candidate && key.jwk.alg && key.jwk.alg !== alg) {
candidate = false;
} else if (!key.algorithms.has(alg)) {
candidate = false;
}
return candidate;
}).sort((first, second) => keyscore(second, search) - keyscore(first, search));
}
get(...args) {
return this.all(...args)[0];
}
static async fromJWKS(jwks, { onlyPublic = false, onlyPrivate = false } = {}) {
if (
!isPlainObject(jwks) ||
!Array.isArray(jwks.keys) ||
jwks.keys.some((k) => !isPlainObject(k) || !('kty' in k))
) {
throw new TypeError('jwks must be a JSON Web Key Set formatted object');
}
const keys = [];
for (let jwk of jwks.keys) {
jwk = clone(jwk);
const { kty, kid, crv } = jwk;
let { alg, use } = jwk;
if (typeof kty !== 'string' || !kty) {
continue;
}
if (use !== undefined && use !== 'sig' && use !== 'enc') {
continue;
}
if (typeof alg !== 'string' && alg !== undefined) {
continue;
}
if (typeof kid !== 'string' && kid !== undefined) {
continue;
}
if (kty === 'EC' && use === 'sig') {
switch (crv) {
case 'P-256':
alg = 'ES256';
break;
case 'P-384':
alg = 'ES384';
break;
case 'P-521':
alg = 'ES512';
break;
default:
break;
}
}
if (crv === 'secp256k1') {
use = 'sig';
alg = 'ES256K';
}
if (kty === 'OKP') {
switch (crv) {
case 'Ed25519':
case 'Ed448':
use = 'sig';
alg = 'EdDSA';
break;
case 'X25519':
case 'X448':
use = 'enc';
break;
default:
break;
}
}
if (alg && !use) {
switch (true) {
case alg.startsWith('ECDH'):
use = 'enc';
break;
case alg.startsWith('RSA'):
use = 'enc';
break;
default:
break;
}
}
if (onlyPrivate && (jwk.kty === 'oct' || !jwk.d)) {
throw new Error('jwks must only contain private keys');
}
if (onlyPublic && (jwk.d || jwk.k)) {
continue;
}
keys.push({
jwk: { ...jwk, alg, use },
async keyObject(alg) {
if (this[alg]) {
return this[alg];
}
const keyObject = await jose.importJWK(this.jwk, alg);
this[alg] = keyObject;
return keyObject;
},
get algorithms() {
Object.defineProperty(this, 'algorithms', {
value: getAlgorithms(this.jwk.use, this.jwk.alg, this.jwk.kty, this.jwk.crv),
enumerable: true,
configurable: false,
});
return this.algorithms;
},
});
}
return new this(internal, keys);
}
filter(...args) {
return this.#keys.filter(...args);
}
find(...args) {
return this.#keys.find(...args);
}
every(...args) {
return this.#keys.every(...args);
}
some(...args) {
return this.#keys.some(...args);
}
map(...args) {
return this.#keys.map(...args);
}
forEach(...args) {
return this.#keys.forEach(...args);
}
reduce(...args) {
return this.#keys.reduce(...args);
}
sort(...args) {
return this.#keys.sort(...args);
}
*[Symbol.iterator]() {
for (const key of this.#keys) {
yield key;
}
}
};
+24
View File
@@ -0,0 +1,24 @@
const isPlainObject = require('./is_plain_object');
function merge(target, ...sources) {
for (const source of sources) {
if (!isPlainObject(source)) {
continue;
}
for (const [key, value] of Object.entries(source)) {
/* istanbul ignore if */
if (key === '__proto__' || key === 'constructor') {
continue;
}
if (isPlainObject(target[key]) && isPlainObject(value)) {
target[key] = merge(target[key], value);
} else if (typeof value !== 'undefined') {
target[key] = value;
}
}
}
return target;
}
module.exports = merge;
+9
View File
@@ -0,0 +1,9 @@
module.exports = function pick(object, ...paths) {
const obj = {};
for (const path of paths) {
if (object[path] !== undefined) {
obj[path] = object[path];
}
}
return obj;
};
+71
View File
@@ -0,0 +1,71 @@
const { STATUS_CODES } = require('http');
const { format } = require('util');
const { OPError } = require('../errors');
const parseWwwAuthenticate = require('./www_authenticate_parser');
const throwAuthenticateErrors = (response) => {
const params = parseWwwAuthenticate(response.headers['www-authenticate']);
if (params.error) {
throw new OPError(params, response);
}
};
const isStandardBodyError = (response) => {
let result = false;
try {
let jsonbody;
if (typeof response.body !== 'object' || Buffer.isBuffer(response.body)) {
jsonbody = JSON.parse(response.body);
} else {
jsonbody = response.body;
}
result = typeof jsonbody.error === 'string' && jsonbody.error.length;
if (result) Object.defineProperty(response, 'body', { value: jsonbody, configurable: true });
} catch (err) {}
return result;
};
function processResponse(response, { statusCode = 200, body = true, bearer = false } = {}) {
if (response.statusCode !== statusCode) {
if (bearer) {
throwAuthenticateErrors(response);
}
if (isStandardBodyError(response)) {
throw new OPError(response.body, response);
}
throw new OPError(
{
error: format(
'expected %i %s, got: %i %s',
statusCode,
STATUS_CODES[statusCode],
response.statusCode,
STATUS_CODES[response.statusCode],
),
},
response,
);
}
if (body && !response.body) {
throw new OPError(
{
error: format(
'expected %i %s with body but no body was returned',
statusCode,
STATUS_CODES[statusCode],
),
},
response,
);
}
return response.body;
}
module.exports = processResponse;
+200
View File
@@ -0,0 +1,200 @@
const assert = require('assert');
const querystring = require('querystring');
const http = require('http');
const https = require('https');
const { once } = require('events');
const { URL } = require('url');
const LRU = require('lru-cache');
const pkg = require('../../package.json');
const { RPError } = require('../errors');
const pick = require('./pick');
const { deep: defaultsDeep } = require('./defaults');
const { HTTP_OPTIONS } = require('./consts');
let DEFAULT_HTTP_OPTIONS;
const NQCHAR = /^[\x21\x23-\x5B\x5D-\x7E]+$/;
const allowed = [
'agent',
'ca',
'cert',
'crl',
'headers',
'key',
'lookup',
'passphrase',
'pfx',
'timeout',
];
const setDefaults = (props, options) => {
DEFAULT_HTTP_OPTIONS = defaultsDeep(
{},
props.length ? pick(options, ...props) : options,
DEFAULT_HTTP_OPTIONS,
);
};
setDefaults([], {
headers: {
'User-Agent': `${pkg.name}/${pkg.version} (${pkg.homepage})`,
'Accept-Encoding': 'identity',
},
timeout: 3500,
});
function send(req, body, contentType) {
if (contentType) {
req.removeHeader('content-type');
req.setHeader('content-type', contentType);
}
if (body) {
req.removeHeader('content-length');
req.setHeader('content-length', Buffer.byteLength(body));
req.write(body);
}
req.end();
}
const nonces = new LRU({ max: 100 });
module.exports = async function request(options, { accessToken, mTLS = false, DPoP } = {}) {
let url;
try {
url = new URL(options.url);
delete options.url;
assert(/^(https?:)$/.test(url.protocol));
} catch (err) {
throw new TypeError('only valid absolute URLs can be requested');
}
const optsFn = this[HTTP_OPTIONS];
let opts = options;
const nonceKey = `${url.origin}${url.pathname}`;
if (DPoP && 'dpopProof' in this) {
opts.headers = opts.headers || {};
opts.headers.DPoP = await this.dpopProof(
{
htu: `${url.origin}${url.pathname}`,
htm: options.method || 'GET',
nonce: nonces.get(nonceKey),
},
DPoP,
accessToken,
);
}
let userOptions;
if (optsFn) {
userOptions = pick(
optsFn.call(this, url, defaultsDeep({}, opts, DEFAULT_HTTP_OPTIONS)),
...allowed,
);
}
opts = defaultsDeep({}, userOptions, opts, DEFAULT_HTTP_OPTIONS);
if (mTLS && !opts.pfx && !(opts.key && opts.cert)) {
throw new TypeError('mutual-TLS certificate and key not set');
}
if (opts.searchParams) {
for (const [key, value] of Object.entries(opts.searchParams)) {
url.searchParams.delete(key);
url.searchParams.set(key, value);
}
}
let responseType;
let form;
let json;
let body;
({ form, responseType, json, body, ...opts } = opts);
for (const [key, value] of Object.entries(opts.headers || {})) {
if (value === undefined) {
delete opts.headers[key];
}
}
let response;
const req = (url.protocol === 'https:' ? https.request : http.request)(url.href, opts);
return (async () => {
if (json) {
send(req, JSON.stringify(json), 'application/json');
} else if (form) {
send(req, querystring.stringify(form), 'application/x-www-form-urlencoded');
} else if (body) {
send(req, body);
} else {
send(req);
}
[response] = await Promise.race([once(req, 'response'), once(req, 'timeout')]);
// timeout reached
if (!response) {
req.destroy();
throw new RPError(`outgoing request timed out after ${opts.timeout}ms`);
}
const parts = [];
for await (const part of response) {
parts.push(part);
}
if (parts.length) {
switch (responseType) {
case 'json': {
Object.defineProperty(response, 'body', {
get() {
let value = Buffer.concat(parts);
try {
value = JSON.parse(value);
} catch (err) {
Object.defineProperty(err, 'response', { value: response });
throw err;
} finally {
Object.defineProperty(response, 'body', { value, configurable: true });
}
return value;
},
configurable: true,
});
break;
}
case undefined:
case 'buffer': {
Object.defineProperty(response, 'body', {
get() {
const value = Buffer.concat(parts);
Object.defineProperty(response, 'body', { value, configurable: true });
return value;
},
configurable: true,
});
break;
}
default:
throw new TypeError('unsupported responseType request option');
}
}
return response;
})()
.catch((err) => {
if (response) Object.defineProperty(err, 'response', { value: response });
throw err;
})
.finally(() => {
const dpopNonce = response && response.headers['dpop-nonce'];
if (dpopNonce && NQCHAR.test(dpopNonce)) {
nonces.set(nonceKey, dpopNonce);
}
});
};
module.exports.setDefaults = setDefaults.bind(undefined, allowed);
+1
View File
@@ -0,0 +1 @@
module.exports = () => Math.floor(Date.now() / 1000);
+1
View File
@@ -0,0 +1 @@
module.exports.keystores = new WeakMap();
+71
View File
@@ -0,0 +1,71 @@
// Credit: https://github.com/rohe/pyoidc/blob/master/src/oic/utils/webfinger.py
// -- Normalization --
// A string of any other type is interpreted as a URI either the form of scheme
// "://" authority path-abempty [ "?" query ] [ "#" fragment ] or authority
// path-abempty [ "?" query ] [ "#" fragment ] per RFC 3986 [RFC3986] and is
// normalized according to the following rules:
//
// If the user input Identifier does not have an RFC 3986 [RFC3986] scheme
// portion, the string is interpreted as [userinfo "@"] host [":" port]
// path-abempty [ "?" query ] [ "#" fragment ] per RFC 3986 [RFC3986].
// If the userinfo component is present and all of the path component, query
// component, and port component are empty, the acct scheme is assumed. In this
// case, the normalized URI is formed by prefixing acct: to the string as the
// scheme. Per the 'acct' URI Scheme [ID.ietfappsawgaccturi], if there is an
// at-sign character ('@') in the userinfo component, it needs to be
// percent-encoded as described in RFC 3986 [RFC3986].
// For all other inputs without a scheme portion, the https scheme is assumed,
// and the normalized URI is formed by prefixing https:// to the string as the
// scheme.
// If the resulting URI contains a fragment portion, it MUST be stripped off
// together with the fragment delimiter character "#".
// The WebFinger [ID.ietfappsawgwebfinger] Resource in this case is the
// resulting URI, and the WebFinger Host is the authority component.
//
// Note: Since the definition of authority in RFC 3986 [RFC3986] is
// [ userinfo "@" ] host [ ":" port ], it is legal to have a user input
// identifier like userinfo@host:port, e.g., alice@example.com:8080.
const PORT = /^\d+$/;
function hasScheme(input) {
if (input.includes('://')) return true;
const authority = input.replace(/(\/|\?)/g, '#').split('#')[0];
if (authority.includes(':')) {
const index = authority.indexOf(':');
const hostOrPort = authority.slice(index + 1);
if (!PORT.test(hostOrPort)) {
return true;
}
}
return false;
}
function acctSchemeAssumed(input) {
if (!input.includes('@')) return false;
const parts = input.split('@');
const host = parts[parts.length - 1];
return !(host.includes(':') || host.includes('/') || host.includes('?'));
}
function normalize(input) {
if (typeof input !== 'string') {
throw new TypeError('input must be a string');
}
let output;
if (hasScheme(input)) {
output = input;
} else if (acctSchemeAssumed(input)) {
output = `acct:${input}`;
} else {
output = `https://${input}`;
}
return output.split('#')[0];
}
module.exports = normalize;
@@ -0,0 +1,14 @@
const REGEXP = /(\w+)=("[^"]*")/g;
module.exports = (wwwAuthenticate) => {
const params = {};
try {
while (REGEXP.exec(wwwAuthenticate) !== null) {
if (RegExp.$1 && RegExp.$2) {
params[RegExp.$1] = RegExp.$2.slice(1, -1);
}
}
} catch (err) {}
return params;
};
+23
View File
@@ -0,0 +1,23 @@
const Issuer = require('./issuer');
const { OPError, RPError } = require('./errors');
const Strategy = require('./passport_strategy');
const TokenSet = require('./token_set');
const { CLOCK_TOLERANCE, HTTP_OPTIONS } = require('./helpers/consts');
const generators = require('./helpers/generators');
const { setDefaults } = require('./helpers/request');
module.exports = {
Issuer,
Strategy,
TokenSet,
errors: {
OPError,
RPError,
},
custom: {
setHttpOptionsDefaults: setDefaults,
http_options: HTTP_OPTIONS,
clock_tolerance: CLOCK_TOLERANCE,
},
generators,
};
+9
View File
@@ -0,0 +1,9 @@
import mod from './index.js';
export default mod;
export const Issuer = mod.Issuer;
export const Strategy = mod.Strategy;
export const TokenSet = mod.TokenSet;
export const errors = mod.errors;
export const custom = mod.custom;
export const generators = mod.generators;
+192
View File
@@ -0,0 +1,192 @@
const { inspect } = require('util');
const url = require('url');
const { RPError } = require('./errors');
const getClient = require('./client');
const registry = require('./issuer_registry');
const processResponse = require('./helpers/process_response');
const webfingerNormalize = require('./helpers/webfinger_normalize');
const request = require('./helpers/request');
const clone = require('./helpers/deep_clone');
const { keystore } = require('./helpers/issuer');
const AAD_MULTITENANT_DISCOVERY = [
'https://login.microsoftonline.com/common/.well-known/openid-configuration',
'https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration',
'https://login.microsoftonline.com/organizations/v2.0/.well-known/openid-configuration',
'https://login.microsoftonline.com/consumers/v2.0/.well-known/openid-configuration',
];
const AAD_MULTITENANT = Symbol();
const ISSUER_DEFAULTS = {
claim_types_supported: ['normal'],
claims_parameter_supported: false,
grant_types_supported: ['authorization_code', 'implicit'],
request_parameter_supported: false,
request_uri_parameter_supported: true,
require_request_uri_registration: false,
response_modes_supported: ['query', 'fragment'],
token_endpoint_auth_methods_supported: ['client_secret_basic'],
};
class Issuer {
#metadata;
constructor(meta = {}) {
const aadIssValidation = meta[AAD_MULTITENANT];
delete meta[AAD_MULTITENANT];
['introspection', 'revocation'].forEach((endpoint) => {
// if intro/revocation endpoint auth specific meta is missing use the token ones if they
// are defined
if (
meta[`${endpoint}_endpoint`] &&
meta[`${endpoint}_endpoint_auth_methods_supported`] === undefined &&
meta[`${endpoint}_endpoint_auth_signing_alg_values_supported`] === undefined
) {
if (meta.token_endpoint_auth_methods_supported) {
meta[`${endpoint}_endpoint_auth_methods_supported`] =
meta.token_endpoint_auth_methods_supported;
}
if (meta.token_endpoint_auth_signing_alg_values_supported) {
meta[`${endpoint}_endpoint_auth_signing_alg_values_supported`] =
meta.token_endpoint_auth_signing_alg_values_supported;
}
}
});
this.#metadata = new Map();
Object.entries(meta).forEach(([key, value]) => {
this.#metadata.set(key, value);
if (!this[key]) {
Object.defineProperty(this, key, {
get() {
return this.#metadata.get(key);
},
enumerable: true,
});
}
});
registry.set(this.issuer, this);
const Client = getClient(this, aadIssValidation);
Object.defineProperties(this, {
Client: { value: Client, enumerable: true },
FAPI1Client: { value: class FAPI1Client extends Client {}, enumerable: true },
FAPI2Client: { value: class FAPI2Client extends Client {}, enumerable: true },
});
}
get metadata() {
return clone(Object.fromEntries(this.#metadata.entries()));
}
static async webfinger(input) {
const resource = webfingerNormalize(input);
const { host } = url.parse(resource);
const webfingerUrl = `https://${host}/.well-known/webfinger`;
const response = await request.call(this, {
method: 'GET',
url: webfingerUrl,
responseType: 'json',
searchParams: { resource, rel: 'http://openid.net/specs/connect/1.0/issuer' },
headers: {
Accept: 'application/json',
},
});
const body = processResponse(response);
const location =
Array.isArray(body.links) &&
body.links.find(
(link) =>
typeof link === 'object' &&
link.rel === 'http://openid.net/specs/connect/1.0/issuer' &&
link.href,
);
if (!location) {
throw new RPError({
message: 'no issuer found in webfinger response',
body,
});
}
if (typeof location.href !== 'string' || !location.href.startsWith('https://')) {
throw new RPError({
printf: ['invalid issuer location %s', location.href],
body,
});
}
const expectedIssuer = location.href;
if (registry.has(expectedIssuer)) {
return registry.get(expectedIssuer);
}
const issuer = await this.discover(expectedIssuer);
if (issuer.issuer !== expectedIssuer) {
registry.del(issuer.issuer);
throw new RPError(
'discovered issuer mismatch, expected %s, got: %s',
expectedIssuer,
issuer.issuer,
);
}
return issuer;
}
static async discover(uri) {
const wellKnownUri = resolveWellKnownUri(uri);
const response = await request.call(this, {
method: 'GET',
responseType: 'json',
url: wellKnownUri,
headers: {
Accept: 'application/json',
},
});
const body = processResponse(response);
return new Issuer({
...ISSUER_DEFAULTS,
...body,
[AAD_MULTITENANT]: !!AAD_MULTITENANT_DISCOVERY.find((discoveryURL) =>
wellKnownUri.startsWith(discoveryURL),
),
});
}
async reloadJwksUri() {
await keystore.call(this, true);
}
/* istanbul ignore next */
[inspect.custom]() {
return `${this.constructor.name} ${inspect(this.metadata, {
depth: Infinity,
colors: process.stdout.isTTY,
compact: false,
sorted: true,
})}`;
}
}
function resolveWellKnownUri(uri) {
const parsed = url.parse(uri);
if (parsed.pathname.includes('/.well-known/')) {
return uri;
} else {
let pathname;
if (parsed.pathname.endsWith('/')) {
pathname = `${parsed.pathname}.well-known/openid-configuration`;
} else {
pathname = `${parsed.pathname}/.well-known/openid-configuration`;
}
return url.format({ ...parsed, pathname });
}
}
module.exports = Issuer;
+3
View File
@@ -0,0 +1,3 @@
const LRU = require('lru-cache');
module.exports = new LRU({ max: 100 });
+205
View File
@@ -0,0 +1,205 @@
const url = require('url');
const { format } = require('util');
const cloneDeep = require('./helpers/deep_clone');
const { RPError, OPError } = require('./errors');
const { BaseClient } = require('./client');
const { random, codeChallenge } = require('./helpers/generators');
const pick = require('./helpers/pick');
const { resolveResponseType, resolveRedirectUri } = require('./helpers/client');
function verified(err, user, info = {}) {
if (err) {
this.error(err);
} else if (!user) {
this.fail(info);
} else {
this.success(user, info);
}
}
function OpenIDConnectStrategy(
{ client, params = {}, passReqToCallback = false, sessionKey, usePKCE = true, extras = {} } = {},
verify,
) {
if (!(client instanceof BaseClient)) {
throw new TypeError('client must be an instance of openid-client Client');
}
if (typeof verify !== 'function') {
throw new TypeError('verify callback must be a function');
}
if (!client.issuer || !client.issuer.issuer) {
throw new TypeError('client must have an issuer with an identifier');
}
this._client = client;
this._issuer = client.issuer;
this._verify = verify;
this._passReqToCallback = passReqToCallback;
this._usePKCE = usePKCE;
this._key = sessionKey || `oidc:${url.parse(this._issuer.issuer).hostname}`;
this._params = cloneDeep(params);
// state and nonce are handled in authenticate()
delete this._params.state;
delete this._params.nonce;
this._extras = cloneDeep(extras);
if (!this._params.response_type) this._params.response_type = resolveResponseType.call(client);
if (!this._params.redirect_uri) this._params.redirect_uri = resolveRedirectUri.call(client);
if (!this._params.scope) this._params.scope = 'openid';
if (this._usePKCE === true) {
const supportedMethods = Array.isArray(this._issuer.code_challenge_methods_supported)
? this._issuer.code_challenge_methods_supported
: false;
if (supportedMethods && supportedMethods.includes('S256')) {
this._usePKCE = 'S256';
} else if (supportedMethods && supportedMethods.includes('plain')) {
this._usePKCE = 'plain';
} else if (supportedMethods) {
throw new TypeError(
'neither code_challenge_method supported by the client is supported by the issuer',
);
} else {
this._usePKCE = 'S256';
}
} else if (typeof this._usePKCE === 'string' && !['plain', 'S256'].includes(this._usePKCE)) {
throw new TypeError(`${this._usePKCE} is not valid/implemented PKCE code_challenge_method`);
}
this.name = url.parse(client.issuer.issuer).hostname;
}
OpenIDConnectStrategy.prototype.authenticate = function authenticate(req, options) {
(async () => {
const client = this._client;
if (!req.session) {
throw new TypeError('authentication requires session support');
}
const reqParams = client.callbackParams(req);
const sessionKey = this._key;
const { 0: parameter, length } = Object.keys(reqParams);
/**
* Start authentication request if this has no authorization response parameters or
* this might a login initiated from a third party as per
* https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin.
*/
if (length === 0 || (length === 1 && parameter === 'iss')) {
// provide options object with extra authentication parameters
const params = {
state: random(),
...this._params,
...options,
};
if (!params.nonce && params.response_type.includes('id_token')) {
params.nonce = random();
}
req.session[sessionKey] = pick(params, 'nonce', 'state', 'max_age', 'response_type');
if (this._usePKCE && params.response_type.includes('code')) {
const verifier = random();
req.session[sessionKey].code_verifier = verifier;
switch (this._usePKCE) {
case 'S256':
params.code_challenge = codeChallenge(verifier);
params.code_challenge_method = 'S256';
break;
case 'plain':
params.code_challenge = verifier;
break;
}
}
this.redirect(client.authorizationUrl(params));
return;
}
/* end authentication request */
/* start authentication response */
const session = req.session[sessionKey];
if (Object.keys(session || {}).length === 0) {
throw new Error(
format(
'did not find expected authorization request details in session, req.session["%s"] is %j',
sessionKey,
session,
),
);
}
const {
state,
nonce,
max_age: maxAge,
code_verifier: codeVerifier,
response_type: responseType,
} = session;
try {
delete req.session[sessionKey];
} catch (err) {}
const opts = {
redirect_uri: this._params.redirect_uri,
...options,
};
const checks = {
state,
nonce,
max_age: maxAge,
code_verifier: codeVerifier,
response_type: responseType,
};
const tokenset = await client.callback(opts.redirect_uri, reqParams, checks, this._extras);
const passReq = this._passReqToCallback;
const loadUserinfo = this._verify.length > (passReq ? 3 : 2) && client.issuer.userinfo_endpoint;
const args = [tokenset, verified.bind(this)];
if (loadUserinfo) {
if (!tokenset.access_token) {
throw new RPError({
message:
'expected access_token to be returned when asking for userinfo in verify callback',
tokenset,
});
}
const userinfo = await client.userinfo(tokenset);
args.splice(1, 0, userinfo);
}
if (passReq) {
args.unshift(req);
}
this._verify(...args);
/* end authentication response */
})().catch((error) => {
if (
(error instanceof OPError &&
error.error !== 'server_error' &&
!error.error.startsWith('invalid')) ||
error instanceof RPError
) {
this.fail(error);
} else {
this.error(error);
}
});
};
module.exports = OpenIDConnectStrategy;
+35
View File
@@ -0,0 +1,35 @@
const base64url = require('./helpers/base64url');
const now = require('./helpers/unix_timestamp');
class TokenSet {
constructor(values) {
Object.assign(this, values);
const { constructor, ...properties } = Object.getOwnPropertyDescriptors(
this.constructor.prototype,
);
Object.defineProperties(this, properties);
}
set expires_in(value) {
this.expires_at = now() + Number(value);
}
get expires_in() {
return Math.max.apply(null, [this.expires_at - now(), 0]);
}
expired() {
return this.expires_in === 0;
}
claims() {
if (!this.id_token) {
throw new TypeError('id_token not present in TokenSet');
}
return JSON.parse(base64url.decode(this.id_token.split('.')[1]));
}
}
module.exports = TokenSet;
+106
View File
@@ -0,0 +1,106 @@
{
"name": "openid-client",
"version": "5.7.1",
"description": "OpenID Connect Relying Party (RP, Client) implementation for Node.js runtime, supports passportjs",
"keywords": [
"auth",
"authentication",
"basic",
"certified",
"client",
"connect",
"dynamic",
"electron",
"hybrid",
"identity",
"implicit",
"oauth",
"oauth2",
"oidc",
"openid",
"passport",
"relying party",
"strategy"
],
"homepage": "https://github.com/panva/openid-client",
"repository": "panva/openid-client",
"funding": {
"url": "https://github.com/sponsors/panva"
},
"license": "MIT",
"author": "Filip Skokan <panva.ip@gmail.com>",
"exports": {
"types": "./types/index.d.ts",
"import": "./lib/index.mjs",
"require": "./lib/index.js"
},
"main": "./lib/index.js",
"types": "./types/index.d.ts",
"files": [
"lib",
"types/index.d.ts"
],
"scripts": {
"format": "npx prettier --loglevel silent --write ./lib ./test ./certification ./types",
"test": "mocha test/**/*.test.js"
},
"dependencies": {
"jose": "^4.15.9",
"lru-cache": "^6.0.0",
"object-hash": "^2.2.0",
"oidc-token-hash": "^5.0.3"
},
"devDependencies": {
"@types/node": "^16.18.106",
"@types/passport": "^1.0.16",
"base64url": "^3.0.1",
"chai": "^4.5.0",
"mocha": "^10.7.3",
"nock": "^13.5.5",
"prettier": "^2.8.8",
"readable-mock-req": "^0.2.2",
"sinon": "^9.2.4",
"timekeeper": "^2.3.1"
},
"standard-version": {
"scripts": {
"postchangelog": "sed -i '' -e 's/### \\[/## [/g' CHANGELOG.md"
},
"types": [
{
"type": "feat",
"section": "Features"
},
{
"type": "fix",
"section": "Fixes"
},
{
"type": "chore",
"hidden": true
},
{
"type": "docs",
"hidden": true
},
{
"type": "style",
"hidden": true
},
{
"type": "refactor",
"section": "Refactor",
"hidden": false
},
{
"type": "perf",
"section": "Performance",
"hidden": false
},
{
"type": "test",
"hidden": true
}
]
}
}
+623
View File
@@ -0,0 +1,623 @@
/// <reference types="node" />
// TypeScript Version: 3.6
import * as http from 'http';
import * as https from 'https';
import * as http2 from 'http2';
import { URL } from 'url';
import * as jose from 'jose';
import * as crypto from 'crypto';
import { format } from 'util';
export type HttpOptions = Partial<
Pick<
https.RequestOptions,
| 'agent'
| 'ca'
| 'cert'
| 'crl'
| 'headers'
| 'key'
| 'lookup'
| 'passphrase'
| 'pfx'
| 'timeout'
>
>;
export type RetryFunction = (retry: number, error: Error) => number;
export type CustomHttpOptionsProvider = (
url: URL,
options: Omit<https.RequestOptions, keyof URL>,
) => HttpOptions;
export type TokenTypeHint = 'access_token' | 'refresh_token' | string;
export type DPoPInput = crypto.KeyObject | Parameters<typeof crypto.createPrivateKey>[0];
interface UnknownObject {
[key: string]: unknown;
}
export const custom: {
setHttpOptionsDefaults(params: HttpOptions): undefined;
readonly http_options: unique symbol;
readonly clock_tolerance: unique symbol;
};
export type ResponseType = 'code' | 'id_token' | 'code id_token' | 'none' | string;
export type ClientAuthMethod =
| 'client_secret_basic'
| 'client_secret_post'
| 'client_secret_jwt'
| 'private_key_jwt'
| 'tls_client_auth'
| 'self_signed_tls_client_auth'
| 'none';
export interface ClientMetadata {
// important
client_id: string;
id_token_signed_response_alg?: string;
token_endpoint_auth_method?: ClientAuthMethod;
client_secret?: string;
redirect_uris?: string[];
response_types?: ResponseType[];
post_logout_redirect_uris?: string[];
default_max_age?: number;
require_auth_time?: boolean;
tls_client_certificate_bound_access_tokens?: boolean;
request_object_signing_alg?: string;
// less important
id_token_encrypted_response_alg?: string;
id_token_encrypted_response_enc?: string;
introspection_endpoint_auth_method?: ClientAuthMethod;
introspection_endpoint_auth_signing_alg?: string;
request_object_encryption_alg?: string;
request_object_encryption_enc?: string;
revocation_endpoint_auth_method?: ClientAuthMethod;
revocation_endpoint_auth_signing_alg?: string;
token_endpoint_auth_signing_alg?: string;
userinfo_encrypted_response_alg?: string;
userinfo_encrypted_response_enc?: string;
userinfo_signed_response_alg?: string;
authorization_encrypted_response_alg?: string;
authorization_encrypted_response_enc?: string;
authorization_signed_response_alg?: string;
[key: string]: unknown;
}
export interface ClaimsParameterMember {
essential?: boolean;
value?: string;
values?: string[];
[key: string]: unknown;
}
export interface AuthorizationParameters {
acr_values?: string;
audience?: string;
claims?:
| string
| {
id_token?: {
[key: string]: null | ClaimsParameterMember;
};
userinfo?: {
[key: string]: null | ClaimsParameterMember;
};
};
claims_locales?: string;
client_id?: string;
code_challenge_method?: string;
code_challenge?: string;
display?: string;
id_token_hint?: string;
login_hint?: string;
max_age?: number;
nonce?: string;
prompt?: string;
redirect_uri?: string;
registration?: string;
request_uri?: string;
request?: string;
resource?: string | string[];
response_mode?: string;
response_type?: string;
scope?: string;
state?: string;
ui_locales?: string;
[key: string]: unknown;
}
export interface EndSessionParameters {
id_token_hint?: TokenSet | string;
post_logout_redirect_uri?: string;
state?: string;
client_id?: string;
logout_hint?: string;
[key: string]: unknown;
}
export interface CallbackParamsType {
access_token?: string;
code?: string;
error?: string;
error_description?: string;
error_uri?: string;
expires_in?: string;
id_token?: string;
state?: string;
token_type?: string;
session_state?: string;
response?: string;
[key: string]: unknown;
}
export interface OAuthCallbackChecks {
response_type?: string;
state?: string;
code_verifier?: string;
jarm?: boolean;
scope?: string; // TODO: remove in v6.x
}
export interface OpenIDCallbackChecks extends OAuthCallbackChecks {
max_age?: number;
nonce?: string;
}
export interface CallbackExtras {
exchangeBody?: object;
clientAssertionPayload?: object;
DPoP?: DPoPInput;
}
export interface RefreshExtras {
exchangeBody?: object;
clientAssertionPayload?: object;
DPoP?: DPoPInput;
}
export interface GrantBody {
grant_type: string;
[key: string]: unknown;
}
export interface GrantExtras {
clientAssertionPayload?: object;
DPoP?: DPoPInput;
}
export interface IntrospectExtras {
introspectBody?: object;
clientAssertionPayload?: object;
}
export interface RevokeExtras {
revokeBody?: object;
clientAssertionPayload?: object;
}
export interface RequestObjectPayload extends AuthorizationParameters {
client_id?: string;
iss?: string;
aud?: string;
iat?: number;
exp?: number;
jti?: string;
[key: string]: unknown;
}
export interface RegisterOther {
jwks?: { keys: jose.JWK[] };
initialAccessToken?: string;
}
export interface DeviceAuthorizationParameters {
client_id?: string;
scope?: string;
[key: string]: unknown;
}
export interface DeviceAuthorizationExtras {
exchangeBody?: object;
clientAssertionPayload?: object;
DPoP?: DPoPInput;
}
export interface PushedAuthorizationRequestExtras {
clientAssertionPayload?: object;
}
export type Address<ExtendedAddress extends {} = UnknownObject> = Override<
{
formatted?: string;
street_address?: string;
locality?: string;
region?: string;
postal_code?: string;
country?: string;
},
ExtendedAddress
>;
export type UserinfoResponse<
UserInfo extends {} = UnknownObject,
ExtendedAddress extends {} = UnknownObject,
> = Override<
{
sub: string;
name?: string;
given_name?: string;
family_name?: string;
middle_name?: string;
nickname?: string;
preferred_username?: string;
profile?: string;
picture?: string;
website?: string;
email?: string;
email_verified?: boolean;
gender?: string;
birthdate?: string;
zoneinfo?: string;
locale?: string;
phone_number?: string;
updated_at?: number;
address?: Address<ExtendedAddress>;
},
UserInfo
>;
export interface IntrospectionResponse {
active: boolean;
client_id?: string;
exp?: number;
iat?: number;
sid?: string;
iss?: string;
jti?: string;
username?: string;
aud?: string | string[];
scope: string;
sub?: string;
nbf?: number;
token_type?: string;
cnf?: {
'x5t#S256'?: string;
[key: string]: unknown;
};
[key: string]: unknown;
}
export interface ClientOptions {
additionalAuthorizedParties?: string | string[];
}
export type Client = InstanceType<Issuer['Client']>;
declare class BaseClient {
constructor(metadata: ClientMetadata, jwks?: { keys: jose.JWK[] }, options?: ClientOptions);
[custom.http_options]: CustomHttpOptionsProvider;
[custom.clock_tolerance]: number;
metadata: ClientMetadata;
issuer: Issuer<this>;
static issuer: Issuer<BaseClient>;
authorizationUrl(parameters?: AuthorizationParameters): string;
endSessionUrl(parameters?: EndSessionParameters): string;
callbackParams(
input: string | http.IncomingMessage | http2.Http2ServerRequest,
): CallbackParamsType;
callback(
redirectUri: string | undefined,
parameters: CallbackParamsType,
checks?: OpenIDCallbackChecks,
extras?: CallbackExtras,
): Promise<TokenSet>;
oauthCallback(
redirectUri: string | undefined,
parameters: CallbackParamsType,
checks?: OAuthCallbackChecks,
extras?: CallbackExtras,
): Promise<TokenSet>;
refresh(refreshToken: TokenSet | string, extras?: RefreshExtras): Promise<TokenSet>;
userinfo<TUserInfo extends {} = UnknownObject, TAddress extends {} = UnknownObject>(
accessToken: TokenSet | string,
options?: {
method?: 'GET' | 'POST';
via?: 'header' | 'body';
tokenType?: string;
params?: object;
DPoP?: DPoPInput;
},
): Promise<UserinfoResponse<TUserInfo, TAddress>>;
requestResource(
resourceUrl: string | URL,
accessToken: TokenSet | string,
options?: {
headers?: object;
body?: string | Buffer;
method?: 'GET' | 'POST' | 'PUT' | 'HEAD' | 'DELETE' | 'OPTIONS' | 'TRACE' | 'PATCH';
tokenType?: string;
DPoP?: DPoPInput;
},
): Promise<{ body?: Buffer } & http.IncomingMessage>;
grant(body: GrantBody, extras?: GrantExtras): Promise<TokenSet>;
introspect(
token: string,
tokenTypeHint?: TokenTypeHint,
extras?: IntrospectExtras,
): Promise<IntrospectionResponse>;
revoke(token: string, tokenTypeHint?: TokenTypeHint, extras?: RevokeExtras): Promise<undefined>;
requestObject(payload: RequestObjectPayload): Promise<string>;
deviceAuthorization(
parameters?: DeviceAuthorizationParameters,
extras?: DeviceAuthorizationExtras,
): Promise<DeviceFlowHandle<BaseClient>>;
pushedAuthorizationRequest(
parameters?: AuthorizationParameters,
extras?: PushedAuthorizationRequestExtras,
): Promise<{
request_uri: string;
expires_in: number;
[key: string]: unknown;
}>;
static register(metadata: object, other?: RegisterOther & ClientOptions): Promise<BaseClient>;
static fromUri(
registrationClientUri: string,
registrationAccessToken: string,
jwks?: { keys: jose.JWK[] },
clientOptions?: ClientOptions,
): Promise<BaseClient>;
static [custom.http_options]: CustomHttpOptionsProvider;
[key: string]: unknown;
}
interface DeviceFlowPollOptions {
// @ts-ignore
signal?: AbortSignal;
}
export class DeviceFlowHandle<TClient extends BaseClient = BaseClient> {
poll(options?: DeviceFlowPollOptions): Promise<TokenSet>;
abort(): void;
expired(): boolean;
expires_at: number;
client: TClient;
user_code: string;
device_code: string;
verification_uri: string;
verification_uri_complete: string;
expires_in: number;
}
export interface IssuerMetadata {
issuer: string;
authorization_endpoint?: string;
token_endpoint?: string;
jwks_uri?: string;
userinfo_endpoint?: string;
revocation_endpoint?: string;
end_session_endpoint?: string;
registration_endpoint?: string;
token_endpoint_auth_methods_supported?: string[];
token_endpoint_auth_signing_alg_values_supported?: string[];
introspection_endpoint_auth_methods_supported?: string[];
introspection_endpoint_auth_signing_alg_values_supported?: string[];
revocation_endpoint_auth_methods_supported?: string[];
revocation_endpoint_auth_signing_alg_values_supported?: string[];
request_object_signing_alg_values_supported?: string[];
mtls_endpoint_aliases?: MtlsEndpointAliases;
[key: string]: unknown;
}
export interface MtlsEndpointAliases {
token_endpoint?: string;
userinfo_endpoint?: string;
revocation_endpoint?: string;
introspection_endpoint?: string;
device_authorization_endpoint?: string;
}
export interface TypeOfGenericClient<TClient extends BaseClient = BaseClient> {
new (metadata: ClientMetadata, jwks?: { keys: jose.JWK[] }, options?: ClientOptions): TClient;
[custom.http_options]: CustomHttpOptionsProvider;
[custom.clock_tolerance]: number;
}
export class Issuer<TClient extends BaseClient = BaseClient> {
constructor(metadata: IssuerMetadata);
Client: TypeOfGenericClient<TClient>;
FAPI1Client: TypeOfGenericClient<TClient>;
FAPI2Client: TypeOfGenericClient<TClient>;
metadata: IssuerMetadata;
[custom.http_options]: CustomHttpOptionsProvider;
static discover(issuer: string): Promise<Issuer<BaseClient>>;
static webfinger(input: string): Promise<Issuer<BaseClient>>;
static [custom.http_options]: CustomHttpOptionsProvider;
[key: string]: unknown;
}
export interface TokenSetParameters {
access_token?: string;
token_type?: string;
id_token?: string;
refresh_token?: string;
scope?: string;
expires_at?: number;
session_state?: string;
[key: string]: unknown;
}
export interface IdTokenClaims extends UserinfoResponse {
acr?: string;
amr?: string[];
at_hash?: string;
aud: string | string[];
auth_time?: number;
azp?: string;
c_hash?: string;
exp: number;
iat: number;
iss: string;
nonce?: string;
s_hash?: string;
sub: string;
[key: string]: unknown;
}
export class TokenSet implements TokenSetParameters {
access_token?: string;
token_type?: string;
id_token?: string;
refresh_token?: string;
expires_in?: number;
expires_at?: number;
session_state?: string;
scope?: string;
constructor(input?: TokenSetParameters);
expired(): boolean;
claims(): IdTokenClaims;
[key: string]: unknown;
}
export type StrategyVerifyCallbackUserInfo<
TUser,
TUserInfo extends {} = UnknownObject,
TAddress extends {} = UnknownObject,
> = (
tokenset: TokenSet,
userinfo: UserinfoResponse<TUserInfo, TAddress>,
done: (err: any, user?: TUser) => void,
) => void;
export type StrategyVerifyCallback<TUser> = (
tokenset: TokenSet,
done: (err: any, user?: TUser) => void,
) => void;
export type StrategyVerifyCallbackReqUserInfo<
TUser,
TUserInfo extends {} = UnknownObject,
TAddress extends {} = UnknownObject,
> = (
req: http.IncomingMessage,
tokenset: TokenSet,
userinfo: UserinfoResponse<TUserInfo, TAddress>,
done: (err: any, user?: TUser) => void,
) => void;
export type StrategyVerifyCallbackReq<TUser> = (
req: http.IncomingMessage,
tokenset: TokenSet,
done: (err: any, user?: TUser) => void,
) => void;
export interface StrategyOptions<TClient extends BaseClient = BaseClient> {
client: TClient;
params?: AuthorizationParameters;
extras?: CallbackExtras;
passReqToCallback?: boolean;
usePKCE?: boolean | string;
sessionKey?: string;
}
export class Strategy<TUser, TClient extends BaseClient = BaseClient> {
constructor(
options: StrategyOptions<TClient>,
verify:
| StrategyVerifyCallback<TUser>
| StrategyVerifyCallbackUserInfo<TUser>
| StrategyVerifyCallbackReq<TUser>
| StrategyVerifyCallbackReqUserInfo<TUser>,
);
authenticate(req: any, options?: any): void;
success(user: any, info?: any): void;
fail(challenge: any, status: number): void;
fail(status: number): void;
redirect(url: string, status?: number): void;
pass(): void;
error(err: Error): void;
}
export namespace generators {
function random(bytes?: number): string;
function state(bytes?: number): string;
function nonce(bytes?: number): string;
function codeVerifier(bytes?: number): string;
function codeChallenge(verifier: string): string;
}
export namespace errors {
class OPError extends Error {
error_description?: string;
error?: string;
error_uri?: string;
state?: string;
scope?: string;
session_state?: string;
response?: { body?: UnknownObject | Buffer } & http.IncomingMessage;
constructor(
params: {
error: string;
error_description?: string;
error_uri?: string;
state?: string;
scope?: string;
session_state?: string;
},
response?: { body?: UnknownObject | Buffer } & http.IncomingMessage,
);
}
class RPError extends Error {
jwt?: string;
checks?: object;
params?: object;
body?: object;
response?: { body?: UnknownObject | Buffer } & http.IncomingMessage;
now?: number;
tolerance?: number;
nbf?: number;
exp?: number;
iat?: number;
auth_time?: number;
constructor(...args: Parameters<typeof format>);
constructor(options: {
message?: string;
printf?: Parameters<typeof format>;
response?: { body?: UnknownObject | Buffer } & http.IncomingMessage;
[key: string]: unknown;
});
}
}
type KnownKeys<T> = {
[K in keyof T]: string extends K ? never : number extends K ? never : K;
} extends { [_ in keyof T]: infer U }
? {} extends U
? never
: U
: never;
type Override<T1, T2> = Omit<T1, keyof Omit<T2, keyof KnownKeys<T2>>> & T2;