Skip to content

msal-react #1: account changes #2356

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Oct 19, 2020
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions lib/msal-browser/src/app/IPublicClientApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { RedirectRequest } from "../request/RedirectRequest";
import { PopupRequest } from "../request/PopupRequest";
import { SilentRequest } from "../request/SilentRequest";
import { SsoSilentRequest } from "../request/SsoSilentRequest";
import { BrowserConfigurationAuthError } from "../error/BrowserConfigurationAuthError";

/*
* Copyright (c) Microsoft Corporation. All rights reserved.
Expand All @@ -21,3 +22,39 @@ export interface IPublicClientApplication {
logout(logoutRequest?: EndSessionRequest): Promise<void>;
ssoSilent(request: SsoSilentRequest): Promise<AuthenticationResult>;
}

export const stubbedPublicClientApplication: IPublicClientApplication = {
acquireTokenPopup: () => {
return Promise.reject(BrowserConfigurationAuthError.createStubPcaInstanceCalledError);
},
acquireTokenRedirect: () => {
return Promise.reject(BrowserConfigurationAuthError.createStubPcaInstanceCalledError);
},
acquireTokenSilent: () => {
return Promise.reject(BrowserConfigurationAuthError.createStubPcaInstanceCalledError);
},
getAllAccounts: () => {
return [];
},
getAccountByHomeId: () => {
return null;
},
getAccountByUsername: () => {
return null;
},
handleRedirectPromise: () => {
return Promise.reject(BrowserConfigurationAuthError.createStubPcaInstanceCalledError);
},
loginPopup: () => {
return Promise.reject(BrowserConfigurationAuthError.createStubPcaInstanceCalledError);
},
loginRedirect: () => {
return Promise.reject(BrowserConfigurationAuthError.createStubPcaInstanceCalledError);
},
logout: () => {
return Promise.reject(BrowserConfigurationAuthError.createStubPcaInstanceCalledError);
},
ssoSilent: () => {
return Promise.reject(BrowserConfigurationAuthError.createStubPcaInstanceCalledError);
}
};
12 changes: 12 additions & 0 deletions lib/msal-browser/src/error/BrowserConfigurationAuthError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export const BrowserConfigurationAuthErrorMessage = {
desc: "The object passed for the callback was invalid. " +
"More information is available here: https://github.com/AzureAD/microsoft-authentication-library-for-js/wiki/MSAL-basics."
},
stubPcaInstanceCalled: {
code: "stubbed_public_client_application_called",
desc: "Stub instance of Public Client Application was called. If using msal-react, please ensure context is not used without a provider."
}
};

/**
Expand Down Expand Up @@ -84,4 +88,12 @@ export class BrowserConfigurationAuthError extends AuthError {
return new BrowserConfigurationAuthError(BrowserConfigurationAuthErrorMessage.noRedirectCallbacksSet.code,
BrowserConfigurationAuthErrorMessage.noRedirectCallbacksSet.desc);
}

/**
* Creates error thrown when the stub instance of PublicClientApplication is called.
*/
static createStubPcaInstanceCalledError(): BrowserConfigurationAuthError {
return new BrowserConfigurationAuthError(BrowserConfigurationAuthErrorMessage.stubPcaInstanceCalled.code,
BrowserConfigurationAuthErrorMessage.stubPcaInstanceCalled.desc);
}
}
2 changes: 1 addition & 1 deletion lib/msal-browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export { BrowserAuthError, BrowserAuthErrorMessage } from "./error/BrowserAuthEr
export { BrowserConfigurationAuthError, BrowserConfigurationAuthErrorMessage } from "./error/BrowserConfigurationAuthError";

