@odatnurd/kinde-auth 0.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.
@@ -0,0 +1,24 @@
1
+ name: Publish to NPMJS
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - v*
7
+ workflow_dispatch:
8
+ inputs:
9
+ tag:
10
+ description: 'The tag to release, e.g., v1.2.3'
11
+ required: true
12
+
13
+ jobs:
14
+ publish-npm-package-after-testing:
15
+ permissions:
16
+ id-token: write
17
+ contents: write
18
+ uses: odatnurd/github-workflows/.github/workflows/npm-publish.yaml@npm-publish-v1.0.3
19
+ with:
20
+ node-version: 24
21
+ package-version: ${{ (github.event_name == 'workflow_dispatch' && inputs.tag) || github.ref_name }}
22
+ create-release: true
23
+ test-script: 'test'
24
+ build-script: 'build:prod'
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Terence Martin
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 ADDED
@@ -0,0 +1,21 @@
1
+ # Kinde Auth Helpers
2
+
3
+ Small library to wrap Kinde Auth; still under heavy development.
4
+
5
+ > **Stormtrooper:** Let me see your identification.
6
+ >
7
+ > **Obi-Wan:** *(waves hand slightly)* You don't need to see his identification.
8
+ >
9
+ > **Stormtrooper:** We don't need to see his identification.
10
+ >
11
+ > **Obi-Wan:** This isn't the Auth library you're looking for.
12
+ >
13
+ > **Stormtrooper:** This isn't the Auth library we're looking for.
14
+ >
15
+ > **Obi-Wan:** He can go about his business.
16
+ >
17
+ > **Stormtrooper:** You can go about your business.
18
+ >
19
+ > **Obi-Wan:** Move along.
20
+ >
21
+ > **Stormtrooper:** Move along... move along.
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@odatnurd/kinde-auth",
3
+ "version": "0.1.0",
4
+ "description": "Authentication handling for Kinde auth",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": {
8
+ "name": "OdatNurd",
9
+ "email": "odatnurd@gmail.com",
10
+ "url": "https://odatnurd.net"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/OdatNurd/kinde-auth"
15
+ },
16
+ "exports": {
17
+ "./core": "./dist/core/index.js",
18
+ "./client": "./dist/client/index.js",
19
+ "./worker": "./dist/worker/index.js"
20
+ },
21
+ "devDependencies": {
22
+ "@kinde-oss/kinde-auth-pkce-js": "^4.3.0",
23
+ "@rollup/plugin-commonjs": "^29.0.2",
24
+ "@rollup/plugin-node-resolve": "^16.0.3",
25
+ "@rollup/plugin-terser": "^1.0.0",
26
+ "jose": "^6.2.3",
27
+ "rollup": "^4.60.4"
28
+ },
29
+ "keywords": [
30
+ "kinde",
31
+ "auth",
32
+ "iam"
33
+ ],
34
+ "scripts": {
35
+ "build:dev": "rollup -c --environment BUILD_ENV:development",
36
+ "build:prod": "rollup -c --environment BUILD_ENV:production",
37
+ "watch": "rollup -c -w --environment BUILD_ENV:development",
38
+ "test": "echo \"Warning: no test specified\" && exit 0"
39
+ }
40
+ }
@@ -0,0 +1,102 @@
1
+ /******************************************************************************/
2
+
3
+
4
+ import resolve from '@rollup/plugin-node-resolve';
5
+ import commonjs from '@rollup/plugin-commonjs';
6
+ import terser from '@rollup/plugin-terser';
7
+
8
+
9
+ /******************************************************************************/
10
+
11
+
12
+ /* The build environment that we are targeting. */
13
+ const build_env = process.env.BUILD_ENV ?? 'development';
14
+
15
+ /* Evaluates if the environment target is configured for deployment. */
16
+ const is_production = build_env === 'production';
17
+
18
+ /* Files mentioned here are not inlined into build bundles; this generally only
19
+ * applies to the code in the core library, which is client and worker agnostic.
20
+ *
21
+ * References to the files are kept as is, and since the output folder is kept
22
+ * in the same file layout as the source folders are, when the compiled code is
23
+ * used, it picks up the proper file automatically. */
24
+ const internalExternal = [
25
+ '../core/index.js'
26
+ ];
27
+
28
+ /* Set up the optimization plugins that should only be injected into the build
29
+ * pipeline when creating production assets. */
30
+ const optimizationPlugins = is_production === true ? [
31
+ terser({
32
+ toplevel: true,
33
+ compress: {
34
+ passes: 2,
35
+ drop_console: true
36
+ },
37
+ format: {
38
+ comments: 'some'
39
+ }
40
+ })
41
+ ] : [];
42
+
43
+
44
+ /******************************************************************************/
45
+
46
+
47
+ export default [
48
+ /* The core module contains all code that is common to the Client and Worker;
49
+ * there is nothing contained in here that is either browser or worker
50
+ * specific. */
51
+ {
52
+ input: 'src/core/index.js',
53
+ output: {
54
+ file: 'dist/core/index.js',
55
+ format: 'esm'
56
+ },
57
+ plugins: [
58
+ resolve(),
59
+ commonjs(),
60
+ ...optimizationPlugins
61
+ ]
62
+ },
63
+
64
+ /* The client module contains all code that is intended to be used in the
65
+ * browser by client-side code. */
66
+ {
67
+ input: 'src/client/index.js',
68
+ external: internalExternal,
69
+ output: {
70
+ file: 'dist/client/index.js',
71
+ format: 'esm'
72
+ },
73
+ plugins: [
74
+ resolve({
75
+ browser: true
76
+ }),
77
+ commonjs(),
78
+ ...optimizationPlugins
79
+ ]
80
+ },
81
+
82
+ /* The worker module contains all code that is intended to be used in worker
83
+ * code. */
84
+ {
85
+ input: 'src/worker/index.js',
86
+ external: internalExternal,
87
+ output: {
88
+ file: 'dist/worker/index.js',
89
+ format: 'esm'
90
+ },
91
+ plugins: [
92
+ resolve({
93
+ exportConditions: ['worker', 'import']
94
+ }),
95
+ commonjs(),
96
+ ...optimizationPlugins
97
+ ]
98
+ }
99
+ ];
100
+
101
+
102
+ /******************************************************************************/
@@ -0,0 +1,115 @@
1
+ /******************************************************************************/
2
+
3
+
4
+ import createKindeClient from '@kinde-oss/kinde-auth-pkce-js';
5
+ import { extractFlags } from '../core/index.js';
6
+
7
+
8
+ /******************************************************************************/
9
+
10
+
11
+ /* Initializes the Kinde authentication client for browser-based Single Page
12
+ * Applications (SPAs) using the PKCE flow.
13
+ *
14
+ * This factory function takes the config parameters provided, instantiates the
15
+ * underlying Kinde SDK, and then returns a clean, simplified API surface.
16
+ *
17
+ * This abstracts away the need for the application to directly parse tokens by
18
+ * providing a getContext() method that fully evaluates the user's session
19
+ * state, roles, permissions, and feature flags.
20
+ *
21
+ * The configuration object is as would be passed to createKindeClient, and can
22
+ * have the following keys:
23
+ * Required:
24
+ * client_id: The unique ID of the application in Kinde.
25
+ * domain: Your Kinde domain (e.g., 'https://yourdomain.kinde.com').
26
+ * redirect_uri: The local URL to redirect back to after a successful login.
27
+ *
28
+ * Optional:
29
+ * logout_uri: The local URL to redirect to after a successful logout.
30
+ * audience: The API identifier to ensure tokens are scoped to your
31
+ * specific backend.
32
+ * scope: A space-separated list of requested scopes (defaults to
33
+ * 'openid profile email offline').
34
+ *
35
+ * The turn value of the call is an object that contains the following methods:
36
+ * login: (Async) Redirects to the Kinde login flow.
37
+ * register: (Async) Redirects to the Kinde registration flow.
38
+ * logout: (Async) Clears the local session and redirects to the
39
+ * Kinde logout flow.
40
+ * getToken: (Async) Returns the raw JWT access token/
41
+ * isAuthenticated: (Async) Returns a boolean indicating if a valid, unexpired
42
+ * session currently exists.
43
+ * getContext: (Async) Returns the master user state object, or null if
44
+ * the user is not authenticated.
45
+ *
46
+ * Application code can call getContext() in order to obtain an object that has
47
+ * decoded information from the token:
48
+ * profile: An object containing the user's profile information extracted
49
+ * from the ID token (e.g., id, given_name, email).
50
+ * permissions: An object containing the permissions granted to the user.
51
+ * roles: A list of role objects assigned to the user within the Kinde
52
+ * environment; null if there are none
53
+ * featureFlag: A function that returns the value of the named feature flag,
54
+ * returning the default value if the flag is not set.
55
+ * flags: A flattened dictionary of all feature flags; the featureFlag()
56
+ * function accesses this.
57
+ */
58
+ export async function createClientAuth(config) {
59
+ // Initializing the client automatically evaluates the current URL to see if
60
+ // the user is returning from a Kinde login redirect, parsing the token
61
+ // fragments out of the URL if present.
62
+ const kinde = await createKindeClient(config);
63
+
64
+ return {
65
+ // Standard authentication flow triggers. These redirect the browser to the
66
+ // hosted Kinde pages to handle user credentials securely.
67
+ login: async () => await kinde.login(),
68
+ register: async () => await kinde.register(),
69
+ logout: async () => await kinde.logout(),
70
+
71
+ // Retrieves the raw JWT access token string, which is necessary to inject
72
+ // into the Authorization header when making calls to our secured backend
73
+ // APIs.
74
+ getToken: async () => await kinde.getToken(),
75
+
76
+ // Quickly evaluates if the user has a valid, non-expired session token.
77
+ isAuthenticated: async () => await kinde.isAuthenticated(),
78
+
79
+ // Constructs and returns the master state object for the authenticated
80
+ // user.
81
+ //
82
+ // This function extracts all relevant claims from both the ID token and the
83
+ // Access token. If the user is not authenticated, it cleanly returns null
84
+ // to allow the application to easily route them away from secured areas
85
+ // as needed.
86
+ getContext: async () => {
87
+ // Not a lot to do if the user is not authenticated.
88
+ const isAuthenticated = await kinde.isAuthenticated();
89
+ if (isAuthenticated === false) {
90
+ return null;
91
+ }
92
+
93
+ // Retrieve ID Token Data; this data contains the profile information for
94
+ // the user.
95
+ const user = await kinde.getUser();
96
+
97
+ // Retrieve Access Token Data; this data contains the authorization info
98
+ // for the user.
99
+ const permissionsData = await kinde.getPermissions();
100
+ const rolesClaim = await kinde.getClaim('roles');
101
+ const flagsClaim = await kinde.getClaim('feature_flags');
102
+ const flagsContext = extractFlags(flagsClaim !== null ? flagsClaim.value : null);
103
+
104
+ return {
105
+ profile: user,
106
+ permissions: permissionsData,
107
+ roles: rolesClaim !== null ? rolesClaim.value : null,
108
+ ...flagsContext,
109
+ };
110
+ }
111
+ };
112
+ }
113
+
114
+
115
+ /******************************************************************************/
@@ -0,0 +1,54 @@
1
+ /******************************************************************************/
2
+
3
+
4
+ /* Extracts and flattens feature flags from the raw token payload into a simple
5
+ * key-value dictionary.
6
+ *
7
+ * Kinde provides feature flags as nested objects that contain the name of the
8
+ * flags, their values, and their types:
9
+ *
10
+ * "feature_flags": {
11
+ * "a_boolean_flag": {
12
+ * "t": "b",
13
+ * "v": false
14
+ * },
15
+ * "a_string_flag": {
16
+ * "t": "s",
17
+ * "v": "control"
18
+ * },
19
+ * "an_integer_flag": {
20
+ * "t": "i",
21
+ * "v": 1000
22
+ * }
23
+ * }
24
+ *
25
+ * The Kinde API for the client side has routines for pulling out flags, but
26
+ * they require you to name the type (and generate errors if you pick the wrong
27
+ * one.
28
+ *
29
+ * This utility flattens the structure down to just an object with the keys and
30
+ * values directly, such as { flag_name: 'value' }, so that application code can
31
+ * easily check for flags without worrying about the nested structure.
32
+ *
33
+ * The return value is an object with a flags key (which may be empty if there
34
+ * are no flags or an invalid object was passed in) and a featureFlag function
35
+ * that returns the value of a given flag, optionally giving you a default value
36
+ * if the flag does not exist. */
37
+ export function extractFlags(featureFlags) {
38
+ const flags = {};
39
+
40
+ // If we got a flags object, pull the keys out into a new flattened object.
41
+ if (featureFlags !== undefined && featureFlags !== null) {
42
+ for (const [key, data] of Object.entries(featureFlags)) {
43
+ flags[key] = data.v;
44
+ }
45
+ }
46
+
47
+ return {
48
+ flags,
49
+ featureFlag: (flag, defaultValue) => flags[flag] === undefined ? defaultValue : flags[flag],
50
+ }
51
+ }
52
+
53
+
54
+ /******************************************************************************/
@@ -0,0 +1,97 @@
1
+ /******************************************************************************/
2
+
3
+
4
+ import { createRemoteJWKSet, jwtVerify } from 'jose';
5
+ import { extractFlags } from '../core/index.js';
6
+
7
+
8
+ /******************************************************************************/
9
+
10
+
11
+ /* A global cache used to persist the JSON Web Key Set (JWKS) across multiple
12
+ * request invocations within the same Cloudflare Worker isolate.
13
+ *
14
+ * Fetching the public keys from Kinde on every single request would add severe
15
+ * latency. By declaring this outside the request handler, the Worker keeps the
16
+ * keys in memory as long as the isolate stays warm. */
17
+ let jwksCache = null;
18
+
19
+
20
+ /******************************************************************************/
21
+
22
+
23
+ /* Parses the incoming HTTP request to extract the JWT from the Authorization
24
+ * header. It strictly expects the standard 'Bearer <token>' format.
25
+ *
26
+ * Returns null if the header is missing or improperly formatted. */
27
+ export function extractBearerToken(request) {
28
+ const authHeader = request.headers.get('Authorization');
29
+ if (authHeader !== null && authHeader.startsWith('Bearer ') === true) {
30
+ return authHeader.split(' ')[1];
31
+ }
32
+
33
+ return null;
34
+ }
35
+
36
+
37
+ /******************************************************************************/
38
+
39
+
40
+ /* Verifies the cryptographic signature of the provided JWT against Kinde's
41
+ * published public keys (JWKS) to guarantee the token was genuinely issued by
42
+ * Kinde and has not been tampered with or expired.
43
+ *
44
+ * If the verification is successful, the payload is decoded and used to create
45
+ * the exact same object that the client side library produces from the client
46
+ * object's getContext() function call.
47
+ *
48
+ * This allows application code to expect a uniform structure regardless of what
49
+ * side it is running on.
50
+ *
51
+ * The returned object will have a success key that indicates if the validation
52
+ * was successful or not; if not, the error field says why the validation failed
53
+ * for error reporting purposes; otherwise, the context key is the same value
54
+ * as would be returned from a call to the client side getContext() function
55
+ * call. */
56
+ export async function verifyToken(token, jwksUrl, issuer) {
57
+ // Lazily initialize the JWKS cache on the first authentication attempt the
58
+ // worker handles. Subsequent requests will reuse this cached object.
59
+ if (jwksCache === null) {
60
+ jwksCache = createRemoteJWKSet(new URL(jwksUrl));
61
+ }
62
+
63
+ try {
64
+ // jwtVerify automatically checks the signature and the expiration (exp)
65
+ // claim. By passing the issuer, we also enforce that this token came
66
+ // specifically from our configured Kinde environment.
67
+ const { payload } = await jwtVerify(token, jwksCache, {
68
+ issuer: issuer
69
+ });
70
+
71
+ // Extract and parse the flags in the token payload.
72
+ const flagsContext = extractFlags(payload.feature_flags);
73
+
74
+ return {
75
+ success: true,
76
+ context: {
77
+ id: payload.sub,
78
+ permissions: payload.permissions || [],
79
+ rawPayload: payload,
80
+ ...flagsContext,
81
+ },
82
+ error: null
83
+ };
84
+ } catch (err) {
85
+ // Verification fails if the token is expired, the signature is invalid, or
86
+ // the issuer mismatches. We return a normalized failure state so the
87
+ // middleware can cleanly reject the request.
88
+ return {
89
+ success: false,
90
+ context: null,
91
+ error: err.message
92
+ };
93
+ }
94
+ }
95
+
96
+
97
+ /******************************************************************************/