Part 4. Authentication with Session Cookie — Building Outlook Add-in with React

May Chen
NEXL Engineering
Published in
7 min readJul 25, 2021

More posts on buiding Outlook add-in with React:

NEXL 360 login screen

For our add-in, we use session cookie for authentication as that’s what we use for our web app and we didn’t want to spend too much backend resource on this project to build another authentication like JWT.

Looking back, it would have been better if we have JWT authentication for

  • we can store the token in the local storage (RoamingSettings) so the users won’t have to log in again every time they restart their Outlook.
  • easier local development as we have backend server on port 4000 and add-in frontend server on port 3000 and there are some security settings on the session cookie that doesn’t work on another port even though we have set up proxy, and we haven’t been able to solve this issue yet.

So, I would suggest you to use JWT authentication if you can, and I have a post on JWT with react-native app but it should be pretty similar.

This post is more about the authentication flow instead of what authentication method to use. Now let’s get into it.

The flow

authentication flow

Our add-in is a stand-alone app and talks to the backend server via GraphQL. When a user loads the add-in, we send a GraphQL query (currentUser) to the server and if it returns the data we know the user is authenticated and we let them into the app dashboard. If the query returns an error we redirect the user to the login page.

As we want to reuse the web app login, we have a button on the login page that opens up the web app login page in a dialog (Outlook’s in-built browser, it won’t work with user’s default browser as we need to set the session cookie within Outlook). And user logs into our service in the dialog, and from the dialog we need to somehow let the add-in know so we can refresh the page and now the GraphQL query will succeed as the session cookie is set and user can start using the add-in.

login ui in action

Log in in details

In CurrentUserContext.tsx, we make the currentUser call, and if it returns error, we redirect the user to the login page.

export const CurrentUserProvider: React.FC = ({ children }) => {
const { data, error, loading } = useGetCurrentUserQuery({
errorPolicy: "all",
});
if (error && location.pathname !== LOGIN_PATH) {
navigate(LOGIN_PATH);
return <></>;
}

return (
<CurrentUserContext.Provider
value={{
currentUser: data?.currentUser,
}}
>
{loading ? (
<div>
<QueryState loading loadingCopy="Logging in..." />
</div>
) : (
children
)}
</CurrentUserContext.Provider>
);
};

(full file here)

In App.tsx, we wrap <CurrentUserProvider /> outside all the routes so it’s called before we render any components.

<ApolloProvider client={client}>
<CurrentUserProvider>
<Container>
<Router>
<Login path={LOGIN_PATH} />
<Home path={HOME_PATH} />
<Tasks path={TASKS_PATH} />
<Opportunities path={OPPORTUNITIES_PATH} />
<Close path={CLOSE_PATH} />
</Router>
</Container>
</CurrentUserProvider>
</ApolloProvider>

(full file here)

When a user is redirected to the login page, we can’t let user log in within the add-in as that will require an API call which we don’t have on the backend, we want to reuse the web app login using session cookie which is already there.

add-in login page

So the trick is, we have a button on the add-in login page. When you click on it, it opens up the web app in a dialog. And after the user logs in and the dialog is closed, we want to refresh the add-in.

In Login.tsx:

