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 => ( +
+

{msal?.getAccount() && `Welcome, ${msal?.getAccount().name}`}

+
+ )} +
+ )} +``` + + +#### 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 ( +
+ {!this.context.getAccount() ? ( + + ) : ( + + )} +
+ ) + } +} + +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 =

( + Component: React.ComponentType

+) => { + const ComponentWithMsal: React.FunctionComponent> = props => { + const msal = useMsal(); + return ; + }; + + const componentName = + Component.displayName || Component.name || 'Component'; + ComponentWithMsal.displayName = `withMsal(${componentName})`; + + return ComponentWithMsal; +}; diff --git a/experimental/msal-react/stories/hoc.stories.tsx b/experimental/msal-react/stories/hoc.stories.tsx new file mode 100644 index 0000000000..6acd0bdef6 --- /dev/null +++ b/experimental/msal-react/stories/hoc.stories.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { MsalProvider, IWithMsalProps, withMsal } from '../src'; + +import { msalInstance } from './msalInstance'; + +export default { + title: 'MSAL React/withMsal', +}; + +export const Example = () => { + return ( + + + + ) +}; + + +const WelcomeMessage: React.FunctionComponent = (props) => { + const accounts = props.msalContext.state.accounts; + + if (accounts.length > 0) { + return Welcome! The

withMsal()
higher-order component can see you are logged in with {accounts.length} accounts.; + } else { + return Welcome! The
withMsal()
higher-order component has detected you are logged out!
; + } +} + +const WithMsalExample = withMsal(WelcomeMessage); \ No newline at end of file diff --git a/experimental/msal-react/stories/hooks.stories.tsx b/experimental/msal-react/stories/hooks.stories.tsx new file mode 100644 index 0000000000..062b84725e --- /dev/null +++ b/experimental/msal-react/stories/hooks.stories.tsx @@ -0,0 +1,86 @@ +import * as React from 'react'; +import { MsalProvider, useMsal, useIsAuthenticated, UnauthenticatedTemplate, AuthenticatedTemplate, IMsalContext, useMsalAuthentication } from '../src'; + +import { msalInstance } from './msalInstance'; +import { useState } from 'react'; + +export default { + title: 'MSAL React/Hooks', +}; + + +export const UseMsalHook = () => ( + + + +); +UseMsalHook.story = { name: 'useMsal' }; + +export const UseIsAuthenticatedHook = () => ( + + + +); +UseIsAuthenticatedHook.story = { name: 'useIsAuthenticated' }; + +export const UseMsalAuthenticationHook = () => ( + + + +); +UseMsalAuthenticationHook.story = { name: 'useMsalAuthentication' }; + +const UseMsalExample = () => { + const context = useMsal(); + + return ( +

The

useMsal()
hook gives access to the MSAL React context. From here, any MSAL methods can be called, and state values accessed. For example, we know there are {context.state.accounts.length} accounts authenticated.

+ ); +} + +const UseIsAuthenticatedExample = () => { + const { state } = useMsal(); + const isAuthenticated = useIsAuthenticated(); + + const [username, setUsername] = useState(isAuthenticated ? state.accounts[0].username : 'user@example.com'); + const [currentUser, setCurrentUser] = useState(username); + + const isSpecificUserAuthenticated = useIsAuthenticated(currentUser); + + return ( + +

The

useIsAuthenticated()
hook will tell you if there is at least one authenticated account. The hook also accepts an optional "username" argument, and will indicate whether the given user is authenticated.

+ +

+ {isAuthenticated && ( + There is at least one account authenticated. + )} + {!isAuthenticated && ( + There are no accounts authenticated. + )} +

+ + setUsername(e.target.value)} value={username} placeholder="Username" /> + +

The user {currentUser} is {isSpecificUserAuthenticated ? 'authenticated' : 'unauthenticated'}

+
+ ); +} + +const UseMsalAuthenticationEample = () => { + useMsalAuthentication(); + + return ( + +

The

useMsalAuthentication()
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.

+ )} +
+
+ ); +} \ No newline at end of file diff --git a/experimental/msal-react/stories/login.stories.tsx b/experimental/msal-react/stories/login.stories.tsx index a8c8d2a9fb..6831a59738 100644 --- a/experimental/msal-react/stories/login.stories.tsx +++ b/experimental/msal-react/stories/login.stories.tsx @@ -1,56 +1,58 @@ import React from 'react'; -import { MsalProvider, MsalConsumer } from '../src'; -import { PublicClientApplication } from '@azure/msal-browser'; +import { MsalProvider, MsalConsumer, useMsal, useIsAuthenticated, AuthenticatedTemplate, UnauthenticatedTemplate } from '../src'; + +import { msalInstance } from './msalInstance'; export default { - title: 'MSAL React/Login & Logout', + title: 'MSAL React/Login & Logout', }; -const msalInstance = new PublicClientApplication({ - auth: { - clientId: "0a61c279-646b-4055-a5f1-1c3da7f70f18", - redirectUri: "http://localhost:6006/" - } -}); +export const LoginPopup = () => ( + + + +); + +export const Logout = () => ( + + +

You must be logged in to be able to logout.

+
+ +
+); + + +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 => ( -
- {msalContext.state.accounts.length ? ( -
-

- Account: {msalContext.state.accounts[0].username} -

- -
- ) : ( -
- -
- )} - -
- )} -
-
- ) + + +

Accounts: {accounts.map(a => a.username).join(', ')}

+ +
+ +
+ ); }; + + +const LogoutExample = () => { + const { instance, state } = useMsal(); + + const accounts = state.accounts; + + return ( + + {accounts.map((account) => ( +
+ {account.username} + +
+ ))} +
+ ); +}; \ No newline at end of file diff --git a/experimental/msal-react/stories/msalInstance.ts b/experimental/msal-react/stories/msalInstance.ts new file mode 100644 index 0000000000..14e2d2e8cc --- /dev/null +++ b/experimental/msal-react/stories/msalInstance.ts @@ -0,0 +1,8 @@ +import { PublicClientApplication } from "@azure/msal-browser"; + +export const msalInstance = new PublicClientApplication({ + auth: { + clientId: "0a61c279-646b-4055-a5f1-1c3da7f70f18", + redirectUri: "http://localhost:6006/" + } +}); \ No newline at end of file diff --git a/experimental/msal-react/stories/protected.stories.tsx b/experimental/msal-react/stories/protected.stories.tsx new file mode 100644 index 0000000000..561f39de3a --- /dev/null +++ b/experimental/msal-react/stories/protected.stories.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { MsalProvider, MsalAuthentication } from '../src'; + +import { msalInstance } from './msalInstance'; + +export default { + title: 'MSAL React/MsalAuthentication', +}; + +export const Example = () => { + return ( + +

This page has a component that will only render if you are authenticated.

+ + + +
+ ) +}; + +const ProtectedComponent: React.FunctionComponent = () => { + return You are authenticated, which means you can see this content. +} diff --git a/experimental/msal-react/stories/templates.stories.tsx b/experimental/msal-react/stories/templates.stories.tsx new file mode 100644 index 0000000000..0442910ba9 --- /dev/null +++ b/experimental/msal-react/stories/templates.stories.tsx @@ -0,0 +1,61 @@ +import * as React from 'react'; +import { MsalProvider, AuthenticatedTemplate, IMsalContext, UnauthenticatedTemplate } from '../src'; + +import { msalInstance } from './msalInstance'; +import { useState } from 'react'; + +export default { + title: 'MSAL React/Templates', +}; + +export const Authenticated = () => ( + +

This page has content that will only render if you are authenticated.

+ + {(context: IMsalContext) => ( + + Welcome, {context.state.accounts[0].username}! + + )} + + + You are authenticated! + +
+); +Authenticated.story = { name: 'AuthenticatedTemplate' }; + +export const Uauthenticated = () => ( + +

This page has content that will only render if you are unauthenticated.

+ + You are not authenticated. + + + {(context: IMsalContext) => ( + There are currently {context.state.accounts.length} active accounts. + )} + +
+); +Uauthenticated.story = { name: 'UauthenticatedTemplate' }; + +export const SpecificUser = () => { + const [username, setUsername] = useState('user@example.com'); + const [currentUser, setCurrentUser] = useState(username); + + return ( + + setUsername(e.target.value)} value={username} /> + +

Authentication status templates can be scoped to a specific username.

+ + +

The user {currentUser} is unauthenticated.

+
+ +

The user {currentUser} is authenticated.

+
+
+ ); +}; \ No newline at end of file diff --git a/experimental/msal-react/stories/tokens.stories.tsx b/experimental/msal-react/stories/tokens.stories.tsx new file mode 100644 index 0000000000..c7adba096e --- /dev/null +++ b/experimental/msal-react/stories/tokens.stories.tsx @@ -0,0 +1,92 @@ +import React, { useState, useCallback } from 'react'; +import { MsalProvider, useMsal, AuthenticatedTemplate, UnauthenticatedTemplate } from '../src'; + +import { msalInstance } from './msalInstance'; +import { AccountInfo } from '@azure/msal-browser'; + +export default { + title: 'MSAL React/Acquire Tokens', +}; + +export const AcquireTokenSilent = () => ( + + +

You must be logged in to fetch a token.

+
+ +
+); + +export const AcquireTokenPopup = () => ( + + +

You must be logged in to fetch a token.

+
+ +
+); + +const AcquireTokenSilentExample = () => { + const { instance, state } = useMsal(); + + const getTokenClick = useCallback(async (setter: React.Dispatch>, account: AccountInfo) => { + const tokenResponse = await instance.acquireTokenSilent({ + account, + scopes: ['user.read'] + }); + + if (tokenResponse) { + setter(tokenResponse.accessToken); + } + }, [instance]); + + return ( + + {state.accounts.map((account) => ( + + ))} + + + ) +}; + +const AcquireTokenPopupExample = () => { + const { instance, state } = useMsal(); + + const getTokenClick = useCallback(async (setter: React.Dispatch>, account: AccountInfo) => { + const tokenResponse = await instance.acquireTokenPopup({ + scopes: ['user.read'], + loginHint: account.username + }); + + if (tokenResponse) { + setter(tokenResponse.accessToken); + } + }, [instance]); + + return ( + + {state.accounts.map((account) => ( + + ))} + + + ) +}; + +interface IAccountTokenFetcherProps { + onFetch: (setter: React.Dispatch>, account: AccountInfo) => void; + account: AccountInfo; +} + +const AccountTokenFetcher: React.FunctionComponent = ({ onFetch, account }) => { + const [ accessToken, setAccessToken ] = useState(undefined); + + return ( +
+ {account.username} + +
{JSON.stringify(accessToken, null, 4)}
+
+ ); +} \ No newline at end of file