Skip to content
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

msal js not suited for "real" single page apps? reload and history state issues #697

Closed
thomas-mindruptive opened this issue May 8, 2019 · 15 comments
Labels
bug

Comments

@thomas-mindruptive
Copy link

@thomas-mindruptive thomas-mindruptive commented May 8, 2019

I'm submitting a...


- [x] Bug report  

Browser:

  • Chrome 74.0.3729.131, 64 bit

Library version

msal js, version: 1.0.0

Attachments

Please see attached word document with screen shots, console log etc.
msalProbs.docx

Current behavior

  • "loginPopup()" and "aquireTokenSiltent()" cause our SPA to be reloaded in iframe.
    This is bad :-) Below, I've added a modified sample from the msal js github. IT is very small and simple and it doe not matter if it reloads. But in real apps with > 25 webpack bundles it does.
  • aquireTokenSiltent() causes reloads even after a successful login. But if you refresh the app and "aquireTokenSiltent()" once, all subsequent calls will be immediate and without reload. The token is taken from memory and/or local storage. Why redirects if the token is already there. The expiration-date can be checkt without roundtrips (or can it not?)
  • This behaviour breaks most SPAs or makes them unpredictable.
  • It's no solution to create an almost empty app-stub just for msal. The app should log in automatically/silently on startup, go to a main page which loads data etc.
  • From a UX perspective in general, the slow login popup workflow is not good.
  • With all due respect, the default ADB2C user flow policy windows (login, forgot password), are ugly. To customize the forms, one needs to create a blob storage account and upload files there (!?).
  • I have worked with several identity providers, but msal + ADB2C drove me almost crazy.

Expected behavior

  • No/fewer reloads

Hash url fragments

Our SPAs rely on hash urls to support deep linking without server roundtrips.
msal uses hash-fragments, too, e.g. for returning error "interaction_required" etc.
This breaks hash-based SPA-routing (unless you tell the router to explicitly ignore those hashes).

Other issues

In our "real" SPA, we had several, not easily reproducible problems, inspite of using the exact same mechanism:

  • History state contained too many states after login and "aquireTokenSilent()"
  • The app was loaded in the login popup window
  • We sometimes saw following error:

Uncaugt ClientAuthError: Error occured in token received callback function. TypeError: this.authResponseCallback is not a function.

It happens here:


