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

Enable Easy Auth Local Dev #789

Closed
wants to merge 6 commits into from
Closed

Enable Easy Auth Local Dev #789

wants to merge 6 commits into from

Conversation

glennamanns
Copy link
Contributor

@glennamanns glennamanns commented Oct 10, 2018

Main goal: Enable local development with Easy Auth / Middleware (rebranding in process).

This is based on Easy Auth Linux, and cannot be incorporated into this repo until the EA Linux + Windows merge is complete. However, I'd like to get feedback on it now.

One new command, used two different ways:

  1. func auth create-aad --aad-name {AADAppName}
    This one is exclusively for local testing. It creates a new AAD application with localhost:7071 in the reply URLs, a homepage of localhost:7071, and a URI with localhost in the name. The client ID, client secret, and other settings that Easy Auth needs to run locally are saved to a new file local.middleware.json. This file can later be consumed by the Easy Auth webhost.

  2. func auth create-aad --aad-name {AADAppName} --app-name {ExistingAzureAppName}
    This is designed to link an existing Function/Site with a new AAD application. It creates a new AAD application, but there is nothing related to localhost in its settings. The settings are the same as if you were to create a new app from the Portal's Easy Auth flow. This command also does not create a new .json file (to avoid writing secrets to disk). The existing Function/Site's authsettings (WEBSITE_AUTH_ENABLED, etc) are updated via an ARM call.

Still to do:

  • Easy Auth Linux + Windows merge. Need to inject dependency upon EasyAuth/Middleware Nuget.

}
else
{
throw new FileNotFoundException("Must have dotnet installed in order to use local authentication.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked at the full PR yet, but this will be a problem. We only have one code path that depends on dotnet right now (other than .NET development scenarios of course) and we are tracking removing that dependency here #367. Ideally we wouldn't add more dependencies on dotnet for other non-.NET specific scenarios. Any plans to have self-contained builds of that console app?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can certainly make it a priority to have a self-contained build if removing the dotnet dependency is important. I'll look into it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. The dotnet dependency is something we're actively working on moving away from, so we definitely don't want to deepen that dependency if possible. Worse case, we'd need to position Easy Auth as an optional feature that depends on it, but we'd need to make the message here clearer, perhaps also pointing to a fwlink with details.

Looked over the PR and it does seem like a lot of things are still pending/in flux, would you mind adding a comment when you feel this is ready for review?

Copy link
Contributor

@ahmelsayed ahmelsayed left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you have a couple of TODOs in the code, I also added few more comments. Let me know if you have any questions and ping us again once you're done with your TODOs and ready for a final review. Over all looks like a great addition to the core-tools that people have been asking for for a while now!


namespace Azure.Functions.Cli.Actions.AuthActions
{
abstract class BaseAuthAction : BaseAzureAction
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't really need this class since it's empty, right? you can just drive from BaseAzureAction if you need the access token, or were you planning on adding some common code here?

var process = Process.Start("CMD.exe", $"/C dotnet {dllPath} {query}");
var cancellationToken = new CancellationTokenSource();
cancellationToken.Token.Register(() => process.Kill());
await process.WaitForExitAsync();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit unsure about the use of the cancellationToken here. It doesn't look like you really need it. You can also use the Executable class I have defined to help with this. The usage is fairly straight forward. Check here or here for samples


(var listenUri, var baseUri, var certificate, string certPath, string certPassword) = await Setup();

int originalPort = Port;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe call it hostPort for clarity.


// Regardless of whether or not auth is enabled, clients should send requests here
var originalListenUri = listenUri.SetPort(originalPort);
var originalBaseUri = baseUri.SetPort(originalPort);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same thing here, I think it'll be clearer if you called them hostListenUri and authListenUri

authSettingsToPublish.Add("issuer", "https://sts.windows.net/" + tenant + "/");
authSettingsToPublish.Add("runtimeVersion", "1.0.0");
authSettingsToPublish.Add("tokenStoreEnabled", "True");
authSettingsToPublish.Add("unauthenticatedClientAction", "1"); // Corresponds to AllowAnonymous
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could use the dictionary initialization syntax, it makes it a bit more readable, i.e:

var authSettingsToPublish = new Dictionary<string, string>
{
    { "allowedAudiences", serializedArray },
    { "isAadAutoProvisioned", "True" },
    ...
}

.Error
.WriteLine(ErrorColor("Error updating app settings:"))
.WriteLine(ErrorColor(result.ErrorResult));
return false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same thing here regarding the error. If this is an error that you would like to communicate as a cli error, then consider either throwing here, or where you call this like I mentioned in the other comment above

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I based this on PublishAppSettings / UpdateFunctionAppAppSettings, which modifies /config/AppSettings (mine modifies /config/authsettings). Is there a reason those do not throw?

return true;
}

static string ComputeSha256Hash(string rawData)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can add this method to StringExtensions.cs

}
}

