diff --git a/.changeset/smart-spiders-film.md b/.changeset/smart-spiders-film.md new file mode 100644 index 00000000..505389c3 --- /dev/null +++ b/.changeset/smart-spiders-film.md @@ -0,0 +1,7 @@ +--- +'@asgardeo/browser': minor +'@asgardeo/javascript': minor +'@asgardeo/react': minor +--- + +Support `SignIn` component in `@asgardeo/react`. diff --git a/README.md b/README.md index ed4c90e1..6a142f47 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ This repository contains the source code of JavaScript SDKs that can be used to | [![@asgardeo/browser](https://img.shields.io/npm/v/@asgardeo/browser?color=%234B32C3&label=%40asgardeo%2Fbrowser&logo=firefox)](./packages/browser/) | Browser-based JavaScript SDK | | [![@asgardeo/nextjs](https://img.shields.io/npm/v/@asgardeo/nextjs?color=%23000000&label=%40asgardeo%2Fnext&logo=next.js)](./packages/next/) | Next.js SDK for building applications with Asgardeo | | [![@asgardeo/node](https://img.shields.io/npm/v/@asgardeo/node?color=%23339933&label=%40asgardeo%2Fnode&logo=node.js)](./packages/node/) | Node.js SDK for server-side integration | +| [![@asgardeo/express](https://img.shields.io/npm/v/@asgardeo/express?color=%23339933&label=%40asgardeo%2Fexpress&logo=express)](./packages/express/) | Express.js SDK for server-side integration | | [![@asgardeo/nuxt](https://img.shields.io/npm/v/@asgardeo/nuxt?color=%2300DC82&label=%40asgardeo%2Fnuxt&logo=nuxt)](./packages/nuxt/) | Nuxt.js SDK for building applications with Asgardeo | | [![@asgardeo/react](https://img.shields.io/npm/v/@asgardeo/react?color=%2361DAFB&label=%40asgardeo%2Freact&logo=react)](./packages/react/) | React SDK for building applications with Asgardeo | | [![@asgardeo/vue](https://img.shields.io/npm/v/@asgardeo/vue?color=%234FC08D&label=%40asgardeo%2Fvue&logo=vue.js)](./packages/vue/) | Vue.js SDK for building applications with Asgardeo | diff --git a/docs/src/react/components/asgardeo-provider.md b/docs/src/react/components/asgardeo-provider.md index 40c4dd88..c21ecf8d 100644 --- a/docs/src/react/components/asgardeo-provider.md +++ b/docs/src/react/components/asgardeo-provider.md @@ -38,9 +38,9 @@ import { AsgardeoProvider } from "@asgardeo/react"; - `baseURL`: The base URL of asgardeo. - - `signInRedirectURL`: The URL where users should be redirected after they sign in. This should be a page in your application. + - `afterSignInUrl`: The URL where users should be redirected after they sign in. This should be a page in your application. - - `clientID`: The ID of your application. You get this when you register your application with Asgardeo. + - `clientId`: The ID of your application. You get this when you register your application with Asgardeo. - `scope`: The scope of the access request. This is a list of scopes separated by spaces. Scopes allow your application to request access only to the resources it needs, and also allow users to control how much access they grant to your application. diff --git a/docs/src/react/hooks/use-authentication.md b/docs/src/react/hooks/use-authentication.md index 9b28b64c..1d13626c 100644 --- a/docs/src/react/hooks/use-authentication.md +++ b/docs/src/react/hooks/use-authentication.md @@ -31,7 +31,7 @@ import { useAuthentication } from "@asgardeo/react"; Then, you can use it in your component: ```ts -const { user, isAuthenticated, accessToken, signOut } = useAuthentication(); +const { user, isSignedIn, accessToken, signOut } = useAuthentication(); ``` ## Return Value @@ -40,7 +40,7 @@ The `useAuthentication` hook returns an object with the following properties: - `user`: The current user. This is an object that contains information about the user. -- `isAuthenticated`: A boolean that indicates whether the user is authenticated. +- `isSignedIn`: A boolean that indicates whether the user is authenticated. - `accessToken`: The access token for the authenticated user. @@ -49,9 +49,9 @@ The `useAuthentication` hook returns an object with the following properties: Here's an example of how to use these values: ```ts -const { user, isAuthenticated, signOut } = useAuthentication(); +const { user, isSignedIn, signOut } = useAuthentication(); -if (isAuthenticated) { +if (isSignedIn) { console.log(`User ${user.username} is authenticated.`); } else { console.log("User is not authenticated."); diff --git a/docs/src/react/introduction.md b/docs/src/react/introduction.md index 77775466..d6c98377 100644 --- a/docs/src/react/introduction.md +++ b/docs/src/react/introduction.md @@ -71,9 +71,9 @@ Then, wrap your application with the `AsgardeoProvider` and pass the basic confi diff --git a/package.json b/package.json index 2d08546b..e73152eb 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,13 @@ "description": "Workspace to hold the Asgardeo JavaScript SDKs.", "author": "WSO2", "license": "Apache-2.0", - "homepage": "https://github.com/asgardeo/javascript#readme", + "homepage": "https://github.com/asgardeo/web-ui-sdks#readme", "bugs": { - "url": "https://github.com/asgardeo/javascript/issues" + "url": "https://github.com/asgardeo/web-ui-sdks/issues" }, "repository": { "type": "git", - "url": "https://github.com/asgardeo/javascript" + "url": "https://github.com/asgardeo/web-ui-sdks" }, "keywords": [ "asgardeo", @@ -24,6 +24,7 @@ "lint": "nx run-many --target=lint --all --parallel", "publish:packages": "changeset publish", "test": "nx run-many --target=test --all --parallel", + "typecheck": "nx run-many --target=typecheck --all --parallel", "version:packages": "changeset version && pnpm install --lockfile-only" }, "devDependencies": { @@ -39,4 +40,4 @@ "publishConfig": { "access": "restricted" } -} \ No newline at end of file +} diff --git a/packages/__legacy__/core/src/api/authenticate.ts b/packages/__legacy__/core/src/api/authenticate.ts index 7d6f0c2c..1fc25e6b 100644 --- a/packages/__legacy__/core/src/api/authenticate.ts +++ b/packages/__legacy__/core/src/api/authenticate.ts @@ -44,7 +44,7 @@ const authenticate = async (props: AuthenticateProps): Promise }; /* Getting baseURL from authClient's data layer */ - const {baseUrl} = await AuthClient.getInstance().getDataLayer().getConfigData(); + const {baseUrl} = await AuthClient.getInstance().getStorageManager().getConfigData(); authnRequest = new Request(`${baseUrl}/oauth2/authn`, requestOptions); } catch (error) { diff --git a/packages/__legacy__/core/src/api/authorize.ts b/packages/__legacy__/core/src/api/authorize.ts index 69b67783..777a2113 100644 --- a/packages/__legacy__/core/src/api/authorize.ts +++ b/packages/__legacy__/core/src/api/authorize.ts @@ -32,7 +32,9 @@ const authorize = async (): Promise => { try { const authInstace: UIAuthClient = AuthClient.getInstance(); - const params: Map = await authInstace.getAuthorizationURLParams(); + // FIXME: We should be able to get the URL itself. + // const params: Map = await authInstace.getAuthorizationURLParams(); + const params: Map = new Map(); const formBody: URLSearchParams = new URLSearchParams(); @@ -41,7 +43,7 @@ const authorize = async (): Promise => { }); /* Save the state temporarily in the data layer, this needs to be passed when token is requested */ - await authInstace.getDataLayer().setTemporaryDataParameter('state', params.get('state')); + await authInstace.getStorageManager().setTemporaryDataParameter('state', params.get('state')); const headers: Headers = new Headers(); headers.append('Accept', 'application/json'); @@ -53,7 +55,7 @@ const authorize = async (): Promise => { method: 'POST', }; - authzURL = (await authInstace.getOIDCServiceEndpoints()).authorizationEndpoint; + authzURL = (await authInstace.getOpenIDProviderEndpoints()).authorizationEndpoint; } catch (error) { throw new AsgardeoUIException('JS_UI_CORE-AUTHZ-A-NF', 'Authorization request building failed', error.stack); } diff --git a/packages/__legacy__/core/src/api/get-branding-preference-text.ts b/packages/__legacy__/core/src/api/get-branding-preference-text.ts index ef5031df..87bd3fb2 100644 --- a/packages/__legacy__/core/src/api/get-branding-preference-text.ts +++ b/packages/__legacy__/core/src/api/get-branding-preference-text.ts @@ -50,7 +50,7 @@ const getBrandingPreferenceText = async ( params.append('screen', screen); params.append('type', type); - const {baseUrl} = await AuthClient.getInstance().getDataLayer().getConfigData(); + const {baseUrl} = await AuthClient.getInstance().getStorageManager().getConfigData(); const textUrl: string = `${baseUrl}/api/server/v1/branding-preference/text/resolve`; const urlWithParams: string = `${textUrl}?${params.toString()}`; let response: Response; diff --git a/packages/__legacy__/core/src/api/get-branding-preference.ts b/packages/__legacy__/core/src/api/get-branding-preference.ts index 4167ffaf..64c63f08 100644 --- a/packages/__legacy__/core/src/api/get-branding-preference.ts +++ b/packages/__legacy__/core/src/api/get-branding-preference.ts @@ -31,7 +31,7 @@ const getBrandingPreference = async (): Promise = baseUrl, type = BrandingPreferenceTypes.Org, name = 'WSO2', - } = await AuthClient.getInstance().getDataLayer().getConfigData(); + } = await AuthClient.getInstance().getStorageManager().getConfigData(); const brandingUrl: string = `${baseUrl}/api/server/v1/branding-preference?type=${type}&name=${name}`; let response: Response; diff --git a/packages/__legacy__/core/src/api/get-profile-information.ts b/packages/__legacy__/core/src/api/get-profile-information.ts index 3ec8e906..7b5faed0 100644 --- a/packages/__legacy__/core/src/api/get-profile-information.ts +++ b/packages/__legacy__/core/src/api/get-profile-information.ts @@ -35,7 +35,7 @@ const getProfileInformation = async (): Promise => { let response: Response; try { - baseUrl = (await AuthClient.getInstance().getDataLayer().getConfigData()).baseUrl; + baseUrl = (await AuthClient.getInstance().getStorageManager().getConfigData()).baseUrl; accessToken = await AuthClient.getInstance().getAccessToken(); } catch (error) { throw new AsgardeoUIException( diff --git a/packages/__legacy__/core/src/api/sign-out.ts b/packages/__legacy__/core/src/api/sign-out.ts index ad6416a0..90f54c57 100644 --- a/packages/__legacy__/core/src/api/sign-out.ts +++ b/packages/__legacy__/core/src/api/sign-out.ts @@ -16,7 +16,7 @@ * under the License. */ -import {AuthClient, ResponseMode} from '../auth-client'; +import {AuthClient} from '../auth-client'; import AsgardeoUIException from '../exception'; import {UIAuthClient} from '../models/auth-config'; @@ -49,9 +49,9 @@ const signOut = async (): Promise => { const authClient: UIAuthClient = AuthClient.getInstance(); try { - formBody.append('id_token_hint', await authClient.getIDToken()); - formBody.append('client_id', (await authClient.getDataLayer().getConfigData()).clientID); - formBody.append('response_mode', ResponseMode.Direct); + formBody.append('id_token_hint', await authClient.getIdToken()); + formBody.append('client_id', (await authClient.getStorageManager().getConfigData()).clientId); + formBody.append('response_mode', 'direct'); } catch (error) { throw new AsgardeoUIException('JS_UI_CORE-SIGNOUT-SO-IV', 'Failed to build the body of the signout request.'); } @@ -63,7 +63,7 @@ const signOut = async (): Promise => { }; try { - const {endSessionEndpoint} = await authClient.getOIDCServiceEndpoints(); + const {endSessionEndpoint} = await authClient.getOpenIDProviderEndpoints(); signOutUrl = endSessionEndpoint; } catch (error) { throw new AsgardeoUIException('JS_UI_CORE-SIGNOUT-SO-NF', 'Failed to retrieve the sign out endpoint.'); diff --git a/packages/__legacy__/core/src/auth-client.ts b/packages/__legacy__/core/src/auth-client.ts index 51e67bca..255ae4b7 100644 --- a/packages/__legacy__/core/src/auth-client.ts +++ b/packages/__legacy__/core/src/auth-client.ts @@ -16,7 +16,7 @@ * under the License. */ -import {AsgardeoAuthClient, Store, Crypto, ResponseMode} from '@asgardeo/auth-js'; +import {AsgardeoAuthClient, Store, Crypto} from '@asgardeo/auth-js'; import {UIAuthClient, UIAuthConfig} from './models/auth-config'; import {BrandingPreferenceTypes} from './models/branding-api-response'; @@ -50,7 +50,7 @@ export class AuthClient { enableConsoleBranding: authClientConfig?.enableConsoleBranding ?? true, enableConsoleTextBranding: authClientConfig?.enableConsoleTextBranding ?? true, name: authClientConfig?.name ?? DEFAULT_NAME, - responseMode: ResponseMode.Direct, + responseMode: 'direct', type: authClientConfig?.type ?? BrandingPreferenceTypes.Org, }; @@ -63,4 +63,4 @@ export class AuthClient { } /* Interfaces, classes and enums required from the auth-js package */ -export {Crypto, JWKInterface, Store, AsgardeoAuthClient, TokenResponse, ResponseMode} from '@asgardeo/auth-js'; +export {Crypto, JWKInterface, Store, AsgardeoAuthClient, TokenResponse} from '@asgardeo/auth-js'; diff --git a/packages/__legacy__/core/src/branding/get-branding.ts b/packages/__legacy__/core/src/branding/get-branding.ts index 5fd57ac8..0320b520 100644 --- a/packages/__legacy__/core/src/branding/get-branding.ts +++ b/packages/__legacy__/core/src/branding/get-branding.ts @@ -42,7 +42,7 @@ const getBranding = async (props: GetBrandingProps): Promise => { let brandingFromConsole: BrandingPreferenceAPIResponse; try { - const {enableConsoleBranding} = await AuthClient.getInstance().getDataLayer().getConfigData(); + const {enableConsoleBranding} = await AuthClient.getInstance().getStorageManager().getConfigData(); if (enableConsoleBranding) { brandingFromConsole = await getBrandingPreference(); diff --git a/packages/__legacy__/core/src/i18n/get-localization.ts b/packages/__legacy__/core/src/i18n/get-localization.ts index 400eacff..b0f453d9 100644 --- a/packages/__legacy__/core/src/i18n/get-localization.ts +++ b/packages/__legacy__/core/src/i18n/get-localization.ts @@ -38,7 +38,7 @@ const getLocalization = async (props: GetLocalizationProps): Promise let textFromConsoleBranding: BrandingPreferenceTextAPIResponse; - const configData: AuthClientConfig = await AuthClient.getInstance().getDataLayer().getConfigData(); + const configData: AuthClientConfig = await AuthClient.getInstance().getStorageManager().getConfigData(); try { if (configData.enableConsoleTextBranding) { diff --git a/packages/__legacy__/react/src/components/SignIn/SignIn.tsx b/packages/__legacy__/react/src/components/SignIn/SignIn.tsx index 518b1b25..9500ccaf 100644 --- a/packages/__legacy__/react/src/components/SignIn/SignIn.tsx +++ b/packages/__legacy__/react/src/components/SignIn/SignIn.tsx @@ -78,7 +78,7 @@ const SignIn: FC = (props: SignInProps): ReactElement => { const [showSelfSignUp, setShowSelfSignUp] = useState(showSignUp); const [componentBranding, setComponentBranding] = useState(); - const {isAuthenticated} = useAuthentication(); + const {isSignedIn} = useAuthentication(); const {config} = useConfig(); const authContext: AuthContext | undefined = useContext(AsgardeoContext); @@ -159,7 +159,7 @@ const SignIn: FC = (props: SignInProps): ReactElement => { /** * Check the origin of the message to ensure it's from the popup window */ - if (event.origin !== config.signInRedirectURL) return; + if (event.origin !== config.afterSignInUrl) return; const {code, state} = event.data; @@ -182,7 +182,7 @@ const SignIn: FC = (props: SignInProps): ReactElement => { authContext?.setAuthResponse(resp); const authInstance: UIAuthClient = AuthClient.getInstance(); - const state: string = (await authInstance.getDataLayer().getTemporaryDataParameter('state')).toString(); + const state: string = (await authInstance.getStorageManager().getTemporaryDataParameter('state')).toString(); await authInstance.requestAccessToken(resp.authData.code, resp.authData.session_state, state); @@ -368,7 +368,7 @@ const SignIn: FC = (props: SignInProps): ReactElement => { {showLogo && !(isLoading || authContext?.isComponentLoading) && ( )} - {authContext?.authResponse?.flowStatus !== FlowStatus.SuccessCompleted && !isAuthenticated && ( + {authContext?.authResponse?.flowStatus !== FlowStatus.SuccessCompleted && !isSignedIn && ( <> {renderSignIn()} @@ -397,7 +397,7 @@ const SignIn: FC = (props: SignInProps): ReactElement => { )} )} - {(authContext?.authResponse?.flowStatus === FlowStatus.SuccessCompleted || isAuthenticated) && ( + {(authContext?.authResponse?.flowStatus === FlowStatus.SuccessCompleted || isSignedIn) && (
Successfully Authenticated
)} diff --git a/packages/__legacy__/react/src/components/SignedIn/SignedIn.tsx b/packages/__legacy__/react/src/components/SignedIn/SignedIn.tsx index 8f719714..58dabe2a 100644 --- a/packages/__legacy__/react/src/components/SignedIn/SignedIn.tsx +++ b/packages/__legacy__/react/src/components/SignedIn/SignedIn.tsx @@ -30,9 +30,9 @@ import SignedProps from '../../models/signed-props'; */ const SignedIn: FC> = (props: PropsWithChildren) => { const {fallback = null, children} = props; - const {isAuthenticated} = useAuthentication(); + const {isSignedIn} = useAuthentication(); - return isAuthenticated ? children : fallback; + return isSignedIn ? children : fallback; }; export default SignedIn; diff --git a/packages/__legacy__/react/src/components/SignedOut/SignedOut.tsx b/packages/__legacy__/react/src/components/SignedOut/SignedOut.tsx index b6aa0c27..1e5825a4 100644 --- a/packages/__legacy__/react/src/components/SignedOut/SignedOut.tsx +++ b/packages/__legacy__/react/src/components/SignedOut/SignedOut.tsx @@ -30,9 +30,9 @@ import SignedProps from '../../models/signed-props'; */ const SignedOut: FC> = (props: PropsWithChildren) => { const {fallback = null, children} = props; - const {isAuthenticated} = useAuthentication(); + const {isSignedIn} = useAuthentication(); - return !isAuthenticated ? children : fallback; + return !isSignedIn ? children : fallback; }; export default SignedOut; diff --git a/packages/__legacy__/react/src/hooks/use-authentication.ts b/packages/__legacy__/react/src/hooks/use-authentication.ts index 43802cfa..08e75395 100644 --- a/packages/__legacy__/react/src/hooks/use-authentication.ts +++ b/packages/__legacy__/react/src/hooks/use-authentication.ts @@ -26,13 +26,13 @@ import UseAuthentication from '../models/use-authentication'; * `useAuthentication` is a custom hook that provides access to the authentication context. * It returns an object containing the current user, the authentication status, the access token, and a sign out function. * - * @returns {UseAuthentication} An object containing the current user (`user`), the authentication status (`isAuthenticated`), + * @returns {UseAuthentication} An object containing the current user (`user`), the authentication status (`isSignedIn`), * the access token (`accessToken`), and a sign out function (`signOut`). */ const useAuthentication = (): UseAuthentication => { const contextValue: AuthContext = useContext(AsgardeoContext); - const {accessToken, authResponse, isAuthenticated, isGlobalLoading, setUsername, user, username} = contextValue; + const {accessToken, authResponse, isSignedIn, isGlobalLoading, setUsername, user, username} = contextValue; const signOut: () => void = () => { signOutApiCall().then(() => { @@ -46,7 +46,7 @@ const useAuthentication = (): UseAuthentication => { return { accessToken, authResponse, - isAuthenticated, + isSignedIn, isGlobalLoading, setUsername, signOut, diff --git a/packages/__legacy__/react/src/models/auth-context.ts b/packages/__legacy__/react/src/models/auth-context.ts index 8bebb966..4a1e1f93 100644 --- a/packages/__legacy__/react/src/models/auth-context.ts +++ b/packages/__legacy__/react/src/models/auth-context.ts @@ -23,7 +23,7 @@ interface AuthContext { authResponse: AuthApiResponse; config: UIAuthConfig; isAuthLoading: boolean; - isAuthenticated: boolean | undefined; + isSignedIn: boolean | undefined; isBrandingLoading: boolean; isComponentLoading: boolean; isGlobalLoading: boolean; diff --git a/packages/__legacy__/react/src/models/use-authentication.ts b/packages/__legacy__/react/src/models/use-authentication.ts index ac52bd89..d48692d8 100644 --- a/packages/__legacy__/react/src/models/use-authentication.ts +++ b/packages/__legacy__/react/src/models/use-authentication.ts @@ -21,7 +21,7 @@ import {AuthApiResponse, MeAPIResponse} from '@asgardeo/js'; interface UseAuthentication { accessToken: string; authResponse: AuthApiResponse; - isAuthenticated: Promise | boolean; + isSignedIn: Promise | boolean; isGlobalLoading: boolean; setUsername: (username: string) => void; signOut: () => void; diff --git a/packages/__legacy__/react/src/providers/AsgardeoProvider.tsx b/packages/__legacy__/react/src/providers/AsgardeoProvider.tsx index 0400cee9..971dcc98 100644 --- a/packages/__legacy__/react/src/providers/AsgardeoProvider.tsx +++ b/packages/__legacy__/react/src/providers/AsgardeoProvider.tsx @@ -53,7 +53,7 @@ const AsgardeoProvider: FC> = ( const {children, config, store, branding} = props; const [accessToken, setAccessToken] = useState(''); - const [isAuthenticated, setIsAuthenticated] = useState(); + const [isSignedIn, setIsAuthenticated] = useState(); const [user, setUser] = useState(); const [isBrandingLoading, setIsBrandingLoading] = useState(true); const [isTextLoading, setIsTextLoading] = useState(true); @@ -89,7 +89,7 @@ const AsgardeoProvider: FC> = ( * Sets the authentication status and access token. */ const setAuthentication: () => void = useCallback((): void => { - authClient.isAuthenticated().then((isAuth: boolean) => { + authClient.isSignedIn().then((isAuth: boolean) => { setIsAuthenticated(isAuth); if (isAuth) { @@ -124,10 +124,10 @@ const AsgardeoProvider: FC> = ( /** * Send the 'code' and 'state' to the parent window and close the current window (popup) */ - window.opener.postMessage({code, state}, config.signInRedirectURL); + window.opener.postMessage({code, state}, config.afterSignInUrl); window.close(); } - }, [config.signInRedirectURL, setAuthentication]); + }, [config.afterSignInUrl, setAuthentication]); const value: AuthContext = useMemo( () => ({ @@ -135,7 +135,7 @@ const AsgardeoProvider: FC> = ( authResponse, config, isAuthLoading, - isAuthenticated, + isSignedIn, isBrandingLoading, isComponentLoading, isGlobalLoading: isAuthLoading || isBrandingLoading || isComponentLoading || isTextLoading, @@ -158,7 +158,7 @@ const AsgardeoProvider: FC> = ( authResponse, config, isAuthLoading, - isAuthenticated, + isSignedIn, isBrandingLoading, isComponentLoading, isTextLoading, diff --git a/packages/__legacy__/react/src/utils/crypto-utils.ts b/packages/__legacy__/react/src/utils/crypto-utils.ts index 56675656..b351a9e3 100644 --- a/packages/__legacy__/react/src/utils/crypto-utils.ts +++ b/packages/__legacy__/react/src/utils/crypto-utils.ts @@ -70,7 +70,7 @@ export default class SPACryptoUtils implements Crypto { idToken: string, jwk: Partial, algorithms: string[], - clientID: string, + clientId: string, issuer: string, subject: string, clockTolerance?: number, @@ -78,7 +78,7 @@ export default class SPACryptoUtils implements Crypto { ): Promise { const jwtVerifyOptions: JwtVerifyOptions = { algorithms, - audience: clientID, + audience: clientId, clockTolerance, issuer, subject, diff --git a/packages/browser/README.md b/packages/browser/README.md index 5f334aeb..64f2dca7 100644 --- a/packages/browser/README.md +++ b/packages/browser/README.md @@ -28,8 +28,8 @@ import { AsgardeoAuthClient } from "@asgardeo/browser"; // Initialize the auth client const authClient = new AsgardeoAuthClient({ - signInRedirectURL: "https://localhost:3000", - clientID: "", + afterSignInUrl: "https://localhost:3000", + clientId: "", baseUrl: "https://api.asgardeo.io/t/" }); @@ -37,7 +37,7 @@ const authClient = new AsgardeoAuthClient({ authClient.signIn(); // Get user info after authentication -const userInfo = await authClient.getBasicUserInfo(); +const userInfo = await authClient.getUser(); // Sign out authClient.signOut(); diff --git a/packages/browser/package.json b/packages/browser/package.json index edb5091f..a75d8004 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,15 +1,15 @@ { "name": "@asgardeo/browser", - "version": "0.0.0", + "version": "0.0.1", "description": "Browser-specific implementation of Asgardeo JavaScript SDK.", "keywords": [ "asgardeo", "browser", "spa" ], - "homepage": "https://github.com/asgardeo/javascript/tree/main/packages/browser#readme", + "homepage": "https://github.com/asgardeo/web-ui-sdks/tree/main/packages/browser#readme", "bugs": { - "url": "https://github.com/asgardeo/javascript/issues" + "url": "https://github.com/asgardeo/web-ui-sdks/issues" }, "author": "WSO2", "license": "Apache-2.0", @@ -28,7 +28,7 @@ "types": "dist/index.d.ts", "repository": { "type": "git", - "url": "https://github.com/asgardeo/javascript", + "url": "https://github.com/asgardeo/web-ui-sdks", "directory": "packages/browser" }, "scripts": { @@ -73,4 +73,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/packages/browser/src/__legacy__/client.ts b/packages/browser/src/__legacy__/client.ts index 17a878d9..11190d60 100755 --- a/packages/browser/src/__legacy__/client.ts +++ b/packages/browser/src/__legacy__/client.ts @@ -20,15 +20,14 @@ import { AsgardeoAuthClient, AsgardeoAuthException, AuthClientConfig, - BasicUserInfo, IsomorphicCrypto, - CustomGrantConfig, - DataLayer, + TokenExchangeRequestConfig, + StorageManager, IdTokenPayload, - FetchResponse, OIDCEndpoints, + User, } from '@asgardeo/javascript'; -import WorkerFile from '../worker'; +// import WorkerFile from '../worker'; import {MainThreadClient, WebWorkerClient} from './clients'; import {Hooks, REFRESH_ACCESS_TOKEN_ERR0R} from './constants'; import {AuthenticationHelper, SPAHelper} from './helpers'; @@ -45,7 +44,7 @@ import { WebWorkerClientConfig, WebWorkerClientInterface, } from './models'; -import {Storage} from './models/storage'; +import {BrowserStorage} from './models/storage'; import {SPAUtils} from './utils'; /** @@ -54,11 +53,10 @@ import {SPAUtils} from './utils'; const DefaultConfig: Partial> = { autoLogoutOnTokenRefreshError: true, checkSessionInterval: 3, - clientHost: origin, enableOIDCSessionManagement: false, periodicTokenRefresh: false, sessionRefreshInterval: 300, - storage: Storage.SessionStorage, + storage: BrowserStorage.SessionStorage, }; /** @@ -70,12 +68,13 @@ const DefaultConfig: Partial> = { export class AsgardeoSPAClient { protected static _instances: Map = new Map(); protected _client: WebWorkerClientInterface | MainThreadClientInterface | undefined; - protected _storage: Storage | undefined; + protected _storage: BrowserStorage | undefined; protected _authHelper: typeof AuthenticationHelper = AuthenticationHelper; - protected _worker: new () => Worker = WorkerFile; + // protected _worker: new () => Worker = WorkerFile; + protected _worker = null; protected _initialized: boolean = false; protected _startedInitialize: boolean = false; - protected _onSignInCallback: (response: BasicUserInfo) => void = () => null; + protected _onSignInCallback: (response: User) => void = () => null; protected _onSignOutCallback: () => void = () => null; protected _onSignOutFailedCallback: (error: SignOutError) => void = () => null; protected _onEndUserSession: (response: any) => void = () => null; @@ -95,13 +94,13 @@ export class AsgardeoSPAClient { } } - public instantiateWorker(worker: new () => Worker) { - if (worker) { - this._worker = worker; - } else { - this._worker = WorkerFile; - } - } + // public instantiateWorker(worker: new () => Worker) { + // if (worker) { + // this._worker = worker; + // } else { + // this._worker = WorkerFile; + // } + // } /** * This method specifies if the `AsgardeoSPAClient` has been initialized or not. @@ -112,7 +111,7 @@ export class AsgardeoSPAClient { * * @private */ - private async _isInitialized(): Promise { + public async isInitialized(): Promise { if (!this._startedInitialize) { return false; } @@ -148,7 +147,7 @@ export class AsgardeoSPAClient { * @private */ private async _validateMethod(validateAuthentication: boolean = true): Promise { - if (!(await this._isInitialized())) { + if (!(await this.isInitialized())) { return Promise.reject( new AsgardeoAuthException( 'SPA-AUTH_CLIENT-VM-NF01', @@ -158,7 +157,7 @@ export class AsgardeoSPAClient { ); } - if (validateAuthentication && !(await this.isAuthenticated())) { + if (validateAuthentication && !(await this.isSignedIn())) { return Promise.reject( new AsgardeoAuthException( 'SPA-AUTH_CLIENT-VM-IV02', @@ -217,8 +216,8 @@ export class AsgardeoSPAClient { * @example * ``` * auth.initialize({ - * signInRedirectURL: "http://localhost:3000/sign-in", - * clientID: "client ID", + * afterSignInUrl: "http://localhost:3000/sign-in", + * clientId: "client ID", * baseUrl: "https://api.asgardeo.io" * }); * ``` @@ -235,16 +234,16 @@ export class AsgardeoSPAClient { authHelper?: typeof AuthenticationHelper, workerFile?: new () => Worker, ): Promise { - this._storage = (config.storage as Storage) ?? Storage.SessionStorage; + this._storage = (config.storage as BrowserStorage) ?? BrowserStorage.SessionStorage; this._initialized = false; this._startedInitialize = true; authHelper && this.instantiateAuthHelper(authHelper); - workerFile && this.instantiateWorker(workerFile); + // workerFile && this.instantiateWorker(workerFile); const _config = await this._client?.getConfigData(); - if (!(this._storage === Storage.WebWorker)) { + if (!(this._storage === BrowserStorage.WebWorker)) { const mainThreadClientConfig = config as AuthClientConfig; const defaultConfig = {...DefaultConfig} as Partial>; const mergedConfig: AuthClientConfig = { @@ -327,11 +326,11 @@ export class AsgardeoSPAClient { /** * This method returns a Promise that resolves with the basic user information obtained from the ID token. * - * @return {Promise} - A promise that resolves with the user information. + * @return {Promise} - A promise that resolves with the user information. * * @example * ``` - * auth.getBasicUserInfo().then((response) => { + * auth.getUser().then((response) => { * // console.log(response); * }).catch((error) => { * // console.error(error); @@ -344,10 +343,10 @@ export class AsgardeoSPAClient { * * @preserve */ - public async getBasicUserInfo(): Promise { + public async getUser(): Promise { await this._validateMethod(); - return this._client?.getBasicUserInfo(); + return this._client?.getUser(); } /** @@ -373,7 +372,7 @@ export class AsgardeoSPAClient { * @param {string} sessionState - The session state. (Optional) * @param {string} state - The state. (Optional) * - * @return {Promise} - A promise that resolves with the user information. + * @return {Promise} - A promise that resolves with the user information. * * @example * ``` @@ -394,8 +393,8 @@ export class AsgardeoSPAClient { tokenRequestConfig?: { params: Record; }, - ): Promise { - await this._isInitialized(); + ): Promise { + await this.isInitialized(); // Discontinues the execution of this method if `config.callOnlyOnRedirect` is true and the `signIn` method // is not being called on redirect. @@ -407,11 +406,9 @@ export class AsgardeoSPAClient { return this._client ?.signIn(config, authorizationCode, sessionState, state, tokenRequestConfig) - .then((response: BasicUserInfo) => { + .then((response: User) => { if (this._onSignInCallback) { - if (response.allowedScopes || response.displayName || response.email || response.username) { - this._onSignInCallback(response); - } + this._onSignInCallback(response); } return response; @@ -426,7 +423,7 @@ export class AsgardeoSPAClient { * If this method is to be called on page load and the `signIn` method is also to be called on page load, * then it is advisable to call this method after the `signIn` call. * - * @return {Promise} - A Promise that resolves with the user information after signing in + * @return {Promise} - A Promise that resolves with the user information after signing in * or with `false` if the user is not signed in. * * @example @@ -437,31 +434,21 @@ export class AsgardeoSPAClient { public async trySignInSilently( additionalParams?: Record, tokenRequestConfig?: {params: Record}, - ): Promise { - await this._isInitialized(); + ): Promise { + await this.isInitialized(); // checks if the `signIn` method has been called. if (SPAUtils.wasSignInCalled()) { return undefined; } - return this._client - ?.trySignInSilently(additionalParams, tokenRequestConfig) - .then((response: BasicUserInfo | boolean) => { - if (this._onSignInCallback && response) { - const basicUserInfo = response as BasicUserInfo; - if ( - basicUserInfo.allowedScopes || - basicUserInfo.displayName || - basicUserInfo.email || - basicUserInfo.username - ) { - this._onSignInCallback(basicUserInfo); - } - } + return this._client?.trySignInSilently(additionalParams, tokenRequestConfig).then((response: User | boolean) => { + if (this._onSignInCallback && response) { + this._onSignInCallback(response as User); + } - return response; - }); + return response; + }); } /** @@ -619,7 +606,7 @@ export class AsgardeoSPAClient { * * @preserve */ - public async requestCustomGrant(config: CustomGrantConfig): Promise | BasicUserInfo | undefined> { + public async exchangeToken(config: TokenExchangeRequestConfig): Promise { if (config.signInRequired) { await this._validateMethod(); } else { @@ -636,7 +623,7 @@ export class AsgardeoSPAClient { ); } - const customGrantResponse = await this._client?.requestCustomGrant(config); + const customGrantResponse = await this._client?.exchangeToken(config); const customGrantCallback = this._onCustomGrant.get(config.id); customGrantCallback && customGrantCallback(this._onCustomGrant?.get(config.id)); @@ -693,10 +680,10 @@ export class AsgardeoSPAClient { * * @preserve */ - public async getOIDCServiceEndpoints(): Promise { - await this._isInitialized(); + public async getOpenIDProviderEndpoints(): Promise { + await this.isInitialized(); - return this._client?.getOIDCServiceEndpoints(); + return this._client?.getOpenIDProviderEndpoints(); } /** @@ -710,7 +697,7 @@ export class AsgardeoSPAClient { */ public getHttpClient(): HttpClientInstance { if (this._client) { - if (this._storage !== Storage.WebWorker) { + if (this._storage !== BrowserStorage.WebWorker) { const mainThreadClient = this._client as MainThreadClientInterface; return mainThreadClient.getHttpClient(); } @@ -738,7 +725,7 @@ export class AsgardeoSPAClient { * * @example * ``` - * auth.getDecodedIDToken().then((response)=>{ + * auth.getDecodedIdToken().then((response)=>{ * // console.log(response); * }).catch((error)=>{ * // console.error(error); @@ -750,10 +737,10 @@ export class AsgardeoSPAClient { * * @preserve */ - public async getDecodedIDToken(): Promise { + public async getDecodedIdToken(): Promise { await this._validateMethod(); - return this._client?.getDecodedIDToken(); + return this._client?.getDecodedIdToken(); } /** @@ -764,22 +751,22 @@ export class AsgardeoSPAClient { * * @example * ``` - * auth.getCryptoHelper().then((response)=>{ + * auth.getCrypto().then((response)=>{ * // console.log(response); * }).catch((error)=>{ * // console.error(error); * }); * ``` - * @link https://github.com/asgardeo/asgardeo-auth-spa-sdk/tree/master#getCryptoHelper + * @link https://github.com/asgardeo/asgardeo-auth-spa-sdk/tree/master#getCrypto * * @memberof AsgardeoSPAClient * * @preserve */ - public async getCryptoHelper(): Promise { + public async getCrypto(): Promise { await this._validateMethod(); - return this._client?.getCryptoHelper(); + return this._client?.getCrypto(); } /** @@ -789,19 +776,19 @@ export class AsgardeoSPAClient { * * @example * ``` - * const idToken = await auth.getIDToken(); + * const idToken = await auth.getIdToken(); * ``` * - * @link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getIDToken + * @link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getIdToken * * @memberof AsgardeoAuthClient * * @preserve */ - public async getIDToken(): Promise { + public async getIdToken(): Promise { await this._validateMethod(); - return this._client?.getIDToken(); + return this._client?.getIdToken(); } /** @@ -829,7 +816,7 @@ export class AsgardeoSPAClient { public async getAccessToken(): Promise { await this._validateMethod(); - if (this._storage && [(Storage.WebWorker, Storage.BrowserMemory)].includes(this._storage)) { + if (this._storage && [(BrowserStorage.WebWorker, BrowserStorage.BrowserMemory)].includes(this._storage)) { return Promise.reject( new AsgardeoAuthException( 'SPA-AUTH_CLIENT-GAT-IV01', @@ -868,7 +855,7 @@ export class AsgardeoSPAClient { public async getIDPAccessToken(): Promise { await this._validateMethod(); - if (this._storage && [(Storage.WebWorker, Storage.BrowserMemory)].includes(this._storage)) { + if (this._storage && [(BrowserStorage.WebWorker, BrowserStorage.BrowserMemory)].includes(this._storage)) { return Promise.reject( new AsgardeoAuthException( 'SPA-AUTH_CLIENT-GIAT-IV01', @@ -891,7 +878,7 @@ export class AsgardeoSPAClient { * * @example * ``` - * auth.getDataLayer().then((dataLayer) => { + * auth.getStorageManager().then((dataLayer) => { * // console.log(dataLayer); * }).catch((error) => { * // console.error(error); @@ -904,10 +891,10 @@ export class AsgardeoSPAClient { * * @preserve */ - public async getDataLayer(): Promise> { + public async getStorageManager(): Promise> { await this._validateMethod(); - if (this._storage && [(Storage.WebWorker, Storage.BrowserMemory)].includes(this._storage)) { + if (this._storage && [(BrowserStorage.WebWorker, BrowserStorage.BrowserMemory)].includes(this._storage)) { return Promise.reject( new AsgardeoAuthException( 'SPA-AUTH_CLIENT-GDL-IV01', @@ -918,7 +905,7 @@ export class AsgardeoSPAClient { } const mainThreadClient = this._client as MainThreadClientInterface; - return mainThreadClient.getDataLayer(); + return mainThreadClient.getStorageManager(); } /** @@ -968,7 +955,7 @@ export class AsgardeoSPAClient { * * @preserve */ - public async refreshAccessToken(): Promise { + public async refreshAccessToken(): Promise { await this._validateMethod(false); return this._client?.refreshAccessToken(); @@ -983,10 +970,10 @@ export class AsgardeoSPAClient { * * @preserve */ - public async isAuthenticated(): Promise { - await this._isInitialized(); + public async isSignedIn(): Promise { + await this.isInitialized(); - return this._client?.isAuthenticated(); + return this._client?.isSignedIn(); } /** @@ -999,9 +986,9 @@ export class AsgardeoSPAClient { * @preserve */ public async isSessionActive(): Promise { - await this._isInitialized(); + await this.isInitialized(); - if (this._storage && [(Storage.WebWorker, Storage.BrowserMemory)].includes(this._storage)) { + if (this._storage && [(BrowserStorage.WebWorker, BrowserStorage.BrowserMemory)].includes(this._storage)) { return Promise.reject( new AsgardeoAuthException( 'SPA-AUTH_CLIENT-ISA-IV01', @@ -1038,7 +1025,7 @@ export class AsgardeoSPAClient { public async on(hook: Hooks.CustomGrant, callback: (response?: any) => void, id: string): Promise; public async on(hook: Exclude, callback: (response?: any) => void): Promise; public async on(hook: Hooks, callback: (response?: any) => void | Promise, id?: string): Promise { - await this._isInitialized(); + await this.isInitialized(); if (callback && typeof callback === 'function') { switch (hook) { case Hooks.SignIn: @@ -1109,7 +1096,7 @@ export class AsgardeoSPAClient { * @preserve */ public async enableHttpHandler(): Promise { - await this._isInitialized(); + await this.isInitialized(); return this._client?.enableHttpHandler(); } @@ -1131,7 +1118,7 @@ export class AsgardeoSPAClient { * @preserve */ public async disableHttpHandler(): Promise { - await this._isInitialized(); + await this.isInitialized(); return this._client?.disableHttpHandler(); } @@ -1144,26 +1131,26 @@ export class AsgardeoSPAClient { * @example * ``` * const config = { - * signInRedirectURL: "http://localhost:3000/sign-in", - * clientID: "client ID", + * afterSignInUrl: "http://localhost:3000/sign-in", + * clientId: "client ID", * baseUrl: "https://api.asgardeo.io" * } - * const auth.updateConfig(config); + * const auth.reInitialize(config); * ``` - * @link https://github.com/asgardeo/asgardeo-auth-spa-sdk/tree/master/lib#updateConfig + * @link https://github.com/asgardeo/asgardeo-auth-spa-sdk/tree/master/lib#reInitialize * * @memberof AsgardeoAuthClient * * @preserve */ - public async updateConfig(config: Partial>): Promise { - await this._isInitialized(); - if (this._storage === Storage.WebWorker) { + public async reInitialize(config: Partial>): Promise { + await this.isInitialized(); + if (this._storage === BrowserStorage.WebWorker) { const client = this._client as WebWorkerClientInterface; - await client.updateConfig(config as Partial>); + await client.reInitialize(config as Partial>); } else { const client = this._client as WebWorkerClientInterface; - await client.updateConfig(config as Partial>); + await client.reInitialize(config as Partial>); } return; diff --git a/packages/browser/src/__legacy__/clients/main-thread-client.ts b/packages/browser/src/__legacy__/clients/main-thread-client.ts index 639ac72a..0abfa445 100755 --- a/packages/browser/src/__legacy__/clients/main-thread-client.ts +++ b/packages/browser/src/__legacy__/clients/main-thread-client.ts @@ -19,36 +19,37 @@ import { AsgardeoAuthClient, AuthClientConfig, - BasicUserInfo, + User, IsomorphicCrypto, - DataLayer, + StorageManager, IdTokenPayload, - FetchResponse, - GetAuthURLConfig, + ExtendedAuthorizeRequestUrlParams, OIDCEndpoints, - ResponseMode, OIDCRequestConstants, SessionData, - Store, + Storage, extractPkceStorageKeyFromState, + initializeApplicationNativeAuthentication, + handleApplicationNativeAuthentication, + TemporaryStore, } from '@asgardeo/javascript'; import {SILENT_SIGN_IN_STATE, TOKEN_REQUEST_CONFIG_KEY} from '../constants'; import {AuthenticationHelper, SPAHelper, SessionManagementHelper} from '../helpers'; import {HttpClient, HttpClientInstance} from '../http-client'; import {HttpError, HttpRequestConfig, HttpResponse, MainThreadClientConfig, MainThreadClientInterface} from '../models'; import {SPACustomGrantConfig} from '../models/request-custom-grant'; -import {Storage} from '../models/storage'; +import {BrowserStorage} from '../models/storage'; import {LocalStore, MemoryStore, SessionStore} from '../stores'; import {SPAUtils} from '../utils'; import {SPACryptoUtils} from '../utils/crypto-utils'; -const initiateStore = (store: Storage | undefined): Store => { +const initiateStore = (store: BrowserStorage | undefined): Storage => { switch (store) { - case Storage.LocalStorage: + case BrowserStorage.LocalStorage: return new LocalStore(); - case Storage.SessionStorage: + case BrowserStorage.SessionStorage: return new SessionStore(); - case Storage.BrowserMemory: + case BrowserStorage.BrowserMemory: return new MemoryStore(); default: return new SessionStore(); @@ -63,18 +64,18 @@ export const MainThreadClient = async ( spaHelper: SPAHelper, ) => AuthenticationHelper, ): Promise => { - const _store: Store = initiateStore(config.storage as Storage); + const _store: Storage = initiateStore(config.storage as BrowserStorage); const _cryptoUtils: SPACryptoUtils = new SPACryptoUtils(); const _authenticationClient = new AsgardeoAuthClient(); await _authenticationClient.initialize(config, _store, _cryptoUtils, instanceID); const _spaHelper = new SPAHelper(_authenticationClient); - const _dataLayer = _authenticationClient.getDataLayer(); + const _dataLayer = _authenticationClient.getStorageManager(); const _sessionManagementHelper = await SessionManagementHelper( async () => { - return _authenticationClient.getSignOutURL(); + return _authenticationClient.getSignOutUrl(); }, - (config.storage as Storage) ?? Storage.SessionStorage, + (config.storage as BrowserStorage) ?? BrowserStorage.SessionStorage, (sessionState: string) => _dataLayer.setSessionDataParameter( OIDCRequestConstants.Params.SESSION_STATE as keyof SessionData, @@ -152,14 +153,14 @@ export const MainThreadClient = async ( }; const checkSession = async (): Promise => { - const oidcEndpoints: OIDCEndpoints = await _authenticationClient.getOIDCServiceEndpoints() as OIDCEndpoints; + const oidcEndpoints: OIDCEndpoints = (await _authenticationClient.getOpenIDProviderEndpoints()) as OIDCEndpoints; const config = await _dataLayer.getConfigData(); _authenticationHelper.initializeSessionManger( config, oidcEndpoints, - async () => (await _authenticationClient.getBasicUserInfo()).sessionState, - async (params?: GetAuthURLConfig): Promise => _authenticationClient.getAuthorizationURL(params), + async () => (await _authenticationClient.getUserSession()).sessionState, + async (params?: ExtendedAuthorizeRequestUrlParams): Promise => _authenticationClient.getSignInUrl(params), _sessionManagementHelper, ); }; @@ -179,14 +180,21 @@ export const MainThreadClient = async ( }; const signIn = async ( - signInConfig?: GetAuthURLConfig, + signInConfig?: ExtendedAuthorizeRequestUrlParams, authorizationCode?: string, sessionState?: string, state?: string, tokenRequestConfig?: { params: Record; }, - ): Promise => { + ): Promise => { + if (signInConfig['flow']) { + return handleApplicationNativeAuthentication({ + url: signInConfig['flow']['requestConfig']['url'], + payload: signInConfig['flow']['payload'], + }); + } + const basicUserInfo = await _authenticationHelper.handleSignIn(shouldStopAuthn, checkSession, undefined); if (basicUserInfo) { @@ -199,7 +207,7 @@ export const MainThreadClient = async ( params: Record; } = {params: {}}; - if (config?.responseMode === ResponseMode.FormPost && authorizationCode) { + if (config?.responseMode === 'form_post' && authorizationCode) { resolvedAuthorizationCode = authorizationCode; resolvedSessionState = sessionState ?? ''; resolvedState = state ?? ''; @@ -227,8 +235,8 @@ export const MainThreadClient = async ( ); } - return _authenticationClient.getAuthorizationURL(signInConfig).then(async (url: string) => { - if (config.storage === Storage.BrowserMemory && config.enablePKCE) { + return _authenticationClient.getSignInUrl(signInConfig).then(async (url: string) => { + if (config.storage === BrowserStorage.BrowserMemory && config.enablePKCE) { const pkceKey: string = extractPkceStorageKeyFromState(resolvedState); SPAUtils.setPKCE(pkceKey, (await _authenticationClient.getPKCECode(resolvedState)) as string); @@ -238,7 +246,16 @@ export const MainThreadClient = async ( _dataLayer.setTemporaryDataParameter(TOKEN_REQUEST_CONFIG_KEY, JSON.stringify(tokenRequestConfig)); } - location.href = url; + if (signInConfig['response_mode'] === 'direct') { + const authorizeUrl: URL = new URL(url); + + return initializeApplicationNativeAuthentication({ + url: `${authorizeUrl.origin}${authorizeUrl.pathname}`, + payload: Object.fromEntries(authorizeUrl.searchParams.entries()), + }); + } else { + location.href = url; + } await SPAUtils.waitTillPageRedirect(); @@ -256,10 +273,10 @@ export const MainThreadClient = async ( }; const signOut = async (): Promise => { - if ((await _authenticationClient.isAuthenticated()) && !_getSignOutURLFromSessionStorage) { - location.href = await _authenticationClient.getSignOutURL(); + if ((await _authenticationClient.isSignedIn()) && !_getSignOutURLFromSessionStorage) { + location.href = await _authenticationClient.getSignOutUrl(); } else { - location.href = SPAUtils.getSignOutURL(config.clientID, instanceID); + location.href = SPAUtils.getSignOutUrl(config.clientId, instanceID); } _spaHelper.clearRefreshTokenTimeout(); @@ -280,11 +297,11 @@ export const MainThreadClient = async ( } }; - const requestCustomGrant = async (config: SPACustomGrantConfig): Promise => { - return await _authenticationHelper.requestCustomGrant(config, enableRetrievingSignOutURLFromSession); + const exchangeToken = async (config: SPACustomGrantConfig): Promise => { + return await _authenticationHelper.exchangeToken(config, enableRetrievingSignOutURLFromSession); }; - const refreshAccessToken = async (): Promise => { + const refreshAccessToken = async (): Promise => { try { return await _authenticationHelper.refreshAccessToken(enableRetrievingSignOutURLFromSession); } catch (error) { @@ -313,7 +330,7 @@ export const MainThreadClient = async ( tokenRequestConfig?: { params: Record; }, - ): Promise => { + ): Promise => { return await _authenticationHelper.requestAccessToken( resolvedAuthorizationCode, resolvedSessionState, @@ -326,7 +343,7 @@ export const MainThreadClient = async ( const constructSilentSignInUrl = async (additionalParams: Record = {}): Promise => { const config = await _dataLayer.getConfigData(); - const urlString: string = await _authenticationClient.getAuthorizationURL({ + const urlString: string = await _authenticationClient.getSignInUrl({ prompt: 'none', state: SILENT_SIGN_IN_STATE, ...additionalParams, @@ -337,7 +354,7 @@ export const MainThreadClient = async ( urlObject.searchParams.set('response_mode', 'query'); const url: string = urlObject.toString(); - if (config.storage === Storage.BrowserMemory && config.enablePKCE) { + if (config.storage === BrowserStorage.BrowserMemory && config.enablePKCE) { const state = urlObject.searchParams.get(OIDCRequestConstants.Params.STATE); SPAUtils.setPKCE( @@ -353,13 +370,13 @@ export const MainThreadClient = async ( * This method checks if there is an active user session in the server by sending a prompt none request. * If the user is signed in, this method sends a token request. Returns false otherwise. * - * @return {Promise, tokenRequestConfig?: {params: Record}, - ): Promise => { + ): Promise => { return await _authenticationHelper.trySignInSilently( constructSilentSignInUrl, requestAccessToken, @@ -369,47 +386,47 @@ export const MainThreadClient = async ( ); }; - const getBasicUserInfo = async (): Promise => { - return _authenticationHelper.getBasicUserInfo(); + const getUser = async (): Promise => { + return _authenticationHelper.getUser(); }; - const getDecodedIDToken = async (): Promise => { - return _authenticationHelper.getDecodedIDToken(); + const getDecodedIdToken = async (): Promise => { + return _authenticationHelper.getDecodedIdToken(); }; - const getCryptoHelper = async (): Promise => { - return _authenticationHelper.getCryptoHelper(); + const getCrypto = async (): Promise => { + return _authenticationHelper.getCrypto(); }; - const getIDToken = async (): Promise => { - return _authenticationHelper.getIDToken(); + const getIdToken = async (): Promise => { + return _authenticationHelper.getIdToken(); }; - const getOIDCServiceEndpoints = async (): Promise => { - return _authenticationHelper.getOIDCServiceEndpoints(); + const getOpenIDProviderEndpoints = async (): Promise => { + return _authenticationHelper.getOpenIDProviderEndpoints(); }; const getAccessToken = async (): Promise => { return _authenticationHelper.getAccessToken(); }; - const getDataLayer = async (): Promise> => { - return _authenticationHelper.getDataLayer(); + const getStorageManager = async (): Promise> => { + return _authenticationHelper.getStorageManager(); }; const getConfigData = async (): Promise> => { return await _dataLayer.getConfigData(); }; - const isAuthenticated = async (): Promise => { - return _authenticationHelper.isAuthenticated(); + const isSignedIn = async (): Promise => { + return _authenticationHelper.isSignedIn(); }; const isSessionActive = async (): Promise => { return (await _dataLayer.getSessionStatus()) === 'true'; }; - const updateConfig = async (newConfig: Partial>): Promise => { + const reInitialize = async (newConfig: Partial>): Promise => { const existingConfig = await _dataLayer.getConfigData(); const isCheckSessionIframeDifferent: boolean = !( existingConfig && @@ -421,7 +438,7 @@ export const MainThreadClient = async ( existingConfig.endpoints.checkSessionIframe === newConfig.endpoints.checkSessionIframe ); const config = {...existingConfig, ...newConfig}; - await _authenticationClient.updateConfig(config); + await _authenticationClient.reInitialize(config); // Re-initiates check session if the check session endpoint is updated. if (config.enableOIDCSessionManagement && isCheckSessionIframeDifferent) { @@ -435,20 +452,20 @@ export const MainThreadClient = async ( disableHttpHandler, enableHttpHandler, getAccessToken, - getBasicUserInfo, + getUser, getConfigData, - getCryptoHelper, - getDataLayer, - getDecodedIDToken, + getCrypto, + getStorageManager, + getDecodedIdToken, getHttpClient, - getIDToken, - getOIDCServiceEndpoints, + getIdToken, + getOpenIDProviderEndpoints, httpRequest, httpRequestAll, - isAuthenticated, + isSignedIn, isSessionActive, refreshAccessToken, - requestCustomGrant, + exchangeToken, revokeAccessToken, setHttpRequestErrorCallback, setHttpRequestFinishCallback, @@ -457,6 +474,6 @@ export const MainThreadClient = async ( signIn, signOut, trySignInSilently, - updateConfig, + reInitialize, }; }; diff --git a/packages/browser/src/__legacy__/clients/web-worker-client.ts b/packages/browser/src/__legacy__/clients/web-worker-client.ts index 9a4ab493..84444cbd 100755 --- a/packages/browser/src/__legacy__/clients/web-worker-client.ts +++ b/packages/browser/src/__legacy__/clients/web-worker-client.ts @@ -20,16 +20,14 @@ import { AsgardeoAuthClient, AsgardeoAuthException, AuthClientConfig, - BasicUserInfo, + User, IsomorphicCrypto, - CustomGrantConfig, + TokenExchangeRequestConfig, IdTokenPayload, - FetchResponse, - GetAuthURLConfig, + ExtendedAuthorizeRequestUrlParams, OIDCEndpoints, - ResponseMode, OIDCRequestConstants, - Store, + Storage, extractPkceStorageKeyFromState, } from '@asgardeo/javascript'; import { @@ -75,18 +73,18 @@ import { WebWorkerClientInterface, } from '../models'; import {SPACustomGrantConfig} from '../models/request-custom-grant'; -import {Storage} from '../models/storage'; +import {BrowserStorage} from '../models/storage'; import {LocalStore, MemoryStore, SessionStore} from '../stores'; import {SPAUtils} from '../utils'; import {SPACryptoUtils} from '../utils/crypto-utils'; -const initiateStore = (store: Storage | undefined): Store => { +const initiateStore = (store: BrowserStorage | undefined): Storage => { switch (store) { - case Storage.LocalStorage: + case BrowserStorage.LocalStorage: return new LocalStore(); - case Storage.SessionStorage: + case BrowserStorage.SessionStorage: return new SessionStore(); - case Storage.BrowserMemory: + case BrowserStorage.BrowserMemory: return new MemoryStore(); default: return new SessionStore(); @@ -113,7 +111,7 @@ export const WebWorkerClient = async ( let _isHttpHandlerEnabled: boolean = true; let _getSignOutURLFromSessionStorage: boolean = false; - const _store: Store = initiateStore(config.storage as Storage); + const _store: Storage = initiateStore(config.storage as BrowserStorage); const _cryptoUtils: SPACryptoUtils = new SPACryptoUtils(); const _authenticationClient = new AsgardeoAuthClient(); await _authenticationClient.initialize(config, _store, _cryptoUtils, instanceID); @@ -130,10 +128,10 @@ export const WebWorkerClient = async ( return signOutURL; } catch { - return SPAUtils.getSignOutURL(config.clientID, instanceID); + return SPAUtils.getSignOutUrl(config.clientId, instanceID); } }, - config.storage as Storage, + config.storage as BrowserStorage, (sessionState: string) => setSessionState(sessionState), ); @@ -189,13 +187,13 @@ export const WebWorkerClient = async ( * @returns {Promise} A promise that resolves with a boolean value or the request * response if the the `returnResponse` attribute in the `requestParams` object is set to `true`. */ - const requestCustomGrant = (requestParams: SPACustomGrantConfig): Promise => { - const message: Message = { + const exchangeToken = (requestParams: SPACustomGrantConfig): Promise => { + const message: Message = { data: requestParams, type: REQUEST_CUSTOM_GRANT, }; - return communicate(message) + return communicate(message) .then(response => { if (requestParams.preventSignOutURLUpdate) { _getSignOutURLFromSessionStorage = true; @@ -380,21 +378,22 @@ export const WebWorkerClient = async ( }; const checkSession = async (): Promise => { - const oidcEndpoints: OIDCEndpoints = await getOIDCServiceEndpoints(); + const oidcEndpoints: OIDCEndpoints = await getOpenIDProviderEndpoints(); const config: AuthClientConfig = await getConfigData(); _authenticationHelper.initializeSessionManger( config, oidcEndpoints, - async () => (await getBasicUserInfo()).sessionState, - async (params?: GetAuthURLConfig): Promise => (await getAuthorizationURL(params)).authorizationURL, + async () => (await _authenticationClient.getUserSession()).sessionState, + async (params?: ExtendedAuthorizeRequestUrlParams): Promise => + (await getSignInUrl(params)).authorizationURL, _sessionManagementHelper, ); }; const constructSilentSignInUrl = async (additionalParams: Record = {}): Promise => { const config: AuthClientConfig = await getConfigData(); - const message: Message = { + const message: Message = { data: { prompt: 'none', state: SILENT_SIGN_IN_STATE, @@ -403,7 +402,9 @@ export const WebWorkerClient = async ( type: GET_AUTH_URL, }; - const response: AuthorizationResponse = await communicate(message); + const response: AuthorizationResponse = await communicate( + message, + ); const pkceKey: string = extractPkceStorageKeyFromState( new URL(response.authorizationURL).searchParams.get(OIDCRequestConstants.Params.STATE) ?? '', @@ -425,13 +426,13 @@ export const WebWorkerClient = async ( * This method checks if there is an active user session in the server by sending a prompt none request. * If the user is signed in, this method sends a token request. Returns false otherwise. * - * @return {Promise, tokenRequestConfig?: {params: Record}, - ): Promise => { + ): Promise => { return await _authenticationHelper.trySignInSilently( constructSilentSignInUrl, requestAccessToken, @@ -444,18 +445,18 @@ export const WebWorkerClient = async ( /** * Generates an authorization URL. * - * @param {GetAuthURLConfig} params Authorization URL params. + * @param {ExtendedAuthorizeRequestUrlParams} params Authorization URL params. * @returns {Promise} Authorization URL. */ - const getAuthorizationURL = async (params?: GetAuthURLConfig): Promise => { + const getSignInUrl = async (params?: ExtendedAuthorizeRequestUrlParams): Promise => { const config: AuthClientConfig = await getConfigData(); - const message: Message = { + const message: Message = { data: params, type: GET_AUTH_URL, }; - return communicate(message).then( + return communicate(message).then( async (response: AuthorizationResponse) => { if (response.pkce && config.enablePKCE) { const pkceKey: string = extractPkceStorageKeyFromState( @@ -477,7 +478,7 @@ export const WebWorkerClient = async ( tokenRequestConfig?: { params: Record; }, - ): Promise => { + ): Promise => { const config: AuthClientConfig = await getConfigData(); const pkceKey: string = extractPkceStorageKeyFromState(resolvedState); @@ -494,7 +495,7 @@ export const WebWorkerClient = async ( config.enablePKCE && SPAUtils.removePKCE(pkceKey); - return communicate(message) + return communicate(message) .then(response => { const message: Message = { type: GET_SIGN_OUT_URL, @@ -502,7 +503,7 @@ export const WebWorkerClient = async ( return communicate(message) .then((url: string) => { - SPAUtils.setSignOutURL(url, config.clientID, instanceID); + SPAUtils.setSignOutURL(url, config.clientId, instanceID); // Enable OIDC Sessions Management only if it is set to true in the config. if (config.enableOIDCSessionManagement) { @@ -528,8 +529,8 @@ export const WebWorkerClient = async ( }); }; - const tryRetrievingUserInfo = async (): Promise => { - if (await isAuthenticated()) { + const tryRetrievingUserInfo = async (): Promise => { + if (await isSignedIn()) { await startAutoRefreshToken(); // Enable OIDC Sessions Management only if it is set to true in the config. @@ -537,9 +538,9 @@ export const WebWorkerClient = async ( checkSession(); } - return getBasicUserInfo(); + return getUser(); } - + return Promise.resolve(undefined); }; @@ -549,14 +550,14 @@ export const WebWorkerClient = async ( * @returns {Promise} A promise that resolves when authentication is successful. */ const signIn = async ( - params?: GetAuthURLConfig, + params?: ExtendedAuthorizeRequestUrlParams, authorizationCode?: string, sessionState?: string, state?: string, tokenRequestConfig?: { params: Record; }, - ): Promise => { + ): Promise => { const basicUserInfo = await _authenticationHelper.handleSignIn( shouldStopAuthn, checkSession, @@ -570,7 +571,7 @@ export const WebWorkerClient = async ( let resolvedSessionState: string; let resolvedState: string; - if (config?.responseMode === ResponseMode.FormPost && authorizationCode) { + if (config?.responseMode === 'form_post' && authorizationCode) { resolvedAuthorizationCode = authorizationCode; resolvedSessionState = sessionState ?? ''; resolvedState = state ?? ''; @@ -588,7 +589,7 @@ export const WebWorkerClient = async ( return requestAccessToken(resolvedAuthorizationCode, resolvedSessionState, resolvedState, tokenRequestConfig); } - return getAuthorizationURL(params) + return getSignInUrl(params) .then(async (response: AuthorizationResponse) => { location.href = response.authorizationURL; @@ -634,7 +635,7 @@ export const WebWorkerClient = async ( return reject(error); }); } else { - window.location.href = SPAUtils.getSignOutURL(config.clientID, instanceID); + window.location.href = SPAUtils.getSignOutUrl(config.clientId, instanceID); return SPAUtils.waitTillPageRedirect().then(() => { return Promise.resolve(true); @@ -663,7 +664,7 @@ export const WebWorkerClient = async ( }); }; - const getOIDCServiceEndpoints = (): Promise => { + const getOpenIDProviderEndpoints = (): Promise => { const message: Message = { type: GET_OIDC_SERVICE_ENDPOINTS, }; @@ -691,12 +692,12 @@ export const WebWorkerClient = async ( }); }; - const getBasicUserInfo = (): Promise => { + const getUser = (): Promise => { const message: Message = { type: GET_BASIC_USER_INFO, }; - return communicate(message) + return communicate(message) .then(response => { return Promise.resolve(response); }) @@ -705,7 +706,7 @@ export const WebWorkerClient = async ( }); }; - const getDecodedIDToken = (): Promise => { + const getDecodedIdToken = (): Promise => { const message: Message = { type: GET_DECODED_ID_TOKEN, }; @@ -733,7 +734,7 @@ export const WebWorkerClient = async ( }); }; - const getCryptoHelper = (): Promise => { + const getCrypto = (): Promise => { const message: Message = { type: GET_CRYPTO_HELPER, }; @@ -747,7 +748,7 @@ export const WebWorkerClient = async ( }); }; - const getIDToken = (): Promise => { + const getIdToken = (): Promise => { const message: Message = { type: GET_ID_TOKEN, }; @@ -761,7 +762,7 @@ export const WebWorkerClient = async ( }); }; - const isAuthenticated = (): Promise => { + const isSignedIn = (): Promise => { const message: Message = { type: IS_AUTHENTICATED, }; @@ -775,12 +776,12 @@ export const WebWorkerClient = async ( }); }; - const refreshAccessToken = (): Promise => { + const refreshAccessToken = (): Promise => { const message: Message = { type: REFRESH_ACCESS_TOKEN, }; - return communicate(message); + return communicate(message); }; const setHttpRequestSuccessCallback = (callback: (response: HttpResponse) => void): void => { @@ -807,7 +808,7 @@ export const WebWorkerClient = async ( } }; - const updateConfig = async (newConfig: Partial>): Promise => { + const reInitialize = async (newConfig: Partial>): Promise => { const existingConfig = await getConfigData(); const isCheckSessionIframeDifferent: boolean = !( existingConfig && @@ -838,19 +839,19 @@ export const WebWorkerClient = async ( return { disableHttpHandler, enableHttpHandler, - getBasicUserInfo, + getUser, getConfigData, - getCryptoHelper, + getCrypto, getDecodedIDPIDToken, - getDecodedIDToken, - getIDToken, - getOIDCServiceEndpoints, + getDecodedIdToken, + getIdToken, + getOpenIDProviderEndpoints, httpRequest, httpRequestAll, initialize, - isAuthenticated, + isSignedIn, refreshAccessToken, - requestCustomGrant, + exchangeToken, revokeAccessToken, setHttpRequestErrorCallback, setHttpRequestFinishCallback, @@ -859,6 +860,6 @@ export const WebWorkerClient = async ( signIn, signOut, trySignInSilently, - updateConfig, + reInitialize, }; }; diff --git a/packages/browser/src/__legacy__/helpers/authentication-helper.ts b/packages/browser/src/__legacy__/helpers/authentication-helper.ts index cb9f83e0..0763ba27 100644 --- a/packages/browser/src/__legacy__/helpers/authentication-helper.ts +++ b/packages/browser/src/__legacy__/helpers/authentication-helper.ts @@ -20,13 +20,12 @@ import { AsgardeoAuthClient, AsgardeoAuthException, AuthClientConfig, - BasicUserInfo, + User, IsomorphicCrypto, - CustomGrantConfig, - DataLayer, + TokenExchangeRequestConfig, + StorageManager, IdTokenPayload, - FetchResponse, - GetAuthURLConfig, + ExtendedAuthorizeRequestUrlParams, OIDCEndpoints, TokenResponse, extractPkceStorageKeyFromState, @@ -56,21 +55,21 @@ import { WebWorkerClientConfig, } from '../models'; import {SPACustomGrantConfig} from '../models/request-custom-grant'; -import {Storage} from '../models/storage'; +import {BrowserStorage} from '../models/storage'; import {SPAUtils} from '../utils'; export class AuthenticationHelper { protected _authenticationClient: AsgardeoAuthClient; - protected _dataLayer: DataLayer; + protected _storageManager: StorageManager; protected _spaHelper: SPAHelper; protected _instanceID: number; protected _isTokenRefreshing: boolean; public constructor(authClient: AsgardeoAuthClient, spaHelper: SPAHelper) { this._authenticationClient = authClient; - this._dataLayer = this._authenticationClient.getDataLayer(); + this._storageManager = this._authenticationClient.getStorageManager(); this._spaHelper = spaHelper; - this._instanceID = this._authenticationClient.getInstanceID(); + this._instanceID = this._authenticationClient.getInstanceId(); this._isTokenRefreshing = false; } @@ -86,24 +85,24 @@ export class AuthenticationHelper, oidcEndpoints: OIDCEndpoints, getSessionState: () => Promise, - getAuthzURL: (params?: GetAuthURLConfig) => Promise, + getAuthzURL: (params?: ExtendedAuthorizeRequestUrlParams) => Promise, sessionManagementHelper: SessionManagementHelperInterface, ): void { sessionManagementHelper.initialize( - config.clientID, + config.clientId, oidcEndpoints.checkSessionIframe ?? '', getSessionState, config.checkSessionInterval ?? 3, config.sessionRefreshInterval ?? 300, - config.signInRedirectURL, + config.afterSignInUrl, getAuthzURL, ); } - public async requestCustomGrant( + public async exchangeToken( config: SPACustomGrantConfig, enableRetrievingSignOutURLFromSession?: (config: SPACustomGrantConfig) => void, - ): Promise { + ): Promise { let useDefaultEndpoint = true; let matches = false; @@ -112,7 +111,7 @@ export class AuthenticationHelper { + .exchangeToken(config) + .then(async (response: Response | TokenResponse) => { if (enableRetrievingSignOutURLFromSession && typeof enableRetrievingSignOutURLFromSession === 'function') { enableRetrievingSignOutURLFromSession(config); } @@ -135,9 +134,9 @@ export class AuthenticationHelper { @@ -156,8 +155,8 @@ export class AuthenticationHelper | null> { - const configString = await this._dataLayer.getTemporaryDataParameter(CUSTOM_GRANT_CONFIG); + public async getCustomGrantConfigData(): Promise | null> { + const configString = await this._storageManager.getTemporaryDataParameter(CUSTOM_GRANT_CONFIG); if (configString) { return JSON.parse(configString as string); @@ -168,16 +167,16 @@ export class AuthenticationHelper void, - ): Promise { + ): Promise { try { await this._authenticationClient.refreshAccessToken(); const customGrantConfig = await this.getCustomGrantConfigData(); if (customGrantConfig) { - await this.requestCustomGrant(customGrantConfig, enableRetrievingSignOutURLFromSession); + await this.exchangeToken(customGrantConfig, enableRetrievingSignOutURLFromSession); } this._spaHelper.refreshAccessTokenAutomatically(this); - return this._authenticationClient.getBasicUserInfo(); + return this._authenticationClient.getUser(); } catch (error) { const refreshTokenError: Message = { type: REFRESH_ACCESS_TOKEN_ERR0R, @@ -225,7 +224,7 @@ export class AuthenticationHelper void, ): Promise { let matches = false; - const config = await this._dataLayer.getConfigData(); + const config = await this._storageManager.getConfigData(); for (const baseUrl of [...((await config?.resourceServerURLs) ?? []), (config as any).baseUrl]) { if (baseUrl && requestConfig?.url?.startsWith(baseUrl)) { @@ -256,7 +255,7 @@ export class AuthenticationHelper void, ): Promise { let matches = true; - const config = await this._dataLayer.getConfigData(); + const config = await this._storageManager.getConfigData(); for (const requestConfig of requestConfigs) { let urlMatches = false; @@ -372,7 +371,7 @@ export class AuthenticationHelper { if (error?.response?.status === 401 || !error?.response) { - let refreshTokenResponse: TokenResponse | BasicUserInfo; + let refreshTokenResponse: TokenResponse | User; try { refreshTokenResponse = await this._authenticationClient.refreshAccessToken(); } catch (refreshError: any) { @@ -453,14 +452,14 @@ export class AuthenticationHelper; }, - ): Promise { - const config = await this._dataLayer.getConfigData(); + ): Promise { + const config = await this._storageManager.getConfigData(); - if (config.storage === Storage.BrowserMemory && config.enablePKCE && sessionState) { + if (config.storage === BrowserStorage.BrowserMemory && config.enablePKCE && sessionState) { const pkce = SPAUtils.getPKCE(extractPkceStorageKeyFromState(sessionState)); await this._authenticationClient.setPKCECode(extractPkceStorageKeyFromState(sessionState), pkce); - } else if (config.storage === Storage.WebWorker && pkce) { + } else if (config.storage === BrowserStorage.WebWorker && pkce) { await this._authenticationClient.setPKCECode(pkce, state ?? ''); } @@ -470,10 +469,10 @@ export class AuthenticationHelper { // Disable this temporarily /* if (config.storage === Storage.BrowserMemory) { - SPAUtils.setSignOutURL(await _authenticationClient.getSignOutURL()); + SPAUtils.setSignOutURL(await _authenticationClient.getSignOutUrl()); } */ - if (config.storage !== Storage.WebWorker) { - SPAUtils.setSignOutURL(await this._authenticationClient.getSignOutURL(), config.clientID, this._instanceID); + if (config.storage !== BrowserStorage.WebWorker) { + SPAUtils.setSignOutURL(await this._authenticationClient.getSignOutUrl(), config.clientId, this._instanceID); if (this._spaHelper) { this._spaHelper.clearRefreshTokenTimeout(); @@ -490,7 +489,7 @@ export class AuthenticationHelper { return Promise.reject(error); @@ -513,11 +512,11 @@ export class AuthenticationHelper}, - ) => Promise, + ) => Promise, sessionManagementHelper: SessionManagementHelperInterface, additionalParams?: Record, tokenRequestConfig?: {params: Record}, - ): Promise { + ): Promise { // This block is executed by the iFrame when the server redirects with the authorization code. if (SPAUtils.isInitializedSilentSignIn()) { await sessionManagementHelper.receivePromptNoneResponse(); @@ -564,7 +563,7 @@ export class AuthenticationHelper { + .then((response: User) => { window.removeEventListener('message', listenToPromptNoneIFrame); resolve(response); }) @@ -585,9 +584,9 @@ export class AuthenticationHelper Promise, checkSession: () => Promise, - tryRetrievingUserInfo?: () => Promise, - ): Promise { - const config = await this._dataLayer.getConfigData(); + tryRetrievingUserInfo?: () => Promise, + ): Promise { + const config = await this._storageManager.getConfigData(); if (await shouldStopAuthn()) { return Promise.resolve({ @@ -601,8 +600,8 @@ export class AuthenticationHelper { - return this._authenticationClient.getBasicUserInfo(); + public async getUser(): Promise { + return this._authenticationClient.getUser(); } - public async getDecodedIDToken(): Promise { - return this._authenticationClient.getDecodedIDToken(); + public async getDecodedIdToken(): Promise { + return this._authenticationClient.getDecodedIdToken(); } public async getDecodedIDPIDToken(): Promise { - return this._authenticationClient.getDecodedIDToken(); + return this._authenticationClient.getDecodedIdToken(); } - public async getCryptoHelper(): Promise { - return this._authenticationClient.getCryptoHelper(); + public async getCrypto(): Promise { + return this._authenticationClient.getCrypto(); } - public async getIDToken(): Promise { - return this._authenticationClient.getIDToken(); + public async getIdToken(): Promise { + return this._authenticationClient.getIdToken(); } - public async getOIDCServiceEndpoints(): Promise { - return this._authenticationClient.getOIDCServiceEndpoints() as any; + public async getOpenIDProviderEndpoints(): Promise { + return this._authenticationClient.getOpenIDProviderEndpoints() as any; } public async getAccessToken(): Promise { @@ -685,14 +684,14 @@ export class AuthenticationHelper { - return (await this._dataLayer.getSessionData())?.access_token; + return (await this._storageManager.getSessionData())?.access_token; } - public getDataLayer(): DataLayer { - return this._dataLayer; + public getStorageManager(): StorageManager { + return this._storageManager; } - public async isAuthenticated(): Promise { - return this._authenticationClient.isAuthenticated(); + public async isSignedIn(): Promise { + return this._authenticationClient.isSignedIn(); } } diff --git a/packages/browser/src/__legacy__/helpers/session-management-helper.ts b/packages/browser/src/__legacy__/helpers/session-management-helper.ts index e6c70b85..184245c9 100644 --- a/packages/browser/src/__legacy__/helpers/session-management-helper.ts +++ b/packages/browser/src/__legacy__/helpers/session-management-helper.ts @@ -16,7 +16,7 @@ * under the License. */ -import {AsgardeoAuthClient, GetAuthURLConfig, OIDCRequestConstants} from '@asgardeo/javascript'; +import {AsgardeoAuthClient, ExtendedAuthorizeRequestUrlParams, OIDCRequestConstants} from '@asgardeo/javascript'; import { CHECK_SESSION_SIGNED_IN, CHECK_SESSION_SIGNED_OUT, @@ -30,7 +30,7 @@ import { STATE_QUERY, } from '../constants'; import {AuthorizationInfo, Message, SessionManagementHelperInterface} from '../models'; -import {Storage} from '../models/storage'; +import {BrowserStorage} from '../models/storage'; import {SPAUtils} from '../utils'; export const SessionManagementHelper = (() => { @@ -43,26 +43,26 @@ export const SessionManagementHelper = (() => { let _signOut: () => Promise; let _sessionRefreshIntervalTimeout: number; let _checkSessionIntervalTimeout: number; - let _storage: Storage; + let _storage: BrowserStorage; let _setSessionState: (sessionState: string) => void; - let _getAuthorizationURL: (params?: GetAuthURLConfig) => Promise; + let _getSignInUrl: (params?: ExtendedAuthorizeRequestUrlParams) => Promise; const initialize = ( - clientID: string, + clientId: string, checkSessionEndpoint: string, getSessionState: () => Promise, interval: number, sessionRefreshInterval: number, redirectURL: string, - getAuthorizationURL: (params?: GetAuthURLConfig) => Promise, + getSignInUrl: (params?: ExtendedAuthorizeRequestUrlParams) => Promise, ): void => { - _clientID = clientID; + _clientID = clientId; _checkSessionEndpoint = checkSessionEndpoint; _sessionState = getSessionState; _interval = interval; _redirectURL = redirectURL; _sessionRefreshInterval = sessionRefreshInterval; - _getAuthorizationURL = getAuthorizationURL; + _getSignInUrl = getSignInUrl; if (_interval > -1) { initiateCheckSession(); @@ -148,11 +148,11 @@ export const SessionManagementHelper = (() => { } }; - if (_storage === Storage.BrowserMemory || _storage === Storage.WebWorker) { + if (_storage === BrowserStorage.BrowserMemory || _storage === BrowserStorage.WebWorker) { window?.addEventListener('message', receiveMessageListener); } - const promptNoneURL: string = await _getAuthorizationURL({ + const promptNoneURL: string = await _getSignInUrl({ prompt: 'none', response_mode: 'query', state: STATE, @@ -203,7 +203,7 @@ export const SessionManagementHelper = (() => { const newSessionState = new URL(window.location.href).searchParams.get('session_state'); - if (_storage === Storage.LocalStorage || _storage === Storage.SessionStorage) { + if (_storage === BrowserStorage.LocalStorage || _storage === BrowserStorage.SessionStorage) { setSessionState && (await setSessionState(newSessionState)); } else { const message: Message = { @@ -242,7 +242,7 @@ export const SessionManagementHelper = (() => { const signOutURL = await _signOut(); // Clearing user session data before redirecting to the signOutURL because user has been already logged // out by the initial logout request in the single logout flow. - await AsgardeoAuthClient.clearUserSessionData(); + await AsgardeoAuthClient.clearSession(); parent.location.href = signOutURL; window.location.href = 'about:blank'; @@ -257,7 +257,7 @@ export const SessionManagementHelper = (() => { return async ( signOut: () => Promise, - storage: Storage, + storage: BrowserStorage, setSessionState: (sessionState: string) => void, ): Promise => { let rpIFrame = document.createElement('iframe'); diff --git a/packages/browser/src/__legacy__/helpers/spa-helper.ts b/packages/browser/src/__legacy__/helpers/spa-helper.ts index 33cc0ac1..ab5085c6 100644 --- a/packages/browser/src/__legacy__/helpers/spa-helper.ts +++ b/packages/browser/src/__legacy__/helpers/spa-helper.ts @@ -16,29 +16,31 @@ * under the License. */ -import {AsgardeoAuthClient, DataLayer, TokenConstants} from '@asgardeo/javascript'; +import {AsgardeoAuthClient, StorageManager, TokenConstants} from '@asgardeo/javascript'; import {AuthenticationHelper} from '../helpers/authentication-helper'; import {MainThreadClientConfig, WebWorkerClientConfig} from '../models/client-config'; export class SPAHelper { private _authenticationClient: AsgardeoAuthClient; - private _dataLayer: DataLayer; + private _storageManager: StorageManager; + public constructor(authClient: AsgardeoAuthClient) { this._authenticationClient = authClient; - this._dataLayer = this._authenticationClient.getDataLayer(); + this._storageManager = this._authenticationClient.getStorageManager(); } public async refreshAccessTokenAutomatically( authenticationHelper: AuthenticationHelper, ): Promise { - const shouldRefreshAutomatically: boolean = (await this._dataLayer.getConfigData())?.periodicTokenRefresh ?? false; + const shouldRefreshAutomatically: boolean = + (await this._storageManager.getConfigData())?.periodicTokenRefresh ?? false; if (!shouldRefreshAutomatically) { return; } - const sessionData = await this._dataLayer.getSessionData(); + const sessionData = await this._storageManager.getSessionData(); if (sessionData.refresh_token) { // Refresh 10 seconds before the expiry time const expiryTime = parseInt(sessionData.expires_in); @@ -48,7 +50,7 @@ export class SPAHelper await authenticationHelper.refreshAccessToken(); }, time * 1000); - await this._dataLayer.setTemporaryDataParameter( + await this._storageManager.setTemporaryDataParameter( TokenConstants.Storage.StorageKeys.REFRESH_TOKEN_TIMER, JSON.stringify(timer), ); @@ -56,9 +58,9 @@ export class SPAHelper } public async getRefreshTimeoutTimer(): Promise { - if (await this._dataLayer.getTemporaryDataParameter(TokenConstants.Storage.StorageKeys.REFRESH_TOKEN_TIMER)) { + if (await this._storageManager.getTemporaryDataParameter(TokenConstants.Storage.StorageKeys.REFRESH_TOKEN_TIMER)) { return JSON.parse( - (await this._dataLayer.getTemporaryDataParameter( + (await this._storageManager.getTemporaryDataParameter( TokenConstants.Storage.StorageKeys.REFRESH_TOKEN_TIMER, )) as string, ); diff --git a/packages/browser/src/__legacy__/models/client-config.ts b/packages/browser/src/__legacy__/models/client-config.ts index c9930e8e..e807e7a7 100644 --- a/packages/browser/src/__legacy__/models/client-config.ts +++ b/packages/browser/src/__legacy__/models/client-config.ts @@ -17,7 +17,7 @@ */ import {AuthClientConfig} from '@asgardeo/javascript'; -import {Storage} from './storage'; +import {BrowserStorage} from './storage'; export interface SPAConfig { /** @@ -42,9 +42,9 @@ export interface MainThreadClientConfig extends SPAConfig { * The storage type to be used for storing the session information. */ storage?: - | Storage.SessionStorage - | Storage.LocalStorage - | Storage.BrowserMemory + | BrowserStorage.SessionStorage + | BrowserStorage.LocalStorage + | BrowserStorage.BrowserMemory | 'sessionStorage' | 'localStorage' | 'browserMemory'; @@ -54,7 +54,7 @@ export interface WebWorkerClientConfig extends SPAConfig { /** * The storage type to be used for storing the session information. */ - storage: Storage.WebWorker | 'webWorker'; + storage: BrowserStorage.WebWorker | 'webWorker'; /** * Specifies in seconds how long a request to the web worker should wait before being timed. */ diff --git a/packages/browser/src/__legacy__/models/client.ts b/packages/browser/src/__legacy__/models/client.ts index 286ac78b..3eac3388 100755 --- a/packages/browser/src/__legacy__/models/client.ts +++ b/packages/browser/src/__legacy__/models/client.ts @@ -18,12 +18,11 @@ import { AuthClientConfig, - BasicUserInfo, + User, IsomorphicCrypto, - CustomGrantConfig, - DataLayer, + TokenExchangeRequestConfig, + StorageManager, IdTokenPayload, - FetchResponse, OIDCEndpoints, } from '@asgardeo/javascript'; import { @@ -50,34 +49,34 @@ export interface MainThreadClientInterface { config?: SignInConfig, authorizationCode?: string, sessionState?: string, - signInRedirectURL?: string, + afterSignInUrl?: string, tokenRequestConfig?: { params: Record; }, - ): Promise; - signOut(signOutRedirectURL?: string): Promise; - requestCustomGrant(config: CustomGrantConfig): Promise; - refreshAccessToken(): Promise; + ): Promise; + signOut(afterSignOutUrl?: string): Promise; + exchangeToken(config: TokenExchangeRequestConfig): Promise; + refreshAccessToken(): Promise; revokeAccessToken(): Promise; - getBasicUserInfo(): Promise; - getDecodedIDToken(): Promise; - getCryptoHelper(): Promise; + getUser(): Promise; + getDecodedIdToken(): Promise; + getCrypto(): Promise; getConfigData(): Promise>; - getIDToken(): Promise; - getOIDCServiceEndpoints(): Promise; + getIdToken(): Promise; + getOpenIDProviderEndpoints(): Promise; getAccessToken(): Promise; - getDataLayer(): Promise>; - isAuthenticated(): Promise; - updateConfig(config: Partial>): Promise; + getStorageManager(): Promise>; + isSignedIn(): Promise; + reInitialize(config: Partial>): Promise; trySignInSilently( additionalParams?: Record, tokenRequestConfig?: {params: Record}, - ): Promise; + ): Promise; isSessionActive(): Promise; } export interface WebWorkerClientInterface { - requestCustomGrant(requestParams: CustomGrantConfig): Promise; + exchangeToken(requestParams: TokenExchangeRequestConfig): Promise; httpRequest(config: HttpRequestConfig): Promise>; httpRequestAll(configs: HttpRequestConfig[]): Promise[]>; enableHttpHandler(): Promise; @@ -87,29 +86,29 @@ export interface WebWorkerClientInterface { params?: SignInConfig, authorizationCode?: string, sessionState?: string, - signInRedirectURL?: string, + afterSignInUrl?: string, tokenRequestConfig?: { params: Record; }, - ): Promise; - signOut(signOutRedirectURL?: string): Promise; + ): Promise; + signOut(afterSignOutUrl?: string): Promise; revokeAccessToken(): Promise; - getOIDCServiceEndpoints(): Promise; - getBasicUserInfo(): Promise; + getOpenIDProviderEndpoints(): Promise; + getUser(): Promise; getConfigData(): Promise>; - getDecodedIDToken(): Promise; + getDecodedIdToken(): Promise; getDecodedIDPIDToken(): Promise; - getCryptoHelper(): Promise; - getIDToken(): Promise; - isAuthenticated(): Promise; + getCrypto(): Promise; + getIdToken(): Promise; + isSignedIn(): Promise; setHttpRequestSuccessCallback(callback: (response: HttpResponse) => void): void; setHttpRequestErrorCallback(callback: (response: HttpError) => void | Promise): void; setHttpRequestStartCallback(callback: () => void): void; setHttpRequestFinishCallback(callback: () => void): void; - refreshAccessToken(): Promise; - updateConfig(config: Partial>): Promise; + refreshAccessToken(): Promise; + reInitialize(config: Partial>): Promise; trySignInSilently( additionalParams?: Record, tokenRequestConfig?: {params: Record}, - ): Promise; + ): Promise; } diff --git a/packages/browser/src/__legacy__/models/request-custom-grant.ts b/packages/browser/src/__legacy__/models/request-custom-grant.ts index 1c8b995f..1718c9ae 100644 --- a/packages/browser/src/__legacy__/models/request-custom-grant.ts +++ b/packages/browser/src/__legacy__/models/request-custom-grant.ts @@ -16,11 +16,11 @@ * under the License. */ -import {CustomGrantConfig} from '@asgardeo/javascript'; +import {TokenExchangeRequestConfig} from '@asgardeo/javascript'; /** * SPA Custom Request Grant config model */ -export interface SPACustomGrantConfig extends CustomGrantConfig { +export interface SPACustomGrantConfig extends TokenExchangeRequestConfig { preventSignOutURLUpdate?: boolean; } diff --git a/packages/browser/src/__legacy__/models/session-management-helper.ts b/packages/browser/src/__legacy__/models/session-management-helper.ts index 795f830a..e5706bb9 100644 --- a/packages/browser/src/__legacy__/models/session-management-helper.ts +++ b/packages/browser/src/__legacy__/models/session-management-helper.ts @@ -16,17 +16,17 @@ * under the License. */ -import {GetAuthURLConfig} from '@asgardeo/javascript'; +import {ExtendedAuthorizeRequestUrlParams} from '@asgardeo/javascript'; export interface SessionManagementHelperInterface { initialize( - clientID: string, + clientId: string, checkSessionEndpoint: string, getSessionState: () => Promise, interval: number, sessionRefreshInterval: number, redirectURL: string, - getAuthorizationURL: (params?: GetAuthURLConfig) => Promise, + getSignInUrl: (params?: ExtendedAuthorizeRequestUrlParams) => Promise, ): void; receivePromptNoneResponse(setSessionState?: (sessionState: string | null) => Promise): Promise; reset(); diff --git a/packages/browser/src/__legacy__/models/sign-in.ts b/packages/browser/src/__legacy__/models/sign-in.ts index 0ce815a5..f8fcd094 100644 --- a/packages/browser/src/__legacy__/models/sign-in.ts +++ b/packages/browser/src/__legacy__/models/sign-in.ts @@ -16,6 +16,6 @@ * under the License. */ -import {GetAuthURLConfig} from '@asgardeo/javascript'; +import {ExtendedAuthorizeRequestUrlParams} from '@asgardeo/javascript'; -export type SignInConfig = GetAuthURLConfig & {callOnlyOnRedirect?: boolean}; +export type SignInConfig = ExtendedAuthorizeRequestUrlParams & {callOnlyOnRedirect?: boolean}; diff --git a/packages/browser/src/__legacy__/models/storage.ts b/packages/browser/src/__legacy__/models/storage.ts index ef4150c5..5f68ae89 100644 --- a/packages/browser/src/__legacy__/models/storage.ts +++ b/packages/browser/src/__legacy__/models/storage.ts @@ -19,21 +19,21 @@ /** * Enum for the different storage types. */ -export enum Storage { +export enum BrowserStorage { /** * Stores the session information in the local storage */ - LocalStorage = "localStorage", + LocalStorage = 'localStorage', /** * Store the session information in the session storage. */ - SessionStorage = "sessionStorage", + SessionStorage = 'sessionStorage', /** * Store the session information in the web worker. */ - WebWorker = "webWorker", + WebWorker = 'webWorker', /** * Store the session information in the browser memory. */ - BrowserMemory = "browserMemory" + BrowserMemory = 'browserMemory', } diff --git a/packages/browser/src/__legacy__/models/web-worker.ts b/packages/browser/src/__legacy__/models/web-worker.ts index f46ed5fe..7abb33b6 100755 --- a/packages/browser/src/__legacy__/models/web-worker.ts +++ b/packages/browser/src/__legacy__/models/web-worker.ts @@ -18,12 +18,11 @@ import { AuthClientConfig, - AuthorizationURLParams, - BasicUserInfo, + AuthorizeRequestUrlParams, + User, IsomorphicCrypto, - CustomGrantConfig, + TokenExchangeRequestConfig, IdTokenPayload, - FetchResponse, OIDCEndpoints, } from '@asgardeo/javascript'; import {HttpRequestConfig, HttpResponse, Message} from '.'; @@ -33,8 +32,9 @@ interface WebWorkerEvent extends MessageEvent { data: Message; } -export class WebWorkerClass extends Worker { - public override onmessage: (this: Worker, event: WebWorkerEvent) => any = () => null; +export class WebWorkerClass { + onmessage: (this: Worker, event: WebWorkerEvent) => any = () => null; + postMessage: (message: Message) => void = () => null; } export interface WebWorkerCoreInterface { @@ -45,28 +45,23 @@ export interface WebWorkerCoreInterface { httpRequestAll(configs: HttpRequestConfig[]): Promise; enableHttpHandler(): void; disableHttpHandler(): void; - getAuthorizationURL(params?: AuthorizationURLParams, signInRedirectURL?: string): Promise; - requestAccessToken( - authorizationCode?: string, - sessionState?: string, - pkce?: string, - state?: string, - ): Promise; - signOut(signOutRedirectURL?: string): Promise; - getSignOutURL(signOutRedirectURL?: string): Promise; - requestCustomGrant(config: CustomGrantConfig): Promise; - refreshAccessToken(): Promise; + getSignInUrl(params?: AuthorizeRequestUrlParams, afterSignInUrl?: string): Promise; + requestAccessToken(authorizationCode?: string, sessionState?: string, pkce?: string, state?: string): Promise; + signOut(afterSignOutUrl?: string): Promise; + getSignOutUrl(afterSignOutUrl?: string): Promise; + exchangeToken(config: TokenExchangeRequestConfig): Promise; + refreshAccessToken(): Promise; revokeAccessToken(): Promise; - getBasicUserInfo(): Promise; - getDecodedIDToken(): Promise; + getUser(): Promise; + getDecodedIdToken(): Promise; getDecodedIDPIDToken(): Promise; - getCryptoHelper(): Promise; - getIDToken(): Promise; - getOIDCServiceEndpoints(): Promise; + getCrypto(): Promise; + getIdToken(): Promise; + getOpenIDProviderEndpoints(): Promise; getAccessToken(): Promise; - isAuthenticated(): Promise; + isSignedIn(): Promise; startAutoRefreshToken(): Promise; setSessionState(sessionState: string): Promise; - updateConfig(config: Partial>): Promise; + reInitialize(config: Partial>): Promise; getConfigData(): Promise>; } diff --git a/packages/browser/src/__legacy__/stores/local-store.ts b/packages/browser/src/__legacy__/stores/local-store.ts index 0a0d0e6f..38c80fb7 100644 --- a/packages/browser/src/__legacy__/stores/local-store.ts +++ b/packages/browser/src/__legacy__/stores/local-store.ts @@ -15,9 +15,9 @@ * specific language governing permissions and limitations * under the License. */ -import {Store} from '@asgardeo/javascript'; +import {Storage} from '@asgardeo/javascript'; -export class LocalStore implements Store { +export class LocalStore implements Storage { public async setData(key: string, value: string): Promise { localStorage.setItem(key, value); } diff --git a/packages/browser/src/__legacy__/stores/memory-store.ts b/packages/browser/src/__legacy__/stores/memory-store.ts index f1362eca..9122e175 100644 --- a/packages/browser/src/__legacy__/stores/memory-store.ts +++ b/packages/browser/src/__legacy__/stores/memory-store.ts @@ -15,9 +15,9 @@ * specific language governing permissions and limitations * under the License. */ -import {Store} from '@asgardeo/javascript'; +import {Storage} from '@asgardeo/javascript'; -export class MemoryStore implements Store { +export class MemoryStore implements Storage { private _data: Map; public constructor() { diff --git a/packages/browser/src/__legacy__/stores/session-store.ts b/packages/browser/src/__legacy__/stores/session-store.ts index b963ec38..67e18e0f 100644 --- a/packages/browser/src/__legacy__/stores/session-store.ts +++ b/packages/browser/src/__legacy__/stores/session-store.ts @@ -15,9 +15,9 @@ * specific language governing permissions and limitations * under the License. */ -import {Store} from '@asgardeo/javascript'; +import {Storage} from '@asgardeo/javascript'; -export class SessionStore implements Store { +export class SessionStore implements Storage { public async setData(key: string, value: string): Promise { sessionStorage.setItem(key, value); } diff --git a/packages/browser/src/__legacy__/utils/crypto-utils.ts b/packages/browser/src/__legacy__/utils/crypto-utils.ts index 3958ab0f..1d379271 100644 --- a/packages/browser/src/__legacy__/utils/crypto-utils.ts +++ b/packages/browser/src/__legacy__/utils/crypto-utils.ts @@ -49,7 +49,7 @@ export class SPACryptoUtils implements Crypto { idToken: string, jwk: Partial, algorithms: string[], - clientID: string, + clientId: string, issuer: string, subject: string, clockTolerance?: number, @@ -57,7 +57,7 @@ export class SPACryptoUtils implements Crypto { ): Promise { const jwtVerifyOptions = { algorithms: algorithms, - audience: clientID, + audience: clientId, clockTolerance: clockTolerance, subject: subject, }; diff --git a/packages/browser/src/__legacy__/utils/spa-utils.ts b/packages/browser/src/__legacy__/utils/spa-utils.ts index f20583d4..bf35ff95 100644 --- a/packages/browser/src/__legacy__/utils/spa-utils.ts +++ b/packages/browser/src/__legacy__/utils/spa-utils.ts @@ -45,17 +45,17 @@ export class SPAUtils { sessionStorage.setItem(pkceKey, pkce); } - public static setSignOutURL(url: string, clientID: string, instanceID: number): void { + public static setSignOutURL(url: string, clientId: string, instanceID: number): void { sessionStorage.setItem( - `${OIDCRequestConstants.SignOut.Storage.StorageKeys.SIGN_OUT_URL}-instance_${instanceID}-${clientID}`, + `${OIDCRequestConstants.SignOut.Storage.StorageKeys.SIGN_OUT_URL}-instance_${instanceID}-${clientId}`, url, ); } - public static getSignOutURL(clientID: string, instanceID: number): string { + public static getSignOutUrl(clientId: string, instanceID: number): string { return ( sessionStorage.getItem( - `${OIDCRequestConstants.SignOut.Storage.StorageKeys.SIGN_OUT_URL}-instance_${instanceID}-${clientID}`, + `${OIDCRequestConstants.SignOut.Storage.StorageKeys.SIGN_OUT_URL}-instance_${instanceID}-${clientId}`, ) ?? '' ); } @@ -120,7 +120,7 @@ export class SPAUtils { const newUrl = window.location.href.split('?')[0]; history.pushState({}, document.title, newUrl); - await AsgardeoAuthClient.clearUserSessionData(); + await AsgardeoAuthClient.clearSession(); return true; } diff --git a/packages/browser/src/__legacy__/worker/worker-core.ts b/packages/browser/src/__legacy__/worker/worker-core.ts index 38b67297..ec1ddc12 100755 --- a/packages/browser/src/__legacy__/worker/worker-core.ts +++ b/packages/browser/src/__legacy__/worker/worker-core.ts @@ -19,16 +19,15 @@ import { AsgardeoAuthClient, AuthClientConfig, - AuthorizationURLParams, - BasicUserInfo, + AuthorizeRequestUrlParams, + User, IsomorphicCrypto, - CustomGrantConfig, + TokenExchangeRequestConfig, IdTokenPayload, - FetchResponse, OIDCEndpoints, OIDCRequestConstants, SessionData, - Store, + Storage, } from '@asgardeo/javascript'; import {AuthenticationHelper, SPAHelper} from '../helpers'; import {HttpClient, HttpClientInstance} from '../http-client'; @@ -49,7 +48,7 @@ export const WebWorkerCore = async ( spaHelper: SPAHelper, ) => AuthenticationHelper, ): Promise => { - const _store: Store = new MemoryStore(); + const _store: Storage = new MemoryStore(); const _cryptoUtils: SPACryptoUtils = new SPACryptoUtils(); const _authenticationClient = new AsgardeoAuthClient(); await _authenticationClient.initialize(config, _store, _cryptoUtils); @@ -61,7 +60,7 @@ export const WebWorkerCore = async ( _spaHelper, ); - const _dataLayer = _authenticationClient.getDataLayer(); + const _dataLayer = _authenticationClient.getStorageManager(); const _httpClient: HttpClientInstance = HttpClient.getInstance(); @@ -99,9 +98,9 @@ export const WebWorkerCore = async ( _authenticationHelper.disableHttpHandler(_httpClient); }; - const getAuthorizationURL = async (params?: AuthorizationURLParams): Promise => { + const getSignInUrl = async (params?: AuthorizeRequestUrlParams): Promise => { return _authenticationClient - .getAuthorizationURL(params) + .getSignInUrl(params) .then(async (url: string) => { const urlObject: URL = new URL(url); const state: string = urlObject.searchParams.get(OIDCRequestConstants.Params.STATE) ?? ''; @@ -124,25 +123,25 @@ export const WebWorkerCore = async ( sessionState?: string, pkce?: string, state?: string, - ): Promise => { + ): Promise => { return await _authenticationHelper.requestAccessToken(authorizationCode, sessionState, undefined, pkce, state); }; const signOut = async (): Promise => { _spaHelper.clearRefreshTokenTimeout(); - return await _authenticationClient.getSignOutURL(); + return await _authenticationClient.getSignOutUrl(); }; - const getSignOutURL = async (): Promise => { - return await _authenticationClient.getSignOutURL(); + const getSignOutUrl = async (): Promise => { + return await _authenticationClient.getSignOutUrl(); }; - const requestCustomGrant = async (config: CustomGrantConfig): Promise => { - return await _authenticationHelper.requestCustomGrant(config); + const exchangeToken = async (config: TokenExchangeRequestConfig): Promise => { + return await _authenticationHelper.exchangeToken(config); }; - const refreshAccessToken = async (): Promise => { + const refreshAccessToken = async (): Promise => { try { return await _authenticationHelper.refreshAccessToken(); } catch (error) { @@ -163,35 +162,35 @@ export const WebWorkerCore = async ( .catch(error => Promise.reject(error)); }; - const getBasicUserInfo = async (): Promise => { - return _authenticationHelper.getBasicUserInfo(); + const getUser = async (): Promise => { + return _authenticationHelper.getUser(); }; - const getDecodedIDToken = async (): Promise => { - return _authenticationHelper.getDecodedIDToken(); + const getDecodedIdToken = async (): Promise => { + return _authenticationHelper.getDecodedIdToken(); }; - const getCryptoHelper = async (): Promise => { - return _authenticationHelper.getCryptoHelper(); + const getCrypto = async (): Promise => { + return _authenticationHelper.getCrypto(); }; const getDecodedIDPIDToken = async (): Promise => { return _authenticationHelper.getDecodedIDPIDToken(); }; - const getIDToken = async (): Promise => { - return _authenticationHelper.getIDToken(); + const getIdToken = async (): Promise => { + return _authenticationHelper.getIdToken(); }; - const getOIDCServiceEndpoints = async (): Promise => { - return _authenticationHelper.getOIDCServiceEndpoints(); + const getOpenIDProviderEndpoints = async (): Promise => { + return _authenticationHelper.getOpenIDProviderEndpoints(); }; const getAccessToken = (): Promise => { return _authenticationHelper.getAccessToken(); }; - const isAuthenticated = (): Promise => { - return _authenticationHelper.isAuthenticated(); + const isSignedIn = (): Promise => { + return _authenticationHelper.isSignedIn(); }; const setSessionState = async (sessionState: string): Promise => { @@ -203,8 +202,8 @@ export const WebWorkerCore = async ( return; }; - const updateConfig = async (config: Partial>): Promise => { - await _authenticationClient.updateConfig(config); + const reInitialize = async (config: Partial>): Promise => { + await _authenticationClient.reInitialize(config); return; }; @@ -217,21 +216,21 @@ export const WebWorkerCore = async ( disableHttpHandler, enableHttpHandler, getAccessToken, - getAuthorizationURL, - getBasicUserInfo, + getSignInUrl, + getUser, getConfigData, - getCryptoHelper, + getCrypto, getDecodedIDPIDToken, - getDecodedIDToken, - getIDToken, - getOIDCServiceEndpoints, - getSignOutURL, + getDecodedIdToken, + getIdToken, + getOpenIDProviderEndpoints, + getSignOutUrl, httpRequest, httpRequestAll, - isAuthenticated, + isSignedIn, refreshAccessToken, requestAccessToken, - requestCustomGrant, + exchangeToken, revokeAccessToken, setHttpRequestFinishCallback, setHttpRequestStartCallback, @@ -239,6 +238,6 @@ export const WebWorkerCore = async ( setSessionState, signOut, startAutoRefreshToken, - updateConfig, + reInitialize, }; }; diff --git a/packages/browser/src/__legacy__/worker/worker-receiver.ts b/packages/browser/src/__legacy__/worker/worker-receiver.ts index 094b52c9..ee7c8def 100644 --- a/packages/browser/src/__legacy__/worker/worker-receiver.ts +++ b/packages/browser/src/__legacy__/worker/worker-receiver.ts @@ -16,7 +16,7 @@ * under the License. */ -import {AsgardeoAuthClient, AsgardeoAuthException, AuthClientConfig, BasicUserInfo} from '@asgardeo/javascript'; +import {AsgardeoAuthClient, AsgardeoAuthException, AuthClientConfig, User} from '@asgardeo/javascript'; import {WebWorkerCore} from './worker-core'; import { DISABLE_HTTP_HANDLER, @@ -97,7 +97,7 @@ export const workerReceiver = ( break; case GET_AUTH_URL: webWorker - .getAuthorizationURL(data?.data) + .getSignInUrl(data?.data) .then((response: AuthorizationResponse) => { port.postMessage(MessageUtils.generateSuccessMessage(response)); }) @@ -109,7 +109,7 @@ export const workerReceiver = ( case REQUEST_ACCESS_TOKEN: webWorker .requestAccessToken(data?.data?.code, data?.data?.sessionState, data?.data?.pkce, data?.data?.state) - .then((response: BasicUserInfo) => { + .then((response: User) => { port.postMessage(MessageUtils.generateSuccessMessage(response)); }) .catch(error => { @@ -162,7 +162,7 @@ export const workerReceiver = ( break; case REQUEST_CUSTOM_GRANT: webWorker - .requestCustomGrant(data.data) + .exchangeToken(data.data) .then(response => { port.postMessage(MessageUtils.generateSuccessMessage(response)); }) @@ -183,7 +183,7 @@ export const workerReceiver = ( break; case GET_OIDC_SERVICE_ENDPOINTS: try { - port.postMessage(MessageUtils.generateSuccessMessage(await webWorker.getOIDCServiceEndpoints())); + port.postMessage(MessageUtils.generateSuccessMessage(await webWorker.getOpenIDProviderEndpoints())); } catch (error) { port.postMessage(MessageUtils.generateFailureMessage(error)); } @@ -191,7 +191,7 @@ export const workerReceiver = ( break; case GET_BASIC_USER_INFO: try { - port.postMessage(MessageUtils.generateSuccessMessage(await webWorker.getBasicUserInfo())); + port.postMessage(MessageUtils.generateSuccessMessage(await webWorker.getUser())); } catch (error) { port.postMessage(MessageUtils.generateFailureMessage(error)); } @@ -199,7 +199,7 @@ export const workerReceiver = ( break; case GET_DECODED_ID_TOKEN: try { - port.postMessage(MessageUtils.generateSuccessMessage(await webWorker.getDecodedIDToken())); + port.postMessage(MessageUtils.generateSuccessMessage(await webWorker.getDecodedIdToken())); } catch (error) { port.postMessage(MessageUtils.generateFailureMessage(error)); } @@ -207,7 +207,7 @@ export const workerReceiver = ( break; case GET_CRYPTO_HELPER: try { - port.postMessage(MessageUtils.generateSuccessMessage(await webWorker.getCryptoHelper())); + port.postMessage(MessageUtils.generateSuccessMessage(await webWorker.getCrypto())); } catch (error) { port.postMessage(MessageUtils.generateFailureMessage(error)); } @@ -215,7 +215,7 @@ export const workerReceiver = ( break; case GET_ID_TOKEN: try { - port.postMessage(MessageUtils.generateSuccessMessage(await webWorker.getIDToken())); + port.postMessage(MessageUtils.generateSuccessMessage(await webWorker.getIdToken())); } catch (error) { port.postMessage(MessageUtils.generateFailureMessage(error)); } @@ -233,7 +233,7 @@ export const workerReceiver = ( break; case IS_AUTHENTICATED: try { - port.postMessage(MessageUtils.generateSuccessMessage(await webWorker.isAuthenticated())); + port.postMessage(MessageUtils.generateSuccessMessage(await webWorker.isSignedIn())); } catch (error) { port.postMessage(MessageUtils.generateFailureMessage(error)); } @@ -241,7 +241,7 @@ export const workerReceiver = ( break; case GET_SIGN_OUT_URL: try { - port.postMessage(MessageUtils.generateSuccessMessage(await webWorker.getSignOutURL())); + port.postMessage(MessageUtils.generateSuccessMessage(await webWorker.getSignOutUrl())); } catch (error) { port.postMessage(MessageUtils.generateFailureMessage(error)); } @@ -273,7 +273,7 @@ export const workerReceiver = ( break; case UPDATE_CONFIG: try { - port.postMessage(MessageUtils.generateSuccessMessage(await webWorker.updateConfig(data?.data))); + port.postMessage(MessageUtils.generateSuccessMessage(await webWorker.reInitialize(data?.data))); } catch (error) { port.postMessage(MessageUtils.generateFailureMessage(error)); } diff --git a/packages/express/.editorconfig b/packages/express/.editorconfig new file mode 100644 index 00000000..1b3ce07d --- /dev/null +++ b/packages/express/.editorconfig @@ -0,0 +1 @@ +../../.editorconfig \ No newline at end of file diff --git a/packages/express/.eslintignore b/packages/express/.eslintignore new file mode 100644 index 00000000..177586b6 --- /dev/null +++ b/packages/express/.eslintignore @@ -0,0 +1,4 @@ +/dist +/build +/node_modules +/coverage \ No newline at end of file diff --git a/packages/node/src/__tests__/greet.test.ts b/packages/express/.eslintrc.cjs similarity index 62% rename from packages/node/src/__tests__/greet.test.ts rename to packages/express/.eslintrc.cjs index c3ad90ee..2676ca81 100644 --- a/packages/node/src/__tests__/greet.test.ts +++ b/packages/express/.eslintrc.cjs @@ -16,11 +16,23 @@ * under the License. */ -import {describe, expect, it} from 'vitest'; -import greet from '../greet'; +const path = require('path'); -describe('greet', () => { - it('should return the proper greeting', () => { - expect(greet('World')).toBe('Hello, World!'); - }); -}); +module.exports = { + env: { + es6: true, + node: true, + }, + extends: [ + 'plugin:@wso2/typescript', + 'plugin:@wso2/strict', + 'plugin:@wso2/internal', + 'plugin:@wso2/jest', + 'plugin:@wso2/prettier', + ], + parserOptions: { + ecmaVersion: 2018, + project: [path.resolve(__dirname, 'tsconfig.eslint.json')], + }, + plugins: ['@wso2'], +}; diff --git a/packages/express/.gitignore b/packages/express/.gitignore new file mode 100644 index 00000000..c6bba591 --- /dev/null +++ b/packages/express/.gitignore @@ -0,0 +1,130 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* diff --git a/packages/express/.prettierignore b/packages/express/.prettierignore new file mode 100644 index 00000000..99b0b518 --- /dev/null +++ b/packages/express/.prettierignore @@ -0,0 +1,4 @@ +/dist +/build +/node_modules +/coverage diff --git a/packages/express/README.md b/packages/express/README.md new file mode 100644 index 00000000..04f1fb77 --- /dev/null +++ b/packages/express/README.md @@ -0,0 +1,72 @@ +

+

@asgardeo/express

+

+

Express.js SDK for Asgardeo

+
+ npm (scoped) + npm + License +
+ +## Installation + +```bash +# Using npm +npm install @asgardeo/express + +# or using pnpm +pnpm add @asgardeo/express + +# or using yarn +yarn add @asgardeo/express +``` + +## Quick Start + +```javascript +import { AsgardeoExpressClient } from "@asgardeo/express"; + +// Initialize the client +const authClient = new AsgardeoExpressClient({ + clientId: "", + clientSecret: "", + baseUrl: "https://api.asgardeo.io/t/", + callbackURL: "http://localhost:3000/callback" +}); + +// Example Express.js integration +import express from "express"; +const app = express(); + +// Login endpoint +app.get("/login", (req, res) => { + const authUrl = authClient.getSignInUrl(); + res.redirect(authUrl); +}); + +// Callback handler +app.get("/callback", async (req, res) => { + try { + const { code } = req.query; + const tokens = await authClient.exchangeAuthorizationCode(code); + // Store tokens and redirect to home page + res.redirect("/"); + } catch (error) { + res.status(500).send("Authentication failed"); + } +}); + +// Get user info +app.get("/userinfo", async (req, res) => { + try { + const userInfo = await authClient.getUserInfo(); + res.json(userInfo); + } catch (error) { + res.status(401).send("Unauthorized"); + } +}); +``` + +## License + +Apache-2.0 diff --git a/packages/express/esbuild.config.mjs b/packages/express/esbuild.config.mjs new file mode 100644 index 00000000..75bd1160 --- /dev/null +++ b/packages/express/esbuild.config.mjs @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {readFileSync} from 'fs'; +import * as esbuild from 'esbuild'; + +const pkg = JSON.parse(readFileSync('./package.json', 'utf8')); + +const commonOptions = { + bundle: true, + entryPoints: ['src/index.ts'], + external: [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})], + platform: 'node', + target: ['es2020'], +}; + +await esbuild.build({ + ...commonOptions, + format: 'esm', + outfile: 'dist/index.js', + sourcemap: true, +}); + +await esbuild.build({ + ...commonOptions, + format: 'cjs', + outfile: 'dist/cjs/index.js', + sourcemap: true, +}); diff --git a/packages/express/package.json b/packages/express/package.json new file mode 100644 index 00000000..cd6f5bf9 --- /dev/null +++ b/packages/express/package.json @@ -0,0 +1,65 @@ +{ + "name": "@asgardeo/express", + "version": "0.0.0", + "description": "Express.js implementation of Asgardeo JavaScript SDK.", + "keywords": [ + "asgardeo", + "express.js", + "server" + ], + "homepage": "https://github.com/asgardeo/web-ui-sdks/tree/main/packages/express#readme", + "bugs": { + "url": "https://github.com/asgardeo/web-ui-sdks/issues" + }, + "author": "WSO2", + "license": "Apache-2.0", + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "commonjs": "dist/cjs/index.js", + "exports": { + "import": "./dist/index.js", + "require": "./dist/cjs/index.js" + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "types": "dist/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/asgardeo/web-ui-sdks", + "directory": "packages/express" + }, + "scripts": { + "build": "pnpm clean && node esbuild.config.mjs && tsc -p tsconfig.lib.json --emitDeclarationOnly --outDir dist", + "clean": "rimraf dist", + "fix:lint": "eslint . --ext .js,.jsx,.ts,.tsx,.cjs,.mjs", + "lint": "eslint . --ext .js,.jsx,.ts,.tsx,.cjs,.mjs", + "test": "vitest", + "typecheck": "tsc -p tsconfig.lib.json" + }, + "devDependencies": { + "@types/node": "^22.15.3", + "@wso2/eslint-plugin": "catalog:", + "@wso2/prettier-config": "catalog:", + "esbuild": "^0.25.4", + "eslint": "8.57.0", + "express": "^5.1.0", + "prettier": "^2.6.2", + "rimraf": "^6.0.1", + "typescript": "~5.7.2", + "vitest": "^3.1.3" + }, + "dependencies": { + "@asgardeo/node": "workspace:^", + "uuid": "^11.1.0" + }, + "peerDependencies": { + "express": ">=4" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/javascript/src/__legacy__/data/index.ts b/packages/express/prettier.config.cjs similarity index 84% rename from packages/javascript/src/__legacy__/data/index.ts rename to packages/express/prettier.config.cjs index a91c45dc..929b9b15 100644 --- a/packages/javascript/src/__legacy__/data/index.ts +++ b/packages/express/prettier.config.cjs @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020, WSO2 LLC. (https://www.wso2.com). All Rights Reserved. + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -16,4 +16,4 @@ * under the License. */ -export * from "./data-layer"; +module.exports = require('@wso2/prettier-config'); diff --git a/packages/express/src/AsgardeoExpressClient.ts b/packages/express/src/AsgardeoExpressClient.ts new file mode 100644 index 00000000..61b0c9c7 --- /dev/null +++ b/packages/express/src/AsgardeoExpressClient.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {LegacyAsgardeoNodeClient, SignOutOptions} from '@asgardeo/node'; +import {AsgardeoExpressConfig} from './models/config'; + +/** + * Base class for implementing Asgardeo in Express.js based applications. + * This class provides the core functionality for managing user authentication and sessions. + * + * @typeParam T - Configuration type that extends AsgardeoExpressConfig. + */ +abstract class AsgardeoExpressClient extends LegacyAsgardeoNodeClient {} + +export default AsgardeoExpressClient; diff --git a/packages/express/src/__legacy__/client.ts b/packages/express/src/__legacy__/client.ts new file mode 100644 index 00000000..ebd79702 --- /dev/null +++ b/packages/express/src/__legacy__/client.ts @@ -0,0 +1,240 @@ +/** + * Copyright (c) 2022, WSO2 Inc. (http://www.wso2.com) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + LegacyAsgardeoNodeClient, + AuthClientConfig, + AuthURLCallback, + TokenResponse, + Storage, + User, + OIDCEndpoints, + IdTokenPayload, + TokenExchangeRequestConfig, + AsgardeoAuthException, + Logger, +} from '@asgardeo/node'; +import {CookieConfig, DEFAULT_LOGIN_PATH, DEFAULT_LOGOUT_PATH} from './constants'; +import {ExpressClientConfig, UnauthenticatedCallback} from './models'; +import express from 'express'; +import {v4 as uuidv4} from 'uuid'; +import {asgardeoExpressAuth, protectRoute} from './middleware'; +import {ExpressUtils} from './utils/express-utils'; + +export class AsgardeoExpressClient { + private _authClient: LegacyAsgardeoNodeClient; + private _storage?: Storage; + private static _clientConfig: ExpressClientConfig; + + private static _instance: AsgardeoExpressClient; + + private constructor(config: ExpressClientConfig, storage?: Storage) { + //Set the client config + AsgardeoExpressClient._clientConfig = {...config}; + + //Add the afterSignInUrl and afterSignOutUrl + //Add custom paths if the user has already declared any or else use the defaults + const nodeClientConfig: AuthClientConfig = { + ...config, + afterSignInUrl: config.appURL + (config.loginPath || DEFAULT_LOGIN_PATH), + afterSignOutUrl: config.appURL + (config.logoutPath || DEFAULT_LOGOUT_PATH), + }; + + //Initialize the user provided storage if there is any + if (storage) { + Logger.debug('Initializing user provided storage'); + this._storage = storage; + } + + //Initialize the Auth Client + this._authClient = new LegacyAsgardeoNodeClient(); + this._authClient.initialize(nodeClientConfig, this._storage); + } + + public static getInstance(config: ExpressClientConfig, storage?: Storage): AsgardeoExpressClient; + public static getInstance(): AsgardeoExpressClient; + public static getInstance(config?: ExpressClientConfig, storage?: Storage): AsgardeoExpressClient { + //Create a new instance if its not instantiated already + if (!AsgardeoExpressClient._instance && config) { + AsgardeoExpressClient._instance = new AsgardeoExpressClient(config, storage); + Logger.debug('Initialized AsgardeoExpressClient successfully'); + } + + if (!AsgardeoExpressClient._instance && !config) { + throw Error( + new AsgardeoAuthException( + 'EXPRESS-CLIENT-GI1-NF01', + 'User configuration is not found', + 'User config has not been passed to initialize AsgardeoExpressClient', + ).toString(), + ); + } + + return AsgardeoExpressClient._instance; + } + + public async signIn( + req: express.Request, + res: express.Response, + next: express.nextFunction, + signInConfig?: Record, + ): Promise { + if (ExpressUtils.hasErrorInURL(req.originalUrl)) { + return Promise.reject( + new AsgardeoAuthException( + 'EXPRESS-CLIENT-SI-IV01', + 'Invalid login request URL', + 'Login request contains an error query parameter in the URL', + ), + ); + } + + //Check if the user has a valid user ID and if not create one + let userId = req.cookies.ASGARDEO_SESSION_ID; + if (!userId) { + userId = uuidv4(); + } + + //Handle signIn() callback + const authRedirectCallback = (url: string) => { + if (url) { + //DEBUG + Logger.debug('Redirecting to: ' + url); + res.cookie('ASGARDEO_SESSION_ID', userId, { + maxAge: AsgardeoExpressClient._clientConfig.cookieConfig?.maxAge + ? AsgardeoExpressClient._clientConfig.cookieConfig.maxAge + : CookieConfig.defaultMaxAge, + httpOnly: AsgardeoExpressClient._clientConfig.cookieConfig?.httpOnly ?? CookieConfig.defaultHttpOnly, + sameSite: AsgardeoExpressClient._clientConfig.cookieConfig?.sameSite ?? CookieConfig.defaultSameSite, + secure: AsgardeoExpressClient._clientConfig.cookieConfig?.secure ?? CookieConfig.defaultSecure, + }); + res.redirect(url); + + next && typeof next === 'function' && next(); + } + }; + + const authResponse: TokenResponse = await this._authClient.signIn( + authRedirectCallback, + userId, + req.query.code, + req.query.session_state, + req.query.state, + signInConfig, + ); + + if (authResponse.accessToken || authResponse.idToken) { + return authResponse; + } else { + return { + accessToken: '', + createdAt: 0, + expiresIn: '', + idToken: '', + refreshToken: '', + scope: '', + tokenType: '', + }; + } + } + + public async signOut(userId: string): Promise { + return this._authClient.signOut(userId); + } + + public async isSignedIn(userId: string): Promise { + return this._authClient.isSignedIn(userId); + } + + public async getIdToken(userId: string): Promise { + return this._authClient.getIdToken(userId); + } + + public async getUser(userId: string): Promise { + return this._authClient.getUser(userId); + } + + public async getOpenIDProviderEndpoints(): Promise { + return this._authClient.getOpenIDProviderEndpoints(); + } + + public async getDecodedIdToken(userId?: string): Promise { + return this._authClient.getDecodedIdToken(userId); + } + + public async getAccessToken(userId?: string): Promise { + return this._authClient.getAccessToken(userId); + } + + public async exchangeToken( + config: TokenExchangeRequestConfig, + userId?: string, + ): Promise { + return this._authClient.exchangeToken(config, userId); + } + + public async reInitialize(config: Partial): Promise { + return this._authClient.reInitialize(config); + } + + public async revokeAccessToken(userId?: string): Promise { + return this._authClient.revokeAccessToken(userId); + } + + public static didSignOutFail(afterSignOutUrl: string): boolean { + return LegacyAsgardeoNodeClient.didSignOutFail(afterSignOutUrl); + } + + public static isSignOutSuccessful(afterSignOutUrl: string): boolean { + return LegacyAsgardeoNodeClient.isSignOutSuccessful(afterSignOutUrl); + } + + public static protectRoute( + callback: UnauthenticatedCallback, + ): (req: express.Request, res: express.Response, next: express.nextFunction) => Promise { + if (!this._instance) { + throw new AsgardeoAuthException( + 'EXPRESS-CLIENT-PR-NF01', + 'AsgardeoExpressClient is not instantiated', + 'Create an instance of AsgardeoExpressClient before using calling this method.', + ); + } + + return protectRoute(this._instance, callback); + } + + public static asgardeoExpressAuth( + onSignIn: (response: TokenResponse) => void, + onSignOut: () => void, + onError: (exception: AsgardeoAuthException) => void, + ): any { + if (!this._instance) { + throw new AsgardeoAuthException( + 'EXPRESS-CLIENT-AEA-NF01', + 'AsgardeoExpressClient is not instantiated', + 'Create an instance of AsgardeoExpressClient before using calling this method.', + ); + } + + return asgardeoExpressAuth(this._instance, AsgardeoExpressClient._clientConfig, onSignIn, onSignOut, onError); + } + + public async getStorageManager() { + return this._authClient.getStorageManager(); + } +} diff --git a/packages/express/src/__legacy__/constants/default-options.ts b/packages/express/src/__legacy__/constants/default-options.ts new file mode 100644 index 00000000..799cb25b --- /dev/null +++ b/packages/express/src/__legacy__/constants/default-options.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2022, WSO2 Inc. (http://www.wso2.com) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export enum CookieConfig { + defaultMaxAge = 90000, + defaultHttpOnly = 'true', + defaultSameSite = 'lax', + defaultSecure = 'false' +} + +export const DEFAULT_LOGIN_PATH = "/login"; + +export const DEFAULT_LOGOUT_PATH = "/logout"; diff --git a/packages/express/src/__legacy__/constants/index.ts b/packages/express/src/__legacy__/constants/index.ts new file mode 100644 index 00000000..565d702c --- /dev/null +++ b/packages/express/src/__legacy__/constants/index.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2022, WSO2 Inc. (http://www.wso2.com) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from "./default-options"; +export * from "./logger-config"; diff --git a/packages/javascript/src/__legacy__/models/custom-grant.ts b/packages/express/src/__legacy__/constants/logger-config.ts similarity index 59% rename from packages/javascript/src/__legacy__/models/custom-grant.ts rename to packages/express/src/__legacy__/constants/logger-config.ts index aedbf25d..62f6ad26 100644 --- a/packages/javascript/src/__legacy__/models/custom-grant.ts +++ b/packages/express/src/__legacy__/constants/logger-config.ts @@ -1,7 +1,7 @@ /** - * Copyright (c) 2020, WSO2 LLC. (https://www.wso2.com). All Rights Reserved. + * Copyright (c) 2022, WSO2 Inc. (http://www.wso2.com) All Rights Reserved. * - * WSO2 LLC. licenses this file to you under the Apache License, + * WSO2 Inc. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except * in compliance with the License. * You may obtain a copy of the License at @@ -16,12 +16,13 @@ * under the License. */ -export interface CustomGrantConfig{ - id: string; - data: any; - signInRequired: boolean; - attachToken: boolean; - returnsSession: boolean; - tokenEndpoint?: string; - shouldReplayAfterRefresh?: boolean; +export const LOGGER_CONFIG = { + bgGreen: "\x1b[42m", + bgRed: "\x1b[41m", + bgYellow: "\x1b[43m", + fgBlack: "\x1b[30m", + fgGreen: "\x1b[32m", + fgRed: "\x1b[31m", + fgYellow: "\x1b[33m", + reset: "\x1b[0m" } diff --git a/packages/express/src/__legacy__/middleware/authentication.ts b/packages/express/src/__legacy__/middleware/authentication.ts new file mode 100644 index 00000000..cc4b7be8 --- /dev/null +++ b/packages/express/src/__legacy__/middleware/authentication.ts @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2022, WSO2 Inc. (http://www.wso2.com) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AsgardeoAuthException, Storage, TokenResponse, Logger } from "@asgardeo/node"; +import express from "express"; +import { AsgardeoExpressClient } from "../client"; +import { DEFAULT_LOGIN_PATH, DEFAULT_LOGOUT_PATH } from "../constants"; +import { ExpressClientConfig } from "../models"; + +export const asgardeoExpressAuth = ( + asgardeoExpressClient: AsgardeoExpressClient, + config: ExpressClientConfig, + onSignIn: (res: express.Response, tokenResponse: TokenResponse) => void, + onSignOut: (res: express.Response) => void, + onError: (res: express.Response, exception: AsgardeoAuthException) => void +): any => { + //Create the router + const router = new express.Router(); + + //Patch AuthClient to the request and the response + router.use(async (req: express.Request, res: express.Response, next: express.nextFunction): Promise => { + req.asgardeoAuth = asgardeoExpressClient; + res.asgardeoAuth = asgardeoExpressClient; + next(); + }); + + //Patch in '/login' route + router.get( + config.loginPath || DEFAULT_LOGIN_PATH, + async (req: express.Request, res: express.Response, next: express.nextFunction): Promise => { + try { + const response: TokenResponse = await asgardeoExpressClient.signIn(req, res, next, config.signInConfig); + if (response.accessToken || response.idToken) { + onSignIn(res, response); + } + } catch (e: any) { + Logger.error(e.message); + onError(res, e); + } + } + ); + + //Patch in '/logout' route + router.get( + config.logoutPath || DEFAULT_LOGOUT_PATH, + async (req: express.Request, res: express.Response, next: express.nextFunction): Promise => { + //Check if it is a logout success response + if (req.query.state === "sign_out_success") { + onSignOut(res); + + return; + } + + //Check if the cookie exists + if (req.cookies.ASGARDEO_SESSION_ID === undefined) { + onError( + res, + new AsgardeoAuthException( + "EXPRESS-AUTH_MW-LOGOUT-NF01", + "No cookie found in the request", + "No cookie was sent with the request. The user may not have signed in yet." + ) + ); + + return; + } else { + //Get the signout URL + try { + const signOutURL = await req.asgardeoAuth.signOut(req.cookies.ASGARDEO_SESSION_ID); + if (signOutURL) { + res.cookie("ASGARDEO_SESSION_ID", null, { maxAge: 0 }); + res.redirect(signOutURL); + + return; + } + } catch (e: any) { + onError(res, e); + + return; + } + } + } + ); + + return router; +}; diff --git a/packages/express/src/__legacy__/middleware/index.ts b/packages/express/src/__legacy__/middleware/index.ts new file mode 100644 index 00000000..d86cb6bc --- /dev/null +++ b/packages/express/src/__legacy__/middleware/index.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2022, WSO2 Inc. (http://www.wso2.com) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from "./protect-route"; +export * from "./authentication"; diff --git a/packages/express/src/__legacy__/middleware/protect-route.ts b/packages/express/src/__legacy__/middleware/protect-route.ts new file mode 100644 index 00000000..545e0d59 --- /dev/null +++ b/packages/express/src/__legacy__/middleware/protect-route.ts @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2022, WSO2 Inc. (http://www.wso2.com) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import express from 'express'; +import {AsgardeoExpressClient} from '../client'; +import {UnauthenticatedCallback} from '../models'; +import {Logger} from '@asgardeo/node'; + +export const protectRoute = ( + asgardeoExpressClient: AsgardeoExpressClient, + callback: UnauthenticatedCallback, +): ((req: express.Request, res: express.Response, next: express.nextFunction) => Promise) => { + return async (req: express.Request, res: express.Response, next: express.nextFunction): Promise => { + if (req.cookies.ASGARDEO_SESSION_ID === undefined) { + Logger.error('No session ID found in the request cookies'); + + if (callback(res, 'Unauthenticated')) { + return; + } + + return next(); + } else { + //validate the cookie + const isCookieValid = await asgardeoExpressClient.isSignedIn(req.cookies.ASGARDEO_SESSION_ID); + if (isCookieValid) { + return next(); + } else { + Logger.error('Invalid session ID found in the request cookies'); + if (callback(res, 'Invalid session cookie')) { + return; + } + + return next(); + } + } + }; +}; diff --git a/packages/express/src/__legacy__/models/client-config.ts b/packages/express/src/__legacy__/models/client-config.ts new file mode 100644 index 00000000..053512c0 --- /dev/null +++ b/packages/express/src/__legacy__/models/client-config.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2022, WSO2 Inc. (http://www.wso2.com) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {AuthClientConfig} from '@asgardeo/node'; + +export interface StrictExpressClientConfig { + appURL: string; + cookieConfig?: { + maxAge?: number; + httpOnly?: boolean; + sameSite?: string; + secure?: boolean; + }; + globalAuth?: boolean; + loginPath?: string; + logoutPath?: string; + signInConfig?: Record; +} + +export type ExpressClientConfig = Exclude & + StrictExpressClientConfig; diff --git a/packages/express/src/__legacy__/models/data.ts b/packages/express/src/__legacy__/models/data.ts new file mode 100644 index 00000000..8b071cec --- /dev/null +++ b/packages/express/src/__legacy__/models/data.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2022, WSO2 Inc. (http://www.wso2.com) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export interface AuthURL { + url: string +} diff --git a/packages/express/src/__legacy__/models/index.ts b/packages/express/src/__legacy__/models/index.ts new file mode 100644 index 00000000..ba80e657 --- /dev/null +++ b/packages/express/src/__legacy__/models/index.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2022, WSO2 Inc. (http://www.wso2.com) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './data'; +export * from "./client-config"; +export * from "./protect-route"; diff --git a/packages/express/src/__legacy__/models/protect-route.ts b/packages/express/src/__legacy__/models/protect-route.ts new file mode 100644 index 00000000..789c6031 --- /dev/null +++ b/packages/express/src/__legacy__/models/protect-route.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2022, WSO2 Inc. (http://www.wso2.com) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import express from "express"; + +export type UnauthenticatedCallback = (res: express.Response, error: string) => boolean; diff --git a/packages/express/src/__legacy__/utils/express-utils.ts b/packages/express/src/__legacy__/utils/express-utils.ts new file mode 100644 index 00000000..a129d9de --- /dev/null +++ b/packages/express/src/__legacy__/utils/express-utils.ts @@ -0,0 +1,16 @@ +export class ExpressUtils { + + private static readonly AUTH_CODE_REGEXP: RegExp = /[?&]error=[^&]+/; + + /** + * Util function to check if the URL contains an error. + * + * @param url - URL to be checked. + * + * @returns {boolean} - True if the URL contains an error. + */ + public static hasErrorInURL(url: string): boolean { + + return this.AUTH_CODE_REGEXP.test(url); + } +} diff --git a/packages/express/src/index.ts b/packages/express/src/index.ts new file mode 100644 index 00000000..b1b4b03f --- /dev/null +++ b/packages/express/src/index.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022, WSO2 Inc. (http://www.wso2.com) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from "./__legacy__/models"; +export * from "./__legacy__/client"; + +export * from '@asgardeo/node'; diff --git a/packages/express/src/models/config.ts b/packages/express/src/models/config.ts new file mode 100644 index 00000000..33b0e154 --- /dev/null +++ b/packages/express/src/models/config.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {AsgardeoNodeConfig} from '@asgardeo/node'; + +/** + * Configuration type for the Asgardeo Express.js SDK. + * Extends the base Config type from @asgardeo/node with Express.js specific settings. + * + * @remarks + * This type is used to configure the Express.js SDK with settings like: + * - Server endpoints + * - Authentication parameters + * - Session management options + */ +export type AsgardeoExpressConfig = AsgardeoNodeConfig; diff --git a/packages/express/tsconfig.eslint.json b/packages/express/tsconfig.eslint.json new file mode 100644 index 00000000..23fadc26 --- /dev/null +++ b/packages/express/tsconfig.eslint.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "**/.*.js", + "**/.*.cjs", + "**/.*.ts", + "**/*.js", + "**/*.cjs", + "**/*.ts", + ] +} diff --git a/packages/express/tsconfig.json b/packages/express/tsconfig.json new file mode 100644 index 00000000..848c2706 --- /dev/null +++ b/packages/express/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "declaration": false, + "emitDecoratorMetadata": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "importHelpers": true, + "lib": ["ESNext"], + "module": "ESNext", + "moduleResolution": "node", + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "sourceMap": true, + "target": "ESNext", + "forceConsistentCasingInFileNames": true, + "strict": false, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "exclude": ["node_modules", "tmp", "dist"], + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/express/tsconfig.lib.json b/packages/express/tsconfig.lib.json new file mode 100644 index 00000000..b2c9c1ae --- /dev/null +++ b/packages/express/tsconfig.lib.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "dist", + "declarationDir": "dist", + "types": ["node"] + }, + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.ts", "types/**/*.d.ts"] +} diff --git a/packages/express/tsconfig.spec.json b/packages/express/tsconfig.spec.json new file mode 100644 index 00000000..46a76e28 --- /dev/null +++ b/packages/express/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "test-configs", + "jest.config.js", + "**/*.test.ts", + "**/*.spec.ts", + "**/*.test.js", + "**/*.spec.js", + "**/*.d.ts" + ] +} diff --git a/packages/javascript/src/__legacy__/core/index.ts b/packages/express/vitest.config.ts similarity index 80% rename from packages/javascript/src/__legacy__/core/index.ts rename to packages/express/vitest.config.ts index 88629484..29a917d1 100644 --- a/packages/javascript/src/__legacy__/core/index.ts +++ b/packages/express/vitest.config.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020, WSO2 LLC. (https://www.wso2.com). All Rights Reserved. + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -16,4 +16,8 @@ * under the License. */ -export * from "./authentication-core"; +import {defineConfig} from 'vitest/config'; + +export default defineConfig({ + test: {}, +}); diff --git a/packages/javascript/README.md b/packages/javascript/README.md index 5236fad9..13be756d 100644 --- a/packages/javascript/README.md +++ b/packages/javascript/README.md @@ -28,8 +28,8 @@ import { AsgardeoAuth } from "@asgardeo/javascript"; // Initialize the auth instance const auth = new AsgardeoAuth({ - signInRedirectURL: "https://localhost:3000", - clientID: "", + afterSignInUrl: "https://localhost:3000", + clientId: "", baseUrl: "https://api.asgardeo.io/t/" }); @@ -43,7 +43,7 @@ auth.signIn() }); // Get authenticated user -auth.getBasicUserInfo() +auth.getUser() .then((userInfo) => { console.log(userInfo); }); diff --git a/packages/javascript/package.json b/packages/javascript/package.json index 69d4a881..7af770e7 100644 --- a/packages/javascript/package.json +++ b/packages/javascript/package.json @@ -1,6 +1,6 @@ { "name": "@asgardeo/javascript", - "version": "0.0.0", + "version": "0.0.1", "description": "Framework agnostic JavaScript SDK for Asgardeo.", "keywords": [ "asgardeo", @@ -9,9 +9,9 @@ "agnostic", "js" ], - "homepage": "https://github.com/asgardeo/javascript/tree/main/packages/javascript#readme", + "homepage": "https://github.com/asgardeo/web-ui-sdks/tree/main/packages/javascript#readme", "bugs": { - "url": "https://github.com/asgardeo/javascript/issues" + "url": "https://github.com/asgardeo/web-ui-sdks/issues" }, "author": "WSO2", "license": "Apache-2.0", @@ -30,7 +30,7 @@ "types": "dist/index.d.ts", "repository": { "type": "git", - "url": "https://github.com/asgardeo/javascript", + "url": "https://github.com/asgardeo/web-ui-sdks", "directory": "packages/javascript" }, "scripts": { @@ -58,4 +58,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/packages/javascript/src/AsgardeoJavaScriptClient.ts b/packages/javascript/src/AsgardeoJavaScriptClient.ts index d1343301..5d46f7b1 100644 --- a/packages/javascript/src/AsgardeoJavaScriptClient.ts +++ b/packages/javascript/src/AsgardeoJavaScriptClient.ts @@ -17,7 +17,7 @@ */ import {AsgardeoClient, SignInOptions, SignOutOptions} from './models/client'; -import {User} from './models/user'; +import {User, UserProfile} from './models/user'; import {Config} from './models/config'; /** @@ -42,6 +42,8 @@ abstract class AsgardeoJavaScriptClient implements AsgardeoClient */ abstract getUser(): Promise; + abstract getUserProfile(): Promise; + /** * Checks if the client is currently loading. * This can be used to determine if the client is in the process of initializing or fetching user data. diff --git a/packages/javascript/src/IsomorphicCrypto.ts b/packages/javascript/src/IsomorphicCrypto.ts index 6cab9808..09cfc567 100644 --- a/packages/javascript/src/IsomorphicCrypto.ts +++ b/packages/javascript/src/IsomorphicCrypto.ts @@ -16,9 +16,9 @@ * under the License. */ -import {AsgardeoAuthException} from './__legacy__/exception'; +import {AsgardeoAuthException} from './errors/exception'; import {Crypto, JWKInterface} from './models/crypto'; -import {IdTokenPayload} from './models/id-token'; +import {IdTokenPayload} from './models/token'; import TokenConstants from './constants/TokenConstants'; export class IsomorphicCrypto { @@ -83,7 +83,7 @@ export class IsomorphicCrypto { * * @param idToken - id_token received from the IdP. * @param jwk - public key used for signing. - * @param clientID - app identification. + * @param clientId - app identification. * @param issuer - id_token issuer. * @param username - Username. * @param clockTolerance - Allowed leeway for id_tokens (in seconds). @@ -95,7 +95,7 @@ export class IsomorphicCrypto { public isValidIdToken( idToken: string, jwk: JWKInterface, - clientID: string, + clientId: string, issuer: string, username: string, clockTolerance: number | undefined, @@ -106,7 +106,7 @@ export class IsomorphicCrypto { idToken, jwk, TokenConstants.SignatureValidation.SUPPORTED_ALGORITHMS as unknown as string[], - clientID, + clientId, issuer, username, clockTolerance, @@ -136,9 +136,9 @@ export class IsomorphicCrypto { * * @throws */ - public decodeIDToken(idToken: string): IdTokenPayload { + public decodeIdToken(idToken: string): IdTokenPayload { try { - const utf8String: string = this._cryptoUtils.base64URLDecode(idToken.split('.')[1]); + const utf8String: string = this._cryptoUtils.base64URLDecode(idToken?.split('.')[1]); const payload: IdTokenPayload = JSON.parse(utf8String); return payload; diff --git a/packages/javascript/src/__legacy__/data/data-layer.ts b/packages/javascript/src/StorageManager.ts similarity index 81% rename from packages/javascript/src/__legacy__/data/data-layer.ts rename to packages/javascript/src/StorageManager.ts index c7729255..50f8df88 100644 --- a/packages/javascript/src/__legacy__/data/data-layer.ts +++ b/packages/javascript/src/StorageManager.ts @@ -16,20 +16,21 @@ * under the License. */ -import {Stores} from '../../models/store'; -import {Store} from '../../models/store'; -import {AuthClientConfig, SessionData} from '../models'; -import {TemporaryStore, TemporaryStoreValue} from '../../models/store'; -import {OIDCDiscoveryApiResponse} from '../../models/oidc-discovery'; +import {Stores} from './models/store'; +import {Storage} from './models/store'; +import {AuthClientConfig} from './__legacy__/models'; +import {SessionData} from './models/session'; +import {TemporaryStore, TemporaryStoreValue} from './models/store'; +import {OIDCDiscoveryApiResponse} from './models/oidc-discovery'; type PartialData = Partial | OIDCDiscoveryApiResponse | SessionData | TemporaryStore>; export const ASGARDEO_SESSION_ACTIVE: string = 'asgardeo-session-active'; -export class DataLayer { +class StorageManager { protected _id: string; - protected _store: Store; - public constructor(instanceID: string, store: Store) { + protected _store: Storage; + public constructor(instanceID: string, store: Storage) { this._id = instanceID; this._store = store; } @@ -74,8 +75,8 @@ export class DataLayer { await this._store.setData(key, dataToBeSavedJSON); } - protected _resolveKey(store: Stores | string, userID?: string): string { - return userID ? `${store}-${this._id}-${userID}` : `${store}-${this._id}`; + protected _resolveKey(store: Stores | string, userId?: string): string { + return userId ? `${store}-${this._id}-${userId}` : `${store}-${this._id}`; } protected isLocalStorageAvailable(): boolean { @@ -99,36 +100,36 @@ export class DataLayer { this.setDataInBulk(this._resolveKey(Stores.OIDCProviderMetaData), oidcProviderMetaData); } - public async setTemporaryData(temporaryData: Partial, userID?: string): Promise { - this.setDataInBulk(this._resolveKey(Stores.TemporaryData, userID), temporaryData); + public async setTemporaryData(temporaryData: Partial, userId?: string): Promise { + this.setDataInBulk(this._resolveKey(Stores.TemporaryData, userId), temporaryData); } - public async setSessionData(sessionData: Partial, userID?: string): Promise { - this.setDataInBulk(this._resolveKey(Stores.SessionData, userID), sessionData); + public async setSessionData(sessionData: Partial, userId?: string): Promise { + this.setDataInBulk(this._resolveKey(Stores.SessionData, userId), sessionData); } - public async setCustomData(key: string, customData: Partial, userID?: string): Promise { - this.setDataInBulk(this._resolveKey(key, userID), customData); + public async setCustomData(key: string, customData: Partial, userId?: string): Promise { + this.setDataInBulk(this._resolveKey(key, userId), customData); } public async getConfigData(): Promise> { return JSON.parse((await this._store.getData(this._resolveKey(Stores.ConfigData))) ?? null); } - public async getOIDCProviderMetaData(): Promise { + public async loadOpenIDProviderConfiguration(): Promise { return JSON.parse((await this._store.getData(this._resolveKey(Stores.OIDCProviderMetaData))) ?? null); } - public async getTemporaryData(userID?: string): Promise { - return JSON.parse((await this._store.getData(this._resolveKey(Stores.TemporaryData, userID))) ?? null); + public async getTemporaryData(userId?: string): Promise { + return JSON.parse((await this._store.getData(this._resolveKey(Stores.TemporaryData, userId))) ?? null); } - public async getSessionData(userID?: string): Promise { - return JSON.parse((await this._store.getData(this._resolveKey(Stores.SessionData, userID))) ?? null); + public async getSessionData(userId?: string): Promise { + return JSON.parse((await this._store.getData(this._resolveKey(Stores.SessionData, userId))) ?? null); } - public async getCustomData(key: string, userID?: string): Promise { - return JSON.parse((await this._store.getData(this._resolveKey(key, userID))) ?? null); + public async getCustomData(key: string, userId?: string): Promise { + return JSON.parse((await this._store.getData(this._resolveKey(key, userId))) ?? null); } public setSessionStatus(status: string): void { @@ -152,12 +153,12 @@ export class DataLayer { await this._store.removeData(this._resolveKey(Stores.OIDCProviderMetaData)); } - public async removeTemporaryData(userID?: string): Promise { - await this._store.removeData(this._resolveKey(Stores.TemporaryData, userID)); + public async removeTemporaryData(userId?: string): Promise { + await this._store.removeData(this._resolveKey(Stores.TemporaryData, userId)); } - public async removeSessionData(userID?: string): Promise { - await this._store.removeData(this._resolveKey(Stores.SessionData, userID)); + public async removeSessionData(userId?: string): Promise { + await this._store.removeData(this._resolveKey(Stores.SessionData, userId)); } public async getConfigDataParameter(key: keyof AuthClientConfig): Promise { @@ -172,14 +173,14 @@ export class DataLayer { return data && JSON.parse(data)[key]; } - public async getTemporaryDataParameter(key: keyof TemporaryStore, userID?: string): Promise { - const data: string = await this._store.getData(this._resolveKey(Stores.TemporaryData, userID)); + public async getTemporaryDataParameter(key: keyof TemporaryStore, userId?: string): Promise { + const data: string = await this._store.getData(this._resolveKey(Stores.TemporaryData, userId)); return data && JSON.parse(data)[key]; } - public async getSessionDataParameter(key: keyof SessionData, userID?: string): Promise { - const data: string = await this._store.getData(this._resolveKey(Stores.SessionData, userID)); + public async getSessionDataParameter(key: keyof SessionData, userId?: string): Promise { + const data: string = await this._store.getData(this._resolveKey(Stores.SessionData, userId)); return data && JSON.parse(data)[key]; } @@ -198,17 +199,17 @@ export class DataLayer { public async setTemporaryDataParameter( key: keyof TemporaryStore, value: TemporaryStoreValue, - userID?: string, + userId?: string, ): Promise { - await this.setValue(this._resolveKey(Stores.TemporaryData, userID), key, value); + await this.setValue(this._resolveKey(Stores.TemporaryData, userId), key, value); } public async setSessionDataParameter( key: keyof SessionData, value: TemporaryStoreValue, - userID?: string, + userId?: string, ): Promise { - await this.setValue(this._resolveKey(Stores.SessionData, userID), key, value); + await this.setValue(this._resolveKey(Stores.SessionData, userId), key, value); } public async removeConfigDataParameter(key: keyof AuthClientConfig): Promise { @@ -219,11 +220,13 @@ export class DataLayer { await this.removeValue(this._resolveKey(Stores.OIDCProviderMetaData), key); } - public async removeTemporaryDataParameter(key: keyof TemporaryStore, userID?: string): Promise { - await this.removeValue(this._resolveKey(Stores.TemporaryData, userID), key); + public async removeTemporaryDataParameter(key: keyof TemporaryStore, userId?: string): Promise { + await this.removeValue(this._resolveKey(Stores.TemporaryData, userId), key); } - public async removeSessionDataParameter(key: keyof SessionData, userID?: string): Promise { - await this.removeValue(this._resolveKey(Stores.SessionData, userID), key); + public async removeSessionDataParameter(key: keyof SessionData, userId?: string): Promise { + await this.removeValue(this._resolveKey(Stores.SessionData, userId), key); } } + +export default StorageManager; diff --git a/packages/javascript/src/__legacy__/client.ts b/packages/javascript/src/__legacy__/client.ts index 044b85e7..3de479fd 100644 --- a/packages/javascript/src/__legacy__/client.ts +++ b/packages/javascript/src/__legacy__/client.ts @@ -16,25 +16,30 @@ * under the License. */ -import {AuthenticationCore} from './core'; -import {DataLayer} from './data'; -import { - AuthClientConfig, - BasicUserInfo, - CustomGrantConfig, - FetchResponse, - GetAuthURLConfig, -} from './models'; +import StorageManager from '../StorageManager'; +import {AuthClientConfig, StrictAuthClientConfig} from './models'; +import {ExtendedAuthorizeRequestUrlParams} from '../models/oauth-request'; import {Crypto} from '../models/crypto'; -import {TokenResponse} from '../models/token'; -import {IdTokenPayload} from '../models/id-token'; +import {TokenResponse, IdTokenPayload, TokenExchangeRequestConfig} from '../models/token'; import {OIDCEndpoints} from '../models/oidc-endpoints'; -import {Store} from '../models/store'; -import {ResponseMode} from '../models/oauth-response'; +import {Storage} from '../models/store'; import ScopeConstants from '../constants/ScopeConstants'; import OIDCDiscoveryConstants from '../constants/OIDCDiscoveryConstants'; import OIDCRequestConstants from '../constants/OIDCRequestConstants'; -import { IsomorphicCrypto } from '../IsomorphicCrypto'; +import {IsomorphicCrypto} from '../IsomorphicCrypto'; +import extractPkceStorageKeyFromState from '../utils/extractPkceStorageKeyFromState'; +import generateStateParamForRequestCorrelation from '../utils/generateStateParamForRequestCorrelation'; +import {AsgardeoAuthException} from '../errors/exception'; +import {AuthenticationHelper} from './helpers'; +import {SessionData, UserSession} from '../models/session'; +import {AuthorizeRequestUrlParams} from '../models/oauth-request'; +import {TemporaryStore} from '../models/store'; +import generatePkceStorageKey from '../utils/generatePkceStorageKey'; +import {OIDCDiscoveryApiResponse} from '../models/oidc-discovery'; +import getAuthorizeRequestUrlParams from '../utils/getAuthorizeRequestUrlParams'; +import PKCEConstants from '../constants/PKCEConstants'; +import {User} from '../models/user'; +import processOpenIDScopes from '../utils/processOpenIDScopes'; /** * Default configurations. @@ -42,8 +47,7 @@ import { IsomorphicCrypto } from '../IsomorphicCrypto'; const DefaultConfig: Partial> = { clockTolerance: 300, enablePKCE: true, - responseMode: ResponseMode.Query, - scope: [ScopeConstants.OPENID], + responseMode: 'query', sendCookiesInRequests: true, validateIDToken: true, validateIDTokenIssuer: true, @@ -53,11 +57,18 @@ const DefaultConfig: Partial> = { * This class provides the necessary methods needed to implement authentication. */ export class AsgardeoAuthClient { - private _dataLayer!: DataLayer; - private _authenticationCore!: AuthenticationCore; + private _storageManager!: StorageManager; + private _config: () => Promise; + private _oidcProviderMetaData: () => Promise; + private _authenticationHelper: AuthenticationHelper; + private _cryptoUtils: Crypto; + private _cryptoHelper: IsomorphicCrypto; private static _instanceID: number; - static _authenticationCore: any; + + // FIXME: Validate this. + // Ref: https://github.com/asgardeo/asgardeo-auth-js-core/pull/205 + static _authenticationHelper: any; /** * This is the constructor method that returns an instance of the . @@ -84,8 +95,8 @@ export class AsgardeoAuthClient { * * @example * const config = \{ - * signInRedirectURL: "http://localhost:3000/sign-in", - * clientID: "client ID", + * afterSignInUrl: "http://localhost:3000/sign-in", + * clientId: "client ID", * baseUrl: "https://localhost:9443" * \} * @@ -97,11 +108,11 @@ export class AsgardeoAuthClient { */ public async initialize( config: AuthClientConfig, - store: Store, + store: Storage, cryptoUtils: Crypto, instanceID?: number, ): Promise { - const clientId: string = config.clientID; + const clientId: string = config.clientId; if (!AsgardeoAuthClient._instanceID) { AsgardeoAuthClient._instanceID = 0; @@ -114,40 +125,44 @@ export class AsgardeoAuthClient { } if (!clientId) { - this._dataLayer = new DataLayer(`instance_${AsgardeoAuthClient._instanceID}`, store); + this._storageManager = new StorageManager(`instance_${AsgardeoAuthClient._instanceID}`, store); } else { - this._dataLayer = new DataLayer(`instance_${AsgardeoAuthClient._instanceID}-${clientId}`, store); + this._storageManager = new StorageManager(`instance_${AsgardeoAuthClient._instanceID}-${clientId}`, store); } - this._authenticationCore = new AuthenticationCore(this._dataLayer, cryptoUtils); - AsgardeoAuthClient._authenticationCore = new AuthenticationCore(this._dataLayer, cryptoUtils); + this._cryptoUtils = cryptoUtils; + this._cryptoHelper = new IsomorphicCrypto(cryptoUtils); + this._authenticationHelper = new AuthenticationHelper(this._storageManager, this._cryptoHelper); + this._config = async () => await this._storageManager.getConfigData(); + this._oidcProviderMetaData = async () => await this._storageManager.loadOpenIDProviderConfiguration(); + + // FIXME: Validate this. + // Ref: https://github.com/asgardeo/asgardeo-auth-js-core/pull/205 + AsgardeoAuthClient._authenticationHelper = this._authenticationHelper; - await this._dataLayer.setConfigData({ + await this._storageManager.setConfigData({ ...DefaultConfig, ...config, - scope: [ - ...(DefaultConfig.scope ?? []), - ...(config.scope?.filter((scope: string) => !DefaultConfig?.scope?.includes(scope)) ?? []), - ], + scope: processOpenIDScopes(config.scopes), }); } /** - * This method returns the `DataLayer` object that allows you to access authentication data. + * This method returns the `StorageManager` object that allows you to access authentication data. * - * @returns - The `DataLayer` object. + * @returns - The `StorageManager` object. * * @example * ``` - * const data = auth.getDataLayer(); + * const data = auth.getStorageManager(); * ``` * - * {@link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getDataLayer} + * {@link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getStorageManager} * * @preserve */ - public getDataLayer(): DataLayer { - return this._dataLayer; + public getStorageManager(): StorageManager { + return this._storageManager; } /** @@ -157,94 +172,102 @@ export class AsgardeoAuthClient { * * @example * ``` - * const instanceId = auth.getInstanceID(); + * const instanceId = auth.getInstanceId(); * ``` * * @preserve */ - public getInstanceID(): number { + public getInstanceId(): number { return AsgardeoAuthClient._instanceID; } - /** - * This is an async method that returns a Promise that resolves with the authorization URL parameters. - * - * @param config - (Optional) A config object to force initialization and pass - * custom path parameters such as the `fidp` parameter. - * @param userID - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user - * scenarios where each user should be uniquely identified. - * - * @returns - A promise that resolves with the authorization URL parameters. - * - * @example - * ``` - * auth.getAuthorizationURLParams().then((params)=>{ - * // console.log(params); - * }).catch((error)=>{ - * // console.error(error); - * }); - * ``` - * - * {@link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getAuthorizationURLParams} - * - * @preserve - */ - public async getAuthorizationURLParams(config?: GetAuthURLConfig, userID?: string): Promise> { - const authRequestConfig: GetAuthURLConfig = {...config}; - - delete authRequestConfig?.forceInit; - - if ( - await this._dataLayer.getTemporaryDataParameter( - OIDCDiscoveryConstants.Storage.StorageKeys.OPENID_PROVIDER_CONFIG_INITIATED, - ) - ) { - return this._authenticationCore.getAuthorizationURLParams(authRequestConfig, userID); - } - - return this._authenticationCore.getOIDCProviderMetaData(config?.forceInit as boolean).then(() => { - return this._authenticationCore.getAuthorizationURLParams(authRequestConfig, userID); - }); - } - /** * This is an async method that returns a Promise that resolves with the authorization URL. * * @param config - (Optional) A config object to force initialization and pass * custom path parameters such as the fidp parameter. - * @param userID - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user + * @param userId - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user * scenarios where each user should be uniquely identified. * * @returns - A promise that resolves with the authorization URL. * * @example * ``` - * auth.getAuthorizationURL().then((url)=>{ + * auth.getSignInUrl().then((url)=>{ * // console.log(url); * }).catch((error)=>{ * // console.error(error); * }); * ``` * - * {@link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getAuthorizationURL} + * {@link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getSignInUrl} * * @preserve */ - public async getAuthorizationURL(config?: GetAuthURLConfig, userID?: string): Promise { - const authRequestConfig: GetAuthURLConfig = {...config}; + public async getSignInUrl(requestConfig?: ExtendedAuthorizeRequestUrlParams, userId?: string): Promise { + const authRequestConfig: ExtendedAuthorizeRequestUrlParams = {...requestConfig}; delete authRequestConfig?.forceInit; + const __TODO__ = async () => { + const authorizeEndpoint: string = (await this._storageManager.getOIDCProviderMetaDataParameter( + OIDCDiscoveryConstants.Storage.StorageKeys.Endpoints.AUTHORIZATION as keyof OIDCDiscoveryApiResponse, + )) as string; + + if (!authorizeEndpoint || authorizeEndpoint.trim().length === 0) { + throw new AsgardeoAuthException( + 'JS-AUTH_CORE-GAU-NF01', + 'No authorization endpoint found.', + 'No authorization endpoint was found in the OIDC provider meta data from the well-known endpoint ' + + 'or the authorization endpoint passed to the SDK is empty.', + ); + } + + const authorizeRequest: URL = new URL(authorizeEndpoint); + const configData: StrictAuthClientConfig = await this._config(); + const tempStore: TemporaryStore = await this._storageManager.getTemporaryData(userId); + const pkceKey: string = await generatePkceStorageKey(tempStore); + + let codeVerifier: string | undefined; + let codeChallenge: string | undefined; + + if (configData.enablePKCE) { + codeVerifier = this._cryptoHelper?.getCodeVerifier(); + codeChallenge = this._cryptoHelper?.getCodeChallenge(codeVerifier); + await this._storageManager.setTemporaryDataParameter(pkceKey, codeVerifier, userId); + } + + const authorizeRequestParams: Map = getAuthorizeRequestUrlParams( + { + redirectUri: configData.afterSignInUrl, + clientId: configData.clientId, + scopes: processOpenIDScopes(configData.scopes), + responseMode: configData.responseMode, + codeChallengeMethod: PKCEConstants.DEFAULT_CODE_CHALLENGE_METHOD, + codeChallenge, + prompt: configData.prompt, + }, + {key: pkceKey}, + authRequestConfig, + ); + + for (const [key, value] of authorizeRequestParams.entries()) { + authorizeRequest.searchParams.append(key, value); + } + + return authorizeRequest.toString(); + }; + if ( - await this._dataLayer.getTemporaryDataParameter( + await this._storageManager.getTemporaryDataParameter( OIDCDiscoveryConstants.Storage.StorageKeys.OPENID_PROVIDER_CONFIG_INITIATED, ) ) { - return this._authenticationCore.getAuthorizationURL(authRequestConfig, userID); + return __TODO__(); } - return this._authenticationCore.getOIDCProviderMetaData(config?.forceInit as boolean).then(() => { - return this._authenticationCore.getAuthorizationURL(authRequestConfig, userID); + return this.loadOpenIDProviderConfiguration(requestConfig?.forceInit as boolean).then(() => { + return __TODO__(); }); } @@ -254,7 +277,7 @@ export class AsgardeoAuthClient { * * @param authorizationCode - The authorization code. * @param sessionState - The session state. - * @param userID - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user + * @param userId - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user * scenarios where each user should be uniquely identified. * * @returns - A Promise that resolves with the token response. @@ -277,40 +300,178 @@ export class AsgardeoAuthClient { authorizationCode: string, sessionState: string, state: string, - userID?: string, + userId?: string, tokenRequestConfig?: { params: Record; }, ): Promise { + const __TODO__ = async () => { + const tokenEndpoint: string | undefined = (await this._oidcProviderMetaData()).token_endpoint; + const configData: StrictAuthClientConfig = await this._config(); + + if (!tokenEndpoint || tokenEndpoint.trim().length === 0) { + throw new AsgardeoAuthException( + 'JS-AUTH_CORE-RAT1-NF01', + 'Token endpoint not found.', + 'No token endpoint was found in the OIDC provider meta data returned by the well-known endpoint ' + + 'or the token endpoint passed to the SDK is empty.', + ); + } + + sessionState && + (await this._storageManager.setSessionDataParameter( + OIDCRequestConstants.Params.SESSION_STATE as keyof SessionData, + sessionState, + userId, + )); + + const body: URLSearchParams = new URLSearchParams(); + + body.set('client_id', configData.clientId); + + if (configData.clientSecret && configData.clientSecret.trim().length > 0) { + body.set('client_secret', configData.clientSecret); + } + + const code: string = authorizationCode; + + body.set('code', code); + + body.set('grant_type', 'authorization_code'); + body.set('redirect_uri', configData.afterSignInUrl); + + if (tokenRequestConfig?.params) { + Object.entries(tokenRequestConfig.params).forEach(([key, value]: [key: string, value: unknown]) => { + body.append(key, value as string); + }); + } + + if (configData.enablePKCE) { + body.set( + 'code_verifier', + `${await this._storageManager.getTemporaryDataParameter(extractPkceStorageKeyFromState(state), userId)}`, + ); + + await this._storageManager.removeTemporaryDataParameter(extractPkceStorageKeyFromState(state), userId); + } + + let tokenResponse: Response; + + try { + tokenResponse = await fetch(tokenEndpoint, { + body: body, + credentials: configData.sendCookiesInRequests ? 'include' : 'same-origin', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + }, + method: 'POST', + }); + } catch (error: any) { + throw new AsgardeoAuthException( + 'JS-AUTH_CORE-RAT1-NE02', + 'Requesting access token failed', + error ?? 'The request to get the access token from the server failed.', + ); + } + + if (!tokenResponse.ok) { + throw new AsgardeoAuthException( + 'JS-AUTH_CORE-RAT1-HE03', + `Requesting access token failed with ${tokenResponse.statusText}`, + (await tokenResponse.json()) as string, + ); + } + + return await this._authenticationHelper.handleTokenResponse(tokenResponse, userId); + }; + if ( - await this._dataLayer.getTemporaryDataParameter( + await this._storageManager.getTemporaryDataParameter( OIDCDiscoveryConstants.Storage.StorageKeys.OPENID_PROVIDER_CONFIG_INITIATED, ) ) { - return this._authenticationCore.requestAccessToken( - authorizationCode, - sessionState, - state, - userID, - tokenRequestConfig, - ); + return __TODO__(); } - return this._authenticationCore.getOIDCProviderMetaData(false).then(() => { - return this._authenticationCore.requestAccessToken( - authorizationCode, - sessionState, - state, - userID, - tokenRequestConfig, - ); + return this.loadOpenIDProviderConfiguration(false).then(() => { + return __TODO__(); }); } + public async loadOpenIDProviderConfiguration(forceInit: boolean): Promise { + const configData: StrictAuthClientConfig = await this._config(); + + if ( + !forceInit && + (await this._storageManager.getTemporaryDataParameter( + OIDCDiscoveryConstants.Storage.StorageKeys.OPENID_PROVIDER_CONFIG_INITIATED, + )) + ) { + return Promise.resolve(); + } + + const wellKnownEndpoint: string = (configData as any).wellKnownEndpoint; + + if (wellKnownEndpoint) { + let response: Response; + + try { + response = await fetch(wellKnownEndpoint); + if (response.status !== 200 || !response.ok) { + throw new Error(); + } + } catch { + throw new AsgardeoAuthException( + 'JS-AUTH_CORE-GOPMD-HE01', + 'Invalid well-known response', + 'The well known endpoint response has been failed with an error.', + ); + } + + await this._storageManager.setOIDCProviderMetaData( + await this._authenticationHelper.resolveEndpoints(await response.json()), + ); + await this._storageManager.setTemporaryDataParameter( + OIDCDiscoveryConstants.Storage.StorageKeys.OPENID_PROVIDER_CONFIG_INITIATED, + true, + ); + + return Promise.resolve(); + } else if ((configData as any).baseUrl) { + try { + await this._storageManager.setOIDCProviderMetaData( + await this._authenticationHelper.resolveEndpointsByBaseURL(), + ); + } catch (error: any) { + throw new AsgardeoAuthException( + 'JS-AUTH_CORE-GOPMD-IV02', + 'Resolving endpoints failed.', + error ?? 'Resolving endpoints by base url failed.', + ); + } + await this._storageManager.setTemporaryDataParameter( + OIDCDiscoveryConstants.Storage.StorageKeys.OPENID_PROVIDER_CONFIG_INITIATED, + true, + ); + + return Promise.resolve(); + } else { + await this._storageManager.setOIDCProviderMetaData(await this._authenticationHelper.resolveEndpointsExplicitly()); + + await this._storageManager.setTemporaryDataParameter( + OIDCDiscoveryConstants.Storage.StorageKeys.OPENID_PROVIDER_CONFIG_INITIATED, + true, + ); + + return Promise.resolve(); + } + } + /** * This method returns the sign-out URL. * - * @param userID - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user + * @param userId - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user * scenarios where each user should be uniquely identified. * * **This doesn't clear the authentication data.** @@ -319,15 +480,58 @@ export class AsgardeoAuthClient { * * @example * ``` - * const signOutUrl = await auth.getSignOutURL(); + * const signOutUrl = await auth.getSignOutUrl(); * ``` * - * {@link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getSignOutURL} + * {@link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getSignOutUrl} * * @preserve */ - public async getSignOutURL(userID?: string): Promise { - return this._authenticationCore.getSignOutURL(userID); + public async getSignOutUrl(userId?: string): Promise { + const logoutEndpoint: string | undefined = (await this._oidcProviderMetaData())?.end_session_endpoint; + const configData: StrictAuthClientConfig = await this._config(); + + if (!logoutEndpoint || logoutEndpoint.trim().length === 0) { + throw new AsgardeoAuthException( + 'JS-AUTH_CORE-GSOU-NF01', + 'Sign-out endpoint not found.', + 'No sign-out endpoint was found in the OIDC provider meta data returned by the well-known endpoint ' + + 'or the sign-out endpoint passed to the SDK is empty.', + ); + } + + const callbackURL: string = configData?.afterSignOutUrl ?? configData?.afterSignInUrl; + + if (!callbackURL || callbackURL.trim().length === 0) { + throw new AsgardeoAuthException( + 'JS-AUTH_CORE-GSOU-NF03', + 'No sign-out redirect URL found.', + 'The sign-out redirect URL cannot be found or the URL passed to the SDK is empty. ' + + 'No sign-in redirect URL has been found either. ', + ); + } + const queryParams: URLSearchParams = new URLSearchParams(); + + queryParams.set('post_logout_redirect_uri', callbackURL); + + if (configData.sendIdTokenInLogoutRequest) { + const idToken: string = (await this._storageManager.getSessionData(userId))?.id_token; + + if (!idToken || idToken.trim().length === 0) { + throw new AsgardeoAuthException( + 'JS-AUTH_CORE-GSOU-NF02', + 'ID token not found.', + 'No ID token could be found. Either the session information is lost or you have not signed in.', + ); + } + queryParams.set('id_token_hint', idToken); + } else { + queryParams.set('client_id', configData.clientId); + } + + queryParams.set('state', OIDCRequestConstants.Params.SIGN_OUT_SUCCESS); + + return `${logoutEndpoint}?${queryParams.toString()}`; } /** @@ -337,78 +541,112 @@ export class AsgardeoAuthClient { * * @example * ``` - * const endpoints = await auth.getOIDCServiceEndpoints(); + * const endpoints = await auth.getOpenIDProviderEndpoints(); * ``` * - * {@link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getOIDCServiceEndpoints} + * {@link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getOpenIDProviderEndpoints} * * @preserve */ - public async getOIDCServiceEndpoints(): Promise> { - return this._authenticationCore.getOIDCServiceEndpoints(); + public async getOpenIDProviderEndpoints(): Promise> { + const oidcProviderMetaData: OIDCDiscoveryApiResponse = await this._oidcProviderMetaData(); + + return { + authorizationEndpoint: oidcProviderMetaData.authorization_endpoint ?? '', + checkSessionIframe: oidcProviderMetaData.check_session_iframe ?? '', + endSessionEndpoint: oidcProviderMetaData.end_session_endpoint ?? '', + introspectionEndpoint: oidcProviderMetaData.introspection_endpoint ?? '', + issuer: oidcProviderMetaData.issuer ?? '', + jwksUri: oidcProviderMetaData.jwks_uri ?? '', + registrationEndpoint: oidcProviderMetaData.registration_endpoint ?? '', + revocationEndpoint: oidcProviderMetaData.revocation_endpoint ?? '', + tokenEndpoint: oidcProviderMetaData.token_endpoint ?? '', + userinfoEndpoint: oidcProviderMetaData.userinfo_endpoint ?? '', + }; } /** * This method decodes the payload of the ID token and returns it. * - * @param userID - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user + * @param userId - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user * scenarios where each user should be uniquely identified. * * @returns - A Promise that resolves with the decoded ID token payload. * * @example * ``` - * const decodedIdToken = await auth.getDecodedIDToken(); + * const decodedIdToken = await auth.getDecodedIdToken(); * ``` * - * {@link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getDecodedIDToken} + * {@link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getDecodedIdToken} * * @preserve */ - public async getDecodedIDToken(userID?: string): Promise { - return this._authenticationCore.getDecodedIDToken(userID); + public async getDecodedIdToken(userId?: string): Promise { + const idToken: string = (await this._storageManager.getSessionData(userId)).id_token; + const payload: IdTokenPayload = this._cryptoHelper.decodeIdToken(idToken); + + return payload; } /** * This method returns the ID token. * - * @param userID - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user + * @param userId - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user * scenarios where each user should be uniquely identified. * * @returns - A Promise that resolves with the ID token. * * @example * ``` - * const idToken = await auth.getIDToken(); + * const idToken = await auth.getIdToken(); * ``` * - * {@link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getIDToken} + * {@link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getIdToken} * * @preserve */ - public async getIDToken(userID?: string): Promise { - return this._authenticationCore.getIDToken(userID); + public async getIdToken(userId?: string): Promise { + return (await this._storageManager.getSessionData(userId)).id_token; } /** * This method returns the basic user information obtained from the ID token. * - * @param userID - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user + * @param userId - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user * scenarios where each user should be uniquely identified. * * @returns - A Promise that resolves with an object containing the basic user information. * * @example * ``` - * const userInfo = await auth.getBasicUserInfo(); + * const userInfo = await auth.getUser(); * ``` * - * {@link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getBasicUserInfo} + * {@link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getUser} * * @preserve */ - public async getBasicUserInfo(userID?: string): Promise { - return this._authenticationCore.getBasicUserInfo(userID); + public async getUser(userId?: string): Promise { + const sessionData: SessionData = await this._storageManager.getSessionData(userId); + const authenticatedUser: User = this._authenticationHelper.getAuthenticatedUserInfo(sessionData?.id_token); + + Object.keys(authenticatedUser).forEach((key: string) => { + if (authenticatedUser[key] === undefined || authenticatedUser[key] === '' || authenticatedUser[key] === null) { + delete authenticatedUser[key]; + } + }); + + return authenticatedUser; + } + + public async getUserSession(userId?: string): Promise { + const sessionData: SessionData = await this._storageManager.getSessionData(userId); + + return { + scopes: sessionData?.scope?.split(' '), + sessionState: sessionData?.session_state ?? '', + }; } /** @@ -421,18 +659,18 @@ export class AsgardeoAuthClient { * const cryptoHelper = await auth.IsomorphicCrypto(); * ``` * - * {@link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getCryptoHelper} + * {@link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getCrypto} * * @preserve */ - public async getCryptoHelper(): Promise { - return this._authenticationCore.getCryptoHelper(); + public async getCrypto(): Promise { + return this._cryptoHelper; } /** * This method revokes the access token. * - * @param userID - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user + * @param userId - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user * scenarios where each user should be uniquely identified. * * **This method also clears the authentication data.** @@ -452,15 +690,67 @@ export class AsgardeoAuthClient { * * @preserve */ - public revokeAccessToken(userID?: string): Promise { - return this._authenticationCore.revokeAccessToken(userID); + public async revokeAccessToken(userId?: string): Promise { + const revokeTokenEndpoint: string | undefined = (await this._oidcProviderMetaData()).revocation_endpoint; + const configData: StrictAuthClientConfig = await this._config(); + + if (!revokeTokenEndpoint || revokeTokenEndpoint.trim().length === 0) { + throw new AsgardeoAuthException( + 'JS-AUTH_CORE-RAT3-NF01', + 'No revoke access token endpoint found.', + 'No revoke access token endpoint was found in the OIDC provider meta data returned by ' + + 'the well-known endpoint or the revoke access token endpoint passed to the SDK is empty.', + ); + } + + const body: string[] = []; + + body.push(`client_id=${configData.clientId}`); + body.push(`token=${(await this._storageManager.getSessionData(userId)).access_token}`); + body.push('token_type_hint=access_token'); + + if (configData.clientSecret && configData.clientSecret.trim().length > 0) { + body.push(`client_secret=${configData.clientSecret}`); + } + + let response: Response; + + try { + response = await fetch(revokeTokenEndpoint, { + body: body.join('&'), + credentials: configData.sendCookiesInRequests ? 'include' : 'same-origin', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + }, + method: 'POST', + }); + } catch (error: any) { + throw new AsgardeoAuthException( + 'JS-AUTH_CORE-RAT3-NE02', + 'The request to revoke access token failed.', + error ?? 'The request sent to revoke the access token failed.', + ); + } + + if (response.status !== 200 || !response.ok) { + throw new AsgardeoAuthException( + 'JS-AUTH_CORE-RAT3-HE03', + `Invalid response status received for revoke access token request (${response.statusText}).`, + (await response.json()) as string, + ); + } + + this._authenticationHelper.clearSession(userId); + + return Promise.resolve(response); } /** * This method refreshes the access token and returns a Promise that resolves with the new access * token and other relevant data. * - * @param userID - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user + * @param userId - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user * scenarios where each user should be uniquely identified. * * @returns - A Promise that resolves with the token response. @@ -478,14 +768,74 @@ export class AsgardeoAuthClient { * * @preserve */ - public refreshAccessToken(userID?: string): Promise { - return this._authenticationCore.refreshAccessToken(userID); + public async refreshAccessToken(userId?: string): Promise { + const tokenEndpoint: string | undefined = (await this._oidcProviderMetaData()).token_endpoint; + const configData: StrictAuthClientConfig = await this._config(); + const sessionData: SessionData = await this._storageManager.getSessionData(userId); + + if (!sessionData.refresh_token) { + throw new AsgardeoAuthException( + 'JS-AUTH_CORE-RAT2-NF01', + 'No refresh token found.', + "There was no refresh token found. Asgardeo doesn't return a " + + 'refresh token if the refresh token grant is not enabled.', + ); + } + + if (!tokenEndpoint || tokenEndpoint.trim().length === 0) { + throw new AsgardeoAuthException( + 'JS-AUTH_CORE-RAT2-NF02', + 'No refresh token endpoint found.', + 'No refresh token endpoint was in the OIDC provider meta data returned by the well-known ' + + 'endpoint or the refresh token endpoint passed to the SDK is empty.', + ); + } + + const body: string[] = []; + + body.push(`client_id=${configData.clientId}`); + body.push(`refresh_token=${sessionData.refresh_token}`); + body.push('grant_type=refresh_token'); + + if (configData.clientSecret && configData.clientSecret.trim().length > 0) { + body.push(`client_secret=${configData.clientSecret}`); + } + + let tokenResponse: Response; + + try { + tokenResponse = await fetch(tokenEndpoint, { + body: body.join('&'), + credentials: configData.sendCookiesInRequests ? 'include' : 'same-origin', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + }, + method: 'POST', + }); + } catch (error: any) { + throw new AsgardeoAuthException( + 'JS-AUTH_CORE-RAT2-NR03', + 'Refresh access token request failed.', + error ?? 'The request to refresh the access token failed.', + ); + } + + if (!tokenResponse.ok) { + throw new AsgardeoAuthException( + 'JS-AUTH_CORE-RAT2-HE04', + `Refreshing access token failed with ${tokenResponse.statusText}`, + (await tokenResponse.json()) as string, + ); + } + + return this._authenticationHelper.handleTokenResponse(tokenResponse, userId); } /** * This method returns the access token. * - * @param userID - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user + * @param userId - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user * scenarios where each user should be uniquely identified. * * @returns - A Promise that resolves with the access token. @@ -499,8 +849,8 @@ export class AsgardeoAuthClient { * * @preserve */ - public async getAccessToken(userID?: string): Promise { - return this._authenticationCore.getAccessToken(userID); + public async getAccessToken(userId?: string): Promise { + return (await this._storageManager.getSessionData(userId))?.access_token; } /** @@ -508,7 +858,7 @@ export class AsgardeoAuthClient { * depending on the config passed. * * @param config - A config object containing the custom grant configurations. - * @param userID - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user + * @param userId - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user * scenarios where each user should be uniquely identified. * * @returns - A Promise that resolves with the response depending @@ -519,7 +869,7 @@ export class AsgardeoAuthClient { * const config = { * attachToken: false, * data: { - * client_id: "{{clientID}}", + * client_id: "{{clientId}}", * grant_type: "account_switch", * scope: "{{scope}}", * token: "{{token}}", @@ -530,46 +880,143 @@ export class AsgardeoAuthClient { * signInRequired: true * } * - * auth.requestCustomGrant(config).then((response)=>{ + * auth.exchangeToken(config).then((response)=>{ * // console.log(response); * }).catch((error)=>{ * // console.error(error); * }); * ``` * - * {@link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#requestCustomGrant} + * {@link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#exchangeToken} * * @preserve */ - public requestCustomGrant(config: CustomGrantConfig, userID?: string): Promise { - return this._authenticationCore.requestCustomGrant(config, userID); + public async exchangeToken( + config: TokenExchangeRequestConfig, + userId?: string, + ): Promise { + const oidcProviderMetadata: OIDCDiscoveryApiResponse = await this._oidcProviderMetaData(); + const configData: StrictAuthClientConfig = await this._config(); + + let tokenEndpoint: string | undefined; + + if (config.tokenEndpoint && config.tokenEndpoint.trim().length !== 0) { + tokenEndpoint = config.tokenEndpoint; + } else { + tokenEndpoint = oidcProviderMetadata.token_endpoint; + } + + if (!tokenEndpoint || tokenEndpoint.trim().length === 0) { + throw new AsgardeoAuthException( + 'JS-AUTH_CORE-RCG-NF01', + 'Token endpoint not found.', + 'No token endpoint was found in the OIDC provider meta data returned by the well-known endpoint ' + + 'or the token endpoint passed to the SDK is empty.', + ); + } + + const data: string[] = await Promise.all( + Object.entries(config.data).map(async ([key, value]: [key: string, value: any]) => { + const newValue: string = await this._authenticationHelper.replaceCustomGrantTemplateTags( + value as string, + userId, + ); + + return `${key}=${newValue}`; + }), + ); + + let requestHeaders: Record = { + Accept: 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + }; + + if (config.attachToken) { + requestHeaders = { + ...requestHeaders, + Authorization: `Bearer ${(await this._storageManager.getSessionData(userId)).access_token}`, + }; + } + + const requestConfig: RequestInit = { + body: data.join('&'), + credentials: configData.sendCookiesInRequests ? 'include' : 'same-origin', + headers: new Headers(requestHeaders), + method: 'POST', + }; + + let response: Response; + + try { + response = await fetch(tokenEndpoint, requestConfig); + } catch (error: any) { + throw new AsgardeoAuthException( + 'JS-AUTH_CORE-RCG-NE02', + 'The custom grant request failed.', + error ?? 'The request sent to get the custom grant failed.', + ); + } + + if (response.status !== 200 || !response.ok) { + throw new AsgardeoAuthException( + 'JS-AUTH_CORE-RCG-HE03', + `Invalid response status received for the custom grant request. (${response.statusText})`, + (await response.json()) as string, + ); + } + + if (config.returnsSession) { + return this._authenticationHelper.handleTokenResponse(response, userId); + } else { + return Promise.resolve((await response.json()) as TokenResponse | Response); + } } /** * This method returns if the user is authenticated or not. * - * @param userID - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user + * @param userId - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user * scenarios where each user should be uniquely identified. * * @returns - A Promise that resolves with `true` if the user is authenticated, `false` otherwise. * * @example * ``` - * await auth.isAuthenticated(); + * await auth.isSignedIn(); * ``` * - * {@link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#isAuthenticated} + * {@link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#isSignedIn} * * @preserve */ - public async isAuthenticated(userID?: string): Promise { - return this._authenticationCore.isAuthenticated(userID); + public async isSignedIn(userId?: string): Promise { + const isAccessTokenAvailable: boolean = Boolean(await this.getAccessToken(userId)); + + // Check if the access token is expired. + const createdAt: number = (await this._storageManager.getSessionData(userId))?.created_at; + + // Get the expires in value. + const expiresInString: string = (await this._storageManager.getSessionData(userId))?.expires_in; + + // If the expires in value is not available, the token is invalid and the user is not authenticated. + if (!expiresInString) { + return false; + } + + // Convert to milliseconds. + const expiresIn: number = parseInt(expiresInString) * 1000; + const currentTime: number = new Date().getTime(); + const isAccessTokenValid: boolean = createdAt + expiresIn > currentTime; + + const isSignedIn: boolean = isAccessTokenAvailable && isAccessTokenValid; + + return isSignedIn; } /** * This method returns the PKCE code generated during the generation of the authentication URL. * - * @param userID - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user + * @param userId - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user * scenarios where each user should be uniquely identified. * @param state - The state parameter that was passed in the authentication URL. * @@ -584,8 +1031,11 @@ export class AsgardeoAuthClient { * * @preserve */ - public async getPKCECode(state: string, userID?: string): Promise { - return this._authenticationCore.getPKCECode(state, userID); + public async getPKCECode(state: string, userId?: string): Promise { + return (await this._storageManager.getTemporaryDataParameter( + extractPkceStorageKeyFromState(state), + userId, + )) as string; } /** @@ -593,7 +1043,7 @@ export class AsgardeoAuthClient { * * @param pkce - The PKCE code. * @param state - The state parameter that was passed in the authentication URL. - * @param userID - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user + * @param userId - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user * scenarios where each user should be uniquely identified. * * @example @@ -605,8 +1055,8 @@ export class AsgardeoAuthClient { * * @preserve */ - public async setPKCECode(pkce: string, state: string, userID?: string): Promise { - await this._authenticationCore.setPKCECode(pkce, state, userID); + public async setPKCECode(pkce: string, state: string, userId?: string): Promise { + return await this._storageManager.setTemporaryDataParameter(extractPkceStorageKeyFromState(state), pkce, userId); } /** @@ -614,7 +1064,7 @@ export class AsgardeoAuthClient { * * @param signOutRedirectUrl - The URL to which the user has been redirected to after signing-out. * - * **The server appends path parameters to the `signOutRedirectURL` and these path parameters + * **The server appends path parameters to the `afterSignOutUrl` and these path parameters * are required for this method to function.** * * @returns - `true` if successful, `false` otherwise. @@ -623,8 +1073,8 @@ export class AsgardeoAuthClient { * * @preserve */ - public static isSignOutSuccessful(signOutRedirectURL: string): boolean { - const url: URL = new URL(signOutRedirectURL); + public static isSignOutSuccessful(afterSignOutUrl: string): boolean { + const url: URL = new URL(afterSignOutUrl); const stateParam: string | null = url.searchParams.get(OIDCRequestConstants.Params.STATE); const error: boolean = Boolean(url.searchParams.get('error')); @@ -636,7 +1086,7 @@ export class AsgardeoAuthClient { * * @param signOutRedirectUrl - The URL to which the user has been redirected to after signing-out. * - * **The server appends path parameters to the `signOutRedirectURL` and these path parameters + * **The server appends path parameters to the `afterSignOutUrl` and these path parameters * are required for this method to function.** * * @returns - `true` if successful, `false` otherwise. @@ -645,8 +1095,8 @@ export class AsgardeoAuthClient { * * @preserve */ - public static didSignOutFail(signOutRedirectURL: string): boolean { - const url: URL = new URL(signOutRedirectURL); + public static didSignOutFail(afterSignOutUrl: string): boolean { + const url: URL = new URL(afterSignOutUrl); const stateParam: string | null = url.searchParams.get(OIDCRequestConstants.Params.STATE); const error: boolean = Boolean(url.searchParams.get('error')); @@ -661,22 +1111,23 @@ export class AsgardeoAuthClient { * @example * ``` * const config = { - * signInRedirectURL: "http://localhost:3000/sign-in", - * clientID: "client ID", + * afterSignInUrl: "http://localhost:3000/sign-in", + * clientId: "client ID", * baseUrl: "https://localhost:9443" * } * - * await auth.updateConfig(config); + * await auth.reInitialize(config); * ``` - * {@link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#updateConfig} + * {@link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#reInitialize} * * @preserve */ - public async updateConfig(config: Partial>): Promise { - await this._authenticationCore.updateConfig(config); + public async reInitialize(config: Partial>): Promise { + await this._storageManager.setConfigData(config); + await this.loadOpenIDProviderConfiguration(true); } - public static async clearUserSessionData(userID?: string): Promise { - await this._authenticationCore.clearUserSessionData(userID); + public static async clearSession(userId?: string): Promise { + await this._authenticationHelper.clearSession(userId); } } diff --git a/packages/javascript/src/__legacy__/core/authentication-core.ts b/packages/javascript/src/__legacy__/core/authentication-core.ts deleted file mode 100644 index ec74a2c5..00000000 --- a/packages/javascript/src/__legacy__/core/authentication-core.ts +++ /dev/null @@ -1,675 +0,0 @@ -/** - * Copyright (c) 2020, WSO2 LLC. (https://www.wso2.com). All Rights Reserved. - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import extractPkceStorageKeyFromState from '../../utils/extractPkceStorageKeyFromState'; -import generateStateParamForRequestCorrelation from '../../utils/generateStateParamForRequestCorrelation'; -import {DataLayer} from '../data'; -import {AsgardeoAuthException} from '../exception'; -import {AuthenticationHelper} from '../helpers'; -import { - AuthClientConfig, - AuthenticatedUserInfo, - AuthorizationURLParams, - BasicUserInfo, - CustomGrantConfig, - FetchRequestConfig, - FetchResponse, - SessionData, - StrictAuthClientConfig, -} from '../models'; -import {TokenResponse} from '../../models/token'; -import {Crypto} from '../../models/crypto'; -import {IdTokenPayload} from '../../models/id-token'; -import {TemporaryStore} from '../../models/store'; -import {OIDCEndpoints} from '../../models/oidc-endpoints'; -import generatePkceStorageKey from '../../utils/generatePkceStorageKey'; -import ScopeConstants from '../../constants/ScopeConstants'; -import OIDCDiscoveryConstants from '../../constants/OIDCDiscoveryConstants'; -import OIDCRequestConstants from '../../constants/OIDCRequestConstants'; -import {OIDCDiscoveryApiResponse} from '../../models/oidc-discovery'; -import { IsomorphicCrypto } from '../../IsomorphicCrypto'; - -export class AuthenticationCore { - private _dataLayer: DataLayer; - private _config: () => Promise; - private _oidcProviderMetaData: () => Promise; - private _authenticationHelper: AuthenticationHelper; - private _cryptoUtils: Crypto; - private _cryptoHelper: IsomorphicCrypto; - - public constructor(dataLayer: DataLayer, cryptoUtils: Crypto) { - this._cryptoUtils = cryptoUtils; - this._cryptoHelper = new IsomorphicCrypto(cryptoUtils); - this._authenticationHelper = new AuthenticationHelper(dataLayer, this._cryptoHelper); - this._dataLayer = dataLayer; - this._config = async () => await this._dataLayer.getConfigData(); - this._oidcProviderMetaData = async () => await this._dataLayer.getOIDCProviderMetaData(); - } - - public async getAuthorizationURLParams( - config?: AuthorizationURLParams, - userID?: string, - ): Promise> { - const configData: StrictAuthClientConfig = await this._config(); - - const authorizeRequestParams: Map = new Map(); - - authorizeRequestParams.set('response_type', 'code'); - authorizeRequestParams.set('client_id', configData.clientID); - - let scope: string = ScopeConstants.OPENID; - - if (configData.scope && configData.scope.length > 0) { - if (!configData.scope.includes(ScopeConstants.OPENID)) { - configData.scope.push(ScopeConstants.OPENID); - } - scope = configData.scope.join(' '); - } - - authorizeRequestParams.set('scope', scope); - authorizeRequestParams.set('redirect_uri', configData.signInRedirectURL); - - if (configData.responseMode) { - authorizeRequestParams.set('response_mode', configData.responseMode); - } - - const tempStore: TemporaryStore = await this._dataLayer.getTemporaryData(userID); - const pkceKey: string = await generatePkceStorageKey(tempStore); - - if (configData.enablePKCE) { - const codeVerifier: string = this._cryptoHelper?.getCodeVerifier(); - const codeChallenge: string = this._cryptoHelper?.getCodeChallenge(codeVerifier); - - await this._dataLayer.setTemporaryDataParameter(pkceKey, codeVerifier, userID); - authorizeRequestParams.set('code_challenge_method', 'S256'); - authorizeRequestParams.set('code_challenge', codeChallenge); - } - - if (configData.prompt) { - authorizeRequestParams.set('prompt', configData.prompt); - } - - const customParams: AuthorizationURLParams | undefined = config; - - if (customParams) { - for (const [key, value] of Object.entries(customParams)) { - if (key != '' && value != '' && key !== OIDCRequestConstants.Params.STATE) { - authorizeRequestParams.set(key, value.toString()); - } - } - } - - authorizeRequestParams.set( - OIDCRequestConstants.Params.STATE, - generateStateParamForRequestCorrelation( - pkceKey, - customParams ? customParams[OIDCRequestConstants.Params.STATE]?.toString() : '', - ), - ); - - return authorizeRequestParams; - } - - public async getAuthorizationURL(config?: AuthorizationURLParams, userID?: string): Promise { - const authorizeEndpoint: string = (await this._dataLayer.getOIDCProviderMetaDataParameter( - OIDCDiscoveryConstants.Storage.StorageKeys.Endpoints.AUTHORIZATION as keyof OIDCDiscoveryApiResponse, - )) as string; - - if (!authorizeEndpoint || authorizeEndpoint.trim().length === 0) { - throw new AsgardeoAuthException( - 'JS-AUTH_CORE-GAU-NF01', - 'No authorization endpoint found.', - 'No authorization endpoint was found in the OIDC provider meta data from the well-known endpoint ' + - 'or the authorization endpoint passed to the SDK is empty.', - ); - } - - const authorizeRequest: URL = new URL(authorizeEndpoint); - - const authorizeRequestParams: Map = await this.getAuthorizationURLParams(config, userID); - - for (const [key, value] of authorizeRequestParams.entries()) { - authorizeRequest.searchParams.append(key, value); - } - - return authorizeRequest.toString(); - } - - public async requestAccessToken( - authorizationCode: string, - sessionState: string, - state: string, - userID?: string, - tokenRequestConfig?: { - params: Record; - }, - ): Promise { - const tokenEndpoint: string | undefined = (await this._oidcProviderMetaData()).token_endpoint; - const configData: StrictAuthClientConfig = await this._config(); - - if (!tokenEndpoint || tokenEndpoint.trim().length === 0) { - throw new AsgardeoAuthException( - 'JS-AUTH_CORE-RAT1-NF01', - 'Token endpoint not found.', - 'No token endpoint was found in the OIDC provider meta data returned by the well-known endpoint ' + - 'or the token endpoint passed to the SDK is empty.', - ); - } - - sessionState && - (await this._dataLayer.setSessionDataParameter( - OIDCRequestConstants.Params.SESSION_STATE as keyof SessionData, - sessionState, - userID, - )); - - const body: URLSearchParams = new URLSearchParams(); - - body.set('client_id', configData.clientID); - - if (configData.clientSecret && configData.clientSecret.trim().length > 0) { - body.set('client_secret', configData.clientSecret); - } - - const code: string = authorizationCode; - - body.set('code', code); - - body.set('grant_type', 'authorization_code'); - body.set('redirect_uri', configData.signInRedirectURL); - - if (tokenRequestConfig?.params) { - Object.entries(tokenRequestConfig.params).forEach(([key, value]: [key: string, value: unknown]) => { - body.append(key, value as string); - }); - } - - if (configData.enablePKCE) { - body.set( - 'code_verifier', - `${await this._dataLayer.getTemporaryDataParameter(extractPkceStorageKeyFromState(state), userID)}`, - ); - - await this._dataLayer.removeTemporaryDataParameter(extractPkceStorageKeyFromState(state), userID); - } - - let tokenResponse: Response; - - try { - tokenResponse = await fetch(tokenEndpoint, { - body: body, - credentials: configData.sendCookiesInRequests ? 'include' : 'same-origin', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/x-www-form-urlencoded', - }, - method: 'POST', - }); - } catch (error: any) { - throw new AsgardeoAuthException( - 'JS-AUTH_CORE-RAT1-NE02', - 'Requesting access token failed', - error ?? 'The request to get the access token from the server failed.', - ); - } - - if (!tokenResponse.ok) { - throw new AsgardeoAuthException( - 'JS-AUTH_CORE-RAT1-HE03', - `Requesting access token failed with ${tokenResponse.statusText}`, - (await tokenResponse.json()) as string, - ); - } - - return await this._authenticationHelper.handleTokenResponse(tokenResponse, userID); - } - - public async refreshAccessToken(userID?: string): Promise { - const tokenEndpoint: string | undefined = (await this._oidcProviderMetaData()).token_endpoint; - const configData: StrictAuthClientConfig = await this._config(); - const sessionData: SessionData = await this._dataLayer.getSessionData(userID); - - if (!sessionData.refresh_token) { - throw new AsgardeoAuthException( - 'JS-AUTH_CORE-RAT2-NF01', - 'No refresh token found.', - "There was no refresh token found. Asgardeo doesn't return a " + - 'refresh token if the refresh token grant is not enabled.', - ); - } - - if (!tokenEndpoint || tokenEndpoint.trim().length === 0) { - throw new AsgardeoAuthException( - 'JS-AUTH_CORE-RAT2-NF02', - 'No refresh token endpoint found.', - 'No refresh token endpoint was in the OIDC provider meta data returned by the well-known ' + - 'endpoint or the refresh token endpoint passed to the SDK is empty.', - ); - } - - const body: string[] = []; - - body.push(`client_id=${configData.clientID}`); - body.push(`refresh_token=${sessionData.refresh_token}`); - body.push('grant_type=refresh_token'); - - if (configData.clientSecret && configData.clientSecret.trim().length > 0) { - body.push(`client_secret=${configData.clientSecret}`); - } - - let tokenResponse: Response; - - try { - tokenResponse = await fetch(tokenEndpoint, { - body: body.join('&'), - credentials: configData.sendCookiesInRequests ? 'include' : 'same-origin', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/x-www-form-urlencoded', - }, - method: 'POST', - }); - } catch (error: any) { - throw new AsgardeoAuthException( - 'JS-AUTH_CORE-RAT2-NR03', - 'Refresh access token request failed.', - error ?? 'The request to refresh the access token failed.', - ); - } - - if (!tokenResponse.ok) { - throw new AsgardeoAuthException( - 'JS-AUTH_CORE-RAT2-HE04', - `Refreshing access token failed with ${tokenResponse.statusText}`, - (await tokenResponse.json()) as string, - ); - } - - return this._authenticationHelper.handleTokenResponse(tokenResponse, userID); - } - - public async revokeAccessToken(userID?: string): Promise { - const revokeTokenEndpoint: string | undefined = (await this._oidcProviderMetaData()).revocation_endpoint; - const configData: StrictAuthClientConfig = await this._config(); - - if (!revokeTokenEndpoint || revokeTokenEndpoint.trim().length === 0) { - throw new AsgardeoAuthException( - 'JS-AUTH_CORE-RAT3-NF01', - 'No revoke access token endpoint found.', - 'No revoke access token endpoint was found in the OIDC provider meta data returned by ' + - 'the well-known endpoint or the revoke access token endpoint passed to the SDK is empty.', - ); - } - - const body: string[] = []; - - body.push(`client_id=${configData.clientID}`); - body.push(`token=${(await this._dataLayer.getSessionData(userID)).access_token}`); - body.push('token_type_hint=access_token'); - - if (configData.clientSecret && configData.clientSecret.trim().length > 0) { - body.push(`client_secret=${configData.clientSecret}`); - } - - let response: Response; - - try { - response = await fetch(revokeTokenEndpoint, { - body: body.join('&'), - credentials: configData.sendCookiesInRequests ? 'include' : 'same-origin', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/x-www-form-urlencoded', - }, - method: 'POST', - }); - } catch (error: any) { - throw new AsgardeoAuthException( - 'JS-AUTH_CORE-RAT3-NE02', - 'The request to revoke access token failed.', - error ?? 'The request sent to revoke the access token failed.', - ); - } - - if (response.status !== 200 || !response.ok) { - throw new AsgardeoAuthException( - 'JS-AUTH_CORE-RAT3-HE03', - `Invalid response status received for revoke access token request (${response.statusText}).`, - (await response.json()) as string, - ); - } - - this._authenticationHelper.clearUserSessionData(userID); - - return Promise.resolve(response); - } - - public async requestCustomGrant( - customGrantParams: CustomGrantConfig, - userID?: string, - ): Promise { - const oidcProviderMetadata: OIDCDiscoveryApiResponse = await this._oidcProviderMetaData(); - const configData: StrictAuthClientConfig = await this._config(); - - let tokenEndpoint: string | undefined; - - if (customGrantParams.tokenEndpoint && customGrantParams.tokenEndpoint.trim().length !== 0) { - tokenEndpoint = customGrantParams.tokenEndpoint; - } else { - tokenEndpoint = oidcProviderMetadata.token_endpoint; - } - - if (!tokenEndpoint || tokenEndpoint.trim().length === 0) { - throw new AsgardeoAuthException( - 'JS-AUTH_CORE-RCG-NF01', - 'Token endpoint not found.', - 'No token endpoint was found in the OIDC provider meta data returned by the well-known endpoint ' + - 'or the token endpoint passed to the SDK is empty.', - ); - } - - const data: string[] = await Promise.all( - Object.entries(customGrantParams.data).map(async ([key, value]: [key: string, value: any]) => { - const newValue: string = await this._authenticationHelper.replaceCustomGrantTemplateTags( - value as string, - userID, - ); - - return `${key}=${newValue}`; - }), - ); - - let requestHeaders: Record = { - Accept: 'application/json', - 'Content-Type': 'application/x-www-form-urlencoded', - }; - - if (customGrantParams.attachToken) { - requestHeaders = { - ...requestHeaders, - Authorization: `Bearer ${(await this._dataLayer.getSessionData(userID)).access_token}`, - }; - } - - const requestConfig: FetchRequestConfig = { - body: data.join('&'), - credentials: configData.sendCookiesInRequests ? 'include' : 'same-origin', - headers: new Headers(requestHeaders), - method: 'POST', - }; - - let response: Response; - - try { - response = await fetch(tokenEndpoint, requestConfig); - } catch (error: any) { - throw new AsgardeoAuthException( - 'JS-AUTH_CORE-RCG-NE02', - 'The custom grant request failed.', - error ?? 'The request sent to get the custom grant failed.', - ); - } - - if (response.status !== 200 || !response.ok) { - throw new AsgardeoAuthException( - 'JS-AUTH_CORE-RCG-HE03', - `Invalid response status received for the custom grant request. (${response.statusText})`, - (await response.json()) as string, - ); - } - - if (customGrantParams.returnsSession) { - return this._authenticationHelper.handleTokenResponse(response, userID); - } else { - return Promise.resolve((await response.json()) as TokenResponse | FetchResponse); - } - } - - public async getBasicUserInfo(userID?: string): Promise { - const sessionData: SessionData = await this._dataLayer.getSessionData(userID); - const authenticatedUser: AuthenticatedUserInfo = this._authenticationHelper.getAuthenticatedUserInfo( - sessionData?.id_token, - ); - - let basicUserInfo: BasicUserInfo = { - allowedScopes: sessionData.scope, - sessionState: sessionData.session_state, - }; - - Object.keys(authenticatedUser).forEach((key: string) => { - if (authenticatedUser[key] === undefined || authenticatedUser[key] === '' || authenticatedUser[key] === null) { - delete authenticatedUser[key]; - } - }); - - basicUserInfo = {...basicUserInfo, ...authenticatedUser}; - - return basicUserInfo; - } - - public async getDecodedIDToken(userID?: string): Promise { - const idToken: string = (await this._dataLayer.getSessionData(userID)).id_token; - const payload: IdTokenPayload = this._cryptoHelper.decodeIDToken(idToken); - - return payload; - } - - public async getCryptoHelper(): Promise { - return this._cryptoHelper; - } - - public async getIDToken(userID?: string): Promise { - return (await this._dataLayer.getSessionData(userID)).id_token; - } - - public async getOIDCProviderMetaData(forceInit: boolean): Promise { - const configData: StrictAuthClientConfig = await this._config(); - - if ( - !forceInit && - (await this._dataLayer.getTemporaryDataParameter( - OIDCDiscoveryConstants.Storage.StorageKeys.OPENID_PROVIDER_CONFIG_INITIATED, - )) - ) { - return Promise.resolve(); - } - - const wellKnownEndpoint: string = (configData as any).wellKnownEndpoint; - - if (wellKnownEndpoint) { - let response: Response; - - try { - response = await fetch(wellKnownEndpoint); - if (response.status !== 200 || !response.ok) { - throw new Error(); - } - } catch { - throw new AsgardeoAuthException( - 'JS-AUTH_CORE-GOPMD-HE01', - 'Invalid well-known response', - 'The well known endpoint response has been failed with an error.', - ); - } - - await this._dataLayer.setOIDCProviderMetaData( - await this._authenticationHelper.resolveEndpoints(await response.json()), - ); - await this._dataLayer.setTemporaryDataParameter( - OIDCDiscoveryConstants.Storage.StorageKeys.OPENID_PROVIDER_CONFIG_INITIATED, - true, - ); - - return Promise.resolve(); - } else if ((configData as any).baseUrl) { - try { - await this._dataLayer.setOIDCProviderMetaData(await this._authenticationHelper.resolveEndpointsByBaseURL()); - } catch (error: any) { - throw new AsgardeoAuthException( - 'JS-AUTH_CORE-GOPMD-IV02', - 'Resolving endpoints failed.', - error ?? 'Resolving endpoints by base url failed.', - ); - } - await this._dataLayer.setTemporaryDataParameter( - OIDCDiscoveryConstants.Storage.StorageKeys.OPENID_PROVIDER_CONFIG_INITIATED, - true, - ); - - return Promise.resolve(); - } else { - await this._dataLayer.setOIDCProviderMetaData(await this._authenticationHelper.resolveEndpointsExplicitly()); - - await this._dataLayer.setTemporaryDataParameter( - OIDCDiscoveryConstants.Storage.StorageKeys.OPENID_PROVIDER_CONFIG_INITIATED, - true, - ); - - return Promise.resolve(); - } - } - - // TODO: Remove `Partial` once the refactoring is done. - public async getOIDCServiceEndpoints(): Promise> { - const oidcProviderMetaData: OIDCDiscoveryApiResponse = await this._oidcProviderMetaData(); - - return { - authorizationEndpoint: oidcProviderMetaData.authorization_endpoint ?? '', - checkSessionIframe: oidcProviderMetaData.check_session_iframe ?? '', - endSessionEndpoint: oidcProviderMetaData.end_session_endpoint ?? '', - introspectionEndpoint: oidcProviderMetaData.introspection_endpoint ?? '', - issuer: oidcProviderMetaData.issuer ?? '', - jwksUri: oidcProviderMetaData.jwks_uri ?? '', - registrationEndpoint: oidcProviderMetaData.registration_endpoint ?? '', - revocationEndpoint: oidcProviderMetaData.revocation_endpoint ?? '', - tokenEndpoint: oidcProviderMetaData.token_endpoint ?? '', - userinfoEndpoint: oidcProviderMetaData.userinfo_endpoint ?? '', - }; - } - - public async getSignOutURL(userID?: string): Promise { - const logoutEndpoint: string | undefined = (await this._oidcProviderMetaData())?.end_session_endpoint; - const configData: StrictAuthClientConfig = await this._config(); - - if (!logoutEndpoint || logoutEndpoint.trim().length === 0) { - throw new AsgardeoAuthException( - 'JS-AUTH_CORE-GSOU-NF01', - 'Sign-out endpoint not found.', - 'No sign-out endpoint was found in the OIDC provider meta data returned by the well-known endpoint ' + - 'or the sign-out endpoint passed to the SDK is empty.', - ); - } - - const callbackURL: string = configData?.signOutRedirectURL ?? configData?.signInRedirectURL; - - if (!callbackURL || callbackURL.trim().length === 0) { - throw new AsgardeoAuthException( - 'JS-AUTH_CORE-GSOU-NF03', - 'No sign-out redirect URL found.', - 'The sign-out redirect URL cannot be found or the URL passed to the SDK is empty. ' + - 'No sign-in redirect URL has been found either. ', - ); - } - const queryParams: URLSearchParams = new URLSearchParams(); - - queryParams.set('post_logout_redirect_uri', callbackURL); - - if (configData.sendIdTokenInLogoutRequest) { - const idToken: string = (await this._dataLayer.getSessionData(userID))?.id_token; - - if (!idToken || idToken.trim().length === 0) { - throw new AsgardeoAuthException( - 'JS-AUTH_CORE-GSOU-NF02', - 'ID token not found.', - 'No ID token could be found. Either the session information is lost or you have not signed in.', - ); - } - queryParams.set('id_token_hint', idToken); - } else { - queryParams.set('client_id', configData.clientID); - } - - queryParams.set('state', OIDCRequestConstants.Params.SIGN_OUT_SUCCESS); - - return `${logoutEndpoint}?${queryParams.toString()}`; - } - - public async clearUserSessionData(userID?: string): Promise { - await this._authenticationHelper.clearUserSessionData(userID); - } - - public async getAccessToken(userID?: string): Promise { - return (await this._dataLayer.getSessionData(userID))?.access_token; - } - - /** - * The created timestamp of the token response in milliseconds. - * - * @param userID - User ID - * @returns Created at timestamp of the token response in milliseconds. - */ - public async getCreatedAt(userID?: string): Promise { - return (await this._dataLayer.getSessionData(userID))?.created_at; - } - - /** - * The expires timestamp of the token response in seconds. - * - * @param userID - User ID - * @returns Expires in timestamp of the token response in seconds. - */ - public async getExpiresIn(userID?: string): Promise { - return (await this._dataLayer.getSessionData(userID))?.expires_in; - } - - public async isAuthenticated(userID?: string): Promise { - const isAccessTokenAvailable: boolean = Boolean(await this.getAccessToken(userID)); - - // Check if the access token is expired. - const createdAt: number = await this.getCreatedAt(userID); - - // Get the expires in value. - const expiresInString: string = await this.getExpiresIn(userID); - - // If the expires in value is not available, the token is invalid and the user is not authenticated. - if (!expiresInString) { - return false; - } - - // Convert to milliseconds. - const expiresIn: number = parseInt(expiresInString) * 1000; - const currentTime: number = new Date().getTime(); - const isAccessTokenValid: boolean = createdAt + expiresIn > currentTime; - - const isAuthenticated: boolean = isAccessTokenAvailable && isAccessTokenValid; - - return isAuthenticated; - } - - public async getPKCECode(state: string, userID?: string): Promise { - return (await this._dataLayer.getTemporaryDataParameter(extractPkceStorageKeyFromState(state), userID)) as string; - } - - public async setPKCECode(pkce: string, state: string, userID?: string): Promise { - return await this._dataLayer.setTemporaryDataParameter(extractPkceStorageKeyFromState(state), pkce, userID); - } - - public async updateConfig(config: Partial>): Promise { - await this._dataLayer.setConfigData(config); - await this.getOIDCProviderMetaData(true); - } -} diff --git a/packages/javascript/src/__legacy__/helpers/authentication-helper.ts b/packages/javascript/src/__legacy__/helpers/authentication-helper.ts index 30e0e8b9..b3b2283f 100644 --- a/packages/javascript/src/__legacy__/helpers/authentication-helper.ts +++ b/packages/javascript/src/__legacy__/helpers/authentication-helper.ts @@ -17,12 +17,14 @@ */ import {IsomorphicCrypto} from '../../IsomorphicCrypto'; -import {DataLayer} from '../data'; -import {AsgardeoAuthException} from '../exception'; -import {AuthClientConfig, AuthenticatedUserInfo, SessionData, StrictAuthClientConfig} from '../models'; +import StorageManager from '../../StorageManager'; +import {AsgardeoAuthException} from '../../errors/exception'; +import {AuthClientConfig, StrictAuthClientConfig} from '../models'; +import {User} from '../../models/user'; +import {SessionData} from '../../models/session'; import {JWKInterface} from '../../models/crypto'; import {TokenResponse, AccessTokenApiResponse} from '../../models/token'; -import {IdTokenPayload} from '../../models/id-token'; +import {IdTokenPayload} from '../../models/token'; import PKCEConstants from '../../constants/PKCEConstants'; import extractTenantDomainFromIdTokenPayload from '../../utils/extractTenantDomainFromIdTokenPayload'; import extractUserClaimsFromIdToken from '../../utils/extractUserClaimsFromIdToken'; @@ -30,17 +32,18 @@ import ScopeConstants from '../../constants/ScopeConstants'; import OIDCDiscoveryConstants from '../../constants/OIDCDiscoveryConstants'; import TokenExchangeConstants from '../../constants/TokenExchangeConstants'; import {OIDCDiscoveryEndpointsApiResponse, OIDCDiscoveryApiResponse} from '../../models/oidc-discovery'; +import processOpenIDScopes from '../../utils/processOpenIDScopes'; export class AuthenticationHelper { - private _dataLayer: DataLayer; + private _storageManager: StorageManager; private _config: () => Promise; private _oidcProviderMetaData: () => Promise; private _cryptoHelper: IsomorphicCrypto; - public constructor(dataLayer: DataLayer, cryptoHelper: IsomorphicCrypto) { - this._dataLayer = dataLayer; - this._config = async () => await this._dataLayer.getConfigData(); - this._oidcProviderMetaData = async () => await this._dataLayer.getOIDCProviderMetaData(); + public constructor(storageManager: StorageManager, cryptoHelper: IsomorphicCrypto) { + this._storageManager = storageManager; + this._config = async () => await this._storageManager.getConfigData(); + this._oidcProviderMetaData = async () => await this._storageManager.loadOpenIDProviderConfiguration(); this._cryptoHelper = cryptoHelper; } @@ -150,7 +153,7 @@ export class AuthenticationHelper { } public async validateIdToken(idToken: string): Promise { - const jwksEndpoint: string | undefined = (await this._dataLayer.getOIDCProviderMetaData()).jwks_uri; + const jwksEndpoint: string | undefined = (await this._storageManager.loadOpenIDProviderConfiguration()).jwks_uri; const configData: StrictAuthClientConfig = await this._config(); if (!jwksEndpoint || jwksEndpoint.trim().length === 0) { @@ -195,17 +198,16 @@ export class AuthenticationHelper { return this._cryptoHelper.isValidIdToken( idToken, jwk, - (await this._config()).clientID, + (await this._config()).clientId, issuer ?? '', - this._cryptoHelper.decodeIDToken(idToken).sub, + this._cryptoHelper.decodeIdToken(idToken).sub, (await this._config()).clockTolerance, (await this._config()).validateIDTokenIssuer ?? true, ); } - public getAuthenticatedUserInfo(idToken: string): AuthenticatedUserInfo { - const payload: IdTokenPayload = this._cryptoHelper.decodeIDToken(idToken); - const tenantDomain: string = extractTenantDomainFromIdTokenPayload(payload); + public getAuthenticatedUserInfo(idToken: string): User { + const payload: IdTokenPayload = this._cryptoHelper.decodeIdToken(idToken); const username: string = payload?.['username'] ?? ''; const givenName: string = payload?.['given_name'] ?? ''; const familyName: string = payload?.['family_name'] ?? ''; @@ -215,23 +217,16 @@ export class AuthenticationHelper { return { displayName: displayName, - tenantDomain, username: username, ...extractUserClaimsFromIdToken(payload), }; } - public async replaceCustomGrantTemplateTags(text: string, userID?: string): Promise { - let scope: string = ScopeConstants.OPENID; + public async replaceCustomGrantTemplateTags(text: string, userId?: string): Promise { const configData: StrictAuthClientConfig = await this._config(); - const sessionData: SessionData = await this._dataLayer.getSessionData(userID); + const sessionData: SessionData = await this._storageManager.getSessionData(userId); - if (configData.scope && configData.scope.length > 0) { - if (!configData.scope.includes(ScopeConstants.OPENID)) { - configData.scope.push(ScopeConstants.OPENID); - } - scope = configData.scope.join(' '); - } + let scope: string = processOpenIDScopes(configData.scopes); return text .replace(TokenExchangeConstants.Placeholders.TOKEN, sessionData.access_token) @@ -240,16 +235,16 @@ export class AuthenticationHelper { this.getAuthenticatedUserInfo(sessionData.id_token).username, ) .replace(TokenExchangeConstants.Placeholders.SCOPE, scope) - .replace(TokenExchangeConstants.Placeholders.CLIENT_ID, configData.clientID) + .replace(TokenExchangeConstants.Placeholders.CLIENT_ID, configData.clientId) .replace(TokenExchangeConstants.Placeholders.CLIENT_SECRET, configData.clientSecret ?? ''); } - public async clearUserSessionData(userID?: string): Promise { - await this._dataLayer.removeTemporaryData(userID); - await this._dataLayer.removeSessionData(userID); + public async clearSession(userId?: string): Promise { + await this._storageManager.removeTemporaryData(userId); + await this._storageManager.removeSessionData(userId); } - public async handleTokenResponse(response: Response, userID?: string): Promise { + public async handleTokenResponse(response: Response, userId?: string): Promise { if (response.status !== 200 || !response.ok) { throw new AsgardeoAuthException( 'JS-AUTH_HELPER-HTR-NE01', @@ -267,7 +262,7 @@ export class AuthenticationHelper { if (shouldValidateIdToken) { return this.validateIdToken(parsedResponse.id_token).then(async () => { - await this._dataLayer.setSessionData(parsedResponse, userID); + await this._storageManager.setSessionData(parsedResponse, userId); const tokenResponse: TokenResponse = { accessToken: parsedResponse.access_token, @@ -292,7 +287,7 @@ export class AuthenticationHelper { tokenType: parsedResponse.token_type, }; - await this._dataLayer.setSessionData(parsedResponse, userID); + await this._storageManager.setSessionData(parsedResponse, userId); return Promise.resolve(tokenResponse); } diff --git a/packages/javascript/src/__legacy__/models/client-config.ts b/packages/javascript/src/__legacy__/models/client-config.ts index c8478e33..61b86ca2 100644 --- a/packages/javascript/src/__legacy__/models/client-config.ts +++ b/packages/javascript/src/__legacy__/models/client-config.ts @@ -16,19 +16,19 @@ * under the License. */ -import {ResponseMode} from '../../models/oauth-response'; +import {OAuthResponseMode} from '../../models/oauth-response'; import {OIDCEndpoints} from '../../models/oidc-endpoints'; export interface DefaultAuthClientConfig { - signInRedirectURL: string; - signOutRedirectURL?: string; + afterSignInUrl: string; + afterSignOutUrl?: string; clientHost?: string; - clientID: string; + clientId: string; clientSecret?: string; enablePKCE?: boolean; prompt?: string; - responseMode?: ResponseMode; - scope?: string[]; + responseMode?: OAuthResponseMode; + scopes?: string | string[] | undefined; validateIDToken?: boolean; validateIDTokenIssuer?: boolean; /** diff --git a/packages/javascript/src/__legacy__/models/fetch.ts b/packages/javascript/src/__legacy__/models/fetch.ts deleted file mode 100644 index ac7cf35d..00000000 --- a/packages/javascript/src/__legacy__/models/fetch.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright (c) 2020, WSO2 LLC. (https://www.wso2.com). All Rights Reserved. - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export type Method = - | "get" - | "GET" - | "delete" - | "DELETE" - | "head" - | "HEAD" - | "options" - | "OPTIONS" - | "post" - | "POST" - | "put" - | "PUT" - | "patch" - | "PATCH" - | "purge" - | "PURGE" - | "link" - | "LINK" - | "unlink" - | "UNLINK"; - -export type FetchCredentials = "omit" | "same-origin" | "include"; - -export type FetchRedirect = "follow" | "error" | "manual"; - -export interface FetchRequestConfig extends RequestInit { - method?: Method; - url?: string; - credentials?: FetchCredentials; - body?: any; // FIXME: Add proper type - bodyUsed?: boolean; - cache?: any; // FIXME: Add proper type - destination?: string; - integrity?: string; - mode?: any; // FIXME: Add proper type - redirect?: FetchRedirect; - referrer?: string; - referrerPolicy?: any; -} - -export interface FetchResponse extends ResponseInit { - body: T; - ok: boolean; - bodyUsed?: boolean; - redirected?: boolean; - type: any; // FIXME: Add proper type - url: string; - //TODO: Implement trailer property once the MDN docs are completed - json(); - text(); - formData(); - blob(); - arrayBuffer(); -} - -export interface FetchError extends Error { - config: FetchRequestConfig; - code?: string; - request?: any; - response?: FetchResponse; - isFetchError: boolean; - // eslint-disable-next-line @typescript-eslint/ban-types - toJSON: () => object; -} diff --git a/packages/javascript/src/__legacy__/models/index.ts b/packages/javascript/src/__legacy__/models/index.ts index 90b310ab..2d461ba2 100644 --- a/packages/javascript/src/__legacy__/models/index.ts +++ b/packages/javascript/src/__legacy__/models/index.ts @@ -17,8 +17,3 @@ */ export * from './client-config'; -export * from './data'; -export * from './custom-grant'; -export * from './authorization-url'; -export * from './user'; -export * from './fetch'; diff --git a/packages/javascript/src/__legacy__/models/user.ts b/packages/javascript/src/__legacy__/models/user.ts deleted file mode 100644 index a4b64e0e..00000000 --- a/packages/javascript/src/__legacy__/models/user.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Copyright (c) 2020, WSO2 LLC. (https://www.wso2.com). All Rights Reserved. - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * Interface containing the basic user information. - */ -export interface BasicUserInfo { - /** - * The email address of the user. - */ - email?: string | undefined; - /** - * The username of the user. - */ - username?: string | undefined; - /** - * The display name of the user. It is the preferred_username in the id token payload or the `sub`. - */ - displayName?: string | undefined; - /** - * The scopes allowed for the user. - */ - allowedScopes: string; - /** - * The tenant domain to which the user belongs. - */ - tenantDomain?: string | undefined; - /** - * The session state. - */ - sessionState: string; - /** - * The `uid` corresponding to the user who the ID token belongs to. - */ - sub?: string; - /** - * Any other attributes retrieved from teh `id_token`. - */ - [ key: string ]: any; -} - -/** - * Interface of the authenticated user. - */ -export interface AuthenticatedUserInfo { - /** - * Authenticated user's display name. - */ - displayName?: string | undefined; - /** - * Authenticated user's display name. - * @deprecated Use `displayName` instead. - */ - display_name?: string | undefined; - /** - * User's email. - */ - email?: string | undefined; - /** - * Available scopes. - */ - scope?: string | undefined; - /** - * Authenticated user's tenant domain. - */ - tenantDomain?: string | undefined; - /** - * Authenticated user's username. - */ - username: string; - [key: string]: any; -} diff --git a/packages/javascript/src/api/oidc/__tests__/getUserInfo.test.ts b/packages/javascript/src/api/__tests__/getUserInfo.test.ts similarity index 97% rename from packages/javascript/src/api/oidc/__tests__/getUserInfo.test.ts rename to packages/javascript/src/api/__tests__/getUserInfo.test.ts index 6ffa0ce0..de167261 100644 --- a/packages/javascript/src/api/oidc/__tests__/getUserInfo.test.ts +++ b/packages/javascript/src/api/__tests__/getUserInfo.test.ts @@ -18,8 +18,8 @@ import {describe, it, expect, vi, beforeEach} from 'vitest'; import getUserInfo from '../getUserInfo'; -import {User} from '../../../models/user'; -import AsgardeoAPIError from '../../../errors/AsgardeoAPIError'; +import {User} from '../../models/user'; +import AsgardeoAPIError from '../../errors/AsgardeoAPIError'; describe('getUserInfo', (): void => { beforeEach((): void => { diff --git a/packages/javascript/src/api/oidc/getUserInfo.ts b/packages/javascript/src/api/getUserInfo.ts similarity index 93% rename from packages/javascript/src/api/oidc/getUserInfo.ts rename to packages/javascript/src/api/getUserInfo.ts index 9d759fe6..37c71913 100644 --- a/packages/javascript/src/api/oidc/getUserInfo.ts +++ b/packages/javascript/src/api/getUserInfo.ts @@ -16,8 +16,8 @@ * under the License. */ -import {User} from '../../models/user'; -import AsgardeoAPIError from '../../errors/AsgardeoAPIError'; +import {User} from '../models/user'; +import AsgardeoAPIError from '../errors/AsgardeoAPIError'; /** * Retrieves the user information from the specified OIDC userinfo endpoint. @@ -70,7 +70,7 @@ const getUserInfo = async ({url, ...requestConfig}: Partial): Promise { + /** + * The base URL of the Asgardeo server. + */ + baseUrl?: string; + /** + * The authorization request payload. + */ + payload: ApplicationNativeAuthenticationHandleRequestPayload; +} + +const handleApplicationNativeAuthentication = async ({ + url, + baseUrl, + payload, + ...requestConfig +}: AuthorizeRequestConfig): Promise => { + if (!payload) { + throw new AsgardeoAPIError( + 'Authorization payload is required', + 'handleApplicationNativeAuthentication-ValidationError-002', + 'javascript', + 400, + 'If an authorization payload is not provided, the request cannot be constructed correctly.', + ); + } + + const {headers: customHeaders, ...otherConfig} = requestConfig; + const response: Response = await fetch(url ?? `${baseUrl}/oauth2/authn`, { + method: requestConfig.method || 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + ...customHeaders, + }, + body: JSON.stringify(payload), + ...otherConfig, + }); + + if (!response.ok) { + const errorText = await response.text(); + + throw new AsgardeoAPIError( + `Authorization request failed: ${errorText}`, + 'initializeApplicationNativeAuthentication-ResponseError-001', + 'javascript', + response.status, + response.statusText, + ); + } + + return (await response.json()) as ApplicationNativeAuthenticationHandleResponse; +}; + +export default handleApplicationNativeAuthentication; diff --git a/packages/javascript/src/api/initializeApplicationNativeAuthentication.ts b/packages/javascript/src/api/initializeApplicationNativeAuthentication.ts new file mode 100644 index 00000000..6f093be0 --- /dev/null +++ b/packages/javascript/src/api/initializeApplicationNativeAuthentication.ts @@ -0,0 +1,171 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import AsgardeoAPIError from '../errors/AsgardeoAPIError'; +import {ApplicationNativeAuthenticationInitiateResponse} from '../models/application-native-authentication'; + +/** + * Represents the authorization request payload that can be sent to the authorization endpoint. + */ +export interface AuthorizationRequest { + /** + * The response type (e.g., 'code', 'token', 'id_token'). + */ + response_type?: string; + /** + * The client identifier. + */ + client_id?: string; + /** + * The redirection URI after authorization. + */ + redirect_uri?: string; + /** + * The scope of the access request. + */ + scope?: string; + /** + * An unguessable random string to prevent CSRF attacks. + */ + state?: string; + /** + * String value used to associate a Client session with an ID Token. + */ + nonce?: string; + /** + * How the authorization response should be returned. + */ + response_mode?: string; + /** + * Space delimited, case sensitive list of ASCII string values. + */ + prompt?: string; + /** + * The allowable elapsed time in seconds since the last time the End-User was actively authenticated. + */ + max_age?: number; + /** + * PKCE code challenge. + */ + code_challenge?: string; + /** + * PKCE code challenge method. + */ + code_challenge_method?: string; + /** + * Additional authorization parameters. + */ + [key: string]: any; +} + +/** + * Request configuration for the authorize function. + */ +export interface AuthorizeRequestConfig extends Partial { + url?: string; + /** + * The base URL of the Asgardeo server. + */ + baseUrl?: string; + /** + * The authorization request payload. + */ + payload: AuthorizationRequest; +} + +/** + * Sends an authorization request to the specified OAuth2/OIDC authorization endpoint. + * + * @param requestConfig - Request configuration object containing URL and payload. + * @returns A promise that resolves with the authorization response. + * @throws AsgardeoAPIError when the request fails or URL is invalid. + * + * @example + * ```typescript + * try { + * const authResponse = await initializeApplicationNativeAuthentication({ + * url: "https://api.asgardeo.io/t//oauth2/authorize", + * payload: { + * response_type: "code", + * client_id: "your-client-id", + * redirect_uri: "https://your-app.com/callback", + * scope: "openid profile email", + * state: "random-state-value", + * code_challenge: "your-pkce-challenge", + * code_challenge_method: "S256" + * } + * }); + * console.log(authResponse); + * } catch (error) { + * if (error instanceof AsgardeoAPIError) { + * console.error('Authorization failed:', error.message); + * } + * } + * ``` + */ +const initializeApplicationNativeAuthentication = async ({ + url, + baseUrl, + payload, + ...requestConfig +}: AuthorizeRequestConfig): Promise => { + if (!payload) { + throw new AsgardeoAPIError( + 'Authorization payload is required', + 'initializeApplicationNativeAuthentication-ValidationError-002', + 'javascript', + 400, + 'If an authorization payload is not provided, the request cannot be constructed correctly.', + ); + } + + const searchParams = new URLSearchParams(); + Object.entries(payload).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + searchParams.append(key, String(value)); + } + }); + + const {headers: customHeaders, ...otherConfig} = requestConfig; + const response: Response = await fetch(url ?? `${baseUrl}/oauth2/authorize`, { + method: requestConfig.method || 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json', + ...customHeaders, + }, + body: searchParams.toString(), + ...otherConfig, + }); + + if (!response.ok) { + const errorText = await response.text(); + + throw new AsgardeoAPIError( + `Authorization request failed: ${errorText}`, + 'initializeApplicationNativeAuthentication-ResponseError-001', + 'javascript', + response.status, + response.statusText, + ); + } + + return (await response.json()) as ApplicationNativeAuthenticationInitiateResponse; +}; + +export default initializeApplicationNativeAuthentication; diff --git a/packages/javascript/src/constants/ApplicationNativeAuthenticationConstants.ts b/packages/javascript/src/constants/ApplicationNativeAuthenticationConstants.ts new file mode 100644 index 00000000..f0e60dc7 --- /dev/null +++ b/packages/javascript/src/constants/ApplicationNativeAuthenticationConstants.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Constants representing Application Native Authentication related configurations and constants. + */ +const ApplicationNativeAuthenticationConstants = { + SupportedAuthenticators: { + IdentifierFirst: 'SWRlbnRpZmllckV4ZWN1dG9yOkxPQ0FM', + EmailOtp: 'ZW1haWwtb3RwLWF1dGhlbnRpY2F0b3I6TE9DQUw', + Totp: 'dG90cDpMT0NBTA', + UsernamePassword: 'QmFzaWNBdXRoZW50aWNhdG9yOkxPQ0FM', + PushNotification: 'cHVzaC1ub3RpZmljYXRpb24tYXV0aGVudGljYXRvcjpMT0NBTA', + Passkey: 'RklET0F1dGhlbnRpY2F0b3I6TE9DQUw', + SmsOtp: 'c21zLW90cC1hdXRoZW50aWNhdG9yOkxPQ0FM', + MagicLink: 'TWFnaWNMaW5rQXV0aGVudGljYXRvcjpMT0NBTA', + Google: 'R29vZ2xlT0lEQ0F1dGhlbnRpY2F0b3I6R29vZ2xl', + GitHub: 'R2l0aHViQXV0aGVudGljYXRvcjpHaXRIdWI', + Microsoft: 'T3BlbklEQ29ubmVjdEF1dGhlbnRpY2F0b3I6TWljcm9zb2Z0', + Facebook: 'RmFjZWJvb2tBdXRoZW50aWNhdG9yOkZhY2Vib29r', + LinkedIn: 'TGlua2VkSW5PSURDOkxpbmtlZElu', + SignInWithEthereum: 'T3BlbklEQ29ubmVjdEF1dGhlbnRpY2F0b3I6U2lnbiBJbiBXaXRoIEV0aGVyZXVt', + }, +} as const; + +export default ApplicationNativeAuthenticationConstants; diff --git a/packages/javascript/src/constants/OIDCRequestConstants.ts b/packages/javascript/src/constants/OIDCRequestConstants.ts index 652b05e7..88b39a47 100644 --- a/packages/javascript/src/constants/OIDCRequestConstants.ts +++ b/packages/javascript/src/constants/OIDCRequestConstants.ts @@ -16,6 +16,8 @@ * under the License. */ +import ScopeConstants from "./ScopeConstants"; + /** * Constants representing standard OpenID Connect (OIDC) request and response parameters. * These parameters are commonly used during authorization, token exchange, and logout flows. @@ -45,7 +47,22 @@ const OIDCRequestConstants = { */ SIGN_OUT_SUCCESS: 'sign_out_success', }, - + + /** + * Constants related to the OpenID Connect (OIDC) sign-in flow. + */ + SignIn: { + /** + * Constants related to the payload of the OIDC sign-in request. + */ + Payload: { + /** + * The default scopes used in OIDC sign-in requests. + */ + DEFAULT_SCOPES: [ScopeConstants.OPENID], + }, + }, + /** * Sign-out related constants for managing the end-session flow in OIDC. */ diff --git a/packages/javascript/src/constants/PKCEConstants.ts b/packages/javascript/src/constants/PKCEConstants.ts index a70e8adc..7191c33e 100644 --- a/packages/javascript/src/constants/PKCEConstants.ts +++ b/packages/javascript/src/constants/PKCEConstants.ts @@ -34,6 +34,7 @@ * ``` */ const PKCEConstants = { + DEFAULT_CODE_CHALLENGE_METHOD: 'S256', /** * Storage-related constants for managing PKCE state */ diff --git a/packages/javascript/src/constants/TokenExchangeConstants.ts b/packages/javascript/src/constants/TokenExchangeConstants.ts index a8822564..efb731cf 100644 --- a/packages/javascript/src/constants/TokenExchangeConstants.ts +++ b/packages/javascript/src/constants/TokenExchangeConstants.ts @@ -61,7 +61,7 @@ const TokenExchangeConstants = { * Placeholder for client ID in token exchange operations. * Required for client authentication. */ - CLIENT_ID: '{{clientID}}', + CLIENT_ID: '{{clientId}}', /** * Placeholder for client secret in token exchange operations. diff --git a/packages/javascript/src/__legacy__/exception/exception.ts b/packages/javascript/src/errors/exception.ts similarity index 90% rename from packages/javascript/src/__legacy__/exception/exception.ts rename to packages/javascript/src/errors/exception.ts index ffb37249..191287ac 100644 --- a/packages/javascript/src/__legacy__/exception/exception.ts +++ b/packages/javascript/src/errors/exception.ts @@ -16,6 +16,9 @@ * under the License. */ +/** + * @deprecated Use `AsgardeoRuntimeError` for runtime errors and `AsgardeoAPIError` for API errors. + */ export class AsgardeoAuthException { public name: string; public code: string | undefined; diff --git a/packages/javascript/src/i18n/en-US.ts b/packages/javascript/src/i18n/en-US.ts new file mode 100644 index 00000000..163f5f40 --- /dev/null +++ b/packages/javascript/src/i18n/en-US.ts @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {I18nTranslations, I18nMetadata, I18nBundle} from '../models/i18n'; + +const translations: I18nTranslations = { + /* |---------------------------------------------------------------| */ + /* | Elements | */ + /* |---------------------------------------------------------------| */ + + //* Buttons */ + 'elements.buttons.signIn': 'Sign In', + 'elements.buttons.signOut': 'Sign Out', + 'elements.buttons.signUp': 'Sign Up', + 'elements.buttons.facebook': 'Continue with Facebook', + 'elements.buttons.google': 'Continue with Google', + 'elements.buttons.github': 'Continue with GitHub', + 'elements.buttons.microsoft': 'Continue with Microsoft', + 'elements.buttons.linkedin': 'Continue with LinkedIn', + 'elements.buttons.ethereum': 'Continue with Sign In Ethereum', + 'elements.buttons.multi.option': 'Continue with {connection}', + 'elements.buttons.social': 'Continue with {connection}', + + /* Fields */ + 'elements.fields.placeholder': 'Enter your {field}', + + /* |---------------------------------------------------------------| */ + /* | Widgets | */ + /* |---------------------------------------------------------------| */ + + /* Base Sign In */ + 'signin.title': 'Sign In', + + /* Email OTP */ + 'email.otp.title': 'OTP Verification', + 'email.otp.subtitle': 'Enter the code sent to your email address.', + 'email.otp.submit.button': 'Continue', + + /* Identifier First */ + 'identifier.first.title': 'Sign In', + 'identifier.first.subtitle': 'Enter your username or email address.', + 'identifier.first.submit.button': 'Continue', + + /* SMS OTP */ + 'sms.otp.title': 'OTP Verification', + 'sms.otp.subtitle': 'Enter the code sent to your phone number.', + 'sms.otp.submit.button': 'Continue', + + /* TOTP */ + 'totp.title': 'Verify Your Identity', + 'totp.subtitle': 'Enter the code from your authenticator app.', + 'totp.submit.button': 'Continue', + + /* Username Password */ + 'username.password.submit.button': 'Continue', + 'username.password.title': 'Sign In', + 'username.password.subtitle': 'Enter your username and password to continue.', + + /* |---------------------------------------------------------------| */ + /* | Messages | */ + /* |---------------------------------------------------------------| */ + + 'messages.loading': 'Loading...', + + /* |---------------------------------------------------------------| */ + /* | Errors | */ + /* |---------------------------------------------------------------| */ + + 'errors.title': 'Error', + 'errors.sign.in.initialization': 'An error occurred while initializing. Please try again later.', + 'errors.sign.in.flow.failure': 'An error occurred during the sign-in flow. Please try again later.', + 'errors.sign.in.flow.completion.failure': 'An error occurred while completing the sign-in flow. Please try again later.', + 'errors.sign.in.flow.passkeys.failure': 'An error occurred while signing in with passkeys. Please try again later.', + 'errors.sign.in.flow.passkeys.completion.failure': 'An error occurred while completing the passkeys sign-in flow. Please try again later.', +}; + +const metadata: I18nMetadata = { + localeCode: 'en-US', + countryCode: 'US', + languageCode: 'en', + displayName: 'English (United States)', + direction: 'ltr', +}; + +const en_US: I18nBundle = { + metadata, + translations, +}; + +export default en_US; diff --git a/packages/javascript/src/__legacy__/exception/index.ts b/packages/javascript/src/i18n/index.ts similarity index 84% rename from packages/javascript/src/__legacy__/exception/index.ts rename to packages/javascript/src/i18n/index.ts index 39dafe58..213b4262 100644 --- a/packages/javascript/src/__legacy__/exception/index.ts +++ b/packages/javascript/src/i18n/index.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020, WSO2 LLC. (https://www.wso2.com). All Rights Reserved. + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -16,4 +16,4 @@ * under the License. */ -export * from "./exception"; +export {default as en_US} from './en-US'; diff --git a/packages/javascript/src/index.ts b/packages/javascript/src/index.ts index 7a3ec267..348ac76d 100644 --- a/packages/javascript/src/index.ts +++ b/packages/javascript/src/index.ts @@ -18,36 +18,72 @@ export * from './__legacy__/client'; export * from './__legacy__/models'; -export * from './models/oauth-response'; + export * from './IsomorphicCrypto'; -export * from './__legacy__/exception'; -export * from './__legacy__/data'; -export {default as getUserInfo} from './api/oidc/getUserInfo'; +export {default as initializeApplicationNativeAuthentication} from './api/initializeApplicationNativeAuthentication'; +export {default as handleApplicationNativeAuthentication} from './api/handleApplicationNativeAuthentication'; +export {default as getUserInfo} from './api/getUserInfo'; +export {default as ApplicationNativeAuthenticationConstants} from './constants/ApplicationNativeAuthenticationConstants'; export {default as TokenConstants} from './constants/TokenConstants'; export {default as OIDCRequestConstants} from './constants/OIDCRequestConstants'; export {default as AsgardeoError} from './errors/AsgardeoError'; export {default as AsgardeoAPIError} from './errors/AsgardeoAPIError'; export {default as AsgardeoRuntimeError} from './errors/AsgardeoRuntimeError'; +export {AsgardeoAuthException} from './errors/exception'; +export { + ApplicationNativeAuthenticationInitiateResponse, + ApplicationNativeAuthenticationFlowStatus, + ApplicationNativeAuthenticationFlowType, + ApplicationNativeAuthenticationStepType, + ApplicationNativeAuthenticationAuthenticator, + ApplicationNativeAuthenticationLink, + ApplicationNativeAuthenticationHandleRequestPayload, + ApplicationNativeAuthenticationHandleResponse, + ApplicationNativeAuthenticationAuthenticatorParamType, + ApplicationNativeAuthenticationAuthenticatorPromptType, + ApplicationNativeAuthenticationAuthenticatorKnownIdPType, +} from './models/application-native-authentication'; export {AsgardeoClient, SignInOptions, SignOutOptions} from './models/client'; -export {BaseConfig, Config, Preferences, ThemePreferences} from './models/config'; -export {TokenResponse} from './models/token'; +export {BaseConfig, Config, Preferences, ThemePreferences, I18nPreferences, WithPreferences} from './models/config'; +export {TokenResponse, IdTokenPayload, TokenExchangeRequestConfig} from './models/token'; export {Crypto, JWKInterface} from './models/crypto'; -export {IdTokenPayload} from './models/id-token'; +export {OAuthResponseMode} from './models/oauth-response'; +export { + AuthorizeRequestUrlParams, + KnownExtendedAuthorizeRequestUrlParams, + ExtendedAuthorizeRequestUrlParams, +} from './models/oauth-request'; export {OIDCEndpoints} from './models/oidc-endpoints'; -export {Store} from './models/store'; -export {User} from './models/user'; -export {Schema, SchemaAttribute, WellKnownSchemaIds} from './models/scim2-schema'; +export {Storage, TemporaryStore} from './models/store'; +export {User, UserProfile} from './models/user'; +export {SessionData} from './models/session'; +export {Schema, SchemaAttribute, WellKnownSchemaIds, FlattenedSchema} from './models/scim2-schema'; export {RecursivePartial} from './models/utility-types'; +export {FieldType} from './models/field'; +export {I18nBundle, I18nTranslations, I18nMetadata} from './models/i18n'; export {default as AsgardeoJavaScriptClient} from './AsgardeoJavaScriptClient'; export {default as createTheme} from './theme/createTheme'; export {ThemeColors, ThemeConfig, Theme, ThemeMode} from './theme/types'; +export {default as deepMerge} from './utils/deepMerge'; export {default as extractUserClaimsFromIdToken} from './utils/extractUserClaimsFromIdToken'; export {default as extractPkceStorageKeyFromState} from './utils/extractPkceStorageKeyFromState'; +export {default as flattenUserSchema} from './utils/flattenUserSchema'; +export {default as generateUserProfile} from './utils/generateUserProfile'; +export {default as getLatestStateParam} from './utils/getLatestStateParam'; +export {default as generateFlattenedUserProfile} from './utils/generateFlattenedUserProfile'; +export {default as getI18nBundles} from './utils/getI18nBundles'; +export {default as set} from './utils/set'; +export {default as get} from './utils/get'; export {default as removeTrailingSlash} from './utils/removeTrailingSlash'; +export {default as resolveFieldType} from './utils/resolveFieldType'; +export {default as resolveFieldName} from './utils/resolveFieldName'; +export {default as processOpenIDScopes} from './utils/processOpenIDScopes'; + +export {default as StorageManager} from './StorageManager'; diff --git a/packages/javascript/src/models/application-native-authentication.ts b/packages/javascript/src/models/application-native-authentication.ts new file mode 100644 index 00000000..42591330 --- /dev/null +++ b/packages/javascript/src/models/application-native-authentication.ts @@ -0,0 +1,93 @@ +export interface ApplicationNativeAuthenticationInitiateResponse { + flowId: string; + flowStatus: ApplicationNativeAuthenticationFlowStatus; + flowType: ApplicationNativeAuthenticationFlowType; + nextStep: { + stepType: ApplicationNativeAuthenticationStepType; + authenticators: ApplicationNativeAuthenticationAuthenticator[]; + }; + links: ApplicationNativeAuthenticationLink[]; +} + +export enum ApplicationNativeAuthenticationFlowStatus { + SuccessCompleted = 'SUCCESS_COMPLETED', + FailCompleted = 'FAIL_COMPLETED', + FailIncomplete = 'FAIL_INCOMPLETE', + Incomplete = 'INCOMPLETE', +} + +export enum ApplicationNativeAuthenticationFlowType { + Authentication = 'AUTHENTICATION', +} + +export enum ApplicationNativeAuthenticationStepType { + AuthenticatorPrompt = 'AUTHENTICATOR_PROMPT', + MultOptionsPrompt = 'MULTI_OPTIONS_PROMPT', +} + +export interface ApplicationNativeAuthenticationAuthenticator { + authenticatorId: string; + authenticator: string; + idp: string; + metadata: { + i18nKey: string; + promptType: ApplicationNativeAuthenticationAuthenticatorPromptType; + params: { + param: string; + type: ApplicationNativeAuthenticationAuthenticatorParamType; + order: number; + i18nKey: string; + displayName: string; + confidential: boolean; + }[]; + }; + requiredParams: string[]; +} + +export interface ApplicationNativeAuthenticationLink { + name: string; + href: string; + method: string; +} + +export interface ApplicationNativeAuthenticationHandleRequestPayload { + flowId: string; + selectedAuthenticator: { + authenticatorId: string; + params: Record; + }; +} + +export interface ApplicationNativeAuthenticationHandleResponse { + flowStatus: string; + authData: Record; +} + +export enum ApplicationNativeAuthenticationAuthenticatorParamType { + String = 'STRING', + Integer = 'INTEGER', + MultiValued = 'MULTI_VALUED', +} + +export enum ApplicationNativeAuthenticationAuthenticatorExtendedParamType { + Otp = 'OTPCode', +} + +export enum ApplicationNativeAuthenticationAuthenticatorKnownIdPType { + Local = 'LOCAL', +} + +export enum ApplicationNativeAuthenticationAuthenticatorPromptType { + /** + * Prompt for user input, typically for username/password or similar credentials. + */ + UserPrompt = 'USER_PROMPT', + /** + * Prompt for internal system use, such as API keys or tokens. + */ + InternalPrompt = 'INTERNAL_PROMPT', + /** + * Prompt for redirection to another page or service. + */ + RedirectionPrompt = 'REDIRECTION_PROMPT', +} diff --git a/packages/javascript/src/models/client.ts b/packages/javascript/src/models/client.ts index 3f06fc08..bc7545b6 100644 --- a/packages/javascript/src/models/client.ts +++ b/packages/javascript/src/models/client.ts @@ -16,7 +16,7 @@ * under the License. */ -import {User} from './user'; +import {User, UserProfile} from './user'; export type SignInOptions = Record; export type SignOutOptions = Record; @@ -39,6 +39,13 @@ export interface AsgardeoClient { */ getUser(): Promise; + /** + * Fetches the user profile along with its schemas and a flattened version of the profile. + * + * @returns A promise resolving to a UserProfile object containing the user's profile information. + */ + getUserProfile(): Promise; + /** * Initializes the authentication client with provided configuration. * @@ -57,7 +64,7 @@ export interface AsgardeoClient { /** * Checks if a user is signed in. - * FIXME: This should be integrated with the existing isAuthenticated method which returns a Promise. + * FIXME: This should be integrated with the existing isSignedIn method which returns a Promise. * * @returns Boolean indicating sign-in status. */ diff --git a/packages/javascript/src/models/config.ts b/packages/javascript/src/models/config.ts index 740065e5..cbf3652d 100644 --- a/packages/javascript/src/models/config.ts +++ b/packages/javascript/src/models/config.ts @@ -17,9 +17,10 @@ */ import {ThemeConfig, ThemeMode} from '../theme/types'; -import { RecursivePartial } from './utility-types'; +import {I18nBundle} from './i18n'; +import {RecursivePartial} from './utility-types'; -export interface BaseConfig { +export interface BaseConfig extends WithPreferences { /** * Optional URL where the authorization server should redirect after authentication. * This must match one of the allowed redirect URIs configured in your IdP. @@ -63,7 +64,9 @@ export interface BaseConfig { * scopes: ["openid", "profile", "email"] */ scopes?: string | string[] | undefined; +} +export interface WithPreferences { /** * Preferences for customizing the Asgardeo UI components */ @@ -83,9 +86,32 @@ export interface ThemePreferences { overrides?: RecursivePartial; } +export interface I18nPreferences { + /** + * The language to use for translations. + * Defaults to the browser's default language. + */ + language?: string; + /** + * The fallback language to use if translations are not available in the specified language. + * Defaults to 'en-US'. + */ + fallbackLanguage?: string; + /** + * Custom translations to override default ones. + */ + bundles?: { + [key: string]: I18nBundle; + }; +} + export interface Preferences { /** * Theme preferences for the Asgardeo UI components */ theme?: ThemePreferences; + /** + * Internationalization preferences for the Asgardeo UI components + */ + i18n?: I18nPreferences; } diff --git a/packages/javascript/src/models/crypto.ts b/packages/javascript/src/models/crypto.ts index 85c43557..036f4d65 100644 --- a/packages/javascript/src/models/crypto.ts +++ b/packages/javascript/src/models/crypto.ts @@ -32,14 +32,14 @@ export interface JWKInterface { * Cryptographic utility interface for OIDC operations. * Provides methods for encoding, decoding, hashing, and JWT verification * used in OAuth2/OIDC flows. - * + * * @remarks * This interface abstracts cryptographic operations needed for: * - PKCE challenge/verifier generation * - JWT token validation * - Base64URL encoding/decoding * - Secure random number generation - * + * * @example * ```typescript * class MyCrypto implements Crypto { @@ -93,7 +93,7 @@ export interface Crypto { * @param idToken - ID Token to be verified. * @param jwk - JWK to be used for verification. * @param algorithms - Algorithms to be used for verification. - * @param clientID - Client ID to be used for verification. + * @param clientId - Client ID to be used for verification. * @param issuer - Issuer to be used for verification. * @param subject - Subject to be used for verification. * @param clockTolerance - Clock tolerance to be used for verification. @@ -106,7 +106,7 @@ export interface Crypto { idToken: string, jwk: JWKInterface, algorithms: string[], - clientID: string, + clientId: string, issuer: string, subject: string, clockTolerance?: number, diff --git a/packages/nextjs/src/__tests__/greet.test.ts b/packages/javascript/src/models/field.ts similarity index 72% rename from packages/nextjs/src/__tests__/greet.test.ts rename to packages/javascript/src/models/field.ts index c3ad90ee..b046b05d 100644 --- a/packages/nextjs/src/__tests__/greet.test.ts +++ b/packages/javascript/src/models/field.ts @@ -16,11 +16,16 @@ * under the License. */ -import {describe, expect, it} from 'vitest'; -import greet from '../greet'; - -describe('greet', () => { - it('should return the proper greeting', () => { - expect(greet('World')).toBe('Hello, World!'); - }); -}); +export enum FieldType { + Text = 'TEXT', + Password = 'PASSWORD', + Email = 'EMAIL', + Number = 'NUMBER', + Select = 'SELECT', + Checkbox = 'CHECKBOX', + Radio = 'RADIO', + Otp = 'OTP', + Date = 'DATE', + Time = 'TIME', + Textarea = 'TEXTAREA', +} diff --git a/packages/javascript/src/models/i18n.ts b/packages/javascript/src/models/i18n.ts new file mode 100644 index 00000000..321f0767 --- /dev/null +++ b/packages/javascript/src/models/i18n.ts @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export interface I18nTranslations { + /* |---------------------------------------------------------------| */ + /* | Elements | */ + /* |---------------------------------------------------------------| */ + + //* Buttons */ + 'elements.buttons.signIn': string; + 'elements.buttons.signOut': string; + 'elements.buttons.signUp': string; + 'elements.buttons.facebook': string; + 'elements.buttons.google': string; + 'elements.buttons.github': string; + 'elements.buttons.microsoft': string; + 'elements.buttons.linkedin': string; + 'elements.buttons.ethereum': string; + 'elements.buttons.multi.option': string; + 'elements.buttons.social': string; + + /* Fields */ + 'elements.fields.placeholder': string; + + /* |---------------------------------------------------------------| */ + /* | Widgets | */ + /* |---------------------------------------------------------------| */ + + /* Base Sign In */ + 'signin.title': string; + + /* Email OTP */ + 'email.otp.title': string; + 'email.otp.subtitle': string; + 'email.otp.submit.button': string; + + /* Identifier First */ + 'identifier.first.title': string; + 'identifier.first.subtitle': string; + 'identifier.first.submit.button': string; + + /* SMS OTP */ + 'sms.otp.title': string; + 'sms.otp.subtitle': string; + 'sms.otp.submit.button': string; + + /* TOTP */ + 'totp.title': string; + 'totp.subtitle': string; + 'totp.submit.button': string; + + /* Username Password */ + 'username.password.submit.button': string; + 'username.password.title': string; + 'username.password.subtitle': string; + + /* |---------------------------------------------------------------| */ + /* | Messages | */ + /* |---------------------------------------------------------------| */ + + 'messages.loading': string; + + /* |---------------------------------------------------------------| */ + /* | Errors | */ + /* |---------------------------------------------------------------| */ + + 'errors.title': string; + 'errors.sign.in.initialization': string; + 'errors.sign.in.flow.failure': string; + 'errors.sign.in.flow.completion.failure': string; + 'errors.sign.in.flow.passkeys.failure': string; + 'errors.sign.in.flow.passkeys.completion.failure': string; +} + +export type I18nTextDirection = 'ltr' | 'rtl'; + +export interface I18nMetadata { + localeCode: string; + countryCode: string; + languageCode: string; + displayName: string; + direction: I18nTextDirection; +} + +export interface I18nBundle { + metadata: I18nMetadata; + translations: I18nTranslations; +} diff --git a/packages/javascript/src/models/id-token.ts b/packages/javascript/src/models/id-token.ts deleted file mode 100644 index 5953b028..00000000 --- a/packages/javascript/src/models/id-token.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright (c) 2020, WSO2 LLC. (https://www.wso2.com). All Rights Reserved. - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * Interface for the standard (required) claims of an ID Token payload. - */ -export interface IdTokenPayloadStandardClaims { - /** - * The audience for which this token is intended. - */ - aud: string | string[]; - - /** - * The unique identifier of the user to whom the ID token belongs. - */ - sub: string; - - /** - * The issuer identifier for the issuer of the response. - */ - iss: string; - - /** - * The email of the user. - */ - email?: string; - - /** - * The username the user prefers to be called. - */ - preferred_username?: string; - - /** - * The tenant domain of the user. - */ - tenant_domain?: string; -} - -/** - * Interface for ID Token payload including custom claims. - */ -export interface IdTokenPayload extends IdTokenPayloadStandardClaims { - /** - * Other custom claims. - */ - [claim: string]: any; -} diff --git a/packages/javascript/src/__legacy__/models/authorization-url.ts b/packages/javascript/src/models/oauth-request.ts similarity index 68% rename from packages/javascript/src/__legacy__/models/authorization-url.ts rename to packages/javascript/src/models/oauth-request.ts index 46287f51..a5ca4f50 100644 --- a/packages/javascript/src/__legacy__/models/authorization-url.ts +++ b/packages/javascript/src/models/oauth-request.ts @@ -16,11 +16,12 @@ * under the License. */ -export type AuthorizationURLParams = Omit; +export type AuthorizeRequestUrlParams = Omit; -export interface StrictGetAuthURLConfig { - fidp?: string; - forceInit?: boolean; +export interface KnownExtendedAuthorizeRequestUrlParams { + fidp?: string; + forceInit?: boolean; } -export type GetAuthURLConfig = StrictGetAuthURLConfig & Record; +export type ExtendedAuthorizeRequestUrlParams = KnownExtendedAuthorizeRequestUrlParams & + Record; diff --git a/packages/javascript/src/models/oauth-response.ts b/packages/javascript/src/models/oauth-response.ts index 593e5c98..4d7269c7 100644 --- a/packages/javascript/src/models/oauth-response.ts +++ b/packages/javascript/src/models/oauth-response.ts @@ -16,22 +16,4 @@ * under the License. */ -/** - * Enum representing different OAuth response modes. - */ -export enum ResponseMode { - /** - * Response is returned as POST parameters in an HTML form. - */ - FormPost = 'form_post', - - /** - * Response is returned as query parameters in the URL. - */ - Query = 'query', - - /** - * Response is returned directly to the client. - */ - Direct = 'direct', -} +export type OAuthResponseMode = 'form_post' | 'query' | 'direct'; diff --git a/packages/javascript/src/models/scim2-schema.ts b/packages/javascript/src/models/scim2-schema.ts index 697debd5..0a6fe200 100644 --- a/packages/javascript/src/models/scim2-schema.ts +++ b/packages/javascript/src/models/scim2-schema.ts @@ -48,6 +48,10 @@ export interface Schema { attributes: SchemaAttribute[]; } +export interface FlattenedSchema extends Schema { + schemaId: string; +} + /** * Well-known SCIM2 schema IDs */ diff --git a/packages/javascript/src/__legacy__/models/data.ts b/packages/javascript/src/models/session.ts similarity index 73% rename from packages/javascript/src/__legacy__/models/data.ts rename to packages/javascript/src/models/session.ts index 54423225..33d88ccb 100644 --- a/packages/javascript/src/__legacy__/models/data.ts +++ b/packages/javascript/src/models/session.ts @@ -17,12 +17,17 @@ */ export interface SessionData { - access_token: string; - id_token: string; - expires_in: string; - scope: string; - refresh_token?: string; - token_type: string; - session_state: string; - created_at: number; + access_token: string; + id_token: string; + expires_in: string; + scope: string; + refresh_token?: string; + token_type: string; + session_state: string; + created_at: number; +} + +export interface UserSession { + scopes: string[]; + sessionState: string; } diff --git a/packages/javascript/src/models/store.ts b/packages/javascript/src/models/store.ts index 986b8376..23882ad8 100644 --- a/packages/javascript/src/models/store.ts +++ b/packages/javascript/src/models/store.ts @@ -23,7 +23,7 @@ import {OIDCEndpoints} from './oidc-endpoints'; * Implementations can include various storage backends like browser storage, * memory cache, or distributed caches like Redis or Memcached. */ -export interface Store { +export interface Storage { /** * Stores a value with the specified key. * diff --git a/packages/javascript/src/models/token.ts b/packages/javascript/src/models/token.ts index 10ffc0b5..5b95f425 100644 --- a/packages/javascript/src/models/token.ts +++ b/packages/javascript/src/models/token.ts @@ -133,3 +133,58 @@ export interface AccessTokenApiResponse { */ token_type: string; } + +/** + * Interface for the standard (required) claims of an ID Token payload. + */ +export interface IdTokenPayloadStandardClaims { + /** + * The audience for which this token is intended. + */ + aud: string | string[]; + + /** + * The unique identifier of the user to whom the ID token belongs. + */ + sub: string; + + /** + * The issuer identifier for the issuer of the response. + */ + iss: string; + + /** + * The email of the user. + */ + email?: string; + + /** + * The username the user prefers to be called. + */ + preferred_username?: string; + + /** + * The tenant domain of the user. + */ + tenant_domain?: string; +} + +/** + * Interface for ID Token payload including custom claims. + */ +export interface IdTokenPayload extends IdTokenPayloadStandardClaims { + /** + * Other custom claims. + */ + [claim: string]: any; +} + +export interface TokenExchangeRequestConfig { + id: string; + data: any; + signInRequired: boolean; + attachToken: boolean; + returnsSession: boolean; + tokenEndpoint?: string; + shouldReplayAfterRefresh?: boolean; +} diff --git a/packages/javascript/src/models/user.ts b/packages/javascript/src/models/user.ts index ee1f0222..726cffdc 100644 --- a/packages/javascript/src/models/user.ts +++ b/packages/javascript/src/models/user.ts @@ -18,100 +18,20 @@ import {Schema} from './scim2-schema'; -/** - * Type for the Meta object in SCIM2 responses - */ -export interface UserMeta { - created: string; - location: string; - lastModified: string; - resourceType: string; +export interface KnownUser { + username?: string; + email?: string; + givenName?: string; + familyName?: string; + displayName?: string; } -/** - * Type for the Role object in SCIM2 responses - */ -export interface UserRole { - audienceValue: string; - display: string; - audienceType: string; - value: string; - $ref: string; - audienceDisplay: string; -} - -/** - * Type for the Name object in SCIM2 responses - */ -export interface UserName { - formatted: string; - givenName: string; - familyName: string; +export interface User extends KnownUser { + [key: string]: any; } -/** - * Represents a user in the Asgardeo system. - * - * @remarks - * This interface defines the user properties based on the SCIM2 schema. - * WSO2 specific properties are flattened to the root level for easier access. - * - * @example - * ```typescript - * const user: User = { - * emails: ["user@example.com"], - * profileUrl: "https://gravatar.com/avatar/123", - * meta: { - * created: "2021-06-21T11:28:21.800618Z", - * location: "https://api.asgardeo.io/scim2/Users/123", - * lastModified: "2025-04-30T06:46:48.520896Z", - * resourceType: "User" - * }, - * name: { - * formatted: "John Smith", - * givenName: "John", - * familyName: "Smith" - * }, - * userName: "john@example.com", - * id: "123", - * // Flattened WSO2 schema properties - * accountState: "UNLOCKED", - * accountLocked: "false", - * photoUrl: "https://gravatar.com/avatar/123", - * emailVerified: "true", - * lastLogonTime: "1749098996152" - * }; - * ``` - */ -export interface User { - // Base SCIM2 properties - emails: string[]; - profileUrl?: string; - meta: UserMeta; +export interface UserProfile { schemas: Schema[]; - roles?: UserRole[]; - name: UserName; - id: string; - userName: string; - - // Flattened WSO2 schema properties - accountState?: string; - accountLocked?: string; - failedLoginLockoutCount?: string; - failedTOTPAttempts?: string; - failedLoginAttempts?: string; - totpEnabled?: string; - photoUrl?: string; - emailVerified?: string; - backupCodeEnabled?: string; - lastLogonTime?: string; - signedUpRegion?: string; - unlockTime?: string; - isReadOnlyUser?: string; - failedLoginAttemptsBeforeSuccess?: string; - - // Allow additional properties - [key: string]: any; + profile: User; + flattenedProfile: User; } - -export default User; diff --git a/packages/javascript/src/theme/createTheme.ts b/packages/javascript/src/theme/createTheme.ts index cfce1a21..7cac18a2 100644 --- a/packages/javascript/src/theme/createTheme.ts +++ b/packages/javascript/src/theme/createTheme.ts @@ -30,7 +30,7 @@ const lightTheme: ThemeConfig = { contrastText: '#ffffff', }, background: { - surface: '#f5f5f5', + surface: '#ffffff', disabled: '#f0f0f0', body: { main: '#1a1a1a', @@ -40,7 +40,6 @@ const lightTheme: ThemeConfig = { main: '#d32f2f', contrastText: '#ffffff', }, - surface: '#f5f5f5', text: { primary: '#1a1a1a', secondary: '#666666', @@ -78,7 +77,6 @@ const darkTheme: ThemeConfig = { main: '#d32f2f', contrastText: '#ffffff', }, - surface: '#2d2d2d', text: { primary: '#ffffff', secondary: '#b3b3b3', @@ -108,7 +106,6 @@ const toCssVariables = (theme: RecursivePartial): Record { + it('should merge simple objects', (): void => { + const target = {a: 1, b: 2}; + const source = {b: 3, c: 4}; + const result = deepMerge(target, source as any); + + expect(result).toEqual({a: 1, b: 3, c: 4}); + expect(result).not.toBe(target); // Should create a new object + }); + + it('should merge nested objects recursively', (): void => { + const target = { + a: 1, + b: { + x: 1, + y: 2, + }, + }; + const source = { + b: { + y: 3, + z: 4, + }, + c: 3, + }; + const result = deepMerge(target, source as any); + + expect(result).toEqual({ + a: 1, + b: {x: 1, y: 3, z: 4}, + c: 3, + }); + }); + + it('should handle deeply nested objects', (): void => { + const target = { + theme: { + colors: { + primary: 'blue', + secondary: 'green', + }, + spacing: { + small: 8, + }, + }, + }; + const source = { + theme: { + colors: { + secondary: 'red', + accent: 'yellow', + }, + typography: { + fontSize: 16, + }, + }, + }; + const result = deepMerge(target, source as any); + + expect(result).toEqual({ + theme: { + colors: { + primary: 'blue', + secondary: 'red', + accent: 'yellow', + }, + spacing: { + small: 8, + }, + typography: { + fontSize: 16, + }, + }, + }); + }); + + it('should replace arrays entirely instead of merging', (): void => { + const target = {arr: [1, 2, 3]}; + const source = {arr: [4, 5]}; + const result = deepMerge(target, source); + + expect(result).toEqual({arr: [4, 5]}); + }); + + it('should handle multiple sources', (): void => { + const target = {a: 1, b: {x: 1}}; + const source1 = {b: {y: 2}, c: 3}; + const source2 = {b: {z: 3}, d: 4}; + const result = deepMerge(target, source1 as any, source2 as any); + + expect(result).toEqual({ + a: 1, + b: {x: 1, y: 2, z: 3}, + c: 3, + d: 4, + }); + }); + + it('should handle undefined and null sources', (): void => { + const target = {a: 1, b: 2}; + const result = deepMerge(target, undefined, null as any, {c: 3} as any); + + expect(result).toEqual({a: 1, b: 2, c: 3}); + }); + + it('should handle empty objects', (): void => { + const target = {}; + const source = {a: 1, b: {x: 2}}; + const result = deepMerge(target, source); + + expect(result).toEqual({a: 1, b: {x: 2}}); + }); + + it('should not modify the original objects', (): void => { + const target = {a: 1, b: {x: 1}}; + const source = {b: {y: 2}, c: 3}; + const originalTarget = JSON.parse(JSON.stringify(target)); + const originalSource = JSON.parse(JSON.stringify(source)); + + deepMerge(target, source as any); + + expect(target).toEqual(originalTarget); + expect(source).toEqual(originalSource); + }); + + it('should handle special object types correctly', (): void => { + const date = new Date('2023-01-01'); + const regex = /test/g; + const target = {a: 1}; + const source = { + date: date, + regex: regex, + func: () => 'test', + }; + const result = deepMerge(target, source as any); + + expect((result as any).date).toBe(date); + expect((result as any).regex).toBe(regex); + expect(typeof (result as any).func).toBe('function'); + }); + + it('should handle nested special objects', (): void => { + const target = { + config: { + timeout: 1000, + }, + }; + const source = { + config: { + date: new Date('2023-01-01'), + patterns: [/test/g, /example/i], + }, + }; + const result = deepMerge(target, source as any); + + expect((result as any).config.timeout).toBe(1000); + expect((result as any).config.date).toBeInstanceOf(Date); + expect(Array.isArray((result as any).config.patterns)).toBe(true); + }); + + it('should handle boolean and number values', (): void => { + const target = {enabled: true, count: 5}; + const source = {enabled: false, count: 10, active: true}; + const result = deepMerge(target, source); + + expect(result).toEqual({enabled: false, count: 10, active: true}); + }); + + it('should handle string values', (): void => { + const target = {name: 'John', nested: {title: 'Mr.'}}; + const source = {name: 'Jane', nested: {title: 'Ms.', surname: 'Doe'}}; + const result = deepMerge(target, source); + + expect(result).toEqual({ + name: 'Jane', + nested: {title: 'Ms.', surname: 'Doe'}, + }); + }); + + it('should throw error for non-object target', (): void => { + expect(() => deepMerge(null as any)).toThrow('Target must be an object'); + expect(() => deepMerge(undefined as any)).toThrow('Target must be an object'); + expect(() => deepMerge('string' as any)).toThrow('Target must be an object'); + expect(() => deepMerge(123 as any)).toThrow('Target must be an object'); + }); + + it('should handle complex real-world scenario', (): void => { + const defaultConfig = { + api: { + baseUrl: 'https://api.example.com', + timeout: 5000, + retries: 3, + }, + ui: { + theme: { + colors: { + primary: '#007bff', + secondary: '#6c757d', + }, + spacing: { + xs: 4, + sm: 8, + md: 16, + }, + }, + components: { + button: { + borderRadius: 4, + }, + }, + }, + features: { + analytics: true, + debug: false, + }, + }; + + const userConfig = { + api: { + baseUrl: 'https://custom-api.example.com', + headers: { + 'X-Custom': 'value', + }, + }, + ui: { + theme: { + colors: { + primary: '#ff0000', + }, + spacing: { + lg: 32, + }, + }, + components: { + input: { + borderWidth: 2, + }, + }, + }, + features: { + debug: true, + experimental: true, + }, + }; + + const result = deepMerge(defaultConfig, userConfig as any); + + expect(result).toEqual({ + api: { + baseUrl: 'https://custom-api.example.com', + timeout: 5000, + retries: 3, + headers: { + 'X-Custom': 'value', + }, + }, + ui: { + theme: { + colors: { + primary: '#ff0000', + secondary: '#6c757d', + }, + spacing: { + xs: 4, + sm: 8, + md: 16, + lg: 32, + }, + }, + components: { + button: { + borderRadius: 4, + }, + input: { + borderWidth: 2, + }, + }, + }, + features: { + analytics: true, + debug: true, + experimental: true, + }, + }); + }); +}); diff --git a/packages/javascript/src/utils/__tests__/getAuthorizeRequestUrlParams.test.ts b/packages/javascript/src/utils/__tests__/getAuthorizeRequestUrlParams.test.ts new file mode 100644 index 00000000..e51a995a --- /dev/null +++ b/packages/javascript/src/utils/__tests__/getAuthorizeRequestUrlParams.test.ts @@ -0,0 +1,179 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {describe, it, expect, vi} from 'vitest'; +import getAuthorizeRequestUrlParams from '../getAuthorizeRequestUrlParams'; +import OIDCRequestConstants from '../../constants/OIDCRequestConstants'; +import ScopeConstants from '../../constants/ScopeConstants'; +import AsgardeoRuntimeError from '../../errors/AsgardeoRuntimeError'; + +vi.mock( + '../generateStateParamForRequestCorrelation', + (): { + default: (pkceKey: string, state?: string) => string; + } => ({ + default: (pkceKey: string, state: string) => `${state || ''}_request_${pkceKey.split('_').pop()}`, + }), +); + +describe('getAuthorizeRequestUrlParams', (): void => { + const pkceKey: string = 'pkce_code_verifier_1'; + + it('should include openid in scope (array)', (): void => { + const params: Map = getAuthorizeRequestUrlParams( + { + redirectUri: 'https://app/callback', + clientId: 'client123', + scope: 'profile', // pass as string, not array + }, + {key: pkceKey}, + {}, + ); + + expect(params.get('scope')).toContain('openid'); + expect(params.get('client_id')).toBe('client123'); + expect(params.get('redirect_uri')).toBe('https://app/callback'); + }); + + it('should include openid in scope (string)', (): void => { + const params: Map = getAuthorizeRequestUrlParams( + { + redirectUri: 'https://app/callback', + clientId: 'client123', + scope: 'profile', + }, + {key: pkceKey}, + {}, + ); + + expect(params.get('scope')).toContain('openid'); + }); + + it('should not duplicate openid in scope', (): void => { + const params: Map = getAuthorizeRequestUrlParams( + { + redirectUri: 'https://app/callback', + clientId: 'client123', + scope: 'openid profile', + }, + {key: pkceKey}, + {}, + ); + const scopes: string[] | undefined = params.get('scope')?.split(' '); + + expect(scopes?.filter((s: string): boolean => s === 'openid').length).toBe(1); + }); + + it('should set response_mode if provided', (): void => { + const params: Map = getAuthorizeRequestUrlParams( + { + redirectUri: 'https://app/callback', + clientId: 'client123', + responseMode: 'fragment', + }, + {key: pkceKey}, + {}, + ); + + expect(params.get('response_mode')).toBe('fragment'); + }); + + it('should set code_challenge and code_challenge_method if provided', (): void => { + const params: Map = getAuthorizeRequestUrlParams( + { + redirectUri: 'https://app/callback', + clientId: 'client123', + codeChallenge: 'abc', + codeChallengeMethod: 'S256', + }, + {key: pkceKey}, + {}, + ); + + expect(params.get('code_challenge')).toBe('abc'); + expect(params.get('code_challenge_method')).toBe('S256'); + }); + + it('should throw if code_challenge is provided without code_challenge_method', (): void => { + expect((): void => { + const params: Map = getAuthorizeRequestUrlParams( + { + redirectUri: 'https://app/callback', + clientId: 'client123', + codeChallenge: 'abc', + }, + {key: pkceKey}, + {}, + ); + }).toThrow(AsgardeoRuntimeError); + }); + + it('should set prompt if provided', (): void => { + const params: Map = getAuthorizeRequestUrlParams( + { + redirectUri: 'https://app/callback', + clientId: 'client123', + prompt: 'login', + }, + {key: pkceKey}, + {}, + ); + + expect(params.get('prompt')).toBe('login'); + }); + + it('should add custom params except state', (): void => { + const params: Map = getAuthorizeRequestUrlParams( + { + redirectUri: 'https://app/callback', + clientId: 'client123', + }, + {key: pkceKey}, + {foo: 'bar', [OIDCRequestConstants.Params.STATE]: 'shouldNotAppear'}, + ); + + expect(params.get('foo')).toBe('bar'); + expect(params.get(OIDCRequestConstants.Params.STATE)).not.toBe('shouldNotAppear'); + }); + + it('should generate state param using pkceKey and custom state', (): void => { + const params: Map = getAuthorizeRequestUrlParams( + { + redirectUri: 'https://app/callback', + clientId: 'client123', + }, + {key: pkceKey}, + {[OIDCRequestConstants.Params.STATE]: 'customState'}, + ); + + expect(params.get(OIDCRequestConstants.Params.STATE)).toBe('customState_request_1'); + }); + + it('should default to openid scope if none provided', (): void => { + const params: Map = getAuthorizeRequestUrlParams( + { + redirectUri: 'https://app/callback', + clientId: 'client123', + }, + {key: pkceKey}, + {}, + ); + + expect(params.get('scope')).toBe(ScopeConstants.OPENID); + }); +}); diff --git a/packages/javascript/src/utils/deepMerge.ts b/packages/javascript/src/utils/deepMerge.ts new file mode 100644 index 00000000..667688af --- /dev/null +++ b/packages/javascript/src/utils/deepMerge.ts @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Checks if a value is a plain object (not an array, function, date, etc.) + * + * @param value - The value to check + * @returns True if the value is a plain object + */ +const isPlainObject = (value: unknown): value is Record => { + return ( + typeof value === 'object' && + value !== null && + !Array.isArray(value) && + !(value instanceof Date) && + !(value instanceof RegExp) && + Object.prototype.toString.call(value) === '[object Object]' + ); +}; + +/** + * Recursively merges the properties of source objects into a target object. + * Similar to Lodash's merge function, this creates a deep copy and merges + * nested objects recursively. Arrays and non-plain objects are replaced entirely. + * + * @param target - The target object to merge into + * @param sources - One or more source objects to merge from + * @returns A new object with merged properties + * + * @example + * ```typescript + * const obj1 = { a: 1, b: { x: 1, y: 2 } }; + * const obj2 = { b: { y: 3, z: 4 }, c: 3 }; + * const result = deepMerge(obj1, obj2); + * // Result: { a: 1, b: { x: 1, y: 3, z: 4 }, c: 3 } + * ``` + * + * @example + * ```typescript + * const config = { theme: { colors: { primary: 'blue' } } }; + * const userPrefs = { theme: { colors: { secondary: 'red' } } }; + * const merged = deepMerge(config, userPrefs); + * // Result: { theme: { colors: { primary: 'blue', secondary: 'red' } } } + * ``` + */ +const deepMerge = >( + target: T, + ...sources: Array | undefined | null> +): T => { + if (!target || typeof target !== 'object') { + throw new Error('Target must be an object'); + } + + const result = {...target} as T; + + sources.forEach(source => { + if (!source || typeof source !== 'object') { + return; + } + + Object.keys(source).forEach(key => { + const sourceValue = source[key]; + const targetValue = (result as any)[key]; + + if (isPlainObject(sourceValue) && isPlainObject(targetValue)) { + (result as any)[key] = deepMerge(targetValue, sourceValue); + } else if (sourceValue !== undefined) { + (result as any)[key] = sourceValue; + } + }); + }); + + return result; +}; + +export default deepMerge; diff --git a/packages/javascript/src/utils/extractTenantDomainFromIdTokenPayload.ts b/packages/javascript/src/utils/extractTenantDomainFromIdTokenPayload.ts index e2018d3e..accc4f3f 100644 --- a/packages/javascript/src/utils/extractTenantDomainFromIdTokenPayload.ts +++ b/packages/javascript/src/utils/extractTenantDomainFromIdTokenPayload.ts @@ -16,7 +16,7 @@ * under the License. */ -import {IdTokenPayload} from '../models/id-token'; +import {IdTokenPayload} from '../models/token'; /** * Extracts the tenant domain from the ID token payload. diff --git a/packages/javascript/src/utils/extractUserClaimsFromIdToken.ts b/packages/javascript/src/utils/extractUserClaimsFromIdToken.ts index 998941b9..5927f051 100644 --- a/packages/javascript/src/utils/extractUserClaimsFromIdToken.ts +++ b/packages/javascript/src/utils/extractUserClaimsFromIdToken.ts @@ -16,7 +16,7 @@ * under the License. */ -import {IdTokenPayload} from '../models/id-token'; +import {IdTokenPayload} from '../models/token'; /** * Removes standard protocol-specific claims from the ID token payload @@ -24,7 +24,7 @@ import {IdTokenPayload} from '../models/id-token'; * * @param payload The raw ID token payload. * @returns A cleaned-up, camelCased object containing only user-specific claims. - * + * * @example * ````typescript * const idTokenPayload = { @@ -34,7 +34,7 @@ import {IdTokenPayload} from '../models/id-token'; * iat: 1712345670, * email: 'user@example.com' * }; - * + * * const userClaims = extractUserClaimsFromIdToken(idTokenPayload); * // // userClaims will be: * // { diff --git a/packages/javascript/src/utils/flattenUserSchema.ts b/packages/javascript/src/utils/flattenUserSchema.ts new file mode 100644 index 00000000..f3d08d6e --- /dev/null +++ b/packages/javascript/src/utils/flattenUserSchema.ts @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {Schema, FlattenedSchema} from '../models/scim2-schema'; + +/** + * Flattens nested schema attributes into a flat structure for easier processing + * + * This function processes SCIM2 schemas and creates a flattened representation by: + * - Processing sub-attributes and creating dot-notation names (e.g., 'name.givenName') + * - Adding schema ID reference to each flattened attribute + * - Preserving all original attribute properties while adding schema context + * - Only including leaf-level attributes (sub-attributes) and top-level simple attributes + * + * @param schemas - Array of SCIM2 schemas containing nested attribute structures + * @returns Array of flattened schema attributes with dot-notation names and schema references + * + * @example + * ```typescript + * const schemas = [ + * { + * id: 'urn:ietf:params:scim:schemas:core:2.0:User', + * attributes: [ + * { + * name: 'userName', + * type: 'string', + * multiValued: false + * }, + * { + * name: 'name', + * type: 'complex', + * multiValued: false, + * subAttributes: [ + * { name: 'givenName', type: 'string', multiValued: false }, + * { name: 'familyName', type: 'string', multiValued: false } + * ] + * } + * ] + * } + * ]; + * + * const flattened = flattenUserSchema(schemas); + * // Result: [ + * // { name: 'userName', type: 'string', multiValued: false, schemaId: 'urn:ietf:params:scim:schemas:core:2.0:User' }, + * // { name: 'name.givenName', type: 'string', multiValued: false, schemaId: 'urn:ietf:params:scim:schemas:core:2.0:User' }, + * // { name: 'name.familyName', type: 'string', multiValued: false, schemaId: 'urn:ietf:params:scim:schemas:core:2.0:User' } + * // ] + * ``` + */ +const flattenUserSchema = (schemas: Schema[]): FlattenedSchema[] => { + const flattenedAttributes: FlattenedSchema[] = []; + + schemas.forEach(schema => { + if (schema.attributes && Array.isArray(schema.attributes)) { + schema.attributes.forEach(attribute => { + // If the attribute has sub-attributes, only add the flattened sub-attributes + if (attribute.subAttributes && Array.isArray(attribute.subAttributes)) { + attribute.subAttributes.forEach(subAttribute => { + flattenedAttributes.push({ + ...subAttribute, + name: `${attribute.name}.${subAttribute.name}`, + schemaId: schema.id, + } as unknown as FlattenedSchema); + }); + } else { + // If it's a simple attribute (no sub-attributes), add it directly + flattenedAttributes.push({ + ...attribute, + schemaId: schema.id, + } as unknown as FlattenedSchema); + } + }); + } + }); + + return flattenedAttributes; +}; + +export default flattenUserSchema; diff --git a/packages/javascript/src/utils/generateFlattenedUserProfile.ts b/packages/javascript/src/utils/generateFlattenedUserProfile.ts new file mode 100644 index 00000000..3ea6d692 --- /dev/null +++ b/packages/javascript/src/utils/generateFlattenedUserProfile.ts @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import get from './get'; +import {User} from '../models/user'; + +/** + * Generates a flattened user profile from a response object and schema definitions. + * + * This function processes user data according to schema specifications, creating + * a flat object with dot notation keys instead of nested objects. Multi-valued + * properties and type-specific defaults are handled appropriately. + * + * @param meResponse - The response object containing user data + * @param processedSchemas - Array of schema objects defining field properties + * @param processedSchemas[].name - The field name/path for the property + * @param processedSchemas[].type - The data type of the field (e.g., 'STRING') + * @param processedSchemas[].multiValued - Whether the field can contain multiple values + * + * @returns A flattened user profile object with dot notation keys + * + * @example + * ```typescript + * const schemas = [ + * { name: 'name.givenName', type: 'STRING', multiValued: false }, + * { name: 'emails', type: 'STRING', multiValued: true } + * ]; + * const response = { name: { givenName: 'John' }, emails: 'john@example.com' }; + * const profile = generateFlattenedUserProfile(response, schemas); + * // Result: { "name.givenName": 'John', emails: ['john@example.com'] } + * ``` + */ +const generateFlattenedUserProfile = (meResponse: any, processedSchemas: any[]): User => { + const profile: User = {}; + + const allSchemaNames = processedSchemas.map(schema => schema.name).filter(Boolean); + + processedSchemas.forEach(schema => { + const {name, type, multiValued} = schema; + + if (!name) return; + + // Skip this property if it's a parent of other flattened properties + // e.g., skip "name" if "name.givenName" or "name.familyName" exists + // skip "roles" if "roles.default" exists + const hasChildProperties = allSchemaNames.some( + schemaName => schemaName !== name && schemaName.startsWith(name + '.'), + ); + + if (hasChildProperties) { + // Skip this parent property + return; + } + + let value = get(meResponse, name); + + if (value !== undefined) { + if (multiValued && !Array.isArray(value)) { + value = [value]; + } + } else { + if (multiValued) { + value = undefined; + } else if (type === 'STRING') { + value = ''; + } else { + value = undefined; + } + } + + profile[name] = value; + }); + + return profile; +}; + +export default generateFlattenedUserProfile; diff --git a/packages/javascript/src/utils/generateUserProfile.ts b/packages/javascript/src/utils/generateUserProfile.ts new file mode 100644 index 00000000..c158d225 --- /dev/null +++ b/packages/javascript/src/utils/generateUserProfile.ts @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import get from './get'; +import set from './set'; +import {User} from '../models/user'; + +/** + * Creates a profile structure from ME response based on processed schemas + * + * This function processes each schema attribute and populates the profile dynamically by: + * - Extracting values from the ME response using the schema attribute names + * - Handling multi-valued attributes by converting single values to arrays when needed + * - Setting appropriate default values based on schema type and multiValued properties + * - Using dynamic property setting to build the final profile object + * + * @param meResponse - The ME API response containing user data + * @param processedSchemas - The processed and flattened schemas with name, type, and multiValued properties + * @returns Flat profile object with dynamically populated user attributes + * + * @example + * ```typescript + * const meResponse = { + * userName: 'john.doe', + * emails: ['john@example.com', 'john.doe@work.com'], + * name: { givenName: 'John', familyName: 'Doe' } + * }; + * + * const schemas = [ + * { name: 'userName', type: 'STRING', multiValued: false }, + * { name: 'emails', type: 'STRING', multiValued: true }, + * { name: 'name.givenName', type: 'STRING', multiValued: false } + * ]; + * + * const profile = generateUserProfile(meResponse, schemas); + * // Result: { + * // userName: 'john.doe', + * // emails: ['john@example.com', 'john.doe@work.com'], + * // 'name.givenName': 'John' + * // } + * ``` + */ +const generateUserProfile = (meResponse: any, processedSchemas: any[]): User => { + const profile: User = {}; + + processedSchemas.forEach(schema => { + const {name, type, multiValued} = schema; + + if (!name) return; + + let value = get(meResponse, name); + + if (value !== undefined) { + if (multiValued && !Array.isArray(value)) { + value = [value]; + } + } else { + if (multiValued) { + value = undefined; + } else if (type === 'STRING') { + value = ''; + } else { + value = undefined; + } + } + + set(profile, name, value); + }); + + return profile; +}; + +export default generateUserProfile; diff --git a/packages/javascript/src/utils/get.ts b/packages/javascript/src/utils/get.ts new file mode 100644 index 00000000..1a796f7e --- /dev/null +++ b/packages/javascript/src/utils/get.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Gets the value at path of object. If the resolved value is undefined, + * the defaultValue is returned in its place. + * Similar to Lodash's get() function + * + * @param object - The object to query + * @param path - The path of the property to get + * @param defaultValue - The value returned for undefined resolved values + * @returns The resolved value + */ +const get = (object: any, path: string | string[], defaultValue?: any): any => { + if (!object || !path) return defaultValue; + + const pathArray = Array.isArray(path) ? path : path.split('.'); + + const result = pathArray.reduce((current, key) => { + return current?.[key]; + }, object); + + return result !== undefined ? result : defaultValue; +}; + +export default get; diff --git a/packages/javascript/src/utils/getAuthorizeRequestUrlParams.ts b/packages/javascript/src/utils/getAuthorizeRequestUrlParams.ts new file mode 100644 index 00000000..f588467c --- /dev/null +++ b/packages/javascript/src/utils/getAuthorizeRequestUrlParams.ts @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import ScopeConstants from '../constants/ScopeConstants'; +import OIDCRequestConstants from '../constants/OIDCRequestConstants'; +import generateStateParamForRequestCorrelation from './generateStateParamForRequestCorrelation'; +import {ExtendedAuthorizeRequestUrlParams} from '../models/oauth-request'; +import AsgardeoRuntimeError from '../errors/AsgardeoRuntimeError'; + +/** + * Generates a map of authorization request URL parameters for OIDC authorization requests. + * + * This utility ensures the `openid` scope is always included, handles both string and array forms of the `scope` parameter, + * and supports PKCE and custom parameters. Throws if a code challenge is provided without a code challenge method. + * + * @param options - The main options for the authorization request, including redirectUri, clientId, scope, responseMode, codeChallenge, codeChallengeMethod, and prompt. + * @param pkceOptions - PKCE options, including the PKCE key for state correlation. + * @param customParams - Optional custom parameters to include in the request (excluding the `state` param, which is handled separately). + * @returns A Map of key-value pairs representing the authorization request URL parameters. + * + * @throws {AsgardeoRuntimeError} If a code challenge is provided without a code challenge method. + * + * @example + * const params = getAuthorizeRequestUrlParams({ + * options: { + * redirectUri: 'https://app/callback', + * clientId: 'client123', + * scope: ['openid', 'profile'], + * responseMode: 'query', + * codeChallenge: 'abc', + * codeChallengeMethod: 'S256', + * prompt: 'login' + * }, + * pkceOptions: { key: 'pkce_code_verifier_1' }, + * customParams: { foo: 'bar' } + * }); + * // Returns a Map with all required OIDC params, PKCE, and custom params. + */ +const getAuthorizeRequestUrlParams = ( + options: { + redirectUri: string; + clientId: string; + scopes?: string; + responseMode?: string; + codeChallenge?: string; + codeChallengeMethod?: string; + prompt?: string; + } & ExtendedAuthorizeRequestUrlParams, + pkceOptions: {key: string}, + customParams: Record, +): Map => { + const {redirectUri, clientId, scopes, responseMode, codeChallenge, codeChallengeMethod, prompt} = options; + const authorizeRequestParams: Map = new Map(); + + authorizeRequestParams.set('response_type', 'code'); + authorizeRequestParams.set('client_id', clientId as string); + + authorizeRequestParams.set('scope', scopes); + authorizeRequestParams.set('redirect_uri', redirectUri as string); + + if (responseMode) { + authorizeRequestParams.set('response_mode', responseMode as string); + } + + const pkceKey: string = pkceOptions?.key; + + if (codeChallenge) { + authorizeRequestParams.set('code_challenge', codeChallenge as string); + + if (codeChallengeMethod) { + authorizeRequestParams.set('code_challenge_method', codeChallengeMethod as string); + } else { + throw new AsgardeoRuntimeError( + 'Code challenge method is required when code challenge is provided.', + 'getAuthorizeRequestUrlParams-ValidationError-001', + 'javascript', + 'When PKCE is enabled, the code challenge method must be provided along with the code challenge.', + ); + } + } + + if (prompt) { + authorizeRequestParams.set('prompt', prompt as string); + } + + if (customParams) { + for (const [key, value] of Object.entries(customParams)) { + if (key !== '' && value !== '' && key !== OIDCRequestConstants.Params.STATE) { + authorizeRequestParams.set(key, value.toString()); + } + } + } + + authorizeRequestParams.set( + OIDCRequestConstants.Params.STATE, + generateStateParamForRequestCorrelation( + pkceKey, + customParams ? customParams[OIDCRequestConstants.Params.STATE]?.toString() : '', + ), + ); + + return authorizeRequestParams; +}; + +export default getAuthorizeRequestUrlParams; diff --git a/packages/react/src/__tests__/greet.test.tsx b/packages/javascript/src/utils/getI18nBundles.ts similarity index 63% rename from packages/react/src/__tests__/greet.test.tsx rename to packages/javascript/src/utils/getI18nBundles.ts index 75aaf494..009c92bf 100644 --- a/packages/react/src/__tests__/greet.test.tsx +++ b/packages/javascript/src/utils/getI18nBundles.ts @@ -16,14 +16,17 @@ * under the License. */ -import {describe, expect, test} from 'vitest'; -import {render} from 'vitest-browser-react'; -import Greet from '../Greet'; +import {I18nBundle, I18nTranslations} from '../models/i18n'; +import * as i18n from '../i18n'; -describe('Greet', () => { - test('should return the proper greeting', async () => { - const {getByText, getByRole} = render(); +/** + * Get internationalization bundles for the specified locale. + * + * @param locale - The locale to get the bundle for (defaults to 'en-US') + * @returns The i18n bundle for the specified locale + */ +const getI18nBundles = () => { + return i18n; +}; - await expect.element(getByText('Hello World!')).toBeInTheDocument(); - }); -}); +export default getI18nBundles; diff --git a/packages/javascript/src/utils/getLatestStateParam.ts b/packages/javascript/src/utils/getLatestStateParam.ts new file mode 100644 index 00000000..b024cea0 --- /dev/null +++ b/packages/javascript/src/utils/getLatestStateParam.ts @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import PKCEConstants from '../constants/PKCEConstants'; +import {TemporaryStore} from '../models/store'; +import generateStateParamForRequestCorrelation from './generateStateParamForRequestCorrelation'; + +/** + * Gets the latest PKCE storage key from the temporary store. + * + * @param tempStore - The object that holds temporary PKCE-related data (e.g., sessionStorage). + * @returns The latest PKCE storage key or null if no keys exist. + */ +const getLatestPkceStorageKey = (tempStore: TemporaryStore): string | null => { + const keys: string[] = []; + + Object.keys(tempStore).forEach((key: string) => { + if (key.startsWith(PKCEConstants.Storage.StorageKeys.CODE_VERIFIER)) { + keys.push(key); + } + }); + + const lastKey: string | undefined = keys.sort().pop(); + + return lastKey ?? null; +}; + +/** + * Finds the latest state parameter based on the most recent PKCE storage key. + * + * This utility combines the functionality of finding the latest PKCE key and generating + * the corresponding state parameter for request correlation. + * + * @param tempStore - The object that holds temporary PKCE-related data (e.g., sessionStorage). + * @param state - Optional state string to prepend to the request correlation. + * @returns The latest state parameter string or null if no PKCE keys exist. + * + * @example + * const latestState = getLatestStateParam(sessionStorage, "myState"); + * // Returns: "myState_request_2" (if latest PKCE key is pkce_code_verifier_2) + * + * const latestStateNoPrefix = getLatestStateParam(sessionStorage); + * // Returns: "request_2" (if latest PKCE key is pkce_code_verifier_2) + * + * const noKeys = getLatestStateParam(emptyStorage); + * // Returns: null (if no PKCE keys exist) + */ +const getLatestStateParam = (tempStore: TemporaryStore, state?: string): string | null => { + const latestPkceKey = getLatestPkceStorageKey(tempStore); + + if (!latestPkceKey) { + return null; + } + + return generateStateParamForRequestCorrelation(latestPkceKey, state); +}; + +export default getLatestStateParam; diff --git a/packages/javascript/src/utils/processOpenIDScopes.ts b/packages/javascript/src/utils/processOpenIDScopes.ts new file mode 100644 index 00000000..2c55ab96 --- /dev/null +++ b/packages/javascript/src/utils/processOpenIDScopes.ts @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import OIDCRequestConstants from '../constants/OIDCRequestConstants'; +import AsgardeoRuntimeError from '../errors/AsgardeoRuntimeError'; + +/** + * Processes OpenID scopes to ensure they are in the correct format. + * If the input is a string, it returns it as is. + * If the input is an array, it joins the elements into a single string separated by spaces. + * If the input is neither, it throws an error. + * + * @param scopes - The OpenID scopes to process, which can be a string or an array of strings. + * @returns A string of OpenID scopes separated by spaces. + * + * @example + * ```typescript + * processOpenIDScopes("openid profile email"); // returns "openid profile email" + * processOpenIDScopes(["openid", "profile", "email"]); // returns "openid profile email" + * processOpenIDScopes(123); // throws AsgardeoRuntimeError + * processOpenIDScopes({}); // throws AsgardeoRuntimeError + * ``` + */ +const processOpenIDScopes = (scopes: string | string[]): string => { + let processedScopes: string[] = []; + + if (Array.isArray(scopes)) { + processedScopes = scopes; + } else if (typeof scopes === 'string') { + processedScopes = scopes.split(' '); + } else { + throw new AsgardeoRuntimeError( + 'Scopes must be a string or an array of strings.', + 'processOpenIDScopes-Invalid-001', + 'javascript', + 'The provided scopes are not in the expected format. Please provide a string or an array of strings.', + ); + } + + OIDCRequestConstants.SignIn.Payload.DEFAULT_SCOPES.forEach((defaultScope: string) => { + if (!processedScopes.includes(defaultScope)) { + processedScopes.push(defaultScope); + } + }); + + return processedScopes.join(' '); +}; + +export default processOpenIDScopes; diff --git a/packages/javascript/src/utils/resolveFieldName.ts b/packages/javascript/src/utils/resolveFieldName.ts new file mode 100644 index 00000000..e8572c91 --- /dev/null +++ b/packages/javascript/src/utils/resolveFieldName.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import AsgardeoRuntimeError from '../errors/AsgardeoRuntimeError'; +import {ApplicationNativeAuthenticationAuthenticatorExtendedParamType} from '../models/application-native-authentication'; +import {FieldType} from '../models/field'; + +const resolveFieldName = (field: any): string => { + if (field.param) { + return field.param; + } + + throw new AsgardeoRuntimeError( + 'Field name is not supported: ', + 'resolveFieldName-Invalid-001', + 'javascript', + 'The provided field name is not supported. Please check the field configuration.', + ); +}; + +export default resolveFieldName; diff --git a/packages/javascript/src/utils/resolveFieldType.ts b/packages/javascript/src/utils/resolveFieldType.ts new file mode 100644 index 00000000..29cd4ab0 --- /dev/null +++ b/packages/javascript/src/utils/resolveFieldType.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import AsgardeoRuntimeError from '../errors/AsgardeoRuntimeError'; +import { + ApplicationNativeAuthenticationAuthenticatorExtendedParamType, + ApplicationNativeAuthenticationAuthenticatorParamType, +} from '../models/application-native-authentication'; +import {FieldType} from '../models/field'; + +const resolveFieldType = (field: any): FieldType => { + if (field.type === ApplicationNativeAuthenticationAuthenticatorParamType.String) { + // Check if there's a `param` property and if it matches a known type. + if (field.param === ApplicationNativeAuthenticationAuthenticatorExtendedParamType.Otp) { + return FieldType.Otp; + } else if (field?.confidential) { + return FieldType.Password; + } + + return FieldType.Text; + } + + throw new AsgardeoRuntimeError( + 'Field type is not supported: ' + field.type, + 'resolveFieldType-Invalid-001', + 'javascript', + 'The provided field type is not supported. Please check the field configuration.', + ); +}; + +export default resolveFieldType; diff --git a/packages/javascript/src/utils/set.ts b/packages/javascript/src/utils/set.ts new file mode 100644 index 00000000..18c42ce8 --- /dev/null +++ b/packages/javascript/src/utils/set.ts @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Sets the value at path of object. If a portion of path doesn't exist, + * it's created. Arrays are created for missing index properties while + * objects are created for all other missing properties. + * Similar to Lodash's set() function + * + * @param object - The object to modify + * @param path - The path of the property to set + * @param value - The value to set + * @returns The object + */ +const set = (object: any, path: string | string[], value: any): any => { + if (!object || !path) return object; + + const pathArray = Array.isArray(path) ? path : path.split('.'); + const lastIndex = pathArray.length - 1; + + pathArray.reduce((current, key, index) => { + if (index === lastIndex) { + current[key] = value; + } else { + if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) { + // Create array if next key is numeric, otherwise create object + const nextKey = pathArray[index + 1]; + current[key] = /^\d+$/.test(nextKey) ? [] : {}; + } + } + return current[key]; + }, object); + + return object; +}; + +export default set; diff --git a/packages/javascript/tsconfig.lib.json b/packages/javascript/tsconfig.lib.json index c05b615f..b2c9c1ae 100644 --- a/packages/javascript/tsconfig.lib.json +++ b/packages/javascript/tsconfig.lib.json @@ -16,5 +16,5 @@ "**/*.spec.jsx", "**/*.test.jsx" ], - "include": ["src/**/*.js", "src/**/*.ts", "types/**/*.d.ts", "../react/src/getUserProfile.ts"] + "include": ["src/**/*.js", "src/**/*.ts", "types/**/*.d.ts"] } diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index b633568d..f9ccb68c 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@asgardeo/nextjs", - "version": "0.0.0", + "version": "0.0.1", "description": "Next.js implementation of Asgardeo JavaScript SDK.", "keywords": [ "asgardeo", @@ -8,9 +8,9 @@ "react", "ssr" ], - "homepage": "https://github.com/asgardeo/javascript/tree/main/packages/next#readme", + "homepage": "https://github.com/asgardeo/web-ui-sdks/tree/main/packages/next#readme", "bugs": { - "url": "https://github.com/asgardeo/javascript/issues" + "url": "https://github.com/asgardeo/web-ui-sdks/issues" }, "author": "WSO2", "license": "Apache-2.0", @@ -30,7 +30,7 @@ "types": "dist/index.d.ts", "repository": { "type": "git", - "url": "https://github.com/asgardeo/javascript", + "url": "https://github.com/asgardeo/web-ui-sdks", "directory": "packages/next" }, "scripts": { @@ -67,4 +67,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/packages/nextjs/src/AsgardeoNextClient.ts b/packages/nextjs/src/AsgardeoNextClient.ts index 491268a2..77479de3 100644 --- a/packages/nextjs/src/AsgardeoNextClient.ts +++ b/packages/nextjs/src/AsgardeoNextClient.ts @@ -22,12 +22,13 @@ import { SignInOptions, SignOutOptions, User, - // removeTrailingSlash, + UserProfile, } from '@asgardeo/node'; import {NextRequest, NextResponse} from 'next/server'; import {AsgardeoNextConfig} from './models/config'; import deleteSessionId from './server/actions/deleteSessionId'; import getSessionId from './server/actions/getSessionId'; +import getIsSignedIn from './server/actions/isSignedIn'; import setSessionId from './server/actions/setSessionId'; import decorateConfigWithNextEnv from './utils/decorateConfigWithNextEnv'; import InternalAuthAPIRoutesConfig from './configs/InternalAuthAPIRoutesConfig'; @@ -49,22 +50,24 @@ class AsgardeoNextClient exte } override initialize(config: T): Promise { - const {baseUrl, clientId, clientSecret, afterSignInUrl} = decorateConfigWithNextEnv({ - afterSignInUrl: config.afterSignInUrl, - baseUrl: config.baseUrl, - clientId: config.clientId, - clientSecret: config.clientSecret, - }); + const {baseUrl, clientId, clientSecret, afterSignInUrl, ...rest} = decorateConfigWithNextEnv(config); return this.asgardeo.initialize({ baseUrl, - clientID: clientId, + clientId: clientId, clientSecret, - signInRedirectURL: afterSignInUrl, + afterSignInUrl: afterSignInUrl, + ...rest, } as any); } - override getUser(): Promise { + override async getUser(userId?: string): Promise { + let resolvedSessionId: string = userId || ((await getSessionId()) as string); + + return this.asgardeo.getUser(resolvedSessionId); + } + + override getUserProfile(): Promise { throw new Error('Method not implemented.'); } @@ -73,7 +76,7 @@ class AsgardeoNextClient exte } override isSignedIn(sessionId?: string): Promise { - return this.asgardeo.isAuthenticated(sessionId as string); + return this.asgardeo.isSignedIn(sessionId as string); } override async signIn( @@ -127,11 +130,11 @@ class AsgardeoNextClient exte {}, undefined, (redirectUrl: string) => { - return response = NextResponse.redirect(redirectUrl, 302); + return (response = NextResponse.redirect(redirectUrl, 302)); }, searchParams.get('code') as string, - searchParams.get('session_state')as string, - searchParams.get('state')as string, + searchParams.get('session_state') as string, + searchParams.get('state') as string, ); // If we already redirected via the callback, return that @@ -153,14 +156,27 @@ class AsgardeoNextClient exte if (method === 'GET' && sanitizedPathname === InternalAuthAPIRoutesConfig.session) { try { - const isAuthenticated: boolean = await this.isSignedIn(); + const isSignedIn: boolean = await getIsSignedIn(); - return NextResponse.json({isSignedIn: isAuthenticated}); + return NextResponse.json({isSignedIn: isSignedIn}); } catch (error) { return NextResponse.json({error: 'Failed to check session'}, {status: 500}); } } + if (method === 'GET' && sanitizedPathname === InternalAuthAPIRoutesConfig.user) { + try { + const user: User = await this.getUser(); + + console.log('[AsgardeoNextClient] User fetched successfully:', user); + + return NextResponse.json({user}); + } catch (error) { + console.error('[AsgardeoNextClient] Failed to get user:', error); + return NextResponse.json({error: 'Failed to get user'}, {status: 500}); + } + } + if (method === 'GET' && sanitizedPathname === InternalAuthAPIRoutesConfig.signOut) { try { const afterSignOutUrl: string = await this.signOut(); diff --git a/packages/nextjs/src/client/components/actions/SignInButton.tsx b/packages/nextjs/src/client/components/actions/SignInButton.tsx deleted file mode 100644 index 8dbec934..00000000 --- a/packages/nextjs/src/client/components/actions/SignInButton.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -'use client'; - -import {FC, forwardRef, HTMLAttributes, PropsWithChildren, ReactElement, Ref} from 'react'; -import InternalAuthAPIRoutesConfig from '../../../configs/InternalAuthAPIRoutesConfig'; -import {BaseSignInButton} from '@asgardeo/react'; - -/** - * Props interface of {@link SignInButton} - */ -export type SignInButtonProps = HTMLAttributes; - -/** - * SignInButton component. This button initiates the sign-in process when clicked. - * - * @example - * ```tsx - * import { SignInButton } from '@asgardeo/auth-react'; - * - * const App = () => { - * const buttonRef = useRef(null); - * return ( - * - * Sign In - * - * ); - * } - * ``` - */ -const SignInButton: FC> = forwardRef< - HTMLButtonElement, - PropsWithChildren ->( - ( - {children = 'Sign In', className, style, ...rest}: PropsWithChildren, - ref: Ref, - ): ReactElement => ( -
- - {children} - -
- ), -); - -export default SignInButton; diff --git a/packages/nextjs/src/client/components/actions/SignInButton/SignInButton.tsx b/packages/nextjs/src/client/components/actions/SignInButton/SignInButton.tsx new file mode 100644 index 00000000..b2732d72 --- /dev/null +++ b/packages/nextjs/src/client/components/actions/SignInButton/SignInButton.tsx @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +'use client'; + +import {forwardRef, ForwardRefExoticComponent, ReactElement, Ref, RefAttributes} from 'react'; +import InternalAuthAPIRoutesConfig from '../../../../configs/InternalAuthAPIRoutesConfig'; +import {BaseSignInButton, BaseSignInButtonProps} from '@asgardeo/react'; + +/** + * Props interface of {@link SignInButton} + */ +export type SignInButtonProps = BaseSignInButtonProps; + +/** + * SignInButton component that supports both render props and traditional props patterns for Next.js. + * + * @example Using render props + * ```tsx + * + * {({isLoading}) => ( + * + * )} + * + * ``` + * + * @example Using traditional props + * ```tsx + * Sign In + * ``` + * + * @remarks + * In Next.js with server actions, the sign-in is handled via form submission. + * When using render props, the custom button should use `type="submit"` instead of `onClick={signIn}`. + * The `signIn` function in render props is provided for API consistency but should not be used directly. + */ +const SignInButton = forwardRef( + ({className, style, ...rest}: SignInButtonProps, ref: Ref): ReactElement => { + return ( +
+ + + ); + }, +); + +SignInButton.displayName = 'SignInButton'; + +export default SignInButton; diff --git a/packages/nextjs/src/client/components/actions/SignOutButton.tsx b/packages/nextjs/src/client/components/actions/SignOutButton/SignOutButton.tsx similarity index 73% rename from packages/nextjs/src/client/components/actions/SignOutButton.tsx rename to packages/nextjs/src/client/components/actions/SignOutButton/SignOutButton.tsx index e234971b..5954c0f2 100644 --- a/packages/nextjs/src/client/components/actions/SignOutButton.tsx +++ b/packages/nextjs/src/client/components/actions/SignOutButton/SignOutButton.tsx @@ -18,14 +18,14 @@ 'use client'; -import {FC, forwardRef, HTMLAttributes, PropsWithChildren, ReactElement, Ref} from 'react'; -import InternalAuthAPIRoutesConfig from '../../../configs/InternalAuthAPIRoutesConfig'; -import {BaseSignOutButton} from '@asgardeo/react'; +import {FC, forwardRef, PropsWithChildren, ReactElement, Ref} from 'react'; +import InternalAuthAPIRoutesConfig from '../../../../configs/InternalAuthAPIRoutesConfig'; +import {BaseSignOutButton, BaseSignOutButtonProps} from '@asgardeo/react'; /** * Interface for SignInButton component props. */ -export type SignOutButtonProps = HTMLAttributes; +export type SignOutButtonProps = BaseSignOutButtonProps; /** * SignInButton component. This button initiates the sign-in process when clicked. @@ -48,14 +48,9 @@ const SignOutButton: FC> = forwardRef< HTMLButtonElement, PropsWithChildren >( - ( - {children = 'Sign Out', className, style, ...rest}: PropsWithChildren, - ref: Ref, - ): ReactElement => ( + ({className, style, ...rest}: PropsWithChildren, ref: Ref): ReactElement => (
- - {children} - + ), ); diff --git a/packages/nextjs/src/client/components/actions/SignUpButton.tsx b/packages/nextjs/src/client/components/actions/SignUpButton/SignUpButton.tsx similarity index 73% rename from packages/nextjs/src/client/components/actions/SignUpButton.tsx rename to packages/nextjs/src/client/components/actions/SignUpButton/SignUpButton.tsx index d14d26a9..cfd0d65b 100644 --- a/packages/nextjs/src/client/components/actions/SignUpButton.tsx +++ b/packages/nextjs/src/client/components/actions/SignUpButton/SignUpButton.tsx @@ -18,14 +18,14 @@ 'use client'; -import {FC, forwardRef, HTMLAttributes, PropsWithChildren, ReactElement, Ref} from 'react'; -import InternalAuthAPIRoutesConfig from '../../../configs/InternalAuthAPIRoutesConfig'; -import {BaseSignUpButton} from '@asgardeo/react'; +import {FC, forwardRef, PropsWithChildren, ReactElement, Ref} from 'react'; +import InternalAuthAPIRoutesConfig from '../../../../configs/InternalAuthAPIRoutesConfig'; +import {BaseSignUpButton, BaseSignUpButtonProps} from '@asgardeo/react'; /** * Interface for SignInButton component props. */ -export type SignUpButtonProps = HTMLAttributes; +export type SignUpButtonProps = BaseSignUpButtonProps; /** * SignInButton component. This button initiates the sign-in process when clicked. @@ -48,14 +48,9 @@ const SignUpButton: FC> = forwardRef< HTMLButtonElement, PropsWithChildren >( - ( - {children = 'Sign Up', className, style, ...rest}: PropsWithChildren, - ref: Ref, - ): ReactElement => ( + ({className, style, ...rest}: PropsWithChildren, ref: Ref): ReactElement => (
- - {children} - + ), ); diff --git a/packages/nextjs/src/client/components/control/SignedIn.tsx b/packages/nextjs/src/client/components/control/SignedIn/SignedIn.tsx similarity index 96% rename from packages/nextjs/src/client/components/control/SignedIn.tsx rename to packages/nextjs/src/client/components/control/SignedIn/SignedIn.tsx index 1c3db787..e1bd3f4d 100644 --- a/packages/nextjs/src/client/components/control/SignedIn.tsx +++ b/packages/nextjs/src/client/components/control/SignedIn/SignedIn.tsx @@ -19,7 +19,7 @@ 'use client'; import {FC, PropsWithChildren, ReactNode, useEffect, useState} from 'react'; -import isSignedIn from '../../../server/actions/isSignedIn'; +import isSignedIn from '../../../../server/actions/isSignedIn'; /** * Props interface of {@link SignedIn} diff --git a/packages/nextjs/src/client/components/control/SignedOut.tsx b/packages/nextjs/src/client/components/control/SignedOut/SignedOut.tsx similarity index 96% rename from packages/nextjs/src/client/components/control/SignedOut.tsx rename to packages/nextjs/src/client/components/control/SignedOut/SignedOut.tsx index b83b7592..2c8ac726 100644 --- a/packages/nextjs/src/client/components/control/SignedOut.tsx +++ b/packages/nextjs/src/client/components/control/SignedOut/SignedOut.tsx @@ -19,7 +19,7 @@ 'use client'; import {FC, PropsWithChildren, ReactNode, useEffect, useState} from 'react'; -import isSignedIn from '../../../server/actions/isSignedIn'; +import isSignedIn from '../../../../server/actions/isSignedIn'; /** * Props interface of {@link SignedOut} diff --git a/packages/react/src/components/presentation/User.tsx b/packages/nextjs/src/client/components/presentation/User/User.tsx similarity index 72% rename from packages/react/src/components/presentation/User.tsx rename to packages/nextjs/src/client/components/presentation/User/User.tsx index f04dbcad..6cf743df 100644 --- a/packages/react/src/components/presentation/User.tsx +++ b/packages/nextjs/src/client/components/presentation/User/User.tsx @@ -16,20 +16,23 @@ * under the License. */ -import {User as AsgardeoUser} from '@asgardeo/browser'; +'use client'; + import {FC, ReactElement, ReactNode} from 'react'; -import useAsgardeo from '../../hooks/useAsgardeo'; +import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; +import {BaseUser, BaseUserProps} from '@asgardeo/react'; /** * Props for the User component. + * Extends BaseUserProps but makes the user prop optional since it will be obtained from useAsgardeo */ -export interface UserProps { +export interface UserProps extends Omit { /** * Render prop that takes the user object and returns a ReactNode. * @param user - The authenticated user object from Asgardeo. * @returns A ReactNode to render. */ - children: (user: AsgardeoUser | null) => ReactNode; + children: (user: any | null) => ReactNode; /** * Optional element to render when no user is signed in. @@ -39,6 +42,9 @@ export interface UserProps { /** * A component that uses render props to expose the authenticated user object. + * This component automatically retrieves the user from Asgardeo context. + * + * @remarks This component is only supported in browser based React applications (CSR). * * @example * ```tsx @@ -61,11 +67,11 @@ export interface UserProps { const User: FC = ({children, fallback = null}): ReactElement => { const {user} = useAsgardeo(); - if (!user) { - return <>{fallback}; - } - - return <>{children(user)}; + return ( + + {children} + + ); }; User.displayName = 'User'; diff --git a/packages/nextjs/src/client/contexts/AsgardeoContext.ts b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoContext.ts similarity index 87% rename from packages/nextjs/src/client/contexts/AsgardeoContext.ts rename to packages/nextjs/src/client/contexts/Asgardeo/AsgardeoContext.ts index d4c27b6c..139e8439 100644 --- a/packages/nextjs/src/client/contexts/AsgardeoContext.ts +++ b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoContext.ts @@ -19,12 +19,19 @@ 'use client'; import {AsgardeoContextProps as AsgardeoReactContextProps} from '@asgardeo/react'; +import {User} from '@asgardeo/node'; import {Context, createContext} from 'react'; /** * Props interface of {@link AsgardeoContext} */ -export type AsgardeoContextProps = Partial; +export type AsgardeoContextProps = Partial & { + user?: User | null; + isSignedIn?: boolean; + isLoading?: boolean; + signIn?: () => void; + signOut?: () => void; +}; /** * Context object for managing the Authentication flow builder core context. diff --git a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx new file mode 100644 index 00000000..0d4c7976 --- /dev/null +++ b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +'use client'; + +import {FC, PropsWithChildren, useEffect, useMemo, useState} from 'react'; +import {I18nProvider, FlowProvider, UserProvider, ThemeProvider} from '@asgardeo/react'; +import {User} from '@asgardeo/node'; +import AsgardeoContext from './AsgardeoContext'; +import InternalAuthAPIRoutesConfig from '../../../configs/InternalAuthAPIRoutesConfig'; + +/** + * Props interface of {@link AsgardeoClientProvider} + */ +export type AsgardeoClientProviderProps = { + /** + * Preferences for theming, i18n, and other UI customizations. + */ + preferences?: any; +}; + +const AsgardeoClientProvider: FC> = ({ + children, + preferences, +}: PropsWithChildren) => { + const [isDarkMode, setIsDarkMode] = useState(false); + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isSignedIn, setIsSignedIn] = useState(false); + + useEffect(() => { + if (!preferences?.theme?.mode || preferences.theme.mode === 'system') { + setIsDarkMode(window.matchMedia('(prefers-color-scheme: dark)').matches); + } else { + setIsDarkMode(preferences.theme.mode === 'dark'); + } + }, [preferences?.theme?.mode]); + + useEffect(() => { + const fetchUserData = async () => { + try { + setIsLoading(true); + + const sessionResponse = await fetch(InternalAuthAPIRoutesConfig.session); + const sessionData = await sessionResponse.json(); + setIsSignedIn(sessionData.isSignedIn); + + if (sessionData.isSignedIn) { + const userResponse = await fetch(InternalAuthAPIRoutesConfig.user); + + if (userResponse.ok) { + const userData = await userResponse.json(); + setUser(userData); + } + } else { + setUser(null); + } + } catch (error) { + setUser(null); + setIsSignedIn(false); + } finally { + setIsLoading(false); + } + }; + + fetchUserData(); + }, []); + + const contextValue = useMemo( + () => ({ + user, + isSignedIn, + isLoading, + signIn: () => (window.location.href = InternalAuthAPIRoutesConfig.signIn), + signOut: () => (window.location.href = InternalAuthAPIRoutesConfig.signOut), + }), + [user, isSignedIn, isLoading], + ); + + return ( + + + + + + {children} + + + + + + ); +}; + +export default AsgardeoClientProvider; diff --git a/packages/nextjs/src/client/hooks/useAsgardeo.ts b/packages/nextjs/src/client/contexts/Asgardeo/useAsgardeo.ts similarity index 92% rename from packages/nextjs/src/client/hooks/useAsgardeo.ts rename to packages/nextjs/src/client/contexts/Asgardeo/useAsgardeo.ts index b21b1e6a..4a4937ec 100644 --- a/packages/nextjs/src/client/hooks/useAsgardeo.ts +++ b/packages/nextjs/src/client/contexts/Asgardeo/useAsgardeo.ts @@ -19,7 +19,7 @@ 'use client'; import {useContext} from 'react'; -import AsgardeoContext, {AsgardeoContextProps} from '../contexts/AsgardeoContext'; +import AsgardeoContext, {AsgardeoContextProps} from './AsgardeoContext'; const useAsgardeo = (): AsgardeoContextProps => { const context: AsgardeoContextProps | null = useContext(AsgardeoContext); diff --git a/packages/nextjs/src/configs/InternalAuthAPIRoutesConfig.ts b/packages/nextjs/src/configs/InternalAuthAPIRoutesConfig.ts index e3651467..a0dd32cb 100644 --- a/packages/nextjs/src/configs/InternalAuthAPIRoutesConfig.ts +++ b/packages/nextjs/src/configs/InternalAuthAPIRoutesConfig.ts @@ -19,6 +19,7 @@ import {InternalAuthAPIRoutes} from '../models/api'; const InternalAuthAPIRoutesConfig: InternalAuthAPIRoutes = { + user: '/api/auth/user', session: '/api/auth/session', signIn: '/api/auth/signin', signOut: '/api/auth/signout', diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts index a3190649..2ba4b280 100644 --- a/packages/nextjs/src/index.ts +++ b/packages/nextjs/src/index.ts @@ -21,19 +21,19 @@ export * from './server/AsgardeoProvider'; export {default as isSignedIn} from './server/actions/isSignedIn'; -export {default as SignedIn} from './client/components/control/SignedIn'; -export * from './client/components/control/SignedIn'; +export {default as SignedIn} from './client/components/control/SignedIn/SignedIn'; +export {SignedInProps} from './client/components/control/SignedIn/SignedIn'; -export {default as SignedOut} from './client/components/control/SignedOut'; -export * from './client/components/control/SignedOut'; +export {default as SignedOut} from './client/components/control/SignedOut/SignedOut'; +export {SignedOutProps} from './client/components/control/SignedOut/SignedOut'; -export {default as SignInButton} from './client/components/actions/SignInButton'; -export type {SignInButtonProps} from './client/components/actions/SignInButton'; +export {default as SignInButton} from './client/components/actions/SignInButton/SignInButton'; +export type {SignInButtonProps} from './client/components/actions/SignInButton/SignInButton'; -export {default as SignOutButton} from './client/components/actions/SignOutButton'; -export type {SignOutButtonProps} from './client/components/actions/SignOutButton'; +export {default as SignOutButton} from './client/components/actions/SignOutButton/SignOutButton'; +export type {SignOutButtonProps} from './client/components/actions/SignOutButton/SignOutButton'; -export {default as AsgardeoContext} from './client/contexts/AsgardeoContext'; -export type {AsgardeoContextProps} from './client/contexts/AsgardeoContext'; +export {default as User} from './client/components/presentation/User/User'; +export type {UserProps} from './client/components/presentation/User/User'; export {default as AsgardeoNext} from './AsgardeoNextClient'; diff --git a/packages/nextjs/src/models/api.ts b/packages/nextjs/src/models/api.ts index 7242edb3..c9d5ae7e 100644 --- a/packages/nextjs/src/models/api.ts +++ b/packages/nextjs/src/models/api.ts @@ -21,6 +21,11 @@ * These routes are used internally by the Asgardeo Next.js SDK for handling authentication flows. */ export interface InternalAuthAPIRoutes { + /** + * Route for handling user information retrieval. + * This route should return the current user's information, such as username, email, etc. + */ + user: string; /** * Route for handling session management. * This route should return the current signed-in status. diff --git a/packages/nextjs/src/server/AsgardeoProvider.tsx b/packages/nextjs/src/server/AsgardeoProvider.tsx index 8ffc1507..358d2b98 100644 --- a/packages/nextjs/src/server/AsgardeoProvider.tsx +++ b/packages/nextjs/src/server/AsgardeoProvider.tsx @@ -17,7 +17,7 @@ */ import {FC, PropsWithChildren, ReactElement} from 'react'; -import AsgardeoClientProvider, {AsgardeoClientProviderProps} from '../client/providers/AsgardeoProvider'; +import AsgardeoClientProvider, {AsgardeoClientProviderProps} from '../client/contexts/Asgardeo/AsgardeoProvider'; /** * Props interface of {@link AsgardeoServerProvider} diff --git a/packages/node/README.md b/packages/node/README.md index 500d505e..9e1dc1c1 100644 --- a/packages/node/README.md +++ b/packages/node/README.md @@ -28,7 +28,7 @@ import { AsgardeoNodeClient } from "@asgardeo/node"; // Initialize the client const authClient = new AsgardeoNodeClient({ - clientID: "", + clientId: "", clientSecret: "", baseUrl: "https://api.asgardeo.io/t/", callbackURL: "http://localhost:3000/callback" @@ -40,7 +40,7 @@ const app = express(); // Login endpoint app.get("/login", (req, res) => { - const authUrl = authClient.getAuthorizationURL(); + const authUrl = authClient.getSignInUrl(); res.redirect(authUrl); }); diff --git a/packages/node/package.json b/packages/node/package.json index 6cda86ed..8a0b4d8a 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,6 +1,6 @@ { "name": "@asgardeo/node", - "version": "0.0.0", + "version": "0.0.1", "description": "Node.js runtime specific implementation of Asgardeo JavaScript SDK.", "keywords": [ "asgardeo", @@ -8,9 +8,9 @@ "node", "server" ], - "homepage": "https://github.com/asgardeo/javascript/tree/main/packages/node#readme", + "homepage": "https://github.com/asgardeo/web-ui-sdks/tree/main/packages/node#readme", "bugs": { - "url": "https://github.com/asgardeo/javascript/issues" + "url": "https://github.com/asgardeo/web-ui-sdks/issues" }, "author": "WSO2", "license": "Apache-2.0", @@ -29,7 +29,7 @@ "types": "dist/index.d.ts", "repository": { "type": "git", - "url": "https://github.com/asgardeo/javascript", + "url": "https://github.com/asgardeo/web-ui-sdks", "directory": "packages/node" }, "scripts": { @@ -65,4 +65,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/packages/node/src/__legacy__/client.ts b/packages/node/src/__legacy__/client.ts index 11733cc7..00406486 100644 --- a/packages/node/src/__legacy__/client.ts +++ b/packages/node/src/__legacy__/client.ts @@ -18,14 +18,13 @@ import { AuthClientConfig, - BasicUserInfo, - CustomGrantConfig, - DataLayer, + TokenExchangeRequestConfig, + StorageManager, IdTokenPayload, - FetchResponse, OIDCEndpoints, - Store, + Storage, TokenResponse, + User, } from '@asgardeo/javascript'; import {AsgardeoNodeCore} from './core'; import {AuthURLCallback} from './models'; @@ -43,15 +42,15 @@ export class AsgardeoNodeClient { * This is the constructor method that returns an instance of the `AsgardeoNodeClient` class. * * @param {AuthClientConfig} config - The configuration object. - * @param {Store} store - The store object. + * @param {Storage} store - The store object. * * @example * ``` - * const _store: Store = new DataStore(); + * const _store: Storage = new DataStore(); * const _config = { - signInRedirectURL: "http://localhost:3000/sign-in", - signOutRedirectURL: "http://localhost:3000/dashboard", - clientID: "client ID", + afterSignInUrl: "http://localhost:3000/sign-in", + afterSignOutUrl: "http://localhost:3000/dashboard", + clientId: "client ID", serverOrigin: "https://api.asgardeo.io/t/" }; * const auth = new AsgardeoNodeClient(_config,_store); @@ -62,7 +61,7 @@ export class AsgardeoNodeClient { */ constructor() {} - public async initialize(config: AuthClientConfig, store?: Store): Promise { + public async initialize(config: AuthClientConfig, store?: Storage): Promise { this._authCore = new AsgardeoNodeCore(config, store); return Promise.resolve(true); @@ -73,7 +72,7 @@ export class AsgardeoNodeClient { * authorization URL to authorize the user. * @param {string} authorizationCode - The authorization code obtained from Asgardeo after a user signs in. * @param {String} sessionState - The session state obtained from Asgardeo after a user signs in. - * @param {string} userID - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user + * @param {string} userId - (Optional) A unique ID of the user to be authenticated. This is useful in multi-user * scenarios where each user should be uniquely identified. * @param {string} state - The state parameter in the redirect URL. * @@ -141,16 +140,16 @@ export class AsgardeoNodeClient { * * @example * ``` - * const isAuth = await authClient.isAuthenticated("a2a2972c-51cd-5e9d-a9ae-058fae9f7927"); + * const isAuth = await authClient.isSignedIn("a2a2972c-51cd-5e9d-a9ae-058fae9f7927"); * ``` * - * @link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#isAuthenticated + * @link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#isSignedIn * * @memberof AsgardeoNodeClient * */ - public async isAuthenticated(userId: string): Promise { - return this._authCore.isAuthenticated(userId); + public async isSignedIn(userId: string): Promise { + return this._authCore.isSignedIn(userId); } /** @@ -162,16 +161,16 @@ export class AsgardeoNodeClient { * * @example * ``` - * const isAuth = await authClient.getIDToken("a2a2972c-51cd-5e9d-a9ae-058fae9f7927"); + * const isAuth = await authClient.getIdToken("a2a2972c-51cd-5e9d-a9ae-058fae9f7927"); * ``` * - * @link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getIDToken + * @link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getIdToken * * @memberof AsgardeoNodeClient * */ - public async getIDToken(userId: string): Promise { - return this._authCore.getIDToken(userId); + public async getIdToken(userId: string): Promise { + return this._authCore.getIdToken(userId); } /** @@ -184,16 +183,16 @@ export class AsgardeoNodeClient { * * @example * ``` - * const basicInfo = await authClient.getBasicUserInfo("a2a2972c-51cd-5e9d-a9ae-058fae9f7927"); + * const basicInfo = await authClient.getUser("a2a2972c-51cd-5e9d-a9ae-058fae9f7927"); * ``` * - * @link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getBasicUserInfo + * @link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getUser * * @memberof AsgardeoNodeClient * */ - public async getBasicUserInfo(userId: string): Promise { - return this._authCore.getBasicUserInfo(userId); + public async getUser(userId: string): Promise { + return this._authCore.getUser(userId); } /** @@ -203,16 +202,16 @@ export class AsgardeoNodeClient { * * @example * ``` - * const oidcEndpoints = await auth.getOIDCServiceEndpoints(); + * const oidcEndpoints = await auth.getOpenIDProviderEndpoints(); * ``` * - * @link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getOIDCServiceEndpoints + * @link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getOpenIDProviderEndpoints * * @memberof AsgardeoNodeClient * */ - public async getOIDCServiceEndpoints(): Promise { - return this._authCore.getOIDCServiceEndpoints(); + public async getOpenIDProviderEndpoints(): Promise { + return this._authCore.getOpenIDProviderEndpoints(); } /** @@ -225,16 +224,16 @@ export class AsgardeoNodeClient { * * @example * ``` - * const decodedIDTokenPayload = await auth.getDecodedIDToken("a2a2972c-51cd-5e9d-a9ae-058fae9f7927"); + * const decodedIDTokenPayload = await auth.getDecodedIdToken("a2a2972c-51cd-5e9d-a9ae-058fae9f7927"); * ``` * - * @link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getDecodedIDToken + * @link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#getDecodedIdToken * * @memberof AsgardeoNodeClient * */ - public async getDecodedIDToken(userId?: string): Promise { - return this._authCore.getDecodedIDToken(userId); + public async getDecodedIdToken(userId?: string): Promise { + return this._authCore.getDecodedIdToken(userId); } /** @@ -260,15 +259,15 @@ export class AsgardeoNodeClient { } /** - * This method returns Promise that resolves with the token information + * This method returns Promise that resolves with the token information * or the response returned by the server depending on the configuration passed. - * @param {CustomGrantConfig} config - The config object contains attributes that would be used + * @param {TokenExchangeRequestConfig} config - The config object contains attributes that would be used * to configure the custom grant request. * * @param {string} userId - The userId of the user. * (If you are using ExpressJS, you may get this from the request cookies) - * - * @return {Promise} -A Promise that resolves with the token information + * + * @return {Promise} -A Promise that resolves with the token information * or the response returned by the server depending on the configuration passed. * * @example @@ -276,7 +275,7 @@ export class AsgardeoNodeClient { * const config = { * attachToken: false, * data: { - * client_id: "{{clientID}}", + * client_id: "{{clientId}}", * grant_type: "account_switch", * scope: "{{scope}}", * token: "{{token}}", @@ -287,20 +286,23 @@ export class AsgardeoNodeClient { * signInRequired: true * } - * auth.requestCustomGrant(config).then((response)=>{ + * auth.exchangeToken(config).then((response)=>{ * console.log(response); * }).catch((error)=>{ * console.error(error); * }); * ``` * - * @link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#requestCustomGrant + * @link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#exchangeToken * * @memberof AsgardeoNodeClient * */ - public async requestCustomGrant(config: CustomGrantConfig, userId?: string): Promise { - return this._authCore.requestCustomGrant(config, userId); + public async exchangeToken( + config: TokenExchangeRequestConfig, + userId?: string, + ): Promise { + return this._authCore.exchangeToken(config, userId); } /** @@ -312,18 +314,18 @@ export class AsgardeoNodeClient { * * @example * ``` - * const updateConfig = await auth.updateConfig({ - * signOutRedirectURL: "http://localhost:3000/sign-out" + * const reInitialize = await auth.reInitialize({ + * afterSignOutUrl: "http://localhost:3000/sign-out" * }); * ``` * - * @link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#updateConfig + * @link https://github.com/asgardeo/asgardeo-auth-js-sdk/tree/master#reInitialize * * @memberof AsgardeoNodeClient * */ - public async updateConfig(config: Partial>): Promise { - return this._authCore.updateConfig(config); + public async reInitialize(config: Partial>): Promise { + return this._authCore.reInitialize(config); } /** @@ -331,7 +333,7 @@ export class AsgardeoNodeClient { * @param {string} userId - The userId of the user. * (If you are using ExpressJS, you may get this from the request cookies) * - * @return {Promise} -A Promise that resolves with the response returned by the server. + * @return {Promise} -A Promise that resolves with the response returned by the server. * * @example * ``` @@ -343,7 +345,7 @@ export class AsgardeoNodeClient { * @memberof AsgardeoNodeClient * */ - public async revokeAccessToken(userId?: string): Promise { + public async revokeAccessToken(userId?: string): Promise { return this._authCore.revokeAccessToken(userId); } @@ -371,7 +373,7 @@ export class AsgardeoNodeClient { /** * This method returns if the user has been successfully signed out or not. - * @param {string} signOutRedirectURL - The URL to which the user is redirected to + * @param {string} afterSignOutUrl - The URL to which the user is redirected to * after signing out from the server. * * @return {boolean} - A boolean value indicating if the user has been signed out or not. @@ -386,13 +388,13 @@ export class AsgardeoNodeClient { * @memberof AsgardeoNodeClient * */ - public static isSignOutSuccessful(signOutRedirectURL: string): boolean { - return AsgardeoNodeClient.isSignOutSuccessful(signOutRedirectURL); + public static isSignOutSuccessful(afterSignOutUrl: string): boolean { + return AsgardeoNodeClient.isSignOutSuccessful(afterSignOutUrl); } /** * This method returns if sign-out failed or not - * @param {string} signOutRedirectURL - The URL to which the user is redirected to + * @param {string} afterSignOutUrl - The URL to which the user is redirected to * after signing out from the server. * * @return {boolean} - A boolean value indicating if sign-out failed or not. @@ -407,11 +409,11 @@ export class AsgardeoNodeClient { * @memberof AsgardeoNodeClient * */ - public static didSignOutFail(signOutRedirectURL: string): boolean { - return AsgardeoNodeClient.didSignOutFail(signOutRedirectURL); + public static didSignOutFail(afterSignOutUrl: string): boolean { + return AsgardeoNodeClient.didSignOutFail(afterSignOutUrl); } - public async getDataLayer(): Promise> { - return this._authCore.getDataLayer(); + public async getStorageManager(): Promise> { + return this._authCore.getStorageManager(); } } diff --git a/packages/node/src/__legacy__/core/authentication.ts b/packages/node/src/__legacy__/core/authentication.ts index fe5cf0e4..f4be26a7 100644 --- a/packages/node/src/__legacy__/core/authentication.ts +++ b/packages/node/src/__legacy__/core/authentication.ts @@ -20,16 +20,15 @@ import { AsgardeoAuthClient, AsgardeoAuthException, AuthClientConfig, - BasicUserInfo, Crypto, - CustomGrantConfig, - DataLayer, + TokenExchangeRequestConfig, + StorageManager, IdTokenPayload, - FetchResponse, OIDCEndpoints, SessionData, - Store, + Storage, TokenResponse, + User, } from '@asgardeo/javascript'; import {AuthURLCallback} from '../models'; import {MemoryCacheStore} from '../stores'; @@ -39,10 +38,10 @@ import {NodeCryptoUtils} from '../utils/crypto-utils'; export class AsgardeoNodeCore { private _auth: AsgardeoAuthClient; private _cryptoUtils: Crypto; - private _store: Store; - private _dataLayer: DataLayer; + private _store: Storage; + private _storageManager: StorageManager; - constructor(config: AuthClientConfig, store?: Store) { + constructor(config: AuthClientConfig, store?: Storage) { //Initialize the default memory cache store if an external store is not passed. if (!store) { this._store = new MemoryCacheStore(); @@ -52,19 +51,19 @@ export class AsgardeoNodeCore { this._cryptoUtils = new NodeCryptoUtils(); this._auth = new AsgardeoAuthClient(); this._auth.initialize(config, this._store, this._cryptoUtils); - this._dataLayer = this._auth.getDataLayer(); + this._storageManager = this._auth.getStorageManager(); Logger.debug('Initialized AsgardeoAuthClient successfully'); } public async signIn( authURLCallback: AuthURLCallback, - userID: string, + userId: string, authorizationCode?: string, sessionState?: string, state?: string, signInConfig?: Record, ): Promise { - if (!userID) { + if (!userId) { return Promise.reject( new AsgardeoAuthException( 'NODE-AUTH_CORE-SI-NF01', @@ -74,8 +73,8 @@ export class AsgardeoNodeCore { ); } - if (await this.isAuthenticated(userID)) { - const sessionData: SessionData = await this._dataLayer.getSessionData(userID); + if (await this.isSignedIn(userId)) { + const sessionData: SessionData = await this._storageManager.getSessionData(userId); return Promise.resolve({ accessToken: sessionData.access_token, @@ -100,7 +99,7 @@ export class AsgardeoNodeCore { ), ); } - const authURL = await this.getAuthURL(userID, signInConfig); + const authURL = await this.getAuthURL(userId, signInConfig); authURLCallback(authURL); return Promise.resolve({ @@ -115,11 +114,11 @@ export class AsgardeoNodeCore { }); } - return this.requestAccessToken(authorizationCode, sessionState ?? '', userID, state); + return this.requestAccessToken(authorizationCode, sessionState ?? '', userId, state); } public async getAuthURL(userId: string, signInConfig?: Record): Promise { - const authURL = await this._auth.getAuthorizationURL(signInConfig, userId); + const authURL = await this._auth.getSignInUrl(signInConfig, userId); if (authURL) { return Promise.resolve(authURL.toString()); @@ -143,8 +142,8 @@ export class AsgardeoNodeCore { return this._auth.requestAccessToken(authorizationCode, sessionState, state, userId); } - public async getIDToken(userId: string): Promise { - const is_logged_in = await this.isAuthenticated(userId); + public async getIdToken(userId: string): Promise { + const is_logged_in = await this.isSignedIn(userId); if (!is_logged_in) { return Promise.reject( new AsgardeoAuthException( @@ -154,7 +153,7 @@ export class AsgardeoNodeCore { ), ); } - const idToken = await this._auth.getIDToken(userId); + const idToken = await this._auth.getIdToken(userId); if (idToken) { return Promise.resolve(idToken); } else { @@ -172,13 +171,13 @@ export class AsgardeoNodeCore { return this._auth.refreshAccessToken(userId); } - public async isAuthenticated(userId: string): Promise { + public async isSignedIn(userId: string): Promise { try { - if (!(await this._auth.isAuthenticated(userId))) { + if (!(await this._auth.isSignedIn(userId))) { return Promise.resolve(false); } - if (await SessionUtils.validateSession(await this._dataLayer.getSessionData(userId))) { + if (await SessionUtils.validateSession(await this._storageManager.getSessionData(userId))) { return Promise.resolve(true); } @@ -188,8 +187,8 @@ export class AsgardeoNodeCore { return Promise.resolve(true); } - this._dataLayer.removeSessionData(userId); - this._dataLayer.getTemporaryData(userId); + this._storageManager.removeSessionData(userId); + this._storageManager.getTemporaryData(userId); return Promise.resolve(false); } catch (error) { return Promise.reject(error); @@ -197,7 +196,7 @@ export class AsgardeoNodeCore { } public async signOut(userId: string): Promise { - const signOutURL = await this._auth.getSignOutURL(userId); + const signOutURL = await this._auth.getSignOutUrl(userId); if (!signOutURL) { return Promise.reject( @@ -212,43 +211,46 @@ export class AsgardeoNodeCore { return Promise.resolve(signOutURL); } - public async getBasicUserInfo(userId: string): Promise { - return this._auth.getBasicUserInfo(userId); + public async getUser(userId: string): Promise { + return this._auth.getUser(userId); } - public async getOIDCServiceEndpoints(): Promise { - return this._auth.getOIDCServiceEndpoints() as Promise; + public async getOpenIDProviderEndpoints(): Promise { + return this._auth.getOpenIDProviderEndpoints() as Promise; } - public async getDecodedIDToken(userId?: string): Promise { - return this._auth.getDecodedIDToken(userId); + public async getDecodedIdToken(userId?: string): Promise { + return this._auth.getDecodedIdToken(userId); } public async getAccessToken(userId?: string): Promise { return this._auth.getAccessToken(userId); } - public async requestCustomGrant(config: CustomGrantConfig, userId?: string): Promise { - return this._auth.requestCustomGrant(config, userId); + public async exchangeToken( + config: TokenExchangeRequestConfig, + userId?: string, + ): Promise { + return this._auth.exchangeToken(config, userId); } - public async updateConfig(config: Partial>): Promise { - return this._auth.updateConfig(config); + public async reInitialize(config: Partial>): Promise { + return this._auth.reInitialize(config); } - public async revokeAccessToken(userId?: string): Promise { + public async revokeAccessToken(userId?: string): Promise { return this._auth.revokeAccessToken(userId); } - public static didSignOutFail(signOutRedirectURL: string): boolean { - return AsgardeoNodeCore.didSignOutFail(signOutRedirectURL); + public static didSignOutFail(afterSignOutUrl: string): boolean { + return AsgardeoNodeCore.didSignOutFail(afterSignOutUrl); } - public static isSignOutSuccessful(signOutRedirectURL: string): boolean { - return AsgardeoNodeCore.isSignOutSuccessful(signOutRedirectURL); + public static isSignOutSuccessful(afterSignOutUrl: string): boolean { + return AsgardeoNodeCore.isSignOutSuccessful(afterSignOutUrl); } - public getDataLayer(): DataLayer { - return this._dataLayer; + public getStorageManager(): StorageManager { + return this._storageManager; } } diff --git a/packages/node/src/__legacy__/stores/memory-cache-store.ts b/packages/node/src/__legacy__/stores/memory-cache-store.ts index eec15371..295653a7 100644 --- a/packages/node/src/__legacy__/stores/memory-cache-store.ts +++ b/packages/node/src/__legacy__/stores/memory-cache-store.ts @@ -16,10 +16,10 @@ * under the License. */ -import {Store} from '@asgardeo/javascript'; +import {Storage} from '@asgardeo/javascript'; import cache from 'memory-cache'; -export class MemoryCacheStore implements Store { +export class MemoryCacheStore implements Storage { public async setData(key: string, value: string): Promise { cache.put(key, value); } diff --git a/packages/node/src/__legacy__/utils/crypto-utils.ts b/packages/node/src/__legacy__/utils/crypto-utils.ts index fef425ad..7a7fccfb 100644 --- a/packages/node/src/__legacy__/utils/crypto-utils.ts +++ b/packages/node/src/__legacy__/utils/crypto-utils.ts @@ -51,7 +51,7 @@ export class NodeCryptoUtils implements Crypto { idToken: string, jwk: Partial, algorithms: string[], - clientID: string, + clientId: string, issuer: string, subject: string, clockTolerance?: number, @@ -60,7 +60,7 @@ export class NodeCryptoUtils implements Crypto { return jose .jwtVerify(idToken, key, { algorithms: algorithms, - audience: clientID, + audience: clientId, clockTolerance: clockTolerance, issuer: issuer, subject: subject, diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index a4bb2756..65d912b6 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -55,7 +55,7 @@ "typecheck": "nuxt prepare && vue-tsc --noEmit" }, "dependencies": { - "@asgardeo/auth-node": "^0.1.3", + "@asgardeo/node": "workspace:^", "@nuxt/kit": "^3.16.2", "defu": "^6.1.4" }, diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index 88185247..0c6b7121 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -29,11 +29,11 @@ export type {BasicUserInfo}; export default defineNuxtModule({ defaults: { baseUrl: process.env.ASGARDEO_BASE_URL || '', - clientID: process.env.ASGARDEO_CLIENT_ID || '', + clientId: process.env.ASGARDEO_CLIENT_ID || '', clientSecret: process.env.ASGARDEO_CLIENT_SECRET || '', scope: ['openid', 'profile'], - signInRedirectURL: process.env.ASGARDEO_SIGN_IN_REDIRECT_URL || '', - signOutRedirectURL: process.env.ASGARDEO_SIGN_OUT_REDIRECT_URL || '', + afterSignInUrl: process.env.ASGARDEO_SIGN_IN_REDIRECT_URL || '', + afterSignOutUrl: process.env.ASGARDEO_SIGN_OUT_REDIRECT_URL || '', }, meta: { configKey: 'asgardeoAuth', @@ -45,29 +45,29 @@ export default defineNuxtModule({ nuxt.options.runtimeConfig.asgardeoAuth, nuxt.options.runtimeConfig.public.asgardeoAuth, { - clientID: process.env.ASGARDEO_CLIENT_ID, + clientId: process.env.ASGARDEO_CLIENT_ID, clientSecret: process.env.ASGARDEO_CLIENT_SECRET, enablePKCE: true, scope: ['openid', 'profile'], serverOrigin: process.env.ASGARDEO_BASE_URL, - signInRedirectURL: process.env.ASGARDEO_SIGN_IN_REDIRECT_URL || '', - signOutRedirectURL: process.env.ASGARDEO_SIGN_OUT_REDIRECT_URL || '', + afterSignInUrl: process.env.ASGARDEO_SIGN_IN_REDIRECT_URL || '', + afterSignOutUrl: process.env.ASGARDEO_SIGN_OUT_REDIRECT_URL || '', }, ) as ModuleOptions; // eslint-disable-next-line no-param-reassign nuxt.options.runtimeConfig.public.asgardeoAuth = defu(nuxt.options.runtimeConfig.public.asgardeoAuth, { - clientID: options.clientID, + clientId: options.clientId, }); // eslint-disable-next-line no-param-reassign nuxt.options.runtimeConfig.asgardeoAuth = defu(nuxt.options.runtimeConfig.asgardeoAuth, { - clientID: options.clientID, + clientId: options.clientId, clientSecret: options.clientSecret, scope: options.scope, serverOrigin: options.baseUrl, - signInRedirectURL: options.signInRedirectURL, - signOutRedirectURL: options.signOutRedirectURL, + afterSignInUrl: options.afterSignInUrl, + afterSignOutUrl: options.afterSignOutUrl, }); const {resolve} = createResolver(import.meta.url); @@ -102,7 +102,7 @@ export default defineNuxtModule({ declare module '@nuxt/schema' { interface PublicRuntimeConfig { - asgardeoAuth: Pick; + asgardeoAuth: Pick; } interface RuntimeConfig { diff --git a/packages/nuxt/src/runtime/composables/asgardeo/useAuth.ts b/packages/nuxt/src/runtime/composables/asgardeo/useAuth.ts index c1b8a910..456377eb 100644 --- a/packages/nuxt/src/runtime/composables/asgardeo/useAuth.ts +++ b/packages/nuxt/src/runtime/composables/asgardeo/useAuth.ts @@ -16,7 +16,7 @@ * under the License. */ -import type {BasicUserInfo, DataLayer, IdTokenPayload, OIDCEndpoints} from '@asgardeo/auth-node'; +import type {User, StorageManager, IdTokenPayload, OIDCEndpoints} from '@asgardeo/node'; import type {AuthInterface} from '../../types'; import {navigateTo} from '#imports'; @@ -100,7 +100,7 @@ export const useAuth = (): AuthInterface => { * * @returns {Promise} - A promise that resolves to the decoded ID token payload if available, or null if not. */ - const getDecodedIDToken = async (): Promise => { + const getDecodedIdToken = async (): Promise => { try { const response: Response = await fetch(`/api/auth/get-decoded-id-token`, { credentials: 'include', @@ -139,9 +139,9 @@ export const useAuth = (): AuthInterface => { * * @returns {Promise} - A promise that resolves to `true` if authenticated, otherwise `false`. */ - const isAuthenticated = async (): Promise => { + const isSignedIn = async (): Promise => { try { - const response: Response = await fetch('/api/auth/isAuthenticated', { + const response: Response = await fetch('/api/auth/isSignedIn', { credentials: 'include', method: 'GET', }); @@ -172,11 +172,11 @@ export const useAuth = (): AuthInterface => { * from the server-side '/api/auth/user' endpoint. * Updates the internal state variables with the result. * - * @returns {Promise} A promise resolving to user info or null. + * @returns {Promise} A promise resolving to user info or null. */ - const getBasicUserInfo = async (): Promise => { + const getUser = async (): Promise => { try { - const userInfo: BasicUserInfo = await $fetch('/api/auth/user', { + const userInfo: User = await $fetch('/api/auth/user', { method: 'GET', }); @@ -197,7 +197,7 @@ export const useAuth = (): AuthInterface => { * OIDC endpoints object if available, or null if an error occurs or * the data is not found. */ - const getOIDCServiceEndpoints = async (): Promise => { + const getOpenIDProviderEndpoints = async (): Promise => { try { const response: Response = await fetch('/api/auth/get-oidc-endpoints', { method: 'GET', @@ -214,7 +214,7 @@ export const useAuth = (): AuthInterface => { } }; - const getDataLayer = async (): Promise | null> => { + const getStorageManager = async (): Promise | null> => { try { const response: Response = await fetch('/api/auth/get-data-layer', { method: 'GET', @@ -231,12 +231,12 @@ export const useAuth = (): AuthInterface => { return { getAccessToken, - getBasicUserInfo, - getDataLayer, - getDecodedIDToken, + getUser, + getStorageManager, + getDecodedIdToken, getIdToken, - getOIDCServiceEndpoints, - isAuthenticated, + getOpenIDProviderEndpoints, + isSignedIn, revokeAccessToken, signIn, signOut, diff --git a/packages/nuxt/src/runtime/server/handler.ts b/packages/nuxt/src/runtime/server/handler.ts index 9853fd10..00db0f3d 100644 --- a/packages/nuxt/src/runtime/server/handler.ts +++ b/packages/nuxt/src/runtime/server/handler.ts @@ -17,7 +17,7 @@ */ import {randomUUID} from 'node:crypto'; -import {AsgardeoNodeClient, type AuthClientConfig, type DataLayer, type TokenResponse} from '@asgardeo/auth-node'; +import {AsgardeoNodeClient, type AuthClientConfig, type StorageManager, type TokenResponse} from '@asgardeo/node'; import type {CookieSerializeOptions} from 'cookie-es'; import {defineEventHandler, sendRedirect, setCookie, deleteCookie, getQuery, getCookie, createError, H3Event} from 'h3'; import {getAsgardeoSdkInstance} from './services/asgardeo/index'; @@ -150,19 +150,19 @@ export const AsgardeoAuthHandler = (config: AuthClientConfig, options?: Asgardeo } try { - return await authClient.getBasicUserInfo(sessionId); + return await authClient.getUser(sessionId); } catch { throw createError({ statusCode: 500, statusMessage: 'Failed to retrieve user information.', }); } - } else if (action === 'isAuthenticated' && method === 'GET') { + } else if (action === 'isSignedIn' && method === 'GET') { const sessionId: string | undefined = getCookie(event, sessionIdCookieName); if (!sessionId) return false; try { - return await authClient.isAuthenticated(sessionId); + return await authClient.isSignedIn(sessionId); } catch { return false; } @@ -176,7 +176,7 @@ export const AsgardeoAuthHandler = (config: AuthClientConfig, options?: Asgardeo } try { - const idToken: string = await authClient.getIDToken(sessionId); + const idToken: string = await authClient.getIdToken(sessionId); return {idToken}; } catch { throw createError({ @@ -212,7 +212,7 @@ export const AsgardeoAuthHandler = (config: AuthClientConfig, options?: Asgardeo } try { - return await authClient.getDecodedIDToken(sessionId); + return await authClient.getDecodedIdToken(sessionId); } catch { throw createError({ statusCode: 500, @@ -238,7 +238,7 @@ export const AsgardeoAuthHandler = (config: AuthClientConfig, options?: Asgardeo } } else if (action === 'get-oidc-endpoints' && method === 'GET') { try { - return await authClient.getOIDCServiceEndpoints(); + return await authClient.getOpenIDProviderEndpoints(); } catch { throw createError({ statusCode: 500, @@ -273,7 +273,7 @@ export const AsgardeoAuthHandler = (config: AuthClientConfig, options?: Asgardeo } } else if (action === 'get-data-layer' && method === 'GET') { try { - const dataLayer: DataLayer = await authClient.getDataLayer(); + const dataLayer: StorageManager = await authClient.getStorageManager(); return dataLayer; } catch (error: any) { throw createError({ diff --git a/packages/nuxt/src/runtime/server/services/asgardeo/index.ts b/packages/nuxt/src/runtime/server/services/asgardeo/index.ts index fb8a1796..dc87fd51 100644 --- a/packages/nuxt/src/runtime/server/services/asgardeo/index.ts +++ b/packages/nuxt/src/runtime/server/services/asgardeo/index.ts @@ -16,7 +16,7 @@ * under the License. */ -import {AsgardeoNodeClient, type AuthClientConfig} from '@asgardeo/auth-node'; +import {AsgardeoNodeClient, type AuthClientConfig} from '@asgardeo/node'; let authClientInstance: AsgardeoNodeClient | null = null; diff --git a/packages/nuxt/src/runtime/types.ts b/packages/nuxt/src/runtime/types.ts index e34ac84e..0300cf3d 100644 --- a/packages/nuxt/src/runtime/types.ts +++ b/packages/nuxt/src/runtime/types.ts @@ -16,7 +16,7 @@ * under the License. */ -import type {BasicUserInfo, DataLayer, IdTokenPayload, OIDCEndpoints} from '@asgardeo/auth-node'; +import type {User, StorageManager, IdTokenPayload, OIDCEndpoints} from '@asgardeo/node'; export interface ModuleOptions { /** @@ -30,7 +30,7 @@ export interface ModuleOptions { * Asgardeo Application Client ID. * @default process.env.ASGARDEO_CLIENT_ID */ - clientID: string; + clientId: string; /** * Asgardeo Application Client Secret. (Server-side only) @@ -49,22 +49,22 @@ export interface ModuleOptions { * Must match the URI configured in your Asgardeo app. * @default process.env.ASGARDEO_SIGN_IN_REDIRECT_URL */ - signInRedirectURL: string; + afterSignInUrl: string; /** * The absolute URI to redirect to after sign-out completes. * @default process.env.ASGARDEO_SIGN_OUT_REDIRECT_URL */ - signOutRedirectURL: string; + afterSignOutUrl: string; } export interface AuthInterface { getAccessToken: () => Promise; - getBasicUserInfo: () => Promise; - getDataLayer: () => Promise | null>; - getDecodedIDToken: () => Promise; + getUser: () => Promise; + getStorageManager: () => Promise | null>; + getDecodedIdToken: () => Promise; getIdToken: () => Promise; - getOIDCServiceEndpoints: () => Promise; - isAuthenticated: () => Promise; + getOpenIDProviderEndpoints: () => Promise; + isSignedIn: () => Promise; revokeAccessToken: () => Promise; signIn: (callbackUrl?: string) => Promise; signOut: () => Promise; @@ -74,4 +74,4 @@ export type SessionLastRefreshedAt = Date | undefined; export type SessionStatus = 'authenticated' | 'unauthenticated' | 'loading'; -export type {BasicUserInfo}; +export type {User}; diff --git a/packages/react/API.md b/packages/react/API.md new file mode 100644 index 00000000..b76f0f65 --- /dev/null +++ b/packages/react/API.md @@ -0,0 +1,753 @@ +# `@asgardeo/react` API Documentation + +This document provides complete API documentation for the Asgardeo React SDK, including all components, hooks, and customization options. + +## Table of Contents + +- [Components](#components) + - [AsgardeoProvider](#asgardeyprovider) + - [SignIn](#signin) + - [SignedIn](#signedin) + - [SignedOut](#signedout) + - [SignInButton](#signinbutton) + - [SignOutButton](#signoutbutton) + - [User](#user) + - [UserProfile](#userprofile) +- [Hooks](#hooks) + - [useAsgardeo](#useasgardeo) +- [Customization](#customization) + +## Components + +### AsgardeoProvider + +The root provider component that configures the Asgardeo SDK and provides authentication context to your React application. + +#### Props + +| Prop | Type | Required | Description | +|------|------|----------|-------------| +| `baseUrl` | `string` | Yes | Your Asgardeo organization URL | +| `clientId` | `string` | Yes | Your application's client ID | +| `afterSignInUrl` | `string` | No | URL to redirect after sign in (defaults to current URL) | +| `afterSignOutUrl` | `string` | No | URL to redirect after sign out (defaults to current URL) | +| `scopes` | `string[] | string` | No | OAuth scopes to request (defaults to `['openid', 'profile']`) | +| `storage` | `'localStorage' \| 'sessionStorage'` | No | Storage mechanism for tokens (defaults to `'localStorage'`) | + +#### Example + +```diff +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; ++ import { AsgardeoProvider } from '@asgardeo/react'; +import App from './App'; + +const root = createRoot(document.getElementById('root')); + +root.render( + ++ + ++ + +); +``` + +**Customization:** See [Customization](#customization) section for theming and styling options. The provider doesn't render any visual elements but can be styled through CSS custom properties. + +#### Available CSS Classes +This component doesn't render any HTML elements with CSS classes. Configuration is handled through props and CSS custom properties. + +--- + +### SignIn + +A comprehensive sign-in component that renders a complete sign-in interface with customizable styling and behavior. + +#### Props + +| Prop | Type | Required | Description | +|------|------|----------|-------------| +| `className` | `string` | No | CSS class name for styling the sign-in container | +| `redirectUrl` | `string` | No | URL to redirect after successful sign-in | +| `onSignInStart` | `() => void` | No | Callback fired when sign-in process starts | +| `onSignInSuccess` | `(user: User) => void` | No | Callback fired when sign-in is successful | +| `onSignInError` | `(error: Error) => void` | No | Callback fired when sign-in fails | +| `buttonText` | `string` | No | Custom text for the sign-in button (defaults to "Sign In") | +| `loadingText` | `string` | No | Text to show while signing in (defaults to "Signing in...") | +| `disabled` | `boolean` | No | Whether the sign-in interface is disabled | + +#### Example + +```diff ++ import { SignIn } from '@asgardeo/react'; + +const SignInPage = () => { + const handleSignInSuccess = (user) => { + console.log('User signed in:', user.username); + // Redirect to dashboard or update UI + }; + + const handleSignInError = (error) => { + console.error('Sign-in failed:', error.message); + // Show error message to user + }; + + return ( +
+

Welcome Back

++ +
+ ); +}; + +export default SignInPage; +``` + +**Customization:** See [Customization](#customization) for comprehensive styling and theming options. + +#### Available CSS Classes +- `.asgardeo-signin` - Main sign-in container +- `.asgardeo-signin--small` - Small size variant +- `.asgardeo-signin--large` - Large size variant +- `.asgardeo-signin--outlined` - Outlined variant +- `.asgardeo-signin--filled` - Filled variant +- `.asgardeo-signin__input` - Input field elements +- `.asgardeo-signin__input--small` - Small input variant +- `.asgardeo-signin__input--large` - Large input variant +- `.asgardeo-signin__button` - Sign-in button element +- `.asgardeo-signin__button--small` - Small button variant +- `.asgardeo-signin__button--large` - Large button variant +- `.asgardeo-signin__button--outlined` - Outlined button variant +- `.asgardeo-signin__button--filled` - Filled button variant +- `.asgardeo-signin__error` - Error message container +- `.asgardeo-signin__messages` - Messages container + +--- + +### SignedIn + +A conditional rendering component that only displays its children when the user is authenticated. + +#### Props + +| Prop | Type | Required | Description | +|------|------|----------|-------------| +| `children` | `React.ReactNode` | Yes | Content to render when user is signed in | +| `fallback` | `React.ReactNode` | No | Content to render while loading | + +#### Example + +```diff ++ import { SignedIn } from '@asgardeo/react'; +import Dashboard from './components/Dashboard'; +import LoadingSpinner from './components/LoadingSpinner'; + +const App = () => { + return ( +
++ ++ ++

Checking authentication...

++
++ }> + ++ + + ); +}; + +export default App; +``` + +**Customization:** See [Customization](#customization) for styling the fallback loading state. + +#### Available CSS Classes +This component doesn't render any HTML elements with CSS classes. It conditionally renders children or fallback content. + +--- + +### SignedOut + +A conditional rendering component that only displays its children when the user is not authenticated. + +#### Props + +| Prop | Type | Required | Description | +|------|------|----------|-------------| +| `children` | `React.ReactNode` | Yes | Content to render when user is signed out | + +#### Example + +```diff ++ import { SignedOut } from '@asgardeo/react'; +import LandingPage from './components/LandingPage'; +import Hero from './components/Hero'; + +const App = () => { + return ( +
++ + + ++ +
+ ); +}; + +export default App; +``` + +**Customization:** See [Customization](#customization) for styling options. + +#### Available CSS Classes +This component doesn't render any HTML elements with CSS classes. It conditionally renders children when user is signed out. + +--- + +### SignInButton + +A pre-built button component that triggers the sign-in flow when clicked. + +#### Props + +| Prop | Type | Required | Description | +|------|------|----------|-------------| +| `children` | `React.ReactNode` | No | Custom button content (defaults to "Sign In") | +| `className` | `string` | No | CSS class name for styling | +| `disabled` | `boolean` | No | Whether the button is disabled | +| `onClick` | `() => void` | No | Additional click handler | + +#### Example + +```diff ++ import { SignInButton } from '@asgardeo/react'; + +const LoginPage = () => { + const handleClick = () => { + console.log('Sign-in button clicked'); + }; + + return ( +
+

Welcome to Our App

+

Please sign in to continue

+ ++ ++ 🔐 Log In to Continue ++ +
+ ); +}; + +export default LoginPage; +``` + +**Customization:** See [Customization](#customization) for theming and styling the sign-in button. + +#### Available CSS Classes +- `.asgardeo-sign-in-button` - Main sign-in button element + +--- + +### SignOutButton + +A pre-built button component that triggers the sign-out flow when clicked. + +#### Props + +| Prop | Type | Required | Description | +|------|------|----------|-------------| +| `children` | `React.ReactNode` | No | Custom button content (defaults to "Sign Out") | +| `className` | `string` | No | CSS class name for styling | +| `disabled` | `boolean` | No | Whether the button is disabled | +| `onClick` | `() => void` | No | Additional click handler | + +#### Example + +```diff ++ import { SignOutButton } from '@asgardeo/react'; + +const UserMenu = () => { + const handleSignOut = () => { + console.log('User signed out'); + // Additional cleanup logic + }; + + return ( +
+

Account Settings

+ ++ ++ 🚪 Logout ++ +
+ ); +}; + +export default UserMenu; +``` + +**Customization:** See [Customization](#customization) for theming and styling the sign-out button. + +#### Available CSS Classes +- `.asgardeo-sign-out-button` - Main sign-out button element + +--- + +### User + +A render prop component that provides access to the current user's information. + +#### Props + +| Prop | Type | Required | Description | +|------|------|----------|-------------| +| `children` | `({ user, isLoading, error }) => React.ReactNode` | Yes | Render function that receives user data | + +#### Render Props + +| Prop | Type | Description | +|------|------|-------------| +| `user` | `User \| null` | Current user object | +| `isLoading` | `boolean` | Whether user data is being loaded | +| `error` | `Error \| null` | Any error that occurred while fetching user data | + +#### User Object Properties + +| Property | Type | Description | +|----------|------|-------------| +| `sub` | `string` | User's unique identifier | +| `username` | `string` | Username | +| `email` | `string` | Email address | +| `givenname` | `string` | First name | +| `familyname` | `string` | Last name | +| `photourl` | `string` | Profile picture URL | + +#### Example + +```diff ++ import { User } from '@asgardeo/react'; + +const UserProfile = () => { + return ( +
+

User Profile

+ ++ ++ {({ user, isLoading, error }) => { + if (isLoading) { + return ( +
+
+

Loading user information...

+
+ ); + } + + if (error) { + return ( +
+

Error loading user: {error.message}

+ +
+ ); + } + + if (!user) { + return
No user data available
; + } + + return ( +
+ {user.username} +

{user.givenname} {user.familyname}

+

@{user.username}

+

{user.email}

+
+ ); ++ }} ++ +
+ ); +}; + +export default UserProfile; +``` + +**Customization:** See [Customization](#customization) for styling user information displays. + +#### Available CSS Classes +This component doesn't render any HTML elements with CSS classes. It uses render props to provide user data to children. + +--- + +### UserProfile + +A pre-built component that displays a formatted user profile card. + +#### Props + +| Prop | Type | Required | Description | +|------|------|----------|-------------| +| `className` | `string` | No | CSS class name for styling | +| `showEmail` | `boolean` | No | Whether to display email (defaults to `true`) | +| `showAvatar` | `boolean` | No | Whether to display profile picture (defaults to `true`) | +| `avatarSize` | `'sm' \| 'md' \| 'lg'` | No | Size of the avatar (defaults to `'md'`) | + +#### Example + +```diff ++ import { UserProfile } from '@asgardeo/react'; + +const Header = () => { + return ( +
+
+

My Dashboard

+ +
++ +
+
+
+ ); +}; + +const ProfileCard = () => { + return ( +
+

Profile Information

+ ++ +
+ ); +}; + +export { Header, ProfileCard }; +``` + +**Customization:** See [Customization](#customization) for comprehensive styling options for the user profile component. + +#### Available CSS Classes +- `.asgardeo-user-profile` - Main user profile container element + +--- + +## Hooks + +### useAsgardeo + +The main hook that provides access to all authentication functionality and state. + +#### Returns + +| Property | Type | Description | +|----------|------|-------------| +| `user` | `User \| null` | Current user object | +| `isSignedIn` | `boolean` | Whether user is authenticated | +| `isLoading` | `boolean` | Whether authentication state is being determined | +| `error` | `Error \| null` | Any authentication error | +| `signIn` | `(redirectUrl?: string) => Promise` | Function to initiate sign in | +| `signOut` | `(redirectUrl?: string) => Promise` | Function to sign out | +| `getAccessToken` | `() => Promise` | Get current access token | +| `getIdToken` | `() => Promise` | Get current ID token | +| `refreshTokens` | `() => Promise` | Refresh authentication tokens | + +#### Example + +```diff ++ import { useAsgardeo } from '@asgardeo/react'; +import { useState } from 'react'; + +const AuthenticatedApp = () => { ++ const { ++ user, ++ isSignedIn, ++ isLoading, ++ error, ++ signIn, ++ signOut, ++ getAccessToken ++ } = useAsgardeo(); + + const [apiData, setApiData] = useState(null); + const [apiLoading, setApiLoading] = useState(false); + + const handleApiCall = async () => { + try { + setApiLoading(true); + const token = await getAccessToken(); + + const response = await fetch('/api/protected-data', { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + const data = await response.json(); + setApiData(data); + } catch (err) { + console.error('API call failed:', err); + } finally { + setApiLoading(false); + } + }; + + const handleSignOut = async () => { + await signOut('/goodbye'); + }; + + if (isLoading) { + return ( +
+
+

Initializing authentication...

+
+ ); + } + + if (error) { + return ( +
+

Authentication Error

+

{error.message}

+ +
+ ); + } + + return ( +
+ {isSignedIn ? ( +
+
+

Welcome, {user?.givenname}!

+ +
+ +
+
+ + + {apiData && ( +
+                  {JSON.stringify(apiData, null, 2)}
+                
+ )} +
+
+
+ ) : ( +
+

Welcome to Our App

+

Please sign in to access your dashboard

+ +
+ )} +
+ ); +}; + +export default AuthenticatedApp; +``` + +--- + +## Customization + +The Asgardeo React SDK provides multiple ways to customize the appearance and behavior of components to match your application's design system. + +### CSS Classes and Styling + +All components accept a `className` prop that allows you to apply custom CSS styles: + +```tsx + + Sign In + + + +``` + +### Default CSS Classes + +Components come with default CSS classes that you can target for styling: + +- `.asgardeo-signin-button` - Sign in button +- `.asgardeo-signout-button` - Sign out button +- `.asgardeo-user-profile` - User profile container +- `.asgardeo-user-avatar` - User avatar image +- `.asgardeo-user-info` - User information container + +### CSS Custom Properties (CSS Variables) + +The SDK supports CSS custom properties for consistent theming: + +```css +:root { + --asgardeo-primary-color: #007bff; + --asgardeo-primary-hover: #0056b3; + --asgardeo-border-radius: 8px; + --asgardeo-font-family: 'Inter', sans-serif; + --asgardeo-button-padding: 12px 24px; + --asgardeo-avatar-size-sm: 32px; + --asgardeo-avatar-size-md: 48px; + --asgardeo-avatar-size-lg: 64px; +} +``` + +### Custom Button Content + +Replace default button text with custom content: + +```tsx + + 🔐 + Login with Asgardeo + + + + + Sign Out + +``` + +### Bring your own UI Library + +For applications using popular UI libraries, you can easily integrate Asgardeo components: + +#### Material-UI Integration +```tsx +import { Button } from '@mui/material' +import { useAsgardeo } from '@asgardeo/react' + +function CustomSignInButton() { + const { signIn } = useAsgardeo() + + return ( + + ) +} +``` + +#### Tailwind CSS Integration +```tsx + + Sign In + +``` + +### Custom Loading States + +Customize loading indicators for better user experience: + +```tsx + + + Authenticating... +
+}> + + +``` + +### Advanced Customization with Render Props + +Use the `User` component for complete control over user data presentation: + +```tsx + + {({ user, isLoading }) => ( +
+ {isLoading ? ( + + ) : ( +
+ +
+

{user?.givenname} {user?.familyname}

+ {user?.email} +
+
+ )} +
+ )} +
+``` + +### Configuration-Based Customization + +Customize behavior through the `AsgardeoProvider` configuration: + +```tsx + + + +``` + +This comprehensive customization approach ensures that Asgardeo components can seamlessly integrate with any design system or UI framework while maintaining functionality and accessibility standards. diff --git a/packages/react/README.md b/packages/react/README.md index c224c260..5568f3eb 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -124,6 +124,10 @@ function App() { } ``` +## Documentation + +For complete API documentation including all components, hooks, and customization options, see [API.md](./API.md). + ## License -Apache-2.0 +Licenses this source under the Apache License, Version 2.0 [LICENSE](../../LICENSE), You may not use this file except in compliance with the License. diff --git a/packages/react/package.json b/packages/react/package.json index ef2eb69a..b60eb8e5 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,15 +1,15 @@ { "name": "@asgardeo/react", - "version": "0.2.4", + "version": "0.3.0", "description": "React implementation of Asgardeo JavaScript SDK.", "keywords": [ "asgardeo", "react", "spa" ], - "homepage": "https://github.com/asgardeo/javascript/tree/main/packages/react#readme", + "homepage": "https://github.com/asgardeo/web-ui-sdks/tree/main/packages/react#readme", "bugs": { - "url": "https://github.com/asgardeo/javascript/issues" + "url": "https://github.com/asgardeo/web-ui-sdks/issues" }, "author": "WSO2", "license": "Apache-2.0", @@ -28,7 +28,7 @@ "types": "dist/index.d.ts", "repository": { "type": "git", - "url": "https://github.com/asgardeo/javascript", + "url": "https://github.com/asgardeo/web-ui-sdks", "directory": "packages/react" }, "scripts": { @@ -63,6 +63,7 @@ }, "dependencies": { "@asgardeo/browser": "workspace:^", + "@floating-ui/react": "^0.27.12", "@types/react-dom": "^19.1.5", "clsx": "^2.1.1", "esbuild": "^0.25.4", @@ -72,4 +73,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/packages/react/src/AsgardeoReactClient.ts b/packages/react/src/AsgardeoReactClient.ts index 951271b7..5dae4ecd 100644 --- a/packages/react/src/AsgardeoReactClient.ts +++ b/packages/react/src/AsgardeoReactClient.ts @@ -18,15 +18,18 @@ import { AsgardeoBrowserClient, - extractUserClaimsFromIdToken, - getUserInfo, + flattenUserSchema, + generateFlattenedUserProfile, + UserProfile, SignInOptions, SignOutOptions, User, + generateUserProfile, } from '@asgardeo/browser'; import AuthAPI from './__temp__/api'; import {AsgardeoReactConfig} from './models/config'; -import getUserProfile from './utils/getUserProfile'; +import getMeProfile from './api/scim2/getMeProfile'; +import getSchemas from './api/scim2/getSchemas'; /** * Client for mplementing Asgardeo in React applications. @@ -49,17 +52,37 @@ class AsgardeoReactClient e return this.asgardeo.init({ baseUrl: config.baseUrl, - clientID: config.clientId, - signInRedirectURL: config.afterSignInUrl, - scope: [...scopes, 'internal_login'], + clientId: config.clientId, + afterSignInUrl: config.afterSignInUrl, + scopes: [...scopes, 'internal_login'], }); } - override async getUser(): Promise { + override async getUser(): Promise { const baseUrl = await (await this.asgardeo.getConfigData()).baseUrl; - const profile = await getUserProfile({baseUrl}); + const profile = await getMeProfile({url: `${baseUrl}/scim2/Me`}); + const schemas = await getSchemas({url: `${baseUrl}/scim2/Schemas`}); - return profile; + return generateUserProfile(profile, flattenUserSchema(schemas)); + } + + async getUserProfile(): Promise { + const baseUrl: string = (await this.asgardeo.getConfigData()).baseUrl; + + const profile = await getMeProfile({url: `${baseUrl}/scim2/Me`}); + const schemas = await getSchemas({url: `${baseUrl}/scim2/Schemas`}); + + console.log('Raw Schemas:', JSON.stringify(schemas, null, 2)); + + const processedSchemas = flattenUserSchema(schemas); + + console.log('Processed Schemas:', JSON.stringify(processedSchemas, null, 2)); + + return { + schemas: processedSchemas, + flattenedProfile: generateFlattenedUserProfile(profile, processedSchemas), + profile, + }; } override isLoading(): boolean { diff --git a/packages/react/src/__temp__/api.ts b/packages/react/src/__temp__/api.ts index dd46c12d..279ebcb8 100644 --- a/packages/react/src/__temp__/api.ts +++ b/packages/react/src/__temp__/api.ts @@ -19,10 +19,9 @@ import { AsgardeoSPAClient, AuthClientConfig, - BasicUserInfo, + User, LegacyConfig as Config, IdTokenPayload, - FetchResponse, Hooks, HttpClientInstance, HttpRequestConfig, @@ -30,6 +29,8 @@ import { OIDCEndpoints, SignInConfig, SPACustomGrantConfig, + initializeApplicationNativeAuthentication, + processOpenIDScopes, } from '@asgardeo/browser'; import {AuthStateInterface} from './models'; @@ -59,10 +60,6 @@ class AuthAPI { return this._isLoading; } - public isSignedIn(): Promise { - return this.isAuthenticated(); - } - public isLoading(): boolean { return this._getIsLoading(); } @@ -108,27 +105,25 @@ class AuthAPI { authorizationCode?: string, sessionState?: string, authState?: string, - callback?: (response: BasicUserInfo) => void, + callback?: (response: User) => void, tokenRequestConfig?: { params: Record; }, - ): Promise { + ): Promise { return this._client .signIn(config, authorizationCode, sessionState, authState, tokenRequestConfig) - .then(async (response: BasicUserInfo) => { + .then(async (response: User) => { if (!response) { return null; // FIXME: Validate this. Temp fix for: error TS7030: Not all code paths return a value. } - if (await this._client.isAuthenticated()) { + if (await this._client.isSignedIn()) { const stateToUpdate = { - allowedScopes: response.allowedScopes, displayName: response.displayName, email: response.email, - isAuthenticated: true, + isSignedIn: true, isLoading: false, isSigningOut: false, - sub: response.sub, username: response.username, }; @@ -183,10 +178,10 @@ class AuthAPI { /** * This method returns a Promise that resolves with the basic user information obtained from the ID token. * - * @return {Promise} - A promise that resolves with the user information. + * @return {Promise} - A promise that resolves with the user information. */ - public async getBasicUserInfo(): Promise { - return this._client.getBasicUserInfo(); + public async getUser(): Promise { + return this._client.getUser(); } /** @@ -197,7 +192,7 @@ class AuthAPI { * * @param {HttpRequestConfig} config - The config object containing attributes necessary to send a request. * - * @return {Promise} - Returns a Promise that resolves with the response to the request. + * @return {Promise} - Returns a Promise that resolves with the response to the request. */ public async httpRequest(config: HttpRequestConfig): Promise> { return this._client.httpRequest(config); @@ -211,7 +206,7 @@ class AuthAPI { * * @param {HttpRequestConfig[]} config - The config object containing attributes necessary to send a request. * - * @return {Promise} - Returns a Promise that resolves with the responses to the requests. + * @return {Promise} - Returns a Promise that resolves with the responses to the requests. */ public async httpRequestAll(configs: HttpRequestConfig[]): Promise[]> { return this._client.httpRequestAll(configs); @@ -222,17 +217,17 @@ class AuthAPI { * * @param {CustomGrantRequestParams} config - The request parameters. * - * @return {Promise | SignInResponse>} - A Promise that resolves with + * @return {Promise} - A Promise that resolves with * the value returned by the custom grant request. */ - public requestCustomGrant( + public exchangeToken( config: SPACustomGrantConfig, - callback: (response: BasicUserInfo | FetchResponse) => void, + callback: (response: User | Response) => void, dispatch: (state: AuthStateInterface) => void, - ): Promise> { + ): Promise { return this._client - .requestCustomGrant(config) - .then((response: BasicUserInfo | FetchResponse) => { + .exchangeToken(config) + .then((response: User | Response) => { if (!response) { return null; // FIXME: Validate this. Temp fix for: error TS7030: Not all code paths return a value. } @@ -240,12 +235,12 @@ class AuthAPI { if (config.returnsSession) { this.updateState({ ...this.getState(), - ...(response as BasicUserInfo), - isAuthenticated: true, + ...(response as User), + isSignedIn: true, isLoading: false, }); - dispatch({...(response as BasicUserInfo), isAuthenticated: true, isLoading: false}); + dispatch({...(response as User), isSignedIn: true, isLoading: false}); } callback && callback(response); @@ -280,8 +275,8 @@ class AuthAPI { * * @return {Promise { - return this._client.getOIDCServiceEndpoints(); + public async getOpenIDProviderEndpoints(): Promise { + return this._client.getOpenIDProviderEndpoints(); } /** @@ -299,8 +294,8 @@ class AuthAPI { * @return {Promise} - A Promise that resolves with * the decoded payload of the id token. */ - public async getDecodedIDToken(): Promise { - return this._client.getDecodedIDToken(); + public async getDecodedIdToken(): Promise { + return this._client.getDecodedIdToken(); } /** @@ -310,7 +305,7 @@ class AuthAPI { * the decoded payload of the idp id token. */ public async getDecodedIDPIDToken(): Promise { - return this._client.getDecodedIDToken(); + return this._client.getDecodedIdToken(); } /** @@ -318,8 +313,8 @@ class AuthAPI { * * @return {Promise} - A Promise that resolves with the id token. */ - public async getIDToken(): Promise { - return this._client.getIDToken(); + public async getIdToken(): Promise { + return this._client.getIdToken(); } /** @@ -351,7 +346,7 @@ class AuthAPI { * @return {TokenResponseInterface} - A Promise that resolves with an object containing * information about the refreshed access token. */ - public async refreshAccessToken(): Promise { + public async refreshAccessToken(): Promise { return this._client.refreshAccessToken(); } @@ -360,8 +355,8 @@ class AuthAPI { * * @return {Promise} - A Promise that resolves with `true` if teh user is authenticated. */ - public async isAuthenticated(): Promise { - return this._client.isAuthenticated(); + public async isSignedIn(): Promise { + return this._client.isSignedIn(); } /** @@ -397,8 +392,8 @@ class AuthAPI { * * @param {Partial>} config - A config object to update the SDK configurations with. */ - public async updateConfig(config: Partial>): Promise { - return this._client.updateConfig(config); + public async reInitialize(config: Partial>): Promise { + return this._client.reInitialize(config); } /** @@ -424,7 +419,7 @@ class AuthAPI { * First, this method sends a prompt none request to see if there is an active user session in the identity server. * If there is one, then it requests the access token and stores it. Else, it returns false. * - * @return {Promise} - A Promise that resolves with the user information after signing in + * @return {Promise} - A Promise that resolves with the user information after signing in * or with `false` if the user is not signed in. * * @example @@ -437,10 +432,10 @@ class AuthAPI { dispatch: (state: AuthStateInterface) => void, additionalParams?: Record, tokenRequestConfig?: {params: Record}, - ): Promise { + ): Promise { return this._client .trySignInSilently(additionalParams, tokenRequestConfig) - .then(async (response: BasicUserInfo | boolean) => { + .then(async (response: User | boolean) => { if (!response) { this.updateState({...this.getState(), isLoading: false}); dispatch({...state, isLoading: false}); @@ -448,16 +443,14 @@ class AuthAPI { return false; } - if (await this._client.isAuthenticated()) { - const basicUserInfo = response as BasicUserInfo; + if (await this._client.isSignedIn()) { + const basicUserInfo = response as User; const stateToUpdate = { - allowedScopes: basicUserInfo.allowedScopes, displayName: basicUserInfo.displayName, email: basicUserInfo.email, - isAuthenticated: true, + isSignedIn: true, isLoading: false, isSigningOut: false, - sub: basicUserInfo.sub, username: basicUserInfo.username, }; @@ -475,12 +468,10 @@ class AuthAPI { } AuthAPI.DEFAULT_STATE = { - allowedScopes: '', displayName: '', email: '', - isAuthenticated: false, + isSignedIn: false, isLoading: true, - sub: '', username: '', }; diff --git a/packages/react/src/__temp__/models.ts b/packages/react/src/__temp__/models.ts index f6d1edae..4ff057e7 100644 --- a/packages/react/src/__temp__/models.ts +++ b/packages/react/src/__temp__/models.ts @@ -20,17 +20,16 @@ import { AsgardeoAuthException, AuthClientConfig, AuthSPAClientConfig, - BasicUserInfo, Config, - CustomGrantConfig, + TokenExchangeRequestConfig, IdTokenPayload, - FetchResponse, Hooks, HttpClientInstance, HttpRequestConfig, HttpResponse, OIDCEndpoints, SignInConfig, + User, } from '@asgardeo/browser'; export interface ReactConfig { @@ -55,10 +54,6 @@ export type AuthReactConfig = AuthSPAClientConfig & ReactConfig; * via `state` object from `useAuthContext` hook. */ export interface AuthStateInterface { - /** - * The scopes that are allowed for the user. - */ - allowedScopes: string; /** * The display name of the user. */ @@ -70,15 +65,11 @@ export interface AuthStateInterface { /** * Specifies if the user is authenticated or not. */ - isAuthenticated: boolean; + isSignedIn: boolean; /** * Are the Auth requests loading. */ isLoading: boolean; - /** - * The uid corresponding to the user who the ID token belonged to. - */ - sub?: string; /** * The username of the user. */ @@ -91,35 +82,32 @@ export interface AuthContextInterface { authorizationCode?: string, sessionState?: string, state?: string, - callback?: (response: BasicUserInfo) => void, + callback?: (response: User) => void, tokenRequestConfig?: { params: Record; }, - ) => Promise; + ) => Promise; signOut: (callback?: (response: boolean) => void) => Promise; - getBasicUserInfo(): Promise; + getUser(): Promise; httpRequest(config: HttpRequestConfig): Promise>; httpRequestAll(configs: HttpRequestConfig[]): Promise[]>; - requestCustomGrant( - config: CustomGrantConfig, - callback?: (response: BasicUserInfo | FetchResponse) => void, - ): void; + exchangeToken(config: TokenExchangeRequestConfig, callback?: (response: User | Response) => void): void; revokeAccessToken(): Promise; - getOIDCServiceEndpoints(): Promise; + getOpenIDProviderEndpoints(): Promise; getHttpClient(): Promise; getDecodedIDPIDToken(): Promise; - getDecodedIDToken(): Promise; - getIDToken(): Promise; + getDecodedIdToken(): Promise; + getIdToken(): Promise; getAccessToken(): Promise; - refreshAccessToken(): Promise; - isAuthenticated(): Promise; + refreshAccessToken(): Promise; + isSignedIn(): Promise; enableHttpHandler(): Promise; disableHttpHandler(): Promise; - updateConfig(config: Partial>): Promise; + reInitialize(config: Partial>): Promise; trySignInSilently: ( additionalParams?: Record, tokenRequestConfig?: {params: Record}, - ) => Promise; + ) => Promise; on(hook: Hooks.CustomGrant, callback: (response?: any) => void, id: string): void; on(hook: Exclude, callback: (response?: any) => void): void; on(hook: Hooks, callback: (response?: any) => void, id?: string): void; diff --git a/packages/react/src/components/actions/SignInButton/BaseSignInButton.tsx b/packages/react/src/components/actions/SignInButton/BaseSignInButton.tsx index c13decb0..737994f6 100644 --- a/packages/react/src/components/actions/SignInButton/BaseSignInButton.tsx +++ b/packages/react/src/components/actions/SignInButton/BaseSignInButton.tsx @@ -25,8 +25,9 @@ import { Ref, RefAttributes, } from 'react'; -import {withVendorCSSClassPrefix} from '@asgardeo/browser'; +import {WithPreferences, withVendorCSSClassPrefix} from '@asgardeo/browser'; import clsx from 'clsx'; +import Button from '../../primitives/Button/Button'; /** * Common props shared by all {@link BaseSignInButton} components. @@ -35,7 +36,7 @@ export interface CommonBaseSignInButtonProps { /** * Function to initiate the sign-in process */ - signIn?: () => Promise; + signIn: () => Promise; /** * Loading state during sign-in process */ @@ -51,8 +52,9 @@ export type BaseSignInButtonRenderProps = CommonBaseSignInButtonProps; * Props interface of {@link BaseSignInButton} */ export interface BaseSignInButtonProps - extends CommonBaseSignInButtonProps, - Omit, 'children'> { + extends Partial, + Omit, 'children'>, + WithPreferences { /** * Render prop function that receives sign-in props, or traditional ReactNode children */ @@ -65,7 +67,7 @@ export interface BaseSignInButtonProps * @example Using render props * ```tsx * - * {({ signIn, isLoading }) => ( + * {({signIn, isLoading}) => ( * @@ -81,7 +83,7 @@ export interface BaseSignInButtonProps const BaseSignInButton: ForwardRefExoticComponent> = forwardRef( ( - {children, className, style, signIn, isLoading, ...rest}: BaseSignInButtonProps, + {children, className, style, signIn, isLoading, preferences, ...rest}: BaseSignInButtonProps, ref: Ref, ): ReactElement => { if (typeof children === 'function') { @@ -89,16 +91,17 @@ const BaseSignInButton: ForwardRefExoticComponent {children} - + ); }, ); diff --git a/packages/react/src/components/actions/SignInButton/SignInButton.tsx b/packages/react/src/components/actions/SignInButton/SignInButton.tsx index cec16807..2ca4f189 100644 --- a/packages/react/src/components/actions/SignInButton/SignInButton.tsx +++ b/packages/react/src/components/actions/SignInButton/SignInButton.tsx @@ -17,8 +17,10 @@ */ import {forwardRef, ForwardRefExoticComponent, MouseEvent, ReactElement, Ref, RefAttributes, useState} from 'react'; -import useAsgardeo from '../../../hooks/useAsgardeo'; +import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; +import useTranslation from '../../../hooks/useTranslation'; import BaseSignInButton, {BaseSignInButtonProps} from './BaseSignInButton'; +import {AsgardeoRuntimeError} from '@asgardeo/browser'; /** * Props interface of {@link SignInButton} @@ -33,8 +35,8 @@ export type SignInButtonProps = BaseSignInButtonProps; * @example Using render props * ```tsx * - * {({ handleSignIn, isLoading }) => ( - * * )} @@ -45,12 +47,33 @@ export type SignInButtonProps = BaseSignInButtonProps; * ```tsx * Sign In * ``` + * + * @example Using component-level preferences + * ```tsx + * + * Custom Sign In + * + * ``` */ const SignInButton: ForwardRefExoticComponent> = forwardRef< HTMLButtonElement, SignInButtonProps ->(({children = 'Sign In', onClick, ...rest}: SignInButtonProps, ref: Ref): ReactElement => { +>(({children, onClick, preferences, ...rest}: SignInButtonProps, ref: Ref): ReactElement => { const {signIn} = useAsgardeo(); + const {t} = useTranslation(preferences?.i18n); + const [isLoading, setIsLoading] = useState(false); const handleSignIn = async (e?: MouseEvent): Promise => { @@ -63,15 +86,27 @@ const SignInButton: ForwardRefExoticComponent - {children} + + {children ?? t('elements.buttons.signIn')} ); }); diff --git a/packages/react/src/components/actions/SignOutButton/BaseSignOutButton.tsx b/packages/react/src/components/actions/SignOutButton/BaseSignOutButton.tsx index 5e59b0f3..d8c18c17 100644 --- a/packages/react/src/components/actions/SignOutButton/BaseSignOutButton.tsx +++ b/packages/react/src/components/actions/SignOutButton/BaseSignOutButton.tsx @@ -25,8 +25,9 @@ import { Ref, RefAttributes, } from 'react'; -import {withVendorCSSClassPrefix} from '@asgardeo/browser'; +import {WithPreferences, withVendorCSSClassPrefix} from '@asgardeo/browser'; import clsx from 'clsx'; +import Button from '../../primitives/Button/Button'; /** * Common props shared by all {@link BaseSignOutButton} components. @@ -35,7 +36,7 @@ export interface CommonBaseSignOutButtonProps { /** * Function to initiate the sign-out process */ - signOut?: () => Promise; + signOut: () => Promise; /** * Loading state during sign-out process */ @@ -51,8 +52,9 @@ export type BaseSignOutButtonRenderProps = CommonBaseSignOutButtonProps; * Props interface of {@link BaseSignOutButton} */ export interface BaseSignOutButtonProps - extends CommonBaseSignOutButtonProps, - Omit, 'children'> { + extends Partial, + Omit, 'children'>, + WithPreferences { /** * Render prop function that receives sign-out props, or traditional ReactNode children */ @@ -65,7 +67,7 @@ export interface BaseSignOutButtonProps * @example Using render props * ```tsx * - * {({ signOut, isLoading }) => ( + * {({signOut, isLoading}) => ( * @@ -81,7 +83,7 @@ export interface BaseSignOutButtonProps const BaseSignOutButton: ForwardRefExoticComponent> = forwardRef( ( - {children, className, style, signOut, isLoading, ...rest}: BaseSignOutButtonProps, + {children, className, style, signOut, isLoading, preferences, ...rest}: BaseSignOutButtonProps, ref: Ref, ): ReactElement => { if (typeof children === 'function') { @@ -89,16 +91,19 @@ const BaseSignOutButton: ForwardRefExoticComponent {children} - + ); }, ); diff --git a/packages/react/src/components/actions/SignOutButton/SignOutButton.tsx b/packages/react/src/components/actions/SignOutButton/SignOutButton.tsx index d9f476e1..fc4f7569 100644 --- a/packages/react/src/components/actions/SignOutButton/SignOutButton.tsx +++ b/packages/react/src/components/actions/SignOutButton/SignOutButton.tsx @@ -17,8 +17,10 @@ */ import {FC, forwardRef, ForwardRefExoticComponent, MouseEvent, ReactElement, Ref, RefAttributes, useState} from 'react'; -import useAsgardeo from '../../../hooks/useAsgardeo'; +import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; +import useTranslation from '../../../hooks/useTranslation'; import BaseSignOutButton, {BaseSignOutButtonProps} from './BaseSignOutButton'; +import {AsgardeoRuntimeError} from '@asgardeo/browser'; /** * Props interface of {@link SignOutButton} @@ -28,10 +30,12 @@ export type SignOutButtonProps = BaseSignOutButtonProps; /** * SignOutButton component that supports both render props and traditional props patterns. * + * @remarks This component is only supported in browser based React applications (CSR). + * * @example Using render props pattern * ```tsx * - * {({ signOut, isLoading }) => ( + * {({signOut, isLoading}) => ( * @@ -43,12 +47,33 @@ export type SignOutButtonProps = BaseSignOutButtonProps; * ```tsx * Sign Out * ``` + * + * @example Using component-level preferences + * ```tsx + * + * Custom Sign Out + * + * ``` */ const SignOutButton: ForwardRefExoticComponent> = forwardRef< HTMLButtonElement, SignOutButtonProps ->(({children = 'Sign Out', onClick, ...rest}: SignOutButtonProps, ref: Ref): ReactElement => { +>(({children, onClick, preferences, ...rest}: SignOutButtonProps, ref: Ref): ReactElement => { const {signOut} = useAsgardeo(); + const {t} = useTranslation(preferences?.i18n); + const [isLoading, setIsLoading] = useState(false); const handleSignOut = async (e?: MouseEvent): Promise => { @@ -60,15 +85,27 @@ const SignOutButton: ForwardRefExoticComponent - {children} + + {children ?? t('elements.buttons.signOut')} ); }); diff --git a/packages/react/src/components/actions/SignUpButton/BaseSignUpButton.tsx b/packages/react/src/components/actions/SignUpButton/BaseSignUpButton.tsx index 256d7584..8e8711de 100644 --- a/packages/react/src/components/actions/SignUpButton/BaseSignUpButton.tsx +++ b/packages/react/src/components/actions/SignUpButton/BaseSignUpButton.tsx @@ -25,8 +25,9 @@ import { Ref, RefAttributes, } from 'react'; -import {withVendorCSSClassPrefix} from '@asgardeo/browser'; +import {WithPreferences, withVendorCSSClassPrefix} from '@asgardeo/browser'; import clsx from 'clsx'; +import Button from '../../primitives/Button/Button'; /** * Common props shared by all {@link BaseSignUpButton} components. @@ -52,7 +53,8 @@ export type BaseSignUpButtonRenderProps = CommonBaseSignUpButtonProps; */ export interface BaseSignUpButtonProps extends CommonBaseSignUpButtonProps, - Omit, 'children'> { + Omit, 'children'>, + WithPreferences { /** * Render prop function that receives sign-up props, or traditional ReactNode children */ @@ -81,7 +83,7 @@ export interface BaseSignUpButtonProps const BaseSignUpButton: ForwardRefExoticComponent> = forwardRef( ( - {children, className, style, signUp, isLoading, ...rest}: BaseSignUpButtonProps, + {children, className, style, signUp, isLoading, preferences, ...rest}: BaseSignUpButtonProps, ref: Ref, ): ReactElement => { if (typeof children === 'function') { @@ -89,16 +91,19 @@ const BaseSignUpButton: ForwardRefExoticComponent {children} - + ); }, ); diff --git a/packages/react/src/components/actions/SignUpButton/SignUpButton.tsx b/packages/react/src/components/actions/SignUpButton/SignUpButton.tsx index 5521e9e9..548b591c 100644 --- a/packages/react/src/components/actions/SignUpButton/SignUpButton.tsx +++ b/packages/react/src/components/actions/SignUpButton/SignUpButton.tsx @@ -17,8 +17,10 @@ */ import {FC, forwardRef, ForwardRefExoticComponent, MouseEvent, ReactElement, Ref, RefAttributes, useState} from 'react'; -import useAsgardeo from '../../../hooks/useAsgardeo'; +import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; +import useTranslation from '../../../hooks/useTranslation'; import BaseSignUpButton, {BaseSignUpButtonProps} from './BaseSignUpButton'; +import {AsgardeoRuntimeError} from '@asgardeo/browser'; /** * Props interface of {@link SignUpButton} @@ -29,6 +31,8 @@ export type SignUpButtonProps = BaseSignUpButtonProps; * SignUpButton component that supports both render props and traditional props patterns. * It redirects the user to the Asgardeo sign-up page configured for the application. * + * @remarks This component is only supported in browser based React applications (CSR). + * * @example Using render props pattern * ```tsx * @@ -44,12 +48,33 @@ export type SignUpButtonProps = BaseSignUpButtonProps; * ```tsx * Create Account * ``` + * + * @example Using component-level preferences + * ```tsx + * + * Custom Sign Up + * + * ``` */ const SignUpButton: ForwardRefExoticComponent> = forwardRef< HTMLButtonElement, SignUpButtonProps ->(({children = 'Sign Up', onClick, ...rest}: SignUpButtonProps, ref: Ref): ReactElement => { +>(({children, onClick, preferences, ...rest}: SignUpButtonProps, ref: Ref): ReactElement => { const {signUp} = useAsgardeo(); + const {t} = useTranslation(preferences?.i18n); + const [isLoading, setIsLoading] = useState(false); const handleSignUp = async (e?: MouseEvent): Promise => { @@ -61,15 +86,27 @@ const SignUpButton: ForwardRefExoticComponent - {children} + + {children ?? t('elements.buttons.signUp')} ); }); diff --git a/packages/react/src/components/control/SignedIn.tsx b/packages/react/src/components/control/SignedIn.tsx index eeeb462f..298e7725 100644 --- a/packages/react/src/components/control/SignedIn.tsx +++ b/packages/react/src/components/control/SignedIn.tsx @@ -17,7 +17,7 @@ */ import {FC, PropsWithChildren, ReactNode} from 'react'; -import useAsgardeo from '../../hooks/useAsgardeo'; +import useAsgardeo from '../../contexts/Asgardeo/useAsgardeo'; /** * Props for the SignedIn component. diff --git a/packages/react/src/components/control/SignedOut.tsx b/packages/react/src/components/control/SignedOut.tsx index b5b94605..61a9a14c 100644 --- a/packages/react/src/components/control/SignedOut.tsx +++ b/packages/react/src/components/control/SignedOut.tsx @@ -17,7 +17,7 @@ */ import {FC, PropsWithChildren, ReactNode} from 'react'; -import useAsgardeo from '../../hooks/useAsgardeo'; +import useAsgardeo from '../../contexts/Asgardeo/useAsgardeo'; /** * Props for the SignedOut component. diff --git a/packages/react/src/components/factories/FieldFactory.tsx b/packages/react/src/components/factories/FieldFactory.tsx new file mode 100644 index 00000000..6c2109c7 --- /dev/null +++ b/packages/react/src/components/factories/FieldFactory.tsx @@ -0,0 +1,235 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {FC, ReactElement} from 'react'; +import TextField from '../primitives/TextField/TextField'; +import Select from '../primitives/Select/Select'; +import {SelectOption} from '../primitives/Select/Select'; +import OtpField from '../primitives/OtpField/OtpField'; +import PasswordField from '../primitives/PasswordField/PasswordField'; +import {FieldType} from '@asgardeo/browser'; + +/** + * Interface for field configuration. + */ +export interface FieldConfig { + name: string; + /** + * The field type based on ApplicationNativeAuthenticationAuthenticatorParamType. + */ + type: FieldType; + /** + * Display name for the field. + */ + label: string; + /** + * Whether the field is required. + */ + required: boolean; + /** + * Current value of the field. + */ + value: string; + /** + * Callback function when the field value changes. + */ + onChange: (value: string) => void; + /** + * Whether the field is disabled. + */ + disabled?: boolean; + /** + * Error message to display. + */ + error?: string; + /** + * Additional CSS class name. + */ + className?: string; + /** + * Additional options for multi-valued fields. + */ + options?: SelectOption[]; + /** + * Whether the field has been touched/interacted with by the user. + */ + touched?: boolean; + /** + * Placeholder text for the field. + */ + placeholder?: string; +} + +/** + * Utility function to parse multi-valued string into array + */ +export const parseMultiValuedString = (value: string): string[] => { + if (!value || value.trim() === '') return []; + return value + .split(',') + .map(item => item.trim()) + .filter(item => item.length > 0); +}; + +/** + * Utility function to format array into multi-valued string + */ +export const formatMultiValuedString = (values: string[]): string => { + return values.join(', '); +}; + +/** + * Utility function to validate field values based on type + */ +export const validateFieldValue = ( + value: string, + type: FieldType, + required: boolean = false, + touched: boolean = false, +): string | null => { + // Only show required field errors if the field has been touched + if (required && touched && (!value || value.trim() === '')) { + return 'This field is required'; + } + + // If not required and empty, no validation needed + if (!value || value.trim() === '') { + return null; + } + + switch (type) { + case FieldType.Number: + const numValue = parseInt(value, 10); + if (isNaN(numValue)) { + return 'Please enter a valid number'; + } + break; + } + + return null; +}; + +/** + * Factory function to create form fields based on the ApplicationNativeAuthenticationAuthenticatorParamType. + * + * @param config - The field configuration + * @returns The appropriate React component for the field type + * + * @example + * ```tsx + * const field = createField({ + * param: 'username', + * type: ApplicationNativeAuthenticationAuthenticatorParamType.String, + * label: 'Username', + * confidential: false, + * required: true, + * value: '', + * onChange: (value) => console.log(value) + * }); + * ``` + */ +export const createField = (config: FieldConfig): ReactElement => { + const { + name, + type, + label, + required, + value, + onChange, + disabled = false, + error, + className, + options = [], + touched = false, + placeholder, + } = config; + + // Auto-validate the field value + const validationError = error || validateFieldValue(value, type, required, touched); + + const commonProps = { + name, + label, + required, + disabled, + error: validationError, + className, + value, + placeholder, + }; + + switch (type) { + case FieldType.Password: + return ; + case FieldType.Text: + return onChange(e.target.value)} autoComplete="off" />; + case FieldType.Otp: + return onChange(e.target.value)} />; + case FieldType.Number: + return ( + onChange(e.target.value)} + helperText="Enter a numeric value" + /> + ); + case FieldType.Select: + const fieldOptions = options.length > 0 ? options : []; + + if (fieldOptions.length > 0) { + return ( +