@yboard/auth-mobile 0.0.1
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/dist/index.d.mts +162 -0
- package/dist/index.d.ts +162 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +43 -0
- package/src/client/cookie-http-client.ts +137 -0
- package/src/client/cookie.ts +82 -0
- package/src/client/create-client.ts +311 -0
- package/src/client/errors.ts +6 -0
- package/src/client/exceptions/index.ts +1 -0
- package/src/client/exceptions/no-client-id-provided.exception.ts +11 -0
- package/src/client/globalFetch.ts +35 -0
- package/src/client/http-client.ts +209 -0
- package/src/client/index.ts +6 -0
- package/src/client/interfaces/authentication-response.interface.ts +34 -0
- package/src/client/interfaces/create-client-options.interface.ts +69 -0
- package/src/client/interfaces/get-authorization-url-options.interface.ts +15 -0
- package/src/client/interfaces/impersonator.interface.ts +9 -0
- package/src/client/interfaces/index.ts +9 -0
- package/src/client/interfaces/user.interface.ts +27 -0
- package/src/client/serializers/authentication-response.serializer.ts +27 -0
- package/src/client/serializers/index.ts +2 -0
- package/src/client/serializers/user.serializer.ts +15 -0
- package/src/client/utils/index.ts +5 -0
- package/src/client/utils/is-redirect-callback.ts +28 -0
- package/src/client/utils/memory-storage.ts +30 -0
- package/src/client/utils/native-storage.ts +43 -0
- package/src/client/utils/pkce.ts +36 -0
- package/src/client/utils/session-data.ts +59 -0
- package/src/client/utils/storage-keys.ts +7 -0
- package/src/index.ts +11 -0
- package/src/provider/authProvider.tsx +91 -0
- package/src/provider/context.ts +12 -0
- package/src/provider/hook.ts +13 -0
- package/src/provider/state.ts +17 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
AuthenticationResponse,
|
|
3
|
+
AuthenticationResponseRaw,
|
|
4
|
+
OnRefreshResponse,
|
|
5
|
+
} from "./authentication-response.interface";
|
|
6
|
+
export type { CreateClientOptions } from "./create-client-options.interface";
|
|
7
|
+
export type { GetAuthorizationUrlOptions } from "./get-authorization-url-options.interface";
|
|
8
|
+
export type { Impersonator, ImpersonatorRaw } from "./impersonator.interface";
|
|
9
|
+
export type { User, UserRaw } from "./user.interface";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface User {
|
|
2
|
+
object: "user";
|
|
3
|
+
id: string;
|
|
4
|
+
email: string;
|
|
5
|
+
emailVerified: boolean;
|
|
6
|
+
profilePictureUrl: string | null;
|
|
7
|
+
firstName: string | null;
|
|
8
|
+
lastName: string | null;
|
|
9
|
+
lastSignInAt: string | null;
|
|
10
|
+
externalId: string | undefined;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
updatedAt: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UserRaw {
|
|
16
|
+
object: "user";
|
|
17
|
+
id: string;
|
|
18
|
+
email: string;
|
|
19
|
+
email_verified: boolean;
|
|
20
|
+
profile_picture_url: string | null;
|
|
21
|
+
first_name: string | null;
|
|
22
|
+
last_name: string | null;
|
|
23
|
+
last_sign_in_at: string | null;
|
|
24
|
+
external_id: string | undefined;
|
|
25
|
+
created_at: string;
|
|
26
|
+
updated_at: string;
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthenticationResponse,
|
|
3
|
+
AuthenticationResponseRaw,
|
|
4
|
+
} from "../interfaces";
|
|
5
|
+
import { deserializeUser } from "./user.serializer";
|
|
6
|
+
|
|
7
|
+
export const deserializeAuthenticationResponse = (
|
|
8
|
+
authenticationResponse: AuthenticationResponseRaw,
|
|
9
|
+
): AuthenticationResponse => {
|
|
10
|
+
const {
|
|
11
|
+
user,
|
|
12
|
+
organization_id,
|
|
13
|
+
access_token,
|
|
14
|
+
refresh_token,
|
|
15
|
+
impersonator,
|
|
16
|
+
...rest
|
|
17
|
+
} = authenticationResponse;
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
user: deserializeUser(user),
|
|
21
|
+
organizationId: organization_id,
|
|
22
|
+
accessToken: access_token,
|
|
23
|
+
refreshToken: refresh_token,
|
|
24
|
+
impersonator,
|
|
25
|
+
...rest,
|
|
26
|
+
};
|
|
27
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { User, UserRaw } from "../interfaces";
|
|
2
|
+
|
|
3
|
+
export const deserializeUser = (user: UserRaw): User => ({
|
|
4
|
+
object: user.object,
|
|
5
|
+
id: user.id,
|
|
6
|
+
email: user.email,
|
|
7
|
+
emailVerified: user.email_verified,
|
|
8
|
+
firstName: user.first_name,
|
|
9
|
+
profilePictureUrl: user.profile_picture_url,
|
|
10
|
+
lastName: user.last_name,
|
|
11
|
+
lastSignInAt: user.last_sign_in_at,
|
|
12
|
+
externalId: user.external_id,
|
|
13
|
+
createdAt: user.created_at,
|
|
14
|
+
updatedAt: user.updated_at,
|
|
15
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { isRedirectCallback } from "./is-redirect-callback";
|
|
2
|
+
export { memoryStorage } from "./memory-storage";
|
|
3
|
+
export { createPkceChallenge } from "./pkce";
|
|
4
|
+
export { setSessionData, removeSessionData } from "./session-data";
|
|
5
|
+
export { storageKeys } from "./storage-keys";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if the current URL is a redirect callback from the auth server.
|
|
3
|
+
*
|
|
4
|
+
* In React Native, you must pass the current URL explicitly (since window.location is not available).
|
|
5
|
+
*/
|
|
6
|
+
export function isRedirectCallback(
|
|
7
|
+
redirectUri: string,
|
|
8
|
+
searchParams: Record<string, any>,
|
|
9
|
+
currentUrl?: string // Pass the current URL explicitly in React Native
|
|
10
|
+
) {
|
|
11
|
+
// Only support the object returned by query-string
|
|
12
|
+
const hasCode = typeof searchParams === "object" && "code" in searchParams;
|
|
13
|
+
|
|
14
|
+
if (!hasCode) return false;
|
|
15
|
+
|
|
16
|
+
// If currentUrl is provided (React Native), use it.
|
|
17
|
+
let currentUri: string;
|
|
18
|
+
if (currentUrl) {
|
|
19
|
+
// Remove query and hash from currentUrl
|
|
20
|
+
const urlObj = new URL(currentUrl);
|
|
21
|
+
currentUri = `${urlObj.protocol}//${urlObj.host}${urlObj.pathname}`;
|
|
22
|
+
} else {
|
|
23
|
+
// Can't determine current URI
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return currentUri === redirectUri || currentUri === `${redirectUri}/`;
|
|
28
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
function createMemoryStorage() {
|
|
2
|
+
let _store: { [key: string]: unknown } = {};
|
|
3
|
+
|
|
4
|
+
function setItem(key: string, value: unknown): void {
|
|
5
|
+
_store[key] = value;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function getItem(key: string): unknown {
|
|
9
|
+
return _store[key];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function removeItem(key: string): void {
|
|
13
|
+
delete _store[key];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function reset(): void {
|
|
17
|
+
_store = {};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
setItem,
|
|
22
|
+
getItem,
|
|
23
|
+
removeItem,
|
|
24
|
+
reset,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const memoryStorage = createMemoryStorage();
|
|
29
|
+
|
|
30
|
+
export { memoryStorage };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
let AsyncStorage: any = null;
|
|
2
|
+
try {
|
|
3
|
+
AsyncStorage = require("@react-native-async-storage/async-storage").default;
|
|
4
|
+
} catch (e) {
|
|
5
|
+
// Not in React Native, ignore
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function isReactNative() {
|
|
9
|
+
return (
|
|
10
|
+
typeof navigator !== "undefined" &&
|
|
11
|
+
navigator.product === "ReactNative"
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const nativeStorage = {
|
|
16
|
+
async setItem(key: string, value: string) {
|
|
17
|
+
if (isReactNative() && AsyncStorage) {
|
|
18
|
+
await AsyncStorage.setItem(key, value);
|
|
19
|
+
} else {
|
|
20
|
+
// fallback to in-memory for web
|
|
21
|
+
(globalThis as any).__nativeMemoryStore = (globalThis as any).__nativeMemoryStore || {};
|
|
22
|
+
(globalThis as any).__nativeMemoryStore[key] = value;
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
async getItem(key: string): Promise<string | null> {
|
|
26
|
+
if (isReactNative() && AsyncStorage) {
|
|
27
|
+
return await AsyncStorage.getItem(key);
|
|
28
|
+
} else {
|
|
29
|
+
return (globalThis as any).__nativeMemoryStore?.[key] ?? null;
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
async removeItem(key: string) {
|
|
33
|
+
if (isReactNative() && AsyncStorage) {
|
|
34
|
+
await AsyncStorage.removeItem(key);
|
|
35
|
+
} else {
|
|
36
|
+
if ((globalThis as any).__nativeMemoryStore) {
|
|
37
|
+
delete (globalThis as any).__nativeMemoryStore[key];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export { nativeStorage };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as Crypto from "expo-crypto";
|
|
2
|
+
|
|
3
|
+
export async function createPkceChallenge() {
|
|
4
|
+
const codeVerifier = await generateCodeVerifier();
|
|
5
|
+
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
6
|
+
return { codeVerifier, codeChallenge };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function generateCodeVerifier(): Promise<string> {
|
|
10
|
+
// Generate 32 random bytes and encode as base64url
|
|
11
|
+
const randomBytes = await Crypto.getRandomBytesAsync(32);
|
|
12
|
+
return base64urlEncode(randomBytes);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function generateCodeChallenge(codeVerifier: string): Promise<string> {
|
|
16
|
+
// Hash the code verifier using SHA-256 and encode as base64url
|
|
17
|
+
const hash = await Crypto.digestStringAsync(
|
|
18
|
+
Crypto.CryptoDigestAlgorithm.SHA256,
|
|
19
|
+
codeVerifier,
|
|
20
|
+
{ encoding: Crypto.CryptoEncoding.BASE64 },
|
|
21
|
+
);
|
|
22
|
+
// Convert base64 to base64url
|
|
23
|
+
return hash.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function base64urlEncode(uint8Array: Uint8Array): string {
|
|
27
|
+
// Convert Uint8Array to base64
|
|
28
|
+
let binary = "";
|
|
29
|
+
for (let i = 0; i < uint8Array.length; i++) {
|
|
30
|
+
binary += String.fromCharCode(uint8Array[i]);
|
|
31
|
+
}
|
|
32
|
+
const base64 = btoa(binary);
|
|
33
|
+
|
|
34
|
+
// Convert base64 to base64url
|
|
35
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
36
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { AuthenticationResponse } from "../interfaces";
|
|
2
|
+
import { memoryStorage } from "./memory-storage";
|
|
3
|
+
import { nativeStorage } from "./native-storage";
|
|
4
|
+
import { storageKeys } from "./storage-keys";
|
|
5
|
+
|
|
6
|
+
interface AccessToken {
|
|
7
|
+
exp: number;
|
|
8
|
+
iat: number;
|
|
9
|
+
iss: string;
|
|
10
|
+
jti: string;
|
|
11
|
+
sid: string;
|
|
12
|
+
sub: string;
|
|
13
|
+
org_id?: string;
|
|
14
|
+
role?: string;
|
|
15
|
+
permissions?: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// should replace this with jose if we ever need to verify the JWT
|
|
19
|
+
export function getClaims(accessToken: string) {
|
|
20
|
+
return JSON.parse(atob(accessToken.split(".")[1])) as AccessToken;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function setSessionData(
|
|
24
|
+
data: AuthenticationResponse,
|
|
25
|
+
{ devMode = false } = {},
|
|
26
|
+
) {
|
|
27
|
+
const { user, accessToken, refreshToken } = data;
|
|
28
|
+
memoryStorage.setItem(storageKeys.user, user);
|
|
29
|
+
memoryStorage.setItem(storageKeys.accessToken, accessToken);
|
|
30
|
+
if (devMode) {
|
|
31
|
+
nativeStorage.setItem(storageKeys.refreshToken, refreshToken);
|
|
32
|
+
} else {
|
|
33
|
+
memoryStorage.setItem(storageKeys.refreshToken, refreshToken);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// compute a local time version of expires at (should avoid issues with slightly-wrong clocks)
|
|
37
|
+
const { exp, iat } = getClaims(accessToken);
|
|
38
|
+
const expiresIn = exp - iat;
|
|
39
|
+
const expiresAt = Date.now() + expiresIn * 1000;
|
|
40
|
+
memoryStorage.setItem(storageKeys.expiresAt, expiresAt);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function removeSessionData({ devMode = false } = {}) {
|
|
44
|
+
memoryStorage.removeItem(storageKeys.user);
|
|
45
|
+
memoryStorage.removeItem(storageKeys.accessToken);
|
|
46
|
+
if (devMode) {
|
|
47
|
+
nativeStorage.removeItem(storageKeys.refreshToken);
|
|
48
|
+
} else {
|
|
49
|
+
memoryStorage.removeItem(storageKeys.refreshToken);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function getRefreshToken({ devMode = false } = {}) {
|
|
54
|
+
if (devMode) {
|
|
55
|
+
return await nativeStorage.getItem(storageKeys.refreshToken);
|
|
56
|
+
} else {
|
|
57
|
+
return memoryStorage.getItem(storageKeys.refreshToken) as string | undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Client exports
|
|
2
|
+
export { createClient } from "./client/create-client";
|
|
3
|
+
export type { RedirectOptions } from "./client/create-client";
|
|
4
|
+
export { getClaims } from "./client/utils/session-data";
|
|
5
|
+
export type { User, AuthenticationResponse } from "./client/interfaces";
|
|
6
|
+
export { AuthKitError, LoginRequiredError } from "./client/errors";
|
|
7
|
+
export { memoryStorage } from "./client/utils/memory-storage";
|
|
8
|
+
|
|
9
|
+
// Provider exports
|
|
10
|
+
export { YboardAuthProvider } from "./provider/authProvider";
|
|
11
|
+
export { useYboardAuth } from "./provider/hook";
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createClient, LoginRequiredError } from "../client";
|
|
2
|
+
import type { RedirectOptions } from "../client/create-client";
|
|
3
|
+
import type { State } from "./state";
|
|
4
|
+
import React, { useState } from "react";
|
|
5
|
+
import { YboardAuthContext } from "./context";
|
|
6
|
+
import { initialState } from "./state";
|
|
7
|
+
|
|
8
|
+
export type Client = Pick<
|
|
9
|
+
Awaited<ReturnType<typeof createClient>>,
|
|
10
|
+
"signIn" | "signUp" | "getUser" | "getAccessToken" | "signOut"
|
|
11
|
+
>;
|
|
12
|
+
|
|
13
|
+
export type CreateClientOptions = NonNullable<
|
|
14
|
+
Parameters<typeof createClient>[1]
|
|
15
|
+
>;
|
|
16
|
+
|
|
17
|
+
type YboardAuthProviderProps = CreateClientOptions & {
|
|
18
|
+
clientId: string;
|
|
19
|
+
organizationId: string;
|
|
20
|
+
children: React.ReactNode;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function YboardAuthProvider(props: YboardAuthProviderProps) {
|
|
24
|
+
const {
|
|
25
|
+
clientId,
|
|
26
|
+
apiHostname,
|
|
27
|
+
https,
|
|
28
|
+
port,
|
|
29
|
+
redirectUri,
|
|
30
|
+
children,
|
|
31
|
+
organizationId,
|
|
32
|
+
refreshBufferInterval,
|
|
33
|
+
} = props;
|
|
34
|
+
const [client, setClient] = useState<Client>(NOOP_CLIENT);
|
|
35
|
+
const [state, setState] = React.useState(initialState);
|
|
36
|
+
|
|
37
|
+
React.useEffect(() => {
|
|
38
|
+
async function initialize() {
|
|
39
|
+
try {
|
|
40
|
+
const client = await createClient(clientId, {
|
|
41
|
+
apiHostname,
|
|
42
|
+
port,
|
|
43
|
+
organizationId,
|
|
44
|
+
https,
|
|
45
|
+
redirectUri,
|
|
46
|
+
refreshBufferInterval,
|
|
47
|
+
});
|
|
48
|
+
const user = await client.getUser();
|
|
49
|
+
|
|
50
|
+
setClient({
|
|
51
|
+
getAccessToken: client.getAccessToken.bind(client),
|
|
52
|
+
getUser: client.getUser.bind(client),
|
|
53
|
+
signIn: async (...args: [Omit<RedirectOptions, "type">?]) => {
|
|
54
|
+
await client.signIn(...args);
|
|
55
|
+
const user = await client.getUser();
|
|
56
|
+
setState((prev: State) => ({ ...prev, user }));
|
|
57
|
+
},
|
|
58
|
+
signUp: async (...args: [Omit<RedirectOptions, "type">?]) => {
|
|
59
|
+
await client.signUp(...args);
|
|
60
|
+
const user = await client.getUser();
|
|
61
|
+
setState((prev: State) => ({ ...prev, user }));
|
|
62
|
+
},
|
|
63
|
+
signOut: async (...args: [{ returnTo: string }?]) => {
|
|
64
|
+
await client.signOut(...args);
|
|
65
|
+
setState((prev: State) => ({ ...prev, user: null }));
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
setState((prev: State) => ({ ...prev, isLoading: false, user }));
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error("Error initializing auth client:", err);
|
|
71
|
+
setState((prev: State) => ({ ...prev, isLoading: false, user: null }));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
initialize();
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<YboardAuthContext.Provider value={{ ...client, ...state }}>
|
|
80
|
+
{children}
|
|
81
|
+
</YboardAuthContext.Provider>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const NOOP_CLIENT: Client = {
|
|
86
|
+
signIn: async () => {},
|
|
87
|
+
signUp: async () => {},
|
|
88
|
+
getUser: () => Promise.resolve(null),
|
|
89
|
+
getAccessToken: () => Promise.reject(new LoginRequiredError()),
|
|
90
|
+
signOut: async () => Promise.resolve(),
|
|
91
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import { type State, initialState } from "./state";
|
|
6
|
+
import type { Client } from "./authProvider";
|
|
7
|
+
|
|
8
|
+
export interface ContextValue extends Client, State { }
|
|
9
|
+
|
|
10
|
+
export const YboardAuthContext = React.createContext<ContextValue>(
|
|
11
|
+
initialState as ContextValue,
|
|
12
|
+
);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { YboardAuthContext } from "./context";
|
|
3
|
+
import { initialState } from "./state";
|
|
4
|
+
|
|
5
|
+
export function useYboardAuth() {
|
|
6
|
+
const context = React.useContext(YboardAuthContext);
|
|
7
|
+
|
|
8
|
+
if (context === initialState) {
|
|
9
|
+
throw new Error("useYboardAuth must be used within a YboardAuthProvider");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return context;
|
|
13
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { User } from "../client/interfaces";
|
|
2
|
+
|
|
3
|
+
export interface State {
|
|
4
|
+
isLoading: boolean;
|
|
5
|
+
user: User | null;
|
|
6
|
+
role: string | null;
|
|
7
|
+
organizationId: string | null;
|
|
8
|
+
permissions: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const initialState: State = {
|
|
12
|
+
isLoading: true,
|
|
13
|
+
user: null,
|
|
14
|
+
role: null,
|
|
15
|
+
organizationId: null,
|
|
16
|
+
permissions: [],
|
|
17
|
+
};
|