oidc-login 0.0.0-stage → 1.0.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 +111 -2
- package/dist/index.d.mts +112 -0
- package/dist/index.mjs +162 -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,112 @@
|
|
|
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
|
+
## Options
|
|
86
|
+
|
|
87
|
+
- `scope`: scopes to request besides `openid`, `email`, and `profile`.
|
|
88
|
+
- `userinfo`: when to request the UserInfo endpoint.
|
|
89
|
+
`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.
|
|
90
|
+
- `clientSecret`: omit it for a public client.
|
|
91
|
+
- `clientAuthentication`: how to send the client secret to the token endpoint.
|
|
92
|
+
`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.
|
|
93
|
+
- `fetch`: sends the HTTP requests to the provider instead of the global `fetch`.
|
|
94
|
+
|
|
95
|
+
## Test the sign-in
|
|
96
|
+
|
|
97
|
+
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:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { createTestOidcIssuer } from "oidc-test-issuer"
|
|
101
|
+
|
|
102
|
+
const issuer = createTestOidcIssuer({ basePath: "/oidc" })
|
|
103
|
+
const login = createOidcLogin({
|
|
104
|
+
issuerUrl: issuer.getIssuerUrl("https://app.example.com"),
|
|
105
|
+
clientId: "client-1",
|
|
106
|
+
fetch: (url, init) => issuer.fetch(new Request(url, init)),
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const { url, flow } = await login.start({ redirectUri: "https://app.example.com/auth/callback" })
|
|
110
|
+
const callback = await issuer.fetch(new Request(url))
|
|
111
|
+
const user = await login.finish(flow, callback.headers.get("location")!)
|
|
112
|
+
```
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage of the sign-in 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
|
+
*/
|
|
9
|
+
type OidcLoginErrorCode = "discovery_failed" | "authorization_failed" | "callback_failed" | "userinfo_failed";
|
|
10
|
+
/** Failure of an OpenID Connect sign-in; the original error is in `cause`. */
|
|
11
|
+
export declare class OidcLoginError extends Error {
|
|
12
|
+
name: string;
|
|
13
|
+
/** Stage of the sign-in that failed. */
|
|
14
|
+
readonly code: OidcLoginErrorCode;
|
|
15
|
+
/** OAuth error code that the provider returned with `authorization_failed`, such as `access_denied`. */
|
|
16
|
+
readonly error?: string;
|
|
17
|
+
constructor(code: OidcLoginErrorCode, options?: {
|
|
18
|
+
cause?: unknown;
|
|
19
|
+
error?: string;
|
|
20
|
+
message?: string;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Checks that an issuer URL is safe to sign in through: HTTPS, or plain HTTP on a loopback host for local development.
|
|
25
|
+
*
|
|
26
|
+
* `createOidcLogin` accepts plain HTTP only for such URLs, so validate issuer URLs that users enter with this function.
|
|
27
|
+
*/
|
|
28
|
+
export declare function isSecureIssuerUrl(value: string): boolean;
|
|
29
|
+
/** User that an OpenID Connect sign-in returns. */
|
|
30
|
+
interface OidcUser {
|
|
31
|
+
/** Subject identifier, unique and stable within the issuer. */
|
|
32
|
+
sub: string;
|
|
33
|
+
email?: string;
|
|
34
|
+
/** Whether the provider verified `email`; `undefined` when it does not say. */
|
|
35
|
+
emailVerified?: boolean;
|
|
36
|
+
/** Display name: the `name` claim, or `preferred_username` without it. */
|
|
37
|
+
name?: string;
|
|
38
|
+
/** Claims of the ID token as the provider sent them. */
|
|
39
|
+
claims: Record<string, unknown>;
|
|
40
|
+
/** UserInfo response as the provider sent it, when the sign-in requested it. */
|
|
41
|
+
userinfo?: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
/** Fetch API-compatible function that sends the HTTP requests to the provider. */
|
|
44
|
+
type OidcFetch = (url: string, init: RequestInit) => Promise<Response>;
|
|
45
|
+
/**
|
|
46
|
+
* When the sign-in requests the UserInfo endpoint.
|
|
47
|
+
*
|
|
48
|
+
* - `auto`: when the ID token has no `email` or no `email_verified`.
|
|
49
|
+
* - `always`: on every sign-in.
|
|
50
|
+
* - `never`: never; the user has only the ID token claims.
|
|
51
|
+
*/
|
|
52
|
+
type OidcUserinfoMode = "auto" | "always" | "never";
|
|
53
|
+
/** How a client with a secret authenticates to the token endpoint. */
|
|
54
|
+
type OidcClientAuthentication = "client_secret_basic" | "client_secret_post";
|
|
55
|
+
/** Options of `createOidcLogin`. */
|
|
56
|
+
interface OidcLoginOptions {
|
|
57
|
+
/** Issuer URL: HTTPS, or plain HTTP on a loopback host. */
|
|
58
|
+
issuerUrl: string | URL;
|
|
59
|
+
clientId: string;
|
|
60
|
+
/** Client secret; omit it for a public client. */
|
|
61
|
+
clientSecret?: string;
|
|
62
|
+
/** How to send the client secret; `client_secret_basic` by default, as OAuth defines. */
|
|
63
|
+
clientAuthentication?: OidcClientAuthentication;
|
|
64
|
+
/** Scopes to request besides `openid`, `email`, and `profile`. */
|
|
65
|
+
scope?: readonly string[];
|
|
66
|
+
/** When to request the UserInfo endpoint; `auto` by default. */
|
|
67
|
+
userinfo?: OidcUserinfoMode;
|
|
68
|
+
/** Sends the HTTP requests to the provider instead of the global `fetch`, such as to a test issuer. */
|
|
69
|
+
fetch?: OidcFetch;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* State of a started sign-in that `finish` needs.
|
|
73
|
+
*
|
|
74
|
+
* 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.
|
|
75
|
+
* It is a plain JSON object.
|
|
76
|
+
*/
|
|
77
|
+
interface OidcLoginFlow {
|
|
78
|
+
redirectUri: string;
|
|
79
|
+
state: string;
|
|
80
|
+
nonce: string;
|
|
81
|
+
codeVerifier: string;
|
|
82
|
+
}
|
|
83
|
+
/** Started sign-in. */
|
|
84
|
+
interface OidcLoginStart {
|
|
85
|
+
/** Authorization URL to redirect the browser to. */
|
|
86
|
+
url: string;
|
|
87
|
+
flow: OidcLoginFlow;
|
|
88
|
+
}
|
|
89
|
+
/** OpenID Connect sign-in through one provider. */
|
|
90
|
+
interface OidcLogin {
|
|
91
|
+
/** Starts a sign-in that returns to `redirectUri`, which must have no query or fragment. */
|
|
92
|
+
start: (options: {
|
|
93
|
+
redirectUri: string | URL;
|
|
94
|
+
loginHint?: string;
|
|
95
|
+
}) => Promise<OidcLoginStart>;
|
|
96
|
+
/**
|
|
97
|
+
* Finishes the sign-in with the query of the request to the redirect URI.
|
|
98
|
+
*
|
|
99
|
+
* Pass the full callback URL, its query string, or its parameters.
|
|
100
|
+
*/
|
|
101
|
+
finish: (flow: OidcLoginFlow, callback: URL | URLSearchParams | string) => Promise<OidcUser>;
|
|
102
|
+
/** Fetches the issuer metadata to check the issuer URL. */
|
|
103
|
+
discover: () => Promise<void>;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Creates an OpenID Connect login handler for one provider: the authorization code flow with PKCE, state, and nonce.
|
|
107
|
+
*
|
|
108
|
+
* The instance fetches the issuer metadata once, on first use; after a failed fetch, the next call fetches it again.
|
|
109
|
+
* Methods reject with `OidcLoginError` when the sign-in fails, and with `TypeError` for invalid arguments.
|
|
110
|
+
*/
|
|
111
|
+
export declare function createOidcLogin({ issuerUrl, clientId, clientSecret, clientAuthentication, scope, userinfo, fetch }: OidcLoginOptions): OidcLogin;
|
|
112
|
+
export type { OidcClientAuthentication, OidcFetch, OidcLogin, OidcLoginErrorCode, OidcLoginFlow, OidcLoginOptions, OidcLoginStart, OidcUser, OidcUserinfoMode };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import * as client from "openid-client";
|
|
2
|
+
// src/errors.ts
|
|
3
|
+
/** Failure of an OpenID Connect sign-in; the original error is in `cause`. */
|
|
4
|
+
var OidcLoginError = class extends Error {
|
|
5
|
+
name = "OidcLoginError";
|
|
6
|
+
/** Stage of the sign-in that failed. */
|
|
7
|
+
code;
|
|
8
|
+
/** OAuth error code that the provider returned with `authorization_failed`, such as `access_denied`. */
|
|
9
|
+
error;
|
|
10
|
+
constructor(code, options = {}) {
|
|
11
|
+
super(options.message ?? `OpenID Connect sign-in 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 = new URL(redirectUri);
|
|
97
|
+
if (/[?#]/.test(redirectUrl.href)) throw new TypeError("The redirect URI must have no query or fragment.");
|
|
98
|
+
const config = await getConfiguration();
|
|
99
|
+
const codeVerifier = client.randomPKCECodeVerifier();
|
|
100
|
+
const flow = {
|
|
101
|
+
redirectUri: redirectUrl.href,
|
|
102
|
+
state: client.randomState(),
|
|
103
|
+
nonce: client.randomNonce(),
|
|
104
|
+
codeVerifier
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
url: client.buildAuthorizationUrl(config, {
|
|
108
|
+
redirect_uri: flow.redirectUri,
|
|
109
|
+
scope: [.../* @__PURE__ */ new Set([...defaultScope, ...scope])].join(" "),
|
|
110
|
+
state: flow.state,
|
|
111
|
+
nonce: flow.nonce,
|
|
112
|
+
code_challenge: await client.calculatePKCECodeChallenge(codeVerifier),
|
|
113
|
+
code_challenge_method: "S256",
|
|
114
|
+
...loginHint === void 0 ? {} : { login_hint: loginHint }
|
|
115
|
+
}).href,
|
|
116
|
+
flow
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
async finish(flow, callback) {
|
|
120
|
+
const config = await getConfiguration();
|
|
121
|
+
const callbackUrl = new URL(flow.redirectUri);
|
|
122
|
+
callbackUrl.search = new URLSearchParams(callback instanceof URL || typeof callback === "string" && URL.canParse(callback) ? new URL(callback).search : callback).toString();
|
|
123
|
+
const tokens = await client.authorizationCodeGrant(config, callbackUrl, {
|
|
124
|
+
pkceCodeVerifier: flow.codeVerifier,
|
|
125
|
+
expectedState: flow.state,
|
|
126
|
+
expectedNonce: flow.nonce,
|
|
127
|
+
idTokenExpected: true
|
|
128
|
+
}).catch((cause) => {
|
|
129
|
+
throw cause instanceof client.AuthorizationResponseError ? new OidcLoginError("authorization_failed", {
|
|
130
|
+
cause,
|
|
131
|
+
error: cause.error
|
|
132
|
+
}) : new OidcLoginError("callback_failed", { cause });
|
|
133
|
+
});
|
|
134
|
+
const claims = tokens.claims();
|
|
135
|
+
const standardClaims = readStandardClaims(claims);
|
|
136
|
+
if (!(userinfo === "always" || userinfo === "auto" && (!standardClaims.email || standardClaims.email_verified === void 0))) return createOidcUser(claims);
|
|
137
|
+
return createOidcUser(claims, { ...await client.fetchUserInfo(config, tokens.access_token, claims.sub).catch((cause) => {
|
|
138
|
+
throw new OidcLoginError("userinfo_failed", { cause });
|
|
139
|
+
}) });
|
|
140
|
+
},
|
|
141
|
+
async discover() {
|
|
142
|
+
await getConfiguration();
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
async function discover({ issuer, clientId, clientSecret, clientAuthentication, fetch }) {
|
|
147
|
+
const insecure = issuer.protocol === "http:" && isLoopbackHostname(issuer.hostname);
|
|
148
|
+
const config = await client.discovery(issuer, clientId, clientSecret, clientSecret === void 0 ? client.None() : clientAuthentication === "client_secret_basic" ? client.ClientSecretBasic(clientSecret) : client.ClientSecretPost(clientSecret), {
|
|
149
|
+
...fetch ? { [client.customFetch]: fetch } : {},
|
|
150
|
+
...insecure ? { execute: [client.allowInsecureRequests] } : {}
|
|
151
|
+
});
|
|
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 no valid authorization or token endpoint.");
|
|
154
|
+
return config;
|
|
155
|
+
}
|
|
156
|
+
/** Checks that an endpoint from the issuer metadata is an absolute URL with an allowed protocol. */
|
|
157
|
+
function isEndpointUrl(value, insecure) {
|
|
158
|
+
if (typeof value !== "string" || !URL.canParse(value)) return false;
|
|
159
|
+
const { protocol } = new URL(value);
|
|
160
|
+
return protocol === "https:" || insecure && protocol === "http:";
|
|
161
|
+
}
|
|
162
|
+
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.0.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
|
+
}
|