public static string GeneratePassword(int length)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you explain a bit what this password is for and what its format is like?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This generates an AAD application client secret. An example of what it generates is: "77S2ca0Gwf8mTr7s85ZEqe7dcxKnF4DdyMAF5gNFaTBY6gBEDtcPwQWlTP3KWNElvld9eFyXZdvFfmEnqmrHQA7ki0$LRQ8TL3uLrj6S$F0PRtRYnZ8Dp9xReKNQK9z"

}
}

static class AADConstants
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this class to the Constants.cs class as a nested class there.

@fabiocav
Copy link
Member

@glennamanns just saw the last commit land. Would you remove the WIP/let us know when this is ready for review? Just want to make sure we don't ignore your changes once this is ready to go :)

Copy link
Contributor

@cgillum cgillum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some initial feedback. I'm not yet done reviewing, so I'll add more later.

{
private readonly IAuthManager _authManager;

public string AADName { get; set; }
Copy link
Contributor

@cgillum cgillum Oct 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming nit: "AAD" is not really a thing, so AADName sounds unnatural. I'm also not sure what the difference is between an "AADName" and an "AppName". Should they be the same? Or should "AADName" be renamed to "AppRegistrationName"? #Closed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment for the parameter syntax below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are your thoughts on AADRegistrationName for both the variable and what goes on the command line? And for Azure App Service apps/functions, appName?

Copy link
Contributor

@cgillum cgillum Oct 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe AADAppRegistrationName instead of AADRegistrationName? appName is fine for the function app name. #Closed

{
Parser
.Setup<string>("aad-name")
.WithDescription("Name of AD application to create")
Copy link
Contributor

@cgillum cgillum Oct 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Product naming nit: "Name of the Azure Active Directory app registration to create." #Closed


Parser
.Setup<string>("app-name")
.WithDescription("Name of Azure Websites Application/Function to link AAD application to")
Copy link
Contributor

@cgillum cgillum Oct 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grammar and product naming nits: "Name of the Azure App Service or Azure Functions app which corresponds to the Azure AD app registration." #Closed

Copy link
Contributor

@cgillum cgillum Oct 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get nit-picky about this since these strings are public-facing and therefore need to be considered somewhat official. #Closed

class AuthSettingsFile
{
public bool IsEncrypted { get; set; }
public Dictionary<string, string> Values { get; set; } = new Dictionary<string, string>();
Copy link
Contributor

@cgillum cgillum Oct 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this a case-insensitive dictionary: new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase). That way you don't have to remember to use case-sensitive lookups every time you reference this. #Closed

// Determine if middleware (Easy Auth) is enabled
var middlewareAuthSettings = _secretsManager.GetMiddlewareAuthSettings();
bool authenticationEnabled = middlewareAuthSettings.ContainsKeyCaseInsensitive(Constants.MiddlewareAuthEnabledSetting) &&
middlewareAuthSettings[Constants.MiddlewareAuthEnabledSetting].ToLower().Equals("true");
Copy link
Contributor

@cgillum cgillum Oct 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be a bit cleaner:

bool authenticationEnabled = 
    middlewareAuthSettings.TryGetValue(Constants.MiddlewareAuthEnabledSetting, out string enabledValue) &&
    bool.TryParse(enabledValue, out bool isEnabled) &&
    isEnabled;
``` #Closed

return (new Uri($"{protocol}://0.0.0.0:{Port}"), new Uri($"{protocol}://localhost:{Port}"), cert);
if (UseHttps)
{
(X509Certificate2 cert, string certPath, string certPassword) = await SecurityHelpers.GetOrCreateCertificate(CertPath, CertPassword);
Copy link
Contributor

@cgillum cgillum Oct 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels unnatural to fetch the certificate and both its path and password. Is this because the Easy Auth code you're calling expects these? Could the Easy Auth code be refactored to take an X509Certificate2 for embedded scenarios like this? #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing all of this altogether. Easy Auth, when truly middleware (which it is now), doesn't need its own reference to the certificate.

get AAD permissions from bindings, cleanup

Cleanup and bug fixes cont.
}
else
{
return Values.ToDictionary(k => k.Key, v => (string)v.Value);
Copy link
Contributor

@cgillum cgillum Oct 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to cast v.Value to string explicitly? Isn't it already a string? #Closed

get
{
var authFile = "local.middleware.json";
var rootPath = ScriptHostHelpers.GetFunctionAppRootDirectory(Environment.CurrentDirectory, new List<string>
Copy link
Contributor

@cgillum cgillum Oct 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var rootPath = ScriptHostHelpers.GetFunctionAppRootDirectory(Environment.CurrentDirectory, new List<string>
var rootPath = ScriptHostHelpers.GetFunctionAppRootDirectory(Environment.CurrentDirectory, new []
``` #Closed

Copy link
Contributor

@cgillum cgillum Oct 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and they are more immutable (you cannot add or remove elements, making them easier to reason about). Generally speaking you should try to use the most restrictive data types. #Closed

@@ -250,6 +250,26 @@ private static async Task ArmHttpAsync(HttpMethod method, Uri uri, string access
}
}

public static async Task<HttpResult<Dictionary<string, string>, string>> UpdateFunctionAppAuthSettings(Site site, string accessToken)
{
var url = new Uri($"{ArmUriTemplates.ArmUrl}{site.SiteId}/config/authsettings?api-version={_storageApiVersion}");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is _storageApiVersion correct? This is an ARM call, not a storage call, so this seems not quite right.

Copy link
Contributor Author

@glennamanns glennamanns Oct 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetStorageAccount, an existing method+ARM call, uses it. Authsettings requires a later API version than appsettings, because I got the following error with the older version: "The current api-version doesn't support RBAC users. Please use a newer version instead"

I think I'll add a second string called _authSettingsApiVersion and hardcode it to 2018-02-01, which is what the storage api version is. That way, it's clear the two are used for separate endpoints and can be changed independently of one another.

@@ -52,6 +52,23 @@ private static IEnumerable<string> GetBindings()
return bindings;
}

public static IEnumerable<(string, BindingDirection)> GetBindingsWithDirection()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who is using this new API?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The create-aad command. In order to know which permissions to add, I need to know the bindings and their direction

@@ -41,5 +43,22 @@ public static string SanitizeImageName(this string imageName)

return cleanImageName.ToLowerInvariant().Substring(0, Math.Min(cleanImageName.Length, 128)).Trim();
}

