-
Notifications
You must be signed in to change notification settings - Fork 444
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
Conversation
} | ||
else | ||
{ | ||
throw new FileNotFoundException("Must have dotnet installed in order to use local authentication."); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this 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 |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
(var listenUri, var baseUri, var certificate, string certPath, string certPassword) = await Setup(); | ||
|
||
int originalPort = Port; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
@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 :) |
There was a problem hiding this 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; } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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>(); |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var rootPath = ScriptHostHelpers.GetFunctionAppRootDirectory(Environment.CurrentDirectory, new List<string> | |
var rootPath = ScriptHostHelpers.GetFunctionAppRootDirectory(Environment.CurrentDirectory, new [] | |
``` #Closed |
There was a problem hiding this comment.
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}"); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
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); |
There was a problem hiding this comment.
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 }, |
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
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
There was a problem hiding this 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>()); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.")] |
There was a problem hiding this comment.
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}.."); |
There was a problem hiding this comment.
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."); |
There was a problem hiding this comment.
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."); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 }, |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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:
public static string ComputeSha256Hash(string rawData) | |
public static string ComputeSha256Hash(this string rawData) |
: null; | ||
return (new Uri($"{protocol}://0.0.0.0:{Port}"), new Uri($"{protocol}://localhost:{Port}"), cert); | ||
if (UseHttps) | ||
{ |
There was a problem hiding this comment.
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
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) |
Closing old PR. Please reopen if this still needs to be looked at |
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. |
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:
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.
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: