Azure / static-web-apps

Azure Static Web Apps. For bugs and feature requests, please create an issue in this repo. For community discussions, latest updates, kindly refer to the Discussions Tab. To know what's new in Static Web Apps, visit https://aka.ms/swa/ThisMonth
https://aka.ms/swa
MIT License
318 stars 53 forks source link

Azure AD Claims with Static Web Apps and Azure Functions (Authorization) #988

Open johnnyreilly opened 1 year ago

johnnyreilly commented 1 year ago

Describe the bug

Azure AD app role claims are not supplied to Azure Functions when linked with Azure Static Web Apps using the "bring your own functions" / linked backend approach. This impairs implementing authorization against endpoints.

To Reproduce Steps to reproduce the behavior:

  1. Create an Azure AD application with some custom app roles; eg:
image
  1. Create a Static Web App and a Function App
  2. Both the SWA and FA should use the Azure AD application and be linked
  3. Take a look at the /.auth/me endpoint in the SWA - note the claims; they should include one of your custom app roles. eg OurApp.Read
{
  "clientPrincipal": {
    "identityProvider": "aad",
    "userId": "d9178465-3847-4d98-9d23-b8b9e403b323",
    "userDetails": "johnny_reilly@hotmail.com",
    "userRoles": ["authenticated", "anonymous"],
    "claims": [
      // ...
      {
        "typ": "http://schemas.microsoft.com/identity/claims/objectidentifier",
        "val": "d9178465-3847-4d98-9d23-b8b9e403b323"
      },
      {
        "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
        "val": "johnny_reilly@hotmail.com"
      },
      {
        "typ": "name",
        "val": "John Reilly"
      },
      {
        "typ": "roles",
        "val": "OurApp.Read"
      },
      // ...
      {
        "typ": "ver",
        "val": "2.0"
      }
    ]
  }
}
  1. Take a look at the claims that the Function App endpoints receive. It's possible to see this by implementing a function which surfaces roles:
[FunctionName("GetRoles")]
public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "GetRoles")] HttpRequest req
)
{
    var roles = req.HttpContext.User?.Claims.Select(c => new { c.Type, c.Value });

    return new OkObjectResult(JsonConvert.SerializeObject(roles));
}

Which will then be accessed at the static web app's /api/GetRoles endpoint:

[
  {
    "Type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
    "Value": "d9178465-3847-4d98-9d23-b8b9e403b323"
  },
  {
    "Type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
    "Value": "johnny_reilly@hotmail.com"
  },
  {
    "Type": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
    "Value": "authenticated"
  },
  {
    "Type": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
    "Value": "anonymous"
  }
]

At first look, this seems great; we have claims! But when we look again we realise that we have far less claims than we might have hoped for. Crucially, our custom claims / app roles like OurApp.Read are missing.

Expected behavior

Where a Static Web App has a linked Function App, the Function App should receive a user's App Roles custom claims in calls to API endpoints, in the same way they do at the SWA's ./auth/me endpoint. Not just UserRoles.

Why is this important?

In a word: authorisation. App Roles custom claims are typically used to apply authorisation against applications. Without this in place people have to handroll an authorisation mechanism. The methods they come up with can be insecure and often do not scale.

Device info (if applicable):

N/A

Additional context

I'd be happy to demo this directly and share code. I'm working with Warren Joubert of Microsoft on a workaround for this and understand this to be a general problem that users are experiencing. It would be tremendous to get this remedied.

Writing this problem up here, with a workaround - ideally this shouldn't be needed

davide-bergamini-sevenit commented 1 year ago

A similar problem exists when linking an App Service. I think it will be great if it could pass the AD token "X-MS-TOKEN-AAD-ID-TOKEN" like EasyAuth do, in this way we could use Microsoft.Identity.Web.

johnnyreilly commented 1 year ago

Agreed @davide-bergamini-sevenit - I suspect this is a general problem and affects all linked backends. So would expect container apps to have a similar issue.

johnnyreilly commented 1 year ago

