diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/CHANGELOG.md b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/CHANGELOG.md index 2cfda098a132..3c708d665a01 100644 --- a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/CHANGELOG.md +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/CHANGELOG.md @@ -2,11 +2,19 @@ ## 1.0.0-beta.4 (Unreleased) +### Features + +- Added metrics to header. Will be used to track the number of requests and responses for each action. + ### Bugs Fixed -- Updated ODataType signature -- Empty or null response actions will throw a bad response -- Made the source field in the request a required field +- Updated ODataType signature. This was done to make sure the contracts were consistent across all services. +- Empty or null response actions will throw a bad response. There should be at lease one action passed as this is the main purpose of the SDK library. +- Made the source field in the request a required field. +- Made the request validation errors return 500. This way, we can identify that 500 errors as internal and should be marked as failures whereas response object errors should return 400s since they are customer input errors and should be identified as CallerErrors in our service. +- Made the ODataType field in the request a required field. +- Made the errors for JSON payload more descriptive when an invalid character is passed. +- Added JsonDocument TryParse check to validate payload is JSON format. ## 1.0.0-beta.3 (2022-12-13) @@ -17,7 +25,7 @@ - Added createdDateTime to AuthenticationEventContextUser - Added new request status type for validation failure. - Validation Errors raise 500 response. -- Added CustomAuthenticaionExtensionId to Data. +- Added CustomAuthenticationExtensionId to Data. - Removed AuthenticationEventsId from Data. ## 1.0.0-beta.2 (2022-11-08) diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/api/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.netstandard2.0.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/api/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.netstandard2.0.cs index 2a98cfb7b289..6eed4186c11e 100644 --- a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/api/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.netstandard2.0.cs +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/api/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.netstandard2.0.cs @@ -8,7 +8,7 @@ internal AuthenticationEventMetadataAttribute() { } } public partial class AuthenticationEventResponseHandler : Microsoft.Azure.WebJobs.Host.Bindings.IValueBinder, Microsoft.Azure.WebJobs.Host.Bindings.IValueProvider { - public AuthenticationEventResponseHandler() { } + internal AuthenticationEventResponseHandler() { } public Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.AuthenticationEventRequestBase Request { get { throw null; } } public Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.AuthenticationEventResponse Response { get { throw null; } } public System.Type Type { get { throw null; } } @@ -34,6 +34,16 @@ public enum EventDefinition [Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.AuthenticationEventMetadataAttribute(typeof(Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.TokenIssuanceStartRequest), "microsoft.graph.authenticationEvent.TokenIssuanceStart", "TokenIssuanceStart", "CloudEventActionableTemplate.json")] TokenIssuanceStart = 0, } + public sealed partial class EventTriggerMetrics + { + internal EventTriggerMetrics() { } + public const string MetricsHeader = "User-Agent"; + public const string ProductName = "AuthenticationEvents"; + public static string Framework { get { throw null; } } + public static Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.EventTriggerMetrics Instance { get { throw null; } } + public static string Platform { get { throw null; } } + public static string ProductVersion { get { throw null; } } + } public enum EventType { OnTokenIssuanceStart = 0, @@ -45,11 +55,6 @@ public enum RequestStatusType Successful = 2, ValidationError = 3, } - public partial class ResponseValidationException : System.Exception - { - public ResponseValidationException(string message) { } - public ResponseValidationException(string message, System.Exception innerException) { } - } } namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework { @@ -128,6 +133,7 @@ protected CloudEventData() { } public abstract partial class CloudEventRequest : Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.AuthenticationEventRequest where TResponse : Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.AuthenticationEventResponse, new() where TData : Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.CloudEventData { internal CloudEventRequest() { } + [System.ComponentModel.DataAnnotations.RequiredAttribute] [System.Text.Json.Serialization.JsonPropertyNameAttribute("oDataType")] public string ODataType { get { throw null; } set { } } [System.ComponentModel.DataAnnotations.RequiredAttribute] diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventBinding.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventBinding.cs index d9859e2c84a8..ebe0ab09d84b 100644 --- a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventBinding.cs +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventBinding.cs @@ -1,11 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework; -using Microsoft.Azure.WebJobs.Host.Bindings; -using Microsoft.Azure.WebJobs.Host.Listeners; -using Microsoft.Azure.WebJobs.Host.Protocols; -using Microsoft.Azure.WebJobs.Host.Triggers; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -14,8 +9,14 @@ using System.Linq; using System.Net.Http; using System.Reflection; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework; +using Microsoft.Azure.WebJobs.Host.Bindings; +using Microsoft.Azure.WebJobs.Host.Listeners; +using Microsoft.Azure.WebJobs.Host.Protocols; +using Microsoft.Azure.WebJobs.Host.Triggers; using static Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.EmptyResponse; using AuthenticationEventMetadata = Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.AuthenticationEventMetadata; @@ -84,7 +85,8 @@ private static IReadOnlyDictionary GetBindingDataContract(Paramete public async Task BindAsync(object value, ValueBindingContext context) { var request = (HttpRequestMessage)value; - AuthenticationEventResponseHandler eventResponseHandler = (AuthenticationEventResponseHandler)request.Properties[AuthenticationEventResponseHandler.EventResponseProperty]; + AuthenticationEventResponseHandler eventResponseHandler = + (AuthenticationEventResponseHandler)request.Properties[AuthenticationEventResponseHandler.EventResponseProperty]; try { if (request == null) @@ -97,7 +99,15 @@ public async Task BindAsync(object value, ValueBindingContext cont AuthenticationEventMetadata eventMetadata = GetEventAndValidateSchema(payload); eventResponseHandler.Request = GetRequestForEvent(request, payload, eventMetadata, Claims); - return new TriggerData(new AuthenticationEventValueBinder(eventResponseHandler.Request, _authEventTriggerAttr), GetBindingData(context, value, eventResponseHandler)) + + return new TriggerData( + new AuthenticationEventValueBinder( + eventResponseHandler.Request, + _authEventTriggerAttr), + GetBindingData( + context, + value, + eventResponseHandler)) { ReturnValueProvider = eventResponseHandler }; @@ -118,7 +128,12 @@ public async Task BindAsync(object value, ValueBindingContext cont /// A TriggerData Object with the failed event request based on the event. With the related request status set. /// /// - private TriggerData GetFaultyRequest(ValueBindingContext context, object value, HttpRequestMessage request, AuthenticationEventResponseHandler eventResponseHandler, Exception ex) + private TriggerData GetFaultyRequest( + ValueBindingContext context, + object value, + HttpRequestMessage request, + AuthenticationEventResponseHandler eventResponseHandler, + Exception ex) { eventResponseHandler.Request = _parameterInfo.ParameterType == typeof(string) ? new EmptyRequest(request) : AuthenticationEventMetadata.CreateEventRequest(request, _parameterInfo.ParameterType, null); eventResponseHandler.Request.StatusMessage = ex.Message; @@ -127,6 +142,7 @@ private TriggerData GetFaultyRequest(ValueBindingContext context, object value, { UnauthorizedAccessException => RequestStatusType.TokenInvalid, ValidationException => RequestStatusType.ValidationError, + AuthenticationEventTriggerRequestValidationException => RequestStatusType.ValidationError, _ => RequestStatusType.Failed, }; @@ -228,11 +244,15 @@ private async Task> GetClaimsAndValidateRequest(HttpR /// The Event Metadata object. /// Aggregates all the schema validation exceptions. /// IF the event cannot be determined or if the object model event differs from the requested event on the incoming payload. - private static AuthenticationEventMetadata GetEventAndValidateSchema(string body) + internal static AuthenticationEventMetadata GetEventAndValidateSchema(string body) { - if (!Helpers.IsJson(body)) + try + { + Helpers.ValidateJson(body); + } + catch (JsonException ex) { - throw new InvalidDataException(); + throw new AuthenticationEventTriggerRequestValidationException($"{AuthenticationEventResource.Ex_Invalid_JsonPayload}: {ex.Message}", ex.InnerException); } return AuthenticationEventMetadataLoader.GetEventMetadata(body); diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventConfigProvider.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventConfigProvider.cs index e7970734574f..5e2cdcd287da 100644 --- a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventConfigProvider.cs +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventConfigProvider.cs @@ -117,7 +117,10 @@ public async Task ConvertAsync(HttpRequestMessage input, Ca }; FunctionResult result = await listener.Value.FunctionExecutor.TryExecuteAsync(triggerData, cancellationToken).ConfigureAwait(false); - return result.Succeeded ? (HttpResponseMessage)eventsResponseHandler.Response : Helpers.HttpErrorResponse(result.Exception); + + return result.Succeeded ? + eventsResponseHandler.Response : + Helpers.HttpErrorResponse(result.Exception); } catch (Exception ex) { diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventResource.Designer.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventResource.Designer.cs index 576d9024e647..4a1cbc426aad 100644 --- a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventResource.Designer.cs +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventResource.Designer.cs @@ -79,6 +79,15 @@ internal static string Ex_Context_Null { } } + /// + /// Looks up a localized string similar to JSON is null or empty.. + /// + internal static string Ex_Empty_Json { + get { + return ResourceManager.GetString("Ex_Empty_Json", resourceCulture); + } + } + /// /// Looks up a localized string similar to Cannot determine the event from payload, please check that the incoming payload is a valid JSON string and contains the event type.. /// @@ -170,11 +179,11 @@ internal static string Ex_Invalid_JsonPath { } /// - /// Looks up a localized string similar to Invalid Payload detected.. + /// Looks up a localized string similar to Invalid Json Payload. /// - internal static string Ex_Invalid_Payload { + internal static string Ex_Invalid_JsonPayload { get { - return ResourceManager.GetString("Ex_Invalid_Payload", resourceCulture); + return ResourceManager.GetString("Ex_Invalid_JsonPayload", resourceCulture); } } @@ -188,7 +197,7 @@ internal static string Ex_Invalid_Response { } /// - /// Looks up a localized string similar to Return type is invalid, please return either an AuthEventResponse, HttpResponse, HttpResponseMessage or string in your function return.. + /// Looks up a localized string similar to Return type is invalid, please return either an AuthEventResponse, HttpResponse, HttpResponseMessage or string in your function return. /// internal static string Ex_Invalid_Return { get { @@ -269,7 +278,7 @@ internal static string Ex_No_Lock { } /// - /// Looks up a localized string similar to Actions can not contain null items. + /// Looks up a localized string similar to Actions can not contain null items.. /// internal static string Ex_Null_Action_Items { get { diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventResource.resx b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventResource.resx index 4e0a56a844f3..31fe2ad8dbd4 100644 --- a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventResource.resx +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventResource.resx @@ -123,6 +123,9 @@ ListenerFactoryContext is null + + JSON is null or empty. + Cannot determine the event from payload, please check that the incoming payload is a valid JSON string and contains the event type. @@ -153,14 +156,14 @@ Cannot find json path set value - - Invalid Payload detected. + + Invalid Json Payload Response validation failed, see inner exceptions. - Return type is invalid, please return either an AuthEventResponse, HttpResponse, HttpResponseMessage or string in your function return. + Return type is invalid, please return either an AuthEventResponse, HttpResponse, HttpResponseMessage or string in your function return Invalid version on Schema diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventResponseHandler.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventResponseHandler.cs index 043dbc2b1eb0..e008f2e60e62 100644 --- a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventResponseHandler.cs +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventResponseHandler.cs @@ -1,12 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using Microsoft.AspNetCore.Http; -using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework; -using Microsoft.Azure.WebJobs.Host.Bindings; using System; using System.Buffers; -using System.ComponentModel.DataAnnotations; using System.Globalization; using System.IO; using System.Net.Http; @@ -14,6 +10,9 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework; +using Microsoft.Azure.WebJobs.Host.Bindings; namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents { @@ -24,9 +23,26 @@ public class AuthenticationEventResponseHandler : IValueBinder /// The response property. internal const string EventResponseProperty = "$event$response"; + private AuthenticationEventResponse _response; + /// Gets or sets the action result. /// The action result. - public AuthenticationEventResponse Response { get; internal set; } + public AuthenticationEventResponse Response + { + get => _response; + private set + { + if (value != null) + { + _response = value; + + // Set metrics on the headers for the response + EventTriggerMetrics.Instance.SetMetricHeaders(_response); + } + } + } + + internal AuthenticationEventResponseHandler() { } /// Gets the type. /// The type. @@ -57,7 +73,7 @@ public Task SetValueAsync(object result, CancellationToken cancellationToken) { if (result == null) { - throw new ResponseValidationException(AuthenticationEventResource.Ex_Invalid_Return); + throw new AuthenticationEventTriggerResponseValidationException(AuthenticationEventResource.Ex_Invalid_Return); } if (result is AuthenticationEventResponse action) @@ -69,7 +85,7 @@ public Task SetValueAsync(object result, CancellationToken cancellationToken) AuthenticationEventResponse response = Request.GetResponseObject(); if (response == null) { - throw new InvalidOperationException(AuthenticationEventResource.Ex_Missing_Request_Response); + throw new AuthenticationEventTriggerRequestValidationException(AuthenticationEventResource.Ex_Missing_Request_Response); } Response = GetActionResult(result, response); @@ -209,9 +225,16 @@ internal static AuthenticationEventJsonElement GetJsonObjectFromStream(Stream st internal static AuthenticationEventJsonElement GetJsonObjectFromString(string result) { - return !Helpers.IsJson(result) - ? throw new InvalidCastException(AuthenticationEventResource.Ex_Invalid_Return) - : new AuthenticationEventJsonElement(result); + try + { + Helpers.ValidateJson(result); + } + catch (JsonException ex) + { + throw new AuthenticationEventTriggerResponseValidationException($"{AuthenticationEventResource.Ex_Invalid_Return}: {ex.Message}", ex.InnerException); + } + + return new AuthenticationEventJsonElement(result); } internal static AuthenticationEventResponse GetAuthEventFromJObject(AuthenticationEventJsonElement result, AuthenticationEventResponse response) diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Common/EventTriggerMetrics.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Common/EventTriggerMetrics.cs new file mode 100644 index 000000000000..792ee8bf04ff --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Common/EventTriggerMetrics.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + /// + /// Static class to set the metric headers for each event trigger. + /// + public sealed class EventTriggerMetrics + { + /// + /// Default constructor for eventmetrics + /// + private EventTriggerMetrics() + { + var assembly = AssemblyName.GetAssemblyName("Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.dll"); + + ProductVersion = assembly.Version.ToString(); + Framework = RuntimeInformation.FrameworkDescription; + Platform = RuntimeInformation.OSDescription ?? "unknown"; + } + + /// + /// Lazy immplementation to make sure that only one instance is created and returned, while delaying the creation till needed. + /// + private static readonly Lazy lazyEventTrigger = new Lazy(() => new EventTriggerMetrics()); + + /// + /// The singleton instance for event trigger metrics + /// + public static EventTriggerMetrics Instance + { + get + { + return lazyEventTrigger.Value; + } + } + + /// + /// The client library's product name + /// + public const string ProductName = "AuthenticationEvents"; + + /// + /// Get the platform of the event trigger based on the OS. + /// + /// OS Name + public static string Platform { get; private set; } + + /// + /// Product version of the event trigger. Example: 1.0.0-beta, 1.0.0, 2.0.0 + /// + public static string ProductVersion { get; private set; } + + /// + /// Framework of the event trigger. Example: .NET, JS, TS, PY + /// + public static string Framework { get; private set; } + + /// + /// Header key to add the metrics to. + /// User-Agent is the standard header to add metrics to recommended by Azure sdk guidelines. + /// + public const string MetricsHeader = "User-Agent"; + + /// + /// Set the metrics on the response message. + /// + /// The reponse message + internal void SetMetricHeaders(HttpResponseMessage message) + { + if (message != null) + { + var headers = message.Headers; + AddOrReplaceToHeader(headers, GetHeaderValueFormatted(Platform, ProductVersion, Framework)); + } + } + + /// + /// Extension function to Add or Replace a header value. + /// + /// to add the key and value to + /// Header value to add + private static void AddOrReplaceToHeader(HttpHeaders headers, string value) + { + if (headers.Contains(MetricsHeader)) + { + value = headers.GetValues(MetricsHeader).First() + " " + value; + headers.Remove(MetricsHeader); + } + + headers.Add(MetricsHeader, value); + } + + private static string GetHeaderValueFormatted(string platform, string version, string runtime) + { + return $"azsdk-net-{ProductName}/{version} ({runtime}; {platform.Trim()})"; + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Common/Exceptions/AuthenticationEventTriggerException.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Common/Exceptions/AuthenticationEventTriggerException.cs new file mode 100644 index 000000000000..3dfcedb6463a --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Common/Exceptions/AuthenticationEventTriggerException.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Net; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + /// + /// Root exception for AuthenticationEventTrigger. + /// + internal abstract class AuthenticationEventTriggerException : Exception + { + /// + /// Status code when exception is thrown. + /// + public abstract HttpStatusCode ExceptionStatusCode { get; } + + /// + /// Initializes a new instance of the class. + /// + /// + public AuthenticationEventTriggerException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// + public AuthenticationEventTriggerException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Common/Exceptions/AuthenticationEventTriggerRequestValidationException.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Common/Exceptions/AuthenticationEventTriggerRequestValidationException.cs new file mode 100644 index 000000000000..9c38731484e2 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Common/Exceptions/AuthenticationEventTriggerRequestValidationException.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Net; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + /// + /// Exception class for request validations. + /// + internal class AuthenticationEventTriggerRequestValidationException : AuthenticationEventTriggerValidationException + { + /// + public override HttpStatusCode ExceptionStatusCode => HttpStatusCode.InternalServerError; + + /// + /// Initializes a new instance of the class. + /// + /// + public AuthenticationEventTriggerRequestValidationException(string message) + : base(message) { } + + /// + /// Initializes a new instance of the class. + /// + /// + /// + public AuthenticationEventTriggerRequestValidationException(string message, Exception innerException) + : base(message, innerException) { } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Common/Exceptions/AuthenticationEventTriggerResponseValidationException.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Common/Exceptions/AuthenticationEventTriggerResponseValidationException.cs new file mode 100644 index 000000000000..3edb614caa9f --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Common/Exceptions/AuthenticationEventTriggerResponseValidationException.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Net; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + /// + /// Exception class for response validations. + /// + internal class AuthenticationEventTriggerResponseValidationException : AuthenticationEventTriggerValidationException + { + /// + public override HttpStatusCode ExceptionStatusCode => HttpStatusCode.BadRequest; + + /// + /// Initializes a new instance of the class. + /// + /// + public AuthenticationEventTriggerResponseValidationException(string message) + : base(message) { } + + /// + /// Initializes a new instance of the class. + /// + /// + /// + public AuthenticationEventTriggerResponseValidationException(string message, Exception innerException) + : base(message, innerException) { } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Common/Exceptions/AuthenticationEventTriggerValidationException.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Common/Exceptions/AuthenticationEventTriggerValidationException.cs new file mode 100644 index 000000000000..45d73ac9d6e9 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Common/Exceptions/AuthenticationEventTriggerValidationException.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + /// + /// Root exception for AuthenticationEventTriggerValidation. + /// + internal abstract class AuthenticationEventTriggerValidationException : AuthenticationEventTriggerException + { + /// + /// Initializes a new instance of the class. + /// + /// asdfasd + public AuthenticationEventTriggerValidationException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// + public AuthenticationEventTriggerValidationException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Common/Exceptions/ResponseValidationException.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Common/Exceptions/ResponseValidationException.cs deleted file mode 100644 index b567483e68a7..000000000000 --- a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Common/Exceptions/ResponseValidationException.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents -{ - /// - /// Exception class for response validations - /// - public class ResponseValidationException : Exception - { - /// - /// Initializes a new instance of the class. - /// - /// - public ResponseValidationException(string message) - : base(message) { } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public ResponseValidationException(string message, Exception innerException) - : base(message, innerException) { } - } -} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventMetadata.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventMetadata.cs index 73045caaa86d..c508b5c332a1 100644 --- a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventMetadata.cs +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventMetadata.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Net.Http; using System.Reflection; @@ -71,7 +72,16 @@ internal AuthenticationEventRequestBase CreateEventRequest(HttpRequestMessage re if (validate) { - Helpers.ValidateGraph(eventRequest); + // Validate the request body. If the validation fails, throw a request validation + // exception that will return a 500 error back to eSTS. + try + { + Helpers.ValidateGraph(eventRequest); + } + catch (ValidationException exception) + { + throw new AuthenticationEventTriggerRequestValidationException(exception.Message, exception.InnerException); + } } responseInfo.SetValue(eventRequest, eventResponse); diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventRequest.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventRequest.cs index 8285222f53b2..5178d5f7c9ae 100644 --- a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventRequest.cs +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventRequest.cs @@ -40,13 +40,9 @@ public async override Task Completed() { try { - if (RequestStatus == RequestStatusType.Failed) + if (RequestStatus == RequestStatusType.Failed || RequestStatus == RequestStatusType.ValidationError) { - Response.MarkAsFailed(new Exception(String.IsNullOrEmpty(StatusMessage) ? AuthenticationEventResource.Ex_Gen_Failure : StatusMessage), true); - } - else if (RequestStatus == RequestStatusType.ValidationError) - { - Response.MarkAsFailed(new Exception(String.IsNullOrEmpty(StatusMessage) ? AuthenticationEventResource.Ex_Gen_Failure : StatusMessage), false); + Response.MarkAsFailed(new Exception(string.IsNullOrEmpty(StatusMessage) ? AuthenticationEventResource.Ex_Gen_Failure : StatusMessage), true); } else if (RequestStatus == RequestStatusType.TokenInvalid) { diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/CloudEventRequest.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/CloudEventRequest.cs index eed463b54727..f6b2ba969dcc 100644 --- a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/CloudEventRequest.cs +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/CloudEventRequest.cs @@ -25,6 +25,7 @@ public abstract class CloudEventRequest : AuthenticationEventR /// Data type of cloud event. [JsonPropertyName("oDataType")] [OneOf("microsoft.graph.onTokenIssuanceStartCalloutData", "")] + [Required] public string ODataType { get; set; } = string.Empty; /// Initializes a new instance of the class. diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Helpers.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Helpers.cs index 79ec49e915fe..6b68464acbf5 100644 --- a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Helpers.cs +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Helpers.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework; -using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Actions; using System; using System.Collections; using System.Collections.Generic; @@ -11,6 +9,9 @@ using System.Linq; using System.Net.Http; using System.Text; +using System.Text.Json; +using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework; +using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Actions; namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents { @@ -46,16 +47,21 @@ internal static EventDefinition GetEventDefintionFromPayload(string payload) } catch (Exception ex) { - throw new InvalidOperationException(AuthenticationEventResource.Ex_Event_Missing, ex); + throw new AuthenticationEventTriggerRequestValidationException(ex.Message, ex.InnerException); } } internal static HttpResponseMessage HttpErrorResponse(Exception ex) { - return new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest) + var response = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest) { Content = new StringContent(GetFailedRequestPayload(ex)) }; + + // Set the metrics on header + EventTriggerMetrics.Instance.SetMetricHeaders(response); + + return response; } internal static string GetFailedRequestPayload(Exception ex) @@ -102,18 +108,15 @@ internal static HttpResponseMessage HttpJsonResponse(AuthenticationEventJsonElem /// Determines whether the specified input is json. /// The input. - /// - /// true if the specified input is json; otherwise, false. - internal static bool IsJson(string input) + internal static void ValidateJson(string input) { if (string.IsNullOrEmpty(input)) { - return false; + throw new JsonException(AuthenticationEventResource.Ex_Empty_Json); } - input = input.Trim(); - return (input.StartsWith("{", StringComparison.OrdinalIgnoreCase) && input.EndsWith("}", StringComparison.OrdinalIgnoreCase)) - || (input.StartsWith("[", StringComparison.OrdinalIgnoreCase) && input.EndsWith("]", StringComparison.OrdinalIgnoreCase)); + // try parsing input to json object + using var _ = JsonDocument.Parse(input); } internal static AuthenticationEventAction GetEventActionForActionType(string actionType) diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/AuthenticationEventBindingTests.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/AuthenticationEventBindingTests.cs new file mode 100644 index 000000000000..230d580678c6 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/AuthenticationEventBindingTests.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using NUnit.Framework; +using Payload = Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests.Payloads.TokenIssuanceStart; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests +{ + [TestFixture] + public class AuthenticationEventBindingTests + { + [Test] + [TestCaseSource(nameof(TestPayloadScenarios))] + public void TestRequestJsonPayload(object eventPayload, string message, bool success, string exceptionMessage) + { + string payload = eventPayload.ToString(); + if (success == false) + { + var ex = Assert.Throws(() => AuthenticationEventBinding.GetEventAndValidateSchema(payload)); + Assert.AreEqual(exceptionMessage, ex.Message); + } + else + { + Assert.DoesNotThrow(() => AuthenticationEventBinding.GetEventAndValidateSchema(payload)); + } + } + + private static IEnumerable TestPayloadScenarios() + { +#region Invalid + yield return new TestCaseStructure() + { + Test = string.Empty, + Message = "Testing request without payload throws an error", + ExceptionMessage = "Invalid Json Payload: JSON is null or empty." + }.ToArray; + yield return new TestCaseStructure() + { + Test = Payload.TokenIssuanceStart.RequestWithXmlBody, + Message = "Testing request with XML payload throws an error", + ExceptionMessage = "Invalid Json Payload: '<' is an invalid start of a value. LineNumber: 0 | BytePositionInLine: 0." + }.ToArray; +#endregion + +#region Valid + yield return new TestCaseStructure() + { + Test = Payload.TokenIssuanceStart.ValidRequestPayload, + Message = "Testing valid full request payload", + Success = true, + }.ToArray; +#endregion + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Common/EventTriggerMetricsTests.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Common/EventTriggerMetricsTests.cs new file mode 100644 index 000000000000..551c68ecf629 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Common/EventTriggerMetricsTests.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Diagnostics.Eventing.Reader; +using System.Linq; +using System.Net.Http; + +using NUnit.Framework; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests +{ + [TestFixture] + public class EventTriggerMetricsTests + { + [Test] + [Description ("Verify it does not throw if response message is null")] + public void TestSetMetricHeadersNull() + { + HttpResponseMessage message = null; + Assert.DoesNotThrow(() => EventTriggerMetrics.Instance.SetMetricHeaders(message)); + Assert.IsNull( + anObject: message, + message: "Verify AuthenticationEventRequestBase is not set to anything when null."); + } + + [Test] + [Description("Verify it sets the headers to the correct default values")] + public void TestSetMetricHeaders() + { + HttpResponseMessage message = new() { }; + EventTriggerMetrics.Instance.SetMetricHeaders(message); + + Assert.IsNotEmpty(EventTriggerMetrics.Framework, "Framework should note be empty"); + Assert.IsNotEmpty(EventTriggerMetrics.ProductVersion, "ProductVersion should not be empty"); + Assert.IsNotEmpty(EventTriggerMetrics.Platform, "Platform should not be empty"); + + var headers = message.Headers; + Assert.IsTrue(headers.Contains(EventTriggerMetrics.MetricsHeader)); + + string headerValue = headers.GetValues(EventTriggerMetrics.MetricsHeader).First(); + Assert.IsNotEmpty(headerValue, "Header value should not be empty or null"); + } + + [Test] + [Description("Verify if sets the headers is in the correct format")] + public void TestSetMetricFormat() + { + HttpResponseMessage message = new() { }; + EventTriggerMetrics.Instance.SetMetricHeaders(message); + + var headers = message.Headers; + Assert.IsTrue(headers.Contains(EventTriggerMetrics.MetricsHeader)); + + string headerValue = headers.GetValues(EventTriggerMetrics.MetricsHeader).First(); + + Assert.AreEqual(GetTestHeaderValue(), headerValue, "Verify format of header values matches"); + } + + [Test] + [Description("Verify it sets the headers to the correct default values when there is already a value")] + public void TestAppendMetricHeaders() + { + HttpResponseMessage message = new() { }; + message.Headers.Add(EventTriggerMetrics.MetricsHeader, "test"); + + EventTriggerMetrics.Instance.SetMetricHeaders(message); + + var headers = message.Headers; + Assert.IsTrue(headers.Contains(EventTriggerMetrics.MetricsHeader)); + + string headerValue = headers.GetValues(EventTriggerMetrics.MetricsHeader).First(); + Assert.AreEqual("test " + GetTestHeaderValue(), headerValue, "Verify default header values match"); + } + + private static string GetTestHeaderValue( + string framework = null, + string version = null, + string platform = null) + { + framework ??= EventTriggerMetrics.Framework; + version ??= EventTriggerMetrics.ProductVersion; + platform ??= EventTriggerMetrics.Platform; + + return $"azsdk-net-{EventTriggerMetrics.ProductName}/{version} ({framework}; {platform.Trim()})"; + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/ConfigProviderTests.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/ConfigProviderTests.cs index b58d209f80f0..f4b326068ffe 100644 --- a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/ConfigProviderTests.cs +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/ConfigProviderTests.cs @@ -1,6 +1,6 @@ -using System; using System.Net; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using static Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests.TestHelper; @@ -34,7 +34,7 @@ public async Task PostConfigProviderTests(string url, HttpStatusCode httpStatusC { AuthenticationEventResponseHandler eventsResponseHandler = GetAuthenticationEventResponseHandler(mockedRequest); - eventsResponseHandler.Response = GetContentForHttpStatus(httpStatusCode); + eventsResponseHandler.SetValueAsync(GetContentForHttpStatus(httpStatusCode), CancellationToken.None); } }); diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Framework/AuthenticationEventDataTests.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Framework/AuthenticationEventDataTests.cs index 43e67c0fcac0..b321b69ef3fc 100644 --- a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Framework/AuthenticationEventDataTests.cs +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Framework/AuthenticationEventDataTests.cs @@ -1,7 +1,10 @@ -using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License.using System; + +using System; +using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework; using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Data; using Newtonsoft.Json.Linq; -using System; using NUnit.Framework; namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests.Framework @@ -33,15 +36,15 @@ public static string BuildDataString( if (hasTenantIdKey) { - dataObj["tenantId"] = hasTenantIdValue ? TenantId : ""; + dataObj["tenantId"] = hasTenantIdValue ? TenantId : string.Empty; } if (hasAuthenticationEventListenerIdKey) { - dataObj["authenticationEventListenerId"] = hasAuthenticationEventListenerIdValue ? AuthenticationEventListenerId : ""; + dataObj["authenticationEventListenerId"] = hasAuthenticationEventListenerIdValue ? AuthenticationEventListenerId : string.Empty; } if (hasCustomAuthenticationExtensionIdKey) { - dataObj["customAuthenticationExtensionId"] = hasCustomAuthenticationExtensionIdValue ? CustomAuthenticationExtensionId : ""; + dataObj["customAuthenticationExtensionId"] = hasCustomAuthenticationExtensionIdValue ? CustomAuthenticationExtensionId : string.Empty; } return obj.ToString(); diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Framework/AuthenticationEventMetadataTests.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Framework/AuthenticationEventMetadataTests.cs index 217fb16fc211..16a090a53c22 100644 --- a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Framework/AuthenticationEventMetadataTests.cs +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Framework/AuthenticationEventMetadataTests.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; using System.Net.Http; using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework; using NUnit.Framework; @@ -14,17 +13,32 @@ namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests.Framewor public class AuthenticationEventMetadataTests { [Test] - [TestCaseSource(nameof(TestScenarios))] - [Ignore("Test needs to be refactored to remove the secret, even though non-impactful.")] - public void TestRequestCreateInstance(object testObject, string message, bool success, string exceptionMessage) + [TestCaseSource(nameof(TestJsonPayloadScenarios))] + public void TestRequestPayloadStructureInstance(object testObject, string message, bool success, string exceptionMessage) + { + string payload = testObject.ToString(); + if (success == false) + { + var ex = Assert.Throws(() => AuthenticationEventMetadataLoader.GetEventMetadata(payload)); + Assert.AreEqual(exceptionMessage, ex.Message); + } + else + { + Assert.DoesNotThrow(() => AuthenticationEventMetadataLoader.GetEventMetadata(payload)); + } + } + + [Test] + [TestCaseSource(nameof(TestAttributeScenarios))] + public void TestRequestAttributesCreateInstance(object testObject, string message, bool success, string exceptionMessage) { string payload = testObject.ToString(); AuthenticationEventMetadata eventMetadata = AuthenticationEventMetadataLoader.GetEventMetadata(payload); - HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post,"<< REDACTED: FIX ME >>"); + HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post,"https://localhost.net/"); if (success == false) { - var ex = Assert.Throws(() => eventMetadata.CreateEventRequestValidate(requestMessage, payload, string.Empty)); + var ex = Assert.Throws(() => eventMetadata.CreateEventRequestValidate(requestMessage, payload, string.Empty)); Assert.AreEqual(exceptionMessage, ex.Message); } else @@ -33,7 +47,28 @@ public void TestRequestCreateInstance(object testObject, string message, bool su } } - private static IEnumerable TestScenarios() + private static IEnumerable TestJsonPayloadScenarios() + { +#region Invalid + yield return new TestCaseStructure() + { + Test = Payload.TokenIssuanceStart.RequestWithInvalidCharacter, + Message = "Testing request payload with invalid character passed and verifies it throws an error", + ExceptionMessage = "The JSON object contains a trailing comma at the end which is not supported in this mode. Change the reader options. LineNumber: 38 | BytePositionInLine: 6." + }.ToArray; +#endregion + +#region Valid + yield return new TestCaseStructure() + { + Test = Payload.TokenIssuanceStart.ValidRequestPayload, + Message = "Testing valid full request payload", + Success = true, + }.ToArray; +#endregion + } + + private static IEnumerable TestAttributeScenarios() { #region Invalid yield return new TestCaseStructure() @@ -42,6 +77,12 @@ private static IEnumerable TestScenarios() Message = "Testing request payload without source field passed and verifies it throws an error", ExceptionMessage = "TokenIssuanceStartRequest: The Source field is required." }.ToArray; + yield return new TestCaseStructure() + { + Test = Payload.TokenIssuanceStart.RequestWithoutODataTypePayload, + Message = "Testing request payload without ODataType field passed and verifies it throws an error", + ExceptionMessage = "TokenIssuanceStartRequest: The ODataType field is required." + }.ToArray; #endregion #region Valid diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Framework/Validators/EnumerableItemsNotNullAttributeTests.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Framework/Validators/EnumerableItemsNotNullAttributeTests.cs index 98b3ab100e27..183fb0f8389e 100644 --- a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Framework/Validators/EnumerableItemsNotNullAttributeTests.cs +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Framework/Validators/EnumerableItemsNotNullAttributeTests.cs @@ -75,7 +75,7 @@ private static IEnumerable TestScenarios() }.ToArray; #endregion - #region Valid +#region Valid yield return new TestCaseStructure() { Test = new List() { new(), new() }, diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests.csproj b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests.csproj index ba453f21d54f..fc4df7fa5f27 100644 --- a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests.csproj +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests.csproj @@ -8,8 +8,11 @@ + + + @@ -21,7 +24,10 @@ + + + @@ -29,15 +35,11 @@ - - - - diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/PayloadTests.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/PayloadTests.cs index d015017052da..f742fe2d0f4c 100644 --- a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/PayloadTests.cs +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/PayloadTests.cs @@ -64,7 +64,7 @@ public async Task Tests(TestTypes testType) case TestTypes.NoAction: return (Payloads.TokenIssuanceStart.TokenIssuanceStart.NoActionResponse, @"{'errors':['No Actions Found. Please supply atleast one action.']}", HttpStatusCode.InternalServerError); case TestTypes.Empty: - return (string.Empty, @"{'errors':['Return type is invalid, please return either an AuthEventResponse, HttpResponse, HttpResponseMessage or string in your function return.']}", HttpStatusCode.InternalServerError); + return (string.Empty, @"{'errors':['Return type is invalid, please return either an AuthEventResponse, HttpResponse, HttpResponseMessage or string in your function return: JSON is null or empty.']}", HttpStatusCode.InternalServerError); case TestTypes.ValidCloudEvent: return (Payloads.TokenIssuanceStart.TokenIssuanceStart.ActionResponse, Payloads.TokenIssuanceStart.TokenIssuanceStart.ExpectedPayload, HttpStatusCode.OK); default: diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/InvalidJsonRequest.json b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/InvalidJsonRequest.json new file mode 100644 index 000000000000..0b314fe2d704 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/InvalidJsonRequest.json @@ -0,0 +1,42 @@ +{ + "type": "microsoft.graph.authenticationEvent.tokenIssuanceStart", + "source": "/tenants/00000000-0000-0000-0000-000000000002/applications/00000000-0000-0000-0000-000000000001", + "data": { + "@odata.type": "microsoft.graph.onTokenIssuanceStartCalloutData", + "tenantId": "00000000-0000-0000-0000-000000000002", + "authenticationEventListenerId": "00000000-0000-0000-0000-000000000003", + "customAuthenticationExtensionId": "00000000-0000-0000-0000-000000000004", + "authenticationContext": { + "correlationId": "00000000-0000-0000-0000-000000000005", + "client": { + "ip": "127.0.0.1", + "locale": "en-us", + "market": "en-us" + }, + "protocol": "OAUTH2.0", + "clientServicePrincipal": { + "id": "00000000-0000-0000-0000-000000000005", + "appId": "00000000-0000-0000-0000-000000000001", + "appDisplayName": "My Test application", + "displayName": "My Test application" + }, + "resourceServicePrincipal": { + "id": "00000000-0000-0000-0000-000000000006", + "appId": "00000000-0000-0000-0000-000000000001", + "appDisplayName": "My Test application", + "displayName": "My Test application" + }, + "user": { + "createdDateTime": "2016-03-01T15:23:40Z", + "displayName": "Bob", + "givenName": "Bob Smith", + "id": "00000000-0000-0000-0000-000000000007", + "mail": "bob@contoso.com", + "preferredLanguage": "en-us", + "surname": "Smith", + "userPrincipalName": "bob@contoso.com", + "userType": "Member", + } + } + } +} \ No newline at end of file diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/InvalidXmlRequest.xml b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/InvalidXmlRequest.xml new file mode 100644 index 000000000000..7f58c9c87e6f --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/InvalidXmlRequest.xml @@ -0,0 +1,45 @@ + + + microsoft.graph.authenticationEvent.tokenIssuanceStart + /tenants/00000000-0000-0000-0000-000000000002/applications/00000000-0000-0000-0000-000000000001 + + + microsoft.graph.onTokenIssuanceStartCalloutData + + 00000000-0000-0000-0000-000000000002 + 00000000-0000-0000-0000-000000000003 + 00000000-0000-0000-0000-000000000004 + + 00000000-0000-0000-0000-000000000005 + + 127.0.0.1 + en-us + en-us + + OAUTH2.0 + + 00000000-0000-0000-0000-000000000005 + 00000000-0000-0000-0000-000000000001 + My Test application + My Test application + + + 00000000-0000-0000-0000-000000000006 + 00000000-0000-0000-0000-000000000001 + My Test application + My Test application + + + 2016-03-01T15:23:40Z + Bob + Bob Smith + 00000000-0000-0000-0000-000000000007 + bob@contoso.com + en-us + Smith + bob@contoso.com + Member + + + + \ No newline at end of file diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/NullResponse.json b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/NullResponse.json index bfa3fc136c4a..485c54513fcb 100644 --- a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/NullResponse.json +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/NullResponse.json @@ -1,3 +1,3 @@ { - "errors": [ "Return type is invalid, please return either an AuthEventResponse, HttpResponse, HttpResponseMessage or string in your function return." ] + "errors": [ "Return type is invalid, please return either an AuthEventResponse, HttpResponse, HttpResponseMessage or string in your function return" ] } diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/RequestMissingODataType.json b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/RequestMissingODataType.json new file mode 100644 index 000000000000..07ae195b13ac --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/RequestMissingODataType.json @@ -0,0 +1,41 @@ +{ + "type": "microsoft.graph.authenticationEvent.tokenIssuanceStart", + "source": "/tenants/00000000-0000-0000-0000-000000000002/applications/00000000-0000-0000-0000-000000000001", + "data": { + "tenantId": "00000000-0000-0000-0000-000000000002", + "authenticationEventListenerId": "00000000-0000-0000-0000-000000000003", + "customAuthenticationExtensionId": "00000000-0000-0000-0000-000000000004", + "authenticationContext": { + "correlationId": "00000000-0000-0000-0000-000000000005", + "client": { + "ip": "127.0.0.1", + "locale": "en-us", + "market": "en-us" + }, + "protocol": "OAUTH2.0", + "clientServicePrincipal": { + "id": "00000000-0000-0000-0000-000000000005", + "appId": "00000000-0000-0000-0000-000000000001", + "appDisplayName": "My Test application", + "displayName": "My Test application" + }, + "resourceServicePrincipal": { + "id": "00000000-0000-0000-0000-000000000006", + "appId": "00000000-0000-0000-0000-000000000001", + "appDisplayName": "My Test application", + "displayName": "My Test application" + }, + "user": { + "createdDateTime": "2016-03-01T15:23:40Z", + "displayName": "Bob", + "givenName": "Bob Smith", + "id": "00000000-0000-0000-0000-000000000007", + "mail": "bob@contoso.com", + "preferredLanguage": "en-us", + "surname": "Smith", + "userPrincipalName": "bob@contoso.com", + "userType": "Member" + } + } + } +} \ No newline at end of file diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/RequestMissingSource.json b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/RequestMissingSource.json index ed0aaf506f36..8461cb4923a4 100644 --- a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/RequestMissingSource.json +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/RequestMissingSource.json @@ -2,26 +2,26 @@ "type": "microsoft.graph.authenticationEvent.tokenIssuanceStart", "data": { "@odata.type": "microsoft.graph.onTokenIssuanceStartCalloutData", - "tenantId": "d33b1c3f-49c2-8cb3-963b-ca195de1e704", - "authenticationEventListenerId": "6fd9cb25-ff72-304b-7b27-5fd793aa3c2f", - "customAuthenticationExtensionId": "ce1b2217-fdf9-a19f-2f46-a639514a4107", + "tenantId": "00000000-0000-0000-0000-000000000002", + "authenticationEventListenerId": "00000000-0000-0000-0000-000000000003", + "customAuthenticationExtensionId": "00000000-0000-0000-0000-000000000004", "authenticationContext": { - "correlationId": "1dc14bea-414b-a99e-64de-b4702d82ab59", + "correlationId": "00000000-0000-0000-0000-000000000005", "client": { - "ip": "30.51.176.110", + "ip": "127.0.0.1", "locale": "en-us", "market": "en-us" }, "protocol": "OAUTH2.0", "clientServicePrincipal": { - "id": "1dc14bea-414b-a99e-64de-b4702d82ab59", - "appId": "d56ef0c3-a4e8-b26c-484c-8873b560eab3", + "id": "00000000-0000-0000-0000-000000000005", + "appId": "00000000-0000-0000-0000-000000000001", "appDisplayName": "My Test application", "displayName": "My Test application" }, "resourceServicePrincipal": { - "id": "8de396f3-6559-a65c-398e-43a2fc6a2141", - "appId": "d56ef0c3-a4e8-b26c-484c-8873b560eab3", + "id": "00000000-0000-0000-0000-000000000006", + "appId": "00000000-0000-0000-0000-000000000001", "appDisplayName": "My Test application", "displayName": "My Test application" }, @@ -29,7 +29,7 @@ "createdDateTime": "2016-03-01T15:23:40Z", "displayName": "Bob", "givenName": "Bob Smith", - "id": "90847c2a-e29d-4d2f-9f54-c5b4d3f26471", + "id": "00000000-0000-0000-0000-000000000007", "mail": "bob@contoso.com", "preferredLanguage": "en-us", "surname": "Smith", diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/TokenIssuanceStart.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/TokenIssuanceStart.cs index be598d80dfc6..7e6bcaadf298 100644 --- a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/TokenIssuanceStart.cs +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/TokenIssuanceStart.cs @@ -1,4 +1,7 @@ -namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests.Payloads.TokenIssuanceStart +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests.Payloads.TokenIssuanceStart { /// Test data for the Token Issuance Start public static class TokenIssuanceStart @@ -88,5 +91,32 @@ public static string RequestWithoutSourcePayload return PayloadHelper.GetPayload("TokenIssuanceStart.RequestMissingSource.json"); } } + + /// Gets the request payload without ODataType field + public static string RequestWithoutODataTypePayload + { + get + { + return PayloadHelper.GetPayload("TokenIssuanceStart.RequestMissingODataType.json"); + } + } + + /// Gets the request payload with invalid json structure + public static string RequestWithInvalidCharacter + { + get + { + return PayloadHelper.GetPayload("TokenIssuanceStart.InvalidJsonRequest.json"); + } + } + + /// Gets the request payload with XML structure + public static string RequestWithXmlBody + { + get + { + return PayloadHelper.GetPayload("TokenIssuanceStart.InvalidXmlRequest.xml"); + } + } } } diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/ValidRequest.json b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/ValidRequest.json index 7b1017ea53c1..31d0c3214109 100644 --- a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/ValidRequest.json +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/ValidRequest.json @@ -1,28 +1,28 @@ { "type": "microsoft.graph.authenticationEvent.tokenIssuanceStart", - "source": "/tenants/d33b1c3f-49c2-8cb3-963b-ca195de1e704/applications/d56ef0c3-a4e8-b26c-484c-8873b560eab3", + "source": "/tenants/00000000-0000-0000-0000-000000000002/applications/00000000-0000-0000-0000-000000000001", "data": { "@odata.type": "microsoft.graph.onTokenIssuanceStartCalloutData", - "tenantId": "d33b1c3f-49c2-8cb3-963b-ca195de1e704", - "authenticationEventListenerId": "6fd9cb25-ff72-304b-7b27-5fd793aa3c2f", - "customAuthenticationExtensionId": "ce1b2217-fdf9-a19f-2f46-a639514a4107", + "tenantId": "00000000-0000-0000-0000-000000000002", + "authenticationEventListenerId": "00000000-0000-0000-0000-000000000003", + "customAuthenticationExtensionId": "00000000-0000-0000-0000-000000000004", "authenticationContext": { - "correlationId": "1dc14bea-414b-a99e-64de-b4702d82ab59", + "correlationId": "00000000-0000-0000-0000-000000000005", "client": { - "ip": "30.51.176.110", + "ip": "127.0.0.1", "locale": "en-us", "market": "en-us" }, "protocol": "OAUTH2.0", "clientServicePrincipal": { - "id": "1dc14bea-414b-a99e-64de-b4702d82ab59", - "appId": "d56ef0c3-a4e8-b26c-484c-8873b560eab3", + "id": "00000000-0000-0000-0000-000000000005", + "appId": "00000000-0000-0000-0000-000000000001", "appDisplayName": "My Test application", "displayName": "My Test application" }, "resourceServicePrincipal": { - "id": "8de396f3-6559-a65c-398e-43a2fc6a2141", - "appId": "d56ef0c3-a4e8-b26c-484c-8873b560eab3", + "id": "00000000-0000-0000-0000-000000000006", + "appId": "00000000-0000-0000-0000-000000000001", "appDisplayName": "My Test application", "displayName": "My Test application" }, @@ -30,7 +30,7 @@ "createdDateTime": "2016-03-01T15:23:40Z", "displayName": "Bob", "givenName": "Bob Smith", - "id": "90847c2a-e29d-4d2f-9f54-c5b4d3f26471", + "id": "00000000-0000-0000-0000-000000000007", "mail": "bob@contoso.com", "preferredLanguage": "en-us", "surname": "Smith", diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/ResponseTypesTests.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/ResponseTypesTests.cs index e752b0ef7a00..3bca59bf1117 100644 --- a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/ResponseTypesTests.cs +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/ResponseTypesTests.cs @@ -98,7 +98,7 @@ private object GetResponseTypeObject(ResponseTypes responseType, StreamWriter st case ResponseTypes.AuthEventResponse: return (code: HttpStatusCode.OK, Payloads.TokenIssuanceStart.TokenIssuanceStart.ActionResponse); case ResponseTypes.Unknown: - return (code: HttpStatusCode.InternalServerError, @"{'errors':['Return type is invalid, please return either an AuthEventResponse, HttpResponse, HttpResponseMessage or string in your function return.']}"); + return (code: HttpStatusCode.InternalServerError, @"{'errors':['Return type is invalid, please return either an AuthEventResponse, HttpResponse, HttpResponseMessage or string in your function return']}"); default: return (code: HttpStatusCode.BadRequest, string.Empty); }; diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/TestHelper.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/TestHelper.cs index ecd995382739..c926df58cfd5 100644 --- a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/TestHelper.cs +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/TestHelper.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License.using System; + +using System; using System.IO; using System.Linq; using System.Net; @@ -205,7 +208,6 @@ internal static AuthenticationEventsTriggerAttribute CreateAuthenticationEventTr /// A newly create AuthenticationEventTriggerAttribute public static AuthenticationEventsTriggerAttribute CreateAuthenticationEventTriggerAttribute(string tenantId, string audienceAppId) { - return new AuthenticationEventsTriggerAttribute() { TenantId = tenantId, @@ -267,7 +269,6 @@ public static TAttribute GetAttribute(this Enum value) where TAttrib /// A newly created TokenIssuanceStartResponse for version preview_10_01_2021 public static TokenIssuanceStartResponse CreateTokenIssuanceStartResponse() { - JObject jBody = JObject.Parse(ReadResource(MainAssembly, String.Join(".", DefaultNamespace, "Templates", "CloudEventActionableTemplate.json"))); (jBody["data"]["@odata.type"] as JValue).Value = "microsoft.graph.onTokenIssuanceStartResponseData";