diff --git a/experimental/msal-react/.prettierignore b/experimental/msal-react/.prettierignore
new file mode 100644
index 0000000000..499258769d
--- /dev/null
+++ b/experimental/msal-react/.prettierignore
@@ -0,0 +1 @@
+src/index.ts
\ No newline at end of file
diff --git a/experimental/msal-react/.storybook/main.js b/experimental/msal-react/.storybook/main.js
index 413d9ffff7..7bfb8ce4ac 100644
--- a/experimental/msal-react/.storybook/main.js
+++ b/experimental/msal-react/.storybook/main.js
@@ -1,6 +1,6 @@
module.exports = {
stories: ['../stories/**/*.stories.(ts|tsx)'],
- addons: ['@storybook/addon-actions', '@storybook/addon-links', '@storybook/addon-docs'],
+ addons: ['@storybook/addon-actions', '@storybook/addon-links', '@storybook/addon-docs', '@storybook/addon-storysource'],
webpackFinal: async (config) => {
config.module.rules.push({
test: /\.(ts|tsx)$/,
diff --git a/experimental/msal-react/package-lock.json b/experimental/msal-react/package-lock.json
index d61d197c4b..5550f96627 100644
--- a/experimental/msal-react/package-lock.json
+++ b/experimental/msal-react/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "@azure/msal-react",
- "version": "0.1.0",
+ "version": "0.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/experimental/msal-react/package.json b/experimental/msal-react/package.json
index e529a119a5..ae18707cdd 100644
--- a/experimental/msal-react/package.json
+++ b/experimental/msal-react/package.json
@@ -58,5 +58,8 @@
"tsdx": "^0.13.2",
"tslib": "^2.0.0",
"typescript": "^3.9.7"
+ },
+ "dependencies": {
+ "@storybook/addon-storysource": "^5.3.19"
}
}
diff --git a/experimental/msal-react/src/MsalAuthentication.tsx b/experimental/msal-react/src/MsalAuthentication.tsx
new file mode 100644
index 0000000000..9a6ede16ad
--- /dev/null
+++ b/experimental/msal-react/src/MsalAuthentication.tsx
@@ -0,0 +1,74 @@
+import { AuthenticationResult } from '@azure/msal-browser';
+import React, { useState, useCallback, useEffect, useMemo } from 'react';
+
+import { IMsalContext } from './MsalContext';
+import { useMsal } from './MsalProvider';
+import { getChildrenOrFunction, defaultLoginHandler } from './utilities';
+import { useIsAuthenticated } from './useIsAuthenticated';
+
+export interface IMsalAuthenticationProps {
+ username?: string;
+ loginHandler?: (context: IMsalContext) => Promise;
+}
+
+type MsalAuthenticationResult = {
+ error: Error | null;
+ msal: IMsalContext;
+};
+
+// TODO: Add optional argument for the `request` object?
+export function useMsalAuthentication(
+ args: IMsalAuthenticationProps = {}
+): MsalAuthenticationResult {
+ const { username, loginHandler = defaultLoginHandler } = args;
+ const msal = useMsal();
+ const isAuthenticated = useIsAuthenticated(username);
+
+ const [error, setError] = useState(null);
+
+ // TODO: How are we passing errors down?
+ const login = useCallback(() => {
+ // TODO: This is error prone because it asynchronously sets state, but the component may be unmounted before the process completes.
+ // Additionally, other authentication components or hooks won't have access to the errors.
+ // May be better to lift this state into the the MsalProvider
+ return loginHandler(msal).catch(error => {
+ setError(error);
+ });
+ }, [msal, loginHandler]);
+
+ useEffect(() => {
+ // TODO: What if there is an error? How do errors get cleared?
+ // TODO: What if user cancels the flow?
+ if (!isAuthenticated) {
+ login();
+ }
+ // TODO: the `login` function needs to be added to the deps array.
+ // Howevever, when it's added it will cause a double login issue because we're not
+ // currently tracking when an existing login is InProgress
+ }, [isAuthenticated]);
+
+ return useMemo(
+ () => ({
+ error,
+ msal,
+ }),
+ [error, msal]
+ );
+}
+
+export const MsalAuthentication: React.FunctionComponent = props => {
+ const { username, loginHandler, children } = props;
+ const { msal } = useMsalAuthentication({ username, loginHandler });
+ const isAuthenticated = useIsAuthenticated(username);
+
+ // TODO: What if the user authentiction is InProgress? How will user show a loading state?
+ if (isAuthenticated) {
+ return (
+
+ {getChildrenOrFunction(children, msal)}
+
+ );
+ }
+
+ return null;
+};
diff --git a/experimental/msal-react/src/MsalContext.tsx b/experimental/msal-react/src/MsalContext.tsx
index 0244b5c16d..27c6021fb3 100644
--- a/experimental/msal-react/src/MsalContext.tsx
+++ b/experimental/msal-react/src/MsalContext.tsx
@@ -1,7 +1,7 @@
-import React from 'react';
+import * as React from 'react';
import { IPublicClientApplication, AccountInfo } from '@azure/msal-browser';
-export type MsalState = {
+type MsalState = {
accounts: AccountInfo[];
};
@@ -24,6 +24,7 @@ const defaultMsalContext: IMsalContext = {
return Promise.reject();
},
getAllAccounts: () => {
+ debugger;
return [];
},
getAccountByUsername: () => {
diff --git a/experimental/msal-react/src/MsalProvider.tsx b/experimental/msal-react/src/MsalProvider.tsx
index f7991d80ee..a71e3e44b2 100644
--- a/experimental/msal-react/src/MsalProvider.tsx
+++ b/experimental/msal-react/src/MsalProvider.tsx
@@ -1,4 +1,5 @@
import * as React from 'react';
+import { useContext } from 'react';
import {
IPublicClientApplication,
AccountInfo,
@@ -18,11 +19,13 @@ export const MsalProvider: React.FunctionComponent = ({
}) => {
// State hook to store accounts
const [accounts, setAccounts] = React.useState(
+ // TODO: Remove the `|| []` hack when PR is finally merged to msal/browser
instance.getAllAccounts() || []
);
// Callback to update accounts after MSAL APIs are invoked
const updateContextState = React.useCallback(() => {
+ // TODO: Remove the `|| []` hack when PR is finally merged to msal/browser
setAccounts(instance.getAllAccounts() || []);
}, [instance]);
@@ -76,14 +79,15 @@ export const MsalProvider: React.FunctionComponent = ({
}, [instance, updateContextState]);
// Memoized context value
- const contextValue = React.useMemo(() => {
- return {
+ const contextValue = React.useMemo(
+ () => ({
instance: wrappedInstance,
state: {
accounts,
},
- };
- }, [wrappedInstance, accounts]);
+ }),
+ [wrappedInstance, accounts]
+ );
return (
@@ -91,3 +95,5 @@ export const MsalProvider: React.FunctionComponent = ({
);
};
+
+export const useMsal = () => useContext(MsalContext);
diff --git a/experimental/msal-react/src/README.md b/experimental/msal-react/src/README.md
new file mode 100644
index 0000000000..7f6fe8e2ef
--- /dev/null
+++ b/experimental/msal-react/src/README.md
@@ -0,0 +1,300 @@
+# MSAL React POC
+
+This folder contains a proof-of-concept for the MSAL React library. It is intended to demonstrate our thinking on what a MSAL React wrapper library might look like.
+
+## Documentation
+
+### Installation
+
+MSAL React will have `@azure/msal-browser` listed as a peer dependency.
+
+```sh
+npm install react react-dom
+npm install @azure/msal-react @azure/msal-browser
+```
+
+### API
+
+#### MsalProvider
+
+MSAL React will be configured with the same configuration for MSAL.js itself, along with any configuration options that are specific to MSAL React (TBD). This will be passed to the `MsalProvider` component, which will put an instance of `PublicClientApplication` in the React context. Using `MsalProvider` will be a requirement for the other APIs. It will be recommended to use `MsalProvider` at the top-level of your application.
+
+`MsalProvider` and `MsalConsumer` are built on the [React context API](https://reactjs.org/docs/context.html). There are various ways of recieving the context values, including components and hooks.
+
+```javascript
+// index.js
+import React from "react";
+import ReactDOM from "react-dom";
+
+import { MsalProvider } from "@azure/msal-react";
+import { Configuration } from "@azure/msal-browser";
+
+import App from "./app.jsx";
+
+// MSAL configuration
+const configuration: Configuration = {
+ auth: {
+ clientId: "client-id"
+ }
+};
+
+// Component
+const AppProvider = () => (
+
+
+
+);
+
+ReactDOM.render(, document.getElementById("root"));
+```
+
+#### useMsal
+
+A hook that returns the instance of `PublicClientApplication` to be used within a function component. Changes to the context from the `MsalProvider` will allow your function component to update as required.
+
+```js
+import React from 'react';
+import { useMsal } from "@azure/msal-react";
+
+export function HomePage() {
+ const msal = useMsal();
+
+ if (msal.isAuthenticated) {
+ return You are currently authenticated.
+ } else {
+ return You are not authenticated yet.
+ }
+}
+```
+
+
+#### MsalConsumer
+
+When using `MsalProvider`, MSAL React will put an instance of `PublicClientApplication` in context, which applications can consume via the `MsalConsumer` component. This will pass down the instance of `PubliClientApplication` using the [function-as-a-child pattern](https://reactjs.org/docs/context.html#contextconsumer).
+
+```js
+import React from 'react';
+import { MsalConsumer } from "@azure/msal-react";
+
+export function HomePage() {
+ return (
+
+ {msal => (
+
+ )}
+
+ )}
+```
+
+
+#### MsalContext
+
+The raw context for MSAL React. It will not be recommended to use this directly, except when your application needs to access the context type itself (e.g. consuming the MSAL React context directly).
+
+```js
+// app.js
+import React from "react";
+import { MsalContext } from "@azure/msal-react";
+
+class App extends React.Component {
+ static contextType = MsalContext;
+
+ render() {
+ return (
+
+ )
+ }
+}
+
+export default App;
+```
+
+#### withMsal
+
+A higher-order component which will pass the MSAL instance as a prop (instead of via context). The instance of `withMsal` must be a child (at any level) of `MsalProvider`.
+
+```js
+// app.js
+import React from "react";
+import { IMsalPropType } from "@azure/msal-react";
+
+const scopes = ['user.read'];
+
+class App extends React.Component {
+ static propTypes = {
+ msal: IMsalPropType
+ }
+
+ render() {
+ if (this.props.msal.isAuthenticated) {
+ return (
+
+ );
+ } else {
+ return (
+
+ );
+ }
+ }
+}
+
+export const WrappedApplication = withMsal(App);
+
+// index.js
+import { WrappedApplication } from "./app.js";
+
+const AppProvider = () => (
+
+
+
+);
+
+ReactDOM.render(, document.getElementById("root"));
+```
+
+#### AuthenticatedTemplate
+
+The `AuthenticatedTemplate` component will only render the children if the user has a currently authenticated account. This allows conditional rendering of content or components that require a certain authentication state.
+
+Additionally, the `AuthenticatedTemplate` provides the option to pass a function as a child using the [function-as-a-child pattern](https://reactjs.org/docs/context.html#contextconsumer). This will pass down the instance of `PubliClientApplication` as the only argument for more advanced conditional logic.
+
+```js
+import React from 'react';
+import { AuthenticatedTemplate } from "@azure/msal-react";
+
+export function HomePage() {
+ return (
+
+
Anyone can see this paragraph.
+
+
But only authenticated users will see this paragraph.
+
+
+ {(msal) => {
+ return (
+
You have {msal.accounts.length} accounts authenticated.
+ );
+ }}
+
+
+ );
+}
+```
+
+#### UnauthenticatedTemplate
+
+The `UnauthenticatedTemplate` component will only render the children if the user has a no accounts currently authenticated. This allows conditional rendering of content or components that require a certain authentication state.
+
+Additionally, the `UnauthenticatedTemplate` provides the option to pass a function as a child using the [function-as-a-child pattern](https://reactjs.org/docs/context.html#contextconsumer). This will pass down the instance of `PubliClientApplication` as the only argument for more advanced conditional logic.
+
+```js
+import React from 'react';
+import { UnauthenticatedTemplate } from "@azure/msal-react";
+
+export function HomePage() {
+ return (
+
+
Anyone can see this paragraph.
+
+
But only user's who have no authenticated accounts will see this paragraph.
+
+
+ {(msal) => {
+ return (
+
+ );
+ }}
+
+
+ );
+}
+```
+
+#### MsalAuthentication
+
+The `MsalAuthentication` component takes props that allow you to configure the authentication method and guarentees that authentication will be executed when the component is rendered. A default authentication flow will be initiated using the instance methods of `PublicClientApplication` provided by the `MsalProvider`.
+
+For more advanced use cases, the default authentication logic implemented with `MsalAuthentication` may not be suitable. In these situations, it may be better to write a custom hook or component that uses the instance of `PublicClientApplication` from the `MsalProvider` to support more specific behavior.
+
+```js
+import React from 'react';
+import { MsalAuthentication, AuthenticationType, UnauthenticatedTemplate, AuthenticatedTemplate } from "@azure/msal-react";
+
+export function ProtectedComponent() {
+ return (
+
+
Protected Component
+
Any children of the MsalAuthentication component will be rendered unless they are wrapped in a conditional template.
+
+
+
Please login before viewing this page.
+
+
+
Thank you for logging in with your account!
+
+
+ );
+}
+```
+
+
+
+
+#### useHandleRedirect
+
+React hook to receive the response from redirect operations (wrapper around `handleRedirectPromise`).
+
+TODO: Error handling
+
+```js
+export function RedirectPage() {
+ const redirectResult = useHandleRedirect();
+
+ if (redirectResult) {
+ return (
+
+
Redirect response:
+
{JSON.stringify(redirectResult, null, 4)}
+
+ );
+ } else {
+ return (
+
This page is not returning from a redirect operation.
+ );
+ }
+}
+```
diff --git a/experimental/msal-react/src/Templates.tsx b/experimental/msal-react/src/Templates.tsx
new file mode 100644
index 0000000000..5493d4c6f1
--- /dev/null
+++ b/experimental/msal-react/src/Templates.tsx
@@ -0,0 +1,39 @@
+import React from 'react';
+
+import { useMsal } from './MsalProvider';
+import { getChildrenOrFunction } from './utilities';
+import { useIsAuthenticated } from './useIsAuthenticated';
+
+export interface IMsalTemplateProps {
+ username?: string;
+}
+
+export const UnauthenticatedTemplate: React.FunctionComponent = props => {
+ const { children, username } = props;
+ const context = useMsal();
+ const isAuthenticated = useIsAuthenticated(username);
+
+ if (!isAuthenticated) {
+ return (
+
+ {getChildrenOrFunction(children, context)}
+
+ );
+ }
+ return null;
+};
+
+export const AuthenticatedTemplate: React.FunctionComponent = props => {
+ const { children, username } = props;
+ const context = useMsal();
+ const isAuthenticated = useIsAuthenticated(username);
+
+ if (isAuthenticated) {
+ return (
+
+ {getChildrenOrFunction(children, context)}
+
+ );
+ }
+ return null;
+};
diff --git a/experimental/msal-react/src/index.ts b/experimental/msal-react/src/index.ts
new file mode 100644
index 0000000000..43a1f7c220
--- /dev/null
+++ b/experimental/msal-react/src/index.ts
@@ -0,0 +1,22 @@
+// TODO: How do we get the token before executing an API call in pure TypeScript, outside of a React component context?
+// TODO: How do we raise the `error` state into the MsalContext, where changes will allow all subscribed components to update?
+// TODO: How do we represent the current state of the authentication process (Authenticated, InProgress, IsError, Unauthenticated)?
+// This will be important for showing intermediary UI such as loading or error components
+
+export type { IMsalContext } from "./MsalContext";
+export { MsalContext, MsalConsumer } from "./MsalContext";
+
+export type { MsalProviderProps } from "./MsalProvider"
+export { MsalProvider, useMsal } from "./MsalProvider";
+
+export type { IMsalAuthenticationProps } from "./MsalAuthentication"
+export { MsalAuthentication, useMsalAuthentication } from "./MsalAuthentication";
+
+export { AuthenticatedTemplate, UnauthenticatedTemplate } from "./Templates";
+
+export type { IWithMsalProps } from "./withMsal";
+export { withMsal } from "./withMsal";
+
+export { useHandleRedirect } from "./useHandleRedirect";
+
+export { useIsAuthenticated } from './useIsAuthenticated';
\ No newline at end of file
diff --git a/experimental/msal-react/src/index.tsx b/experimental/msal-react/src/index.tsx
deleted file mode 100644
index 02ce841efd..0000000000
--- a/experimental/msal-react/src/index.tsx
+++ /dev/null
@@ -1,2 +0,0 @@
-export { MsalConsumer, MsalContext } from './MsalContext';
-export { MsalProvider } from './MsalProvider';
diff --git a/experimental/msal-react/src/useHandleRedirect.ts b/experimental/msal-react/src/useHandleRedirect.ts
new file mode 100644
index 0000000000..123a7d1d59
--- /dev/null
+++ b/experimental/msal-react/src/useHandleRedirect.ts
@@ -0,0 +1,23 @@
+import { AuthenticationResult } from '@azure/msal-browser';
+import { useState, useEffect } from 'react';
+
+import { useMsal } from './MsalProvider';
+
+export function useHandleRedirect(): AuthenticationResult | null {
+ const { instance } = useMsal();
+ const [
+ redirectResponse,
+ setRedirectResponse,
+ ] = useState(null);
+
+ useEffect(() => {
+ instance.handleRedirectPromise().then(response => {
+ if (response) {
+ setRedirectResponse(response);
+ }
+ });
+ // TODO: error handling
+ }, [instance]);
+
+ return redirectResponse;
+}
diff --git a/experimental/msal-react/src/useIsAuthenticated.ts b/experimental/msal-react/src/useIsAuthenticated.ts
new file mode 100644
index 0000000000..3267510d27
--- /dev/null
+++ b/experimental/msal-react/src/useIsAuthenticated.ts
@@ -0,0 +1,21 @@
+import { useState, useEffect } from 'react';
+
+import { useMsal } from './MsalProvider';
+import { isAuthenticated } from './utilities';
+
+export function useIsAuthenticated(username?: string): boolean {
+ const {
+ state: { accounts },
+ instance,
+ } = useMsal();
+ const [hasAuthenticated, setHasAuthenticated] = useState(
+ isAuthenticated(instance, username)
+ );
+
+ useEffect(() => {
+ const result = isAuthenticated(instance, username);
+ setHasAuthenticated(result);
+ }, [accounts, username, instance]);
+
+ return hasAuthenticated;
+}
diff --git a/experimental/msal-react/src/utilities.ts b/experimental/msal-react/src/utilities.ts
new file mode 100644
index 0000000000..75da5c1a15
--- /dev/null
+++ b/experimental/msal-react/src/utilities.ts
@@ -0,0 +1,37 @@
+import { IMsalContext } from './MsalContext';
+import {
+ IPublicClientApplication,
+ AuthenticationResult,
+} from '@azure/msal-browser';
+
+type FaaCFunction = (args: T) => React.ReactNode;
+
+export function getChildrenOrFunction(
+ children: React.ReactNode | FaaCFunction,
+ args: T
+): React.ReactNode {
+ if (typeof children === 'function') {
+ return children(args);
+ }
+ return children;
+}
+
+export function isAuthenticated(
+ instance: IPublicClientApplication,
+ username?: string
+): boolean {
+ // TODO: Remove the `|| []` hack when the @azure/msal-browser is updated
+ return username
+ ? !!instance.getAccountByUsername(username)
+ : (instance.getAllAccounts() || []).length > 0;
+}
+
+export function defaultLoginHandler(
+ context: IMsalContext
+): Promise {
+ const { instance } = context;
+ return instance.loginPopup({
+ scopes: ['user.read'],
+ prompt: 'select_account',
+ });
+}
diff --git a/experimental/msal-react/src/withMsal.tsx b/experimental/msal-react/src/withMsal.tsx
new file mode 100644
index 0000000000..dd31bb194a
--- /dev/null
+++ b/experimental/msal-react/src/withMsal.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+
+import { IMsalContext } from './MsalContext';
+import { useMsal } from './MsalProvider';
+
+// Utility types
+// Reference: https://github.com/piotrwitek/utility-types
+type SetDifference = A extends B ? never : A;
+type SetComplement = SetDifference;
+type Subtract = Pick<
+ T,
+ SetComplement
+>;
+
+export interface IWithMsalProps {
+ msalContext: IMsalContext;
+}
+
+export const withMsal =
hook initiates the authentication process. It accepts optional parameters for a specific "username", or a custom "loginHandler" function which initiates a custom authentication flow using the MSAL API.
+
+
Authenticating...
+
+
+ {(context: IMsalContext) => (
+
Welcome {context.state.accounts[0].username}! You have been authenticated.
+
+
+
+);
+
+
+const PopupExample = () => {
+ const { instance, state } = useMsal();
+
+ const accounts = state.accounts;
-// By passing optional props to this story, you can control the props of the component when
-// you consume the story in a test.
-export const LoginPopup = (props?: Partial) => {
return (
-
-
- {msalContext => (
-