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

Support EasyAuth #33

Open
davidebbo opened this Issue Feb 25, 2016 · 83 comments

Comments

Projects
None yet
@davidebbo
Copy link
Contributor

davidebbo commented Feb 25, 2016

To make it possible for the Functions to perform authorization, we should pass the various auth claims into the context object.

Idea is to allow users to specify a new "user" auth level value to indicate that a function requires an EasyAuth validated identity.

@christopheranderson christopheranderson added this to the backlog milestone Mar 12, 2016

@mathewc mathewc changed the title Include auth claims in token Support EasyAuth Mar 22, 2016

@mathewc

This comment has been minimized.

Copy link
Contributor

mathewc commented Mar 22, 2016

Retitling

@mattchenderson

This comment has been minimized.

Copy link
Contributor

mattchenderson commented Aug 22, 2016

Update here: investigated with @fabiocav on Friday and confirmed that .NET functions can just rely on the ClaimsPrincipal, per the code below. Other languages would have to rely on the headers, and that is not code we should make users write. Would be nice to expose as part of the context object or similar.

using System.Net;
using System.Threading;
using System.Security.Claims;

public static void Run(HttpRequestMessage req, TraceWriter log)
{
    if (!Thread.CurrentPrincipal.Identity.IsAuthenticated)
    {
        log.Info("Not authenticated");
        return;
    }

    ClaimsIdentity identity = (Thread.CurrentPrincipal as ClaimsPrincipal)?.Identity as ClaimsIdentity;
    foreach (var claim in identity.Claims)
    {
       log.Info($"{claim.Type} = {claim.Value}");
    }
}
@mathewc

This comment has been minimized.

Copy link
Contributor

mathewc commented Aug 23, 2016

@mattchenderson I assume both C# and F# will now work, since we just merged the new F# model. For Node, can you give some examples of what needs to be exposed? Is it just things like context.identity.isAuthenticated and context.identity.claims? I.e. can we simply translate the identity into a json object?

Also, we'd then start exposing one more enum value for our existing authLevel property: "user", to indicate that a function requires an authenticated identity. I've already made a provision for that in our enum in anticipation of this :)

@fabiocav

This comment has been minimized.

Copy link
Member

fabiocav commented Aug 23, 2016

@mathewc we may also want to consider making the identity information available as named inputs. Thoughts on that?

@mathewc mathewc self-assigned this Jun 16, 2017

@mathewc mathewc modified the milestones: Next - Triaged, Sprint 1 Jun 28, 2017

@paulbatum paulbatum modified the milestones: Sprint 1, Sprint 3 Jul 12, 2017

@mathewc mathewc modified the milestones: Sprint 4, Sprint 3 Aug 9, 2017

@paulbatum paulbatum modified the milestones: Sprint 5, Sprint 4 Aug 10, 2017

@mathewc mathewc modified the milestones: Sprint 6, Sprint 5 Aug 28, 2017

@paulbatum paulbatum modified the milestones: Next, Sprint 6 Sep 6, 2017

@mathewc mathewc closed this Oct 24, 2017

@mathewc mathewc reopened this Nov 30, 2017

@johndowns

This comment has been minimized.

Copy link

johndowns commented Dec 3, 2018

Thanks @ConnorMcMahon. I don't mind the ClaimsPrincipal coming through as null for now, but perhaps you can advise what I'm doing wrong here as it's not working when I run this locally using the default Visual Studio function template with a ClaimsPrincipal added. Here is the function method signature:

[FunctionName("MyFunc")]
public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]HttpRequest req,
    ClaimsPrincipal claimsPrincipal,
    ILogger log)
{
}

When I run this, I get this error:

Microsoft.Azure.WebJobs.Host: Error indexing method 'MyFunc.Run'.
 
Microsoft.Azure.WebJobs.Host: Cannot bind parameter 'claimsPrincipal' to type ClaimsPrincipal. Make sure the parameter Type is supported by the binding. If you're using binding extensions (e.g. ServiceBus, Timers, etc.) make sure you've called the registration method for the extension(s) in your startup code (e.g. config.UseServiceBus(), config.UseTimers(), etc.).