// Interfaces
export { IPublicClientApplication } from "./app/IPublicClientApplication";
export { IPublicClientApplication, stubbedPublicClientApplication } from "./app/IPublicClientApplication";
export { PopupRequest } from "./request/PopupRequest";
export { RedirectRequest } from "./request/RedirectRequest";
export { SilentRequest } from "./request/SilentRequest";
Expand Down
31 changes: 22 additions & 9 deletions lib/msal-react/package.json
Original file line number Diff line number Diff line change
@@ -1,33 +1,46 @@
{
"name": "@azure/msal-react",
"version": "0.0.1",
"author": {
"name": "Microsoft",
"email": "[email protected]",
"url": "https://www.microsoft.com"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/AzureAD/microsoft-authentication-library-for-js.git"
},
"description": "Microsoft Authentication Library for React",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"files": [
"dist",
"src"
"dist"
],
"engines": {
"node": ">=10"
},
"scripts": {
"start": "tsdx watch",
"build": "tsdx build",
"test": "tsdx test --passWithNoTests",
"lint": "tsdx lint",
"prepare": "tsdx build",
"build:modules:watch": "tsdx watch --verbose",
"test": "tsdx test .*.spec.*",
"lint": "cd ../../ && npm run lint:react",
"lint:fix": "npm run lint -- -- --fix",
"build:all": "npm run build:common && npm run build:browser && npm run build",
"build:browser": "cd ../msal-browser && npm run build",
"build:common": "cd ../msal-common && npm run build",
"prepack": "npm run build:all",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
},
"peerDependencies": {
"@azure/msal-browser": "^2.0.0",
"@azure/msal-browser": "^2.2.0",
"react": ">=16"
},
"name": "@azure/msal-react",
"author": "Jason Nutter",
"module": "dist/msal-react.esm.js",
"devDependencies": {
"@azure/msal-browser": "^2.0.0",
"@azure/msal-browser": "^2.2.0",
"@babel/core": "^7.10.5",
"@storybook/addon-actions": "^5.3.19",
"@storybook/addon-docs": "^5.3.19",
Expand Down
25 changes: 16 additions & 9 deletions lib/msal-react/src/MsalAuthentication.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ 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";
import { AccountIdentifiers, useIsAuthenticated } from "./useIsAuthenticated";

export interface IMsalAuthenticationProps {
username?: string;
homeAccountId?: string;
loginHandler?: (context: IMsalContext) => Promise<AuthenticationResult>;
}

Expand All @@ -17,12 +18,14 @@ type MsalAuthenticationResult = {
};

// TODO: Add optional argument for the `request` object?
export function useMsalAuthentication(
args: IMsalAuthenticationProps = {}
): MsalAuthenticationResult {
const { username, loginHandler = defaultLoginHandler } = args;
export function useMsalAuthentication(args: IMsalAuthenticationProps = {}): MsalAuthenticationResult {
const { username, homeAccountId, loginHandler = defaultLoginHandler } = args;
const msal = useMsal();
const isAuthenticated = useIsAuthenticated(username);
const accountIdentifier: AccountIdentifiers = {
username,
homeAccountId
};
const isAuthenticated = useIsAuthenticated(accountIdentifier);

const [error, setError] = useState<Error | null>(null);

Expand Down Expand Up @@ -63,9 +66,13 @@ export function useMsalAuthentication(
}

export const MsalAuthentication: React.FunctionComponent<IMsalAuthenticationProps> = props => {
const { username, loginHandler, children } = props;
const { msal } = useMsalAuthentication({ username, loginHandler });
const isAuthenticated = useIsAuthenticated(username);
const { username, homeAccountId, loginHandler, children } = props;
const { msal } = useMsalAuthentication({ username, homeAccountId, loginHandler });
const accountIdentifier: AccountIdentifiers = {
username,
homeAccountId
};
const isAuthenticated = useIsAuthenticated(accountIdentifier);

// TODO: What if the user authentiction is InProgress? How will user show a loading state?
if (isAuthenticated) {
Expand Down
50 changes: 6 additions & 44 deletions lib/msal-react/src/MsalContext.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from "react";
import { IPublicClientApplication, AccountInfo } from "@azure/msal-browser";
import { IPublicClientApplication, stubbedPublicClientApplication, AccountInfo } from "@azure/msal-browser";

type MsalState = {
accounts: AccountInfo[];
Expand All @@ -10,50 +10,12 @@ export interface IMsalContext {
state: MsalState;
}

// Stubbed context implementation
/*
* Stubbed context implementation
* Only used when there is no provider, which is an unsupported scenario
*/
const defaultMsalContext: IMsalContext = {
instance: {
// Msal methods
acquireTokenPopup: () => {
return Promise.reject();
},
acquireTokenRedirect: () => {
return Promise.reject();
},
acquireTokenSilent: () => {
return Promise.reject();
},
getAllAccounts: () => {
debugger;
return [];
},
getAccountByUsername: () => {
/*
* TODO: getAccountByUsername should have a return type of `AccountInfo | null`
* Also should prevent possible null reference exception if no account matches the username
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/src/app/PublicClientApplication.ts#L466
*/
return (null as unknown) as AccountInfo;
},
getAccountByHomeId: () => {
return null;
},
handleRedirectPromise: () => {
return Promise.reject(null);
},
loginPopup: () => {
return Promise.reject();
},
loginRedirect: () => {
return Promise.reject();
},
logout: () => {
return Promise.reject();
},
ssoSilent: () => {
return Promise.reject();
},
},
instance: stubbedPublicClientApplication,
state: {
accounts: [],
},
Expand Down
11 changes: 3 additions & 8 deletions lib/msal-react/src/MsalProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,15 @@ export type MsalProviderProps = {
instance: IPublicClientApplication;
};

export const MsalProvider: React.FunctionComponent<MsalProviderProps> = ({
instance,
children,
}) => {
export const MsalProvider: React.FunctionComponent<MsalProviderProps> = ({instance, children}) => {
// State hook to store accounts
const [accounts, setAccounts] = React.useState<AccountInfo[]>(
// TODO: Remove the `|| []` hack when PR is finally merged to msal/browser
instance.getAllAccounts() || []
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() || []);
setAccounts(instance.getAllAccounts());
}, [instance]);

// Wrapped instance of MSAL that updates accounts after MSAL APIs are invoked
Expand Down
21 changes: 15 additions & 6 deletions lib/msal-react/src/Templates.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@ import React from "react";

import { useMsal } from "./MsalProvider";
import { getChildrenOrFunction } from "./utilities";
import { useIsAuthenticated } from "./useIsAuthenticated";
import { AccountIdentifiers, useIsAuthenticated } from "./useIsAuthenticated";

export interface IMsalTemplateProps {
username?: string;
username?: string,
homeAccountId?: string
}

export const UnauthenticatedTemplate: React.FunctionComponent<IMsalTemplateProps> = props => {
const { children, username } = props;
const { children, username, homeAccountId } = props;
const context = useMsal();
const isAuthenticated = useIsAuthenticated(username);
const accountIdentifier: AccountIdentifiers = {
username,
homeAccountId
};
const isAuthenticated = useIsAuthenticated(accountIdentifier);

if (!isAuthenticated) {
return (
Expand All @@ -24,9 +29,13 @@ export const UnauthenticatedTemplate: React.FunctionComponent<IMsalTemplateProps
};

export const AuthenticatedTemplate: React.FunctionComponent<IMsalTemplateProps> = props => {
const { children, username } = props;
const { children, username, homeAccountId } = props;
const context = useMsal();
const isAuthenticated = useIsAuthenticated(username);
const accountIdentifier: AccountIdentifiers = {
username,
homeAccountId
};
const isAuthenticated = useIsAuthenticated(accountIdentifier);

if (isAuthenticated) {
return (
Expand Down
1 change: 1 addition & 0 deletions lib/msal-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ export { withMsal } from "./withMsal";

export { useHandleRedirect } from "./useHandleRedirect";

export type { AccountIdentifiers } from "./useIsAuthenticated";
export { useIsAuthenticated } from "./useIsAuthenticated";
23 changes: 18 additions & 5 deletions lib/msal-react/src/useIsAuthenticated.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
import { AccountInfo, IPublicClientApplication } from "@azure/msal-browser";
import { useState, useEffect } from "react";

import { useMsal } from "./MsalProvider";
import { isAuthenticated } from "./utilities";

export function useIsAuthenticated(username?: string): boolean {
export type AccountIdentifiers = Partial<Pick<AccountInfo, "homeAccountId"|"username">>;

function isAuthenticated(instance: IPublicClientApplication, account?: AccountIdentifiers): boolean {
if (account?.homeAccountId) {
return !!instance.getAccountByHomeId(account.homeAccountId);
} else if (account?.username) {
return !!instance.getAccountByUsername(account.username);
}

return instance.getAllAccounts().length > 0;
}

export function useIsAuthenticated(account?: AccountIdentifiers): boolean {
const {
state: { accounts },
instance,
} = useMsal();

const [hasAuthenticated, setHasAuthenticated] = useState<boolean>(
isAuthenticated(instance, username)
isAuthenticated(instance, account)
);

useEffect(() => {
const result = isAuthenticated(instance, username);
const result = isAuthenticated(instance, account);
setHasAuthenticated(result);
}, [accounts, username, instance]);
}, [accounts, account, instance]);

return hasAuthenticated;
}
15 changes: 1 addition & 14 deletions lib/msal-react/src/utilities.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { IMsalContext } from "./MsalContext";
import {
IPublicClientApplication,
AuthenticationResult,
} from "@azure/msal-browser";
import { AuthenticationResult } from "@azure/msal-browser";

type FaaCFunction = <T>(args: T) => React.ReactNode;

Expand All @@ -16,16 +13,6 @@ export function getChildrenOrFunction<T>(
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<AuthenticationResult> {
Expand Down
Loading