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
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@

namespace Azure.Functions.Cli.Actions.AzureActions
glennamanns marked this conversation as resolved.
Show resolved Hide resolved
{
// Invoke via `func azure auth create-aad --appRegistrationName {yourAppRegistrationName} --appName {yourAppName}`
[Action(Name = "create-aad", Context = Context.Azure, SubContext = Context.Auth, HelpText = "Creates an Azure Active Directory registration. Can be linked to an Azure App Service or Function app.")]
// 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
Member

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.

class CreateAADApplication : BaseAzureAction
glennamanns marked this conversation as resolved.
Show resolved Hide resolved
{
private readonly IAuthManager _authManager;
private readonly ISecretsManager _secretsManager;

public string AADName { get; set; }
public string AADAppRegistrationName { get; set; }

public string AppName { get; set; }

Expand All @@ -29,15 +29,15 @@ public override async Task RunAsync()
{
var workerRuntime = WorkerRuntimeLanguageHelper.GetCurrentWorkerRuntimeLanguage(_secretsManager);

await _authManager.CreateAADApplication(AccessToken, AADName, workerRuntime, AppName);
await _authManager.CreateAADApplication(AccessToken, AADAppRegistrationName, workerRuntime, AppName);
}

public override ICommandLineParserResult ParseArgs(string[] args)
{
Parser
.Setup<string>("appRegistrationName")
.Setup<string>("AADAppRegistrationName")
.WithDescription("Name of the Azure Active Directory app registration to create.")
.Callback(f => AADName = f);
.Callback(f => AADAppRegistrationName = f);

Parser
.Setup<string>("appName")
Expand Down
69 changes: 34 additions & 35 deletions src/Azure.Functions.Cli/Common/AuthManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,41 +30,45 @@ public AuthManager() { }

private WorkerRuntime _workerRuntime;

public async Task CreateAADApplication(string accessToken, string AADName, WorkerRuntime workerRuntime, string appName)
private static string _requiredResourceFilename = "requiredResourceAccessList.txt";

public async Task CreateAADApplication(string accessToken, string AADAppRegistrationName, WorkerRuntime workerRuntime, string appName)
{
if (string.IsNullOrEmpty(AADName))
if (string.IsNullOrEmpty(AADAppRegistrationName))
{
throw new CliArgumentsException("Must specify name of new Azure Active Directory registration with --appRegistrationName parameter.",
new CliArgument { Name = "appRegistrationName", Description = "Name of new Azure Active Directory registration" });
throw new CliArgumentsException("Must provide name for a new Azure Active Directory app registration with --AADAppRegistrationName parameter.",
new CliArgument { Name = "AADAppRegistrationName", Description = "Name of new Azure Active Directory app registration" });
}

if (CommandChecker.CommandExists("az"))
{
_workerRuntime = workerRuntime;
_requiredResourceFilename = string.Format("{0}-{1}", AADAppRegistrationName, _requiredResourceFilename);

List<string> replyUrls;
Copy link
Member

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
Member

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.

string tempFile, clientSecret, hostName, query = CreateQuery(AADName, appName, out tempFile, out clientSecret, out replyUrls, out hostName);
string clientSecret, hostName, commandLineArgs = GetCommandLineArguments(AADAppRegistrationName, appName, out clientSecret, out replyUrls, out hostName);

ColoredConsole.WriteLine("Query successfully constructed. Creating new Azure AD Application now..");
string command = $"ad app create {commandLineArgs}";
ColoredConsole.WriteLine($"Creating new Azure AD Application via:\n" +
$"az {command}");

var az = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? new Executable("cmd", $"/c az ad app create {query}")
: new Executable("az", $"ad app create {query}");
? new Executable("cmd", $"/c az {command}")
: new Executable("az", command);


var stdout = new StringBuilder();
var stderr = new StringBuilder();

int exitCode = await az.RunAsync(o => stdout.AppendLine(o), e => stderr.AppendLine(e));

// Delete temporary file created to pass data in proper format to az CLI
File.Delete($"{tempFile}");

if (exitCode != 0)
{
throw new CliException(stderr.ToString().Trim(' ', '\n', '\r', '"'));
}

string response = stdout.ToString().Trim(' ', '\n', '\r', '"');
ColoredConsole.WriteLine(Green($"Successfully created new AAD registration {AADName}"));
ColoredConsole.WriteLine(Green($"Successfully created new AAD registration {AADAppRegistrationName}"));
ColoredConsole.WriteLine(White(response));

JObject application = JObject.Parse(response);
Expand All @@ -75,7 +79,7 @@ public async Task CreateAADApplication(string accessToken, string AADName, Worke
if (appName == null)
{
// Update function application's (local) auth settings
CreateAndCommitAuthSettings(hostName, clientId, clientSecret, tenantId, replyUrls);
CreateAndCommitAuthSettings(hostName, clientId, clientSecret, tenantId, replyUrls);
}
else
{
Expand All @@ -88,7 +92,7 @@ public async Task CreateAADApplication(string accessToken, string AADName, Worke

await PublishAuthSettingAsync(connectedSite, accessToken, authSettingsToPublish);

ColoredConsole.WriteLine(Green($"Successfully updated {appName}'s auth settings to reference new AAD registration {AADName}"));
ColoredConsole.WriteLine(Green($"Successfully updated {appName}'s auth settings to reference new AAD app registration {AADAppRegistrationName}"));
}
}
else
Expand All @@ -98,20 +102,20 @@ public async Task CreateAADApplication(string accessToken, string AADName, Worke
}

/// <summary>
/// Create the query to send to az ad app create
/// Create the string of arguments to send to az ad app create
/// </summary>
/// <param name="AADName">Name of the new AAD application</param>
/// <param name="AADAppRegistrationName">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)
private string GetCommandLineArguments(string AADAppRegistrationName, string appName, out string clientSecret, out List<string> replyUrls, out string hostName)
{
clientSecret = GeneratePassword(128);
string authCallback = "/.auth/login/aad/callback";

var requiredResourceAccess = GetRequiredResourceAccesses();

// It is easiest to pass them in the right format to the az CLI via a (temp) file + filename
tempFile = $"{Guid.NewGuid()}.txt";
File.WriteAllText(tempFile, JsonConvert.SerializeObject(requiredResourceAccess));
File.WriteAllText(_requiredResourceFilename, JsonConvert.SerializeObject(requiredResourceAccess));

// Based on whether or not this AAD application is to be used in production or a local environment,
// these parameters are different (plus reply URLs):
Expand All @@ -122,7 +126,7 @@ private string CreateQuery(string AADName, string appName, out string tempFile,
{
// OAuth is port sensitive. There is no way of using a wildcard in the reply URLs to allow for variable ports
// Set the port in the reply URLs to the default used by the Functions Host
identifierUrl = "http://" + AADName + ".localhost";
identifierUrl = "http://" + AADAppRegistrationName + ".localhost";
homepage = "http://localhost:" + StartHostAction.DefaultPort;
hostName = "localhost:" + StartHostAction.DefaultPort;
string localhostSSL = "https://localhost:" + StartHostAction.DefaultPort + authCallback;
Expand All @@ -149,8 +153,8 @@ private string CreateQuery(string AADName, string appName, out string tempFile,

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

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.


return $"--display-name {AADName} --homepage {homepage} --identifier-uris {identifierUrl} --password {clientSecret}" +
$" --reply-urls {serializedReplyUrls} --oauth2-allow-implicit-flow true --required-resource-access @{tempFile}";
return $"--display-name {AADAppRegistrationName} --homepage {homepage} --identifier-uris {identifierUrl} --password {clientSecret}" +
$" --reply-urls {serializedReplyUrls} --oauth2-allow-implicit-flow true --required-resource-access @{_requiredResourceFilename}";

}

Expand Down Expand Up @@ -213,7 +217,7 @@ private List<requiredResourceAccess> GetRequiredResourceAccesses()
private void CreateAndCommitAuthSettings(string hostName, string clientId, string clientSecret, string tenant, List<string> replyUrls)
{
// The WEBSITE_AUTH_ALLOWED_AUDIENCES setting is of the form "{replyURL1} {replyURL2}"
string serializedReplyUrls = string.Join(" ", replyUrls.ToArray<string>());
string serializedReplyUrls = string.Join(" ", replyUrls);

// Create a local auth .json file that will be used by the middleware
var middlewareAuthSettingsFile = SecretsManager.MiddlewareAuthSettingsFileName;
Expand Down Expand Up @@ -265,24 +269,19 @@ public static string GetCSProjFilePath()
}

// If we're in the function root\bin\debug\netstandard2.x, the .csproj file will be up three directories
try
var functionDir = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, @"..\..\..\"));

if (Directory.Exists(functionDir))
{
var functionDir = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, @"..\..\..\"));
var functionDirProjFiles = FileSystemHelpers.GetFiles(functionDir, searchPattern: "*.csproj").ToList();

if (functionDirProjFiles.Count == 1)
{
return functionDirProjFiles.First();
}
else
{
return null;
}
}
catch (DirectoryNotFoundException e)
{
return null;
}

return null;
}

/// <summary>
Expand Down Expand Up @@ -319,11 +318,11 @@ private static void ModifyCSProj(string csProj)
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);
string serializedReplyUrls = JsonConvert.SerializeObject(replyUrls, Formatting.Indented);

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

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.

{ "isAadAutoProvisioned", "True" },
{ "clientId", clientId },
{ "clientSecret", clientSecret },
Expand Down
50 changes: 39 additions & 11 deletions src/Azure.Functions.Cli/Common/AuthSettingsFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,32 @@ class AuthSettingsFile

public AuthSettingsFile(string filePath)
{
_filePath = filePath;
try
_filePath = filePath ?? throw new CliException("Received null value for auth settings filename.");

string path = Path.Combine(Environment.CurrentDirectory, _filePath);

if (File.Exists(path))
{
string path = Path.Combine(Environment.CurrentDirectory, _filePath);
var content = FileSystemHelpers.ReadAllTextFromFile(path);
var authSettings = JObject.Parse(content);
Values = new Dictionary<string, string>(authSettings.ToObject<Dictionary<string, string>>(), StringComparer.OrdinalIgnoreCase);
try
{
var content = FileSystemHelpers.ReadAllTextFromFile(path);
var authSettings = JObject.Parse(content);
Values = new Dictionary<string, string>(authSettings.ToObject<Dictionary<string, string>>(), StringComparer.OrdinalIgnoreCase);
}
catch (UnauthorizedAccessException unauthorizedAccess)
{
throw new CliException(unauthorizedAccess.Message);
}
catch (JsonReaderException jsonError)
{
throw new CliException(jsonError.Message);
}
catch (Exception generic)
{
throw new CliException(generic.ToString());
}
}
catch
else
{
Values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
IsEncrypted = false;
Expand Down Expand Up @@ -55,7 +72,18 @@ public void RemoveSetting(string name)

public void Commit()
{
FileSystemHelpers.WriteAllTextToFile(_filePath, JsonConvert.SerializeObject(this.GetValues(), Formatting.Indented));
try
{
FileSystemHelpers.WriteAllTextToFile(_filePath, JsonConvert.SerializeObject(this.GetValues(), Formatting.Indented));
}
catch (UnauthorizedAccessException unauthorizedAccess)
{
throw new CliException(unauthorizedAccess.Message);
}
catch (Exception generic)
{
throw new CliException(generic.ToString());
}
}

public IDictionary<string, string> GetValues()
Expand All @@ -64,8 +92,8 @@ public IDictionary<string, string> GetValues()
{
try
{
return Values.ToDictionary(k => k.Key, v => string.IsNullOrEmpty((string)v.Value) ? string.Empty :
Encoding.Default.GetString(ProtectedData.Unprotect(Convert.FromBase64String((string)v.Value), reason)));
return Values.ToDictionary(k => k.Key, v => string.IsNullOrEmpty(v.Value) ? string.Empty :
Encoding.Default.GetString(ProtectedData.Unprotect(Convert.FromBase64String(v.Value), reason)));
}
catch (Exception e)
{
Expand All @@ -74,7 +102,7 @@ public IDictionary<string, string> GetValues()
}
else
{
return Values.ToDictionary(k => k.Key, v => (string)v.Value);
return Values.ToDictionary(k => k.Key, v => v.Value);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Azure.Functions.Cli/Common/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public static class ResourceAccessTypes
{
MicrosoftGraphReadPermissions.FilesRead,
MicrosoftGraphReadPermissions.MailRead,
MicrosoftGraphReadPermissions.UserRead // TODO !! check if this is the case
MicrosoftGraphReadPermissions.UserRead
}
},
{ ("outlook", BindingDirection.In), new List<Guid>
Expand Down
2 changes: 1 addition & 1 deletion src/Azure.Functions.Cli/Common/SecretsManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public static string MiddlewareAuthSettingsFilePath
get
{
var authFile = "local.middleware.json";
var rootPath = ScriptHostHelpers.GetFunctionAppRootDirectory(Environment.CurrentDirectory, new List<string>
var rootPath = ScriptHostHelpers.GetFunctionAppRootDirectory(Environment.CurrentDirectory, new []
{
ScriptConstants.HostMetadataFileName,
authFile,
Expand Down
7 changes: 0 additions & 7 deletions src/Azure.Functions.Cli/Common/Utilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,5 @@ internal static bool EqualsIgnoreCaseAndSpace(string str, string another)
{
return str.Replace(" ", string.Empty).Equals(another.Replace(" ", string.Empty), StringComparison.OrdinalIgnoreCase);
}

public static Uri SetPort(this Uri uri, int newPort)
{
var builder = new UriBuilder(uri);
builder.Port = newPort;
return builder.Uri;
}
}
}
3 changes: 2 additions & 1 deletion src/Azure.Functions.Cli/Helpers/AzureHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ namespace Azure.Functions.Cli.Helpers
public static class AzureHelper
{
private static string _storageApiVersion = "2018-02-01";
private static string _authSettingsApiVersion = "2018-02-01";

public static async Task<Site> GetFunctionApp(string name, string accessToken)
{
Expand Down Expand Up @@ -252,7 +253,7 @@ public static async Task<HttpResult<Dictionary<string, string>, string>> UpdateF

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}");
var url = new Uri($"{ArmUriTemplates.ArmUrl}{site.SiteId}/config/authsettings?api-version={_authSettingsApiVersion}");
var response = await ArmClient.HttpInvoke(HttpMethod.Put, url, accessToken, new { properties = site.AzureAuthSettings });
if (response.IsSuccessStatusCode)
{
Expand Down