But if I remove the ClaimsPrincipal from the signature, I don't get the error. I'm using version 1.0.24 of the Microsoft.NET.Sdk.Functions package, and the CLI is reporting it's running v2.0.3 of the core tools and Functions runtime version 2.0.12115.0.

Am I missing a step somewhere?

@johndowns

This comment has been minimized.

Copy link

johndowns commented Dec 3, 2018

I also just tried this using the CLI from NPM, running v2.2.70 of the core tools and Function runtime 2.0.12175.0. I got the same result.

@ConnorMcMahon

This comment has been minimized.

Copy link
Contributor

ConnorMcMahon commented Dec 3, 2018

Ah, this is only supported in versions later than 2.0.12210.0. I'm not positive we have a CLI update for that version yet.

@johndowns

This comment has been minimized.

Copy link

johndowns commented Dec 3, 2018

@myusrn

This comment has been minimized.

Copy link

myusrn commented Dec 3, 2018

I'm am trying to validate this new ClaimsPrincipal object access support initially using the in-portal author, compile and run experience. As of this morning my functions app | function apps settings is reporting Runtime version: 2.0.12210.0 (~2) so it would appear we are good on that front.

Under platform features | network | authentication/authorization i have app service authentication [ / easyauth ??? ] turned on and "Action to take when request is not authenticated" = "Log in with Azure Active Directory" so that private browser client tests will produce a browser private client session cookie access_token authenticated request leaving the desktop app public client authorization header bearer token authenticated request scenario for later.

Under platform features | network | authentication/authorization | authentication providers | Azure active directory i have management mode = express selected and have confirmed i see entry for this app in azure active directory blade's app registrations (preview) | all applications and enterprise applications views.

I am using a sample code reference function implementation.

When i run code in portal console it says error that @johndowns was seeing specifically the following.
2018-12-03T21:34:28.578 [Error] Executed 'Functions.HttpTrigger2' (Failed, Id=730632f3-affb-41b3-b918-d94168ae8602) | Object reference not set to an instance of an object.

When i open the function url in a private browser session I get prompted for authentication to which i respond with one of my azuread tenant user credentials and do the first time application consent approval. After that it just says HTTP 500 error | That’s odd... the website can’t display this page?

If i comment out string[] identityStrings = principal.Identities.Select(GetIdentityString).ToArray(); it runs w/o errors.

Insights on what i'm doing wrong to make this authN/authZ functionality work using in-portal authoring, compile, debug experience while awaiting the vs17 project + localhost func.exe cli support?

@ConnorMcMahon

This comment has been minimized.

Copy link
Contributor

ConnorMcMahon commented Dec 3, 2018

@myusrn It's possible that the identities on your claims principal are missing some of the claims that GetIdentityString are looking for. If you look at the /.auth/me endpoint, you can see what claims are populated from your AAD tenant.

@myusrn

This comment has been minimized.

Copy link

myusrn commented Dec 4, 2018

@ConnorMcMahon thanks for followup.

I find name and other claims in the https://<my azure functions app url>/.auth/me returned payload.

To bypass the possibility of claim retrieval code issues, which incidentally one can address by including the ? null check operator in Value property references, i'm using a ClaimsPrincipal parameter injection test that just looks to confirm that IsAuthenticated and Name property accessors return true and some non-null value respectively.

I'm finding those calls produce unexpected principal.Identity.IsAuthenticated = 'False' and principal.Identity.Name = 'null' result using a new private browser instance against function where I'm assuming successfully signed in user by fact that i get prompted for credentials when opening https://<my azure functions app url>/api/HttpTrigger2?name=foobar url and visiting https://<my azure functions app url>/.auth/me in separate tab from the same private browser instance dumps authenticated user details.

Thoughts?

public static async Task<IActionResult>  Run(HttpRequest req, ILogger log, ClaimsPrincipal principal)
{
    log.LogInformation("C# HTTP trigger function processed a request."); 

    var isAuthenticated = principal.Identity.IsAuthenticated; 
    var idName = string.IsNullOrEmpty(principal.Identity.Name) ? "null" : principal.Identity.Name;
    log.LogInformation($"principal.Identity.IsAuthenticated = '{isAuthenticated}' and principal.Identity.Name = '{idName}'");
    var owner = (principal.FindFirst(ClaimTypes.NameIdentifier))?.Value;
    //string[] identityStrings = principal.Identities.Select(GetIdentityString).ToArray();
    
    return new OkObjectResult($"principal.Identity.IsAuthenticated = '{isAuthenticated}' and principal.Identity.Name = '{idName}'");
    //return new OkObjectResult(string.Join(";", identityStrings));
}

private static string GetIdentityString(ClaimsIdentity identity)
{
    var userIdClaim = identity.FindFirst(ClaimTypes.NameIdentifier);
    if (userIdClaim != null)
    {
        // user identity
        var userNameClaim = identity.FindFirst(ClaimTypes.Name);
        return $"Identity: ({identity.AuthenticationType}, {userNameClaim?.Value}, {userIdClaim?.Value})";
    }
    else
    {
        // key based identity
        var authLevelClaim = identity.FindFirst("http://schemas.microsoft.com/2017/07/functions/claims/authlevel");
        var keyIdClaim = identity.FindFirst("http://schemas.microsoft.com/2017/07/functions/claims/keyid");
        return $"Identity: ({identity.AuthenticationType}, {authLevelClaim?.Value}, {keyIdClaim?.Value})";
    }
}
@petrce

This comment has been minimized.

Copy link

petrce commented Dec 4, 2018

Happy it is working :)
How can I set up Auth locally (ClientId and Issuer)?
in local.setting.json -> Host?
thanks

@johndowns

This comment has been minimized.

Copy link

johndowns commented Dec 5, 2018

@ConnorMcMahon I've just been testing this out and have run into what I think may be a bug (at least, I hope it is!).

I've configured AD authentication on my function app and disallowed anonymous requests. When I've been deploying a function with a ClaimsPrincipal parameter, I've found that:

  1. If the authLevel is set to function and the function key is provided then the request is processed, and the ClaimsPrincipal is populated with two identities - one from the function runtime and one from the AAD token.
  2. If the authLevel is set to anonymous and the function key is omitted then the request is processed, but the ClaimsPrincipal doesn't get the AAD identity populated.
  3. If the authLevel is set to anonymous and the function key is provided then the request is processed, and the ClaimsPrincipal gets both identities again.

FYI, the documentation says that when an AD token is used for authentication, the function authLevel should be set to anonymous - hence my assumption that this is a bug.

I'm not sure if this issue is the right place for this, or if I should open a new issue?

@ConnorMcMahon

This comment has been minimized.

Copy link
Contributor

ConnorMcMahon commented Dec 5, 2018

@johndowns, good catch! If you could open a new issue so I could track a PR against it, that would be great.

@myusrn

This comment has been minimized.

Copy link

myusrn commented Dec 5, 2018

@johndowns @ConnorMcMahon are you folks using a desktop app authorization header bearer token secured requests to verify in portal function implementation with ". . . , ClaimsPrincipal principal)" parameter injection or are you using browser/user agent session cookie access_token secured requests?

@johndowns are you using a vs17 azure functions project deployment to verify ". . . , ClaimsPrincipal principal)" parameter injection and since this currently cannot be f5 locally debugged using %localappdata%\azurefunctionstools\releases\2.11.3\cli\func.exe are you deploying to cloud and using functions | | monitor | | invocation details to view log.LogInformation() instrumentation added to enable this interim debugging solution for vs17 functions project with ClaimsPrincipal usage?

@ConnorMcMahon

This comment has been minimized.

Copy link
Contributor

ConnorMcMahon commented Dec 5, 2018

