@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,311 @@
|
|
|
1
|
+
import * as AuthSession from "expo-auth-session";
|
|
2
|
+
import * as SecureStore from "expo-secure-store";
|
|
3
|
+
import * as WebBrowser from "expo-web-browser";
|
|
4
|
+
import { Platform } from "react-native";
|
|
5
|
+
import { getCookie, getSetCookie } from "./cookie";
|
|
6
|
+
import { CookieHttpClient } from "./cookie-http-client";
|
|
7
|
+
import { NoClientIdProvidedException } from "./exceptions";
|
|
8
|
+
import { NoOrganizationIdProvidedException } from "./exceptions/no-client-id-provided.exception";
|
|
9
|
+
import { HttpClient } from "./http-client";
|
|
10
|
+
import type { CreateClientOptions, User } from "./interfaces";
|
|
11
|
+
import {
|
|
12
|
+
createPkceChallenge,
|
|
13
|
+
isRedirectCallback,
|
|
14
|
+
memoryStorage,
|
|
15
|
+
storageKeys,
|
|
16
|
+
} from "./utils";
|
|
17
|
+
|
|
18
|
+
export interface RedirectOptions {
|
|
19
|
+
provider?: string;
|
|
20
|
+
context?: string;
|
|
21
|
+
invitationToken?: string;
|
|
22
|
+
loginHint?: string;
|
|
23
|
+
organizationId?: string;
|
|
24
|
+
passwordResetToken?: string;
|
|
25
|
+
state?: any;
|
|
26
|
+
type: "sign-in" | "sign-up";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class Client {
|
|
30
|
+
readonly #httpClient: HttpClient | CookieHttpClient;
|
|
31
|
+
#redirectUri: string;
|
|
32
|
+
|
|
33
|
+
constructor(
|
|
34
|
+
clientId: string,
|
|
35
|
+
{
|
|
36
|
+
apiHostname: hostname,
|
|
37
|
+
https,
|
|
38
|
+
port,
|
|
39
|
+
organizationId,
|
|
40
|
+
redirectUri,
|
|
41
|
+
devMode,
|
|
42
|
+
}: CreateClientOptions,
|
|
43
|
+
) {
|
|
44
|
+
if (!clientId) {
|
|
45
|
+
throw new NoClientIdProvidedException();
|
|
46
|
+
}
|
|
47
|
+
if (!organizationId) {
|
|
48
|
+
throw new NoOrganizationIdProvidedException();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Platform-specific defaults
|
|
52
|
+
if (Platform.OS === "web") {
|
|
53
|
+
// --- Web defaults ---
|
|
54
|
+
redirectUri = redirectUri ?? window.origin;
|
|
55
|
+
devMode =
|
|
56
|
+
devMode ??
|
|
57
|
+
(location.hostname === "localhost" ||
|
|
58
|
+
location.hostname === "127.0.0.1");
|
|
59
|
+
} else {
|
|
60
|
+
// --- Native defaults ---
|
|
61
|
+
redirectUri = redirectUri ?? "";
|
|
62
|
+
devMode = devMode ?? false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Use CookieHttpClient for web (cookie-based), HttpClient for native (token-based)
|
|
66
|
+
if (Platform.OS === "web") {
|
|
67
|
+
this.#httpClient = new CookieHttpClient({
|
|
68
|
+
clientId,
|
|
69
|
+
hostname,
|
|
70
|
+
port,
|
|
71
|
+
organizationId,
|
|
72
|
+
https,
|
|
73
|
+
});
|
|
74
|
+
} else {
|
|
75
|
+
this.#httpClient = new HttpClient({
|
|
76
|
+
clientId,
|
|
77
|
+
hostname,
|
|
78
|
+
port,
|
|
79
|
+
organizationId,
|
|
80
|
+
https,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.#redirectUri = redirectUri;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ============================================================
|
|
88
|
+
// Cross-platform methods
|
|
89
|
+
// ============================================================
|
|
90
|
+
|
|
91
|
+
async getAccessToken(): Promise<string | null> {
|
|
92
|
+
if (Platform.OS === "web") {
|
|
93
|
+
return this.#getAccessTokenWeb();
|
|
94
|
+
} else {
|
|
95
|
+
return this.#getAccessTokenNative();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async getUser(): Promise<User | null> {
|
|
100
|
+
if (Platform.OS === "web") {
|
|
101
|
+
return this.#getUserWeb();
|
|
102
|
+
} else {
|
|
103
|
+
return this.#getUserNative();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async #redirect(options: RedirectOptions) {
|
|
108
|
+
if (Platform.OS === "web") {
|
|
109
|
+
return this.#redirectWeb(options);
|
|
110
|
+
} else {
|
|
111
|
+
return this.#redirectNative(options);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async initialize() {
|
|
116
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
117
|
+
if (isRedirectCallback(this.#redirectUri, searchParams)) {
|
|
118
|
+
await this.#handleCallback();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async signIn(opts: Omit<RedirectOptions, "type"> = {}) {
|
|
123
|
+
return this.#redirect({ ...opts, type: "sign-in" });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async signUp(opts: Omit<RedirectOptions, "type"> = {}) {
|
|
127
|
+
return this.#redirect({ ...opts, type: "sign-up" });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async signOut(options?: { returnTo: string }) {
|
|
131
|
+
if (Platform.OS === "web") {
|
|
132
|
+
return this.#signOutWeb(options);
|
|
133
|
+
} else {
|
|
134
|
+
return this.#signOutNative(options);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Native methods
|
|
139
|
+
async #signOutNative(options?: { returnTo: string }) {
|
|
140
|
+
const redirectUri =
|
|
141
|
+
options?.returnTo ||
|
|
142
|
+
AuthSession.makeRedirectUri({ native: "yboardstarter:" });
|
|
143
|
+
|
|
144
|
+
const logoutUrl = await (this.#httpClient as HttpClient).getLogoutUrl({
|
|
145
|
+
returnTo: redirectUri,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (logoutUrl) {
|
|
149
|
+
try {
|
|
150
|
+
const result = await WebBrowser.openAuthSessionAsync(
|
|
151
|
+
logoutUrl,
|
|
152
|
+
redirectUri,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
if (result.type === "success") {
|
|
156
|
+
await SecureStore.deleteItemAsync("yboard-session");
|
|
157
|
+
} else {
|
|
158
|
+
console.warn("User cancelled or dismissed logout:", result.type);
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error("Error during sign out:", error);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async #getAccessTokenNative(): Promise<string | null> {
|
|
167
|
+
const stored = await SecureStore.getItemAsync("yboard-session");
|
|
168
|
+
const cookie = getCookie(stored || "{}");
|
|
169
|
+
const accessToken = await (this.#httpClient as HttpClient).getAccessToken({
|
|
170
|
+
cookie,
|
|
171
|
+
});
|
|
172
|
+
return (accessToken as string) || null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async #getUserNative(): Promise<User | null> {
|
|
176
|
+
const stored = await SecureStore.getItemAsync("yboard-session");
|
|
177
|
+
const cookie = getCookie(stored || "{}");
|
|
178
|
+
const user = await (this.#httpClient as HttpClient).getUser({ cookie });
|
|
179
|
+
if (!user) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
return user as User;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async #redirectNative({
|
|
186
|
+
context,
|
|
187
|
+
invitationToken,
|
|
188
|
+
loginHint,
|
|
189
|
+
organizationId,
|
|
190
|
+
passwordResetToken,
|
|
191
|
+
state,
|
|
192
|
+
}: RedirectOptions) {
|
|
193
|
+
const { codeVerifier, codeChallenge } = await createPkceChallenge();
|
|
194
|
+
memoryStorage.setItem(storageKeys.codeVerifier, codeVerifier);
|
|
195
|
+
|
|
196
|
+
const { code } = await (
|
|
197
|
+
this.#httpClient as HttpClient
|
|
198
|
+
).startAuthorizationFlow({
|
|
199
|
+
codeChallenge,
|
|
200
|
+
codeChallengeMethod: "S256",
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (code) {
|
|
204
|
+
const cookie = await this.#authenticate({
|
|
205
|
+
code,
|
|
206
|
+
codeVerifier,
|
|
207
|
+
});
|
|
208
|
+
if (cookie && typeof cookie === "string") {
|
|
209
|
+
const prev = await SecureStore.getItemAsync("yboard-session");
|
|
210
|
+
const updated = getSetCookie(cookie, prev || undefined);
|
|
211
|
+
await SecureStore.setItemAsync("yboard-session", updated);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async #authenticate({
|
|
217
|
+
code,
|
|
218
|
+
codeVerifier,
|
|
219
|
+
}: {
|
|
220
|
+
code: string;
|
|
221
|
+
codeVerifier: string;
|
|
222
|
+
}): Promise<string | null> {
|
|
223
|
+
if (typeof codeVerifier !== "string") return null;
|
|
224
|
+
|
|
225
|
+
const cookie = await (this.#httpClient as HttpClient).authenticateWithCode({
|
|
226
|
+
code,
|
|
227
|
+
codeVerifier: codeVerifier,
|
|
228
|
+
redirectUri: this.#redirectUri,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return cookie;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Web methods
|
|
235
|
+
async getCookieDomain(origin: string) {
|
|
236
|
+
const url = new URL(origin);
|
|
237
|
+
const domainParts = url.hostname.split(".");
|
|
238
|
+
if (domainParts.length > 1) {
|
|
239
|
+
return "." + domainParts.slice(-2).join(".");
|
|
240
|
+
}
|
|
241
|
+
return url.hostname;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async #signOutWeb(options?: { returnTo: string }) {
|
|
245
|
+
const url = await (this.#httpClient as CookieHttpClient).getLogoutUrl({
|
|
246
|
+
returnTo: options?.returnTo,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
if (url) {
|
|
250
|
+
const domain = await this.getCookieDomain(this.#redirectUri);
|
|
251
|
+
document.cookie = `yboard-session=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${domain}`;
|
|
252
|
+
window.location.assign(url);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async #getAccessTokenWeb(): Promise<string | null> {
|
|
257
|
+
const accessToken = await (
|
|
258
|
+
this.#httpClient as CookieHttpClient
|
|
259
|
+
).getAccessToken();
|
|
260
|
+
return accessToken as string | null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async #getUserWeb(): Promise<User | null> {
|
|
264
|
+
const user = await (this.#httpClient as CookieHttpClient).getUser();
|
|
265
|
+
return user as User | null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async #handleCallback() {
|
|
269
|
+
const cleanUrl = new URL(window.location.toString());
|
|
270
|
+
cleanUrl.search = "";
|
|
271
|
+
window.history.replaceState({}, "", cleanUrl);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async #redirectWeb({
|
|
275
|
+
context,
|
|
276
|
+
invitationToken,
|
|
277
|
+
loginHint,
|
|
278
|
+
organizationId,
|
|
279
|
+
passwordResetToken,
|
|
280
|
+
state,
|
|
281
|
+
type,
|
|
282
|
+
provider,
|
|
283
|
+
}: RedirectOptions) {
|
|
284
|
+
const url = await (
|
|
285
|
+
this.#httpClient as CookieHttpClient
|
|
286
|
+
).getAuthorizationUrl({
|
|
287
|
+
context,
|
|
288
|
+
invitationToken,
|
|
289
|
+
loginHint,
|
|
290
|
+
organizationId,
|
|
291
|
+
passwordResetToken,
|
|
292
|
+
redirectUri: this.#redirectUri,
|
|
293
|
+
state: state ? JSON.stringify(state) : undefined,
|
|
294
|
+
});
|
|
295
|
+
window.location.assign(url);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export async function createClient(
|
|
300
|
+
clientId: string,
|
|
301
|
+
options: CreateClientOptions,
|
|
302
|
+
) {
|
|
303
|
+
const client = new Client(clientId, options);
|
|
304
|
+
|
|
305
|
+
// Only initialize for web platform
|
|
306
|
+
if (Platform.OS === "web") {
|
|
307
|
+
await client.initialize();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return client;
|
|
311
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export class AuthKitError extends Error {}
|
|
2
|
+
export class RefreshError extends AuthKitError {}
|
|
3
|
+
export class CodeExchangeError extends AuthKitError {}
|
|
4
|
+
export class LoginRequiredError extends AuthKitError {
|
|
5
|
+
readonly message: string = "No access token available";
|
|
6
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { NoClientIdProvidedException } from "./no-client-id-provided.exception";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export class NoClientIdProvidedException extends Error {
|
|
2
|
+
readonly status: number = 500;
|
|
3
|
+
readonly name: string = "NoClientIdProvidedException";
|
|
4
|
+
readonly message: string = `Missing Client ID. Pass it to the constructor (createClient("client_01HXRMBQ9BJ3E7QSTQ9X2PHVB7"))`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class NoOrganizationIdProvidedException extends Error {
|
|
8
|
+
readonly status: number = 500;
|
|
9
|
+
readonly name: string = "NoOrganizationIdProvidedException";
|
|
10
|
+
readonly message: string = `Missing Organization ID. Pass it to the constructor`;
|
|
11
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const fetchWithOrganizationIdAndCookie = ({
|
|
2
|
+
baseUrl,
|
|
3
|
+
organizationId,
|
|
4
|
+
cookie,
|
|
5
|
+
path,
|
|
6
|
+
method,
|
|
7
|
+
options,
|
|
8
|
+
body = {},
|
|
9
|
+
}: {
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
organizationId: string;
|
|
12
|
+
cookie?: string;
|
|
13
|
+
path: string;
|
|
14
|
+
method: "GET" | "POST" | "PUT" | "DELETE";
|
|
15
|
+
options?: RequestInit;
|
|
16
|
+
body?: Record<string, unknown>;
|
|
17
|
+
}) => {
|
|
18
|
+
const fetchOptions: RequestInit = {
|
|
19
|
+
method,
|
|
20
|
+
headers: {
|
|
21
|
+
"Accept": "application/json, text/plain, */*",
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
"X-Organization-Id": organizationId,
|
|
24
|
+
"cookie": cookie || "",
|
|
25
|
+
},
|
|
26
|
+
credentials: "include",
|
|
27
|
+
...options,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
if (method !== "GET" && body) {
|
|
31
|
+
fetchOptions.body = JSON.stringify(body);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return fetch(`${baseUrl}${path}`, fetchOptions);
|
|
35
|
+
};
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import * as AuthSession from "expo-auth-session";
|
|
2
|
+
import * as WebBrowser from "expo-web-browser";
|
|
3
|
+
import { CodeExchangeError } from "./errors";
|
|
4
|
+
import { fetchWithOrganizationIdAndCookie } from "./globalFetch";
|
|
5
|
+
import { User } from "./interfaces";
|
|
6
|
+
|
|
7
|
+
export class HttpClient {
|
|
8
|
+
readonly #baseUrl: string;
|
|
9
|
+
readonly #clientId: string;
|
|
10
|
+
readonly #organizationId: string;
|
|
11
|
+
|
|
12
|
+
constructor({
|
|
13
|
+
clientId,
|
|
14
|
+
hostname,
|
|
15
|
+
port,
|
|
16
|
+
organizationId,
|
|
17
|
+
https = true,
|
|
18
|
+
}: {
|
|
19
|
+
clientId: string;
|
|
20
|
+
hostname?: string;
|
|
21
|
+
organizationId: string;
|
|
22
|
+
port?: number;
|
|
23
|
+
https?: boolean;
|
|
24
|
+
}) {
|
|
25
|
+
this.#baseUrl = `${https ? "https" : "http"}://${hostname}${port ? `:${port}` : ""}`;
|
|
26
|
+
this.#clientId = clientId;
|
|
27
|
+
this.#organizationId = organizationId;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#buildAuthUrl({
|
|
31
|
+
redirectUri,
|
|
32
|
+
codeChallenge,
|
|
33
|
+
codeChallengeMethod,
|
|
34
|
+
clientId,
|
|
35
|
+
organizationId,
|
|
36
|
+
}: {
|
|
37
|
+
redirectUri: string;
|
|
38
|
+
codeChallenge: string;
|
|
39
|
+
codeChallengeMethod: string;
|
|
40
|
+
clientId: string;
|
|
41
|
+
organizationId: string;
|
|
42
|
+
}) {
|
|
43
|
+
const params = new URLSearchParams({
|
|
44
|
+
response_type: "code",
|
|
45
|
+
clientId,
|
|
46
|
+
redirectUri,
|
|
47
|
+
codeChallenge,
|
|
48
|
+
codeChallengeMethod,
|
|
49
|
+
organizationId,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return `${this.#baseUrl}/api/user_management/mobile/signIn?${params.toString()}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async startAuthorizationFlow({
|
|
56
|
+
codeChallenge,
|
|
57
|
+
codeChallengeMethod,
|
|
58
|
+
}: {
|
|
59
|
+
codeChallenge: string;
|
|
60
|
+
codeChallengeMethod: "S256";
|
|
61
|
+
}) {
|
|
62
|
+
const redirectUri = AuthSession.makeRedirectUri({
|
|
63
|
+
native: "yboardstarter:",
|
|
64
|
+
}).toString();
|
|
65
|
+
const authUrl = this.#buildAuthUrl({
|
|
66
|
+
redirectUri,
|
|
67
|
+
codeChallenge,
|
|
68
|
+
codeChallengeMethod,
|
|
69
|
+
clientId: this.#clientId,
|
|
70
|
+
organizationId: this.#organizationId,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
let code: string | undefined;
|
|
74
|
+
|
|
75
|
+
const webResult = await WebBrowser.openAuthSessionAsync(
|
|
76
|
+
authUrl,
|
|
77
|
+
redirectUri,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
switch (webResult.type) {
|
|
81
|
+
case "success":
|
|
82
|
+
if (webResult.url) {
|
|
83
|
+
const urlParts = webResult.url.split("?");
|
|
84
|
+
if (urlParts.length > 1) {
|
|
85
|
+
const queryString = urlParts[1];
|
|
86
|
+
const params = queryString.split("&");
|
|
87
|
+
for (const param of params) {
|
|
88
|
+
const [key, value] = param.split("=");
|
|
89
|
+
if (key === "code") {
|
|
90
|
+
code = decodeURIComponent(value);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
console.warn("Success result but no URL returned");
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
|
|
100
|
+
case "cancel":
|
|
101
|
+
console.warn("User cancelled the auth flow.");
|
|
102
|
+
throw new Error("Authentication cancelled.");
|
|
103
|
+
|
|
104
|
+
case "dismiss":
|
|
105
|
+
console.warn(
|
|
106
|
+
"Auth flow was dismissed (possibly due to backgrounding).",
|
|
107
|
+
);
|
|
108
|
+
throw new Error("Authentication dismissed.");
|
|
109
|
+
|
|
110
|
+
case "locked":
|
|
111
|
+
console.warn("Another auth session is already in progress.");
|
|
112
|
+
throw new Error("Authentication session already active.");
|
|
113
|
+
|
|
114
|
+
default:
|
|
115
|
+
console.error("Unhandled web result type:", webResult.type);
|
|
116
|
+
throw new Error(`Unhandled auth session result: ${webResult.type}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!code) {
|
|
120
|
+
throw new Error("No authorization code found in redirect URL.");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { code };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async authenticateWithCode({
|
|
127
|
+
code,
|
|
128
|
+
codeVerifier,
|
|
129
|
+
redirectUri,
|
|
130
|
+
}: {
|
|
131
|
+
code: string;
|
|
132
|
+
codeVerifier: string;
|
|
133
|
+
redirectUri: string;
|
|
134
|
+
}): Promise<string | null> {
|
|
135
|
+
const response = await fetch(
|
|
136
|
+
`${this.#baseUrl}/api/user_management/mobile/authenticate`,
|
|
137
|
+
{
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: {
|
|
140
|
+
"Content-Type": "application/json",
|
|
141
|
+
"X-Organization-Id": this.#organizationId,
|
|
142
|
+
},
|
|
143
|
+
body: JSON.stringify({
|
|
144
|
+
code,
|
|
145
|
+
client_id: this.#clientId,
|
|
146
|
+
grant_type: "authorization_code",
|
|
147
|
+
code_verifier: codeVerifier,
|
|
148
|
+
redirect_uri: redirectUri,
|
|
149
|
+
}),
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
const error = await response.json();
|
|
155
|
+
throw new CodeExchangeError(error.error_description);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const setCookie = response.headers.get("set-cookie");
|
|
159
|
+
return setCookie;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async getUser({ cookie }: { cookie?: string } = {}) {
|
|
163
|
+
const response = await fetchWithOrganizationIdAndCookie({
|
|
164
|
+
baseUrl: this.#baseUrl,
|
|
165
|
+
organizationId: this.#organizationId,
|
|
166
|
+
cookie,
|
|
167
|
+
path: "/api/user_management/mobile/getUser",
|
|
168
|
+
method: "GET",
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
const { user } = (await response.json()) as { user: User };
|
|
175
|
+
return user;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async getAccessToken({ cookie }: { cookie?: string } = {}) {
|
|
179
|
+
const response = await fetchWithOrganizationIdAndCookie({
|
|
180
|
+
baseUrl: this.#baseUrl,
|
|
181
|
+
organizationId: this.#organizationId,
|
|
182
|
+
cookie,
|
|
183
|
+
path: "/api/user_management/mobile/getAccessToken",
|
|
184
|
+
method: "GET",
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const { accessToken } = (await response.json()) as { accessToken: string };
|
|
188
|
+
return accessToken;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async getLogoutUrl({ returnTo }: { returnTo?: string } = {}): Promise<
|
|
192
|
+
string | null
|
|
193
|
+
> {
|
|
194
|
+
const response = await fetch(
|
|
195
|
+
`${this.#baseUrl}/api/user_management/mobile/getLogOutUrl`,
|
|
196
|
+
{
|
|
197
|
+
method: "POST",
|
|
198
|
+
headers: {
|
|
199
|
+
"Content-Type": "application/json",
|
|
200
|
+
"X-Organization-Id": this.#organizationId,
|
|
201
|
+
},
|
|
202
|
+
body: JSON.stringify({ client_id: this.#clientId, returnTo }),
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const { url } = await response.json();
|
|
207
|
+
return url;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createClient } from "./create-client";
|
|
2
|
+
export type { RedirectOptions } from "./create-client";
|
|
3
|
+
export { getClaims } from "./utils/session-data";
|
|
4
|
+
export type { User, AuthenticationResponse } from "./interfaces";
|
|
5
|
+
export { AuthKitError, LoginRequiredError } from "./errors";
|
|
6
|
+
export { memoryStorage } from "./utils/memory-storage";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { User, UserRaw } from "./user.interface";
|
|
2
|
+
import type { Impersonator, ImpersonatorRaw } from "./impersonator.interface";
|
|
3
|
+
|
|
4
|
+
export interface AuthenticationResponse {
|
|
5
|
+
user: User;
|
|
6
|
+
accessToken: string;
|
|
7
|
+
refreshToken: string;
|
|
8
|
+
organizationId?: string;
|
|
9
|
+
impersonator?: Impersonator;
|
|
10
|
+
sealedSession?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AuthenticationResponseWithOrganizationSelection {
|
|
14
|
+
rawData: {
|
|
15
|
+
code: string;
|
|
16
|
+
message: string;
|
|
17
|
+
pending_authentication_token: string;
|
|
18
|
+
user: User;
|
|
19
|
+
organizations: { id: string; name: string }[];
|
|
20
|
+
requestId: string;
|
|
21
|
+
}
|
|
22
|
+
status: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type OnRefreshResponse = Omit<AuthenticationResponse, "refreshToken">;
|
|
26
|
+
|
|
27
|
+
export interface AuthenticationResponseRaw {
|
|
28
|
+
user: UserRaw;
|
|
29
|
+
access_token: string;
|
|
30
|
+
refresh_token: string;
|
|
31
|
+
organization_id?: string;
|
|
32
|
+
impersonator?: ImpersonatorRaw;
|
|
33
|
+
sealedSession?: string;
|
|
34
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthenticationResponse,
|
|
3
|
+
OnRefreshResponse,
|
|
4
|
+
} from "./authentication-response.interface";
|
|
5
|
+
|
|
6
|
+
export interface CreateClientOptions {
|
|
7
|
+
/**
|
|
8
|
+
* How many seconds before the access token expiration to attempt a refresh.
|
|
9
|
+
*/
|
|
10
|
+
refreshBufferInterval?: number;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The API hostname to connect to
|
|
14
|
+
*/
|
|
15
|
+
apiHostname?: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Whether to use HTTPS (default: true)
|
|
19
|
+
*/
|
|
20
|
+
https?: boolean;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The port number to connect to
|
|
24
|
+
*/
|
|
25
|
+
port?: number;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The organization ID for authentication
|
|
29
|
+
*/
|
|
30
|
+
organizationId: string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The redirect URI for OAuth flow
|
|
34
|
+
*/
|
|
35
|
+
redirectUri?: string;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Whether the app is running in development mode
|
|
39
|
+
* For web: defaults to true if on localhost or 127.0.0.1
|
|
40
|
+
* For native: defaults to false
|
|
41
|
+
*/
|
|
42
|
+
devMode?: boolean;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Callback to determine if auto-refresh should proceed
|
|
46
|
+
* For web: defaults to checking if document is not hidden
|
|
47
|
+
* For native: defaults to always true
|
|
48
|
+
*/
|
|
49
|
+
onBeforeAutoRefresh?: () => boolean;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Callback invoked after successful redirect with authentication data
|
|
53
|
+
*/
|
|
54
|
+
onRedirectCallback?: (params: RedirectParams) => void;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Callback invoked when a token refresh occurs
|
|
58
|
+
*/
|
|
59
|
+
onRefresh?: (response: OnRefreshResponse) => void;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Callback invoked when token refresh fails
|
|
63
|
+
*/
|
|
64
|
+
onRefreshFailure?: (params: { signIn: () => Promise<void> }) => void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface RedirectParams extends AuthenticationResponse {
|
|
68
|
+
state: Record<string, any> | null;
|
|
69
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface GetAuthorizationUrlOptions {
|
|
2
|
+
connectionId?: string;
|
|
3
|
+
context?: string;
|
|
4
|
+
organizationId?: string;
|
|
5
|
+
domainHint?: string;
|
|
6
|
+
loginHint?: string;
|
|
7
|
+
provider?: string;
|
|
8
|
+
redirectUri?: string;
|
|
9
|
+
state?: string;
|
|
10
|
+
screenHint?: "sign-up" | "sign-in";
|
|
11
|
+
invitationToken?: string;
|
|
12
|
+
passwordResetToken?: string;
|
|
13
|
+
codeChallenge?: string;
|
|
14
|
+
codeChallengeMethod?: string;
|
|
15
|
+
}
|