I've written up my workaround in this post: https://johnnyreilly.com/azure-ad-claims-static-web-apps-azure-functions

Thanks @warrenandre for this assistance.

houlgap commented 1 year ago

Apologies if I have missed something in your description, and with a caveat that I've already upgraded my project to use .net 7.0 but I managed to get the user claims/roles working as expected when I deserialize the payload of the request to /getroles, rather than using the req.HttpContext.User property...

[Function("GetRoles")]
public async Task<HttpResponseData> GetRoles(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")]
    HttpRequestData req)
{
    var payload = JsonConvert.DeserializeObject<UserPayload>(await req.ReadAsStringAsync());

    var roles = new List<string>();
    foreach (var claim in payload.claims)
    {
        if (claim.typ == ClaimTypes.Role)
        {
            roles.Add(claim.val);
        }
    }
   var response = req.CreateResponse(HttpStatusCode.OK);
   await response.WriteAsJsonAsync(new { roles = roles });
   return response;
}

public class UserPayload
{
    public string identityProvider { get; set; }
    public string userId { get; set; }
    public string userDetails { get; set; }
    public string accessToken { get; set; }
    public List<UserClaims> claims { get; set; } = new();

    public class UserClaims
    {
        public string typ { get; set; }
        public string val { get; set; }
    }
}

If I then query the /.auth/me endpoint of the deployed application, I see both the claim and the userRole set. The role is also included in all subsequent requests in the x-ms-client-principal header

{
  "clientPrincipal": {
    "identityProvider": "aad",
    "userId": "...",
    "userDetails": "...",
    "userRoles": [
      "anonymous",
      "authenticated",
      "test-role"
    ],
    "claims": [
      ...
      {
        "typ": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
        "val": "test-role"
      },
      ...
    ]
  }
}
johnnyreilly commented 1 year ago

Is your test-role role a custom claims that you've configured against your Azure AD App Registration? That's what we were / are missing.

Oh wait, you're manually adding a role first? I'm not quite following what your Function("GetRoles") is intended to do?

houlgap commented 1 year ago

Yes, I created an application role in the App registration, then assign it to the required user/group in the corresponding Enterprise Application.

Without the getRoles function reading the claims and returning the custom role it isn't included in the userRoles array

image

houlgap commented 1 year ago

Sorry, missed a crucial detail as it coincided with how you had named your function. In your staticwebapp.config.json file, you need to specify the rolesSource property to point to the GetRoles function. This is then called by the platform when a user logs in.

 "auth": {
        "rolesSource": "/api/GetRoles",
        "identityProviders": ...
    }
johnnyreilly commented 1 year ago

Oh I see! (I think)

You're not manually calling /api/getroles yourself, you're implementing that function and with that in place, *other" functions will now (behind the scenes) invoke this and as a consequence have the custom claims that you've configured against your Azure AD App Registration?

https://learn.microsoft.com/en-us/azure/static-web-apps/assign-roles-microsoft-graph

That's worth knowing! And also mighty peculiar!

houlgap commented 1 year ago

yes, sorry for the confusion!

johnnyreilly commented 1 year ago

I'll try and test this out - thanks for sharing!

johnnyreilly commented 1 year ago

It's funny, you read the docs here: https://learn.microsoft.com/en-us/azure/static-web-apps/assign-roles-microsoft-graph#verify-custom-roles and it's not obvious how the approach you're suggesting would work. However that could totally be ashortcoming of the docs - will have to suck it and see

gbelenky commented 1 year ago

works for me too as @houlgap suggested. Thanks, I spent so much time with @johnnyreilly blog, but was happy to find a solution which does not involve all those Graph queries. Happy New Year!

steverhall commented 1 year ago

We had mixed results using solution from @houlgap, then realized that sometimes, the "typ" of the claim was just "roles", not "http://schemas.microsoft.com/ws/2008/06/identity/claims/role". Unclear as to when or why this happens, but it happens frequently with the same browser from the same user.

Changed code to:

if (claim.typ == ClaimTypes.Role || claim.typ == "roles")
{
    roles.Add(claim.val);
}

