What do I do with the access token in an Office Outlook Add-in using external authentication?

225 views Asked by At

I've been tasked to make this happen: Outlook Add-in with OIDC authentication to a non-microsoft API. The task pane will be written in Angular, and I'm already using angular-auth-oidc-client as the client-side OIDC auth package for other projects, so I'm thinking I'll use it here, too. I've gotten to the point of being able to load the Outlook add-in and it uses Office.UI.displayDialogAsync to open a dialog window for user to sign in, and it uses Office.UI.messageParent to send the access token to the task pane.

But then what? Once I have the access token on the task pane, what do I do with it? I have to manually store and keep track of expiration, and manually trigger a refresh? There's no way to load the access token into the client auth package on the task pane.

I'm runing into an issue of documentation, as I can't find any samples of a scenario like mine. All the documentation says is:

Your code in the dialog box window sends the access token to the host window either by using messageParent to send the stringified access token or by storing the access token where the host window can retrieve it (and using messageParent to tell the host window that the token is available). The token has a time limit, but while it lasts, the host window can use it to directly access the user's resources without any further prompting.

Here it says don't store access tokens, but I guess that's just in the context of using SSO? Which is another point of confusion. Is Microsoft calling an Office-specific authentication scheme "SSO", like, the generic term for external authentication? This makes all the information out there bleed together so it's hard to discern SSO™ from SSO.

The main questions are:

  1. What do I do with the access token once it's on the task pane?
  2. How do I deal with token expiration in this regime?
  3. Are there any samples that show authentication to something other than a microsoft IDP?
2

There are 2 answers

0
ZrSiO4 On BEST ANSWER

Alright, folks. After a couple days non-stop of working on this, I've figured out how to make it all work nicely.

So. Using angular-auth-oidc-client, here's the code to make it all work.

app.module.ts:

AuthModule.forRoot({
    config: {
        authority: authorityUrl,
        redirectUrl: callbackUrl,
        postLogoutRedirectUri: postLogoutRedirectUrl,
        clientId: client_id,
        scope: "openid offline_access otherscopes",
        responseType: "code",
        useRefreshToken: true,
        renewTimeBeforeTokenExpiresInSeconds: renewTimeBeforeTokenExpiresS,
        logLevel: LogLevel.Debug,// convertLogLevel(logLevel),
        maxIdTokenIatOffsetAllowedInSeconds: allowedIatOffsetSeconds,
    }
}),

Take note of callbackUrl supplied to redirectUrl.

app.component.ts:

constructor(private router: Router, private oidcSecurityService: OidcSecurityService) {
    this.oidcSecurityService.checkAuth().subscribe(checkRes => {
        if (checkRes.isAuthenticated) {
            this.redirectToMain();
        }
        else {
            this.oidcSecurityService.getAuthorizeUrl().subscribe(url => {
                Office.context.ui.displayDialogAsync(url, res => {
                    const dialog = res.value;
                    dialog.addEventHandler(Office.EventType.DialogMessageReceived, e => {
                        console.debug(`Message received from authentication dialog: ${JSON.stringify(e)}`);
                        if ("message" in e) {
                            dialog.close();

                            const oidcRespUrl = e.message;
                            this.oidcSecurityService.checkAuth(oidcRespUrl).subscribe(checkRes2 => {
                                if (checkRes2.isAuthenticated) {
                                    this.redirectToMain();
                                }
                                else {
                                    //  do what you gotta do
                                }
                            });
                        }
                        else if ("error" in e) {
                            console.error(e.error);
                        }
                    });
                });
            })

        }
    });
}

Now, once the user logs in in the Office dialog window, it will redirect to callbackUrl with query params including code=blahblah. All you need to do is to send that URL back to the parent window like this:

html at callbackUrl:

<html>
<head>
    <script src="https://appsforoffice.microsoft.com/lib/1.1/hosted/office.js" type="text/javascript"></script>
    <script>
        Office.onReady(function (info) {
            const urlWithOidcResp = window.location.href;
            Office.context.ui.messageParent(window.location.href);
        });
    </script>
</head>
<body>
    Transmitting authentication result ... (this popup will be closed automatically).
</body>
</html>

And that's it. It seems to be a pretty decent solution without resorting to any questionable hacks or anything.

Time for whiskey.

Edit

Turns out there's a missing piece. Cookies are being blocked, at least on Chrome, so if the user wants to log out, IdentityServer4 won't have enough info to properly log out.

This describes how to get third party cookies to work in an add-in. What wasn't clear from that doc is that for document.requestStorageAccess to not be automatically rejected, it must be called within the context of a user interaction, like a click or a tap. See here

So here's the new process. HTML template:

<button (click)="loginClick()">Log in</button>

And code:

loginClick(): void {
    document.requestStorageAccess().then(() => {
        //  Ok, now do the Office.context.ui.displayDialogAsync stuff
    });
}

Now, user log off can play nice with IdentityServer.

0
Rick Kirkham On

The non-SSO documentation for Office Add-ins is at Authorize to Microsoft Graph without SSO, and the samples listed at the end of that article. The endpoint is Microsoft, but notionally it is parallel to what you are trying to do. You can implement token storage (preferably on the server-side for security) and check for expiration each time you use the token. Alternatively, you can catch whatever error your endpoint is going to return from an expired token and get a new token in your error handler.