@myusrn I can get the sample working with the following authentication methods

  1. In browser server-directed flow
  2. Bearer token with an AAD access token for EasyAuth
  3. X-ZUMO-AUTH session token. You can generate a session token by hitting /.auth/login/aad with a POST request with the following body
    { "id_token": "", "access_token": "" }
@myusrn

This comment has been minimized.

Copy link

myusrn commented Dec 5, 2018

@ConnorMcMahon thanks for clarifications supported authentication methods that helps.

@johndowns thanks for finding and mentioning the bug you found on this thread as that was what was causing me to get the unexpected IsAuthenticated = false and Identity: (, , ) results. Once i set in portal function | Integrate | Authorization = Anonymous -> Function and included code in request i started getting expected IsAuthenticated = true and Identity: (aad, robertob@mymsdn.onmicrosoft.com, Rf-5wbc2D1QRXm0s9VlJfzmMjt-rQ3IYSp6aIjWAbr0);Identity: (WebJobsAuthLevel, Function, default) results.

Please share issue that gets created to track work on this related bug that really can leave a person confused as to why things seem to not be working.

Also note that when i did launched and f5 localhost debug of my vs17 functions project this afternoon it pulled down a new %localappdata%\azurefunctionstools\releases\2.14.0\cli\func.exe replacing the previous \2.11.3\cli\func.exe . The output window showed Function Runtime Version: 2.0.12210.0 implying that local debugging now has required 2.0.12210.0+ runtime version in place as well. I tested (. . . , ClaimsPrincipal principal) parameter injection in the function signature and it was okay with that and does execute with an IsAuthenticated = true identity present but no an authenticated user one, GetIdentityString() output was Identity: (WebJobsAuthLevel, Admin, ). Perhaps this is addressed by calling localhost debug instances using unit test or desktop app with Authorization header bearer token secured call vs browser/user agent session access_token cookie secured call.

@johndowns

This comment has been minimized.

Copy link

johndowns commented Dec 5, 2018

@ConnorMcMahon I have opened #3857 as requested.

@myusrn

This comment has been minimized.

Copy link

myusrn commented Dec 6, 2018

Anyone had succes this easyAuth authentication of request using desktop app / postman request using function authZ code and authorization header bearer token to authN request, i.e. what @ConnorMcMahon referred to as 2. Bearer token with an AAD access token for EasyAuth above?

I ask because i'm now testing that target scenario and am getting 401 Unauthorized against same setup that works if I use browser/user agent request with function authZ code and session access_token cookie secured request, i.e. what @ConnorMcMahon referred to as 1. In browser server-directed flow above.

I was able to use X-ZUMO-AUTH header, i.e. what @ConnorMcMahon referred to as option 3. X-ZUMO-AUTH session token acquired by hitting /.auth/login/aad with a POST request and body { "id_token": "", "access_token": "" } outlined above but using /.auth/login/aad GET that returned me to .auth/login/done#token=<json containing authentication_token / X-ZUMO-AUTH value> query string encoded result. I pulled the authenticationToken from that result and attached it to function request with function authZ code and X-ZUMO-AUTH header containing that session token. While this was reassuring seeing it work I really need option 2 authorization header bearer token story to work so I can acquire tokens using Microsoft authentication library [msal] in wpf/uwp desktop app for token acquisition and refreshes.

@ConnorMcMahon

This comment has been minimized.

Copy link
Contributor

ConnorMcMahon commented Dec 6, 2018

@myusrn, Could you share your function app name? You can share it privately by following these instructions. I can help you investigate why scenario 2 is not working for you. I just tested it the other day and it worked for my application.

@myusrn

This comment has been minimized.

Copy link

myusrn commented Dec 6, 2018

@ConnorMcMahon,

Below is log streaming ouput guids from calling my functions app using browser openid connect session cookie secured request.

