oidc-login 1.0.0 → 1.1.1
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/README.md +13 -0
- package/dist/index.d.mts +15 -5
- package/dist/index.mjs +48 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,6 +82,19 @@ await createOidcLogin({ issuerUrl, clientId }).discover()
|
|
|
82
82
|
The issuer URL must use HTTPS; plain HTTP works only on loopback hosts, such as `localhost`, for local development.
|
|
83
83
|
Validate a URL that users enter with `isSecureIssuerUrl`.
|
|
84
84
|
|
|
85
|
+
Call `checkClient` with your redirect URI to check the client ID and secret as well, without signing anyone in:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
await createOidcLogin({ issuerUrl, clientId, clientSecret }).checkClient({ redirectUri })
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
It exchanges a made-up code and expects the provider to reject only the code.
|
|
92
|
+
When the provider rejects the client, it rejects with `OidcLoginError` with the `client_rejected` code; when the response of the provider is inconclusive, with `client_check_failed`.
|
|
93
|
+
The OAuth error of the provider, if any, is in `error`.
|
|
94
|
+
|
|
95
|
+
The provider sees each check as a failed code exchange: it may log it as an error and count it against its rate limits.
|
|
96
|
+
Call `checkClient` when an administrator checks the settings, not as a periodic health check.
|
|
97
|
+
|
|
85
98
|
## Options
|
|
86
99
|
|
|
87
100
|
- `scope`: scopes to request besides `openid`, `email`, and `profile`.
|
package/dist/index.d.mts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Stage of the
|
|
2
|
+
* Stage of a login or of a check of the provider settings that failed.
|
|
3
3
|
*
|
|
4
4
|
* - `discovery_failed`: the issuer metadata could not be fetched or is invalid.
|
|
5
5
|
* - `authorization_failed`: the provider redirected back with an error, such as `access_denied`.
|
|
6
6
|
* - `callback_failed`: the callback does not match the flow, or the code exchange or ID token validation failed.
|
|
7
7
|
* - `userinfo_failed`: the UserInfo request failed.
|
|
8
|
+
* - `client_rejected`: `checkClient` found that the provider rejects the client: its ID or secret is wrong, or it may not use the authorization code flow.
|
|
9
|
+
* - `client_check_failed`: `checkClient` could not tell whether the provider accepts the client.
|
|
8
10
|
*/
|
|
9
|
-
type OidcLoginErrorCode = "discovery_failed" | "authorization_failed" | "callback_failed" | "userinfo_failed";
|
|
10
|
-
/** Failure of an OpenID Connect
|
|
11
|
+
type OidcLoginErrorCode = "discovery_failed" | "authorization_failed" | "callback_failed" | "userinfo_failed" | "client_rejected" | "client_check_failed";
|
|
12
|
+
/** Failure of an OpenID Connect login or of a check of the provider settings; the original error is in `cause`. */
|
|
11
13
|
export declare class OidcLoginError extends Error {
|
|
12
14
|
name: string;
|
|
13
|
-
/** Stage
|
|
15
|
+
/** Stage that failed. */
|
|
14
16
|
readonly code: OidcLoginErrorCode;
|
|
15
|
-
/** OAuth error code that the provider returned with `authorization_failed
|
|
17
|
+
/** OAuth error code that the provider returned, such as `access_denied` with `authorization_failed` or `invalid_client` with `client_rejected`. */
|
|
16
18
|
readonly error?: string;
|
|
17
19
|
constructor(code: OidcLoginErrorCode, options?: {
|
|
18
20
|
cause?: unknown;
|
|
@@ -101,6 +103,14 @@ interface OidcLogin {
|
|
|
101
103
|
finish: (flow: OidcLoginFlow, callback: URL | URLSearchParams | string) => Promise<OidcUser>;
|
|
102
104
|
/** Fetches the issuer metadata to check the issuer URL. */
|
|
103
105
|
discover: () => Promise<void>;
|
|
106
|
+
/**
|
|
107
|
+
* Checks that the provider accepts the client ID and secret, without a user.
|
|
108
|
+
*
|
|
109
|
+
* It exchanges a made-up authorization code for `redirectUri` and resolves when the provider rejects only the code.
|
|
110
|
+
*/
|
|
111
|
+
checkClient: (options: {
|
|
112
|
+
redirectUri: string | URL;
|
|
113
|
+
}) => Promise<void>;
|
|
104
114
|
}
|
|
105
115
|
/**
|
|
106
116
|
* Creates an OpenID Connect login handler for one provider: the authorization code flow with PKCE, state, and nonce.
|
package/dist/index.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import * as client from "openid-client";
|
|
2
2
|
// src/errors.ts
|
|
3
|
-
/** Failure of an OpenID Connect
|
|
3
|
+
/** Failure of an OpenID Connect login or of a check of the provider settings; the original error is in `cause`. */
|
|
4
4
|
var OidcLoginError = class extends Error {
|
|
5
5
|
name = "OidcLoginError";
|
|
6
|
-
/** Stage
|
|
6
|
+
/** Stage that failed. */
|
|
7
7
|
code;
|
|
8
|
-
/** OAuth error code that the provider returned with `authorization_failed
|
|
8
|
+
/** OAuth error code that the provider returned, such as `access_denied` with `authorization_failed` or `invalid_client` with `client_rejected`. */
|
|
9
9
|
error;
|
|
10
10
|
constructor(code, options = {}) {
|
|
11
|
-
super(options.message ?? `OpenID Connect
|
|
11
|
+
super(options.message ?? `OpenID Connect request failed: ${code}.`, { cause: options.cause });
|
|
12
12
|
this.code = code;
|
|
13
13
|
this.error = options.error;
|
|
14
14
|
}
|
|
@@ -93,8 +93,7 @@ function createOidcLogin({ issuerUrl, clientId, clientSecret, clientAuthenticati
|
|
|
93
93
|
};
|
|
94
94
|
return {
|
|
95
95
|
async start({ redirectUri, loginHint }) {
|
|
96
|
-
const redirectUrl =
|
|
97
|
-
if (/[?#]/.test(redirectUrl.href)) throw new TypeError("The redirect URI must have no query or fragment.");
|
|
96
|
+
const redirectUrl = parseRedirectUri(redirectUri);
|
|
98
97
|
const config = await getConfiguration();
|
|
99
98
|
const codeVerifier = client.randomPKCECodeVerifier();
|
|
100
99
|
const flow = {
|
|
@@ -140,23 +139,61 @@ function createOidcLogin({ issuerUrl, clientId, clientSecret, clientAuthenticati
|
|
|
140
139
|
},
|
|
141
140
|
async discover() {
|
|
142
141
|
await getConfiguration();
|
|
142
|
+
},
|
|
143
|
+
async checkClient({ redirectUri }) {
|
|
144
|
+
const redirectUrl = parseRedirectUri(redirectUri);
|
|
145
|
+
const config = await getConfiguration();
|
|
146
|
+
const failure = await client.genericGrantRequest(config, "authorization_code", {
|
|
147
|
+
code: client.randomState(),
|
|
148
|
+
redirect_uri: redirectUrl.href,
|
|
149
|
+
code_verifier: client.randomPKCECodeVerifier()
|
|
150
|
+
}).then(() => void 0, (cause) => cause);
|
|
151
|
+
const error = await readOAuthError(failure);
|
|
152
|
+
if (error === "invalid_grant") return;
|
|
153
|
+
throw new OidcLoginError(error === "invalid_client" || error === "unauthorized_client" || error === void 0 && failure instanceof client.WWWAuthenticateChallengeError && failure.status === 401 ? "client_rejected" : "client_check_failed", {
|
|
154
|
+
cause: failure,
|
|
155
|
+
error
|
|
156
|
+
});
|
|
143
157
|
}
|
|
144
158
|
};
|
|
145
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Parses the redirect URI of a sign-in.
|
|
162
|
+
*
|
|
163
|
+
* It must have no query or fragment: openid-client sends the redirect URI of the token request without its query, which would not match the authorization request.
|
|
164
|
+
*/
|
|
165
|
+
function parseRedirectUri(redirectUri) {
|
|
166
|
+
const redirectUrl = new URL(redirectUri);
|
|
167
|
+
if (/[?#]/.test(redirectUrl.href)) throw new TypeError("The redirect URI must have no query or fragment.");
|
|
168
|
+
return redirectUrl;
|
|
169
|
+
}
|
|
170
|
+
/** Reads the OAuth error code of a failed token request from its JSON body or its `WWW-Authenticate` challenge. */
|
|
171
|
+
async function readOAuthError(failure) {
|
|
172
|
+
if (failure instanceof client.ResponseBodyError) return failure.error;
|
|
173
|
+
if (failure instanceof client.WWWAuthenticateChallengeError) {
|
|
174
|
+
const body = await failure.response.clone().json().catch(() => void 0);
|
|
175
|
+
if (body && typeof body === "object" && "error" in body && typeof body.error === "string") return body.error;
|
|
176
|
+
return failure.cause.find((challenge) => challenge.parameters.error)?.parameters.error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
146
179
|
async function discover({ issuer, clientId, clientSecret, clientAuthentication, fetch }) {
|
|
147
180
|
const insecure = issuer.protocol === "http:" && isLoopbackHostname(issuer.hostname);
|
|
148
181
|
const config = await client.discovery(issuer, clientId, clientSecret, clientSecret === void 0 ? client.None() : clientAuthentication === "client_secret_basic" ? client.ClientSecretBasic(clientSecret) : client.ClientSecretPost(clientSecret), {
|
|
149
182
|
...fetch ? { [client.customFetch]: fetch } : {},
|
|
150
183
|
...insecure ? { execute: [client.allowInsecureRequests] } : {}
|
|
151
184
|
});
|
|
152
|
-
const { authorization_endpoint, token_endpoint } = config.serverMetadata();
|
|
153
|
-
if (![authorization_endpoint, token_endpoint].every((endpoint) => isEndpointUrl(endpoint, insecure))) throw new Error("The issuer metadata has
|
|
185
|
+
const { authorization_endpoint, token_endpoint, userinfo_endpoint, jwks_uri } = config.serverMetadata();
|
|
186
|
+
if (![authorization_endpoint, token_endpoint].every((endpoint) => isEndpointUrl(endpoint, insecure)) || ![userinfo_endpoint, jwks_uri].every((endpoint) => endpoint === void 0 || isEndpointUrl(endpoint, insecure))) throw new Error("The issuer metadata has an invalid endpoint.");
|
|
154
187
|
return config;
|
|
155
188
|
}
|
|
156
|
-
/**
|
|
189
|
+
/**
|
|
190
|
+
* Checks that an endpoint from the issuer metadata is an absolute URL with an allowed protocol.
|
|
191
|
+
*
|
|
192
|
+
* A plain HTTP issuer on a loopback host may name HTTPS endpoints or plain HTTP endpoints on loopback hosts, so that the client secret and tokens do not leave the machine unencrypted.
|
|
193
|
+
*/
|
|
157
194
|
function isEndpointUrl(value, insecure) {
|
|
158
195
|
if (typeof value !== "string" || !URL.canParse(value)) return false;
|
|
159
|
-
const { protocol } = new URL(value);
|
|
160
|
-
return protocol === "https:" || insecure && protocol === "http:";
|
|
196
|
+
const { protocol, hostname } = new URL(value);
|
|
197
|
+
return protocol === "https:" || insecure && protocol === "http:" && isLoopbackHostname(hostname);
|
|
161
198
|
}
|
|
162
199
|
export { OidcLoginError, createOidcLogin, isSecureIssuerUrl };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oidc-login",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "OpenID Connect login flow for web apps on top of openid-client",
|
|
5
5
|
"homepage": "https://github.com/IlyaSemenov/oidc-login#readme",
|
|
6
6
|
"bugs": "https://github.com/IlyaSemenov/oidc-login/issues",
|