1

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
}
TheDoc
  • 688
  • 2
  • 14
  • 33
  • You should be receiving a cookie which will automatically be sent in subsequent requests, however if you are experiencing issues with that, you can use the `id_token` returned as a bearer token (send the header `Authorization: Bearer TOKEN` on the request). – Icehorn Jan 08 '20 at 00:32
  • So basically, you want to know how to call an Azure function which enabled easy auth from a VUE SPA app which is hosted on Azure app service, right? – Stanley Gong Jan 08 '20 at 02:46
  • @Icehorn I have tried passing the id_token as a bearer token, as shown in the second code block in the post. That code block is copy/pasted from Chrome Dev Tools on the response headers for the request. I still get a 401 Unauthorized response. – TheDoc Jan 08 '20 at 17:12
  • @StanleyGong Yes, that's correct. Azure Web App (Vue SPA) -> Azure Functions App. Both hosted in Azure, both with Easy Auth enabled on the same Azure AD tenant – TheDoc Jan 08 '20 at 17:13

1 Answers1

4

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.

Stanley Gong
  • 11,522
  • 1
  • 8
  • 16
  • First, thank you for such an amazing response! So much detail! I believe I'm wanting to use the first method you mentioned, just calling service to service. I've followed your instructions on the other post and I'm getting a CORS issue on the call to login.microsoftonline.com. I get an access token when using Postman to make the request, and when reading the token in the JWT Decoder, everything looks good. When I call from the browser, though, I get CORS error. Thoughts? – TheDoc Jan 09 '20 at 22:26
  • Sorry , it was my fault. As my first method based on service to service call it is for service side . As your VUE app is a client which can't use it(Azure AD not allows CORS) if you do want to use it, you should implement a backend as a proxy to get an access token for your VUE app . So pls kindly use the second way. – Stanley Gong Jan 10 '20 at 01:30
  • Hi , @TheDoc , I noticed that you can get access token in your VUE project.Could you pls show me the code that how you get it ? Seems you are getting microsoft graph access token instead of your Azure function access token . You can just modify the resource from "https://graph.microsoft.com" to your Azure function application ID so that you can get an access token to call your Azure functrion – Stanley Gong Jan 10 '20 at 03:08
  • I was able to get everything working by using `msal.acquireToken()` and specifying the scopes as so: `let token = msal.acquireToken( { scopes: [ 'myFunctionApp.azurewebsites.net' ] } )`. I found this info on a single small paragraph here: https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-acquire-cache-tokens#request-specific-scopes-for-a-web-api. Thank you so much for your help! While I didn't ultimately use your exact solutions, they led me to the answer. Many thanks! – TheDoc Jan 10 '20 at 22:33
  • Looks like I spoke too soon...the solution I posted works on localhost testing, but when I uploaded to Azure, I get `Unsafe JavaScript attempt to initiate navigation for frame with origin 'https://.azurewebsites.net' from frame with URL 'https://login.microsoftonline.com/...&prompt=none&response_mode=fragment'. The frame attempting navigation of the top-level window is sandboxed, but the flag of 'allow-top-navigation' or 'allow-top-navigation-by-user-activation' is not set. If cross-origin and JS nav stuff is so bad, why would it be part of the auth kit for SPAs??? – TheDoc Jan 10 '20 at 23:21
  • 1
    Hi @TheDoc , I have appended my updated in my answer , pls have a check – Stanley Gong Jan 13 '20 at 03:52
  • You're the man, @StanleyGong. All working as expected now. Thanks again for your exceptionally detailed answer! – TheDoc Jan 13 '20 at 17:31