azureazure-functionsazure-web-app-serviceeasy-auth

Calling Azure Function App from static-file SPA


ANSWER: To anyone looking for this answer, I was able to get the token I needed by specifying the web API in the scopes of an MSAL acquireToken call, like so:

let token = msal.acquireToken({ 
  scopes: [ 'myFunctionApp.azurewebsites.net/user_impersonation' ] 
})

After doing this, I used the token as a Bearer token in the Authentication header. I'm able to use this in addition to calling MS Graph endpoints. I found this information in a quiet little out of the way place here:

https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-acquire-cache-tokens#request-specific-scopes-for-a-web-api

Many thanks to @StanleyGong for his assistance that ultimately led to the answer.

/////

I've been looking for the past few days for how to effectively secure a Vue SPA served from an Azure Web App calling into an Azure Function App. I have Easy Auth turned on for both the web app and the function app and I'm calling the /.auth/me endpoint to get an id token, which I've read can be used as the bearer token. So what I'm doing is calling /.auth/me and using the returned id_token to create an Authorization header, but I still get a 401 Unauthorized when calling the function app.

Code getting the id_token from /.auth/me and adding it as a default header to all Axios calls (I do recognize that this will need to be refreshed...I'm going to create all that logic after I get a single call working):

let authUrl = '/.auth/me';
let response = await axios.get(authUrl);
let token = response.data[0].id_token;
axios.defaults.headers.common['Authorization'] = "Bearer " + token;

I can see the token being used in the Authorization header in the request, which immediately follows the section above:

Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Authorization: Bearer eyJ0eXAiO...

I saw this post and tried to use some of that info, including the blog post by Chris Gillum (which was helpful, but still didn't get me there): Authentication for Azure Functions

Any suggestions on how to get this working? I feel like I'm close, but I'm not quite there. If this isn't the right way to go about it, any advice would be helpful, too.

I'm horribly inexperienced at identity in general, so I keep getting turned around with the terminology which isn't helped by the inconsistency in documentation.

Also, is there any way to test this stuff without deploying code on every change? It would be great to test this by getting a token from the store somehow without the code being on the web app server...wishful thinking, I guess, but just wondering.

EDIT: I just realized that all the posts I've read suggest that an Access Token is returned from /.auth/me but I'm not getting one. The returned JSON looks like this, and this is all:

id_token: "eyJ0eXA...7_A"
provider_name: "aad"
user_claims: [{typ: "aud", val: "2...fa"},…]
user_id: "<<my email address>>"

ANOTHER EDIT: I've found I can get a token using MSAL but the info it contains is slightly different than that from /.auth/me. Using either token as the bearer token still results in a 401. I did notice that in the AAD auth setup for the apps that the issuer URL was different. One was sts.windows.net and the other was login.windows.net. Both had the tenant ID after them. Not sure if this made a difference, but I tried setting them to the same value and that didn't help.

/.auth/me token (sanitized, of course):

{
  "aud": "2fe...fa", (AAD app id)
  "iss": "https://sts.windows.net/<< AD tenant id >>/",
  "iat": 15785xxx,
  "nbf": 15785xxx,
  "exp": 15785xxx,
  "aio": "AVQAq/...UQ=",
  "amr": [
    "pwd",
    "mfa"
  ],
  "family_name": "<< my last name >>",
  "given_name": "<< my first name >>",
  "ipaddr": "<< my ip >>",
  "name": "<< my full name >>",
  "nonce": "e32a...48",
  "oid": "a0...0e",
  "sub": "LNu...8l8",
  "tid": "f14...2ca",
  "unique_name": "<< my email >>",
  "upn": "<< my email >>",
  "uti": "i9O...TAQ",
  "ver": "1.0"
}

MSAL access token:

{
  "aud": "000...000", (mostly 0s...not sure what this id is)
  "iss": "https://sts.windows.net/<< AD tenant id >>",
  "iat": 15785xxx,
  "nbf": 15785xxx,
  "exp": 15785xxx,
  "acct": 0,
  "acr": "1",
  "aio": "AVQAq/8O...ZZ12s=", (different than above)
  "amr": [
    "pwd",
    "mfa"
  ],
  "app_displayname": "<< app name in AAD app registration >>",
  "appid": "<< app GUID from AAD >>",
  "appidacr": "0",
  "family_name": "<< my last name >>",
  "given_name": "<< my first name >>",
  "ipaddr": "<< my ip >>",
  "name": "<< my full name >>",
  "oid": "a0...0e", (same as above)
  "platf": "3",
  "puid": "10...1B",
  "scp": "User.Read profile openid email",
  "signin_state": [
    "kmsi"
  ],
  "sub": "WuX...L3A",
  "tid": "f14...2ca", (tenant id, same as above)
  "unique_name": "<< my email >>",
  "upn": "<< my email >>",
  "uti": "UD...AA",
  "ver": "1.0",
  "xms_st": {
    "sub": "LNu...8l8"
  },
  "xms_tcdt": 14...37
}

Solution

  • For your scenario, you can register an Azure AD native application in your tenant as a client to get an access_token as a Bearer token to call your Azure function. If you are using service-to-service call, just refer to my previous post here.

    If you will log in users,you should register an Azure AD application in your tenant too with additional configs : enter image description here Add user_impersonation permission to this app so that users can login your app and call your Azure function (pls note your azure function application ID here and we will use it later): enter image description here Add this permission and grant it : enter image description here enter image description here

    With these steps are done, your new application will be able to login users to call your Azure function.

    I am not sure what your VUE application code like, but this is a VUE adal sample and my demo will be based on it. 1. Download the code and go to src/main.js replace its content with code below :

    import Vue from 'vue'
    import axios from 'axios'
    import { default as Adal, AxiosAuthHttp } from 'vue-adal'
    import App from './App.vue'
    import router from './router'
    
    Vue.config.productionTip = false
    const functionBase = `<your Azure function URL>`
    const functionResource = '<your Azure function application ID>'
    
    Vue.use(Adal, {
      config: {
        tenant: '<your tenant ID>',
        clientId: '<your new resistered app ID>',
        redirectUri: '<redirect url you mapped in your new resgistered app ID, in this case it should be : http://localhost:8080>',
        cacheLocation: 'localStorage'
      },
      requireAuthOnInitialize: true,
      router: router
    })
    
    Vue.use({
      install (vue, opts = {}) {
        // Configures an axios http client with a interceptor to auto-acquire tokens
        vue.prototype.$functionApi = AxiosAuthHttp.createNewClient({
          // Required Params
          axios: axios,
          resourceId: functionResource, // Resource id to get a token against
    
          // Optional Params
          router: router, // Enables a router hook to auto-acquire a token for the specific resource
    
          baseUrl: functionBase, // Base url to configure the client with
    
          onTokenSuccess (http, context, token) { // Token success hook
            // When an attempt to retrieve a token is successful, this will get called.
            // This enables modification of the client after a successful call.
            if (context.user) {
              // Setup the client to talk with the Microsoft Graph API
              http.defaults.baseURL = `${functionBase}`
              console.log(token)
            }
          },
    
          onTokenFailure (error) { // Token failure hook
            // When an attempt to retrieve a token is not successful, this will get called.
            console.log(error)
          }
        })
      }
    })
    
    new Vue({
      router,
      render: h => h(App)
    }).$mount('#app')
    

    2. Go to src/views/Home.vue , replace the content of <script> section with code below :

    <script>
    // @ is an alias to /src
    import HelloWorld from '@/components/HelloWorld.vue'
    
    export default {
      name: 'home',
      components: {
        HelloWorld
      },
      data () {
        return {
          msg: "Signing in..."
        }
      },
      async created () {
        if (this.$adal.isAuthenticated()) {
          this.msg = "Hello, " + this.$adal.user.profile.name 
    
          let functionInfo = await this.getAzureFunctionInfo()
          this.msg += "  |  function result : " + functionInfo
    
    
        } else {
          this.msg = "Please sign in"
        }
      },
    
      methods: {
        async getAzureFunctionInfo () {
          let res = await this.$functionApi.get('',{
            params: {
              'name': 'test'
            }
          })
          console.log(res)
          return res.data
        }
      }
    }
    </script>
    

    Let's test the result: As this project will run locally , you should go to your Azure function =>Platform features => All settings to enable all cors requests so that you can test your function locally from static vue project : enter image description here

    My function logic was simple , if you call it with a param , it will reply a hello response : enter image description here

    Run the project, and open it in a private browser window by url : http://localhost:8080 You will be requested to login and you can see it could call Azure function successfully : enter image description here

    I think this is the answer that you are looking for.

    Update:

    Based on your code, you are using Azure AD V2 to login users, Pls :

    1)follow the second solution to resginster an Azure ad app and finish the permission grant process so that it could login users and access your Azure function.

    2)In your Azure AD, find the Azure AD app whcih on behalf of your Azure function app by name(my Azure function name is :stanfuntest ): Copy the scope here : enter image description here

    2) In your VUE code,use this app ID as a client id to login users , modify the code to get access token with the scope you just found :

    let token = msal.acquireToken( { scopes: [ 'https://stanfuntest.azurewebsites.net/user_impersonation' ] } )
    

    Hope it helps.