export const Login: React.FC<RouteComponentProps> = ({}) => {
const showLoginDialog = useCallback(() => {
const display = () =>
new Promise<string>((resolve) => {
displayDialogAsync(
`${window.location.origin}/sessions/new?outlook_plugin=true`,
{
displayInIframe: false,
},
resolve
);
});
const displayAsync = async () => {
await display();
location.replace(HOME_PATH); //very important,

};
return displayAsync();
}, []);
return (
<div className={classes.root}>
...
<Button
variant="contained"
color="primary"
size="small"
onClick={showLoginDialog}
className={classes.login}
>
Log in
</Button>
...
</div>
);

in displayDialogAsync.ts:

export const displayDialogAsync = (
startAddress: string,
options: Office.DialogOptions,
onSuccess?: (message: string) => void,
onError?: (error: any) => void,
onEnd?: () => void
) =>
Office.context.ui.displayDialogAsync(
startAddress,
options,
(showDialogResult) => {
/**
* The following are errors resulting from the attempt to show the dialog.
* A successful result will be emitted if the dialog was successfully displayed
* and is now visible to the user.
*/
if (showDialogResult.status == Office.AsyncResultStatus.Failed) {
// In addition to general system errors, there are 3 specific errors for
// displayDialogAsync that you can handle individually.
switch (showDialogResult.error.code) {
case 12004:
console.log("Domain is not trusted");
break;
case 12005:
console.log("HTTPS is required");
break;
case 12007:
console.log("A dialog is already opened.");
break;
default:
console.log(showDialogResult.error.message);
onError && onError(showDialogResult.error);
break;
}
} else {
const dialog = showDialogResult.value;
const messageHandler = ({ message }: any) => {
onSuccess && onSuccess(message);
dialog.close(); // notify the parent dialog is closed
if (onEnd) onEnd();
};
const eventHandler = (arg: any) => {
// In addition to general system errors, there are 2 specific errors
// and one event that you can handle individually.
switch (arg.error) {
case 12002:
console.error("Cannot load URL, no such page or bad URL syntax.");
break;
case 12003:
console.error("HTTPS is required.");
break;
case 12006:
// The dialog was closed, typically because the user the pressed X button.
console.log("Dialog closed by user");
break;
default:
console.error("Undefined error in dialog window");
break;
}
if (onEnd) onEnd();
};
/*Messages are sent by developers programatically from the dialog using office.context.ui.messageParent(...)*/
dialog.addEventHandler(
Office.EventType.DialogMessageReceived,
messageHandler
);
/*Events are sent by the platform in response to user actions or errors. For example, the dialog is closed via the 'x' button*/
dialog.addEventHandler(
Office.EventType.DialogEventReceived,
eventHandler
);

}
}
);

And the web app login handles the authentication and sets the session cookie.

web app login

Once the user is logged in, we display a logged in page, which has a button which will send an event to Office API, and our displayDialogAsync function will resolve and the login page will then run location.replace and refresh the add-in and call the currentUser query again and this time, it will return data as the session cookie is set.

logged in page

In LoggedIn.tsx:

export const LoggedIn: React.FC<RouteComponentProps> = ({ }) => {
const classes = useStyles({});
return (
<div className={classes.root}>
...
<Button
variant="contained"
color="primary"
size="small"
onClick={() => Office.context.ui.messageParent('true')}
className={classes.login}
>
Start Exploring
</Button>
</div>
);
};

That’s our login flow using session cookie.

Set up proxy for local development

This flow brings an issue for our local development as add-in are a different server from the backend server(where the web app login is), we have to set up proxy so the add-in can have the session cookie from backend server. But this isn’t an issue on production server as the add-in will be compiled and hosted on the same server as the web app.

This might not be an issue for you depending how you app is set up, and you can follow this create-react-app documentation on how to set up proxy.

It looks roughly like this:

//setupProxy.jsconst { createProxyMiddleware } = require("http-proxy-middleware");module.exports = function (app) {
app.use(
"/sessions/new",
createProxyMiddleware({
target: "http://localhost:4000",
changeOrigin: true,
})
);

app.use(
"/auth/developer",
createProxyMiddleware({
target: "http://localhost:4000",
changeOrigin: true,
})
);
}

And if your add-in doesn’t get the cookie, it might be some security setting on your session cookie. Try go to application >cookies and find your session cookie and untick the Secure option and change SameSite option from None to Lax manually.

Hope this helps =)

--

--

May Chen
NEXL Engineering

A developer who occasionally has existential crisis and thinks if we are heading to the wrong direction, technology is just getting us there sooner.