@witsauth/react-client 1.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/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # @witsauth/react-client
2
+
3
+ Official React client library for integrating Witsauth Single Sign-On (SSO) authentication or similar services into your React applications.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @witsauth/react-client
9
+ # or
10
+ yarn add @witsauth/react-client
11
+ # or
12
+ pnpm add @witsauth/react-client
13
+ ```
14
+
15
+ ## Features
16
+ - Complete OAuth2 Authorization Code Flow with PKCE.
17
+ - Hooks (`useAuth`) for accessing user session state securely.
18
+ - Built-in Axios interceptors for automatic Bearer token injection.
19
+ - Seamless integration with Witsauth Console.
20
+
21
+ ## Setup
22
+
23
+ Wrap your application with the `AuthProvider` and pass your configuration.
24
+
25
+ ```tsx
26
+ import { AuthProvider, AuthCallback } from '@witsauth/react-client';
27
+ import { BrowserRouter, Routes, Route } from 'react-router-dom';
28
+
29
+ const authConfig = {
30
+ clientId: 'your-client-id',
31
+ authorizationEndpoint: 'https://api.yourdomain.com/auth/authorize',
32
+ tokenEndpoint: 'https://api.yourdomain.com/auth/oauth/token',
33
+ redirectUri: window.location.origin + '/auth/callback',
34
+ scope: 'openid profile email'
35
+ };
36
+
37
+ function App() {
38
+ return (
39
+ <BrowserRouter>
40
+ <AuthProvider config={authConfig}>
41
+ <Routes>
42
+ <Route path="/" element={<Home />} />
43
+ {/* Ensure you map a route for the OAuth callback */}
44
+ <Route path="/auth/callback" element={<AuthCallback />} />
45
+ </Routes>
46
+ </AuthProvider>
47
+ </BrowserRouter>
48
+ );
49
+ }
50
+ ```
51
+
52
+ ## Usage
53
+
54
+ Use the `useAuth` hook to access user state and login/logout functions.
55
+
56
+ ```tsx
57
+ import { useAuth } from '@witsauth/react-client';
58
+
59
+ function Profile() {
60
+ const { isAuthenticated, login, logout, isLoading } = useAuth();
61
+
62
+ if (isLoading) return <div>Loading...</div>;
63
+
64
+ return (
65
+ <div>
66
+ {isAuthenticated ? (
67
+ <>
68
+ <p>Welcome!</p>
69
+ <button onClick={logout}>Logout</button>
70
+ </>
71
+ ) : (
72
+ <button onClick={login}>Login</button>
73
+ )}
74
+ </div>
75
+ );
76
+ }
77
+ ```
78
+
79
+ ## API Interceptors
80
+
81
+ To automatically attach the Bearer token to your secure API requests, use the `setupAxiosInterceptors` helper.
82
+
83
+ ```tsx
84
+ import axios from 'axios';
85
+ import { setupAxiosInterceptors } from '@witsauth/react-client';
86
+
87
+ export const apiClient = axios.create({
88
+ baseURL: 'https://api.yourdomain.com'
89
+ });
90
+
91
+ // The interceptor will automatically inject Authorization: Bearer <token>
92
+ // Note: You must pass the AuthClient instance, for example from useAuth().client
93
+ // setupAxiosInterceptors(apiClient, authClient);
94
+ ```
@@ -0,0 +1,7 @@
1
+ import { AxiosInstance } from 'axios';
2
+ import { AuthClient } from '../core/AuthClient';
3
+ /**
4
+ * Attaches a Request Interceptor to the provided Axios instance to inject the Bearer token,
5
+ * and a Response Interceptor to automatically handle 401s by refreshing the token.
6
+ */
7
+ export declare function setupAxiosInterceptors(axiosInstance: AxiosInstance, authClient: AuthClient): void;
@@ -0,0 +1,14 @@
1
+ import { OAuth2Config, TokenInfo } from '../models/types';
2
+ import { TokenStorage } from './storage';
3
+ export declare class AuthClient {
4
+ private config;
5
+ private storage;
6
+ constructor(config: OAuth2Config, storage?: TokenStorage);
7
+ startAuthorization(): Promise<void>;
8
+ exchangeCodeForTokens(code: string, state: string): Promise<TokenInfo>;
9
+ refreshAccessToken(): Promise<TokenInfo>;
10
+ private saveTokenInfo;
11
+ getAccessToken(): string | null;
12
+ isAuthenticated(): boolean;
13
+ logout(): void;
14
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Generates a cryptographically secure code verifier for PKCE
3
+ * RFC 7636 specifies it must be 43-128 characters long
4
+ */
5
+ export declare function generateCodeVerifier(): string;
6
+ /**
7
+ * Generates a state parameter for CSRF protection
8
+ */
9
+ export declare function generateState(): string;
10
+ /**
11
+ * Creates a SHA-256 code challenge from the code verifier
12
+ */
13
+ export declare function createCodeChallenge(verifier: string): Promise<string>;
@@ -0,0 +1,9 @@
1
+ export declare class TokenStorage {
2
+ private memoryStorage;
3
+ private getStorage;
4
+ setItem(key: string, value: string): void;
5
+ getItem(key: string): string | null;
6
+ removeItem(key: string): void;
7
+ clearTokens(): void;
8
+ clearTemporary(): void;
9
+ }
@@ -0,0 +1,7 @@
1
+ export * from './models/types';
2
+ export * from './core/pkce';
3
+ export * from './core/storage';
4
+ export * from './core/AuthClient';
5
+ export * from './react/AuthContext';
6
+ export * from './react/AuthCallback';
7
+ export * from './axios/interceptor';
@@ -0,0 +1,273 @@
1
+ import { createContext as e, useContext as t, useEffect as n, useMemo as r, useRef as i, useState as a } from "react";
2
+ import { Fragment as o, jsx as s } from "react/jsx-runtime";
3
+ //#region src/models/types.ts
4
+ var c = {
5
+ ACCESS_TOKEN: "witsauth_access_token",
6
+ REFRESH_TOKEN: "witsauth_refresh_token",
7
+ TOKEN_EXPIRES_AT: "witsauth_expires_at",
8
+ TOKEN_TYPE: "witsauth_token_type",
9
+ SCOPE: "witsauth_scope",
10
+ CODE_VERIFIER: "witsauth_code_verifier",
11
+ STATE: "witsauth_state",
12
+ NONCE: "witsauth_nonce"
13
+ };
14
+ //#endregion
15
+ //#region src/core/pkce.ts
16
+ function l() {
17
+ let e = /* @__PURE__ */ new Uint8Array(32);
18
+ return crypto.getRandomValues(e), btoa(String.fromCharCode(...e)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
19
+ }
20
+ function u() {
21
+ let e = /* @__PURE__ */ new Uint8Array(32);
22
+ return crypto.getRandomValues(e), btoa(String.fromCharCode(...e)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
23
+ }
24
+ async function d(e) {
25
+ let t = new TextEncoder().encode(e), n = await crypto.subtle.digest("SHA-256", t), r = new Uint8Array(n);
26
+ return btoa(String.fromCharCode(...r)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
27
+ }
28
+ //#endregion
29
+ //#region src/core/storage.ts
30
+ var f = class {
31
+ memoryStorage = /* @__PURE__ */ new Map();
32
+ getStorage() {
33
+ if (typeof window < "u" && window.localStorage) return window.localStorage;
34
+ let e = this.memoryStorage;
35
+ return {
36
+ getItem: (t) => e.has(t) ? e.get(t) : null,
37
+ setItem: (t, n) => {
38
+ e.set(t, n);
39
+ },
40
+ removeItem: (t) => {
41
+ e.delete(t);
42
+ },
43
+ clear: () => {
44
+ e.clear();
45
+ },
46
+ get length() {
47
+ return e.size;
48
+ },
49
+ key: (t) => Array.from(e.keys())[t] || null
50
+ };
51
+ }
52
+ setItem(e, t) {
53
+ this.getStorage().setItem(e, t);
54
+ }
55
+ getItem(e) {
56
+ return this.getStorage().getItem(e);
57
+ }
58
+ removeItem(e) {
59
+ this.getStorage().removeItem(e);
60
+ }
61
+ clearTokens() {
62
+ let e = this.getStorage();
63
+ e.removeItem(c.ACCESS_TOKEN), e.removeItem(c.REFRESH_TOKEN), e.removeItem(c.TOKEN_EXPIRES_AT), e.removeItem(c.TOKEN_TYPE), e.removeItem(c.SCOPE);
64
+ }
65
+ clearTemporary() {
66
+ let e = this.getStorage();
67
+ e.removeItem(c.CODE_VERIFIER), e.removeItem(c.STATE), e.removeItem(c.NONCE);
68
+ }
69
+ }, p = class {
70
+ config;
71
+ storage;
72
+ constructor(e, t) {
73
+ this.config = e, this.storage = t || new f();
74
+ }
75
+ async startAuthorization() {
76
+ let e = l(), t = await d(e), n = u(), r = u();
77
+ this.storage.setItem(c.CODE_VERIFIER, e), this.storage.setItem(c.STATE, n), this.storage.setItem(c.NONCE, r);
78
+ let i = new URLSearchParams({
79
+ client_id: this.config.clientId,
80
+ redirect_uri: this.config.redirectUri,
81
+ response_type: this.config.responseType || "code",
82
+ code_challenge_method: "S256",
83
+ code_challenge: t,
84
+ state: n,
85
+ nonce: r
86
+ });
87
+ this.config.scope && i.append("scope", this.config.scope), this.config.audience && i.append("audience", this.config.audience);
88
+ let a = `${this.config.authorizationEndpoint}?${i.toString()}`;
89
+ if (typeof window > "u" || !window.location) throw Error("startAuthorization requires a browser environment to redirect the user.");
90
+ window.location.href = a;
91
+ }
92
+ async exchangeCodeForTokens(e, t) {
93
+ let n = this.storage.getItem(c.STATE), r = this.storage.getItem(c.CODE_VERIFIER);
94
+ if (t !== n) throw Error("Invalid state parameter. Possible CSRF attack.");
95
+ if (!r) throw Error("Code verifier missing from storage.");
96
+ let i = new URLSearchParams({
97
+ grant_type: "authorization_code",
98
+ client_id: this.config.clientId,
99
+ redirect_uri: this.config.redirectUri,
100
+ code: e,
101
+ code_verifier: r
102
+ }), a = await fetch(this.config.tokenEndpoint, {
103
+ method: "POST",
104
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
105
+ body: i.toString()
106
+ });
107
+ if (!a.ok) throw Error("Failed to exchange code for tokens");
108
+ let o = await a.json(), s = {
109
+ accessToken: o.access_token,
110
+ refreshToken: o.refresh_token,
111
+ tokenType: o.token_type || "Bearer",
112
+ expiresAt: Date.now() + o.expires_in * 1e3,
113
+ scope: o.scope,
114
+ idToken: o.id_token
115
+ };
116
+ return this.saveTokenInfo(s), this.storage.clearTemporary(), s;
117
+ }
118
+ async refreshAccessToken() {
119
+ let e = this.storage.getItem(c.REFRESH_TOKEN);
120
+ if (!e) throw Error("No refresh token available");
121
+ let t = new URLSearchParams({
122
+ grant_type: "refresh_token",
123
+ client_id: this.config.clientId,
124
+ refresh_token: e
125
+ }), n = await fetch(this.config.tokenEndpoint, {
126
+ method: "POST",
127
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
128
+ body: t.toString()
129
+ });
130
+ if (!n.ok) throw this.storage.clearTokens(), Error("Failed to refresh token");
131
+ let r = await n.json(), i = {
132
+ accessToken: r.access_token,
133
+ refreshToken: r.refresh_token || e,
134
+ tokenType: r.token_type || "Bearer",
135
+ expiresAt: Date.now() + r.expires_in * 1e3,
136
+ scope: r.scope,
137
+ idToken: r.id_token
138
+ };
139
+ return this.saveTokenInfo(i), i;
140
+ }
141
+ saveTokenInfo(e) {
142
+ this.storage.setItem(c.ACCESS_TOKEN, e.accessToken), this.storage.setItem(c.TOKEN_EXPIRES_AT, e.expiresAt.toString()), this.storage.setItem(c.TOKEN_TYPE, e.tokenType), e.refreshToken && this.storage.setItem(c.REFRESH_TOKEN, e.refreshToken), e.scope && this.storage.setItem(c.SCOPE, e.scope);
143
+ }
144
+ getAccessToken() {
145
+ return this.storage.getItem(c.ACCESS_TOKEN);
146
+ }
147
+ isAuthenticated() {
148
+ let e = this.storage.getItem(c.TOKEN_EXPIRES_AT);
149
+ return e ? Date.now() < parseInt(e, 10) - 3e4 && !!this.getAccessToken() : !1;
150
+ }
151
+ logout() {
152
+ this.storage.clearTokens(), this.storage.clearTemporary();
153
+ }
154
+ }, m = e(null), h = ({ config: e, children: t }) => {
155
+ let [i, o] = a({
156
+ isAuthenticated: !1,
157
+ isLoading: !0
158
+ }), c = r(() => new p(e, new f()), [e]);
159
+ n(() => {
160
+ (async () => {
161
+ try {
162
+ if (c.isAuthenticated()) o({
163
+ isAuthenticated: !0,
164
+ isLoading: !1
165
+ });
166
+ else try {
167
+ await c.refreshAccessToken(), o({
168
+ isAuthenticated: !0,
169
+ isLoading: !1
170
+ });
171
+ } catch {
172
+ o({
173
+ isAuthenticated: !1,
174
+ isLoading: !1
175
+ });
176
+ }
177
+ } catch (e) {
178
+ o({
179
+ isAuthenticated: !1,
180
+ isLoading: !1,
181
+ error: e instanceof Error ? e.message : String(e)
182
+ });
183
+ }
184
+ })();
185
+ }, [c]);
186
+ let l = async () => {
187
+ o((e) => ({
188
+ ...e,
189
+ isLoading: !0
190
+ })), await c.startAuthorization();
191
+ }, u = () => {
192
+ c.logout(), o({
193
+ isAuthenticated: !1,
194
+ isLoading: !1
195
+ });
196
+ }, d = async (e, t) => {
197
+ o((e) => ({
198
+ ...e,
199
+ isLoading: !0
200
+ }));
201
+ try {
202
+ let n = await c.exchangeCodeForTokens(e, t);
203
+ return o({
204
+ isAuthenticated: !0,
205
+ isLoading: !1,
206
+ tokenInfo: n
207
+ }), n;
208
+ } catch (e) {
209
+ throw o({
210
+ isAuthenticated: !1,
211
+ isLoading: !1,
212
+ error: e instanceof Error ? e.message : String(e)
213
+ }), e;
214
+ }
215
+ }, h = {
216
+ ...i,
217
+ login: l,
218
+ logout: u,
219
+ handleCallback: d,
220
+ client: c
221
+ };
222
+ return /* @__PURE__ */ s(m.Provider, {
223
+ value: h,
224
+ children: t
225
+ });
226
+ }, g = () => {
227
+ let e = t(m);
228
+ if (!e) throw Error("useAuth must be used within an AuthProvider");
229
+ return e;
230
+ }, _ = ({ onSuccess: e, onError: t, loadingComponent: r = /* @__PURE__ */ s("div", { children: "Processing authentication..." }) }) => {
231
+ let { handleCallback: a } = g(), c = i(!1);
232
+ return n(() => {
233
+ c.current || (c.current = !0, (async () => {
234
+ let n = new URLSearchParams(window.location.search), r = new URLSearchParams(window.location.hash.substring(1)), i = n.get("code") || r.get("code"), o = n.get("state") || r.get("state"), s = n.get("error") || r.get("error");
235
+ if (s) {
236
+ let e = /* @__PURE__ */ Error(`OAuth Error: ${s}`);
237
+ t && t(e);
238
+ return;
239
+ }
240
+ if (i && o) try {
241
+ await a(i, o), e && e();
242
+ } catch (e) {
243
+ t && t(e instanceof Error ? e : Error(String(e)));
244
+ }
245
+ })());
246
+ }, [
247
+ a,
248
+ e,
249
+ t
250
+ ]), /* @__PURE__ */ s(o, { children: r });
251
+ };
252
+ //#endregion
253
+ //#region src/axios/interceptor.ts
254
+ function v(e, t) {
255
+ e.interceptors.request.use((e) => {
256
+ let n = t.getAccessToken();
257
+ return n && e.headers && (e.headers.Authorization = `Bearer ${n}`), e;
258
+ }, (e) => Promise.reject(e)), e.interceptors.response.use((e) => e, async (n) => {
259
+ let r = n.config;
260
+ if (n.response?.status === 401 && r && !r._retry) {
261
+ r._retry = !0;
262
+ try {
263
+ let n = await t.refreshAccessToken();
264
+ return r.headers && (r.headers.Authorization = `Bearer ${n.accessToken}`), e(r);
265
+ } catch (e) {
266
+ return t.logout(), Promise.reject(e);
267
+ }
268
+ }
269
+ return Promise.reject(n);
270
+ });
271
+ }
272
+ //#endregion
273
+ export { _ as AuthCallback, p as AuthClient, h as AuthProvider, c as STORAGE_KEYS, f as TokenStorage, d as createCodeChallenge, l as generateCodeVerifier, u as generateState, v as setupAxiosInterceptors, g as useAuth };
@@ -0,0 +1 @@
1
+ (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports,require("react"),require("react/jsx-runtime")):typeof define==`function`&&define.amd?define([`exports`,`react`,`react/jsx-runtime`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.WitsAuthReactClient={},e.React,e.react_jsx_runtime))})(this,function(e,t,n){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var r=Object.create,i=Object.defineProperty,a=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyNames,s=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty,l=(e,t,n,r)=>{if(t&&typeof t==`object`||typeof t==`function`)for(var s=o(t),l=0,u=s.length,d;l<u;l++)d=s[l],!c.call(e,d)&&d!==n&&i(e,d,{get:(e=>t[e]).bind(null,d),enumerable:!(r=a(t,d))||r.enumerable});return e};t=((e,t,n)=>(n=e==null?{}:r(s(e)),l(t||!e||!e.__esModule?i(n,`default`,{value:e,enumerable:!0}):n,e)))(t,1);var u={ACCESS_TOKEN:`witsauth_access_token`,REFRESH_TOKEN:`witsauth_refresh_token`,TOKEN_EXPIRES_AT:`witsauth_expires_at`,TOKEN_TYPE:`witsauth_token_type`,SCOPE:`witsauth_scope`,CODE_VERIFIER:`witsauth_code_verifier`,STATE:`witsauth_state`,NONCE:`witsauth_nonce`};function d(){let e=new Uint8Array(32);return crypto.getRandomValues(e),btoa(String.fromCharCode(...e)).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=/g,``)}function f(){let e=new Uint8Array(32);return crypto.getRandomValues(e),btoa(String.fromCharCode(...e)).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=/g,``)}async function p(e){let t=new TextEncoder().encode(e),n=await crypto.subtle.digest(`SHA-256`,t),r=new Uint8Array(n);return btoa(String.fromCharCode(...r)).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=/g,``)}var m=class{memoryStorage=new Map;getStorage(){if(typeof window<`u`&&window.localStorage)return window.localStorage;let e=this.memoryStorage;return{getItem:t=>e.has(t)?e.get(t):null,setItem:(t,n)=>{e.set(t,n)},removeItem:t=>{e.delete(t)},clear:()=>{e.clear()},get length(){return e.size},key:t=>Array.from(e.keys())[t]||null}}setItem(e,t){this.getStorage().setItem(e,t)}getItem(e){return this.getStorage().getItem(e)}removeItem(e){this.getStorage().removeItem(e)}clearTokens(){let e=this.getStorage();e.removeItem(u.ACCESS_TOKEN),e.removeItem(u.REFRESH_TOKEN),e.removeItem(u.TOKEN_EXPIRES_AT),e.removeItem(u.TOKEN_TYPE),e.removeItem(u.SCOPE)}clearTemporary(){let e=this.getStorage();e.removeItem(u.CODE_VERIFIER),e.removeItem(u.STATE),e.removeItem(u.NONCE)}},h=class{config;storage;constructor(e,t){this.config=e,this.storage=t||new m}async startAuthorization(){let e=d(),t=await p(e),n=f(),r=f();this.storage.setItem(u.CODE_VERIFIER,e),this.storage.setItem(u.STATE,n),this.storage.setItem(u.NONCE,r);let i=new URLSearchParams({client_id:this.config.clientId,redirect_uri:this.config.redirectUri,response_type:this.config.responseType||`code`,code_challenge_method:`S256`,code_challenge:t,state:n,nonce:r});this.config.scope&&i.append(`scope`,this.config.scope),this.config.audience&&i.append(`audience`,this.config.audience);let a=`${this.config.authorizationEndpoint}?${i.toString()}`;if(typeof window>`u`||!window.location)throw Error(`startAuthorization requires a browser environment to redirect the user.`);window.location.href=a}async exchangeCodeForTokens(e,t){let n=this.storage.getItem(u.STATE),r=this.storage.getItem(u.CODE_VERIFIER);if(t!==n)throw Error(`Invalid state parameter. Possible CSRF attack.`);if(!r)throw Error(`Code verifier missing from storage.`);let i=new URLSearchParams({grant_type:`authorization_code`,client_id:this.config.clientId,redirect_uri:this.config.redirectUri,code:e,code_verifier:r}),a=await fetch(this.config.tokenEndpoint,{method:`POST`,headers:{"Content-Type":`application/x-www-form-urlencoded`},body:i.toString()});if(!a.ok)throw Error(`Failed to exchange code for tokens`);let o=await a.json(),s={accessToken:o.access_token,refreshToken:o.refresh_token,tokenType:o.token_type||`Bearer`,expiresAt:Date.now()+o.expires_in*1e3,scope:o.scope,idToken:o.id_token};return this.saveTokenInfo(s),this.storage.clearTemporary(),s}async refreshAccessToken(){let e=this.storage.getItem(u.REFRESH_TOKEN);if(!e)throw Error(`No refresh token available`);let t=new URLSearchParams({grant_type:`refresh_token`,client_id:this.config.clientId,refresh_token:e}),n=await fetch(this.config.tokenEndpoint,{method:`POST`,headers:{"Content-Type":`application/x-www-form-urlencoded`},body:t.toString()});if(!n.ok)throw this.storage.clearTokens(),Error(`Failed to refresh token`);let r=await n.json(),i={accessToken:r.access_token,refreshToken:r.refresh_token||e,tokenType:r.token_type||`Bearer`,expiresAt:Date.now()+r.expires_in*1e3,scope:r.scope,idToken:r.id_token};return this.saveTokenInfo(i),i}saveTokenInfo(e){this.storage.setItem(u.ACCESS_TOKEN,e.accessToken),this.storage.setItem(u.TOKEN_EXPIRES_AT,e.expiresAt.toString()),this.storage.setItem(u.TOKEN_TYPE,e.tokenType),e.refreshToken&&this.storage.setItem(u.REFRESH_TOKEN,e.refreshToken),e.scope&&this.storage.setItem(u.SCOPE,e.scope)}getAccessToken(){return this.storage.getItem(u.ACCESS_TOKEN)}isAuthenticated(){let e=this.storage.getItem(u.TOKEN_EXPIRES_AT);return e?Date.now()<parseInt(e,10)-3e4&&!!this.getAccessToken():!1}logout(){this.storage.clearTokens(),this.storage.clearTemporary()}},g=(0,t.createContext)(null),_=({config:e,children:r})=>{let[i,a]=(0,t.useState)({isAuthenticated:!1,isLoading:!0}),o=(0,t.useMemo)(()=>new h(e,new m),[e]);(0,t.useEffect)(()=>{(async()=>{try{if(o.isAuthenticated())a({isAuthenticated:!0,isLoading:!1});else try{await o.refreshAccessToken(),a({isAuthenticated:!0,isLoading:!1})}catch{a({isAuthenticated:!1,isLoading:!1})}}catch(e){a({isAuthenticated:!1,isLoading:!1,error:e instanceof Error?e.message:String(e)})}})()},[o]);let s=async()=>{a(e=>({...e,isLoading:!0})),await o.startAuthorization()},c=()=>{o.logout(),a({isAuthenticated:!1,isLoading:!1})},l=async(e,t)=>{a(e=>({...e,isLoading:!0}));try{let n=await o.exchangeCodeForTokens(e,t);return a({isAuthenticated:!0,isLoading:!1,tokenInfo:n}),n}catch(e){throw a({isAuthenticated:!1,isLoading:!1,error:e instanceof Error?e.message:String(e)}),e}},u={...i,login:s,logout:c,handleCallback:l,client:o};return(0,n.jsx)(g.Provider,{value:u,children:r})},v=()=>{let e=(0,t.useContext)(g);if(!e)throw Error(`useAuth must be used within an AuthProvider`);return e},y=({onSuccess:e,onError:r,loadingComponent:i=(0,n.jsx)(`div`,{children:`Processing authentication...`})})=>{let{handleCallback:a}=v(),o=(0,t.useRef)(!1);return(0,t.useEffect)(()=>{o.current||(o.current=!0,(async()=>{let t=new URLSearchParams(window.location.search),n=new URLSearchParams(window.location.hash.substring(1)),i=t.get(`code`)||n.get(`code`),o=t.get(`state`)||n.get(`state`),s=t.get(`error`)||n.get(`error`);if(s){let e=Error(`OAuth Error: ${s}`);r&&r(e);return}if(i&&o)try{await a(i,o),e&&e()}catch(e){r&&r(e instanceof Error?e:Error(String(e)))}})())},[a,e,r]),(0,n.jsx)(n.Fragment,{children:i})};function b(e,t){e.interceptors.request.use(e=>{let n=t.getAccessToken();return n&&e.headers&&(e.headers.Authorization=`Bearer ${n}`),e},e=>Promise.reject(e)),e.interceptors.response.use(e=>e,async n=>{let r=n.config;if(n.response?.status===401&&r&&!r._retry){r._retry=!0;try{let n=await t.refreshAccessToken();return r.headers&&(r.headers.Authorization=`Bearer ${n.accessToken}`),e(r)}catch(e){return t.logout(),Promise.reject(e)}}return Promise.reject(n)})}e.AuthCallback=y,e.AuthClient=h,e.AuthProvider=_,e.STORAGE_KEYS=u,e.TokenStorage=m,e.createCodeChallenge=p,e.generateCodeVerifier=d,e.generateState=f,e.setupAxiosInterceptors=b,e.useAuth=v});
@@ -0,0 +1,43 @@
1
+ export interface OAuth2Config {
2
+ clientId: string;
3
+ authorizationEndpoint: string;
4
+ tokenEndpoint: string;
5
+ redirectUri: string;
6
+ userInfoEndpoint?: string;
7
+ scope?: string;
8
+ audience?: string;
9
+ responseType?: string;
10
+ autoRefresh?: boolean;
11
+ refreshThreshold?: number;
12
+ oAuthProvider?: 'witsauth' | string;
13
+ }
14
+ export interface TokenInfo {
15
+ accessToken: string;
16
+ refreshToken?: string;
17
+ tokenType: string;
18
+ expiresAt: number;
19
+ scope?: string;
20
+ idToken?: string;
21
+ }
22
+ export interface AuthState {
23
+ isAuthenticated: boolean;
24
+ isLoading: boolean;
25
+ tokenInfo?: TokenInfo;
26
+ error?: string;
27
+ }
28
+ export interface UserInfo {
29
+ sub?: string;
30
+ name?: string;
31
+ email?: string;
32
+ [key: string]: any;
33
+ }
34
+ export declare const STORAGE_KEYS: {
35
+ ACCESS_TOKEN: string;
36
+ REFRESH_TOKEN: string;
37
+ TOKEN_EXPIRES_AT: string;
38
+ TOKEN_TYPE: string;
39
+ SCOPE: string;
40
+ CODE_VERIFIER: string;
41
+ STATE: string;
42
+ NONCE: string;
43
+ };
@@ -0,0 +1,8 @@
1
+ import { default as React } from 'react';
2
+ interface AuthCallbackProps {
3
+ onSuccess?: () => void;
4
+ onError?: (error: Error) => void;
5
+ loadingComponent?: React.ReactNode;
6
+ }
7
+ export declare const AuthCallback: React.FC<AuthCallbackProps>;
8
+ export {};
@@ -0,0 +1,15 @@
1
+ import { default as React } from 'react';
2
+ import { OAuth2Config, AuthState, TokenInfo } from '../models/types';
3
+ import { AuthClient } from '../core/AuthClient';
4
+ interface AuthContextValue extends AuthState {
5
+ login: () => Promise<void>;
6
+ logout: () => void;
7
+ client: AuthClient;
8
+ handleCallback: (code: string, state: string) => Promise<TokenInfo>;
9
+ }
10
+ export declare const AuthProvider: React.FC<{
11
+ config: OAuth2Config;
12
+ children: React.ReactNode;
13
+ }>;
14
+ export declare const useAuth: () => AuthContextValue;
15
+ export {};
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@witsauth/react-client",
3
+ "version": "1.0.0",
4
+ "description": "Official React client library for integrating Witsauth Single Sign-On (SSO) authentication or similar services into your React applications.",
5
+ "main": "./dist/index.umd.js",
6
+ "module": "./dist/index.es.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.es.js",
11
+ "require": "./dist/index.umd.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "keywords": [
19
+ "react",
20
+ "auth",
21
+ "oauth2",
22
+ "pkce"
23
+ ],
24
+ "author": "",
25
+ "license": "ISC",
26
+ "devEngines": {
27
+ "packageManager": {
28
+ "name": "pnpm",
29
+ "version": "^11.15.1",
30
+ "onFail": "download"
31
+ }
32
+ },
33
+ "type": "module",
34
+ "peerDependencies": {
35
+ "axios": "^1.18.1",
36
+ "react": "^18.0.0 || ^19.0.0",
37
+ "react-dom": "^18.0.0 || ^19.0.0"
38
+ },
39
+ "devDependencies": {
40
+ "@testing-library/jest-dom": "^7.0.0",
41
+ "@testing-library/react": "^16.3.2",
42
+ "@types/node": "^26.1.1",
43
+ "@types/react": "^19.2.17",
44
+ "@types/react-dom": "^19.2.3",
45
+ "@typescript/typescript6": "^6.0.2",
46
+ "jsdom": "^29.1.1",
47
+ "typescript": "^7.0.2",
48
+ "vite": "^8.1.5",
49
+ "vite-plugin-dts": "^5.0.3",
50
+ "vitest": "^4.1.10"
51
+ },
52
+ "scripts": {
53
+ "dev": "vite",
54
+ "build": "tsc && vite build",
55
+ "test": "vitest run"
56
+ }
57
+ }