oidc-login 0.0.0-stage → 1.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 +124 -2
- package/dist/index.d.mts +122 -0
- package/dist/index.mjs +195 -0
- package/package.json +53 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ilya Semenov
|
|
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
CHANGED
|
@@ -1,3 +1,125 @@
|
|
|
1
|
-
#
|
|
1
|
+
# oidc-login
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
"Sign in with SSO" for web applications through any OpenID Connect provider, such as Keycloak, Microsoft Entra ID, Google, or Yandex.
|
|
4
|
+
It wraps [openid-client](https://github.com/panva/openid-client) into 2 calls: start the sign-in and finish it on the callback.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install oidc-login
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
Configure the login handler with your provider settings:
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { createOidcLogin } from "oidc-login"
|
|
18
|
+
|
|
19
|
+
const login = createOidcLogin({
|
|
20
|
+
issuerUrl: "https://keycloak.example.com/realms/company",
|
|
21
|
+
clientId: "tracker",
|
|
22
|
+
clientSecret: process.env.OIDC_CLIENT_SECRET,
|
|
23
|
+
})
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
When the user clicks the sign-in button, start the sign-in, store the flow in the user's session, and redirect the browser to the provider:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
const { url, flow } = await login.start({ redirectUri: "https://app.example.com/auth/callback" })
|
|
30
|
+
session.oidcFlow = flow
|
|
31
|
+
return redirect(url)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The redirect URI must have no query or fragment.
|
|
35
|
+
Store the flow where only the user's browser session can read it, such as in a signed and encrypted cookie: it holds the PKCE verifier.
|
|
36
|
+
It is a plain JSON object.
|
|
37
|
+
|
|
38
|
+
When the provider redirects the browser back to the redirect URI, finish the sign-in with the flow and the callback URL:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const user = await login.finish(session.oidcFlow, request.url)
|
|
42
|
+
delete session.oidcFlow
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`finish` also accepts the query string of the callback or its parameters, such as `location.search` that a client-side callback page sends to the server.
|
|
46
|
+
It returns the user:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
user.sub // "f81d4fae-7dec-11d0-a765-00a0c91e6bf6"
|
|
50
|
+
user.email // "jane@example.com"
|
|
51
|
+
user.emailVerified // true, false, or undefined when the provider does not say
|
|
52
|
+
user.name // "Jane Doe"
|
|
53
|
+
user.claims // ID token claims
|
|
54
|
+
user.userinfo // UserInfo response, if requested
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Identify the user by `sub` within the issuer: the provider may change the email.
|
|
58
|
+
Before you link the sign-in to an existing account by email, check `emailVerified`, or trust the provider to verify emails.
|
|
59
|
+
|
|
60
|
+
Reuse the instance for the same provider settings: it fetches the issuer metadata once, on first use.
|
|
61
|
+
|
|
62
|
+
## Handle errors
|
|
63
|
+
|
|
64
|
+
`start` and `finish` reject with `OidcLoginError`, whose `code` names the failed stage:
|
|
65
|
+
|
|
66
|
+
- `discovery_failed`: the issuer URL is wrong or the provider is unavailable.
|
|
67
|
+
- `authorization_failed`: the provider redirected back with an error in `error`, such as `access_denied` when the user cancels.
|
|
68
|
+
- `callback_failed`: the callback does not belong to the flow, or the provider rejected the code.
|
|
69
|
+
- `userinfo_failed`: the UserInfo request failed.
|
|
70
|
+
|
|
71
|
+
The original error is in `cause`.
|
|
72
|
+
Invalid arguments, such as a redirect URI with a query, reject with `TypeError`.
|
|
73
|
+
|
|
74
|
+
## Check the provider settings
|
|
75
|
+
|
|
76
|
+
Call `discover` to check that the issuer URL points to a working provider, such as when an administrator saves the settings:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
await createOidcLogin({ issuerUrl, clientId }).discover()
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The issuer URL must use HTTPS; plain HTTP works only on loopback hosts, such as `localhost`, for local development.
|
|
83
|
+
Validate a URL that users enter with `isSecureIssuerUrl`.
|
|
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
|
+
|
|
98
|
+
## Options
|
|
99
|
+
|
|
100
|
+
- `scope`: scopes to request besides `openid`, `email`, and `profile`.
|
|
101
|
+
- `userinfo`: when to request the UserInfo endpoint.
|
|
102
|
+
`auto`, the default, requests it when the ID token has no `email` or `email_verified`; `always` requests it on every sign-in; `never` uses only the ID token.
|
|
103
|
+
- `clientSecret`: omit it for a public client.
|
|
104
|
+
- `clientAuthentication`: how to send the client secret to the token endpoint.
|
|
105
|
+
`client_secret_basic`, the default, sends it in the `Authorization` header; `client_secret_post` sends it in the request body, for providers that accept only that.
|
|
106
|
+
- `fetch`: sends the HTTP requests to the provider instead of the global `fetch`.
|
|
107
|
+
|
|
108
|
+
## Test the sign-in
|
|
109
|
+
|
|
110
|
+
Pass the handler of [oidc-test-issuer](https://github.com/IlyaSemenov/oidc-test-issuer) as `fetch` to run the whole sign-in in a unit test, without a server:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { createTestOidcIssuer } from "oidc-test-issuer"
|
|
114
|
+
|
|
115
|
+
const issuer = createTestOidcIssuer({ basePath: "/oidc" })
|
|
116
|
+
const login = createOidcLogin({
|
|
117
|
+
issuerUrl: issuer.getIssuerUrl("https://app.example.com"),
|
|
118
|
+
clientId: "client-1",
|
|
119
|
+
fetch: (url, init) => issuer.fetch(new Request(url, init)),
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const { url, flow } = await login.start({ redirectUri: "https://app.example.com/auth/callback" })
|
|
123
|
+
const callback = await issuer.fetch(new Request(url))
|
|
124
|
+
const user = await login.finish(flow, callback.headers.get("location")!)
|
|
125
|
+
```
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage of a login or of a check of the provider settings that failed.
|
|
3
|
+
*
|
|
4
|
+
* - `discovery_failed`: the issuer metadata could not be fetched or is invalid.
|
|
5
|
+
* - `authorization_failed`: the provider redirected back with an error, such as `access_denied`.
|
|
6
|
+
* - `callback_failed`: the callback does not match the flow, or the code exchange or ID token validation failed.
|
|
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.
|
|
10
|
+
*/
|
|
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`. */
|
|
13
|
+
export declare class OidcLoginError extends Error {
|
|
14
|
+
name: string;
|
|
15
|
+
/** Stage that failed. */
|
|
16
|
+
readonly code: OidcLoginErrorCode;
|
|
17
|
+
/** OAuth error code that the provider returned, such as `access_denied` with `authorization_failed` or `invalid_client` with `client_rejected`. */
|
|
18
|
+
readonly error?: string;
|
|
19
|
+
constructor(code: OidcLoginErrorCode, options?: {
|
|
20
|
+
cause?: unknown;
|
|
21
|
+
error?: string;
|
|
22
|
+
message?: string;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Checks that an issuer URL is safe to sign in through: HTTPS, or plain HTTP on a loopback host for local development.
|
|
27
|
+
*
|
|
28
|
+
* `createOidcLogin` accepts plain HTTP only for such URLs, so validate issuer URLs that users enter with this function.
|
|
29
|
+
*/
|
|
30
|
+
export declare function isSecureIssuerUrl(value: string): boolean;
|
|
31
|
+
/** User that an OpenID Connect sign-in returns. */
|
|
32
|
+
interface OidcUser {
|
|
33
|
+
/** Subject identifier, unique and stable within the issuer. */
|
|
34
|
+
sub: string;
|
|
35
|
+
email?: string;
|
|
36
|
+
/** Whether the provider verified `email`; `undefined` when it does not say. */
|
|
37
|
+
emailVerified?: boolean;
|
|
38
|
+
/** Display name: the `name` claim, or `preferred_username` without it. */
|
|
39
|
+
name?: string;
|
|
40
|
+
/** Claims of the ID token as the provider sent them. */
|
|
41
|
+
claims: Record<string, unknown>;
|
|
42
|
+
/** UserInfo response as the provider sent it, when the sign-in requested it. */
|
|
43
|
+
userinfo?: Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
/** Fetch API-compatible function that sends the HTTP requests to the provider. */
|
|
46
|
+
type OidcFetch = (url: string, init: RequestInit) => Promise<Response>;
|
|
47
|
+
/**
|
|
48
|
+
* When the sign-in requests the UserInfo endpoint.
|
|
49
|
+
*
|
|
50
|
+
* - `auto`: when the ID token has no `email` or no `email_verified`.
|
|
51
|
+
* - `always`: on every sign-in.
|
|
52
|
+
* - `never`: never; the user has only the ID token claims.
|
|
53
|
+
*/
|
|
54
|
+
type OidcUserinfoMode = "auto" | "always" | "never";
|
|
55
|
+
/** How a client with a secret authenticates to the token endpoint. */
|
|
56
|
+
type OidcClientAuthentication = "client_secret_basic" | "client_secret_post";
|
|
57
|
+
/** Options of `createOidcLogin`. */
|
|
58
|
+
interface OidcLoginOptions {
|
|
59
|
+
/** Issuer URL: HTTPS, or plain HTTP on a loopback host. */
|
|
60
|
+
issuerUrl: string | URL;
|
|
61
|
+
clientId: string;
|
|
62
|
+
/** Client secret; omit it for a public client. */
|
|
63
|
+
clientSecret?: string;
|
|
64
|
+
/** How to send the client secret; `client_secret_basic` by default, as OAuth defines. */
|
|
65
|
+
clientAuthentication?: OidcClientAuthentication;
|
|
66
|
+
/** Scopes to request besides `openid`, `email`, and `profile`. */
|
|
67
|
+
scope?: readonly string[];
|
|
68
|
+
/** When to request the UserInfo endpoint; `auto` by default. */
|
|
69
|
+
userinfo?: OidcUserinfoMode;
|
|
70
|
+
/** Sends the HTTP requests to the provider instead of the global `fetch`, such as to a test issuer. */
|
|
71
|
+
fetch?: OidcFetch;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* State of a started sign-in that `finish` needs.
|
|
75
|
+
*
|
|
76
|
+
* Store it between `start` and `finish` where only the user's browser session can read it, such as in a signed and encrypted cookie: it holds the PKCE verifier.
|
|
77
|
+
* It is a plain JSON object.
|
|
78
|
+
*/
|
|
79
|
+
interface OidcLoginFlow {
|
|
80
|
+
redirectUri: string;
|
|
81
|
+
state: string;
|
|
82
|
+
nonce: string;
|
|
83
|
+
codeVerifier: string;
|
|
84
|
+
}
|
|
85
|
+
/** Started sign-in. */
|
|
86
|
+
interface OidcLoginStart {
|
|
87
|
+
/** Authorization URL to redirect the browser to. */
|
|
88
|
+
url: string;
|
|
89
|
+
flow: OidcLoginFlow;
|
|
90
|
+
}
|
|
91
|
+
/** OpenID Connect sign-in through one provider. */
|
|
92
|
+
interface OidcLogin {
|
|
93
|
+
/** Starts a sign-in that returns to `redirectUri`, which must have no query or fragment. */
|
|
94
|
+
start: (options: {
|
|
95
|
+
redirectUri: string | URL;
|
|
96
|
+
loginHint?: string;
|
|
97
|
+
}) => Promise<OidcLoginStart>;
|
|
98
|
+
/**
|
|
99
|
+
* Finishes the sign-in with the query of the request to the redirect URI.
|
|
100
|
+
*
|
|
101
|
+
* Pass the full callback URL, its query string, or its parameters.
|
|
102
|
+
*/
|
|
103
|
+
finish: (flow: OidcLoginFlow, callback: URL | URLSearchParams | string) => Promise<OidcUser>;
|
|
104
|
+
/** Fetches the issuer metadata to check the issuer URL. */
|
|
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>;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Creates an OpenID Connect login handler for one provider: the authorization code flow with PKCE, state, and nonce.
|
|
117
|
+
*
|
|
118
|
+
* The instance fetches the issuer metadata once, on first use; after a failed fetch, the next call fetches it again.
|
|
119
|
+
* Methods reject with `OidcLoginError` when the sign-in fails, and with `TypeError` for invalid arguments.
|
|
120
|
+
*/
|
|
121
|
+
export declare function createOidcLogin({ issuerUrl, clientId, clientSecret, clientAuthentication, scope, userinfo, fetch }: OidcLoginOptions): OidcLogin;
|
|
122
|
+
export type { OidcClientAuthentication, OidcFetch, OidcLogin, OidcLoginErrorCode, OidcLoginFlow, OidcLoginOptions, OidcLoginStart, OidcUser, OidcUserinfoMode };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import * as client from "openid-client";
|
|
2
|
+
// src/errors.ts
|
|
3
|
+
/** Failure of an OpenID Connect login or of a check of the provider settings; the original error is in `cause`. */
|
|
4
|
+
var OidcLoginError = class extends Error {
|
|
5
|
+
name = "OidcLoginError";
|
|
6
|
+
/** Stage that failed. */
|
|
7
|
+
code;
|
|
8
|
+
/** OAuth error code that the provider returned, such as `access_denied` with `authorization_failed` or `invalid_client` with `client_rejected`. */
|
|
9
|
+
error;
|
|
10
|
+
constructor(code, options = {}) {
|
|
11
|
+
super(options.message ?? `OpenID Connect request failed: ${code}.`, { cause: options.cause });
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.error = options.error;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
// src/issuer-url.ts
|
|
17
|
+
const loopbackHostnames = /* @__PURE__ */ new Set([
|
|
18
|
+
"localhost",
|
|
19
|
+
"127.0.0.1",
|
|
20
|
+
"[::1]"
|
|
21
|
+
]);
|
|
22
|
+
/**
|
|
23
|
+
* Checks that an issuer URL is safe to sign in through: HTTPS, or plain HTTP on a loopback host for local development.
|
|
24
|
+
*
|
|
25
|
+
* `createOidcLogin` accepts plain HTTP only for such URLs, so validate issuer URLs that users enter with this function.
|
|
26
|
+
*/
|
|
27
|
+
function isSecureIssuerUrl(value) {
|
|
28
|
+
if (!URL.canParse(value)) return false;
|
|
29
|
+
const url = new URL(value);
|
|
30
|
+
return url.protocol === "https:" || url.protocol === "http:" && isLoopbackHostname(url.hostname);
|
|
31
|
+
}
|
|
32
|
+
/** Checks whether a hostname points to the local machine. */
|
|
33
|
+
function isLoopbackHostname(hostname) {
|
|
34
|
+
return loopbackHostnames.has(hostname) || hostname.endsWith(".localhost");
|
|
35
|
+
}
|
|
36
|
+
// src/user.ts
|
|
37
|
+
/** Reads standard claims from an untrusted claims object. */
|
|
38
|
+
function readStandardClaims(claims) {
|
|
39
|
+
return {
|
|
40
|
+
email: readString(claims?.email),
|
|
41
|
+
email_verified: typeof claims?.email_verified === "boolean" ? claims.email_verified : void 0,
|
|
42
|
+
name: readString(claims?.name),
|
|
43
|
+
preferred_username: readString(claims?.preferred_username)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Builds the user from the ID token claims, filling missing claims from UserInfo. */
|
|
47
|
+
function createOidcUser(claims, userinfo) {
|
|
48
|
+
const fromToken = readStandardClaims(claims);
|
|
49
|
+
const fromUserinfo = readStandardClaims(userinfo);
|
|
50
|
+
const email = fromToken.email ?? fromUserinfo.email;
|
|
51
|
+
return {
|
|
52
|
+
sub: claims.sub,
|
|
53
|
+
email,
|
|
54
|
+
emailVerified: email === void 0 ? void 0 : getEmailVerified(fromToken, email) ?? getEmailVerified(fromUserinfo, email),
|
|
55
|
+
name: fromToken.name ?? fromToken.preferred_username ?? fromUserinfo.name ?? fromUserinfo.preferred_username,
|
|
56
|
+
claims,
|
|
57
|
+
userinfo
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function getEmailVerified(claims, email) {
|
|
61
|
+
return claims.email === email ? claims.email_verified : void 0;
|
|
62
|
+
}
|
|
63
|
+
function readString(value) {
|
|
64
|
+
return typeof value === "string" && value ? value : void 0;
|
|
65
|
+
}
|
|
66
|
+
// src/login.ts
|
|
67
|
+
const defaultScope = [
|
|
68
|
+
"openid",
|
|
69
|
+
"email",
|
|
70
|
+
"profile"
|
|
71
|
+
];
|
|
72
|
+
/**
|
|
73
|
+
* Creates an OpenID Connect login handler for one provider: the authorization code flow with PKCE, state, and nonce.
|
|
74
|
+
*
|
|
75
|
+
* The instance fetches the issuer metadata once, on first use; after a failed fetch, the next call fetches it again.
|
|
76
|
+
* Methods reject with `OidcLoginError` when the sign-in fails, and with `TypeError` for invalid arguments.
|
|
77
|
+
*/
|
|
78
|
+
function createOidcLogin({ issuerUrl, clientId, clientSecret, clientAuthentication = "client_secret_basic", scope = [], userinfo = "auto", fetch }) {
|
|
79
|
+
const issuer = new URL(issuerUrl);
|
|
80
|
+
let configuration;
|
|
81
|
+
const getConfiguration = () => {
|
|
82
|
+
configuration ??= discover({
|
|
83
|
+
issuer,
|
|
84
|
+
clientId,
|
|
85
|
+
clientSecret,
|
|
86
|
+
clientAuthentication,
|
|
87
|
+
fetch
|
|
88
|
+
}).catch((cause) => {
|
|
89
|
+
configuration = void 0;
|
|
90
|
+
throw new OidcLoginError("discovery_failed", { cause });
|
|
91
|
+
});
|
|
92
|
+
return configuration;
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
async start({ redirectUri, loginHint }) {
|
|
96
|
+
const redirectUrl = parseRedirectUri(redirectUri);
|
|
97
|
+
const config = await getConfiguration();
|
|
98
|
+
const codeVerifier = client.randomPKCECodeVerifier();
|
|
99
|
+
const flow = {
|
|
100
|
+
redirectUri: redirectUrl.href,
|
|
101
|
+
state: client.randomState(),
|
|
102
|
+
nonce: client.randomNonce(),
|
|
103
|
+
codeVerifier
|
|
104
|
+
};
|
|
105
|
+
return {
|
|
106
|
+
url: client.buildAuthorizationUrl(config, {
|
|
107
|
+
redirect_uri: flow.redirectUri,
|
|
108
|
+
scope: [.../* @__PURE__ */ new Set([...defaultScope, ...scope])].join(" "),
|
|
109
|
+
state: flow.state,
|
|
110
|
+
nonce: flow.nonce,
|
|
111
|
+
code_challenge: await client.calculatePKCECodeChallenge(codeVerifier),
|
|
112
|
+
code_challenge_method: "S256",
|
|
113
|
+
...loginHint === void 0 ? {} : { login_hint: loginHint }
|
|
114
|
+
}).href,
|
|
115
|
+
flow
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
async finish(flow, callback) {
|
|
119
|
+
const config = await getConfiguration();
|
|
120
|
+
const callbackUrl = new URL(flow.redirectUri);
|
|
121
|
+
callbackUrl.search = new URLSearchParams(callback instanceof URL || typeof callback === "string" && URL.canParse(callback) ? new URL(callback).search : callback).toString();
|
|
122
|
+
const tokens = await client.authorizationCodeGrant(config, callbackUrl, {
|
|
123
|
+
pkceCodeVerifier: flow.codeVerifier,
|
|
124
|
+
expectedState: flow.state,
|
|
125
|
+
expectedNonce: flow.nonce,
|
|
126
|
+
idTokenExpected: true
|
|
127
|
+
}).catch((cause) => {
|
|
128
|
+
throw cause instanceof client.AuthorizationResponseError ? new OidcLoginError("authorization_failed", {
|
|
129
|
+
cause,
|
|
130
|
+
error: cause.error
|
|
131
|
+
}) : new OidcLoginError("callback_failed", { cause });
|
|
132
|
+
});
|
|
133
|
+
const claims = tokens.claims();
|
|
134
|
+
const standardClaims = readStandardClaims(claims);
|
|
135
|
+
if (!(userinfo === "always" || userinfo === "auto" && (!standardClaims.email || standardClaims.email_verified === void 0))) return createOidcUser(claims);
|
|
136
|
+
return createOidcUser(claims, { ...await client.fetchUserInfo(config, tokens.access_token, claims.sub).catch((cause) => {
|
|
137
|
+
throw new OidcLoginError("userinfo_failed", { cause });
|
|
138
|
+
}) });
|
|
139
|
+
},
|
|
140
|
+
async discover() {
|
|
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
|
+
});
|
|
157
|
+
}
|
|
158
|
+
};
|
|
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
|
+
}
|
|
179
|
+
async function discover({ issuer, clientId, clientSecret, clientAuthentication, fetch }) {
|
|
180
|
+
const insecure = issuer.protocol === "http:" && isLoopbackHostname(issuer.hostname);
|
|
181
|
+
const config = await client.discovery(issuer, clientId, clientSecret, clientSecret === void 0 ? client.None() : clientAuthentication === "client_secret_basic" ? client.ClientSecretBasic(clientSecret) : client.ClientSecretPost(clientSecret), {
|
|
182
|
+
...fetch ? { [client.customFetch]: fetch } : {},
|
|
183
|
+
...insecure ? { execute: [client.allowInsecureRequests] } : {}
|
|
184
|
+
});
|
|
185
|
+
const { authorization_endpoint, token_endpoint } = config.serverMetadata();
|
|
186
|
+
if (![authorization_endpoint, token_endpoint].every((endpoint) => isEndpointUrl(endpoint, insecure))) throw new Error("The issuer metadata has no valid authorization or token endpoint.");
|
|
187
|
+
return config;
|
|
188
|
+
}
|
|
189
|
+
/** Checks that an endpoint from the issuer metadata is an absolute URL with an allowed protocol. */
|
|
190
|
+
function isEndpointUrl(value, insecure) {
|
|
191
|
+
if (typeof value !== "string" || !URL.canParse(value)) return false;
|
|
192
|
+
const { protocol } = new URL(value);
|
|
193
|
+
return protocol === "https:" || insecure && protocol === "http:";
|
|
194
|
+
}
|
|
195
|
+
export { OidcLoginError, createOidcLogin, isSecureIssuerUrl };
|
package/package.json
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oidc-login",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "OpenID Connect login flow for web apps on top of openid-client",
|
|
5
|
+
"homepage": "https://github.com/IlyaSemenov/oidc-login#readme",
|
|
6
|
+
"bugs": "https://github.com/IlyaSemenov/oidc-login/issues",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "Ilya Semenov",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/IlyaSemenov/oidc-login.git"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"README.md",
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"type": "module",
|
|
18
|
+
"sideEffects": false,
|
|
19
|
+
"types": "./dist/index.d.mts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": "./dist/index.mjs",
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsdown",
|
|
29
|
+
"lint": "oxlint --fix && oxfmt",
|
|
30
|
+
"lint:check": "oxlint && oxfmt --check",
|
|
31
|
+
"prepare": "lefthook install",
|
|
32
|
+
"prepublishOnly": "bun run build",
|
|
33
|
+
"release": "changesets-local",
|
|
34
|
+
"test": "bun test",
|
|
35
|
+
"types": "bun check && bun check -p tests"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"openid-client": "^6.9.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@arethetypeswrong/core": "^0.18.5",
|
|
42
|
+
"@changesets/cli": "^3.0.3",
|
|
43
|
+
"@ilyasemenov/oxc-config": "^0.1.3",
|
|
44
|
+
"@tsconfig/bun": "^1.0.12",
|
|
45
|
+
"@types/bun": "^1.4.4",
|
|
46
|
+
"changesets-local": "^1.6.0",
|
|
47
|
+
"oidc-test-issuer": "^1.0.0",
|
|
48
|
+
"oxfmt": "^0.78.0",
|
|
49
|
+
"oxlint": "^1.93.0",
|
|
50
|
+
"publint": "^0.3.25",
|
|
51
|
+
"tsdown": "^0.23.7",
|
|
52
|
+
"typescript": "^7.0.2"
|
|
53
|
+
},
|
|
54
|
+
"packageManager": "bun@1.4.4"
|
|
55
|
+
}
|