as our work-around.

leevi-sa commented 11 months ago

@houlgap @steverhall Thank you for all sharing. But I found more mysterious problem.

In my PoC, the /api/getRoles is not called at all by the SWA platform. I can call that function directly through browser with the generated function-call url with the code parameter. But through the Function's Monitor, I can see there's no other calls except my manual ones.

Do you have thoughts on this issue?

I'm wondering if it's caused by that my backend function is deployed independently and linked to the SWA afterwards. Except this issue, all the interactions among AAD, SWA and AZ Function work well as excepted.

My frontend is a simple React app. And the backend is .Net/C# AZ Function.

steverhall commented 11 months ago

Hi,

This function gets called by SWA during the authentication process. In order for it to be called, it must be an open API (no function key or authentication required).

Get Outlook for iOShttps://aka.ms/o0ukef


From: leevi-sa @.> Sent: Monday, July 31, 2023 7:38:14 AM To: Azure/static-web-apps @.> Cc: Mention @.>; Comment @.> Subject: Re: [Azure/static-web-apps] Azure AD Claims with Static Web Apps and Azure Functions (Authorization) (Issue #988)

@houlgaphttps://github.com/houlgap @steverhallhttps://github.com/steverhall Thank you for all sharing. But I found more mysterious problem.

In my PoC, the /api/getRoles is not called at all by the SWA platform. I can call that function directly through browser with the generate function-call url. But through the Function's Monitor, I can see there's no other calls except my manual ones.

Do you have thoughts on this issue?

I'm wondering if it's caused by that my backend function is deployed independently and linked to the SWA afterwards. Except this issue, all the interactions among AAD, SWA and AZ Function work well as excepted.

My frontend is a simple React app. And the backend is .Net/C# AZ Function.

— Reply to this email directly, view it on GitHubhttps://github.com/Azure/static-web-apps/issues/988#issuecomment-1658199868 or unsubscribehttps://github.com/notifications/unsubscribe-auth/ABVMVCHBWH52APHDTPCHSADXS6KKPBFKMF2HI4TJMJ2XIZLTSOBKK5TBNR2WLJDUOJ2WLJDOMFWWLO3UNBZGKYLEL5YGC4TUNFRWS4DBNZ2F6YLDORUXM2LUPGBKK5TBNR2WLJDUOJ2WLJDOMFWWLLTXMF2GG2C7MFRXI2LWNF2HTAVFOZQWY5LFUVUXG43VMWSG4YLNMWVXI2DSMVQWIX3UPFYGLLDTOVRGUZLDORPXI6LQMWWES43TOVSUG33NNVSW45FGORXXA2LDOOJIFJDUPFYGLKTSMVYG643JORXXE6NFOZQWY5LFVEZDKMZVG42TEOBRQKSHI6LQMWSWS43TOVS2K5TBNR2WLKRRGQ2DSMZRGUYDCMVHORZGSZ3HMVZKMY3SMVQXIZI. You are receiving this email because you were mentioned.

Triage notifications on the go with GitHub Mobile for iOShttps://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Androidhttps://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub.

leevi-sa commented 11 months ago

Hi @steverhall, thank you for the direction. Yes. It works now.

The mistake I made is that I did not change the default authLevel. VS uses AuthorizationLevel.Function as the default value. It's working perfectly after I change it back to AuthorizationLevel.Anonymous.

johschmidt42 commented 10 months ago

Thank you for providing this approach @johnnyreilly and others. I've got a few questions:

Edit: Questions got pretty much answered by reading the docs more carefully. Leaving the answers here though.

johnnyreilly commented 10 months ago

To be honest, it's been a while since I did this and all the knowledge I have had been composed into this post:

https://johnnyreilly.com/azure-ad-claims-static-web-apps-azure-functions

carlin-q-scott commented 1 week ago

Am understanding correctly that the backchannel principal header only contains claims of type role from the Web App /.auth/me document? I wanted to use this for a multi-tenant app, so the tenantid claim is the most important to me, and it's missing.