public static string ComputeSha256Hash(string rawData)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who uses this new API?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create-aad to generate an encryption and signing key

{
_workerRuntime = workerRuntime;

List<string> replyUrls;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need this to be a List<string> or can you instead use string[]. string[] array is preferred since it is less overhead and it is a fixed size, making it easier to reason about how it is used throughout the code (i.e. nobody is going to add to it or remove from it).

Copy link
Contributor Author

@glennamanns glennamanns Oct 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a List in order to get the serialized array (string serializedArray = JsonConvert.SerializeObject(replyUrls, Formatting.Indented)) that we use to update /authsettings in the proper format. The other parts of the code could use string[], but not this one. I can either change all of them to string[] and then convert to List in the publish to /authsettings scenario, or eat the overhead elsewhere.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a serialization perspective, List<string> and string[] create exactly the same JSON and can be used interchangeably.

/// <param name="AADName">Name of the new AAD application</param>
/// <param name="appName">Name of an existing Azure Application to link to this AAD application</param>
/// <returns></returns>
private string CreateQuery(string AADName, string appName, out string tempFile, out string clientSecret, out List<string> replyUrls, out string hostName)
Copy link
Contributor

@cgillum cgillum Oct 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be named GetCommandLineArguments? #Closed

src/Azure.Functions.Cli/Common/AuthManager.cs Outdated Show resolved Hide resolved
private Dictionary<string, string> CreateAuthSettingsToPublish(string clientId, string clientSecret, string tenant, List<string> replyUrls)
{
// The 'allowedAudiences' setting of /config/authsettings is of the form ["{replyURL1}", "{replyURL2}"]
string serializedArray = JsonConvert.SerializeObject(replyUrls, Formatting.Indented);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's important that variable names give us an idea of what the data actually is and/or what it will be used for. In this case, replyUrlArray or something similar seems more appropriate.


var authSettingsToPublish = new Dictionary<string, string>
{
{ "allowedAudiences", serializedArray },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allowedAudiences should not be the same as the reply URLs. It should just be the base URL - e.g. "https://myapp.azurewebsites.net" or "http://myapp.localhost".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could be misunderstanding, but I'm not sure that's true. I looked at the allowed_audiences of several function apps set up with Easy Auth (via the portal, not this new command) and they have the /.auth/login/aad/callback in their allowed audiences.

Examples: glennahttps, graphbinding-experiment, graphextension-beta1, FunctionsEasyAuth

Copy link
Contributor

@cgillum cgillum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more feedback items.

@@ -149,8 +153,8 @@ private string CreateQuery(string AADName, string appName, out string tempFile,

string serializedReplyUrls = string.Join(" ", replyUrls.ToArray<string>());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I don't think you need the .ToArray<string>() here.

{
_workerRuntime = workerRuntime;

List<string> replyUrls;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a serialization perspective, List<string> and string[] create exactly the same JSON and can be used interchangeably.

namespace Azure.Functions.Cli.Actions.AzureActions
{
// Invoke via `func azure auth create-aad --AADAppRegistrationName {yourAppRegistrationName} --appName {yourAppName}`
[Action(Name = "create-aad", Context = Context.Azure, SubContext = Context.Auth, HelpText = "Creates an Azure Active Directory app registration. Can be linked to an Azure App Service or Function app.")]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest "create-aad-app" as the action name since "aad" is not really a thing you can create.

{
// Connect this AAD application to the Site whose name was supplied (set site's /config/authsettings)
// Tell customer what we're doing, since finding and updating the site can take a number of seconds
ColoredConsole.WriteLine($"\nUpdating auth settings of application {appName}..");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Environment.NewLine instead of \n.

}
else
{
throw new FileNotFoundException("Cannot find az cli. `auth create-aad` requires the Azure CLI.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we rename create-aad, don't forget to update it here as well.


if (csProjFile == null)
{
throw new CliException($"Auth settings file {SecretsManager.MiddlewareAuthSettingsFileName} could not be added to a .csproj and will not be present in the the bin or output directories.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C# scripts (.csx) are .NET, but they don't have a .csproj file. Will this exception exclude those users? I wonder if it's safer to just skip that step and write an informational trace if we can't find the .csproj file.

{
// If this is a dotnet function, we also need to add this file to the .csproj
// so that it copies to \bin\debug\netstandard2.x when the Function builds
string csProjFile = GetCSProjFilePath();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about .fsproj for F# developers?

// Create a local auth .json file that will be used by the middleware
var middlewareAuthSettingsFile = SecretsManager.MiddlewareAuthSettingsFileName;
MiddlewareAuthSettings = new AuthSettingsFile(middlewareAuthSettingsFile);
MiddlewareAuthSettings.SetAuthSetting("WEBSITE_AUTH_ALLOWED_AUDIENCES", serializedReplyUrls);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GitHub isn't letting me reply to our existing comment thread on this, but the portal behavior you observed is wrong, and a bug which can cause CRIs. The allowed_audiences should NOT contain /.auth/login/aad/callback.


var authSettingsToPublish = new Dictionary<string, string>
{
{ "allowedAudiences", serializedReplyUrls },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Duplicating my comment from elsewhere)

GitHub isn't letting me reply to our existing comment thread on this, but the portal behavior you observed is wrong, and a bug which can cause CRIs. The allowed_audiences should NOT contain /.auth/login/aad/callback.

@@ -41,5 +43,22 @@ public static string SanitizeImageName(this string imageName)

return cleanImageName.ToLowerInvariant().Substring(0, Math.Min(cleanImageName.Length, 128)).Trim();
}

public static string ComputeSha256Hash(string rawData)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Since this is a class intended to create extension methods, it's better to be consistent and make this method also an extension method:

Suggested change
public static string ComputeSha256Hash(string rawData)
public static string ComputeSha256Hash(this string rawData)

@glennamanns glennamanns changed the title WIP: Enable Easy Auth Local Dev Enable Easy Auth Local Dev Nov 13, 2018
@ahmelsayed ahmelsayed changed the base branch from master to dev January 23, 2019 01:15
: null;
return (new Uri($"{protocol}://0.0.0.0:{Port}"), new Uri($"{protocol}://localhost:{Port}"), cert);
if (UseHttps)
{
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo: revert this to the original logic

@jabbera
Copy link

jabbera commented Sep 5, 2019

Any chance of this making it any time soon? This breaks so many things. (Such as not being able to debug authenticated serverless Azure Functions locally at all)

@ahmelsayed
Copy link
Contributor

Closing old PR. Please reopen if this still needs to be looked at

@ahmelsayed ahmelsayed closed this Oct 15, 2019
@lock
Copy link

lock bot commented Dec 18, 2019

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@lock lock bot locked as resolved and limited conversation to collaborators Dec 18, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants