kerliix-oauth 1.0.3 → 2.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/.github/FUNDING.yml +13 -0
- package/.github/workflows/ci.yml +38 -0
- package/.github/workflows/publish.yml +48 -0
- package/LICENSE +201 -0
- package/README.md +203 -151
- package/index.js +8 -0
- package/lib/client.js +214 -0
- package/lib/jwks.js +34 -0
- package/lib/middleware.js +104 -0
- package/lib/pkce.js +16 -0
- package/lib/token.js +107 -0
- package/lib/utils.js +17 -0
- package/package.json +16 -31
- package/.github/workflows/build.yml +0 -35
- package/changelog.md +0 -131
- package/dist/cache.d.ts +0 -7
- package/dist/cache.js +0 -22
- package/dist/client.d.ts +0 -15
- package/dist/client.js +0 -132
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -3
- package/dist/types.d.ts +0 -25
- package/dist/types.js +0 -8
- package/src/cache.ts +0 -27
- package/src/client.ts +0 -163
- package/src/index.ts +0 -3
- package/src/types.ts +0 -33
- package/tests/client.test.js +0 -12
- package/tsconfig.json +0 -13
package/lib/client.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { generatePKCE } from "./pkce.js";
|
|
3
|
+
import { createVerifier } from "./jwks.js";
|
|
4
|
+
import { generateState, generateNonce } from "./utils.js";
|
|
5
|
+
import {
|
|
6
|
+
exchangeCode,
|
|
7
|
+
refreshAccessToken,
|
|
8
|
+
introspectToken,
|
|
9
|
+
revokeToken,
|
|
10
|
+
clientCredentialsGrant,
|
|
11
|
+
} from "./token.js";
|
|
12
|
+
|
|
13
|
+
export class KerliixClient {
|
|
14
|
+
/**
|
|
15
|
+
* @param {object} config
|
|
16
|
+
* @param {string} config.clientId
|
|
17
|
+
* @param {string} [config.clientSecret] - omit for public clients
|
|
18
|
+
* @param {string} config.redirectUri
|
|
19
|
+
* @param {string} [config.issuer] - accounts-center base URL, default https://accounts.kerliix.com
|
|
20
|
+
* @param {string} [config.scope] - default scope, default "openid profile email"
|
|
21
|
+
*/
|
|
22
|
+
constructor({
|
|
23
|
+
clientId,
|
|
24
|
+
clientSecret,
|
|
25
|
+
redirectUri,
|
|
26
|
+
issuer = "https://accounts.kerliix.com",
|
|
27
|
+
scope = "openid profile email",
|
|
28
|
+
}) {
|
|
29
|
+
this.clientId = clientId;
|
|
30
|
+
this.clientSecret = clientSecret || null;
|
|
31
|
+
this.redirectUri = redirectUri;
|
|
32
|
+
this.issuer = issuer.replace(/\/$/, "");
|
|
33
|
+
this.defaultScope = scope;
|
|
34
|
+
this._endpoints = null;
|
|
35
|
+
this._verify = null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Discovery ──────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Fetch the OIDC discovery document and cache it.
|
|
42
|
+
* The discovery URL is <issuer>/oauth/.well-known/openid-configuration.
|
|
43
|
+
*/
|
|
44
|
+
async init() {
|
|
45
|
+
if (this._endpoints) return;
|
|
46
|
+
const discoveryUrl = `${this.issuer}/oauth/.well-known/openid-configuration`;
|
|
47
|
+
const res = await axios.get(discoveryUrl);
|
|
48
|
+
this._endpoints = res.data;
|
|
49
|
+
this._verify = createVerifier(this._endpoints.jwks_uri, this._endpoints.issuer);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── Authorization ──────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build an authorization URL and return it alongside the PKCE verifier, state,
|
|
56
|
+
* and nonce that MUST be persisted in the user session before redirecting.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} [opts]
|
|
59
|
+
* @param {string} [opts.scope] - overrides the default scope
|
|
60
|
+
* @param {string} [opts.state] - supply your own; one is auto-generated if omitted
|
|
61
|
+
* @param {string} [opts.nonce] - supply your own; one is auto-generated if omitted
|
|
62
|
+
* @returns {{ url: string, codeVerifier: string, state: string, nonce: string }}
|
|
63
|
+
*/
|
|
64
|
+
async getLoginUrl({ scope, state, nonce } = {}) {
|
|
65
|
+
await this.init();
|
|
66
|
+
const pkce = generatePKCE();
|
|
67
|
+
const usedState = state ?? generateState();
|
|
68
|
+
const usedNonce = nonce ?? generateNonce();
|
|
69
|
+
|
|
70
|
+
const params = new URLSearchParams({
|
|
71
|
+
client_id: this.clientId,
|
|
72
|
+
redirect_uri: this.redirectUri,
|
|
73
|
+
response_type: "code",
|
|
74
|
+
scope: scope ?? this.defaultScope,
|
|
75
|
+
code_challenge: pkce.challenge,
|
|
76
|
+
code_challenge_method: "S256",
|
|
77
|
+
state: usedState,
|
|
78
|
+
nonce: usedNonce,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
url: `${this._endpoints.authorization_endpoint}?${params.toString()}`,
|
|
83
|
+
codeVerifier: pkce.verifier,
|
|
84
|
+
state: usedState,
|
|
85
|
+
nonce: usedNonce,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Callback ───────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Exchange the authorization code for tokens and verify the ID token.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} code - `code` query param from the callback URL
|
|
95
|
+
* @param {string} codeVerifier - PKCE verifier from getLoginUrl()
|
|
96
|
+
* @param {object} [opts]
|
|
97
|
+
* @param {string} [opts.expectedNonce] - nonce from getLoginUrl(); verified if supplied
|
|
98
|
+
* @returns {{ tokens: object, user: object|null }}
|
|
99
|
+
*/
|
|
100
|
+
async handleCallback(code, codeVerifier, { expectedNonce } = {}) {
|
|
101
|
+
await this.init();
|
|
102
|
+
|
|
103
|
+
const tokens = await exchangeCode({
|
|
104
|
+
code,
|
|
105
|
+
clientId: this.clientId,
|
|
106
|
+
clientSecret: this.clientSecret,
|
|
107
|
+
redirectUri: this.redirectUri,
|
|
108
|
+
codeVerifier,
|
|
109
|
+
tokenEndpoint: this._endpoints.token_endpoint,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
let user = null;
|
|
113
|
+
if (tokens.id_token) {
|
|
114
|
+
user = await this._verify(tokens.id_token, this.clientId, expectedNonce);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { tokens, user };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Token management ───────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Refresh an access token using a refresh token.
|
|
124
|
+
* @param {string} token - the refresh_token
|
|
125
|
+
*/
|
|
126
|
+
async refresh(token) {
|
|
127
|
+
await this.init();
|
|
128
|
+
return refreshAccessToken({
|
|
129
|
+
refreshToken: token,
|
|
130
|
+
clientId: this.clientId,
|
|
131
|
+
clientSecret: this.clientSecret,
|
|
132
|
+
tokenEndpoint: this._endpoints.token_endpoint,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Fetch the UserInfo claims for an access token.
|
|
138
|
+
* @param {string} accessToken
|
|
139
|
+
*/
|
|
140
|
+
async getUser(accessToken) {
|
|
141
|
+
await this.init();
|
|
142
|
+
const res = await axios.get(this._endpoints.userinfo_endpoint, {
|
|
143
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
144
|
+
});
|
|
145
|
+
return res.data;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Introspect a token (RFC 7662). Requires client credentials.
|
|
150
|
+
* @param {string} token - access_token or refresh_token to introspect
|
|
151
|
+
*/
|
|
152
|
+
async introspect(token) {
|
|
153
|
+
await this.init();
|
|
154
|
+
return introspectToken({
|
|
155
|
+
token,
|
|
156
|
+
clientId: this.clientId,
|
|
157
|
+
clientSecret: this.clientSecret,
|
|
158
|
+
introspectEndpoint: this._endpoints.introspection_endpoint,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Revoke a token (RFC 7009).
|
|
164
|
+
* @param {string} token - access_token or refresh_token to revoke
|
|
165
|
+
*/
|
|
166
|
+
async revoke(token) {
|
|
167
|
+
await this.init();
|
|
168
|
+
await revokeToken({
|
|
169
|
+
token,
|
|
170
|
+
clientId: this.clientId,
|
|
171
|
+
clientSecret: this.clientSecret,
|
|
172
|
+
revokeEndpoint: this._endpoints.revocation_endpoint,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Obtain an access token using the client_credentials grant.
|
|
178
|
+
* Only available for confidential clients.
|
|
179
|
+
* @param {object} [opts]
|
|
180
|
+
* @param {string} [opts.scope]
|
|
181
|
+
*/
|
|
182
|
+
async clientCredentials({ scope } = {}) {
|
|
183
|
+
await this.init();
|
|
184
|
+
return clientCredentialsGrant({
|
|
185
|
+
clientId: this.clientId,
|
|
186
|
+
clientSecret: this.clientSecret,
|
|
187
|
+
scope: scope ?? this.defaultScope,
|
|
188
|
+
tokenEndpoint: this._endpoints.token_endpoint,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─── Logout ─────────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Build an RP-initiated logout URL (OIDC Session Management).
|
|
196
|
+
* Redirect the user's browser to this URL to end their session.
|
|
197
|
+
*
|
|
198
|
+
* @param {object} [opts]
|
|
199
|
+
* @param {string} [opts.idTokenHint] - ID token from the session
|
|
200
|
+
* @param {string} [opts.postLogoutRedirectUri] - where to send the user after logout
|
|
201
|
+
* @param {string} [opts.state]
|
|
202
|
+
*/
|
|
203
|
+
async getLogoutUrl({ idTokenHint, postLogoutRedirectUri, state } = {}) {
|
|
204
|
+
await this.init();
|
|
205
|
+
const base = this._endpoints.end_session_endpoint;
|
|
206
|
+
if (!base) throw new Error("end_session_endpoint not advertised by this server.");
|
|
207
|
+
const params = new URLSearchParams();
|
|
208
|
+
if (idTokenHint) params.set("id_token_hint", idTokenHint);
|
|
209
|
+
if (postLogoutRedirectUri) params.set("post_logout_redirect_uri", postLogoutRedirectUri);
|
|
210
|
+
if (state) params.set("state", state);
|
|
211
|
+
const qs = params.toString();
|
|
212
|
+
return qs ? `${base}?${qs}` : base;
|
|
213
|
+
}
|
|
214
|
+
}
|
package/lib/jwks.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
2
|
+
|
|
3
|
+
// Cache JWKS key sets by URI so we don't refetch on every token verification
|
|
4
|
+
const _jwksCache = new Map();
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a JWT verifier for RS256 ID tokens.
|
|
8
|
+
*
|
|
9
|
+
* @param {string} jwksUri - from the OIDC discovery document (jwks_uri)
|
|
10
|
+
* @param {string} issuer - from the OIDC discovery document (issuer)
|
|
11
|
+
* @returns {(idToken: string, audience: string, nonce?: string) => Promise<object>}
|
|
12
|
+
*/
|
|
13
|
+
export function createVerifier(jwksUri, issuer) {
|
|
14
|
+
if (!_jwksCache.has(jwksUri)) {
|
|
15
|
+
_jwksCache.set(jwksUri, createRemoteJWKSet(new URL(jwksUri)));
|
|
16
|
+
}
|
|
17
|
+
const jwks = _jwksCache.get(jwksUri);
|
|
18
|
+
|
|
19
|
+
return async function verify(idToken, audience, nonce) {
|
|
20
|
+
const { payload } = await jwtVerify(idToken, jwks, {
|
|
21
|
+
issuer,
|
|
22
|
+
audience,
|
|
23
|
+
algorithms: ["RS256"],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (nonce !== undefined && payload.nonce !== nonce) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Nonce mismatch: expected "${nonce}", got "${payload.nonce}"`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return payload;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `protect(client)` — Express middleware that enforces a valid Bearer token.
|
|
5
|
+
*
|
|
6
|
+
* Calls client.getUser(accessToken) against the userinfo endpoint.
|
|
7
|
+
* On success, attaches the claims to req.user and calls next().
|
|
8
|
+
* On failure, responds 401.
|
|
9
|
+
*
|
|
10
|
+
* @param {import("./client.js").KerliixClient} client
|
|
11
|
+
*/
|
|
12
|
+
export function protect(client) {
|
|
13
|
+
return async (req, res, next) => {
|
|
14
|
+
const auth = req.headers.authorization;
|
|
15
|
+
if (!auth || !auth.startsWith("Bearer ")) {
|
|
16
|
+
return res.status(401).json({
|
|
17
|
+
error: "unauthorized",
|
|
18
|
+
error_description: "Bearer token required.",
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const token = auth.slice(7);
|
|
23
|
+
req.user = await client.getUser(token);
|
|
24
|
+
next();
|
|
25
|
+
} catch {
|
|
26
|
+
res.status(401).json({
|
|
27
|
+
error: "unauthorized",
|
|
28
|
+
error_description: "Invalid or expired token.",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* `routes(client)` — Mounts pre-built login and callback routes on an Express router.
|
|
36
|
+
*
|
|
37
|
+
* Requires `express-session` (or compatible session middleware) to be set up
|
|
38
|
+
* before these routes so that PKCE state can be persisted across the redirect.
|
|
39
|
+
*
|
|
40
|
+
* Mounted routes:
|
|
41
|
+
* GET /login — redirect user to Kerliix accounts
|
|
42
|
+
* GET /callback — exchange code, store tokens in session, call next()
|
|
43
|
+
*
|
|
44
|
+
* After /callback succeeds, req.oauthResult contains { tokens, user }.
|
|
45
|
+
* Add your own next() handler after this router to redirect or respond.
|
|
46
|
+
*
|
|
47
|
+
* @param {import("./client.js").KerliixClient} client
|
|
48
|
+
*/
|
|
49
|
+
export function routes(client) {
|
|
50
|
+
const router = express.Router();
|
|
51
|
+
|
|
52
|
+
router.get("/login", async (req, res, next) => {
|
|
53
|
+
try {
|
|
54
|
+
const { url, codeVerifier, state, nonce } = await client.getLoginUrl();
|
|
55
|
+
// Persist PKCE and CSRF state in session before redirecting
|
|
56
|
+
req.session = req.session ?? {};
|
|
57
|
+
req.session.oauthCodeVerifier = codeVerifier;
|
|
58
|
+
req.session.oauthState = state;
|
|
59
|
+
req.session.oauthNonce = nonce;
|
|
60
|
+
res.redirect(url);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
next(err);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
router.get("/callback", async (req, res, next) => {
|
|
67
|
+
try {
|
|
68
|
+
const { code, state, error, error_description } = req.query;
|
|
69
|
+
|
|
70
|
+
if (error) {
|
|
71
|
+
return res.status(400).json({ error, error_description });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const session = req.session ?? {};
|
|
75
|
+
|
|
76
|
+
// Validate state to prevent CSRF
|
|
77
|
+
if (session.oauthState && state !== session.oauthState) {
|
|
78
|
+
return res.status(400).json({
|
|
79
|
+
error: "invalid_state",
|
|
80
|
+
error_description: "State parameter mismatch.",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const result = await client.handleCallback(
|
|
85
|
+
code,
|
|
86
|
+
session.oauthCodeVerifier,
|
|
87
|
+
{ expectedNonce: session.oauthNonce }
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Clean up PKCE state from session
|
|
91
|
+
delete session.oauthCodeVerifier;
|
|
92
|
+
delete session.oauthState;
|
|
93
|
+
delete session.oauthNonce;
|
|
94
|
+
|
|
95
|
+
// Make result available to the next handler
|
|
96
|
+
req.oauthResult = result;
|
|
97
|
+
next();
|
|
98
|
+
} catch (err) {
|
|
99
|
+
next(err);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return router;
|
|
104
|
+
}
|
package/lib/pkce.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate a PKCE (Proof Key for Code Exchange) pair.
|
|
5
|
+
* Returns a random `verifier` and its S256 `challenge`.
|
|
6
|
+
*
|
|
7
|
+
* RFC 7636 §4.1 — verifier must be 43-128 characters of [A-Z a-z 0-9 - . _ ~]
|
|
8
|
+
*/
|
|
9
|
+
export function generatePKCE() {
|
|
10
|
+
const verifier = crypto.randomBytes(48).toString("base64url");
|
|
11
|
+
const challenge = crypto
|
|
12
|
+
.createHash("sha256")
|
|
13
|
+
.update(verifier)
|
|
14
|
+
.digest("base64url");
|
|
15
|
+
return { verifier, challenge };
|
|
16
|
+
}
|
package/lib/token.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build an HTTP Basic Authorization header from clientId and clientSecret.
|
|
5
|
+
* Percent-encodes each value per RFC 6749 §2.3.1.
|
|
6
|
+
*/
|
|
7
|
+
function basicAuth(clientId, clientSecret) {
|
|
8
|
+
const encoded = Buffer.from(
|
|
9
|
+
`${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret)}`
|
|
10
|
+
).toString("base64");
|
|
11
|
+
return `Basic ${encoded}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* POST to a token endpoint with application/x-www-form-urlencoded body.
|
|
16
|
+
* Uses client_secret_basic (Authorization header) when a clientSecret is present,
|
|
17
|
+
* falling back to including client_id in the body for public clients.
|
|
18
|
+
*/
|
|
19
|
+
async function postToken(endpoint, params, clientId, clientSecret) {
|
|
20
|
+
const body = new URLSearchParams(params);
|
|
21
|
+
const headers = { "Content-Type": "application/x-www-form-urlencoded" };
|
|
22
|
+
|
|
23
|
+
if (clientSecret) {
|
|
24
|
+
headers.Authorization = basicAuth(clientId, clientSecret);
|
|
25
|
+
} else {
|
|
26
|
+
body.set("client_id", clientId);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const res = await axios.post(endpoint, body.toString(), { headers });
|
|
30
|
+
return res.data;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ─── Grant handlers ──────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
export async function exchangeCode({
|
|
36
|
+
code,
|
|
37
|
+
clientId,
|
|
38
|
+
clientSecret,
|
|
39
|
+
redirectUri,
|
|
40
|
+
codeVerifier,
|
|
41
|
+
tokenEndpoint,
|
|
42
|
+
}) {
|
|
43
|
+
const params = {
|
|
44
|
+
grant_type: "authorization_code",
|
|
45
|
+
code,
|
|
46
|
+
redirect_uri: redirectUri,
|
|
47
|
+
...(codeVerifier ? { code_verifier: codeVerifier } : {}),
|
|
48
|
+
};
|
|
49
|
+
return postToken(tokenEndpoint, params, clientId, clientSecret);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function refreshAccessToken({
|
|
53
|
+
refreshToken,
|
|
54
|
+
clientId,
|
|
55
|
+
clientSecret,
|
|
56
|
+
tokenEndpoint,
|
|
57
|
+
}) {
|
|
58
|
+
return postToken(
|
|
59
|
+
tokenEndpoint,
|
|
60
|
+
{ grant_type: "refresh_token", refresh_token: refreshToken },
|
|
61
|
+
clientId,
|
|
62
|
+
clientSecret
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function introspectToken({
|
|
67
|
+
token,
|
|
68
|
+
clientId,
|
|
69
|
+
clientSecret,
|
|
70
|
+
introspectEndpoint,
|
|
71
|
+
}) {
|
|
72
|
+
const body = new URLSearchParams({ token });
|
|
73
|
+
const headers = {
|
|
74
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
75
|
+
Authorization: basicAuth(clientId, clientSecret),
|
|
76
|
+
};
|
|
77
|
+
const res = await axios.post(introspectEndpoint, body.toString(), { headers });
|
|
78
|
+
return res.data;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function revokeToken({
|
|
82
|
+
token,
|
|
83
|
+
clientId,
|
|
84
|
+
clientSecret,
|
|
85
|
+
revokeEndpoint,
|
|
86
|
+
}) {
|
|
87
|
+
const body = new URLSearchParams({ token });
|
|
88
|
+
const headers = {
|
|
89
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
90
|
+
Authorization: basicAuth(clientId, clientSecret),
|
|
91
|
+
};
|
|
92
|
+
await axios.post(revokeEndpoint, body.toString(), { headers });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function clientCredentialsGrant({
|
|
96
|
+
clientId,
|
|
97
|
+
clientSecret,
|
|
98
|
+
scope,
|
|
99
|
+
tokenEndpoint,
|
|
100
|
+
}) {
|
|
101
|
+
return postToken(
|
|
102
|
+
tokenEndpoint,
|
|
103
|
+
{ grant_type: "client_credentials", scope },
|
|
104
|
+
clientId,
|
|
105
|
+
clientSecret
|
|
106
|
+
);
|
|
107
|
+
}
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
|
|
3
|
+
export function buildQueryString(params) {
|
|
4
|
+
return Object.entries(params)
|
|
5
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
6
|
+
.join("&");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Generate a cryptographically random state value (hex). */
|
|
10
|
+
export function generateState() {
|
|
11
|
+
return crypto.randomBytes(16).toString("hex");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Generate a cryptographically random nonce value (hex). */
|
|
15
|
+
export function generateNonce() {
|
|
16
|
+
return crypto.randomBytes(16).toString("hex");
|
|
17
|
+
}
|
package/package.json
CHANGED
|
@@ -1,31 +1,16 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "kerliix-oauth",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Official Kerliix OAuth SDK for Node.js
|
|
5
|
-
"main": "
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"sdk",
|
|
18
|
-
"authentication",
|
|
19
|
-
"typescript",
|
|
20
|
-
"oauth"
|
|
21
|
-
],
|
|
22
|
-
"author": "Kerliix Corporation",
|
|
23
|
-
"license": "MIT",
|
|
24
|
-
"repository": {
|
|
25
|
-
"type": "git",
|
|
26
|
-
"url": "git+https://github.com/kerliix-corp/kerliix-oauth-nodejs.git"
|
|
27
|
-
},
|
|
28
|
-
"devDependencies": {
|
|
29
|
-
"typescript": "^5.6.3"
|
|
30
|
-
}
|
|
31
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "kerliix-oauth",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Official Kerliix OAuth 2.0 / OIDC SDK for Node.js",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": ["oauth", "openid", "oidc", "pkce", "kerliix", "authentication", "sdk"],
|
|
8
|
+
"author": "Kerliix Corporation Limited",
|
|
9
|
+
"license": "Apache-2.0",
|
|
10
|
+
"engines": { "node": ">=18" },
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"axios": "^1.6.0",
|
|
13
|
+
"express": "^4.18.2",
|
|
14
|
+
"jose": "^5.0.0"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
name: Build
|
|
2
|
-
|
|
3
|
-
# Run workflow on pushes and pull requests to main
|
|
4
|
-
on:
|
|
5
|
-
push:
|
|
6
|
-
branches: [ main ]
|
|
7
|
-
pull_request:
|
|
8
|
-
branches: [ main ]
|
|
9
|
-
|
|
10
|
-
jobs:
|
|
11
|
-
build:
|
|
12
|
-
name: Build and Test
|
|
13
|
-
runs-on: ubuntu-latest
|
|
14
|
-
|
|
15
|
-
strategy:
|
|
16
|
-
matrix:
|
|
17
|
-
node-version: [18, 20] # Test against Node 18 and 20
|
|
18
|
-
|
|
19
|
-
steps:
|
|
20
|
-
- name: Checkout repository
|
|
21
|
-
uses: actions/checkout@v3
|
|
22
|
-
|
|
23
|
-
- name: Setup Node.js
|
|
24
|
-
uses: actions/setup-node@v3
|
|
25
|
-
with:
|
|
26
|
-
node-version: ${{ matrix.node-version }}
|
|
27
|
-
|
|
28
|
-
- name: Install dependencies
|
|
29
|
-
run: npm install
|
|
30
|
-
|
|
31
|
-
- name: Build TypeScript
|
|
32
|
-
run: npm run build
|
|
33
|
-
|
|
34
|
-
- name: Run tests
|
|
35
|
-
run: npm test
|