@nexys/user-management-sdk 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/client.d.ts +29 -0
- package/dist/client.js +49 -0
- package/dist/context.d.ts +16 -0
- package/dist/context.js +65 -0
- package/dist/login-utils.d.ts +4 -0
- package/dist/login-utils.js +9 -0
- package/dist/sso-response.d.ts +13 -0
- package/dist/sso-response.js +36 -0
- package/dist/type.d.ts +36 -0
- package/dist/type.js +12 -0
- package/dist/utils.d.ts +6 -0
- package/dist/utils.js +34 -0
- package/dist/utils.test.d.ts +1 -0
- package/dist/utils.test.js +78 -0
- package/dist/webauthn.d.ts +9 -0
- package/dist/webauthn.js +150 -0
- package/package.json +33 -0
package/README.md
ADDED
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Locale, Profile, SSOService, UserAdmin, Permission } from "./type";
|
|
2
|
+
declare class AuthClient {
|
|
3
|
+
apiBasename: string;
|
|
4
|
+
constructor(apiBasename: string);
|
|
5
|
+
authSSOUrl: (service: SSOService) => Promise<{
|
|
6
|
+
url: string;
|
|
7
|
+
}>;
|
|
8
|
+
authGoogleRedirect: (ssoService: SSOService, code: string, state: string) => Promise<{
|
|
9
|
+
profile: {
|
|
10
|
+
id: string;
|
|
11
|
+
};
|
|
12
|
+
permissions: number[];
|
|
13
|
+
locale: {
|
|
14
|
+
country: string;
|
|
15
|
+
lang: string;
|
|
16
|
+
};
|
|
17
|
+
}>;
|
|
18
|
+
getProfile: () => Promise<{
|
|
19
|
+
profile: Profile;
|
|
20
|
+
permissions: Permission[];
|
|
21
|
+
locale: Locale;
|
|
22
|
+
}>;
|
|
23
|
+
authLogout: () => Promise<any>;
|
|
24
|
+
adminUserList: () => Promise<UserAdmin[]>;
|
|
25
|
+
authRefresh: () => Promise<{
|
|
26
|
+
message: string;
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
29
|
+
export default AuthClient;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// auth stuff
|
|
2
|
+
const redirectUrl = (ssoService) => [window.location.origin, "auth", "sso", ssoService, "redirect"].join("/");
|
|
3
|
+
class AuthClient {
|
|
4
|
+
apiBasename;
|
|
5
|
+
constructor(apiBasename) {
|
|
6
|
+
this.apiBasename = apiBasename;
|
|
7
|
+
}
|
|
8
|
+
authSSOUrl = async (service) => {
|
|
9
|
+
const response = await fetch(`${this.apiBasename}/auth/${service}/url?redirectUrl=${encodeURIComponent(redirectUrl(service))}`);
|
|
10
|
+
return response.json();
|
|
11
|
+
};
|
|
12
|
+
authGoogleRedirect = async (ssoService, code, state) => {
|
|
13
|
+
const response = await fetch(this.apiBasename +
|
|
14
|
+
"/auth/" +
|
|
15
|
+
ssoService +
|
|
16
|
+
"/redirect?code=" +
|
|
17
|
+
encodeURIComponent(code) +
|
|
18
|
+
"&state=" +
|
|
19
|
+
encodeURIComponent(state));
|
|
20
|
+
const j = await response.json();
|
|
21
|
+
if (response.ok) {
|
|
22
|
+
return j;
|
|
23
|
+
}
|
|
24
|
+
return j;
|
|
25
|
+
};
|
|
26
|
+
getProfile = async () => {
|
|
27
|
+
const response = await fetch(this.apiBasename + "/profile");
|
|
28
|
+
const j = await response.json();
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw j;
|
|
31
|
+
}
|
|
32
|
+
return j;
|
|
33
|
+
};
|
|
34
|
+
authLogout = async () => {
|
|
35
|
+
const response = await fetch(this.apiBasename + "/auth/logout");
|
|
36
|
+
return response.json();
|
|
37
|
+
};
|
|
38
|
+
adminUserList = async () => {
|
|
39
|
+
const response = await fetch(this.apiBasename + "/admin/user/list", {
|
|
40
|
+
method: "POST",
|
|
41
|
+
});
|
|
42
|
+
return response.json();
|
|
43
|
+
};
|
|
44
|
+
authRefresh = async () => {
|
|
45
|
+
const response = await fetch(this.apiBasename + "/auth/refresh");
|
|
46
|
+
return response.json();
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export default AuthClient;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React, { ReactNode } from "react";
|
|
2
|
+
import AuthClient from "./client.js";
|
|
3
|
+
import { Profile } from "./type.js";
|
|
4
|
+
export type AuthContextType = {
|
|
5
|
+
profile: Profile;
|
|
6
|
+
logout: () => void;
|
|
7
|
+
};
|
|
8
|
+
export declare const AdminContext: React.Context<AuthContextType | undefined>;
|
|
9
|
+
export declare const AuthProvider: ({ authClient, Spinner, loginPath, }: {
|
|
10
|
+
authClient: AuthClient;
|
|
11
|
+
Spinner: () => JSX.Element;
|
|
12
|
+
loginPath: string;
|
|
13
|
+
}) => ({ children }: {
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export declare const useAuth: () => AuthContextType;
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React, { createContext, useContext, useState } from "react";
|
|
3
|
+
import { useNavigate } from "react-router-dom";
|
|
4
|
+
export const AdminContext = createContext(undefined);
|
|
5
|
+
export const AuthProvider = ({ authClient, Spinner, loginPath, }) => ({ children }) => {
|
|
6
|
+
const navigate = useNavigate();
|
|
7
|
+
const [profile, setProfile] = useState(null);
|
|
8
|
+
React.useEffect(() => {
|
|
9
|
+
try {
|
|
10
|
+
authClient
|
|
11
|
+
.getProfile()
|
|
12
|
+
.then(({ profile }) => setProfile(profile))
|
|
13
|
+
.catch(async () => {
|
|
14
|
+
try {
|
|
15
|
+
await authClient.authRefresh();
|
|
16
|
+
const { profile } = await authClient.getProfile();
|
|
17
|
+
setProfile(profile);
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
console.log("e1", e);
|
|
21
|
+
const navigationMessage = {
|
|
22
|
+
message: "Your session is no longer active, you were redirected",
|
|
23
|
+
contextClass: "warning",
|
|
24
|
+
};
|
|
25
|
+
navigate("/login", { state: navigationMessage });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
console.log("err2", err);
|
|
31
|
+
const navigationMessage = {
|
|
32
|
+
message: "Your session is no longer active, you were redirected",
|
|
33
|
+
contextClass: "warning",
|
|
34
|
+
};
|
|
35
|
+
return navigate(loginPath, { state: navigationMessage });
|
|
36
|
+
}
|
|
37
|
+
}, []);
|
|
38
|
+
if (profile === null) {
|
|
39
|
+
return _jsx(Spinner, {});
|
|
40
|
+
}
|
|
41
|
+
const logout = () => {
|
|
42
|
+
setProfile(null);
|
|
43
|
+
authClient.authLogout();
|
|
44
|
+
const navigationMessage = {
|
|
45
|
+
message: "You were successfully logged out",
|
|
46
|
+
contextClass: "info",
|
|
47
|
+
};
|
|
48
|
+
const q = btoa([profile.id, profile.instance.uuid].join(":"));
|
|
49
|
+
return navigate(loginPath + "?q=" + q, {
|
|
50
|
+
state: navigationMessage,
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
const value = {
|
|
54
|
+
profile,
|
|
55
|
+
logout,
|
|
56
|
+
};
|
|
57
|
+
return (_jsx(AdminContext.Provider, { value: value, children: children }));
|
|
58
|
+
};
|
|
59
|
+
export const useAuth = () => {
|
|
60
|
+
const context = useContext(AdminContext);
|
|
61
|
+
if (context === undefined) {
|
|
62
|
+
throw new Error("useAdminUseCase must be used within an AuthProvider");
|
|
63
|
+
}
|
|
64
|
+
return context;
|
|
65
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const getLoginInfoFromQuery = (lSearch) => {
|
|
2
|
+
const queryParams = new URLSearchParams(lSearch);
|
|
3
|
+
const paramValue = queryParams.get("q");
|
|
4
|
+
if (!paramValue) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
const [userId, instanceId] = atob(paramValue).split(":");
|
|
8
|
+
return { userId, instanceId };
|
|
9
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import AuthClient from "./client.js";
|
|
2
|
+
declare const SsoResponse: ({ authClient, links, Banner, Spinner, }: {
|
|
3
|
+
authClient: AuthClient;
|
|
4
|
+
links: {
|
|
5
|
+
home: string;
|
|
6
|
+
login: string;
|
|
7
|
+
};
|
|
8
|
+
Banner: (props: {
|
|
9
|
+
label: string;
|
|
10
|
+
}) => JSX.Element;
|
|
11
|
+
Spinner: () => JSX.Element;
|
|
12
|
+
}) => () => JSX.Element;
|
|
13
|
+
export default SsoResponse;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useNavigate, useParams } from "react-router-dom";
|
|
3
|
+
const SsoResponse = ({ authClient, links, Banner, Spinner, }) => () => {
|
|
4
|
+
const navigate = useNavigate();
|
|
5
|
+
const params = useParams();
|
|
6
|
+
const queryParams = new URLSearchParams(window.location.search);
|
|
7
|
+
const code = queryParams.get("code");
|
|
8
|
+
const state = queryParams.get("state");
|
|
9
|
+
const ssoService = params.ssoService;
|
|
10
|
+
if (!ssoService) {
|
|
11
|
+
return _jsx(Banner, { label: "Could not find associated sso service" });
|
|
12
|
+
}
|
|
13
|
+
if (code === null) {
|
|
14
|
+
return _jsx(Banner, { label: "Code must be given" });
|
|
15
|
+
}
|
|
16
|
+
if (state === null) {
|
|
17
|
+
return _jsx(Banner, { label: "State must be given" });
|
|
18
|
+
}
|
|
19
|
+
authClient
|
|
20
|
+
.authGoogleRedirect(ssoService, code, state)
|
|
21
|
+
.then(() => {
|
|
22
|
+
navigate(links.home);
|
|
23
|
+
})
|
|
24
|
+
.catch((err) => {
|
|
25
|
+
console.log("err", err);
|
|
26
|
+
const navigationMessage = {
|
|
27
|
+
message: "Could not log you in",
|
|
28
|
+
contextClass: "error",
|
|
29
|
+
};
|
|
30
|
+
navigate(links.login, {
|
|
31
|
+
state: navigationMessage,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
return _jsx(Spinner, {});
|
|
35
|
+
};
|
|
36
|
+
export default SsoResponse;
|
package/dist/type.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface Profile {
|
|
2
|
+
id: string;
|
|
3
|
+
firstName: string;
|
|
4
|
+
lastName: string;
|
|
5
|
+
email: string;
|
|
6
|
+
instance: {
|
|
7
|
+
uuid: string;
|
|
8
|
+
name: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export interface Locale {
|
|
12
|
+
country: string;
|
|
13
|
+
lang: string;
|
|
14
|
+
}
|
|
15
|
+
export declare enum UserStatus {
|
|
16
|
+
active = 1,
|
|
17
|
+
inactive = 2,
|
|
18
|
+
pending = 3
|
|
19
|
+
}
|
|
20
|
+
export interface UserAdmin extends Omit<Profile, "id"> {
|
|
21
|
+
uuid: string;
|
|
22
|
+
locale: Locale;
|
|
23
|
+
status: UserStatus;
|
|
24
|
+
}
|
|
25
|
+
export type SSOService = "microsoft" | "apple" | "github" | "google";
|
|
26
|
+
export declare const ssoAvailable: SSOService[];
|
|
27
|
+
export type ContextClass = "success" | "error" | "warning" | "info" | "default";
|
|
28
|
+
export interface NavigationMessage {
|
|
29
|
+
message: string;
|
|
30
|
+
contextClass: ContextClass;
|
|
31
|
+
}
|
|
32
|
+
export declare enum Permission {
|
|
33
|
+
app = 1,
|
|
34
|
+
admin = 2,
|
|
35
|
+
superadmin = 3
|
|
36
|
+
}
|
package/dist/type.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export var UserStatus;
|
|
2
|
+
(function (UserStatus) {
|
|
3
|
+
UserStatus[UserStatus["active"] = 1] = "active";
|
|
4
|
+
UserStatus[UserStatus["inactive"] = 2] = "inactive";
|
|
5
|
+
UserStatus[UserStatus["pending"] = 3] = "pending";
|
|
6
|
+
})(UserStatus || (UserStatus = {}));
|
|
7
|
+
export const ssoAvailable = [
|
|
8
|
+
"microsoft",
|
|
9
|
+
"apple",
|
|
10
|
+
"github",
|
|
11
|
+
"google",
|
|
12
|
+
];
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const getLoginInfoFromQuery: (lSearch: string) => {
|
|
2
|
+
userId: string;
|
|
3
|
+
instanceId: string;
|
|
4
|
+
} | null;
|
|
5
|
+
export declare const base64UrlToUint8Array: (base64Url: string) => Uint8Array;
|
|
6
|
+
export declare const arrayBufferToBase64: (buffer: ArrayBuffer) => string;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const getLoginInfoFromQuery = (lSearch) => {
|
|
2
|
+
const queryParams = new URLSearchParams(lSearch);
|
|
3
|
+
const paramValue = queryParams.get("q");
|
|
4
|
+
if (!paramValue) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
const [userId, instanceId] = atob(paramValue).split(":");
|
|
8
|
+
return { userId, instanceId };
|
|
9
|
+
};
|
|
10
|
+
export const base64UrlToUint8Array = (base64Url) => {
|
|
11
|
+
// Replace Base64URL characters with Base64 characters
|
|
12
|
+
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
|
13
|
+
// Decode Base64 string to binary string
|
|
14
|
+
const binaryString = atob(base64);
|
|
15
|
+
// Create a Uint8Array from the binary string
|
|
16
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
17
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
18
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
19
|
+
}
|
|
20
|
+
return bytes;
|
|
21
|
+
};
|
|
22
|
+
export const arrayBufferToBase64 = (buffer) => {
|
|
23
|
+
let binary = "";
|
|
24
|
+
const bytes = new Uint8Array(buffer);
|
|
25
|
+
const len = bytes.byteLength;
|
|
26
|
+
for (let i = 0; i < len; i++) {
|
|
27
|
+
binary += String.fromCharCode(bytes[i]);
|
|
28
|
+
}
|
|
29
|
+
// Convert binary data to a base64 string
|
|
30
|
+
const base64 = btoa(binary);
|
|
31
|
+
// Optionally, you might want to make the base64 string URL-safe
|
|
32
|
+
// by replacing "+" with "-", "/" with "_", and stripping "=" padding
|
|
33
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
34
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Import necessary modules
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { describe, it } from "node:test";
|
|
4
|
+
import { getLoginInfoFromQuery, base64UrlToUint8Array, arrayBufferToBase64, } from "./utils.js";
|
|
5
|
+
// Mock browser's atob and btoa for Node.js
|
|
6
|
+
global.atob = (base64) => Buffer.from(base64, "base64").toString("binary");
|
|
7
|
+
global.btoa = (binary) => Buffer.from(binary, "binary").toString("base64");
|
|
8
|
+
// Describe the test suite
|
|
9
|
+
describe("getLoginInfoFromQuery", () => {
|
|
10
|
+
it('should return null if the query does not contain the "q" parameter', () => {
|
|
11
|
+
const result = getLoginInfoFromQuery("?someOtherParam=abc");
|
|
12
|
+
assert.strictEqual(result, null);
|
|
13
|
+
});
|
|
14
|
+
it('should return null if the "q" parameter is missing', () => {
|
|
15
|
+
const result = getLoginInfoFromQuery("");
|
|
16
|
+
assert.strictEqual(result, null);
|
|
17
|
+
});
|
|
18
|
+
it('should decode the "q" parameter and return the correct userId and instanceId', () => {
|
|
19
|
+
const encodedParam = Buffer.from("user123:instance456").toString("base64");
|
|
20
|
+
const query = `?q=${encodedParam}`;
|
|
21
|
+
const result = getLoginInfoFromQuery(query);
|
|
22
|
+
assert.deepStrictEqual(result, {
|
|
23
|
+
userId: "user123",
|
|
24
|
+
instanceId: "instance456",
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
it('should handle missing ":" in decoded string and return undefined values', () => {
|
|
28
|
+
const encodedParam = Buffer.from("user123").toString("base64");
|
|
29
|
+
const query = `?q=${encodedParam}`;
|
|
30
|
+
const result = getLoginInfoFromQuery(query);
|
|
31
|
+
assert.deepStrictEqual(result, {
|
|
32
|
+
userId: "user123",
|
|
33
|
+
instanceId: undefined,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
// Tests for base64UrlToUint8Array
|
|
38
|
+
describe("base64UrlToUint8Array", () => {
|
|
39
|
+
it("should correctly convert Base64URL string to Uint8Array", () => {
|
|
40
|
+
const base64Url = "SGVsbG8td29ybGQ_"; // Base64URL for "Hello-world?"
|
|
41
|
+
const expectedArray = new Uint8Array([
|
|
42
|
+
72, 101, 108, 108, 111, 45, 119, 111, 114, 108, 100, 63,
|
|
43
|
+
]);
|
|
44
|
+
const result = base64UrlToUint8Array(base64Url);
|
|
45
|
+
assert.deepStrictEqual(result, expectedArray);
|
|
46
|
+
});
|
|
47
|
+
it("should handle padding correctly", () => {
|
|
48
|
+
const base64Url = "SGVsbG8gd29ybGQ"; // Base64URL for "Hello world" without padding
|
|
49
|
+
const expectedArray = new Uint8Array([
|
|
50
|
+
72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100,
|
|
51
|
+
]);
|
|
52
|
+
const result = base64UrlToUint8Array(base64Url);
|
|
53
|
+
assert.deepStrictEqual(result, expectedArray);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
// Tests for arrayBufferToBase64
|
|
57
|
+
describe("arrayBufferToBase64", () => {
|
|
58
|
+
it("should correctly convert ArrayBuffer to Base64URL string", () => {
|
|
59
|
+
const buffer = new Uint8Array([
|
|
60
|
+
72, 101, 108, 108, 111, 45, 119, 111, 114, 108, 100, 63,
|
|
61
|
+
]).buffer;
|
|
62
|
+
const expectedBase64Url = "SGVsbG8td29ybGQ_";
|
|
63
|
+
const result = arrayBufferToBase64(buffer);
|
|
64
|
+
assert.strictEqual(result, expectedBase64Url);
|
|
65
|
+
});
|
|
66
|
+
it("should handle empty ArrayBuffer", () => {
|
|
67
|
+
const buffer = new ArrayBuffer(0);
|
|
68
|
+
const expectedBase64Url = "";
|
|
69
|
+
const result = arrayBufferToBase64(buffer);
|
|
70
|
+
assert.strictEqual(result, expectedBase64Url);
|
|
71
|
+
});
|
|
72
|
+
it("should handle large ArrayBuffer", () => {
|
|
73
|
+
const buffer = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]).buffer;
|
|
74
|
+
const expectedBase64Url = "AAECAwQFBgcICQ";
|
|
75
|
+
const result = arrayBufferToBase64(buffer);
|
|
76
|
+
assert.strictEqual(result, expectedBase64Url);
|
|
77
|
+
});
|
|
78
|
+
});
|
package/dist/webauthn.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import * as U from "./utils";
|
|
2
|
+
// webauthn client
|
|
3
|
+
const getPreLogin = async (user, instance) => {
|
|
4
|
+
const r = await fetch("/napi/auth/webauthn/prelogin", {
|
|
5
|
+
method: "POST",
|
|
6
|
+
headers: { "content-type": "application/json" },
|
|
7
|
+
body: JSON.stringify({ uuid: user.uuid, instance }), // send user id
|
|
8
|
+
});
|
|
9
|
+
const j = await r.json();
|
|
10
|
+
if (!r.ok && "error" in j) {
|
|
11
|
+
throw Error(j.error);
|
|
12
|
+
}
|
|
13
|
+
return j;
|
|
14
|
+
};
|
|
15
|
+
const loginRequest = async (jwt, { credentialId, authenticatorData, clientDataJSON, signature, }) => {
|
|
16
|
+
const respose = await fetch("/napi/auth/webauthn/login", {
|
|
17
|
+
method: "POST",
|
|
18
|
+
body: JSON.stringify({
|
|
19
|
+
jwt,
|
|
20
|
+
credentialId,
|
|
21
|
+
authenticatorData,
|
|
22
|
+
clientDataJSON,
|
|
23
|
+
signature,
|
|
24
|
+
}),
|
|
25
|
+
headers: { "content-type": "application/json" },
|
|
26
|
+
});
|
|
27
|
+
return respose.json();
|
|
28
|
+
};
|
|
29
|
+
function uuidToUint8Array(uuid) {
|
|
30
|
+
// Remove all hyphens from the UUID
|
|
31
|
+
const hexNoDashes = uuid.replace(/-/g, "");
|
|
32
|
+
// Make sure the cleaned hex string is of length 32
|
|
33
|
+
if (hexNoDashes.length !== 32) {
|
|
34
|
+
throw new Error("Invalid UUID format");
|
|
35
|
+
}
|
|
36
|
+
const arrayBuffer = new Uint8Array(16);
|
|
37
|
+
for (let i = 0, j = 0; i < 32; i += 2, j++) {
|
|
38
|
+
// Convert each pair of hexadecimal characters to a byte
|
|
39
|
+
arrayBuffer[j] = parseInt(hexNoDashes.substr(i, 2), 16);
|
|
40
|
+
}
|
|
41
|
+
return arrayBuffer;
|
|
42
|
+
}
|
|
43
|
+
export const login = async (user, instance) => {
|
|
44
|
+
const { challenge, authenticators } = await getPreLogin(user, instance);
|
|
45
|
+
// console.log({ challenge });
|
|
46
|
+
const transports = [
|
|
47
|
+
// "usb",
|
|
48
|
+
// "nfc",
|
|
49
|
+
// "ble",
|
|
50
|
+
"internal",
|
|
51
|
+
]; // Specify acceptable transports if known
|
|
52
|
+
const allowCredentials = authenticators.map((cred) => ({
|
|
53
|
+
id: U.base64UrlToUint8Array(cred.credentialId), // Convert from Base64 URL to Uint8Array
|
|
54
|
+
type: "public-key",
|
|
55
|
+
transports,
|
|
56
|
+
}));
|
|
57
|
+
const newCredentialInfo = await navigator.credentials.get({
|
|
58
|
+
publicKey: {
|
|
59
|
+
challenge: U.base64UrlToUint8Array(challenge),
|
|
60
|
+
allowCredentials,
|
|
61
|
+
userVerification: "required", // "preferred", // or 'required' or 'discouraged'
|
|
62
|
+
timeout: 60000, // Optional: adjust according to your needs
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
if (!newCredentialInfo) {
|
|
66
|
+
throw Error("failing to retrieve credentials");
|
|
67
|
+
}
|
|
68
|
+
// Find which credential was used
|
|
69
|
+
const matchedCredential = authenticators.find((cred) => U.base64UrlToUint8Array(cred.credentialId).toString() ===
|
|
70
|
+
new Uint8Array(newCredentialInfo.rawId).toString());
|
|
71
|
+
// console.log(matchedCredential, "!");
|
|
72
|
+
if (!matchedCredential) {
|
|
73
|
+
throw Error("could not match credentials");
|
|
74
|
+
}
|
|
75
|
+
const { response } = newCredentialInfo;
|
|
76
|
+
const authenticatorData = U.arrayBufferToBase64(response.authenticatorData);
|
|
77
|
+
const clientDataJSON = U.arrayBufferToBase64(response.clientDataJSON);
|
|
78
|
+
const signature = U.arrayBufferToBase64(response.signature);
|
|
79
|
+
return loginRequest(matchedCredential.jwt, {
|
|
80
|
+
credentialId: matchedCredential.credentialId,
|
|
81
|
+
authenticatorData,
|
|
82
|
+
clientDataJSON,
|
|
83
|
+
signature,
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
const preRegister = async () => {
|
|
87
|
+
const response = await fetch("/napi/profile/webauthn/register");
|
|
88
|
+
return response.json();
|
|
89
|
+
};
|
|
90
|
+
const registerRequest = async ({ credentialId, attestationObject, clientDataJSON, jwt, }) => {
|
|
91
|
+
const respose = await fetch("/napi/profile/webauthn/register", {
|
|
92
|
+
method: "POST",
|
|
93
|
+
body: JSON.stringify({
|
|
94
|
+
jwt,
|
|
95
|
+
credentialId,
|
|
96
|
+
attestationObject,
|
|
97
|
+
clientDataJSON,
|
|
98
|
+
}),
|
|
99
|
+
headers: { "content-type": "application/json" },
|
|
100
|
+
});
|
|
101
|
+
return respose.json();
|
|
102
|
+
};
|
|
103
|
+
export const register = async () => {
|
|
104
|
+
//getChallenge();
|
|
105
|
+
const { challenge, rp, user, jwt } = await preRegister();
|
|
106
|
+
const publicKey = {
|
|
107
|
+
// Relying Party (your service)
|
|
108
|
+
rp,
|
|
109
|
+
// User Information
|
|
110
|
+
user: {
|
|
111
|
+
id: uuidToUint8Array(user.uuid),
|
|
112
|
+
name: user.name,
|
|
113
|
+
displayName: user.displayName,
|
|
114
|
+
},
|
|
115
|
+
// Cryptographic challenge from the server
|
|
116
|
+
challenge: U.base64UrlToUint8Array(challenge),
|
|
117
|
+
// Public key parameters
|
|
118
|
+
pubKeyCredParams: [
|
|
119
|
+
{ alg: -7, type: "public-key" }, // ES256
|
|
120
|
+
{ alg: -257, type: "public-key" }, // RS256
|
|
121
|
+
], // Registration timeout
|
|
122
|
+
timeout: 60000, // 60 seconds
|
|
123
|
+
// Attestation preference
|
|
124
|
+
attestation: "direct",
|
|
125
|
+
// Exclude existing credentials to prevent re-registration
|
|
126
|
+
excludeCredentials: [], // Use this to exclude already registered credentials
|
|
127
|
+
authenticatorSelection: {
|
|
128
|
+
authenticatorAttachment: ["platform", "cross-platform"], // for now platform, in a second step add phones and external keys
|
|
129
|
+
requireResidentKey: false,
|
|
130
|
+
userVerification: "preferred",
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
// Request the creation of new credentials
|
|
134
|
+
const newCredentialInfo = (await navigator.credentials.create({
|
|
135
|
+
publicKey,
|
|
136
|
+
}));
|
|
137
|
+
const credentialId = newCredentialInfo.id;
|
|
138
|
+
// const publicKey = new TextDecoder().decode(
|
|
139
|
+
// newCredentialInfo.response.clientDataJSON
|
|
140
|
+
//); // For demonstration; actual extraction differs
|
|
141
|
+
const attestationObject = U.arrayBufferToBase64(newCredentialInfo.response.attestationObject);
|
|
142
|
+
const clientDataJSON = U.arrayBufferToBase64(newCredentialInfo.response.clientDataJSON);
|
|
143
|
+
console.log({ credentialId, attestationObject, clientDataJSON });
|
|
144
|
+
return registerRequest({
|
|
145
|
+
jwt,
|
|
146
|
+
credentialId,
|
|
147
|
+
attestationObject,
|
|
148
|
+
clientDataJSON,
|
|
149
|
+
});
|
|
150
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nexys/user-management-sdk",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "react client/sdk that faciliates connecting to the user management",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@types/node": "^22.5.4",
|
|
9
|
+
"@types/react": "^18.3.5",
|
|
10
|
+
"react": "^18.3.1",
|
|
11
|
+
"react-router-dom": "^6.26.2",
|
|
12
|
+
"typescript": "^5.6.2"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"test": "node dist/*.test.js",
|
|
21
|
+
"buildPackage": "yarn build; rm dist/*.test.js; rm dist/*.test.d.ts"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+ssh://git@github.com/nexys-system/user-management-client.git"
|
|
26
|
+
},
|
|
27
|
+
"author": "Nexys",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/nexys-system/user-management-client/issues"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/nexys-system/user-management-client#readme"
|
|
33
|
+
}
|