UserAgentApplication.prototype.handleAuthenticationResponse = function (hash) {
  if (this.parentIsMsal()) {
    tokenResponseCallback = 
    window.parent.callbackMappedToRenewStates[stateInfo.state];
    // Debugger: tokenResponseCallback == null 
  }

UserAgentApplication.prototype.processCallBack = function (hash, stateInfo, parentCallback) {
    // Debugger: 
    //   stateInfo.state = „RENEW_TOKEN“
    //    stateInfo.state: "15c80c88-b9b5-45d6-bf8f-51a0130f019d"
        this.logger.info("Processing the callback from redirect response");
        // get the state info from the hash
        if (!stateInfo) {
            stateInfo = this.getResponseState(hash);
        }
        var response;
        var authErr;
        // Save the token info from the hash
        try {
            response = this.saveTokenFromHash(hash, stateInfo);
        }
        catch (err) {
            authErr = err;
        }

Minimal reproduction of the problem with instructions

  • Register app ind ADB2C portal
    • Add redirect URL (in our test case http://localhost:8082)
    • Use app id as "Msal.UserAgentApplication.auth.clientId"
  • Use the sample code below to reproduce problem
  • Watch console output and Chrome debugger to identify reloads in iframe JS-context

Sample-code




    <title>Calling a Web API as a user authenticated with Msal.js app</title>
    <style>
        body {
            font-family: 'Arial Narrow', Arial, sans-serif
        }

        button {
            background-color: blue;
            color: whitesmoke;
            border-style: solid;
            border-width: 1px;
            padding: 10px;
            border-radius: 3px;
        }

        button:hover {
            background-color: #8181ee;
        }

        hr {
            margin-top: 20px;
        }

        .hidden {
            display: none;
            visibility: hidden
        }

        .visible {
            visibility: visible
        }

        .response {
            border-style: none;
            border-width: thin;
            background-color: rgb(255, 228, 198);
            padding: 0px;
            border-radius: 2px;
        }

        .blockTitle {
            font-size: 130%;
            font-weight: bold;
            margin-right: 25px;
        }
    </style>


    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/bluebird/3.3.4/bluebird.min.js" class="pre"></script>
    <script src="https://secure.aadcdn.microsoftonline-p.com/lib/1.0.0/js/msal.js"></script>
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<h2>Test Authentication with Azure AD B2C</h2>
<div>
    <div id="label">Not logged in</div>
    <br />

    <span class="blockTitle">Azure AD B2C Authentication</span>
    <button id="auth" onclick="login()">Login with azure ADB2C</button>
    <button id="callApiButton" class="hidden" onclick="getTokenSilently()">Test get token silently</button>
    <hr>

    <!-- Credentials - User/PW -  Not used at the moment 
        <span class="blockTitle">Authentication with username/pw</span>
        <button id="loginWithCredentialsButton" onclick="loginWithCredentials()">
            Login with username/pw (user@test.com/password)
        </button>
        <button id="callApiWithSessionButton" class="hidden" onclick="callApiWithLoggedInSession()">
            Call Web API with Session User
        </button>
        <hr>
    -->

    <span class="blockTitle">Logout</span>
    <button id="logoutButton" class="hidden" onclick="logout()">Logout</button>
    <hr>

</div>

<pre class="response"></pre>

<script>
    console.info(`Start script: history.length: %s`, history.length);

    const b2bScopesAuthTest = ["https://tgwdsb2c.onmicrosoft.com/xxx/demo.read"];
    const userFlowPolicy = "B2C_1_Test";
    const clientIDTGWAuthTest = "the appid from adb2c portal";

    // azure B2C config.
    let applicationConfig = {
        clientID: clientIDTGWAuthTest,
        authority: `https://tgwdsb2c.b2clogin.com/tgwdsb2c.onmicrosoft.com/${userFlowPolicy}`,
        // OLD!!! authority: "https://login.microsoftonline.com/tfp/tgwdsb2c.onmicrosoft.com/B2C_1_Test",
        b2cScopes: b2bScopesAuthTest,
        webApi: 'http://localhost:5000/api/test',
        loginApi: 'http://localhost:5000/login',
        logoutApi: 'http://localhost:5000/api/logout',
    };

    let curAuthStrategy;
    let authenticated = false;
    let clientApplication;
    let AuthStrategy = {};
    AuthStrategy[AuthStrategy.PassportToken = 1] = "PassportToken";
    AuthStrategy[AuthStrategy.Credentials = 2] = "Credentials";
    let mockUser = {
        email: "user@test.com",
        password: "password"
    };
    let loggedInUser = undefined;

    window.onload = doIt;

    /**
     * Start test.
     */
    async function doIt() {
        updateUI();
        console.info(`doIt: appConfig: %O, history.length: %s`, applicationConfig, history.length);
        
        clientApplication = new Msal.UserAgentApplication(
            {
                auth: {
                    clientId: applicationConfig.clientID,
                    authority: applicationConfig.authority,
                    validateAuthority: false // It breaks with some discovery error, if you don't set this
                },
                cache: {
                    cacheLocation: "localStorage",
                    storeAuthStateInCookie: true
                }
            }
        );

        /**
         * Try to get access token silently without interaction.
         * => Try to automatically login when app starts.
         */
        // let account = clientApplication.getAccount();
        // if (account) {
        //     console.info("******* Found account in client app.");
        //     try {
        //         let tokenRequest = { scopes: applicationConfig.b2cScopes };
        //         let accessToken = await clientApplication.acquireTokenSilent(tokenRequest);
        //         authenticated = true;
        //         curAuthStrategy = AuthStrategy.PassportToken;
        //         updateUI()
        //     } catch (e) {
        //         logMessage("Error acquiring the token silently:\n" + e);
        //     }
        // }
    }

    /* azure AD B2C.
     ----------------------------------------------------------------------------------------*/

    /**
     * Login to azure AD.
     */
    function login() {
        let loginRequest = { scopes: applicationConfig.b2cScopes };
        clientApplication.loginPopup(loginRequest)
            .then(function (authResponse) {
                console.log(`After loginPopup, authResponse == %O`, authResponse);
                console.info(`login: history.length: %s`, history.length);
                if (authResponse.accessToken) {
                    curAuthStrategy = AuthStrategy.PassportToken;
                    authenticated = true;
                    console.info(`login: history.length: %s`, history.length);
                    updateUI();
                } else {
                    let tokenRequest = { scopes: applicationConfig.b2cScopes };
                    clientApplication.acquireTokenSilent(tokenRequest)
                        .then(function (accessToken) {
                            console.info("After login & acquireTokenSilent, token == %O", accessToken);
                            console.info(`login: history.length: %s`, history.length);
                            curAuthStrategy = AuthStrategy.PassportToken;
                            authenticated = true;
                            updateUI();
                        }, function (error) {
                            clientApplication.acquireTokenPopup(tokenRequest)
                                .then(function (accessToken) {
                                    console.info("After login & acquireTokenPopup, token == %O", accessToken);
                                    console.info(`login: history.length: %s`, history.length);
                                    curAuthStrategy = AuthStrategy.PassportToken;
                                    authenticated = true;
                                    updateUI();
                                }, function (error) {
                                    logMessage("Error acquiring the token silently:\n" + error);
                                });
                        })
                }
            }, function (error) {
                logMessage("Error during loginPopup:\n" + error);
            });
    }

    /**
     * Update the UI according to the state of login etc.
     */
    function updateUI() {
        console.info("Update UI");
        if (authenticated) {
            let userName = clientApplication && clientApplication.getAccount() ? clientApplication.getAccount().name : loggedInUser ? loggedInUser.email : "no logged in user";
            logMessage("User '" + userName + "' logged-in");
            let authButton = document.getElementById('auth');
            authButton.setAttribute("class", "hidden");
            // Not used: let loginWithCredButton = document.getElementById('loginWithCredentialsButton');
            // Not used: loginWithCredButton.setAttribute("class", "hidden");
            let logoutButton = document.getElementById('logoutButton');
            logoutButton.setAttribute("class", "visible");
            let label = document.getElementById('label');
            label.innerText = "Hello " + userName;

            if (curAuthStrategy === AuthStrategy.PassportToken) {
                let callWebApiButton = document.getElementById('callApiButton');
                callWebApiButton.setAttribute('class', 'visible');
                // Not used: let callApiWithSessionButton = document.getElementById('callApiWithSessionButton');
                // Not used: callApiWithSessionButton.setAttribute('class', 'hidden');
            } else {
                let callWebApiButton = document.getElementById('callApiButton');
                callWebApiButton.setAttribute('class', 'hidden');
                // Not used: let callApiWithSessionButton = document.getElementById('callApiWithSessionButton');
                // Not used: callApiWithSessionButton.setAttribute('class', 'visible');
            }
        } else {
            let logoutButton = document.getElementById('logoutButton');
            logoutButton.setAttribute("class", "hidden");
            let authButton = document.getElementById('auth');
            authButton.setAttribute("class", "visible");
            // Not used: let loginWithCredButton = document.getElementById('loginWithCredentialsButton');
            // Not used: authButton.setAttribute("class", "visible");
        }
    }

    /**
     * Test getting a token silently. 
     */
    function getTokenSilently() {
        let tokenRequest = { scopes: applicationConfig.b2cScopes };
        clientApplication.acquireTokenSilent(tokenRequest).then(function (authResponse) {
            logMessage(`getTokenSilently: got token silently: ${authResponse.accessToken}`);
            console.info(`getTokenSilently: history.length: %s`, history.length);
        }, function (error) {
            clientApplication.acquireTokenPopup(tokenRequest).then(function (authResponse) {
                logMessage(`getTokenSilently: got token from popup:   ${authResponse.accessToken}`);
                console.info(`getTokenSilently: history.length: %s`, history.length);
            }, function (error) {
                logMessage("Error acquiring the access token to call the Web api:\n" + error);
            });
        })
    }

Remainder of code not relevant…

Any help is appreciated, thanks a lot!

@sameerag

This comment has been minimized.

Copy link
Member

@sameerag sameerag commented May 9, 2019

@thomas-mindruptive Thanks for the detailed description of the issues you are facing integrating msal JS into your SPA.

Since MSAL JS is a facilitator library that communicates with the service (endpoint in this case which also gives the tokens) through http calls, it always handles a 302 redirect as the method of retrieving the token from the service. Our app is designed to parse the hash returned from the service. This is the basic design principle today behind msal js functionality.

We behave in two different ways for popup/silent calls:

  • popup opens a new popup window -> we handle the redirect within the popup window -> separate the hash, process it and return to the main window -> which is your SPA
  • silent essentially does the same but in a hidden iframe.

We are brainstorming on how to make this work with as little impact on the developer as possible but today as it stands, the refresh can only be contained in a popup or iframe but cannot be completely avoided.

Now regarding your questions:

  • Is the iframe refresh causing performance issues for you as your actual "SPA" remains unrefreshed in a popup/silent case?

  • The first roundtrip to the service cannot be avoided for acquireToken as the loginPopup only retrieves idToken. Once an accessToken is granted from the service we cache it and until expiry do not make any roundtrips. No extra calls are actually made to the service unless the claims changed, user lost their session with the service or the token is expired. Hence I am confused regarding the "refresh" on success cases.

Can you please help me understand if I am missing anything here?

@thomas-mindruptive

This comment has been minimized.

Copy link
Author

@thomas-mindruptive thomas-mindruptive commented May 10, 2019

@sameerag Thanks for the quick response!
I investigated further:

  • One could catch the refresh by checking the hashes. => If certain hashes are present, don't load the app, only a minimal "empty page"
  • But: One must at least create the Msal.UserAgentApplication in the "empty page"
  • But: The problem with the roundtrips still exists. In my sample, each time I call "aquireTokenSilently", the "empty page" is loaded in the iframe (claims have not changed). Accoring to the docs and your explanation, this should not happen.
    • Strange enough, after I refresh the page, it works as expected: No roundtrips
    • => Problem with the local caching (local storage and/or cookie)?

Thanks a lot
Tom

@thomas-mindruptive

This comment has been minimized.

Copy link
Author

@thomas-mindruptive thomas-mindruptive commented May 10, 2019

@sameerag I've ammended my sample: If I add the code between the comments "login silently", the app works without roundtrips. So it reallys seems to be a local cache problem?

I would kindly suggest to add this information to the docs: rountrips, popup and iframe. What happens exactly, what are the consequences? And especially how to prevent a full re- instantiation of the app (i.e. check hash and if it's an auth-redirect, create empty page with client app) etc. It took me many hours of re-engineering, debugging and trial and error.
My "real" SPA still doesnt work. We will probably eliminate msal and azure ADB2C and use a different product.





    <title>Calling a Web API as a user authenticated with Msal.js app</title>
    <style>
        body {
            font-family: 'Arial Narrow', Arial, sans-serif
        }

        button {
            background-color: blue;
            color: whitesmoke;
            border-style: solid;
            border-width: 1px;
            padding: 10px;
            border-radius: 3px;
        }

        button:hover {
            background-color: #8181ee;
        }

        hr {
            margin-top: 20px;
        }

        .hidden {
            display: none;
            visibility: hidden
        }

        .visible {
            visibility: visible
        }

        .response {
            border-style: none;
            border-width: thin;
            background-color: rgb(255, 228, 198);
            padding: 0px;
            border-radius: 2px;
        }

        .blockTitle {
            font-size: 130%;
            font-weight: bold;
            margin-right: 25px;
        }
    </style>


    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/bluebird/3.3.4/bluebird.min.js" class="pre"></script>
    <script src="https://secure.aadcdn.microsoftonline-p.com/lib/1.0.0/js/msal.js"></script>
    <script src="https://code.jquery.com/jquery-3.2.1.min.js" class="pre"></script>
<h2>Test Authentication with Azure AD B2C</h2>
<div>
    <div id="label">Not logged in</div>
    <br />

    <span class="blockTitle">Azure AD B2C Authentication</span>
    <button id="auth" onclick="login()">Login with azure ADB2C</button>
    <button id="callApiButton" class="hidden" onclick="getTokenSilently()">Test get token silently</button>
    <hr>

    <!-- Credentials - User/PW -  Not used at the moment 
        <span class="blockTitle">Authentication with username/pw</span>
        <button id="loginWithCredentialsButton" onclick="loginWithCredentials()">
            Login with username/pw (user@test.com/password)
        </button>
        <button id="callApiWithSessionButton" class="hidden" onclick="callApiWithLoggedInSession()">
            Call Web API with Session User
        </button>
        <hr>
    -->

    <span class="blockTitle">Logout</span>
    <button id="logoutButton" class="hidden" onclick="logout()">Logout</button>
    <hr>

</div>

<pre class="response"></pre>

<script>
    console.info(`Start script: history.length: %s`, history.length);
    if (window.location.hash) {
        console.info(`Got hash for auth-redirect, leaving page: ${window.location.hash}`);
    }

    const b2bScopesAuthTest = ["https://tgwdsb2c.onmicrosoft.com/xxx/demo.read"];
    const userFlowPolicy = "B2C_1_Test";
    const clientIDTGWAuthTest = "***************************************";

    // azure B2C config.
    let applicationConfig = {
        clientID: clientIDTGWAuthTest,
        authority: `https://tgwdsb2c.b2clogin.com/tgwdsb2c.onmicrosoft.com/${userFlowPolicy}`,
        // OLD!!! authority: "https://login.microsoftonline.com/tfp/tgwdsb2c.onmicrosoft.com/B2C_1_Test",
        b2cScopes: b2bScopesAuthTest,
        webApi: 'http://localhost:5000/api/test',
        loginApi: 'http://localhost:5000/login',
        logoutApi: 'http://localhost:5000/api/logout',
    };

    let curAuthStrategy;
    let authenticated = false;
    let clientApplication;
    let AuthStrategy = {};
    AuthStrategy[AuthStrategy.PassportToken = 1] = "PassportToken";
    AuthStrategy[AuthStrategy.Credentials = 2] = "Credentials";
    let mockUser = {
        email: "user@test.com",
        password: "password"
    };
    let loggedInUser = undefined;

    clientApplication = new Msal.UserAgentApplication(
        {
            auth: {
                clientId: applicationConfig.clientID,
                authority: applicationConfig.authority,
                validateAuthority: false
            },
            cache: {
                cacheLocation: "localStorage",
                storeAuthStateInCookie: true
            }
        }
    );

    if (!window.location.hash) window.onload = doIt;


    /**
     * Start test.
     */
    async function doIt() {
        updateUI();

        /*
         * Login silently --------------------------------------------------------------------------
         */
        let tokenRequest = { scopes: applicationConfig.b2cScopes };
        clientApplication.acquireTokenSilent(tokenRequest).then(function (authResponse) {
            logMessage(`getTokenSilently: got token silently: ${authResponse.accessToken}`);
            console.info(`App:init, after getTokenSilently: history.length: %s`, history.length);
            if (authResponse.accessToken) {
                curAuthStrategy = AuthStrategy.PassportToken;
                authenticated = true;
                console.info(`App:init, got valid token silently without login`);
                updateUI();
            }
        });
        /*
         * Login silently --------------------------------------------------------------------------
         */

        console.info(`doIt: appConfig: %O, history.length: %s`, applicationConfig, history.length);

        /**
         * Try to get access token silently without interaction.
         * => Try to automatically login when app starts.
         */
        // let account = clientApplication.getAccount();
        // if (account) {
        //     console.info("******* Found account in client app.");
        //     try {
        //         let tokenRequest = { scopes: applicationConfig.b2cScopes };
        //         let accessToken = await clientApplication.acquireTokenSilent(tokenRequest);
        //         authenticated = true;
        //         curAuthStrategy = AuthStrategy.PassportToken;
        //         updateUI()
        //     } catch (e) {
        //         logMessage("Error acquiring the token silently:\n" + e);
        //     }
        // }
    }

    /* azure AD B2C.
     ----------------------------------------------------------------------------------------*/

    /**
     * Login to azure AD.
     */
    function login() {
        let loginRequest = { scopes: applicationConfig.b2cScopes };
        clientApplication.loginPopup(loginRequest)
            .then(function (authResponse) {
                console.log(`After loginPopup, authResponse == %O`, authResponse);
                console.info(`login: history.length: %s`, history.length);
                if (authResponse.accessToken) {
                    curAuthStrategy = AuthStrategy.PassportToken;
                    authenticated = true;
                    console.info(`login: history.length: %s`, history.length);
                    updateUI();
                } else {
                    let tokenRequest = { scopes: applicationConfig.b2cScopes };
                    clientApplication.acquireTokenSilent(tokenRequest)
                        .then(function (accessToken) {
                            console.info("After login & acquireTokenSilent, token == %O", accessToken);
                            console.info(`login: history.length: %s`, history.length);
                            curAuthStrategy = AuthStrategy.PassportToken;
                            authenticated = true;
                            updateUI();
                        }, function (error) {
                            clientApplication.acquireTokenPopup(tokenRequest)
                                .then(function (accessToken) {
                                    console.info("After login & acquireTokenPopup, token == %O", accessToken);
                                    console.info(`login: history.length: %s`, history.length);
                                    curAuthStrategy = AuthStrategy.PassportToken;
                                    authenticated = true;
                                    updateUI();
                                }, function (error) {
                                    logMessage("Error acquiring the token silently:\n" + error);
                                });
                        })
                }
            }, function (error) {
                logMessage("Error during loginPopup:\n" + error);
            });
    }

    /**
     * Update the UI according to the state of login etc.
     */
    function updateUI() {
        console.info("Update UI");
        if (authenticated) {
            let userName = clientApplication && clientApplication.getAccount() ? clientApplication.getAccount().name : loggedInUser ? loggedInUser.email : "no logged in user";
            logMessage("User '" + userName + "' logged-in");
            let authButton = document.getElementById('auth');
            authButton.setAttribute("class", "hidden");
            // Not used: let loginWithCredButton = document.getElementById('loginWithCredentialsButton');
            // Not used: loginWithCredButton.setAttribute("class", "hidden");
            let logoutButton = document.getElementById('logoutButton');
            logoutButton.setAttribute("class", "visible");
            let label = document.getElementById('label');
            label.innerText = "Hello " + userName;

            if (curAuthStrategy === AuthStrategy.PassportToken) {
                let callWebApiButton = document.getElementById('callApiButton');
                callWebApiButton.setAttribute('class', 'visible');
                // Not used: let callApiWithSessionButton = document.getElementById('callApiWithSessionButton');
                // Not used: callApiWithSessionButton.setAttribute('class', 'hidden');
            } else {
                let callWebApiButton = document.getElementById('callApiButton');
                callWebApiButton.setAttribute('class', 'hidden');
                // Not used: let callApiWithSessionButton = document.getElementById('callApiWithSessionButton');
                // Not used: callApiWithSessionButton.setAttribute('class', 'visible');
            }
        } else {
            let logoutButton = document.getElementById('logoutButton');
            logoutButton.setAttribute("class", "hidden");
            let authButton = document.getElementById('auth');
            authButton.setAttribute("class", "visible");
            // Not used: let loginWithCredButton = document.getElementById('loginWithCredentialsButton');
            // Not used: authButton.setAttribute("class", "visible");
        }
    }

    /**
     * Test getting a token silently. 
     */
    function getTokenSilently() {
        let tokenRequest = { scopes: applicationConfig.b2cScopes };
        clientApplication.acquireTokenSilent(tokenRequest).then(function (authResponse) {
            logMessage(`getTokenSilently: got token silently: ${authResponse.accessToken}`);
            console.info(`getTokenSilently: history.length: %s`, history.length);
        }, function (error) {
            clientApplication.acquireTokenPopup(tokenRequest).then(function (authResponse) {
                logMessage(`getTokenSilently: got token from popup:   ${authResponse.accessToken}`);
                console.info(`getTokenSilently: history.length: %s`, history.length);
            }, function (error) {
                logMessage("Error acquiring the access token to call the Web api:\n" + error);
            });
        })
    }

    /**
     * Call the API.
     */
    function callApi() {
        let tokenRequest = { scopes: applicationConfig.b2cScopes };
        clientApplication.acquireTokenSilent(tokenRequest).then(function (accessToken) {
            callApiWithAccessToken(accessToken);
        }, function (error) {
            clientApplication.acquireTokenPopup(tokenRequest).then(function (accessToken) {
                callApiWithAccessToken(accessToken);
            }, function (error) {
                logMessage("Error acquiring the access token to call the Web api:\n" + error);
            });
        })
    }

    /**
     * Call the API with the access token.
     */
    function callApiWithAccessToken(accessToken) {
        // Call the Web API with the AccessToken
        $.ajax({
            type: "GET",
            url: applicationConfig.webApi,
            headers: {
                'Authorization': 'Bearer ' + accessToken
            },
        }).done(function (data) {
            logMessage("Web API returned: " + JSON.stringify(data));
        })
            .fail(function (jqXHR, textStatus) {
                logMessage("Error calling the web api: " + textStatus);
            })
    }

    function logMessage(s) {
        document.body.querySelector('.response').appendChild(document.createTextNode('\n' + s));
    }


    /* Login with user/PW credentials. Not used at the moment.
     ----------------------------------------------------------------------------------------*/

    /**
     * Login with "ordinary" credentials => Will create a session on server. 
     */
    function loginWithCredentials(userName, password) {
        // Call the Web API with the AccessToken
        $.ajax({
            type: "post",
            url: applicationConfig.loginApi,
            data: mockUser,
            xhrFields: {
                withCredentials: true
            }
        }).done(function (data) {
            logMessage("Login API returned:\n" + JSON.stringify(data));
            loggedInUser = mockUser;
            curAuthStrategy = AuthStrategy.Credentials;
            authenticated = true;
            updateUI();
        })
            .fail(function (jqXHR, textStatus) {
                logMessage("Error calling the login api:\n" + textStatus);
            })
    }


    /**
     * Call the API with the current session. (withCredentials)
     * "loginWithCredentials()" must have been called before.
     */
    function callApiWithLoggedInSession(accessToken) {
        // Call the Web API with the AccessToken
        $.ajax({
            type: "GET",
            url: applicationConfig.webApi,
            xhrFields: {
                withCredentials: true
            }
        }).done(function (data) {
            logMessage("Web API returned: " + JSON.stringify(data));
        })
            .fail(function (jqXHR, textStatus) {
                logMessage("Error calling the Web api: " + textStatus);
            })
    }

    /**
     * Logout: Either from access-token based clientApp or from session.
     */
    function logout() {
        // Removes all sessions, need to call AAD endpoint to do full logout
        if (curAuthStrategy = AuthStrategy.PassportToken) {
            clientApplication.logout();
        } else {
            loggedInUser = undefined;
            $.ajax({
                type: "post",
                url: applicationConfig.logoutApi,
                xhrFields: {
                    withCredentials: true
                }
            }).done(function (data) {
                logMessage("Logout API returned:\n" + JSON.stringify(data));
                updateUI();
            })
                .fail(function (jqXHR, textStatus) {
                    logMessage("Error calling the logout api:\n" + textStatus);
                })
        }
    }

</script>
@chansen-p44

This comment has been minimized.

Copy link

@chansen-p44 chansen-p44 commented May 20, 2019

I am seeing exactly the same behavior as @thomas-mindruptive and I completely agree with him that reloading the whole SPA into an iframe isn't a viable solution.
It might work fine with your small example, but not so much with an "enterprise" SPA.

@DarylThayil

This comment has been minimized.

Copy link
Contributor

@DarylThayil DarylThayil commented May 21, 2019

@chansen-p44 that is really good feedback, I think that is something we need to take to heart in future iterations. We are in the early stages of talking about supporting the auth-code flow client side which should help us get around a lot of these issues

@DarylThayil

This comment has been minimized.

Copy link
Contributor

@DarylThayil DarylThayil commented May 21, 2019

@thomas-mindruptive I am really sorry to hear that our product is not up to your current use case. I think you bring a lot of valid feedback, this is something we will make sure to consider moving forward. I will create work on our end to document the things you suggested better. Please do follow us and our roadmap, I think we will have more elegant solutions moving forward.

@thomas-mindruptive

This comment has been minimized.

Copy link
Author

@thomas-mindruptive thomas-mindruptive commented May 22, 2019

@DarylThayil Thanks a lot for taking the time. In the meantime, I created following work-around, a poor man's token approach. I'm fully aware that it isn't as secure as the OpenID-Connect-flow, because users enter the credentials in the app's UI, not a trusted identity-provider. But it's simple, fast and better than "session-based".

  • The client sends its credentials to my ID-server via xhr/HTTPS
  • The ID-server creates a token, stores the clients claims and scopes in it and signs the token with its private key
  • The client (s) can do whatever he needs with the token. The token will be sent as authorization token with each request to an API-backend.
  • The API backends can easily validate the token with the ID-server's public key.

Best
Thomas

@worthy7

This comment has been minimized.

Copy link

@worthy7 worthy7 commented Jun 7, 2019

Just wanted to offer my 2cents here since I seem to have created something similar to what MSAL does already.

My solution is using a backend server (asp net) which returns a challenge to the front end, but the redirect afterwards goes to a very small minimal page which passes back the extracted access token to my angular app, then closes the window:


@model MyProject.Models.TokenAuth.AuthCompleteModel


<head>
    <title></title>

</head>
<body>
    <script type="text/javascript">



        console.log("@Model.ReturnOrigin")
        window.opener.postMessage(@Html.Raw(Model.ExternalAuthenticateResultModelJson),
            "@Model.ReturnOrigin"
        );

        window.close()

    </script>
</body>

This is picked up by a listener which is started in the original app upon clicking to connect.

From here my front end Angular app can do whatever it wants with the access token, whether the user is logged in or not.

I wrote a very massive writeup of how I did this here:
aspnetboilerplate/aspnetboilerplate#3342 (comment)

But the point is, this is going to be real hard without some dumb redirect location somewhere to catch and send back the fragment.

How about msal just has a static asset which is some HTML+JS, and you instruct the user to add that asset to their project by referencing the node_module/..., and then that asset should automatically have a simple route set up which will avoid any app and just run that raw js (in a window)

@ShieldPad

This comment has been minimized.

Copy link

@ShieldPad ShieldPad commented Jul 2, 2019

I must chime in on this sentiment unfortunately. Given that it's the official MS library I expected it to be rather plugin-and-play with AD.

Been using it for about 1.5 years but it still confuses me a lot compared to other OIDC experiences. I really need to get into the details to get it to work and sometimes the documentation is not in sync - so I have to go into the source code.

Although I really appreciate you guys putting it out there! It's just that the level of quality and user experience needs to be bumped up a few notches.

@DarylThayil

This comment has been minimized.

Copy link
Contributor

@DarylThayil DarylThayil commented Jul 3, 2019

@ShieldPad that is really helpful feedback, we definitely want to make it easier to use.

Do you have any suggestions on what a simple, plug and play api could look like?

@01010100010100100100100101000111

This comment has been minimized.

Copy link

@01010100010100100100100101000111 01010100010100100100100101000111 commented Jul 23, 2019

@ShieldPad that is really helpful feedback, we definitely want to make it easier to use.

Do you have any suggestions on what a simple, plug and play api could look like?

I am not using any framework, just vanilla JavaScript - for me plug and play would be plug in the required settings (authority, clientId etc..) then be able to call a method like getAccessToken() and it return a useable token.

Right now, you are asking me to check if the instance has an account, if not, log in, then ask for a token etc... The login popup is provided by Microsoft, so I don't really understand why the above is pushed onto the msal js consumer - can you not just handle this for me and "Give me a token"?

Could we not just have a "getAccessToken" and then let msal js worry about if it can retreive from cache, refresh or show the login-in popup?

I have been working on this now for days (ADFS with SAML was honestly less painful); there is out of date documentation all over the internet and it's really, really painful. Although I can get a token, the code feels hacky and it's not clear what the current, right way is - some serious open bugs are also muddying the waters here.

it makes me want to ask, who is this library actually for?

@DarylThayil

This comment has been minimized.

Copy link
Contributor

@DarylThayil DarylThayil commented Jul 23, 2019

0101010101010101010 (sp?) as a whole we are thinking about higher level apis vs lower level apis. Right now, I agree, we have lower level apis, and they require some patterns that are not extremely obvious.

The goal is the flesh out higher level apis accross msal libraries, that make sense and have the right level of abstraction. Hoping that we have something to share in the near future

@DarylThayil DarylThayil added the bug label Jul 25, 2019
@DarylThayil DarylThayil added this to Needs triage in Triage Board via automation Jul 25, 2019
@DarylThayil

This comment has been minimized.

Copy link
Contributor

@DarylThayil DarylThayil commented Jul 25, 2019

Putting in Triage, to determine the set of work to come out of this

@01010100010100100100100101000111

This comment has been minimized.

Copy link

@01010100010100100100100101000111 01010100010100100100100101000111 commented Aug 9, 2019

0101010101010101010 (sp?) as a whole we are thinking about higher level apis vs lower level apis. Right now, I agree, we have lower level apis, and they require some patterns that are not extremely obvious.

The goal is the flesh out higher level apis accross msal libraries, that make sense and have the right level of abstraction. Hoping that we have something to share in the near future

Just wanted to reply after the 1.1.2 release - this has cleared up all my issues and it seems to be working really well now from a vanilla JS point of view, everything appears to be working as intended.

@DarylThayil

This comment has been minimized.

Copy link
Contributor

@DarylThayil DarylThayil commented Aug 9, 2019

Great to hear!

Triage Board automation moved this from Needs triage to Closed Sep 19, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Triage Board
  
Closed
8 participants
You can’t perform that action at this time.