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 overrides of token issuer from environment variables #41945

Merged
merged 26 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
26ad4f9
Support custom token issures
HarmanDhunna Feb 8, 2024
10fc3ba
v2
HarmanDhunna Feb 8, 2024
7651d40
Comments and clean up
HarmanDhunna Feb 13, 2024
f66245c
Clean up
HarmanDhunna Feb 13, 2024
504f0a9
Add token issuer versions to the trigger attribute
HarmanDhunna Feb 13, 2024
43be0d3
api updates
HarmanDhunna Feb 13, 2024
1ae2b25
Added tests and changed variable name to AuthenticationEvents__OIDCMe…
HarmanDhunna Feb 13, 2024
389c782
Reminder changes for var name change
HarmanDhunna Feb 13, 2024
0e95b4a
API Updates
HarmanDhunna Feb 13, 2024
cf8cce9
SpellCheck fix to OidcMetadataUrl
HarmanDhunna Feb 13, 2024
587b156
Updating cspell.json for oidc
HarmanDhunna Feb 14, 2024
58e3f7b
PR Recommendations
HarmanDhunna Feb 14, 2024
5df0b70
Updated tests
HarmanDhunna Feb 21, 2024
5657ef9
Merge branch 'main' of https://github.com/Azure/azure-sdk-for-net int…
HarmanDhunna Feb 21, 2024
a610a18
API Updates
HarmanDhunna Feb 21, 2024
386e001
Missed the file.
HarmanDhunna Feb 21, 2024
16a8875
Modified how token validation validates
HarmanDhunna Feb 22, 2024
d43091a
Changing sign to back to async
HarmanDhunna Feb 26, 2024
44d4d4b
Update API with new Attribute and remove the old one. Will be a break…
HarmanDhunna Feb 26, 2024
0f5da7d
checked in Test files
HarmanDhunna Feb 26, 2024
3a6bd32
Pull request review recommendations
HarmanDhunna Feb 26, 2024
78e8d82
Update ChangeLog
HarmanDhunna Feb 27, 2024
c173327
Added error Logging for invalid tokens.
HarmanDhunna Mar 2, 2024
5e0b207
Minor changes
HarmanDhunna Mar 2, 2024
bbc50f3
Missing a using statment
HarmanDhunna Mar 2, 2024
8dcc036
Switched to using IsUnsafeSupportLoggingEnabled
HarmanDhunna Mar 5, 2024
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 @@ -22,6 +22,7 @@ public partial class AuthenticationEventsTriggerAttribute : System.Attribute
{
public AuthenticationEventsTriggerAttribute() { }
public string AudienceAppId { get { throw null; } set { } }
public string AuthorityUrl { get { throw null; } set { } }
public string TenantId { get { throw null; } set { } }
}
public partial class AuthenticationEventWebJobsStartup : Microsoft.Azure.WebJobs.Hosting.IWebJobsStartup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,13 +219,13 @@ private Dictionary<string, object> GetBindingData(ValueBindingContext context, o
private async Task<Dictionary<string, string>> GetClaimsAndValidateRequest(HttpRequestMessage requestMessage)
{
ConfigurationManager configurationManager = new ConfigurationManager(_authEventTriggerAttr);
if (ConfigurationManager.BypassValidation)
if (configurationManager.BypassValidation)
{
return null;
}

TokenValidator validator = ConfigurationManager.EZAuthEnabled && requestMessage.Headers.Matches(ConfigurationManager.HEADER_EZAUTH_ICP, ConfigurationManager.HEADER_EZAUTH_ICP_VERIFY) ?
(TokenValidator)new TokenValidatorEZAuth() :
TokenValidator validator = configurationManager.EZAuthEnabled && requestMessage.Headers.Matches(ConfigurationManager.HEADER_EZAUTH_ICP, ConfigurationManager.HEADER_EZAUTH_ICP_VERIFY) ?
new TokenValidatorEZAuth() :
new TokenValidatorInternal();

(bool valid, Dictionary<string, string> claims) = await validator.GetClaimsAndValidate(requestMessage, configurationManager).ConfigureAwait(false);
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,11 @@
<data name="Ex_Token_Version" xml:space="preserve">
<value>Invalid token version {0}, supported versions are: {1}</value>
</data>
<data name="Ex_Trigger_Required_Attrs" xml:space="preserve">
<value>Please supply both the TenantId and AudienceAppId in variables in your binding configuration. (Or app settings {0} and {1})</value>
<data name="Ex_Trigger_ApplicationId_Required" xml:space="preserve">
<value>Please supply the ApplicationId {0} in variables in your binding configuration.</value>
</data>
<data name="Ex_Trigger_TenantId_Required" xml:space="preserve">
<value>Please supply the TenantId {0} in variables in your binding configuration.</value>
</data>
<data name="Log_EventHandler_Url" xml:space="preserve">
<value>Listener registered at: {0}</value>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,18 @@ public AuthenticationEventsTriggerAttribute()
}

/// <summary>Gets or sets the tenant identifier.</summary>
/// <value>The tenant identifier.</value>
/// <value>The tenant identifier only needed for workforce or AAD Tenant</value>
public string TenantId { get; set; }

/// <summary>Gets or sets the audience application identifier.</summary>
/// <value>The audience application identifier.</value>
public string AudienceAppId { get; set; }

/// <summary>
/// The authority is a URL that indicates the directory where the token came from
/// </summary>
public string AuthorityUrl { get; set; }

internal bool IsParameterString { get; set; } = true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,77 +2,194 @@
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents
{
/// <summary>
/// Configuration manager for loading up token validations.
/// </summary>
internal class ConfigurationManager
{
public static Dictionary<string, ServiceInfo> SERVICES = new Dictionary<string, ServiceInfo>()
{
{ "99045fe1-7639-4a75-9d4a-577b6ca3810f", new ServiceInfo("https://login.microsoftonline.com","https://sts.windows.net/{0}/","https://login.microsoftonline.com/{0}/v2.0"){DefaultService=true } } //Public cloud
};
private const string AzureActiveDirectoryAppId = "99045fe1-7639-4a75-9d4a-577b6ca3810f";
private const string AzureActiveDirectoryAuthority = "https://login.microsoftonline.com/common";

/// <summary>
/// Application Ids for the services to validate against.
/// </summary>
internal ServiceInfo ConfiguredService { get; private set; }

private const string EZAUTH_ENABLED = "WEBSITE_AUTH_ENABLED";
private const string BYPASS_VALIDATION_KEY = "AuthenticationEvents__BypassTokenValidation";

private const string AUTHORITY_URL = "AuthenticationEvents__AuthorityUrl";
private const string TENANT_ID_KEY = "AuthenticationEvents__TenantId";
private const string AUDIENCE_APPID_KEY = "AuthenticationEvents__AudienceAppId";

private const string BYPASS_VALIDATION = "AuthenticationEvents__BypassTokenValidation";
private const string CUSTOM_CALLER_APPID = "AuthenticationEvents__CustomCallerAppId";
internal const string TENANT_ID = "AuthenticationEvents__TenantId";
internal const string AUDIENCE_APPID = "AuthenticationEvents__AudienceAppId";
internal const string TOKEN_V1_VERIFY = "appid";
internal const string TOKEN_V2_VERIFY = "azp";
private const string EZAUTH_ENABLED = "WEBSITE_AUTH_ENABLED";

internal const string HEADER_EZAUTH_ICP = "X-MS-CLIENT-PRINCIPAL-IDP";
internal const string HEADER_EZAUTH_ICP_VERIFY = "aad";
internal const string HEADER_EZAUTH_PRINCIPAL = "X-MS-CLIENT-PRINCIPAL";

/// <summary>
/// Annotation for the trigger attribute.
/// </summary>
private readonly AuthenticationEventsTriggerAttribute triggerAttribute;

internal ConfigurationManager(AuthenticationEventsTriggerAttribute triggerAttribute)
{
this.triggerAttribute = triggerAttribute;

// if any of the values are missing, use the default AAD service info.
// Don't need to check tenant id or application id
// because they are required and will throw an exception if missing.
if (string.IsNullOrEmpty(AuthorityUrl))
{
// Continue to support the aad as the default service if overrides not provided.
ConfiguredService = GetAADServiceInfo(TenantId);
}
else
{
ConfiguredService = new ServiceInfo(
appId: AudienceAppId,
authorityUrl: AuthorityUrl);
}
}

internal static bool BypassValidation => GetConfigValue(BYPASS_VALIDATION, false);
internal static bool EZAuthEnabled => GetConfigValue(EZAUTH_ENABLED, false);
internal static string CallerAppId => GetConfigValue(CUSTOM_CALLER_APPID, null);
internal string TenantId => GetConfigValue(TENANT_ID, triggerAttribute.TenantId);
internal string AudienceAppId => GetConfigValue(AUDIENCE_APPID, triggerAttribute.AudienceAppId);
/// <summary>
/// Get the tenant id from the environment variable or use the default value from the trigger attribute.
/// </summary>
internal string TenantId
{
get
{
string value = GetConfigValue(TENANT_ID_KEY, triggerAttribute?.TenantId);

if (string.IsNullOrEmpty(value))
{
throw new MissingFieldException(
string.Format(
provider: CultureInfo.CurrentCulture,
format: AuthenticationEventResource.Ex_Trigger_TenantId_Required,
arg0: TENANT_ID_KEY));
}

internal static bool GetService(string serviceId, out ServiceInfo serviceInfo)
return value;
}
}

/// <summary>
/// Get the audience app id from the environment variable or use the default value from the trigger attribute.
/// </summary>
internal string AudienceAppId
{
serviceInfo = null;
if (serviceId is null)
get
{
throw new ArgumentNullException(nameof(serviceId));
string value = GetConfigValue(AUDIENCE_APPID_KEY, triggerAttribute?.AudienceAppId);

if (string.IsNullOrEmpty(value))
{
throw new MissingFieldException(
string.Format(
provider: CultureInfo.CurrentCulture,
format: AuthenticationEventResource.Ex_Trigger_ApplicationId_Required,
arg0: AUDIENCE_APPID_KEY));
}

return value;
}
}

if (CallerAppId != null && serviceId.Equals(CallerAppId))
/// <summary>
/// Get the OpenId connection host from the environment variable or use the default value.
/// </summary>
internal string AuthorityUrl => GetConfigValue(AUTHORITY_URL, triggerAttribute?.AuthorityUrl);

/// <summary>
/// If we should bypass the token validation.
/// Use only for testing and development.
/// </summary>
internal bool BypassValidation => GetConfigValue(BYPASS_VALIDATION_KEY, false);

/// <summary>
/// If the EZAuth is enabled.
/// </summary>
internal bool EZAuthEnabled => GetConfigValue(EZAUTH_ENABLED, false);

/// <summary>
/// Try to get the service info based on the service id.
/// </summary>
/// <param name="appId">The service id to look for.</param>
/// <param name="serviceInfo">The service info we found based on the service id.</param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
internal bool TryGetServiceByAppId(string appId, out ServiceInfo serviceInfo)
{
serviceInfo = null;

if (appId is null)
{
serviceInfo = SERVICES.Values.FirstOrDefault(x => x.DefaultService);
throw new ArgumentNullException(nameof(appId));
}
else if (SERVICES.ContainsKey(serviceId))

if (appId.Equals(ConfiguredService.ApplicationId, StringComparison.OrdinalIgnoreCase))
{
serviceInfo = SERVICES[serviceId];
serviceInfo = ConfiguredService;
return true;
}

return serviceInfo != null;
}

internal static bool VerifyServiceId(string testId)
/// <summary>
/// Verify if the service id is valid by checking if we have the service info for it.
/// </summary>
/// <param name="testId"></param>
/// <returns></returns>
internal bool VerifyServiceId(string testId)
{
return GetService(testId, out _);
return TryGetServiceByAppId(testId, out _);
}

/// <summary>
/// Get config value from environment variable or use the default value.
/// </summary>
/// <param name="environmentVariable">Definied Azure function application settings</param>
/// <param name="defaultValue">Default value, most likely from auth trigger anotation</param>
/// <returns></returns>
private static string GetConfigValue(string environmentVariable, string defaultValue)
{
return Environment.GetEnvironmentVariable(environmentVariable) ?? defaultValue;
}

private static T GetConfigValue<T>(string environmentVariable, T defaultValue) where T : struct
{
return Environment.GetEnvironmentVariable(environmentVariable) == null ?
string value = GetConfigValue(environmentVariable, null);

return value == null ?
defaultValue :
(T)Convert.ChangeType(Environment.GetEnvironmentVariable(environmentVariable), typeof(T), CultureInfo.CurrentCulture);
(T)Convert.ChangeType(
value: Environment.GetEnvironmentVariable(environmentVariable),
conversionType: typeof(T),
provider: CultureInfo.CurrentCulture);
}

private static ServiceInfo GetAADServiceInfo(string tenantId)
{
/* The authority URL is based on the tenant id. if we provide common, it returns a template version of the issure URL.
* But if we were to provide the actual tenant id, it will return the actual issuer URL with the tenant id in the issure string.
* Examples below using a random GUID: fa1f83dc-7b13-456e-8358-ba27aebd79ad
* https://login.windows.net/common/.well-known/openid-configuration : "https://sts.windows.net/{tenantid}/"
* https://login.windows.net/common/v2.0/.well-known/openid-configuration : "https://login.microsoftonline.com/{tenantid}/v2.0"
* https://login.windows.net/fa1f83dc-7b13-456e-8358-ba27aebd79ad/.well-known/openid-configuration : "https://sts.windows.net/fa1f83dc-7b13-456e-8358-ba27aebd79ad/"
* https://login.windows.net/fa1f83dc-7b13-456e-8358-ba27aebd79ad/v2.0/.well-known/openid-configuration : "https://login.microsoftonline.com/fa1f83dc-7b13-456e-8358-ba27aebd79ad/v2.0"
*/

return new ServiceInfo(
appId: AzureActiveDirectoryAppId,
authorityUrl: AzureActiveDirectoryAuthority + "/" + tenantId);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,42 @@

namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents
{
/// <summary>
/// The services we support and their configuration.
/// </summary>
internal class ServiceInfo
{
internal string OpenIdConnectionHost { get; set; }
internal string TokenIssuerV1 { get; set; }
internal string TokenIssuerV2 { get; set; }
internal bool DefaultService { get; set; }
public ServiceInfo(string openIdConnectionHost, string tokenIssuerV1, string tokenIssuerV2)
private const string OpenIdConfigurationPath = "/.well-known/openid-configuration";
private const string OpenIdConfigurationPathV2 = "/v2.0/.well-known/openid-configuration";

public ServiceInfo(
string appId,
string authorityUrl)
{
ApplicationId = appId;
Authority = authorityUrl;
}

/// <summary>
/// The Application Id of the custom extension. This is the audience of the token.
/// </summary>
internal string ApplicationId { get; private set; }

/// <summary>
/// The authority is a URL that indicates a directory that the tokens from.
/// </summary>
internal string Authority { get; private set; }

/// <summary>
/// Get the issuer string based on the token schema version.
/// </summary>
/// <param name="tokenSchemaVersion">v2 will return v2 odic url, v1 will return v1</param>
/// <returns></returns>
internal string GetOpenIDConfigurationUrlString(SupportedTokenSchemaVersions tokenSchemaVersion)
{
OpenIdConnectionHost = openIdConnectionHost;
TokenIssuerV1 = tokenIssuerV1;
TokenIssuerV2 = tokenIssuerV2;
return tokenSchemaVersion == SupportedTokenSchemaVersions.V2_0 ?
this.Authority + OpenIdConfigurationPathV2 :
this.Authority + OpenIdConfigurationPath;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents
internal enum SupportedTokenSchemaVersions
{
/// <summary>Version 1.</summary>
[Description("1.0")] V1_0,
[Description("1.0")]
V1_0,

/// <summary>Version 2.</summary>
[Description("2.0")] V2_0
[Description("2.0")]
V2_0
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents
{
internal class TokenValidatorEZAuth : TokenValidator
{
internal override Task<(bool Valid, Dictionary<string, string> Claims)> GetClaimsAndValidate(HttpRequestMessage request, ConfigurationManager configurationManager)
internal override Task<(bool Valid, Dictionary<string, string> Claims)> GetClaimsAndValidate(
HttpRequestMessage request,
ConfigurationManager configurationManager)
{
Dictionary<string, string> Claims = new Dictionary<string, string>();
try
Expand All @@ -26,7 +28,7 @@ internal class TokenValidatorEZAuth : TokenValidator
SupportedTokenSchemaVersions tokenSchemaVersion = TokenValidatorHelper.ParseSupportedTokenVersion(Claims["ver"]);

return Task.FromResult((Claims.Any(x => x.Key.Equals(tokenSchemaVersion == SupportedTokenSchemaVersions.V2_0 ? ConfigurationManager.TOKEN_V2_VERIFY : ConfigurationManager.TOKEN_V1_VERIFY) &&
ConfigurationManager.VerifyServiceId(x.Value)), Claims));
configurationManager.VerifyServiceId(x.Value)), Claims));
}
catch (Exception)
{
Expand Down
Loading