2018-12-06T19:18:27.230 [Information] Executing 'Functions.HttpTrigger1' (Reason='This function was programmatically called via the host APIs.', Id=3b1f14e5-f67e-4289-a297-4e914c7c4fdc)
. . . 
2018-12-06T19:18:27.232 [Information] Executed 'Functions.HttpTrigger1' (Succeeded, Id=3b1f14e5-f67e-4289-a297-4e914c7c4fdc)

In my wpf app where i'm using using azuread v2 compliant msft authentication library [msal] vs azuread v1 compliant azure ad authentication library [adal], nuget to acquire access token used in Authorization header Bearer <access token> secured api requests I have scopes set to scopes = new string[] { "https://myfunctionsapp.azurewebsites.net/user_impersonation" } which does produce token with "scp" claim set to "user_impersonation". I expect that doesn't help you repro in your desktop/mobile test client unless you also have the azuread provisioned clientId and tenantId.

I'm fine with granting your work or msa account access to my resource group and associated azuread if you need ability to look closer at simple inportal + easyauth & express security setup and/or the desktop/mobile client app entry I have in azuread that has api permissions configured to allow requesting token with scope setting that aligns with azure function api its trying to call. Can send me message at myusrn@outlook.com .

@bryans2k

This comment has been minimized.

Copy link

bryans2k commented Dec 6, 2018

Anyone had succes this easyAuth authentication of request using desktop app / postman request using function authZ code and authorization header bearer token to authN request, i.e. what @ConnorMcMahon referred to as 2. Bearer token with an AAD access token for EasyAuth above?

I ask because i'm now testing that target scenario and am getting 401 Unauthorized against same setup that works if I use browser/user agent request with function authZ code and session access_token cookie secured request, i.e. what @ConnorMcMahon referred to as 1. In browser server-directed flow above.

I was able to use X-ZUMO-AUTH header, i.e. what @ConnorMcMahon referred to as option 3. X-ZUMO-AUTH session token acquired by hitting /.auth/login/aad with a POST request and body { "id_token": "", "access_token": "" } outlined above but using /.auth/login/aad GET that returned me to .auth/login/done#token=<json containing authentication_token / X-ZUMO-AUTH value> query string encoded result. I pulled the authenticationToken from that result and attached it to function request with function authZ code and X-ZUMO-AUTH header containing that session token. While this was reassuring seeing it work I really need option 2 authorization header bearer token story to work so I can acquire tokens using Microsoft authentication library [msal] in wpf/uwp desktop app for token acquisition and refreshes.

Try this link: https://www.bruttin.com/2017/11/21/azure-api-postman.html

It shows you how to use postman correctly.

@myusrn

This comment has been minimized.

Copy link

myusrn commented Dec 6, 2018

@ConnorMcMahon, Things working now using desktop/mobile app authorization header bearer token secured requests.

The issue and fix for me was to ensure that acquired token has audience "aud" claim was set to the application (client) id guid of the functions app azuread registered application.

When using new azuread v2 endpoints compatible microsoft authentication library [msal] I was providing the scope parameter that matched what shows up in registered app's api permission settings. Turns you can arbitrarily configure the starting part of that scope argument to be whatever you want the issued tokens audience claim to contain. So the following did the trick.

//var scopes = new string[] { "https://azfndn1ipt.azurewebsites.net/user_impersonation" }; // matching azfndn1ipt express provisioned entry
var scopes = new string[] { "5270888e-273e-4bba-9283-872088abe9f8/user_impersonation" }; // arbitrarily assigned value to first part of scope which is set to app(client)id of function app registration
authResult = await app.AcquireTokenSilentAsync(scopes, accounts.FirstOrDefault());
@myusrn

This comment has been minimized.

Copy link

myusrn commented Dec 7, 2018

@bryans2k thank you for the postman article reference on how to enable oauth token acquisition from azuread endpoints within postman as I've always been acquiring token using scratch app with adal -> msal calls and then pasting the result into postman request to test calls versus less flexible scratch app httpClient requests.

I was able to successfully use it to acquire token matching adal/msal produced versions by using the same desktop/mobile app azuread app registration with required scope claim of "function app id/user_impersonation" with only change being need to add logical authentication | redirect uris such as "https://clientapp/auth".

Not sure if i'll ever need the X-ZUMO-AUTH token, for scenarios other than when trying to debug if its working when oauth bearer token is not, but if that arises i'm curious if you've also been able to configure postman authorization token acquisition to carry out the /.auth/login/aad GET followed extraction of access token from the .auth/login/done#token=<json containing authentication_token / X-ZUMO-AUTH value> url?

@bryans2k

This comment has been minimized.

Copy link

bryans2k commented Dec 7, 2018

@myusrn I've never tried but I think you'd have to change the callback to your own website to log the callback from AAD. I don't think Postman offers an option to pass-through the callback.

@maryammadzadeh

This comment has been minimized.

Copy link

maryammadzadeh commented Jan 8, 2019

Hi there,

When adding the ClaimsPrincipal to the function:
public static async Task<IActionResult> RunAsync([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)]HttpRequest req, ILogger log, ClaimsPrincipal principal)

We have our roles defined in AAD and can clearly see the roles in the req param's bearer token.

However, the principal param is only populated with
{
"type": "WebJobsAuthLevel",
"level": "Admin"
}

Our roles are not defined there. We have tried using all AuthorizationLevel, though documentation says to use Anonymous.

Any updates on this issue?

@ConnorMcMahon

This comment has been minimized.

Copy link
Contributor

ConnorMcMahon commented Jan 8, 2019

@maryammadzadeh, do you have App Service Authentication / Authorization enabled? Unfortunately, at this time the ClaimsPrincipal does not take Bearer tokens unless you have configured that feature with AAD.

@JoshCollinsMSFT

This comment has been minimized.

Copy link

JoshCollinsMSFT commented Jan 8, 2019

@ConnorMcMahon is it possible to test this functionality locally? Or do we have to deploy to get AAD auth integration to flow the claims principals of bearer tokens?

@ConnorMcMahon

This comment has been minimized.

Copy link
Contributor

ConnorMcMahon commented Jan 8, 2019

In the next month or two we are planning on releasing ways to test EasyAuth locally with the Functions CLI. The work on the CLI is actually already done, but we are working on giving the Linux and Windows version of EasyAuth parity so that the local development experience matches up exactly with the production experience.

@JoshCollinsMSFT

This comment has been minimized.

Copy link

JoshCollinsMSFT commented Jan 8, 2019

Appreciate the response. Could you create a link between the PR enabling that functionality and this issue? Thanks!

@myusrn

This comment has been minimized.

Copy link

myusrn commented Jan 8, 2019

@JoshCollinsMSFT [ & @ConnorMcMahon ] in the mean time I was able to make use of "unit testing azure functions v2" -> https://medium.com/@tsuyoshiushio/writing-unit-test-for-azure-durable-functions-80f2af07c65e guidance to enable localhost unit tests of function app with injected principal containing test case relevant identity and claims.

See https://github.com/myusrn/lnsexploration/blob/master/xUnit.Tests/FunctionControllerUnitTest.cs and https://github.com/myusrn/lnsexploration/blob/master/xUnit.Tests/AzFuncApp1WebApp1UnitTests.cs for my implementation.

For localhost integration tests I wrote my production implementation with the following types of logic "if (principal.IsInRoleFuncApp("SomeRbacAuthZRole") || req.Host.Value == "localhost:7071")" to cover that case until things Conner & Co are working on get released .

@ConnorMcMahon

This comment has been minimized.

Copy link
Contributor

ConnorMcMahon commented Jan 9, 2019

@JoshcolllinsMSFT, Azure/azure-functions-core-tools#789

@maryammadzadeh

This comment has been minimized.

Copy link

maryammadzadeh commented Jan 9, 2019

@ConnorMcMahon I do have app service authN/authR enabled. I think I am just waiting for your MR to complete. I verified that AuthorizationLevel.Function works with a function key. When AuthorizationLevel.Anonymous work, then I should see my claims in the principal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.