diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/CHANGELOG.md b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/CHANGELOG.md new file mode 100644 index 000000000000..e089e9e4a3d3 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/CHANGELOG.md @@ -0,0 +1,12 @@ +# Release History + +## 1.0.0-beta.1 (Unreleased) +- The initial beta release + +### Features Added + +### Breaking Changes + +### Bugs Fixed + +### Other Changes \ No newline at end of file diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/Directory.Build.props b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/Directory.Build.props new file mode 100644 index 000000000000..ead8f407d87d --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/Directory.Build.props @@ -0,0 +1,13 @@ + + + true + + + + true + false + + + + + \ No newline at end of file diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/README.md b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/README.md new file mode 100644 index 000000000000..02b377edaf46 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/README.md @@ -0,0 +1,289 @@ +# Authentication Events Trigger for Azure Functions client library for .NET + +Authentication Event Trigger for Azure Functions handles all the backend processing, (e.g. token/json schema validation) for incoming Http requests for Authentication events. And provides the developer with a strongly typed, versioned object model to work with, meaning the developer need not have any prior knowledge of the request and response json payloads. + +This project framework provides the following features: + +* Token validation for securing the API call +* Object model, typing, and IDE intellisense +* Inbound and outbound validation of the API request and response schemas +* Versioning +* No need for boilerplate code. + +## Getting started + +### Install the package + +```dotnetcli +dotnet add package Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents --prerelease +``` + +### Prerequisites + +* [Visual Studio 2019](https://visualstudio.microsoft.com/vs/) for Windows **OR** [Visual Studio Code >= 1.61](https://code.visualstudio.com/download) +* [Dotnet core 3.1](https://dotnet.microsoft.com/download/dotnet/3.1) +* [Azure function tools 3.30](https://github.com/Azure/azure-functions-core-tools) +* [Nuget](https://docs.microsoft.com/nuget/install-nuget-client-tools) +* [Azure Function Core Tools](https://github.com/Azure/azure-functions-core-tools#installing) +* If using Visual Studio Code the following extensions: + * [ms-azuretools.vscode-azurefunctions](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions) + * [ms-dotnettools.csharp](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) +* For private preview register a Nuget source either using Nuget''s cli **OR** Visual Studio's Nuget Package Manager. + * Generate a GIT Personal Access Token. Go to [https://github.com/settings/tokens](https://github.com/settings/tokens) + * When generating the token, it needs to have the following permissions/scope: **read:package** + * Authorize **Azure** to use the Personal Access Token. (::Only if you are a member of the Azure Organization on GitHub::) + + * Nuget CLI (**Recommended**): + * **GIT-USERNAME**: Your GIT username that you log into GIT with. + * **GIT-PERSONAL-ACCESS-TOKEN**: Your GIT Personal access token (see below reference on how to generate one) + + ```shell + nuget sources add -Name "Azure" -Source "https://nuget.pkg.github.com/Azure/index.json" -username "**[GIT-USERNAME]**" -password "**[GIT-PERSONAL-ACCESS-TOKEN]**" + ``` + + * More details can be found here + * [Nuget Source](https://docs.microsoft.com/nuget/reference/cli-reference/cli-ref-sources) + * [How to Generate a GIT Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) + * Visual Studio's Nuget Package Manager + * You'll be prompted for credentials when accessing the source, so keep you Personal Access Token handy as this would be your password. + +### Authenticate the Client + +When Azure AD authentication events service calls your custom extension, it will send an `Authorization` header with a `Bearer {token}`. This token will represent a [service to service authentication](https://review.docs.microsoft.com/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) in which: + +* The '**resource**', also known as the **audience**, is the application that you register to represent your API. This is represented by the `aud` claim in the token. +* The '**client**' is a Microsoft application that represents the Azure AD authentication events service. It has an `appId` value of `99045fe1-7639-4a75-9d4a-577b6ca3810f`. This is represented by: + * The `azp` claim in the token if your application `accessTokenAcceptedVersion` property is set to `2`. + * The `appid` claim in the token if your resource application's `accessTokenAcceptedVersion` property is set to `1` or `null`. + +There are three approaches to dealing with the token. You can customize the behavior using [application settings](https://docs.microsoft.com/azure/azure-functions/functions-how-to-use-azure-function-app-settings?tabs=portal#settings) as shown below or via the [local.settings.json](https://docs.microsoft.com/azure/azure-functions/functions-develop-local#local-settings-file) file in local environments. + +#### Validate tokens using Azure Functions Azure AD authentication integration + +When running your function in production, it is **highly recommended** to use the [Azure Functions Azure AD authentication integration](https://docs.microsoft.com/azure/app-service/configure-authentication-provider-aad#-option-2-use-an-existing-registration-created-separately) for validating incoming tokens. + +1. Go to the "Authentication" tab in your Function App +2. Click on "Add identity provider" +3. Select "Microsoft" as the identity provider +4. Select "Provide the details of an existing app registration" +5. Enter the `Application ID` of the app that represents your API in Azure AD + +The issuer and allowed audience depends on the [`accessTokenAcceptedVersion`](https://review.docs.microsoft.com/azure/active-directory/develop/access-tokens) property of your application (can be found in the "Manifest" of the application). + +If the `accessTokenAcceptedVersion` property is set to `2`: +6. Set the `Issuer URL to "https://login.microsoftonline.com/{tenantId}/v2.0" +7. Set an 'Allowed Audience' to the Application ID (`appId`) + +If the `accessTokenAcceptedVersion` property is set to `1` or `null`: +6. Set the `Issuer URL to "https://sts.windows.net/{tenantId}/" +7. Set an 'Allowed Audience' to the Application ID URI (also known as`identifierUri`). It should be in the format of`api://{azureFunctionAppName}.azurewebsites.net/{resourceApiAppId}` or `api://{FunctionAppFullyQualifiedDomainName}/{resourceApiAppId}` if using a [custom domain name](https://docs.microsoft.com/azure/dns/dns-custom-domain#:~:text=Azure%20Function%20App%201%20Navigate%20to%20Function%20App,Custom%20domain%20text%20field%20and%20select%20Validate.%20). + +By default, the Authentication event trigger will validate that Azure Function authentication integration is configured and it will check that the **client** in the token is set to `99045fe1-7639-4a75-9d4a-577b6ca3810f` (via the `azp` or `appid` claims in the token). + +If you want to test your API against some other client that is not Azure AD authentication events service, like using Postman, you can configure an _optional_ application setting: + +* **AuthenticationEvents__CustomCallerAppId** - the guid of your desired client. If not provided, `99045fe1-7639-4a75-9d4a-577b6ca3810f` is assumed. + +#### Have the trigger validate the token + +In local environments or environments that aren't hosted in the Azure Function service, the trigger can do the token validation. Set the following application settings: + +* **AuthenticationEvents__TenantId** - your tenant ID +* **AuthenticationEvents__AudienceAppId** - the same value as "Allowed audience" in option 1. +* **AuthenticationEvents__CustomCallerAppId** (_optional_) - the guid of your desired client. If not provided, `99045fe1-7639-4a75-9d4a-577b6ca3810f` is assumed. + +An example `local.settings.json` file: + +```json +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "FUNCTIONS_WORKER_RUNTIME": "dotnet", + "AuthenticationEvents__TenantId": "8615397b-****-****-****-********06c8", + "AuthenticationEvents__AudienceAppId": "api://46f98993-****-****-****-********0038", + "AuthenticationEvents__CustomCallerAppId": "46f98993-****-****-****-********0038" + } +} +``` + +#### No token validation + +If you would like to _not_ authenticate the token while in local development, set the following application setting: + +* **AuthenticationEvents__BypassTokenValidation** - value of `true` will make the trigger not check for a validation of the token. + +### Quickstart + +* Visual Studio 2019 + * Start Visual Studio + * Select "Create a new project" + * In the template search area search and select "AzureAuthEventsTrigger" + * Give your project a meaningful Project Name, Location, Solution and Solution Name. + +* Visual Studio Code + * Start Visual Studio Code + * Run the command "Create Azure Authentication Events Trigger Project" via the command palette + * Follow the project creation prompts +* Please note: that on a first time run it might take awhile to download the the required packages. +* For development purpose turn of token validation for testing: +* Add the **AuthenticationEvents__BypassTokenValidation** application key to the "Values" section in the local.settings.json file and set it's value to **true**. If you do not have a local.settings.json file in your local environment, create one in the root of your Function App. + +```json +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "FUNCTIONS_WORKER_RUNTIME": "dotnet", + "AuthenticationEvents__BypassTokenValidation": true + } +} +``` + +* Once the project is loaded, you can run the sample code and you should see the Azure functions developer's application load your end point. + +## Key concepts + +Key concepts of the Azure .NET SDK can be found [here](https://azure.github.io/azure-sdk/dotnet_introduction.html) + +## Documentation + +* One the function has been published, there's some good reading about logging and metrics that can be found [here](https://docs.microsoft.com/azure/azure-functions/functions-monitor-log-analytics?tabs=csharp) + +* For API Documentation, please see the (Link TBD) +* Once this moves to preview, we except no breaking changes and would be as simple as removing the the nuget source that points to the private preview. + +## Examples + +To Test Token Augmentation, please do the following. + +* Start Visual Studio. +* Open the project that was created in the prior step. (QuickStart) +* Run the Application. (F5) +* Once the Azure functions developer's application has started, copy the listening url that is displayed with the application starts up. +* Note: All Authentication functions are listed, in the case we have one function listener registered called "**OnTokenIssuanceStart**" +* Your function endpoint will then be a combination of the listening url and function, for example: "http://localhost:7071/runtime/webhooks/AuthenticationEvents?code=(YOUR_CODE)&function=OnTokenIssuanceStart" +* Post the following payload using something like Postman or Fiddler. +* Steps for using Postman can be found (Link TBD) + +```json +{ + "type":"microsoft.graph.authenticationEvent.TokenIssuanceStart", + "source":"/tenants/{tenantId}/applications/{resourceAppId}", + "data":{ + "@odata.type": "microsoft.graph.onTokenIssuanceStartCalloutData", + "tenantId": "30000000-0000-0000-0000-000000000003", + "authenticationEventListenerId1": "10000000-0000-0000-0000-000000000001", + "customAuthenticationExtensionId": "10000000-0000-0000-0000-000000000002", + "authenticationContext1":{ + "correlationId": "20000000-0000-0000-0000-000000000002", + "client": { + "ip": "127.0.0.1", + "locale": "en-us", + "market": "en-au" + }, + "authenticationProtocol": "OAUTH2.0", + "clientServicePrincipal": { + "id": "40000000-0000-0000-0000-000000000001", + "appId": "40000000-0000-0000-0000-000000000002", + "appDisplayName": "Test client app", + "displayName": "Test client application" + }, + "resourceServicePrincipal": { + "id": "40000000-0000-0000-0000-000000000003", + "appId": "40000000-0000-0000-0000-000000000004", + "appDisplayName": "Test resource app", + "displayName": "Test resource application" + }, + "user": { + "companyName": "Nick Gomez", + "country": "USA", + "createdDateTime": "0001-01-01T00:00:00Z", + "displayName": "Dummy display name", + "givenName": "Example", + "id": "60000000-0000-0000-0000-000000000006", + "mail": "test@example.com", + "onPremisesSamAccountName": "testadmin", + "onPremisesSecurityIdentifier": "DummySID", + "onPremisesUserPrincipalName": "Dummy Name", + "preferredDataLocation": "DummyDataLocation", + "preferredLanguage": "DummyLanguage", + "surname": "Test", + "userPrincipalName": "testadmin@example.com", + "userType": "UserTypeCloudManaged" + } + } + } +} +``` + +* You should see this response: + +```json +{ + "data": { + "@odata.type": "microsoft.graph.onTokenIssuanceStartResponseData", + "actions": [ + { + "@odata.type": "ProvideClaimsForToken", + "claims": [ + { + "DateOfBirth": "01/01/2000" + }, + { + "CustomRoles": [ + "Writer", + "Editor" + ] + } + ] + } + ] + } +} +``` + +## Troubleshooting + +* Visual Studio Code + * If running in Visual Studio Code, you get an error along the lines of the local Azure Storage Emulator is unavailable, you can start the emulator manually.! (Note: Azure Storage emulator is now deprecated and the suggested replacement is [Azurite](https://docs.microsoft.com/azure/storage/common/storage-use-azurite?tabs=visual-studio)) + * If using Visual Studio Code on Mac please use [Azurite](https://docs.microsoft.com/azure/storage/common/storage-use-azurite?tabs=visual-studio) + * If you see the following error on Windows (it's a bug) when trying to run the created projected. + * This can be resolved by executing this command in powershell `Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope LocalMachine` more info on this can be found [here](https://github.com/Azure/azure-functions-core-tools/issues/1821) and [here](https://docs.microsoft.com/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7) + +## Next steps + +For more information on Azure SDK, please refer to [this website](https://azure.github.io/azure-sdk/) + +## Publish + +* Follow the instruction here to create and publish your Azure Application. +* To determine your published posting endpoint, combine the azure function endpoint you created, route to the listener and listener code, the listen code can be found by navigating to your azure function application, selecting "App Keys" and copying the value of AuthenticationEvents_extension. +* For example: "https://azureautheventstriggerdemo.azurewebsites.net/runtime/webhooks/AuthenticationEvents?code=(AuthenticationEvents_extension_key)&function=OnTokenIssuanceStart" +* Make sure your production environment has the correct application settings for token authentication. +* Once again you can test the published function by posting the above payload to the new endpoint. + +## Contributing + +For details on contributing to this repository, see the [contributing +guide][cg]. + +This project welcomes contributions and suggestions. Most contributions +require you to agree to a Contributor License Agreement (CLA) declaring +that you have the right to, and actually do, grant us the rights to use +your contribution. For details, visit . + +When you submit a pull request, a CLA-bot will automatically determine +whether you need to provide a CLA and decorate the PR appropriately +(e.g., label, comment). Simply follow the instructions provided by the +bot. You will only need to do this once across all repositories using +our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct][coc]. For +more information see the [Code of Conduct FAQ][coc_faq] or contact + with any additional questions or comments. + + +[cg]: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/resourcemanager/Azure.ResourceManager/docs/CONTRIBUTING.md +[coc]: https://opensource.microsoft.com/codeofconduct/ +[coc_faq]: https://opensource.microsoft.com/codeofconduct/faq/ \ No newline at end of file diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/WebJobs.Extensions.AuthenticationEvents.sln b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/WebJobs.Extensions.AuthenticationEvents.sln new file mode 100644 index 000000000000..dd10b8305fce --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/WebJobs.Extensions.AuthenticationEvents.sln @@ -0,0 +1,53 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32630.192 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{FD6F52B7-03AC-4EBB-A79A-3F02F8782E5C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{FAB04E28-402B-4039-8F7A-027CE8E0E343}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents", "src\Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.csproj", "{BB38310C-9B9A-436E-B100-7739ABDBB75A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{40B476A8-C30C-46BB-97DC-1AD8F405B529}" + ProjectSection(SolutionItems) = preProject + ..\..\..\eng\images\azureicon.png = ..\..\..\eng\images\azureicon.png + CHANGELOG.md = CHANGELOG.md + ..\ci.yml = ..\ci.yml + Directory.Build.props = Directory.Build.props + README.md = README.md + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests", "tests\Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests.csproj", "{0373F757-E998-4C45-9118-C264231E2944}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + TriggerRelease|Any CPU = TriggerRelease|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BB38310C-9B9A-436E-B100-7739ABDBB75A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB38310C-9B9A-436E-B100-7739ABDBB75A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB38310C-9B9A-436E-B100-7739ABDBB75A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB38310C-9B9A-436E-B100-7739ABDBB75A}.Release|Any CPU.Build.0 = Release|Any CPU + {BB38310C-9B9A-436E-B100-7739ABDBB75A}.TriggerRelease|Any CPU.ActiveCfg = Release|Any CPU + {BB38310C-9B9A-436E-B100-7739ABDBB75A}.TriggerRelease|Any CPU.Build.0 = Release|Any CPU + {0373F757-E998-4C45-9118-C264231E2944}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0373F757-E998-4C45-9118-C264231E2944}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0373F757-E998-4C45-9118-C264231E2944}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0373F757-E998-4C45-9118-C264231E2944}.Release|Any CPU.Build.0 = Release|Any CPU + {0373F757-E998-4C45-9118-C264231E2944}.TriggerRelease|Any CPU.ActiveCfg = Release|Any CPU + {0373F757-E998-4C45-9118-C264231E2944}.TriggerRelease|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {BB38310C-9B9A-436E-B100-7739ABDBB75A} = {FD6F52B7-03AC-4EBB-A79A-3F02F8782E5C} + {0373F757-E998-4C45-9118-C264231E2944} = {FAB04E28-402B-4039-8F7A-027CE8E0E343} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {21348D28-E450-49EE-AEA4-ADD8168DD20B} + EndGlobalSection +EndGlobal 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 new file mode 100644 index 000000000000..bf0895496d72 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/api/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.netstandard2.0.cs @@ -0,0 +1,255 @@ +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + [System.AttributeUsageAttribute(System.AttributeTargets.Field, AllowMultiple=false)] + public partial class AuthenticationEventMetadataAttribute : System.Attribute + { + internal AuthenticationEventMetadataAttribute() { } + public string EventNamespace { get { throw null; } set { } } + } + public partial class AuthenticationEventResponseHandler : Microsoft.Azure.WebJobs.Host.Bindings.IValueBinder, Microsoft.Azure.WebJobs.Host.Bindings.IValueProvider + { + public 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; } } + public System.Threading.Tasks.Task GetValueAsync() { throw null; } + public System.Threading.Tasks.Task SetValueAsync(object result, System.Threading.CancellationToken cancellationToken) { throw null; } + public string ToInvokeString() { throw null; } + } + [Microsoft.Azure.WebJobs.Description.BindingAttribute(TriggerHandlesReturnValue=true)] + [System.AttributeUsageAttribute(System.AttributeTargets.Parameter)] + public partial class AuthenticationEventsTriggerAttribute : System.Attribute + { + public AuthenticationEventsTriggerAttribute() { } + public string AudienceAppId { get { throw null; } set { } } + public string TenantId { get { throw null; } set { } } + } + public partial class AuthenticationEventWebJobsStartup : Microsoft.Azure.WebJobs.Hosting.IWebJobsStartup + { + public AuthenticationEventWebJobsStartup() { } + public void Configure(Microsoft.Azure.WebJobs.IWebJobsBuilder builder) { } + } + public enum EventDefinition + { + [Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.AuthenticationEventMetadataAttribute(typeof(Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.TokenIssuanceStartRequest), "onTokenIssuanceStartCustomExtension", "TokenIssuanceStart.preview_10_01_2021", "ResponseTemplate.json")] + TokenIssuanceStartV20211001Preview = 0, + [Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.AuthenticationEventMetadataAttribute(typeof(Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.TokenIssuanceStartRequest), "microsoft.graph.authenticationEvent.TokenIssuanceStart", "TokenIssuanceStart", "CloudEventActionableTemplate.json")] + TokenIssuanceStart = 1, + } + public enum EventType + { + OnTokenIssuanceStart = 0, + } + public enum RequestStatusType + { + Failed = 0, + TokenInvalid = 1, + Successful = 2, + } +} +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework +{ + public abstract partial class ActionableCloudEventResponse : Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.ActionableResponse where T : Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.AuthenticationEventAction + { + protected ActionableCloudEventResponse() { } + internal abstract string DataTypeIdentifier { get; } + [System.ComponentModel.DataAnnotations.RequiredAttribute] + [System.Text.Json.Serialization.JsonPropertyNameAttribute("oDataType")] + public string ODataType { get { throw null; } } + } + public abstract partial class ActionableResponse : Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.AuthenticationEventResponse where T : Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.AuthenticationEventAction + { + protected ActionableResponse() { } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("actions")] + public System.Collections.Generic.List Actions { get { throw null; } set { } } + } + public abstract partial class AuthenticationEventAction + { + public AuthenticationEventAction() { } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("actionType")] + internal abstract string ActionType { get; } + internal abstract Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.AuthenticationEventJsonElement BuildActionBody(); + internal abstract void FromJson(Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.AuthenticationEventJsonElement actionBody); + } + public abstract partial class AuthenticationEventData + { + protected AuthenticationEventData() { } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("authenticationEventListenerId")] + public System.Guid AuthenticationEventListenerId { get { throw null; } set { } } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("AuthenticationEventsId")] + public System.Guid AuthenticationEventsId { get { throw null; } set { } } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("tenantId")] + public System.Guid TenantId { get { throw null; } set { } } + } + public abstract partial class AuthenticationEventRequestBase + { + internal AuthenticationEventRequestBase() { } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("queryParameters")] + public System.Collections.Generic.Dictionary QueryParameters { get { throw null; } } + [System.ComponentModel.DataAnnotations.RequiredAttribute] + [System.Text.Json.Serialization.JsonConverterAttribute(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))] + [System.Text.Json.Serialization.JsonPropertyNameAttribute("requestStatus")] + public Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.RequestStatusType RequestStatus { get { throw null; } set { } } + [System.ComponentModel.DataAnnotations.RequiredAttribute] + [System.Text.Json.Serialization.JsonPropertyNameAttribute("statusMessage")] + public string StatusMessage { get { throw null; } set { } } + [System.ComponentModel.DataAnnotations.RequiredAttribute] + [System.Text.Json.Serialization.JsonPropertyNameAttribute("type")] + public string Type { get { throw null; } set { } } + public abstract System.Threading.Tasks.Task Completed(); + public abstract System.Threading.Tasks.Task Failed(System.Exception exception); + public override string ToString() { throw null; } + } + public abstract partial class AuthenticationEventRequest : Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.AuthenticationEventRequestBase where TResponse : Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.AuthenticationEventResponse where TData : Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.AuthenticationEventData + { + internal AuthenticationEventRequest() { } + [System.ComponentModel.DataAnnotations.RequiredAttribute] + [System.Text.Json.Serialization.JsonPropertyNameAttribute("payload")] + public TData Payload { get { throw null; } set { } } + [System.ComponentModel.DataAnnotations.RequiredAttribute] + [System.Text.Json.Serialization.JsonPropertyNameAttribute("response")] + public TResponse Response { get { throw null; } set { } } + public override System.Threading.Tasks.Task Completed() { throw null; } + public override System.Threading.Tasks.Task Failed(System.Exception exception) { throw null; } + } + public abstract partial class AuthenticationEventResponse : System.Net.Http.HttpResponseMessage + { + protected AuthenticationEventResponse() { } + [System.ComponentModel.DataAnnotations.RequiredAttribute] + public string Body { get { throw null; } set { } } + internal abstract void Invalidate(); + } + public abstract partial class CloudEventData : Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.AuthenticationEventData + { + protected CloudEventData() { } + } + public abstract partial class CloudEventRequest : Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.AuthenticationEventRequest where TResponse : Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.AuthenticationEventResponse where TData : Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.CloudEventData + { + internal CloudEventRequest() { } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("oDataType")] + public string ODataType { get { throw null; } set { } } + [System.ComponentModel.DataAnnotations.RequiredAttribute] + [System.Text.Json.Serialization.JsonPropertyNameAttribute("source")] + public string Source { get { throw null; } set { } } + } +} +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart +{ + public partial class TokenIssuanceStartRequest : Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.CloudEventRequest + { + public TokenIssuanceStartRequest(System.Net.Http.HttpRequestMessage request) { } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("tokenClaims")] + public System.Collections.Generic.Dictionary TokenClaims { get { throw null; } } + } + public partial class TokenIssuanceStartResponse : Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.ActionableCloudEventResponse + { + public TokenIssuanceStartResponse() { } + } +} +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Actions +{ + public partial class ProvideClaimsForToken : Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Actions.TokenIssuanceAction + { + public ProvideClaimsForToken() { } + public ProvideClaimsForToken(params Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Actions.TokenClaim[] claim) { } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("claims")] + public System.Collections.Generic.List Claims { get { throw null; } } + public void AddClaim(string Id, params string[] Values) { } + } + public partial class TokenClaim + { + public TokenClaim(string id, params string[] values) { } + [System.ComponentModel.DataAnnotations.RequiredAttribute] + [System.Text.Json.Serialization.JsonPropertyNameAttribute("id")] + public string Id { get { throw null; } set { } } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("values")] + public string[] Values { get { throw null; } set { } } + } + public abstract partial class TokenIssuanceAction : Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.AuthenticationEventAction + { + public TokenIssuanceAction() { } + } +} +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Data +{ + public partial class AuthenticationEventContext + { + public AuthenticationEventContext() { } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("client")] + public Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Data.AuthenticationEventContextClient Client { get { throw null; } set { } } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("clientServicePrincipal")] + public Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Data.AuthenticationEventContextServicePrincipal ClientServicePrincipal { get { throw null; } set { } } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("correlationId")] + public System.Guid CorrelationId { get { throw null; } set { } } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("protocol")] + public string Protocol { get { throw null; } set { } } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("resourceServicePrincipal")] + public Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Data.AuthenticationEventContextServicePrincipal ResourceServicePrincipal { get { throw null; } set { } } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("user")] + public Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Data.AuthenticationEventContextUser User { get { throw null; } set { } } + } + public partial class AuthenticationEventContextClient + { + public AuthenticationEventContextClient() { } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("ip")] + public string Ip { get { throw null; } set { } } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("locale")] + public string Locale { get { throw null; } set { } } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("market")] + public string Market { get { throw null; } set { } } + } + public partial class AuthenticationEventContextServicePrincipal + { + public AuthenticationEventContextServicePrincipal() { } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("appDisplayName")] + public string AppDisplayName { get { throw null; } set { } } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("appId")] + public System.Guid AppId { get { throw null; } set { } } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("displayName")] + public string DisplayName { get { throw null; } set { } } + [System.ComponentModel.DataAnnotations.RequiredAttribute] + [System.Text.Json.Serialization.JsonPropertyNameAttribute("id")] + public System.Guid Id { get { throw null; } set { } } + } + public partial class AuthenticationEventContextUser + { + public AuthenticationEventContextUser() { } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("companyName")] + public string CompanyName { get { throw null; } set { } } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("country")] + public string Country { get { throw null; } set { } } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("displayName")] + public string DisplayName { get { throw null; } set { } } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("givenName")] + public string GivenName { get { throw null; } set { } } + [System.ComponentModel.DataAnnotations.RequiredAttribute] + [System.Text.Json.Serialization.JsonPropertyNameAttribute("id")] + public System.Guid Id { get { throw null; } set { } } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("mail")] + public string Mail { get { throw null; } set { } } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("onPremisesSamAccountName")] + public string OnPremisesSamAccountName { get { throw null; } set { } } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("onPremisesSecurityIdentifier")] + public string OnPremisesSecurityIdentifier { get { throw null; } set { } } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("onPremiseUserPrincipalName")] + public string OnPremiseUserPrincipalName { get { throw null; } set { } } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("preferredDataLocation")] + public string PreferredDataLocation { get { throw null; } set { } } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("preferredLanguage")] + public string PreferredLanguage { get { throw null; } set { } } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("surname")] + public string Surname { get { throw null; } set { } } + [System.ComponentModel.DataAnnotations.RequiredAttribute] + [System.Text.Json.Serialization.JsonPropertyNameAttribute("userPrincipalName")] + public string UserPrincipalName { get { throw null; } set { } } + [System.Text.Json.Serialization.JsonPropertyNameAttribute("userType")] + public string UserType { get { throw null; } set { } } + } + public partial class TokenIssuanceStartData : Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.CloudEventData + { + public TokenIssuanceStartData() { } + [System.ComponentModel.DataAnnotations.RequiredAttribute] + [System.Text.Json.Serialization.JsonPropertyNameAttribute("authenticationContext")] + public Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Data.AuthenticationEventContext AuthenticationContext { get { throw null; } set { } } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventBinding.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventBinding.cs new file mode 100644 index 000000000000..3e2d3313c18e --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventBinding.cs @@ -0,0 +1,300 @@ +// 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.Globalization; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using static Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.EmptyResponse; +using AuthenticationEventMetadata = Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.AuthenticationEventMetadata; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + /// Main trigger binding class where we handle the incoming HTTP request message. + /// + internal class AuthenticationEventBinding : ITriggerBinding + { + internal const string InstanceIdBindingPropertyName = "instanceId"; + private const string DataBindingPropertyName = "data"; + + private readonly AuthenticationEventsTriggerAttribute _authEventTriggerAttr; + private readonly AuthenticationEventConfigProvider _configuration; + private readonly ParameterInfo _parameterInfo; + private readonly IReadOnlyDictionary _contract; + /// Gets the type of the trigger value. + /// The type of the trigger value. + public Type TriggerValueType => typeof(HttpRequestMessage); + + /// Gets the binding data contract. + /// The binding data contract. + public IReadOnlyDictionary BindingDataContract => _contract; + + /// Initializes a new instance of the class. + /// The authentication event trigger attribute. + /// The configuration provider. + /// Initial parameter details. + internal AuthenticationEventBinding(AuthenticationEventsTriggerAttribute authEventTriggerAttr, AuthenticationEventConfigProvider configProvider, ParameterInfo parameterInfo) + { + _authEventTriggerAttr = authEventTriggerAttr; + _configuration = configProvider; + _parameterInfo = parameterInfo; + _contract = GetBindingDataContract(parameterInfo); + } + + /// Gets the binding data contract. + /// The parameter information. + /// A contract with the return type and Instance Id in the dictionary. + private static IReadOnlyDictionary GetBindingDataContract(ParameterInfo parameterInfo) + { + var contract = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + // This binding supports return values of any type + { "$return", typeof(object).MakeByRefType() }, + { InstanceIdBindingPropertyName, typeof(string) }, + }; + + // allow binding to the parameter name + contract[parameterInfo.Name] = parameterInfo.ParameterType; + + // allow binding directly to the JSON representation of the data. + contract[DataBindingPropertyName] = typeof(string); + + return contract; + } + + /// Binds the asynchronous. + /// The value. + /// The context. + /// The Trigger data containing the event related request object. + /// An HttpRequestMessage is required. + /// If the token is invalid. + /// blah + /// + public async Task BindAsync(object value, ValueBindingContext context) + { + var request = (HttpRequestMessage)value; + AuthenticationEventResponseHandler eventResponseHandler = (AuthenticationEventResponseHandler)request.Properties[AuthenticationEventResponseHandler.EventResponseProperty]; + try + { + if (request == null) + { + throw new NotSupportedException(AuthenticationEventResource.Ex_Invalid_Inbound); + } + + Dictionary Claims = await GetClaimsAndValidateRequest(request).ConfigureAwait(false); + string payload = await request.Content.ReadAsStringAsync().ConfigureAwait(false); + AuthenticationEventMetadata eventMetadata = GetEventAndValidateSchema(payload); + + eventResponseHandler.Request = GetRequestForEvent(request, payload, eventMetadata, Claims); + return new TriggerData(new AuthenticationEventValueBinder(eventResponseHandler.Request, _authEventTriggerAttr), GetBindingData(context, value, eventResponseHandler)) + { + ReturnValueProvider = eventResponseHandler + }; + } + catch (Exception ex) + { + return GetFaultyRequest(context, value, request, eventResponseHandler, ex); + } + } + + #region Request Handlers + /// Gets the faulty request. + /// Current binding context + /// Incoming value. + /// The incoming HTTP request message. + /// The event response handler. + /// The exception that caused the fault. + /// 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) + { + eventResponseHandler.Request = _parameterInfo.ParameterType == typeof(string) ? new EmptyRequest(request) : AuthenticationEventMetadata.CreateEventRequest(request, _parameterInfo.ParameterType, null); + eventResponseHandler.Request.StatusMessage = ex.Message; + eventResponseHandler.Request.RequestStatus = ex is UnauthorizedAccessException ? RequestStatusType.TokenInvalid : RequestStatusType.Failed; + if (eventResponseHandler.Response != null) + { + if (ex is UnauthorizedAccessException) + { + eventResponseHandler.Response.StatusCode = System.Net.HttpStatusCode.Unauthorized; + } + else + { + eventResponseHandler.Response.StatusCode = System.Net.HttpStatusCode.BadRequest; + eventResponseHandler.Response.ReasonPhrase = ex.Message; + } + } + + return new TriggerData(new AuthenticationEventValueBinder(eventResponseHandler.Request, _authEventTriggerAttr), GetBindingData(context, value, eventResponseHandler)) + { + ReturnValueProvider = eventResponseHandler + }; + } + /// Gets the request for the event. + /// The HTTP request message. + /// The incoming payload. + /// The event metadata. + /// The token claims. + /// The related EventRequest based on the event requested.
+ /// + /// + private AuthenticationEventRequestBase GetRequestForEvent(HttpRequestMessage request, string payload, AuthenticationEventMetadata eventMetadata, Dictionary tokenClaims) + => GetRequestForEvent(request, payload, eventMetadata, tokenClaims, null); + + /// Gets the request for event. + /// And sets the status message and state based on the exception. + /// The HTTP request message. + /// The body of the request (Json). + /// The event metadata. + /// The token claims. + /// An exception if any occurred along the way. + /// The related EventRequest based on the event requested. + /// An exception is thrown if the IRequestEvent could not be determined from the incoming HTTP request message. + /// + /// + private AuthenticationEventRequestBase GetRequestForEvent(HttpRequestMessage request, string payload, AuthenticationEventMetadata eventMetaData, Dictionary tokenClaims, Exception ex) + { + AuthenticationEventRequestBase requestEvent = eventMetaData?.CreateEventRequestValidate(request, payload, tokenClaims); + if (requestEvent == null) + { + throw new Exception(AuthenticationEventResource.Ex_Invalid_Event); + } + else if (requestEvent.GetType() != _parameterInfo.ParameterType && ex == null && _parameterInfo.ParameterType != typeof(string)) + { + throw new Exception(string.Format(CultureInfo.CurrentCulture, AuthenticationEventResource.Ex_Parm_Mismatch, requestEvent.GetType(), _parameterInfo.ParameterType)); + } + + requestEvent.StatusMessage = ex == null ? AuthenticationEventResource.Status_Good : ex.Message; + requestEvent.RequestStatus = ex == null ? RequestStatusType.Successful : ex is UnauthorizedAccessException ? RequestStatusType.TokenInvalid : RequestStatusType.Failed; + + return requestEvent; + } + + private Dictionary GetBindingData(ValueBindingContext context, object value, AuthenticationEventResponseHandler eventResponseHandler) + { + return new Dictionary(StringComparer.OrdinalIgnoreCase) + { + [InstanceIdBindingPropertyName] = context.FunctionContext.FunctionInstanceId, + [_parameterInfo.Name] = value.ToString(), + [DataBindingPropertyName] = eventResponseHandler.Request?.ToString() + }; + } + #endregion + + #region Validators + private async Task> GetClaimsAndValidateRequest(HttpRequestMessage requestMessage) + { + ConfigurationManager configurationManager = new ConfigurationManager(_authEventTriggerAttr); + if (ConfigurationManager.BypassValidation) + { + return null; + } + + TokenValidator validator = ConfigurationManager.EZAuthEnabled && requestMessage.Headers.Matches(ConfigurationManager.HEADER_EZAUTH_ICP, ConfigurationManager.HEADER_EZAUTH_ICP_VERIFY) ? + (TokenValidator)new TokenValidatorEZAuth() : + new TokenValidatorInternal(); + + (bool valid, Dictionary claims) = await validator.GetClaimsAndValidate(requestMessage, configurationManager).ConfigureAwait(false); + if (valid) + { + return claims; + } + else + { + throw new UnauthorizedAccessException(); + } + } + + /// Gets the event and validates the income Json against schema provided on the Event Metadata Attribute. + /// The body of the incoming request. + /// 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) + { + if (!Helpers.IsJson(body)) + { + throw new InvalidDataException(); + } + + return AuthenticationEventMetadataLoader.GetEventMetadata(body); + } + #endregion + + /// Creates the listener asynchronous. + /// The context. + /// An IListener. + /// context. + public Task CreateListenerAsync(ListenerFactoryContext context) + { + if (context == null) + { + throw new ArgumentNullException(AuthenticationEventResource.Ex_Context_Null); + } + + var function = context.Descriptor.ShortName.Split('.').Last(); + _configuration.Listeners.Add(function, new AuthenticationEventListener(context.Executor, _authEventTriggerAttr)); + //_configuration.LogInformation($"Added function listener: {function}"); + _configuration.DisplayAzureFunctionInfoToConsole(function); + + return Task.FromResult(new NullListener()); + } + + /// Converts to parameter-descriptor. + /// The parameter description for the trigger and binding. + public ParameterDescriptor ToParameterDescriptor() + { + return new TriggerParameterDescriptor() + { + Name = "Authentication Event Trigger", + DisplayHints = new ParameterDisplayHints() + { + Prompt = "Host", + Description = "Authentication Event Trigger" + } + }; + } + + #region listener + /// This is a null listener that does nothing. But is requested for the WebJobs Framework, our isolated hosting is handled by the configuration. + private class NullListener : IListener + { + /// Cancels this instance. + public void Cancel() + { + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + } + + /// Starts the asynchronous listener. + /// The cancellation token. + /// A task that is flagged as completed. + public Task StartAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + /// Stops the asynchronous listener. + /// The cancellation token. + /// A task that is flagged as completed. + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } + #endregion + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventBindingProvider.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventBindingProvider.cs new file mode 100644 index 000000000000..69c8a3f82b50 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventBindingProvider.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Azure.WebJobs.Host.Triggers; +using System.Reflection; +using System.Threading.Tasks; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + /// The main trigger binding provider class.
+ /// + internal class AuthenticationEventBindingProvider : ITriggerBindingProvider + { + private readonly AuthenticationEventConfigProvider _config; + /// Initializes a new instance of the class. + /// The configuration provider. + internal AuthenticationEventBindingProvider(AuthenticationEventConfigProvider configProvider) + { + _config = configProvider; + } + + /// This is called from the WebJobs framework, where we look for our attribute and create and new instance of our trigger binding. + /// The context that we get from the framework. + /// A new instance of EventsTriggerBinding. + /// Is thrown when we cannot find the correct event definition attribute attached to the trigger attribute. + /// Is thrown when the object model is out of event sync or the wrong parameter for the event is specified on the function signature. + /// + public Task TryCreateAsync(TriggerBindingProviderContext context) + { + AuthenticationEventsTriggerAttribute attribute = context.Parameter.GetCustomAttribute(false); + if (attribute == null) + { + return Task.FromResult(null); + } + + attribute.IsParameterString = context.Parameter.ParameterType == typeof(string); + + return Task.FromResult(new AuthenticationEventBinding(attribute, _config, context.Parameter)); + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventConfigProvider.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventConfigProvider.cs new file mode 100644 index 000000000000..e7970734574f --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventConfigProvider.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Azure.WebJobs.Description; +using Microsoft.Azure.WebJobs.Host.Config; +using Microsoft.Azure.WebJobs.Host.Executors; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Web; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + /// The main configuration provider, this also handles the initial HTTP requests and response via IAsyncConverter. + /// + /// + [Extension("CustomAuthenticationExtension", "CustomAuthenticationExtension")] + internal class AuthenticationEventConfigProvider : IExtensionConfigProvider, IAsyncConverter + { + private readonly ILogger _logger; + private Uri _base_uri; + + /// The listeners that are attached to the functions that implement the AuthenticationEventTriggerAttribute. + public Dictionary Listeners { get; } = new Dictionary(); + + /// Initializes a new instance of the class. + /// The logger factory from the WebJobs framework. + public AuthenticationEventConfigProvider(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + /// Initializes the specified configuration. + /// This is where we create the main HTTP Listener endpoint and bind the trigger. (For all intents and purposes this is not obsolete). + /// The context we get from the Webjobs framework. + [Obsolete("Is not obsolete marked by webjobs team, but chatted and this is correct. It is not being deprecated")] + public void Initialize(ExtensionConfigContext context) + { + context.AddBindingRule() + .BindToTrigger(new AuthenticationEventBindingProvider(this)); + + _base_uri = context.GetWebhookHandler(); + //LogInformation(string.Format(AuthenticationEventResource.Log_EventHandler_Url, Uri)); + } + + internal void LogInformation(string message) + { + Console.WriteLine(message); + _logger.LogInformation(message); + } + + internal void DisplayAzureFunctionInfoToConsole(string functionName) + { + Console.ForegroundColor = ConsoleColor.DarkYellow; + if (Listeners.Count == 1) + { + Console.WriteLine(); + Console.WriteLine(AuthenticationEventResource.Out_Console_Seperator); + } + Console.Write(AuthenticationEventResource.Out_Console_FunctName); + Console.ForegroundColor = ConsoleColor.Blue; + Console.WriteLine(functionName); + Console.ForegroundColor = ConsoleColor.DarkYellow; + Console.Write(AuthenticationEventResource.Out_Console_FunctUrl); + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"{_base_uri}&functionName={functionName}"); + Console.ForegroundColor = ConsoleColor.DarkYellow; + Console.WriteLine(AuthenticationEventResource.Out_Console_Seperator); + Console.ResetColor(); + } + + /// The main entry point when we get an HTTP post. + /// The incoming HTTP request. + /// The cancellation token. + /// The HTTP response that is build based on the events request's response.
+ /// Is thrown if the function parameter is not in the incoming query string. + /// Is thrown if the incoming function parameter is not associated to a trigger listener. + /// Is thrown when the incoming request method is NOT 'POST'. + public async Task ConvertAsync(HttpRequestMessage input, CancellationToken cancellationToken) + { + try + { + NameValueCollection queryStringParmeters = HttpUtility.ParseQueryString(input.RequestUri.Query); + + if (input.Method != HttpMethod.Post) + { + throw new InvalidOperationException("Method can only be post."); + } + + //We find the attached listener assigned to the function and execute it. + var functionName = queryStringParmeters["function"] ?? queryStringParmeters["functionName"]; + if (string.IsNullOrEmpty(functionName)) + { + throw new MissingFieldException(string.Format(CultureInfo.CurrentCulture, AuthenticationEventResource.Ex_Missing_Function, string.Join(", ", Listeners.Select(l => l.Key)))); + } + + KeyValuePair listener = Listeners.FirstOrDefault(l => l.Key.Equals(functionName, StringComparison.OrdinalIgnoreCase)); + if (listener.Key == null) + { + throw new MissingMethodException(string.Format(CultureInfo.CurrentCulture, AuthenticationEventResource.Ex_Invalid_Function, functionName, string.Join(", ", Listeners.Select(l => l.Key)))); + } + + //We create an event response handler and attach it to the income HTTP message, then on the trigger we set the function response + //in the event response handler and after the executor calls the functions we have reference to the function response. + AuthenticationEventResponseHandler eventsResponseHandler = new AuthenticationEventResponseHandler(); + input.Properties.Add(AuthenticationEventResponseHandler.EventResponseProperty, eventsResponseHandler); + + TriggeredFunctionData triggerData = new TriggeredFunctionData() + { + TriggerValue = input + }; + + FunctionResult result = await listener.Value.FunctionExecutor.TryExecuteAsync(triggerData, cancellationToken).ConfigureAwait(false); + return result.Succeeded ? (HttpResponseMessage)eventsResponseHandler.Response : Helpers.HttpErrorResponse(result.Exception); + } + catch (Exception ex) + { + return Helpers.HttpErrorResponse(ex); + } + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventListener.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventListener.cs new file mode 100644 index 000000000000..8657aa607972 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventListener.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Azure.WebJobs.Host.Executors; +using Microsoft.Azure.WebJobs.Host.Listeners; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + /// This class is attached to each function that calls into our authentication event trigger. + internal class AuthenticationEventListener : IListener + { + /// Gets or sets the function executor. + /// The function executor that would execute the attached function. + /// + internal ITriggeredFunctionExecutor FunctionExecutor { get; private set; } + + /// Gets or sets the attribute. + /// The event trigger attribute assigned to the function that the listener is attached to. + internal AuthenticationEventsTriggerAttribute Attribute { get; set; } + + /// Initializes a new instance of the class. + /// The executor. + /// The attribute to assign to the listener. + internal AuthenticationEventListener(ITriggeredFunctionExecutor executor, AuthenticationEventsTriggerAttribute attribute) + { + FunctionExecutor = executor; + Attribute = attribute; + } + + /// Cancels this instance. + public void Cancel() + { } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + GC.SuppressFinalize(this); + } + + /// Starts the asynchronous listener, we do not do anything here as all we need is the reference to the executor. + /// The cancellation token. + /// A task flagged as completed with the value as true. + public Task StartAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + /// Stops the asynchronous listener, we do not do anything here as all we need is the reference to the executor. + /// The cancellation token. + /// A task flagged as completed with the value true. + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventMetadataAttribute.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventMetadataAttribute.cs new file mode 100644 index 000000000000..2846d623007c --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventMetadataAttribute.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework; +using System; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + /// EventMetadata enum attribute that controls the related request object, schemas and json payloads + /// + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] + public class AuthenticationEventMetadataAttribute : Attribute + { + /// Gets or sets the type of the request. + /// The type of the request. + /// Which is must inherit EventRequest + /// + internal Type RequestType { get; set; } + /// Gets or sets the request schema. + /// The name of the schema file in the event folder. + /// The request schema. + + /// Gets or sets the event namespace. + /// The event namespace. + public string EventNamespace { get; set; } + + /// Gets or sets the response template. + /// This with be the base response Json template file located in the event folder + /// The response template. + internal string ResponseTemplate { get; set; } + /// Gets or sets the EventIdentifer. + /// The Event Identifier. + internal string EventIdentifier { get; set; } + + /// Initializes a new instance of the class. + /// Type of the request. + /// The event identifier. + /// The name space related to the event + /// The response template. + /// Defaulted to ResponseTemplate.json + /// If the requestType in not of type EventRequest + internal AuthenticationEventMetadataAttribute(Type requestType, string eventIdentifier, string eventNamespace, string responseTemplate = "ResponseTemplate.json") + { + if (!typeof(AuthenticationEventRequestBase).IsAssignableFrom(requestType)) + { + throw new Exception(AuthenticationEventResource.Ex_Invalid_EventType); + } + + RequestType = requestType; + EventNamespace = eventNamespace; + ResponseTemplate = responseTemplate; + EventIdentifier = eventIdentifier; + } + } +} 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 new file mode 100644 index 000000000000..36a449c92916 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventResource.Designer.cs @@ -0,0 +1,379 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class AuthenticationEventResource { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal AuthenticationEventResource() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.AuthenticationEventResour" + + "ce", typeof(AuthenticationEventResource).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The action '{0}' is invalid, please use one of the following actions: '{1}'. + /// + internal static string Ex_Action_Invalid { + get { + return ResourceManager.GetString("Ex_Action_Invalid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Comparable '{0}' cannot be match.. + /// + internal static string Ex_Comparable_Not_Found { + get { + return ResourceManager.GetString("Ex_Comparable_Not_Found", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ListenerFactoryContext is null. + /// + internal static string Ex_Context_Null { + get { + return ResourceManager.GetString("Ex_Context_Null", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot determine event from payload. (Error: {0}). + /// + internal static string Ex_Event_Missing { + get { + return ResourceManager.GetString("Ex_Event_Missing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to File '{0}' could not be written. Already exists.. + /// + internal static string Ex_File_Exists { + get { + return ResourceManager.GetString("Ex_File_Exists", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed. + /// + internal static string Ex_Gen_Failure { + get { + return ResourceManager.GetString("Ex_Gen_Failure", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid event type.. + /// + internal static string Ex_Invalid_Event { + get { + return ResourceManager.GetString("Ex_Invalid_Event", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid schema event.. + /// + internal static string Ex_Invalid_EventSchema { + get { + return ResourceManager.GetString("Ex_Invalid_EventSchema", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type for Versioning needs to be derived from IEventRequest. + /// + internal static string Ex_Invalid_EventType { + get { + return ResourceManager.GetString("Ex_Invalid_EventType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot find related listener for function: {0}, available functions: {1}. + /// + internal static string Ex_Invalid_Function { + get { + return ResourceManager.GetString("Ex_Invalid_Function", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An HttpRequestMessage is required. + /// + internal static string Ex_Invalid_Inbound { + get { + return ResourceManager.GetString("Ex_Invalid_Inbound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot find json path set value. + /// + internal static string Ex_Invalid_JsonPath { + get { + return ResourceManager.GetString("Ex_Invalid_JsonPath", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid Payload detected.. + /// + internal static string Ex_Invalid_Payload { + get { + return ResourceManager.GetString("Ex_Invalid_Payload", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Response validation failed, see inner exceptions.. + /// + internal static string Ex_Invalid_Response { + get { + return ResourceManager.GetString("Ex_Invalid_Response", resourceCulture); + } + } + + /// + /// 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 { + return ResourceManager.GetString("Ex_Invalid_Return", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid version on Schema. + /// + internal static string Ex_Invalid_SchemaVersion { + get { + return ResourceManager.GetString("Ex_Invalid_SchemaVersion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot assign legacy payload to cloud events.. + /// + internal static string Ex_Leg_payload { + get { + return ResourceManager.GetString("Ex_Leg_payload", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot find the correct Event Metadata based on the input parameter: {0}. + /// + internal static string Ex_Missing_Def { + get { + return ResourceManager.GetString("Ex_Missing_Def", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please supply the function name via the query parameter: functionName, available functions: {0}. + /// + internal static string Ex_Missing_Function { + get { + return ResourceManager.GetString("Ex_Missing_Function", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot find the response on the request.. + /// + internal static string Ex_Missing_Request_Response { + get { + return ResourceManager.GetString("Ex_Missing_Request_Response", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No Actions Found. Please supply atleast one action.. + /// + internal static string Ex_No_Action { + get { + return ResourceManager.GetString("Ex_No_Action", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot determine the validation path, missing version attribute on Version Enum.. + /// + internal static string Ex_No_Attr { + get { + return ResourceManager.GetString("Ex_No_Attr", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not get exclusive lock to the resource.. + /// + internal static string Ex_No_Lock { + get { + return ResourceManager.GetString("Ex_No_Lock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No associated open api document found for version.. + /// + internal static string Ex_OpenApi_Missing { + get { + return ResourceManager.GetString("Ex_OpenApi_Missing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The parameter on the function signature does not match the expected parameter type of '{0}'. Got: '{1}'. + /// + internal static string Ex_Parm_Mismatch { + get { + return ResourceManager.GetString("Ex_Parm_Mismatch", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The response is expected to be of type: {0}. + /// + internal static string Ex_Response_Mismatch { + get { + return ResourceManager.GetString("Ex_Response_Mismatch", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid token version {0}, supported versions are: {1}. + /// + internal static string Ex_Token_Version { + get { + return ResourceManager.GetString("Ex_Token_Version", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please supply both the TenantId and AudienceAppId in variables in your binding configuration. (Or app settings {0} and {1}). + /// + internal static string Ex_Trigger_Required_Attrs { + get { + return ResourceManager.GetString("Ex_Trigger_Required_Attrs", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Listener registered at: {0}. + /// + internal static string Log_EventHandler_Url { + get { + return ResourceManager.GetString("Log_EventHandler_Url", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Function name: . + /// + internal static string Out_Console_FunctName { + get { + return ResourceManager.GetString("Out_Console_FunctName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Function url: . + /// + internal static string Out_Console_FunctUrl { + get { + return ResourceManager.GetString("Out_Console_FunctUrl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to --------------------------. + /// + internal static string Out_Console_Seperator { + get { + return ResourceManager.GetString("Out_Console_Seperator", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Ready. + /// + internal static string Status_Good { + get { + return ResourceManager.GetString("Status_Good", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The '{0}' field is missing.. + /// + internal static string Val_Non_Default { + get { + return ResourceManager.GetString("Val_Non_Default", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The '{0}' field needs to contain one of the following values: . + /// + internal static string Val_One_Of { + get { + return ResourceManager.GetString("Val_One_Of", resourceCulture); + } + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventResource.resx b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventResource.resx new file mode 100644 index 000000000000..e2cf2bf597ca --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventResource.resx @@ -0,0 +1,225 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The action '{0}' is invalid, please use one of the following actions: '{1}' + + + Comparable '{0}' cannot be match. + + + ListenerFactoryContext is null + + + Cannot determine event from payload. (Error: {0}) + + + File '{0}' could not be written. Already exists. + + + Failed + + + Invalid event type. + + + Invalid schema event. + + + Type for Versioning needs to be derived from IEventRequest + + + Cannot find related listener for function: {0}, available functions: {1} + + + An HttpRequestMessage is required + + + Cannot find json path set value + + + Invalid Payload detected. + + + Response validation failed, see inner exceptions. + + + Return type is invalid, please return either an AuthEventResponse, HttpResponse, HttpResponseMessage or string in your function return. + + + Invalid version on Schema + + + Cannot assign legacy payload to cloud events. + + + Cannot find the correct Event Metadata based on the input parameter: {0} + + + Please supply the function name via the query parameter: functionName, available functions: {0} + + + Cannot find the response on the request. + + + No Actions Found. Please supply atleast one action. + + + Cannot determine the validation path, missing version attribute on Version Enum. + + + Could not get exclusive lock to the resource. + + + No associated open api document found for version. + + + The parameter on the function signature does not match the expected parameter type of '{0}'. Got: '{1}' + + + The response is expected to be of type: {0} + + + Invalid token version {0}, supported versions are: {1} + + + Please supply both the TenantId and AudienceAppId in variables in your binding configuration. (Or app settings {0} and {1}) + + + Listener registered at: {0} + + + Function name: + + + Function url: + + + -------------------------- + + + Ready + + + The '{0}' field is missing. + + + The '{0}' field needs to contain one of the following values: + + \ No newline at end of file diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventResponseHandler.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventResponseHandler.cs new file mode 100644 index 000000000000..e103cbd836f0 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventResponseHandler.cs @@ -0,0 +1,241 @@ +// 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.Globalization; +using System.IO; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + /// Contains the IActionRequest response when the function is called. + /// + public class AuthenticationEventResponseHandler : IValueBinder + { + /// The response property. + internal const string EventResponseProperty = "$event$response"; + + /// Gets or sets the action result. + /// The action result. + public AuthenticationEventResponse Response { get; internal set; } + + /// Gets the type. + /// The type. + public Type Type => typeof(AuthenticationEventResponse).MakeByRefType(); + + /// Gets or sets the request. + /// The associated request. + public AuthenticationEventRequestBase Request { get; internal set; } + + /// Gets the value asynchronous. + /// + ///
+ ///
+ public Task GetValueAsync() + { + return Task.FromResult(Response); + } + + /// Sets the value asynchronous. + /// The result. + /// The cancellation token. + /// + /// A task flag as completed. + /// + public Task SetValueAsync(object result, CancellationToken cancellationToken) + { + try + { + if (result == null) + { + throw new ArgumentNullException(AuthenticationEventResource.Ex_Invalid_Return); + } + + if (result is AuthenticationEventResponse action) + { + Response = action; + } + else + { + AuthenticationEventResponse response = Request.GetResponseObject(); + if (response == null) + { + throw new InvalidOperationException(AuthenticationEventResource.Ex_Missing_Request_Response); + } + + Response = GetActionResult(result, response); + } + + if (Response.StatusCode == System.Net.HttpStatusCode.OK) + { + Response.Validate(); + Response.Invalidate(); + } + } + catch (Exception ex) + { + Response = Request.Failed(ex).Result; + } + + return Task.CompletedTask; + } + + internal AuthenticationEventResponse GetActionResult(object result, AuthenticationEventResponse response) + { + AuthenticationEventJsonElement jResult; + + //If the request was unsuccessful we return the IActionResult based on the error and do no further processing. + if (Request.RequestStatus != RequestStatusType.Successful) + { + return Request.Failed(null).Result; + } + else if (result is string strResult)//A string was returned from the function execution + { + jResult = GetJsonObjectFromString(strResult); + } + else if (result is HttpResponse httpResponse)//A HttpResponse Object was returned from the function execution + { + jResult = GetJsonObjectFromHttpResponse(httpResponse); + } + else if (result is HttpResponseMessage responseMessage)//A HttpResponseMessage Object was returned from the function execution + { + jResult = GetJsonObjectFromHttpResponseMessage(responseMessage); + } + else if (result is Stream stream)//A HttpResponseMessage Object was returned from the function execution + { + jResult = GetJsonObjectFromStream(stream); + } + else//An unexpected return type was found + { + throw new InvalidCastException(AuthenticationEventResource.Ex_Invalid_Return); + } + + AuthenticationEventResponse convertedResponse = ConvertToEventResponse(jResult, response.GetType()); + + if (convertedResponse != null && !string.IsNullOrEmpty(response.Body)) + { + if (convertedResponse.GetType() != response.GetType()) + { + throw new Exception(string.Format(CultureInfo.CurrentCulture, AuthenticationEventResource.Ex_Response_Mismatch, Request.GetResponseObject().GetType())); + } + + if (string.IsNullOrEmpty(convertedResponse.Body)) + { + convertedResponse.Body = response.Body; + } + + return convertedResponse; + } + + return GetAuthEventFromJObject(jResult, response); + } + + /// Tries to convert a JSON response payload to an string type EventResponse. + /// The response payload. + /// Type of the response to generate. + /// If the EventResponse is generated then the Typed EventResponse else null. + internal static AuthenticationEventResponse ConvertToEventResponse(AuthenticationEventJsonElement response, Type responseType) + { + if (response.Properties.ContainsKey("data") && response.Properties["data"] is AuthenticationEventJsonElement jsonElement) + { + response = jsonElement; + } + + return responseType.BaseType.GetGenericTypeDefinition() == typeof(ActionableResponse<>) || + responseType.BaseType.GetGenericTypeDefinition() == typeof(ActionableCloudEventResponse<>) ? + (AuthenticationEventResponse)JsonSerializer.Deserialize(response.ToString(), responseType, GetSerializerOptions()) : + null; + } + + private static JsonSerializerOptions GetSerializerOptions() + { + JsonSerializerOptions options = new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = true + }; + options.Converters.Add(new ActionConverterFactoryOfT()); + return options; + } + + internal static AuthenticationEventJsonElement GetJsonObjectFromHttpResponseMessage(HttpResponseMessage httpResponseMessage) + { + return GetJsonObjectFromString(httpResponseMessage.Content.ReadAsStringAsync().Result); + } + + internal static AuthenticationEventJsonElement GetJsonObjectFromHttpResponse(HttpResponse result) + { + return GetJsonObjectFromStream(result.Body); + } + + internal static AuthenticationEventJsonElement GetJsonObjectFromStream(Stream stream) + { + // Build up the request body in a string builder. + StringBuilder builder = new StringBuilder(); + + // Rent a shared buffer to write the request body into. + byte[] buffer = ArrayPool.Shared.Rent(4096); + + while (true) + { + var bytesRemaining = stream.Read(buffer, offset: 0, buffer.Length); + if (bytesRemaining == 0) + { + break; + } + + // Append the encoded string into the string builder. + var encodedString = Encoding.UTF8.GetString(buffer, 0, bytesRemaining); + builder.Append(encodedString); + } + + ArrayPool.Shared.Return(buffer); + + return GetJsonObjectFromString(builder.ToString()); + } + + internal static AuthenticationEventJsonElement GetJsonObjectFromString(string result) + { + return !Helpers.IsJson(result) + ? throw new InvalidCastException(AuthenticationEventResource.Ex_Invalid_Return) + : new AuthenticationEventJsonElement(result); + } + + internal static AuthenticationEventResponse GetAuthEventFromJObject(AuthenticationEventJsonElement result, AuthenticationEventResponse response) + { + //see if the jObject contains an error + if (result.Properties.ContainsKey("error")) + { + throw new Exception(result.GetPropertyValue("error")); + } + + if (result.Properties.ContainsKey("schema")) + { + result.Properties.Remove("schema"); + } + + var jBody = new AuthenticationEventJsonElement(response.Body); + jBody.Merge(result); + + response.Body = jBody.ToString(); + + return response; + } + + /// Converts to invokestring. + /// + /// The string "response". + /// + public string ToInvokeString() + { + return "response"; + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventValueBinder.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventValueBinder.cs new file mode 100644 index 000000000000..29ff56621fa1 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventValueBinder.cs @@ -0,0 +1,58 @@ +// 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 System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + /// Our response value binder that has reference the EventResponse. + /// + internal class AuthenticationEventValueBinder : IValueBinder + { + private object _value; + private readonly AuthenticationEventsTriggerAttribute _attr; + /// Gets the type. + /// The type. + public Type Type => _attr.IsParameterString ? typeof(string) : typeof(AuthenticationEventRequestBase); + + /// Initializes a new instance of the class. + /// The EventRequest event as the value. + /// The event trigger attribute assign to the function that we are assigning the value from. + internal AuthenticationEventValueBinder(object value, AuthenticationEventsTriggerAttribute attribute) + { + _attr = attribute; + _value = _attr.IsParameterString ? value.ToString() : value; + } + + /// Gets the value asynchronous. + /// The EventRequest.
+ /// + public Task GetValueAsync() + { + return Task.FromResult(_value); + } + + /// Sets the value asynchronous. + /// The EventResponse as the value. + /// The cancellation token. + /// A task flagged as completed. + /// + public Task SetValueAsync(object value, CancellationToken cancellationToken) + { + _value = value; + return Task.CompletedTask; + } + + /// Converts to string. + /// The string representation of the EventResponse. + /// + public string ToInvokeString() + { + return _value.ToString(); + } + } +} \ No newline at end of file diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventWebJobsStartup.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventWebJobsStartup.cs new file mode 100644 index 000000000000..8eabeef611b3 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventWebJobsStartup.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents; +using Microsoft.Azure.WebJobs.Hosting; + +[assembly: WebJobsStartup(typeof(AuthenticationEventWebJobsStartup))] +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + /// Entry point for our trigger and bindings. + public class AuthenticationEventWebJobsStartup : IWebJobsStartup//fix + { + /// Configures the specified builder. + /// The builder. + public void Configure(IWebJobsBuilder builder) + { + builder.AddExtension(); + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventsTriggerAttribute.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventsTriggerAttribute.cs new file mode 100644 index 000000000000..efc796205014 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/AuthenticationEventsTriggerAttribute.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Azure.WebJobs.Description; +using System; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + /// Authentication Event Trigger that will trigger incoming authentication events. + [AttributeUsage(AttributeTargets.Parameter)] +#pragma warning disable CS0618 // Type or member is obsolete + [Binding(TriggerHandlesReturnValue = true)] +#pragma warning restore CS0618 // Type or member is obsolete + + public class AuthenticationEventsTriggerAttribute : Attribute + { + /// Initializes a new instance of the class. + public AuthenticationEventsTriggerAttribute() + { + } + + /// Gets or sets the tenant identifier. + /// The tenant identifier. + public string TenantId { get; set; } + + /// Gets or sets the audience application identifier. + /// The audience application identifier. + public string AudienceAppId { get; set; } + + internal bool IsParameterString { get; set; } = true; + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/ConfigurationManager.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/ConfigurationManager.cs new file mode 100644 index 000000000000..9b1d64acc6c4 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/ConfigurationManager.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + internal class ConfigurationManager + { + public static Dictionary SERVICES = new Dictionary() + { + { "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 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"; + + private readonly AuthenticationEventsTriggerAttribute triggerAttribute; + internal ConfigurationManager(AuthenticationEventsTriggerAttribute triggerAttribute) + { + this.triggerAttribute = triggerAttribute; + } + + 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); + + internal static bool GetService(string serviceId, out ServiceInfo serviceInfo) + { + serviceInfo = null; + if (serviceId is null) + { + throw new ArgumentNullException(nameof(serviceId)); + } + + if (CallerAppId != null && serviceId.Equals(CallerAppId)) + { + serviceInfo = SERVICES.Values.FirstOrDefault(x => x.DefaultService); + } + else if (SERVICES.ContainsKey(serviceId)) + { + serviceInfo = SERVICES[serviceId]; + } + + return serviceInfo != null; + } + + internal static bool VerifyServiceId(string testId) + { + return GetService(testId, out _); + } + + private static string GetConfigValue(string environmentVariable, string defaultValue) + { + return Environment.GetEnvironmentVariable(environmentVariable) ?? defaultValue; + } + + private static T GetConfigValue(string environmentVariable, T defaultValue) where T : struct + { + return Environment.GetEnvironmentVariable(environmentVariable) == null ? + defaultValue : + (T)Convert.ChangeType(Environment.GetEnvironmentVariable(environmentVariable), typeof(T), CultureInfo.CurrentCulture); + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/EventDefinition.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/EventDefinition.cs new file mode 100644 index 000000000000..082b3030b0d8 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/EventDefinition.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework; +using tisEvents = Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + /// Events available by event type. + public enum EventDefinition + { + /// onTokenIssuanceStart event -> preview 10 01 2021. + [AuthenticationEventMetadata(typeof(tisEvents.TokenIssuanceStartRequest), + "onTokenIssuanceStartCustomExtension", + "TokenIssuanceStart.preview_10_01_2021")] + TokenIssuanceStartV20211001Preview, + /// onTokenIssuanceStart event. + [AuthenticationEventMetadata(typeof(tisEvents.TokenIssuanceStartRequest), + "microsoft.graph.authenticationEvent.TokenIssuanceStart", + "TokenIssuanceStart", responseTemplate: "CloudEventActionableTemplate.json")] + TokenIssuanceStart + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/EventType.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/EventType.cs new file mode 100644 index 000000000000..48b78490fdc1 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/EventType.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + /// Types of events to listen for and attach a function to. + public enum EventType + { + /// When a token is issued, this event will be called and the ability to append claim to the token is enabled via the response. + OnTokenIssuanceStart + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Extensions.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Extensions.cs new file mode 100644 index 000000000000..ad49ed7631d6 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Extensions.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using System.Net.Http.Headers; +using System.Text; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + /// Static class for extension methods. + internal static class Extensions + { + /// Verifies the claim based on the type and value. + /// The claims. + /// The type. + /// The value. + /// True if the type and value match. + internal static bool VerifyClaim(this IEnumerable claims, string type, string value) => + claims.Where(x => x.Type.Equals(type, StringComparison.OrdinalIgnoreCase)) + .Any(x => x.Value.Equals(value, StringComparison.OrdinalIgnoreCase)); + + /// Gets the attribute that is assigned to an enum. + /// The type of the attribute. + /// The enum to check. + /// Return the Attribute if found. + internal static TAttribute GetAttribute(this Enum value) where TAttribute : Attribute + { + var type = value.GetType(); + var name = Enum.GetName(type, value); + return type.GetField(name) + .GetCustomAttributes(false) + .OfType() + .SingleOrDefault(); + } + + /// Gets the description attribute value assigned to an enum. + /// The value. + /// The description string value if found. + public static String GetDescription(this Enum value) + { + var description = GetAttribute(value); + return description?.Description; + } + + /// Adds a range of objects to a dictionary. + /// The type of the key. + /// The type of the value. + /// The dictionary to append to. + /// The dictionary to append from. + public static void AddRange(this IDictionary dictionary, IReadOnlyDictionary other) + { + if (other == null) + { + return; + } + + foreach (var pair in other) + { + dictionary[pair.Key] = pair.Value; + } + } + + internal static bool Matches(this HttpRequestHeaders headers, string key, string value) => headers.Any(h => h.Key.Equals(key, StringComparison.OrdinalIgnoreCase) && h.Value.Any(v => v.Equals(value, StringComparison.OrdinalIgnoreCase))); + + internal static string DecodeBase64(this HttpRequestHeaders headers, string key) + { + return headers.Contains(key) ? Encoding.UTF8.GetString(Convert.FromBase64String(headers.GetValues(key).First())) : String.Empty; + } + + internal static Dictionary ToDictionary(this NameValueCollection nvc) + { + Dictionary dic = new Dictionary(); + foreach (string key in nvc.Keys) + dic.Add(key, nvc[key]); + return dic; + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/ActionConverterFactoryOfT.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/ActionConverterFactoryOfT.cs new file mode 100644 index 000000000000..2ef758ee24a2 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/ActionConverterFactoryOfT.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework +{ + internal class ActionConverterFactoryOfT : JsonConverterFactory + { + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsGenericType ? typeof(AuthenticationEventAction).IsAssignableFrom(typeToConvert.GenericTypeArguments[0]) : false; + } + + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + Type actionType = typeToConvert.GenericTypeArguments[0]; + JsonConverter converter = (JsonConverter)Activator.CreateInstance( + typeof(ActionConverter<>).MakeGenericType(new Type[] { actionType })); + return converter; + } + + internal class ActionConverter : JsonConverter> where T : AuthenticationEventAction + { + public override List Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + List result = new List(); + AuthenticationEventJsonElement actions = new AuthenticationEventJsonElement(ref reader); + foreach (AuthenticationEventJsonElement action in actions.Elements) + { + T eventAction = (T)Helpers.GetEventActionForActionType(GetActionValue(action)); + if (eventAction != null) + { + eventAction.FromJson(action); + result.Add(eventAction); + } + } + + return result.Count == 0 ? throw new Exception(AuthenticationEventResource.Ex_No_Action) : result; + } + + public override void Write(Utf8JsonWriter writer, List value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value); + } + + private static string GetActionValue(AuthenticationEventJsonElement jAction) + { + if (jAction.Properties.ContainsKey("actionType")) + { + return jAction.GetPropertyValue("actionType"); + } + else if (jAction.Properties.ContainsKey("type")) + { + return jAction.GetPropertyValue("type"); + } + else if (jAction.Properties.ContainsKey("@odata.type")) + { + return jAction.GetPropertyValue("@odata.type"); + } + + return null; + } + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/ActionableCloudEventResponse.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/ActionableCloudEventResponse.cs new file mode 100644 index 000000000000..660ee1e20044 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/ActionableCloudEventResponse.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Actions; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework +{ + /// Abstract class for any responses that implement an cloud event payload and has actions on it. + /// Of type EventAction. + /// + public abstract class ActionableCloudEventResponse : ActionableResponse where T : AuthenticationEventAction + { + /// Gets the Cloud Event @odata.type. + /// Gets the Cloud Event @odata.type. + [JsonPropertyName("oDataType")] + [Required] + public string ODataType { get { return DataTypeIdentifier; } } + internal abstract string DataTypeIdentifier { get; } + + /// Invalidates this instance. + /// Subsequently invalidates the actions. + internal override void Invalidate() + { + Actions.RemoveAll(a => a == null); + AuthenticationEventJsonElement eventJsonElement = new AuthenticationEventJsonElement(Body); + if (eventJsonElement.SetProperty(ODataType, "data", "@odata.type")) + { + if (Actions.Any(a => LegacyMap.Any(m => m.Value == a.GetType()))) + throw new Exception(AuthenticationEventResource.Ex_Leg_payload); + Body = eventJsonElement.ToString(); + } + else//TODO: Remove when support for legacy data contracts are dropped. + { + for (int i = Actions.Count - 1; i >= 0; i--) + { + if (LegacyMap.ContainsKey(Actions[i].GetType())) + { + Actions[i] = (T)Activator.CreateInstance(LegacyMap[Actions[i].GetType()], Actions[i]); + } + } + } + + base.Invalidate(); + } + + //TODO: Remove when support for legacy data contracts are dropped. + internal static Dictionary LegacyMap => new Dictionary() { + {typeof(ProvideClaimsForToken),typeof(ProvideClaimsForTokenLegacy) } + }; + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/ActionableResponse.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/ActionableResponse.cs new file mode 100644 index 000000000000..111fc6412589 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/ActionableResponse.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework +{ + /// And abstract class for responses that implements actions. + /// Of type EventAction. + /// + /// + + public abstract class ActionableResponse : AuthenticationEventResponse where T : AuthenticationEventAction + { + /// Gets or sets the actions. + /// The actions. + [JsonPropertyName("actions")] + public List Actions { get; set; } = new List(); + + /// Invalidates this instance. + /// Subsequently invalidates the actions. + /// + internal override void Invalidate() + { + InvalidateActions(); + } + + /// Invalidates the actions. + internal void InvalidateActions() + { + string actionElement = "actions"; + + AuthenticationEventJsonElement jPayload = new AuthenticationEventJsonElement(Body); + AuthenticationEventJsonElement jActions = jPayload.FindFirstElementNamed(actionElement); + if (jActions == null) + { + jActions = new AuthenticationEventJsonElement(); + jPayload.Properties.Add(actionElement, jActions); + } + + foreach (T action in Actions) + { + AuthenticationEventJsonElement jBody = action.BuildActionBody(); + AuthenticationEventJsonElement jAction = jActions.FindFirstElementWithPropertyNamed(action.TypeProperty); + + if (jAction == null || !jAction.Properties[action.TypeProperty].ToString().Equals(action.ActionType, StringComparison.OrdinalIgnoreCase)) + { + jAction = new AuthenticationEventJsonElement(); + jActions.Elements.Add(jAction); + } + else + { + jAction.RemoveAll(); + } + + jAction.Properties.Add(action.TypeProperty, action.ActionType); + jAction.Merge(jBody); + } + + Body = jPayload.ToString(); + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventAction.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventAction.cs new file mode 100644 index 000000000000..e29e3ee87f52 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventAction.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework +{ + /// Abstract class for response actions. + public abstract class AuthenticationEventAction + { + /// Gets the type of the action. + /// This will be used as the name of the action in the response Json. + /// The type of the action. + [JsonPropertyName("actionType")] + internal abstract string ActionType { get; } + internal virtual string TypeProperty => "@odata.type"; + /// Initializes a new instance of the class. + public AuthenticationEventAction() { } + + /// Builds the action body. + /// The return will be the json of the action. + internal abstract AuthenticationEventJsonElement BuildActionBody(); + + /// Creates the action from Json. + /// The action body. + internal abstract void FromJson(AuthenticationEventJsonElement actionBody); + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventData.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventData.cs new file mode 100644 index 000000000000..fc2d55f65c1c --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventData.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework +{ + /// Or Data class that represents the inbound Json payload, also has helper functions for serialization. + public abstract class AuthenticationEventData + { + /// Gets the event identifier. + /// The event identifier. + [JsonPropertyName("tenantId")] + public Guid TenantId { get; set; } + + /// Gets the event identifier. + /// The event identifier. + [JsonPropertyName("authenticationEventListenerId")] + public Guid AuthenticationEventListenerId { get; set; } + + /// Gets or sets the custom extension identifier. + /// The custom extension identifier. + [JsonPropertyName("AuthenticationEventsId")] + public Guid AuthenticationEventsId { get; set; } + + /// Gets the Json settings. + /// Which is over-ridable for sub class. + /// The json settings. + private JsonSerializerOptions JsonSettings + { + get + { + var jsonOptions = new JsonSerializerOptions() + { + WriteIndented = true, + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + InitJsonSerializerSettings(jsonOptions); + return jsonOptions; + } + } + + internal virtual void InitJsonSerializerSettings(JsonSerializerOptions jsonSerializerSettings) { } + + /// De-serializes the json the its associated typed object. + /// The json containing the typed structure. + /// Returns the typed structure that inherits EventData. + internal virtual AuthenticationEventData FromJson(AuthenticationEventJsonElement json) => (AuthenticationEventData)JsonSerializer.Deserialize(json.ToString(), GetType(), JsonSettings); + + /// Creates an instance of a EventDaa sub class based on the type and json payload via reflection. + /// The type to create. + /// The json payload. + /// A created instance of EventData based on the Type. + /// + internal static AuthenticationEventData CreateInstance(Type type, AuthenticationEventJsonElement json) + { + AuthenticationEventData data = (AuthenticationEventData)Activator.CreateInstance(type, true); + return data.FromJson(json); + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventJsonElement.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventJsonElement.cs new file mode 100644 index 000000000000..c8f8298d3022 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventJsonElement.cs @@ -0,0 +1,515 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework +{ + /// Class to wrap System.Json.Text.JsonElement. + internal class AuthenticationEventJsonElement : ICloneable + { + private JsonElement _jsonElement; + + /// Gets or sets the properties. + /// The properties. + internal Dictionary Properties { get; } = new Dictionary(); + /// Gets or sets the string value. + /// The value. + internal string Value { get; set; } + /// Gets or sets the child json elements. + /// The elements. + internal List Elements { get; } = new List(); + /// If the object is derived from a property, the is represents the property name. + /// The key. + internal string Key { get; set; } + + /// Initializes a new instance of the class. + internal AuthenticationEventJsonElement() { } + + /// Initializes a new instance of the class. + /// Predefined properties and values. + internal AuthenticationEventJsonElement(Dictionary properties) + { + Properties = properties; + } + + /// Initializes a new instance of the class. + /// A json string to build the object from. + internal AuthenticationEventJsonElement(string value) + { + Utf8JsonReader reader = new Utf8JsonReader(new ReadOnlySequence(Encoding.UTF8.GetBytes(value))); + if (!BuildElement(ref reader)) + { + Value = value; + } + } + + /// Initializes a new instance of the class. + /// A json reader to build the object from. + internal AuthenticationEventJsonElement(ref Utf8JsonReader reader) + { + BuildElement(ref reader); + } + + /// Initializes a new instance of the class. + /// A System.Text.Json.JsonElement to build the object from. + internal AuthenticationEventJsonElement(JsonElement jsonElement) + { + BuildElement(jsonElement); + } + + /// Builds the element. + /// The reader. + /// If the element was built successfully then true. + internal bool BuildElement(ref Utf8JsonReader reader) + { + return JsonDocument.TryParseValue(ref reader, out JsonDocument jDoc) ? BuildElement(jDoc.RootElement) : false; + } + + /// Builds the element. + /// The json element. + /// True. + internal bool BuildElement(JsonElement jsonElement) + { + _jsonElement = jsonElement; + + if (_jsonElement.ValueKind == JsonValueKind.String) + { + Value = _jsonElement.GetString(); + } + else if (_jsonElement.ValueKind == JsonValueKind.Object) + { + foreach (var prop in _jsonElement.EnumerateObject()) + Properties.Add(prop.Name, GetUnderlying(prop.Value, prop.Name)); + } + else if (_jsonElement.ValueKind == JsonValueKind.Array) + { + foreach (var prop in _jsonElement.EnumerateArray()) + Elements.Add(new AuthenticationEventJsonElement(prop)); + } + + return true; + } + + /// Sets the property value. + /// The type of the property value to be set. + /// The property value. + /// The json path. + /// If the property exists and was set successfully then true else false. + internal bool SetProperty(T propertyValue, params string[] path) + { + (string key, Dictionary props) = FindPropertyDictionary(false, path); + if (key != null) + { + props[key] = propertyValue; + return true; + } + + return false; + } + + /// Finds the elements based on the parent property name. + /// The name. + /// A list of all elements that match the name. + internal List FindElementsNamed(string name) + { + List result = new List(); + SearchForElements(name, result, this); + return result; + } + + /// Finds the elements with that has a child property that matches the name parameter. + /// The name. + /// A list of elements that contain the property name. + internal List FindElementsWithPropertyNamed(string name) + { + List result = new List(); + SearchForElements(name, result, this, false, true); + return result; + } + + /// Finds the first element that that property name matches the name parameter. + /// The name. + /// The first element found else null. + internal AuthenticationEventJsonElement FindFirstElementNamed(string name) + { + List result = new List(); + SearchForElements(name, result, this, true); + return result.FirstOrDefault(); + } + + /// Finds the first element with that has a child property that matches the name parameter. + /// The name. + /// The first element matched or null. + internal AuthenticationEventJsonElement FindFirstElementWithPropertyNamed(string name) + { + List result = new List(); + SearchForElements(name, result, this, true, true); + return result.FirstOrDefault(); + } + + /// Finds the elements that property matches the regular expression parameter. + /// The regular expression. + /// + /// A list of elements that match. + /// + internal List FindElementsByExpression(Regex expression) + { + List result = new List(); + SearchForElementsByRegex(expression, result, this); + return result; + } + + /// Finds the elements with that has a child property that matches on the regular expression parameter. + /// The regular expression. + /// All elements that matching child properties. + internal List FindElementsByPropertyExpression(Regex expression) + { + List result = new List(); + SearchForElementsByRegex(expression, result, this, true); + return result; + } + + internal void RenameProperty(string oldProperty, string newProperty) + { + if (Properties.ContainsKey(oldProperty)) + { + Properties.Add(newProperty, FindFirstElementNamed(oldProperty)); + Properties.Remove(oldProperty); + } + } + + private void SearchForElementsByRegex(Regex expresson, List container, AuthenticationEventJsonElement element, bool withPropertyValue = false) + { + foreach (KeyValuePair keyValue in element.Properties) + { + if (expresson.IsMatch(keyValue.Key)) + { + if (keyValue.Value is AuthenticationEventJsonElement ele) + { + container.Add(ele); + } + else if (withPropertyValue) + { + container.Add(element); + } + } + else if (withPropertyValue && keyValue.Value is string checkValue && expresson.IsMatch(checkValue)) + { + container.Add(element); + } + + if (keyValue.Value is AuthenticationEventJsonElement e) + { + SearchForElementsByRegex(expresson, container, e, withPropertyValue); + } + } + foreach (AuthenticationEventJsonElement eventJsonElement in element.Elements) + SearchForElementsByRegex(expresson, container, eventJsonElement, withPropertyValue); + } + + private void SearchForElements(string name, List container, AuthenticationEventJsonElement element, bool findFirst = false, bool withPropertyValue = false) + { + foreach (KeyValuePair keyValue in element.Properties) + { + if (keyValue.Key.Equals(name)) + { + if (keyValue.Value is AuthenticationEventJsonElement ele) + { + container.Add(ele); + } + else if (withPropertyValue) + { + container.Add(element); + } + + if (findFirst) + { + break; + } + } + else if (withPropertyValue && keyValue.Value is string checkValue && name.Equals(checkValue)) + { + container.Add(element); + } + + if ((container.Count == 0 || !findFirst) && (keyValue.Value is AuthenticationEventJsonElement e)) + { + SearchForElements(name, container, e, findFirst, withPropertyValue); + } + } + if (container.Count == 0 || !findFirst) + { + foreach (AuthenticationEventJsonElement eventJsonElement in element.Elements) + SearchForElements(name, container, eventJsonElement, findFirst, withPropertyValue); + } + } + + /// Gets the property value for a string value. + /// The path to the element. (For example "ParentElement", "ChildElement"). + /// String value of the property or null. + internal string GetPropertyValue(params string[] path) + { + return GetPropertyValue(path); + } + + /// Gets the property value of a certain type. + /// The type to return. + /// The path to the element. (For example "ParentElement", "ChildElement"). + /// The object if able to case or null if not found. + internal T GetPropertyValue(params string[] path) + { + return FindPropertyValue(out T result, path) ? result : default; + } + + /// Gets the property value of a certain type. + /// The type to return. + /// If the property exists return the result of type T. + /// The path to the element. (For example "ParentElement", "ChildElement"). + /// True of the property exists else false. + internal bool FindPropertyValue(out T result, params string[] path) + { + (string key, Dictionary props) = FindPropertyDictionary(false, path); + if (key != null) + { + result = (T)props[key]; + return true; + } + else + { + result = (T)Activator.CreateInstance(typeof(T)); + return false; + } + } + + /// Verifies that a JSON path exists. + /// The path. + /// True if the elements and sub-elements are present. + internal bool PathExists(params string[] path) + { + (string key, Dictionary props) = FindPropertyDictionary(false, path); + return key != null && props.ContainsKey(key); + } + + /// Finds the property dictionary that the property key belongs to based on the json path.. + /// if set to true created the property if it does not exists in the current search path.. + /// The path to the element. (For example "ParentElement", "ChildElement"). + /// Returns the key and it's related dictionary. + internal (string Key, Dictionary Container) FindPropertyDictionary(bool create = false, params string[] path) + { + AuthenticationEventJsonElement current = this; + for (int i = 0; i < path.Length - 1; i++) + { + if (current != null && current.Properties.ContainsKey(path[i]) && current.Properties[path[i]] is AuthenticationEventJsonElement jsonElement) + { + current = jsonElement; + } + else + { + if (create) + { + AuthenticationEventJsonElement newEle = new AuthenticationEventJsonElement(); + current.Properties.Add(path[i], newEle); + current = newEle; + } + else + current = null; + } + } + + if (current != null && current.Properties.ContainsKey(path.Last()) && current.Properties[path.Last()] != null) + { + return (path.Last(), current.Properties); + } + else if (create) + { + AuthenticationEventJsonElement newEle = new AuthenticationEventJsonElement(); + current.Properties.Add(path.Last(), newEle); + return (path.Last(), current.Properties); + } + + return (null, null); + } + + /// Removes all elements and properties. + internal void RemoveAll() + { + Properties.Clear(); + Value = null; + Elements.Clear(); + } + + /// Merges the specified element with another element, coping the parameter element's properties and elements. If there is a conflict, the parameter element value is used. + /// The element to merge in. + internal void Merge(AuthenticationEventJsonElement element) + { + foreach (string key in element.Properties.Keys) + { + object value = element.Properties[key]; + + if (!Properties.ContainsKey(key)) + { + Properties.Add(key, (value is ICloneable cloneable) ? cloneable.Clone() : value); + } + else + { + if (Properties[key] is AuthenticationEventJsonElement subElement && value is AuthenticationEventJsonElement mergeSubElement) + { + subElement.Merge(mergeSubElement); + } + else + { + Properties[key] = (value is ICloneable cloneable) ? cloneable.Clone() : value; + } + } + } + Value = element.Value; + foreach (AuthenticationEventJsonElement e in element.Elements) + Elements.Add((AuthenticationEventJsonElement)e.Clone()); + } + + /// Output the element to a json string. + /// A that represents this instance as json. + public override string ToString() + { + using (MemoryStream ms = new MemoryStream()) + { + using (var jsonWriter = new Utf8JsonWriter(ms)) + { + WriteTo(jsonWriter); + + jsonWriter.Flush(); + return Encoding.UTF8.GetString(ms.ToArray()); + } + } + } + + private void WriteTo(Utf8JsonWriter jsonWriter) + { + if (_jsonElement.ValueKind == JsonValueKind.Object || _jsonElement.ValueKind == JsonValueKind.Undefined) + { + if (Properties.Count > 0) + { + jsonWriter.WriteStartObject(); + foreach (string propertyName in Properties.Keys) + { + jsonWriter.WritePropertyName(propertyName); + WriteObjectToWriter(jsonWriter, Properties[propertyName]); + } + jsonWriter.WriteEndObject(); + } + else if (Elements.Count == 1)//If there are no properties but one child element attached to the parent property write it out. + { + WriteObjectToWriter(jsonWriter, Elements[0]); + } + else if (Elements.Count > 1)//If there are no properties but child elements attached to the parent property write it out to array. + { + WriteObjectToWriter(jsonWriter, Elements); + } + else if (_jsonElement.ValueKind == JsonValueKind.Object)//Write out empty object. + { + jsonWriter.WriteStartObject(); + jsonWriter.WriteEndObject(); + } + else if (!string.IsNullOrEmpty(Value))//If there are no Elements or Properties but a value, write out the value. + { + WriteObjectToWriter(jsonWriter, Value); + } + else if (Properties.Count == 0) + { + jsonWriter.WriteStartObject(); + jsonWriter.WriteEndObject(); + } + } + else if (_jsonElement.ValueKind == JsonValueKind.Array) + { + WriteObjectToWriter(jsonWriter, Elements); + } + else if (_jsonElement.ValueKind == JsonValueKind.String) + { + WriteObjectToWriter(jsonWriter, Value); + } + } + + private static void WriteObjectToWriter(Utf8JsonWriter writer, object value) + { + if (value == null) + { + writer.WriteNullValue(); + } + else + { + switch (value) + { + case string v: + writer.WriteStringValue(v); + break; + case bool v: + writer.WriteBooleanValue(v); + break; + case decimal v: + writer.WriteNumberValue(v); + break; + case int v: + writer.WriteNumberValue(v); + break; + case double v: + writer.WriteNumberValue(v); + break; + case float v: + writer.WriteNumberValue(v); + break; + case DateTime v: + writer.WriteStringValue(v); + break; + case Guid v: + writer.WriteStringValue(v); + break; + case AuthenticationEventJsonElement v: + v.WriteTo(writer); + break; + case List arr: + writer.WriteStartArray(); + foreach (var item in arr) + item.WriteTo(writer); + writer.WriteEndArray(); + break; + default: + writer.WriteStringValue(value.ToString()); + break; + } + } + } + + private static object GetUnderlying(JsonElement value, string key) + { + switch (value.ValueKind) + { + case JsonValueKind.False: return false; + case JsonValueKind.Null: return null; + case JsonValueKind.Number: return value.GetInt32(); + case JsonValueKind.True: return true; + case JsonValueKind.String: return value.GetString(); + default: + return new AuthenticationEventJsonElement(value) + { + Key = key + }; + } + } + + /// Creates a new object that is a copy of the current instance. + /// A new object that is a copy of this instance. + public object Clone() + { + return MemberwiseClone(); + } + } +} \ No newline at end of file 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 new file mode 100644 index 000000000000..d4398cc8d954 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventMetadata.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Globalization; +using System.Net.Http; +using System.Reflection; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework +{ + /// Represents event meta-data. + internal class AuthenticationEventMetadata + { + /// Gets or sets the type of the request. + /// The type of the request. + internal Type RequestType { get; set; } + + /// Gets or sets the identifier. + /// The identifier. + internal EventDefinition Id { get; set; } + + /// Gets or sets the response template content. + /// The response template content. + internal string ResponseTemplate { get; set; } + + /// Creates the event request,response and data model instances for the related event. + /// The incoming HTTP request message. + /// The Json payload. + /// The arguments. + /// A newly create EventRequest with related EventResponse and EventData based on event type. + /// + /// + internal AuthenticationEventRequestBase CreateEventRequestValidate(HttpRequestMessage request, string payload, params object[] args) + { + return CreateEventRequest(request, payload, true, args); + } + + /// Creates the event request,response and data model instances for the related event. + /// The incoming HTTP request message. + /// The Json payload. + /// Validate the generated object. + /// The arguments. + /// A newly create EventRequest with related EventResponse and EventData based on event type. + /// + /// + internal AuthenticationEventRequestBase CreateEventRequest(HttpRequestMessage request, string payload, bool validate, params object[] args) + { + AuthenticationEventRequestBase eventRequest = (AuthenticationEventRequestBase)Activator.CreateInstance(RequestType, new object[] { request }); + PropertyInfo responseInfo = eventRequest.GetType().GetProperty("Response"); + PropertyInfo dataInfo = eventRequest.GetType().GetProperty("Payload"); + + AuthenticationEventResponse eventResponse = AuthenticationEventResponse.CreateInstance( + responseInfo.PropertyType, + // ResponseSchema ?? string.Empty, + ResponseTemplate ?? string.Empty); + + responseInfo.SetValue(eventRequest, eventResponse); + + if (args != null && args.Length != 0) + { + eventRequest.InstanceCreated(args); + } + + if (!string.IsNullOrEmpty(payload)) + { + AuthenticationEventJsonElement jsonPayload = new AuthenticationEventJsonElement(payload); + eventResponse.InstanceCreated(jsonPayload); + dataInfo.SetValue(eventRequest, AuthenticationEventData.CreateInstance(dataInfo.PropertyType, jsonPayload)); + eventRequest.ParseInbound(jsonPayload); + } + + eventRequest.StatusMessage = AuthenticationEventResource.Status_Good; + + if (validate) + { + Helpers.ValidateGraph(eventRequest); + } + + return eventRequest; + } + + internal static AuthenticationEventRequestBase CreateEventRequest(HttpRequestMessage request, Type type, params object[] args) + { + foreach (EventDefinition eventDefinition in Enum.GetValues(typeof(EventDefinition))) + { + AuthenticationEventMetadataAttribute eventMetadata = eventDefinition.GetAttribute(); + if (eventMetadata.RequestType == type) + { + AuthenticationEventMetadata metadata = AuthenticationEventMetadataLoader.GetEventMetadata(eventDefinition); + return metadata.CreateEventRequest(request, null, false, args); + } + } + + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, AuthenticationEventResource.Ex_Missing_Def, type)); + } + } +} \ No newline at end of file diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventMetadataLoader.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventMetadataLoader.cs new file mode 100644 index 000000000000..c879e0021621 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventMetadataLoader.cs @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework +{ + /// Lazy loader and caching for events and the event's related event. + internal class AuthenticationEventMetadataLoader + { + private static AuthenticationEventMetadataLoader _instance; + private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1); + private readonly Dictionary _events = new Dictionary(); + + /// Prevents a default instance of the class from being created. + private AuthenticationEventMetadataLoader() { } + + /// Gets the instance. + /// The instance. + internal static AuthenticationEventMetadataLoader Instance + { + get + { + if (_instance == null) + { + _instance = new AuthenticationEventMetadataLoader(); + } + + return _instance; + } + } + + /// Gets the event based on the event type. + /// The incoming payload to determine the correct event metadata to use. + /// The related event meta-data. + /// Is thrown when the event metadata attribute is not found on the event definition enum. + /// If not lock is achieved on the thread. + /// + internal static AuthenticationEventMetadata GetEventMetadata(string payload) + { + return GetEventMetadata(Helpers.GetEventDefintionFromPayload(payload)); + } + + internal static AuthenticationEventMetadata GetEventMetadata(EventDefinition eventDef) + { + var eventMetadataAttr = eventDef.GetAttribute(); + if (eventMetadataAttr == null) + { + throw new MissingFieldException(AuthenticationEventResource.Ex_No_Attr); + } + + AuthenticationEventMetadata eventMetadata = Instance._events.ContainsKey(eventMetadataAttr.EventIdentifier) ? Instance._events[eventMetadataAttr.EventIdentifier] : null; + + //If the current event meta-data is not associated to the event we load it from file, associated it and store it in memory + return eventMetadata ?? LoadFromResource(eventMetadataAttr); + } + + /// Loads from schema and payload resource files that are stored as an embedded resource. + /// The event metadata attribute with reference the schema files and template payloads. + /// EventMetadata with the contents of the embedded resources. + /// + private static AuthenticationEventMetadata LoadFromResource(AuthenticationEventMetadataAttribute eventMetadataAttr) + { + //As we read from files we lock this thread to only one execution at a time. + _semaphore.Wait(); + try + { + AuthenticationEventMetadata eventMetadata = Instance._events.ContainsKey(eventMetadataAttr.EventIdentifier) ? Instance._events[eventMetadataAttr.EventIdentifier] : null; + if (eventMetadata == null) + { + eventMetadata = CreateEventMetadata(eventMetadataAttr); + Instance._events.Add(eventMetadataAttr.EventIdentifier, eventMetadata); + } + + return eventMetadata; + } + finally + { + _semaphore.Release(); + } + } + + internal static AuthenticationEventMetadata CreateEventMetadata(AuthenticationEventMetadataAttribute eventMetadataAttr) + { + var assembly = Assembly.GetExecutingAssembly(); + var defaultNS = typeof(AuthenticationEventsTriggerAttribute).Namespace; + return new AuthenticationEventMetadata() + { + RequestType = eventMetadataAttr.RequestType, + ResponseTemplate = GetResponseTemplate(assembly, defaultNS ?? string.Empty, eventMetadataAttr) + }; + } + + private static string GetResponseTemplate(Assembly assembly, string defaultNS, AuthenticationEventMetadataAttribute verAttr) + { + string resource = string.Join(".", defaultNS, "Templates", verAttr.ResponseTemplate); + + if (!assembly.GetManifestResourceNames().Any(x => x == resource)) + { + resource = string.Join(".", defaultNS, "Templates", "ActionableTemplate.json"); + } + + return ReadResource(assembly, resource); + } + + /// Reads the embedded resource. + /// The assembly. + /// The resource. + /// If no resource is found, what default value should be used, if null an error is thrown. + /// The content of the embedded resource. + /// + private static string ReadResource(Assembly assembly, string resource, string defaultBody = null) + { + Stream stream = null; + StreamReader reader = null; + try + { + if (!assembly.GetManifestResourceNames().Any(x => x == resource)) + { + return defaultBody != null ? defaultBody : throw new Exception(AuthenticationEventResource.Ex_Invalid_EventSchema); + } + stream = assembly.GetManifestResourceStream(resource); + + reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + finally + { + if (reader != null) + { + reader.Close(); + reader = null; + } + if (stream != null) + { + stream.Close(); + stream = null; + } + } + } + } +} 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 new file mode 100644 index 000000000000..bc7279464b7e --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventRequest.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Net.Http; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework +{ + /// The base class for all typed event requests and its related response and data model. + /// The EventResponse related to the request. + /// The EventData model related to the request. + /// + /// + public abstract class AuthenticationEventRequest : AuthenticationEventRequestBase where TResponse : AuthenticationEventResponse where TData : AuthenticationEventData + { + /// Initializes a new instance of the class. + /// The request. + internal AuthenticationEventRequest(HttpRequestMessage request) : base(request) { } + /// Gets or sets the related EventResponse object. + /// The response. + /// + [JsonPropertyName("response")] + [Required] + public TResponse Response { get; set; } + + /// Gets or sets the related EventData model. + /// The Json payload. + /// + [JsonPropertyName("payload")] + [Required] + public TData Payload { get; set; } + + /// Validates the response and creates the IActionResult with the json payload based on the status of the request. + /// IActionResult based on the EventStatus (UnauthorizedResult, BadRequestObjectResult or JsonResult). + public async override Task Completed() + { + try + { + if (RequestStatus == RequestStatusType.Failed) + { + Response.MarkAsFailed(new Exception(String.IsNullOrEmpty(StatusMessage) ? AuthenticationEventResource.Ex_Gen_Failure : StatusMessage)); + } + + if (RequestStatus == RequestStatusType.TokenInvalid) + { + Response.MarkAsUnauthorized(); + } + } + catch (Exception ex) + { + return await Failed(ex).ConfigureAwait(false); + } + + return Response; + } + + /// Set the response to Failed mode. + /// The exception to return in the response. + /// The Underlying AuthEventResponse. + public override Task Failed(Exception exception) + { + Response.MarkAsFailed(exception); + return Task.FromResult((TResponse)Response); + } + } +} \ No newline at end of file diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventRequestBase.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventRequestBase.cs new file mode 100644 index 000000000000..8bc29d2859f1 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventRequestBase.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.Validators; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using System.Web; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework +{ + /// The base class for all typed event requests. + public abstract class AuthenticationEventRequestBase + { + private readonly Dictionary queryParameters; + /// Initializes a new instance of the class. + /// The HTTP request message. + internal AuthenticationEventRequestBase(HttpRequestMessage request) + { + HttpRequestMessage = request; + queryParameters = HttpUtility.ParseQueryString(request.RequestUri.Query).ToDictionary(); + } + + internal HttpRequestMessage HttpRequestMessage { get; set; } + + /// Gets or sets the type. + /// The type. + [JsonPropertyName("type")] + [Required] + [AuthEventIdentifier] + public string Type { get; set; } = string.Empty; + + /// Gets or sets the request status. + /// The request status. + [JsonPropertyName("requestStatus")] + [JsonConverter(typeof(JsonStringEnumConverter))] + [Required] + public RequestStatusType RequestStatus { get; set; } + + /// Gets or sets the status message. + /// The status message. + [JsonPropertyName("statusMessage")] + [Required] + public string StatusMessage { get; set; } = string.Empty; + + /// Gets the query parameters passed from the http request message. + /// The query parameters. + [JsonPropertyName("queryParameters")] + public Dictionary QueryParameters { get { return queryParameters; } } + + /// Once an instance is created the framework will pass addition arguments to the created sub class for use. + /// The arguments. + internal abstract void InstanceCreated(params object[] args); + + internal virtual void ParseInbound(AuthenticationEventJsonElement payload) { } + + /// Converts to string. + /// A that represents this instance. + public override string ToString() + { + JsonSerializerOptions options = new JsonSerializerOptions(); + options.Converters.Add(new AuthenticationEventResponseConverterFactory()); + return JsonSerializer.Serialize((object)this, options); + } + + internal virtual JsonSerializerOptions JsonSerializerOptions => new JsonSerializerOptions() { WriteIndented = true, PropertyNameCaseInsensitive = true }; + + internal abstract AuthenticationEventResponse GetResponseObject(); + + /// Set the response to Failed mode. + /// The exception to return in the response. + /// The Underlying AuthEventResponse. + public abstract Task Failed(Exception exception); + + /// Validates the response and creates the IActionResult with the json payload based on the status of the request. + /// IActionResult based on the EventStatus (UnauthorizedResult, BadRequestObjectResult or JsonResult). + public abstract Task Completed(); + } +} \ No newline at end of file diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventResponse.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventResponse.cs new file mode 100644 index 000000000000..5ed0a577a90f --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventResponse.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Net.Http; +using System.Text; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework +{ + /// Represents an event response based on the event type and request. + public abstract class AuthenticationEventResponse : HttpResponseMessage + { + // internal HttpResponseMessage HttpResponseMessage { get; set; } + /// Invalidates this instance. (Builds the Json payload). + internal abstract void Invalidate(); + + /// Gets or sets the body of the event response. + [Required] + public string Body + { + get { return Content == null ? string.Empty : Content.ReadAsStringAsync()?.Result; } + set + { + Content = new StringContent(value, Encoding.UTF8, "application/json"); + } + } + + /// Creates an instance of a sub class of EventResponse based on type and assigns the json schema and payload to the newly created instance. + /// The type to create. + /// The Json payload for the body. + /// A created instance of EventResponse based on the Type. + /// + internal static AuthenticationEventResponse CreateInstance(Type type, string body) + { + AuthenticationEventResponse response = (AuthenticationEventResponse)Activator.CreateInstance(type, true); + response.Body = body; + return response; + } + + internal virtual void InstanceCreated(AuthenticationEventJsonElement payload) + { + AuthenticationEventJsonElement jBody = new AuthenticationEventJsonElement(Body); + + Dictionary updates = new Dictionary(); + + if (payload.Properties.ContainsKey("type") && jBody.Properties.ContainsKey("type")) + { + updates.Add(new string[] { "type" }, payload.GetPropertyValue("type")); + } + + if (payload.Properties.ContainsKey("apiSchemaVersion") && jBody.Properties.ContainsKey("apiSchemaVersion")) + { + updates.Add(new string[] { "apiSchemaVersion" }, payload.GetPropertyValue("apiSchemaVersion")); + } + + if (updates.Count != 0) + { + SetJsonValue(updates); + } + } + + /// Sets the json value in the current payload if path exists. + /// The type of the incoming value to set. + /// The value to set. + /// The path to the Json Property, this will not navigate JArrays. + /// Thrown if the path cannot be found. + internal void SetJsonValue(T value, params string[] path) + { + AuthenticationEventJsonElement payload = new AuthenticationEventJsonElement(Body); + (string key, Dictionary props) = payload.FindPropertyDictionary(true, path); + if (key == null) + { + throw new Exception(AuthenticationEventResource.Ex_Invalid_JsonPath); + } + + props[key] = value; + Body = payload.ToString(); + } + + internal void SetJsonValue(Dictionary values) + { + AuthenticationEventJsonElement payload = new AuthenticationEventJsonElement(Body); + foreach (KeyValuePair keyValuePair in values) + { + (string key, Dictionary props) = payload.FindPropertyDictionary(true, keyValuePair.Key); + if (key == null) + { + throw new Exception(AuthenticationEventResource.Ex_Invalid_JsonPath); + } + + props[key] = keyValuePair.Value; + } + + Body = payload.ToString(); + } + + /// Validates the current Json body payload against it's associated Json schema. + /// An aggregation of the errors within the Json if the payload fails the validation. + internal void Validate() + { + Helpers.ValidateGraph(this); + } + + internal void MarkAsFailed(Exception ex) + { + StatusCode = System.Net.HttpStatusCode.BadRequest; + ReasonPhrase = String.Empty; + Body = Helpers.GetFailedRequestPayload(ex); + } + + internal void MarkAsUnauthorized() + { + StatusCode = System.Net.HttpStatusCode.Unauthorized; + ReasonPhrase = String.Empty; + Body = String.Empty; + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventResponseConverterFactory.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventResponseConverterFactory.cs new file mode 100644 index 000000000000..9499c7afa3b2 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/AuthenticationEventResponseConverterFactory.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework +{ + internal class AuthenticationEventResponseConverterFactory : JsonConverterFactory + { + public override bool CanConvert(Type typeToConvert) + { + return typeof(AuthenticationEventResponse).IsAssignableFrom(typeToConvert); + } + + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + JsonConverter converter = (JsonConverter)Activator.CreateInstance( + typeof(AuthEventResponseConverter<>).MakeGenericType(new Type[] { typeToConvert })); + return converter; + } + + internal class AuthEventResponseConverter : JsonConverter where T : AuthenticationEventResponse + { + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return (T)JsonSerializer.Deserialize(ref reader, typeToConvert, options); + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + foreach (var property in value.GetType().GetProperties()) + { + var attribute = property.GetCustomAttributes(false).FirstOrDefault(x => x.GetType() == typeof(JsonPropertyNameAttribute)); + if (attribute is JsonPropertyNameAttribute nameAttribute) + { + writer.WritePropertyName(nameAttribute.Name); + JsonSerializer.Serialize(writer, property.GetValue(value, null), property.PropertyType, options); + } + } + writer.WriteEndObject(); + } + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/CloudEventData.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/CloudEventData.cs new file mode 100644 index 000000000000..1bace19c49e0 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/CloudEventData.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework +{ + /// Abstract class that handles payload strongly typed payloads conversions. + public abstract class CloudEventData : AuthenticationEventData + { + /// De-serializes the json the its associated typed object. + /// The json containing the typed structure. + /// Returns the typed structure that inherits EventData. + internal override AuthenticationEventData FromJson(AuthenticationEventJsonElement cloudEvent) + { + //TODO: REMOVE!!!! THis is temporary to handle the legacy payload. + if (cloudEvent != null) + { + if (cloudEvent.Properties.ContainsKey("data")) + { + return base.FromJson(cloudEvent.GetPropertyValue("data")); + } + else if (cloudEvent.Properties.ContainsKey("context")) + { + cloudEvent.RenameProperty("context", "authenticationContext"); + var authContext = cloudEvent.FindFirstElementNamed("authenticationContext"); + + if (authContext != null) + { + authContext.RenameProperty("authProtocol", "protocol"); + if (authContext.PathExists("protocol", "type")) + { + authContext.Properties["protocol"] = authContext.GetPropertyValue("protocol", "type"); + } + } + + return base.FromJson(cloudEvent); + } + } + + return base.FromJson(cloudEvent); + + //Proper implementation + //return cloudEvent == null || !cloudEvent.Properties.ContainsKey("data") ? base.FromJson(cloudEvent): base.FromJson(cloudEvent.GetPropertyValue("data")); + } + } +} 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 new file mode 100644 index 000000000000..93e42c686e77 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/CloudEventRequest.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.Validators; +using System.ComponentModel.DataAnnotations; +using System.Net.Http; +using System.Text.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework +{ + /// Abstract class that wraps any request that relies on cloud events. + /// The Cloud Event Response. + /// The Cloud Event Data. + public abstract class CloudEventRequest : AuthenticationEventRequest where TResponse : AuthenticationEventResponse where TData : CloudEventData + { + /// Gets or sets the source. + /// The source. + [JsonPropertyName("source")] + [Required] + public string Source { get; set; } = string.Empty; + + /// Gets or sets the cloud event data type. + /// Data type of cloud event. + [JsonPropertyName("oDataType")] + [OneOf("microsoft.graph.onTokenIssuanceStartCalloutData", "")] + public string ODataType { get; set; } = string.Empty; + + /// Initializes a new instance of the class. + /// The request. + internal CloudEventRequest(HttpRequestMessage request) : base(request) { } + + internal override void ParseInbound(AuthenticationEventJsonElement payload) + { + if (payload.Properties.ContainsKey("type")) + { + Type = payload.GetPropertyValue("type"); + Source = payload.GetPropertyValue("type");//REMOVE: To handle legacy code. + } + + if (payload.Properties.ContainsKey("source")) + { + Source = payload.GetPropertyValue("source"); + } + + if (payload.PathExists("data", "@odata.type")) + { + ODataType = payload.GetPropertyValue("data", "@odata.type"); + } + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/EmptyResponse.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/EmptyResponse.cs new file mode 100644 index 000000000000..2949f1b3cc5b --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/EmptyResponse.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Net.Http; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework +{ + internal class EmptyResponse : AuthenticationEventResponse + { + internal override void InstanceCreated(AuthenticationEventJsonElement payload) { } + internal override void Invalidate() { } + + #region Empty Response/Data + + internal class EmptyData : AuthenticationEventData { } + + internal class EmptyRequest : AuthenticationEventRequest + { + public EmptyRequest(HttpRequestMessage request) : base(request) + { + Payload = new EmptyData(); + Response = new EmptyResponse(); + } + + internal override AuthenticationEventResponse GetResponseObject() + { + return Response; + } + + internal override void InstanceCreated(params object[] args) { } + } + #endregion + } +} \ No newline at end of file diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/Validators/AuthEventIdentifierAttribute.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/Validators/AuthEventIdentifierAttribute.cs new file mode 100644 index 000000000000..d936aca49763 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/Validators/AuthEventIdentifierAttribute.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Linq; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.Validators +{ + /// Confirms that a field is set to one of the valid event identifiers. + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] + internal class AuthEventIdentifierAttribute : ValidationAttribute + { + private static string[] _eventIds; + /// Gets the current registered event ids. + /// (Lazy load). + /// The event ids. + internal static string[] EventIds + { + get + { + if (_eventIds == null) + { + _eventIds = Enum.GetValues(typeof(EventDefinition)).Cast().Select(x => x.GetAttribute().EventIdentifier.ToLower(CultureInfo.CurrentCulture)).ToArray(); + } + + return _eventIds; + } + } + /// Applies formatting to an error message, based on the data field where the error occurred. + /// The name to include in the formatted message. + /// An instance of the formatted error message. + public override string FormatErrorMessage(string name) + { + return String.Format(CultureInfo.CurrentCulture, AuthenticationEventResource.Val_One_Of, name) + string.Join(", ", EventIds); + } + + /// Returns true if the value is a valid event identifier. + /// The value of the object to validate. + /// true if the specified value is valid; otherwise, false. + public override bool IsValid(object value) + { + if (value == null) + { + return false; + } + + if (value is string val) + { + return EventIds.Contains(val.ToLower(CultureInfo.CurrentCulture)); + } + + return false; + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/Validators/OneOfAttribute.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/Validators/OneOfAttribute.cs new file mode 100644 index 000000000000..e4d52cde5358 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/Validators/OneOfAttribute.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.Validators +{ + /// Validator to validated that a value is one of the the values in a list. + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] + internal class OneOfAttribute : ValidationAttribute + { + private readonly string[] _allowed; + /// Initializes a new instance of the class. + /// The allowed values. + internal OneOfAttribute(params string[] allowed) : base($"{AuthenticationEventResource.Val_One_Of} '{string.Join("' ,'", allowed)}'") + { + _allowed = allowed; + } + + /// Returns true if the value is in the list of allowed values. + /// The value of the object to validate. + /// true if the specified value is valid; otherwise, false. + public override bool IsValid(object value) + { + if (value == null) + { + return true; + } + + return value is string val && _allowed.Contains(val); + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/Validators/RequireNonDefaultAttribute.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/Validators/RequireNonDefaultAttribute.cs new file mode 100644 index 000000000000..96d21da9851b --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Framework/Validators/RequireNonDefaultAttribute.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.Validators +{ + /// Validator to ensure that a type value is not set to it's default value. + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] + internal class RequireNonDefaultAttribute : ValidationAttribute + { + /// Initializes a new instance of the class. + public RequireNonDefaultAttribute() + : base(AuthenticationEventResource.Val_Non_Default) + { + } + + /// Returns true if the value is not set to it's default value. + /// The value of the object to validate. + /// true if the specified value is valid; otherwise, false. + public override bool IsValid(object value) + { + if (value is null) + { + return true; + } + + var type = value.GetType(); + return !Equals(value, Activator.CreateInstance(Nullable.GetUnderlyingType(type) ?? type)); + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/GlobalSuppressions.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/GlobalSuppressions.cs new file mode 100644 index 000000000000..c7da4a546b9d --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/GlobalSuppressions.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Usage", "AZC0001:Use one of the following pre-approved namespace groups (https://azure.github.io/azure-sdk/registered_namespaces.html): Azure.AI, Azure.Analytics, Azure.Communication, Azure.Data, Azure.DigitalTwins, Azure.IoT, Azure.Learn, Azure.Media, Azure.Management, Azure.Messaging, Azure.ResourceManager, Azure.Search, Azure.Security, Azure.Storage, Azure.Template, Azure.Identity, Microsoft.Extensions.Azure", Justification = "", Scope = "namespace", Target = "~N:Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework")] +[assembly: SuppressMessage("Usage", "AZC0001:Use one of the following pre-approved namespace groups (https://azure.github.io/azure-sdk/registered_namespaces.html): Azure.AI, Azure.Analytics, Azure.Communication, Azure.Data, Azure.DigitalTwins, Azure.IoT, Azure.Learn, Azure.Media, Azure.Management, Azure.Messaging, Azure.ResourceManager, Azure.Search, Azure.Security, Azure.Storage, Azure.Template, Azure.Identity, Microsoft.Extensions.Azure", Justification = "", Scope = "namespace", Target = "~N:Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents")] +[assembly: SuppressMessage("Usage", "AZC0001:Use one of the following pre-approved namespace groups (https://azure.github.io/azure-sdk/registered_namespaces.html): Azure.AI, Azure.Analytics, Azure.Communication, Azure.Data, Azure.DigitalTwins, Azure.IoT, Azure.Learn, Azure.Media, Azure.Management, Azure.Messaging, Azure.ResourceManager, Azure.Search, Azure.Security, Azure.Storage, Azure.Template, Azure.Identity, Microsoft.Extensions.Azure", Justification = "", Scope = "namespace", Target = "~N:Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Data")] +[assembly: SuppressMessage("Usage", "AZC0007:DO provide a minimal constructor that takes only the parameters required to connect to the service.", Justification = "Seems like a misfire, as this is a simple class that does not have client options.(Perhaps the name ending in Client)", Scope = "type", Target = "~T:Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Data.AuthenticationEventContextClient")] +[assembly: SuppressMessage("Usage", "AZC0001:Use one of the following pre-approved namespace groups (https://azure.github.io/azure-sdk/registered_namespaces.html): Azure.AI, Azure.Analytics, Azure.Communication, Azure.Data, Azure.DigitalTwins, Azure.IoT, Azure.Learn, Azure.Media, Azure.Management, Azure.Messaging, Azure.ResourceManager, Azure.Search, Azure.Security, Azure.Storage, Azure.Template, Azure.Identity, Microsoft.Extensions.Azure", Justification = "", Scope = "namespace", Target = "~N:Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Actions")] +[assembly: SuppressMessage("Usage", "AZC0001:Use one of the following pre-approved namespace groups (https://azure.github.io/azure-sdk/registered_namespaces.html): Azure.AI, Azure.Analytics, Azure.Communication, Azure.Data, Azure.DigitalTwins, Azure.IoT, Azure.Learn, Azure.Media, Azure.Management, Azure.Messaging, Azure.ResourceManager, Azure.Search, Azure.Security, Azure.Storage, Azure.Template, Azure.Identity, Microsoft.Extensions.Azure", Justification = "", Scope = "namespace", Target = "~N:Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart")] +[assembly: SuppressMessage("Usage", "AZC0001:Use one of the following pre-approved namespace groups (https://azure.github.io/azure-sdk/registered_namespaces.html): Azure.AI, Azure.Analytics, Azure.Communication, Azure.Data, Azure.DigitalTwins, Azure.IoT, Azure.Learn, Azure.Media, Azure.Management, Azure.Messaging, Azure.ResourceManager, Azure.Search, Azure.Security, Azure.Storage, Azure.Template, Azure.Identity, Microsoft.Extensions.Azure", Justification = "", Scope = "namespace", Target = "~N:Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.Schema")] +[assembly: SuppressMessage("Usage", "AZC0001:Use one of the following pre-approved namespace groups (https://azure.github.io/azure-sdk/registered_namespaces.html): Azure.AI, Azure.Analytics, Azure.Communication, Azure.Data, Azure.DigitalTwins, Azure.IoT, Azure.Learn, Azure.Media, Azure.Management, Azure.Messaging, Azure.ResourceManager, Azure.Search, Azure.Security, Azure.Storage, Azure.Template, Azure.Identity, Microsoft.Extensions.Azure", Justification = "", Scope = "namespace", Target = "~N:Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.Validators")] +[assembly: SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Json Deserializer needs to the be settable and public", Scope = "member", Target = "~P:Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.ActionableResponse`1.Actions")] +[assembly: SuppressMessage("Usage", "AZC0001:Use one of the following pre-approved namespace groups (https://azure.github.io/azure-sdk/registered_namespaces.html): Azure.AI, Azure.Analytics, Azure.Communication, Azure.Data, Azure.DigitalTwins, Azure.IoT, Azure.Learn, Azure.Media, Azure.Management, Azure.Messaging, Azure.ResourceManager, Azure.Search, Azure.Security, Azure.Storage, Azure.Template, Azure.Identity, Microsoft.Extensions.Azure", Justification = "", Scope = "namespace", Target = "~N:Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Properties")] diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Help/WebJobsExtensionsAuthenticationEvents.chm b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Help/WebJobsExtensionsAuthenticationEvents.chm new file mode 100644 index 000000000000..8a97097d97a8 Binary files /dev/null and b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Help/WebJobsExtensionsAuthenticationEvents.chm differ diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Helpers.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Helpers.cs new file mode 100644 index 000000000000..e841f05dc7c8 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Helpers.cs @@ -0,0 +1,158 @@ +// 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.Generic; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Linq; +using System.Net.Http; +using System.Text; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + internal static class Helpers + { + internal static Dictionary _actionMapping = new Dictionary() + { + {"microsoft.graph.provideclaimsfortoken",typeof(ProvideClaimsForToken) }, + {"provideclaimsfortoken",typeof(ProvideClaimsForTokenLegacy) } + }; + + internal static EventDefinition GetEventDefintionFromPayload(string payload) + { + try + { + AuthenticationEventJsonElement jPayload = new AuthenticationEventJsonElement(payload); + string comparable = string.Empty; + if (jPayload.Properties.ContainsKey("type")) + { + comparable = jPayload.GetPropertyValue("type"); + } + + foreach (EventDefinition eventDefinition in Enum.GetValues(typeof(EventDefinition))) + { + AuthenticationEventMetadataAttribute eventMetadata = eventDefinition.GetAttribute(); + if (eventMetadata.EventIdentifier.Equals(comparable, StringComparison.OrdinalIgnoreCase)) + { + return eventDefinition; + } + } + + throw new Exception(string.Format(CultureInfo.CurrentCulture, AuthenticationEventResource.Ex_Comparable_Not_Found, comparable)); + } + catch (Exception ex) + { + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, AuthenticationEventResource.Ex_Event_Missing, ex.Message)); + } + } + + internal static HttpResponseMessage HttpErrorResponse(Exception ex) + { + return new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest) + { + Content = new StringContent(GetFailedRequestPayload(ex)) + }; + } + + internal static string GetFailedRequestPayload(Exception ex) + { + List errors = new List(); + if (ex != null) + { + errors.Add(ex.Message); + ex = ex.InnerException; + while (ex != null) + { + if (ex is AggregateException agEx) + { + errors.AddRange(agEx.InnerExceptions.Select(m => m.Message).ToArray()); + } + else + { + errors.Add(ex.Message); + } + + ex = ex.InnerException; + } + } + else + { + errors.Add(AuthenticationEventResource.Ex_Gen_Failure); + } + + return $"{{\"errors\":[\"{String.Join("\",\"", errors.Select(m => m))}\"]}}"; + } + + internal static HttpResponseMessage HttpUnauthorizedResponse() + { + return new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized); + } + + internal static HttpResponseMessage HttpJsonResponse(AuthenticationEventJsonElement json) + { + return new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(json.ToString(), Encoding.UTF8, "application/json") + }; + } + + /// Determines whether the specified input is json. + /// The input. + /// + /// true if the specified input is json; otherwise, false. + internal static bool IsJson(string input) + { + if (string.IsNullOrEmpty(input)) + { + return false; + } + + input = input.Trim(); + return (input.StartsWith("{", StringComparison.OrdinalIgnoreCase) && input.EndsWith("}", StringComparison.OrdinalIgnoreCase)) + || (input.StartsWith("[", StringComparison.OrdinalIgnoreCase) && input.EndsWith("]", StringComparison.OrdinalIgnoreCase)); + } + + internal static AuthenticationEventAction GetEventActionForActionType(string actionType) + { + return actionType != null && _actionMapping.ContainsKey(actionType.ToLower(CultureInfo.CurrentCulture)) + ? (AuthenticationEventAction)Activator.CreateInstance(_actionMapping[actionType.ToLower(CultureInfo.CurrentCulture)]) + : throw new Exception(String.Format(CultureInfo.CurrentCulture, AuthenticationEventResource.Ex_Action_Invalid, actionType, String.Join("', '", _actionMapping.Select(x => x.Key)))); + } + + internal static void ValidateGraph(object graph) + { + var validationResults = new List(); + + ValidateGraph(graph, validationResults); + + if (validationResults.Count > 0) + { + throw new AggregateException(AuthenticationEventResource.Ex_Invalid_Payload, validationResults.Select(v => new Exception(v.ErrorMessage))); + } + } + + private static void ValidateGraph(object obj, List validationResults) + { + List objectValidations = new List(); + + var props = obj.GetType().GetProperties().Where(p => p.GetCustomAttributes(false).FirstOrDefault(a => typeof(ValidationAttribute).IsAssignableFrom(a.GetType())) != null); + + foreach (var prop in props) + { + object inst = prop.GetValue(obj); + + Validator.TryValidateProperty(inst, new ValidationContext(obj) { MemberName = prop.Name }, objectValidations); + + if (inst != null)//Short circuit the validation if the parent is null. + { + ValidateGraph(inst, validationResults); + } + } + + validationResults.AddRange(objectValidations.Select(f => { f.ErrorMessage = $"{obj.GetType().Name}: {f.ErrorMessage}"; return f; })); + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.csproj b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.csproj new file mode 100644 index 000000000000..335be7097c47 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.csproj @@ -0,0 +1,55 @@ + + + $(RequiredTargetFrameworks) + true + 1.0.0-beta.1 + Triggers for Azure AD Authentication event custom extensions. Lets you focus on your business logic. + + + + + + + + + + + + + + + + + + + + True + True + AuthenticationEventResource.resx + + + True + True + AuthEventResourceAuthEventResource.resx + + + + + + + ResXFileCodeGenerator + AuthenticationEventResource.Designer.cs + + + ResXFileCodeGenerator + AuthEventResourceAuthEventResource.Designer.cs + + + + + + + + + + diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Properties/AssembyInfo.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Properties/AssembyInfo.cs new file mode 100644 index 000000000000..1ed2926970a1 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Properties/AssembyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100d15ddcb29688295338af4b7686603fe614abd555e09efba8fb88ee09e1f7b1ccaeed2e8f823fa9eef3fdd60217fc012ea67d2479751a0b8c087a4185541b851bd8b16f8d91b840e51b1cb0ba6fe647997e57429265e85ef62d565db50a69ae1647d54d7bd855e4db3d8a91510e5bcbd0edfbbecaa20a7bd9ae74593daa7b11b4")] diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/RequestStatusType.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/RequestStatusType.cs new file mode 100644 index 000000000000..2bbd7e8cf209 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/RequestStatusType.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + #region Global Enums + + /// The status of the incoming request. + public enum RequestStatusType + { + /// If there is any failures on the incoming status, the StatusMessage property will contain the reason for the failure. + Failed, + /// All check have passed except for the Token, which is invalid. + TokenInvalid, + /// Incoming request and token has passed all checks and is in a successful state. + Successful + } + #endregion +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/ServiceInfo.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/ServiceInfo.cs new file mode 100644 index 000000000000..2179cea03b71 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/ServiceInfo.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + 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) + { + OpenIdConnectionHost = openIdConnectionHost; + TokenIssuerV1 = tokenIssuerV1; + TokenIssuerV2 = tokenIssuerV2; + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/SupportedTokenSchemaVersions.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/SupportedTokenSchemaVersions.cs new file mode 100644 index 000000000000..3bbbb4bae5f3 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/SupportedTokenSchemaVersions.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ComponentModel; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + /// Supported Azure token schema. + internal enum SupportedTokenSchemaVersions + { + /// Version 1. + [Description("1.0")] V1_0, + /// Version 2. + [Description("2.0")] V2_0 + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Templates/ActionableTemplate.json b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Templates/ActionableTemplate.json new file mode 100644 index 000000000000..7f4b0f722613 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Templates/ActionableTemplate.json @@ -0,0 +1,6 @@ +{ + "type": "", + "apiSchemaVersion": "", + "actions": [ + ] +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Templates/CloudEventActionableTemplate.json b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Templates/CloudEventActionableTemplate.json new file mode 100644 index 000000000000..c91e1801879e --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/Templates/CloudEventActionableTemplate.json @@ -0,0 +1,6 @@ +{ + "data": { + "@odata.type": "", + "actions": [] + } +} \ No newline at end of file diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/Actions/ProvideClaimsForToken.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/Actions/ProvideClaimsForToken.cs new file mode 100644 index 000000000000..42141d472017 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/Actions/ProvideClaimsForToken.cs @@ -0,0 +1,81 @@ +// 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.Framework.Validators; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Actions +{ + /// A representation of the ProvideClaimsForToken action. + public partial class ProvideClaimsForToken : TokenIssuanceAction + { + /// Gets or sets the claims. + /// The claims. + [JsonPropertyName("claims")] + public List Claims { get; } = new List(); + + /// Gets the type of the action of ProvideClaimsForToken. + /// The type of the action. + [JsonPropertyName("actionType")] + [OneOf("microsoft.graph.provideClaimsForToken")] + internal override string ActionType => "microsoft.graph.provideClaimsForToken"; + + /// Initializes a new instance of the class. + public ProvideClaimsForToken() { } + /// Initializes a new instance of the class. + /// A collection of claims to add. + public ProvideClaimsForToken(params TokenClaim[] claim) + { + if (claim != null) + { + Claims.AddRange(claim); + } + } + + /// Adds a claim to the collection. + /// The claim identifier. + /// The claim values. + public void AddClaim(string Id, params string[] Values) + { + Claims.Add(new TokenClaim(Id, Values)); + } + + /// Builds the action body. + /// A JObject representing the claims in Json format. + internal override AuthenticationEventJsonElement BuildActionBody() + { + AuthenticationEventJsonElement jsonClaims = new AuthenticationEventJsonElement(); + Claims.ForEach(c => jsonClaims.Properties.Add(c.Id, c.Values.Length == 1 ? + (object)c.Values[0] : + c.Values.Select(x => new AuthenticationEventJsonElement() { Value = x }).ToList())); + + return new AuthenticationEventJsonElement(new Dictionary { { "claims", jsonClaims } }); + } + + /// Create the ProvideClaimsForToken action + /// from Json. + /// The action body. + internal override void FromJson(AuthenticationEventJsonElement actionBody) + { + AuthenticationEventJsonElement claims = actionBody.FindFirstElementNamed("claims"); + if (claims != null) + { + foreach (var key in claims.Properties.Keys) + { + var val = claims.GetPropertyValue(key); + if (val is string sValue) + { + AddClaim(key, sValue); + } + else if (val is AuthenticationEventJsonElement jValue) + { + AddClaim(key, jValue.Elements.Select(x => x.Value).ToArray()); + } + } + } + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/Actions/ProvideClaimsForTokenLegacy.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/Actions/ProvideClaimsForTokenLegacy.cs new file mode 100644 index 000000000000..0eb597a70b90 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/Actions/ProvideClaimsForTokenLegacy.cs @@ -0,0 +1,76 @@ +// 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.Framework.Validators; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Actions +{ + /// A representation of the ProvideClaimsForToken action for the legacy payload. + /// TODO: This must be removed legacy support is dropped. + internal partial class ProvideClaimsForTokenLegacy : ProvideClaimsForToken + { + /// Gets the type of the action of ProvideClaimsForToken. + /// The type of the action. + [JsonPropertyName("actionType")] + [OneOf("ProvideClaimsForToken")] + internal override string ActionType => "ProvideClaimsForToken"; + internal override string TypeProperty => "type"; + /// Initializes a new instance of the class. + public ProvideClaimsForTokenLegacy() { } + public ProvideClaimsForTokenLegacy(ProvideClaimsForToken provideClaimsForToken) + { + if (provideClaimsForToken != null) + { + Claims.AddRange(provideClaimsForToken.Claims); + } + } + /// Initializes a new instance of the class. + /// A collection of claims to add. + public ProvideClaimsForTokenLegacy(params TokenClaim[] claim) + { + if (claim != null) + { + Claims.AddRange(claim); + } + } + + /// Builds the action body. + /// A JObject representing the claims in Json format. + internal override AuthenticationEventJsonElement BuildActionBody() + { + var body = Claims.Select(x => x.Values.Length == 1 ? + new AuthenticationEventJsonElement($"{{\"id\":\"{x.Id}\", \"value\":\"{x.Values[0]}\" }}") : + new AuthenticationEventJsonElement($"{{\"id\":\"{x.Id}\", \"value\":[{string.Join(", ", x.Values.Select(v => $"\"{v}\""))}] }}") + ).ToList(); + + return new AuthenticationEventJsonElement(new Dictionary { { "claims", body } }); + } + + /// Create the ProvideClaimsForToken action + /// from Json. + /// The action body. + internal override void FromJson(AuthenticationEventJsonElement actionBody) + { + AuthenticationEventJsonElement claims = actionBody.FindFirstElementNamed("claims"); + if (claims != null) + { + foreach (AuthenticationEventJsonElement claim in claims.Elements) + { + var value = claim.GetPropertyValue("value"); + if (value is string sValue) + { + AddClaim(claim.GetPropertyValue("id"), sValue); + } + else if (value is AuthenticationEventJsonElement jValue) + { + AddClaim(claim.GetPropertyValue("id"), jValue.Elements.Select(x => x.Value).ToArray()); + } + } + } + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/Actions/TokenClaim.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/Actions/TokenClaim.cs new file mode 100644 index 000000000000..b66bbc0bdc77 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/Actions/TokenClaim.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.Validators; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Actions +{ + /// Represents a Token Claim. + public class TokenClaim + { + /// Initializes a new instance of the class. + /// The identifier. + /// The values. + public TokenClaim(string id, params string[] values) + { + Id = id; + Values = values; + } + + /// Gets or sets the identifier. + /// The identifier. + [JsonPropertyName("id")] + [Required] + public string Id { get; set; } + + /// Gets or sets the values. + /// The values. + [JsonPropertyName("values")] + [RequireNonDefault] + public string[] Values { get; set; } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/Data/AuthenticationEventContext.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/Data/AuthenticationEventContext.cs new file mode 100644 index 000000000000..292df85fc8fd --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/Data/AuthenticationEventContext.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework.Validators; +using System; +using System.Text.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Data +{ + /// Represents the Context Data Model Object. + public class AuthenticationEventContext + { + /// Gets the correlation identifier. + /// The correlation identifier. + [JsonPropertyName("correlationId")] + public Guid CorrelationId { get; set; } + + /// Gets the client. + /// The client. + [JsonPropertyName("client")] + public AuthenticationEventContextClient Client { get; set; } + + /// Gets the authentication protocol. + /// The authentication protocol. + [JsonPropertyName("protocol")] + [OneOf("OAUTH2.0", "SAML", "WS-FED", "unknownFutureValue", "")] + public string Protocol { get; set; } + + /// Gets the client service principal. + /// The client service principal. + [JsonPropertyName("clientServicePrincipal")] + public AuthenticationEventContextServicePrincipal ClientServicePrincipal { get; set; } + + /// Gets the resource service principal. + /// The resource service principal. + [JsonPropertyName("resourceServicePrincipal")] + public AuthenticationEventContextServicePrincipal ResourceServicePrincipal { get; set; } + + /// Gets the user. + /// The user. + [JsonPropertyName("user")] + public AuthenticationEventContextUser User { get; set; } + } +} \ No newline at end of file diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/Data/AuthenticationEventContextClient.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/Data/AuthenticationEventContextClient.cs new file mode 100644 index 000000000000..ba3c40850280 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/Data/AuthenticationEventContextClient.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Data +{ + /// Represents the Client Data Model Object. + public class AuthenticationEventContextClient + { + /// Gets the ip. + /// The ip. + [JsonPropertyName("ip")] + public string Ip { get; set; } + + /// Gets or sets the locale. + /// The locale. + [JsonPropertyName("locale")] + public string Locale { get; set; } + + /// Gets or sets the market. + /// The market. + [JsonPropertyName("market")] + public string Market { get; set; } + } +} \ No newline at end of file diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/Data/AuthenticationEventContextServicePrincipal.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/Data/AuthenticationEventContextServicePrincipal.cs new file mode 100644 index 000000000000..6512aca7c82a --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/Data/AuthenticationEventContextServicePrincipal.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Data +{ + /// Represents the Role Data Model Object. + public class AuthenticationEventContextServicePrincipal + { + /// Gets the identifier. + /// The identifier. + [JsonPropertyName("id")] + [Required] + public Guid Id { get; set; } + + /// Gets the application identifier. + /// The application identifier. + [JsonPropertyName("appId")] + public Guid AppId { get; set; } + + /// Gets the display name of the application. + /// The display name of the application. + [JsonPropertyName("appDisplayName")] + public string AppDisplayName { get; set; } + + /// Gets the display name. + /// The display name. + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } + } +} \ No newline at end of file diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/Data/AuthenticationEventContextUser.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/Data/AuthenticationEventContextUser.cs new file mode 100644 index 000000000000..0d98d9ae05f1 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/Data/AuthenticationEventContextUser.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Data +{ + /// Represents the User Data Model Object. + public partial class AuthenticationEventContextUser + { + /// Gets the name of the company. + /// The name of the company. + [JsonPropertyName("companyName")] + public string CompanyName { get; set; } + + /// Gets the country. + /// The country. + [JsonPropertyName("country")] + public string Country { get; set; } + + /// Gets the display name. + /// The display name. + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } + + /// Gets the first name. + /// The name of the given. + [JsonPropertyName("givenName")] + public string GivenName { get; set; } + + /// Gets the identifier. + /// The identifier. + [JsonPropertyName("id")] + [Required] + public Guid Id { get; set; } + + /// Gets the mail address. + /// The mail. + [JsonPropertyName("mail")] + public string Mail { get; set; } + + /// Gets the name of the on premises sam account. + /// The name of the on premises sam account. + [JsonPropertyName("onPremisesSamAccountName")] + public string OnPremisesSamAccountName { get; set; } + + /// Gets the on premises security identifier. + /// The on premises security identifier. + [JsonPropertyName("onPremisesSecurityIdentifier")] + public string OnPremisesSecurityIdentifier { get; set; } + + /// Gets the name of the on premise user principal. + /// The name of the on premise user principal. + [JsonPropertyName("onPremiseUserPrincipalName")] + public string OnPremiseUserPrincipalName { get; set; } + + /// Gets the preferred data location. + /// The preferred data location. + [JsonPropertyName("preferredDataLocation")] + public string PreferredDataLocation { get; set; } + + /// Gets the preferred language. + /// The preferred language. + [JsonPropertyName("preferredLanguage")] + public string PreferredLanguage { get; set; } + + /// Gets the surname. + /// The surname. + [JsonPropertyName("surname")] + public string Surname { get; set; } + + /// Gets the name of the user principal. + /// The name of the user principal. + [JsonPropertyName("userPrincipalName")] + [Required] + public string UserPrincipalName { get; set; } + + /// Gets the type of the user. + /// The type of the user. + [JsonPropertyName("userType")] + public string UserType { get; set; } + } +} \ No newline at end of file diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/Data/TokenIssuanceStartData.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/Data/TokenIssuanceStartData.cs new file mode 100644 index 000000000000..946efdd53900 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/Data/TokenIssuanceStartData.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Data +{ + /// Represents the TokenIssuanceStartData (Root Element) Data Model Object. + public class TokenIssuanceStartData : CloudEventData + { + /// Gets the context. + /// The context. + [JsonPropertyName("authenticationContext")] + [Required] + public AuthenticationEventContext AuthenticationContext { get; set; } + } +} \ No newline at end of file diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/TokenIssuanceAction.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/TokenIssuanceAction.cs new file mode 100644 index 000000000000..0854f7caf1d7 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/TokenIssuanceAction.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Actions +{ + /// Actions for the onTokenIssuanceStart must inherit this. + public abstract class TokenIssuanceAction : AuthenticationEventAction + { + /// Initializes a new instance of the class. + public TokenIssuanceAction() : base() { } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/TokenIssuanceStartRequest.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/TokenIssuanceStartRequest.cs new file mode 100644 index 000000000000..4b33221fa871 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/TokenIssuanceStartRequest.cs @@ -0,0 +1,46 @@ +// 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.Data; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart +{ + /// + /// A representation of the onTokenIssuanceStart event request for preview_10_01_2021. + /// Relates the EventResponse-TokenIssuanceStartResponse(preview_10_01_2021) and EventData-TokenIssuanceStartData(preview_10_01_2021). + /// + /// + [Serializable] + public class TokenIssuanceStartRequest : CloudEventRequest + { + /// Initializes a new instance of the class. + /// The incoming HTTP request message. + public TokenIssuanceStartRequest(HttpRequestMessage request) : base(request) { } + + /// Gets or sets the token claims. + /// The token claims. + [JsonPropertyName("tokenClaims")] + public Dictionary TokenClaims { get; } = new Dictionary(); + + internal override AuthenticationEventResponse GetResponseObject() + { + return Response; + } + + /// Assigns the token claims from the incoming parameters. + /// The arguments. + internal override void InstanceCreated(params object[] args) + { + if (args[0] is Dictionary inboundTokenClaims) + { + TokenClaims.Clear(); + TokenClaims.AddRange(inboundTokenClaims); + } + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/TokenIssuanceStartResponse.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/TokenIssuanceStartResponse.cs new file mode 100644 index 000000000000..75ac8f22a06c --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenIssuanceStart/TokenIssuanceStartResponse.cs @@ -0,0 +1,19 @@ +// 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.Framework.Validators; +using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Actions; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart +{ + /// A representation an actionable onTokenIssuanceStart event response for preview_10_01_2021. + public class TokenIssuanceStartResponse : ActionableCloudEventResponse + { + /// Gets the Cloud Event @odata.type. + /// Gets the Cloud Event @odata.type. + + [OneOf("microsoft.graph.onTokenIssuanceStartResponseData")] + internal override string DataTypeIdentifier => "microsoft.graph.onTokenIssuanceStartResponseData"; + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenValidator.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenValidator.cs new file mode 100644 index 000000000000..608142102f09 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenValidator.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + internal abstract class TokenValidator + { + internal abstract Task<(bool Valid, Dictionary Claims)> GetClaimsAndValidate(HttpRequestMessage request, ConfigurationManager configurationManager); + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenValidatorEZAuth.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenValidatorEZAuth.cs new file mode 100644 index 000000000000..633fd000884d --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenValidatorEZAuth.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + internal class TokenValidatorEZAuth : TokenValidator + { + internal override Task<(bool Valid, Dictionary Claims)> GetClaimsAndValidate(HttpRequestMessage request, ConfigurationManager configurationManager) + { + Dictionary Claims = new Dictionary(); + try + { + string principal = request.Headers.DecodeBase64(ConfigurationManager.HEADER_EZAUTH_PRINCIPAL); + AuthenticationEventJsonElement jPrincipal = new AuthenticationEventJsonElement(principal); + + foreach (AuthenticationEventJsonElement jVal in jPrincipal.GetPropertyValue("claims").Elements) + Claims.Add(jVal.GetPropertyValue("typ"), jVal.GetPropertyValue("val")); + + 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)); + } + catch (Exception) + { + return Task.FromResult((false, Claims)); + } + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenValidatorHelper.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenValidatorHelper.cs new file mode 100644 index 000000000000..2ede54538262 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenValidatorHelper.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Globalization; +using System.Linq; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + /// Helper functions for token validations. + internal static class TokenValidatorHelper + { + public static SupportedTokenSchemaVersions ParseSupportedTokenVersion(string version) + { + string[] v1 = { "1.0", "1", "ver1" }; + string[] v2 = { "2.0", "2", "ver2" }; + if (v1.Contains(version)) + { + return SupportedTokenSchemaVersions.V1_0; + } + else if (v2.Contains(version)) + { + return SupportedTokenSchemaVersions.V2_0; + } + else + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, AuthenticationEventResource.Ex_Token_Version, version, String.Join(",", Enum.GetNames(typeof(SupportedTokenSchemaVersions))))); + } + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenValidatorInternal.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenValidatorInternal.cs new file mode 100644 index 000000000000..a56790e0c219 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/src/TokenValidatorInternal.cs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents; +using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework; +using Microsoft.IdentityModel.Tokens; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Net.Http; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents +{ + internal class TokenValidatorInternal : TokenValidator + { + internal static List _cacheKeys = new List(); + internal static DateTime? _lastSync; + internal static TimeSpan _cacheRefresh = new TimeSpan(0, 0, 5, 0); + private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1); + + internal override async Task<(bool Valid, Dictionary Claims)> GetClaimsAndValidate(HttpRequestMessage request, ConfigurationManager configurationManager) + { + if (string.IsNullOrEmpty(configurationManager.TenantId) || string.IsNullOrEmpty(configurationManager.AudienceAppId)) + { + throw new MissingFieldException(string.Format(CultureInfo.CurrentCulture, AuthenticationEventResource.Ex_Trigger_Required_Attrs, ConfigurationManager.TENANT_ID, ConfigurationManager.AUDIENCE_APPID)); + } + + string accessToken = request.Headers?.Authorization?.Parameter; + + if (string.IsNullOrWhiteSpace(accessToken)) + { + return (false, null); + } + + string[] authenticationAppIds = configurationManager.AudienceAppId.Split(';'); + + var handler = new JwtSecurityTokenHandler(); + JwtSecurityToken token = handler.ReadJwtToken(accessToken); + SupportedTokenSchemaVersions tokenSchemaVersion = TokenValidatorHelper.ParseSupportedTokenVersion(token.Claims.First(x => x.Type.Equals("ver")).Value); + + if (!ConfigurationManager.GetService(token.Payload[(tokenSchemaVersion == SupportedTokenSchemaVersions.V2_0 ? ConfigurationManager.TOKEN_V2_VERIFY : ConfigurationManager.TOKEN_V1_VERIFY)].ToString(), out ServiceInfo serviceInfo)) + { + return (false, null); + } + + try + { + await Task.Run(() => + { + if (MustCacheRefresh()) + { + try + { + _cacheKeys.Clear(); + _semaphore.Wait(); + + using (HttpClient httpClient = new HttpClient()) + { + string openidConfiguration = httpClient.GetStringAsync(new Uri($"{serviceInfo.OpenIdConnectionHost}/common/v2.0/.well-known/openid-configuration", UriKind.Absolute)).Result; + + AuthenticationEventJsonElement openidConfigurationJson = new AuthenticationEventJsonElement(openidConfiguration); + string jwksUri = openidConfigurationJson.GetPropertyValue("jwks_uri"); + string certs = httpClient.GetStringAsync(new Uri(jwksUri, UriKind.Absolute)).Result; + AuthenticationEventJsonElement certsJson = new AuthenticationEventJsonElement(certs); + foreach (AuthenticationEventJsonElement key in certsJson.GetPropertyValue("keys").Elements) + { + _cacheKeys.Add(new RsaSecurityKey(new RSAParameters { Modulus = FromBase64Url(key.GetPropertyValue("n")), Exponent = FromBase64Url(key.GetPropertyValue("e")) })); + } + } + } + finally + { + _semaphore.Release(); + } + _lastSync = DateTime.Now; + } + }).ConfigureAwait(false); + + var result = handler.ValidateToken(accessToken, new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateIssuerSigningKey = true, + ValidateLifetime = true, + ValidIssuer = string.Format(CultureInfo.CurrentCulture, tokenSchemaVersion == SupportedTokenSchemaVersions.V2_0 ? + serviceInfo.TokenIssuerV2 : + serviceInfo.TokenIssuerV1, configurationManager.TenantId), + ValidAudiences = authenticationAppIds, + IssuerSigningKeys = _cacheKeys + }, out _); + + //Here we validate the version and for version 1 validate that the appid claim matches the authorization party, if version 2, we validate that the azp claim matches the authorization party. + return (result.Claims.VerifyClaim("ver", $"{tokenSchemaVersion.GetDescription()}"), + result.Claims.ToDictionary(x => x.Type, x => x.Value)); + } + catch (Exception) + { + return (false, null); + } + } + + internal static byte[] FromBase64Url(string base64Url) + { + string padded = base64Url.Length % 4 == 0 + ? base64Url : base64Url + "====".Substring(base64Url.Length % 4); + string base64 = padded.Replace("_", "/") + .Replace("-", "+"); + return Convert.FromBase64String(base64); + } + + internal static bool MustCacheRefresh() + { + if (_cacheKeys.Count == 0) + return true; + + if (!_lastSync.HasValue) + return true; + + return (_lastSync.Value.Add(_cacheRefresh) < DateTime.Now); + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/ActionParameters.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/ActionParameters.cs new file mode 100644 index 000000000000..b620b326dd06 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/ActionParameters.cs @@ -0,0 +1,18 @@ +using Microsoft.Azure.WebJobs.Host.Executors; +using System.Net.Http; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests +{ + + /// Parameter class for action parameters passed from BaseTest + public class ActionParameters + { + /// Gets or sets the request message. + /// The request message. + public HttpRequestMessage RequestMessage { get; set; } + /// Gets or sets the function data. + /// The function data. + public TriggeredFunctionData FunctionData { get; set; } + } + +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/ConfigProviderTests.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/ConfigProviderTests.cs new file mode 100644 index 000000000000..7ce8c92cf0b7 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/ConfigProviderTests.cs @@ -0,0 +1,48 @@ +using System; +using System.Net; +using System.Net.Http; +using Xunit; +using static Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests.TestHelper; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests +{ + /// + /// this class will test EventsTriggerConfigProvider + /// + public class ConfigProviderTests + { + /// + /// This functions runs the test case defined by params and in-line data + /// + /// + /// + /// + /// + /// + [Obsolete] + [Theory] + [InlineData("http://test/mock?function=onTokenissuancestart", HttpStatusCode.Unauthorized, HttpMethods.Post, null)] + [InlineData("http://test/mock?function=onTokenissuancestart", HttpStatusCode.BadRequest, HttpMethods.Post, null)] + [InlineData("http://test/mock?function=onTokenissuancestart", HttpStatusCode.OK, HttpMethods.Post, "{'hi':'bye'}")] + [InlineData("http://test/mock?", HttpStatusCode.BadRequest, HttpMethods.Post, "{\"errors\":[\"Please supply the function name via the query parameter: functionName, available functions: onTokenIssuanceStart\"]}")] + [InlineData("http://test/mock?function=onTokenissuancestart", HttpStatusCode.BadRequest, HttpMethods.Get, "{\"errors\":[\"Method can only be post.\"]}")] + public async void PostConfigProviderTests(string url, HttpStatusCode httpStatusCode, HttpMethods method, string expectedMessage) + { + HttpResponseMessage httpResponse = await BaseTest(method, url, t => + { + if (t.FunctionData.TriggerValue is HttpRequestMessage mockedRequest) + { + + AuthenticationEventResponseHandler eventsResponseHandler = (AuthenticationEventResponseHandler)mockedRequest.Properties[AuthenticationEventResponseHandler.EventResponseProperty]; + eventsResponseHandler.Response = GetContentForHttpStatus(httpStatusCode); + } + }); + + Assert.Equal(httpStatusCode, httpResponse.StatusCode); + if (expectedMessage != null) + { + Assert.Equal(expectedMessage, await httpResponse.Content.ReadAsStringAsync()); + } + } + } +} 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 new file mode 100644 index 000000000000..76001aced095 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests.csproj @@ -0,0 +1,59 @@ + + + + $(RequiredTargetFrameworks) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/MiscTests.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/MiscTests.cs new file mode 100644 index 000000000000..55a8bc220305 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/MiscTests.cs @@ -0,0 +1,64 @@ + +using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests.Payloads.TokenIssuanceStart.Legacy; +using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart; +using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart.Actions; +using System; +using System.Net.Http; +using System.Threading; +using Xunit; +using payloads = Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests.Payloads.TokenIssuanceStart; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests +{ + + /// Tests the OnTokenIssuanceStart request and response for the csharp object model for version preview_10_01_2021 + [Obsolete] + public class MiscTests + { + /// Runs 10000 calls to the library concurrently with success payload and response + [Fact] + public void ConcurrencyTest() + { + for (int i = 0; i < 10000; i++) + ThreadPool.QueueUserWorkItem(async w => + { + HttpResponseMessage httpResponseMessage = await TestHelper.EventResponseBaseTest(eventsResponseHandler => + { + eventsResponseHandler.SetValueAsync(payloads.TokenIssuanceStart.ActionResponse, CancellationToken.None); + }); + + Assert.Equal(System.Net.HttpStatusCode.OK, httpResponseMessage.StatusCode); + Assert.True(TestHelper.DoesPayloadMatch(TokenIssuanceStartLegacy.ExpectedPayload, httpResponseMessage.Content.ReadAsStringAsync().Result)); + }); + } + + /// Tests query string parameter conversions. + [Fact] + public void QueryParameterTest() + { + TokenIssuanceStartRequest tokenIssuanceStartRequest = new TokenIssuanceStartRequest(new HttpRequestMessage(HttpMethod.Get, "http://test?param1=test1¶m2=test2")); + Assert.True(TestHelper.DoesPayloadMatch(payloads.TokenIssuanceStart.TokenIssuanceStartQueryParameter, tokenIssuanceStartRequest.ToString())); + } + + /// Tests the OnTokenIssuanceStart request and response object model for CSharp for version: 10_01_2021 + [Fact] + public async void TokenIssuanceStartObjectModelTest() + { + HttpResponseMessage httpResponseMessage = await TestHelper.EventResponseBaseTest(eventsResponseHandler => + { + if (eventsResponseHandler.Request is TokenIssuanceStartRequest request) + { + request.Response.Actions.Add(new ProvideClaimsForToken( + new TokenClaim("DateOfBirth", "01/01/2000"), + new TokenClaim("CustomRoles", "Writer", "Editor") + )); + + eventsResponseHandler.SetValueAsync(request.Completed().Result, CancellationToken.None); + } + }); + + Assert.Equal(System.Net.HttpStatusCode.OK, httpResponseMessage.StatusCode); + Assert.True(TestHelper.DoesPayloadMatch(TokenIssuanceStartLegacy.ExpectedPayload, httpResponseMessage.Content.ReadAsStringAsync().Result)); + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/PayloadTests.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/PayloadTests.cs new file mode 100644 index 000000000000..43c90c713717 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/PayloadTests.cs @@ -0,0 +1,89 @@ +using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Framework; +using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests.Payloads.TokenIssuanceStart.Legacy; +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using Xunit; +using static Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests.TestHelper; +using payloads = Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests.Payloads.TokenIssuanceStart; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests +{ + /// Payload test types + public enum TestTypes + { + /// A valid payload + Valid, + /// A payload with an invalid action + InvalidAction, + /// No actions supplied payload + NoAction, + /// An empty return payload + Empty, + /// A string response that wll be converted to an IActionResult + Conversion, + /// A valid payload for supported cloud event envelope + ValidCloudEvent + } + + + /// Class to house all payload tests + public class PayloadTests + { + /// Tests the specified payload based on TestType + /// Type of the test. + [Theory] + [InlineData(TestTypes.Valid)] + [InlineData(TestTypes.InvalidAction)] + [InlineData(TestTypes.NoAction)] + [InlineData(TestTypes.Empty)] + [InlineData(TestTypes.Conversion)] + [InlineData(TestTypes.ValidCloudEvent)] + [Obsolete] + public async void Tests(TestTypes testType) + { + var (payload, expected, expectedStatus) = GetTestData(testType); + + HttpResponseMessage httpResponseMessage = await EventResponseBaseTest(eventsResponseHandler => + { + eventsResponseHandler.SetValueAsync(payload, CancellationToken.None); + }, testType); + + Assert.Equal(expectedStatus, httpResponseMessage.StatusCode); + Assert.True(DoesPayloadMatch(expected, httpResponseMessage.Content.ReadAsStringAsync().Result)); + } + + + private (string payload, string expected, HttpStatusCode expectedStatus) GetTestData(TestTypes testTypes) + { + switch (testTypes) + { + case TestTypes.Valid: return (TokenIssuanceStartLegacy.ActionResponse, TokenIssuanceStartLegacy.ExpectedPayload, HttpStatusCode.OK); + case TestTypes.Conversion: return (payloads.TokenIssuanceStart.ConversionPayload, TokenIssuanceStartLegacy.ExpectedPayload, HttpStatusCode.OK);// + case TestTypes.InvalidAction: return (payloads.TokenIssuanceStart.InvalidActionResponse, @"{'errors':['The action \'ProvideClaims\' is invalid, please use one of the following actions: \'microsoft.graph.provideclaimsfortoken\', \'provideclaimsfortoken\'']}", HttpStatusCode.BadRequest); + case TestTypes.NoAction: return (payloads.TokenIssuanceStart.NoActionResponse, @"{'errors':['No Actions Found. Please supply atleast one action.']}", HttpStatusCode.BadRequest); + 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.BadRequest); + case TestTypes.ValidCloudEvent: return (payloads.TokenIssuanceStart.ActionResponse, payloads.TokenIssuanceStart.ExpectedPayload, HttpStatusCode.OK); + default: return (string.Empty, string.Empty, HttpStatusCode.NotFound); + } + } + } + + internal class TestAuthResponse : AuthenticationEventResponse + { + internal TestAuthResponse(HttpStatusCode code, string content) + : this(code) + { + Content = new StringContent(content); + } + + internal TestAuthResponse(HttpStatusCode code) + { + StatusCode = code; + } + + internal override void Invalidate() + { } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/OpenApi/OpenApiDocument.json b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/OpenApi/OpenApiDocument.json new file mode 100644 index 000000000000..60bec0a55026 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/OpenApi/OpenApiDocument.json @@ -0,0 +1,68 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "OnTokenIssuanceStart", + "version": "10-01-2021-preview", + "contact": { + "name": "Microsoft" + }, + "summary": "OnTokenIssuanceStart 10-01-2021-preview", + "description": "API outline for OnTokenIssuanceStart 10-01-2021-preview" + }, + "servers": [ + { + "url": "" + } + ], + "paths": { + "/onTokenIssuanceStart/10-01-2021-preview": { + "post": { + "tags": [ + "Authentication Events", + "OnTokenIssuanceStart", + "10-01-2021-preview" + ], + "summary": "", + "operationId": "post-preview_10_01_2021", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "./responseSchema.json" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "./requestSchema.json" + } + } + }, + "description": "OnTokenIssuanceStart payload" + }, + "description": "When a new token is issued" + }, + "parameters": [] + } + }, + "components": { + "schemas": {} + }, + "tags": [ + { + "name": "10-01-2021-preview" + }, + { + "name": "Authentication Events" + }, + { + "name": "OnTokenIssuanceStart" + } + ] +} \ No newline at end of file diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/OpenApi/OpenApiDocumentMerge.json b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/OpenApi/OpenApiDocumentMerge.json new file mode 100644 index 000000000000..d33a4d3d515c --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/OpenApi/OpenApiDocumentMerge.json @@ -0,0 +1,437 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "OnTokenIssuanceStart", + "version": "10-01-2021-preview", + "contact": { + "name": "Microsoft" + }, + "summary": "OnTokenIssuanceStart 10-01-2021-preview", + "description": "API outline for OnTokenIssuanceStart 10-01-2021-preview" + }, + "servers": [ + { + "url": "" + } + ], + "paths": { + "/onTokenIssuanceStart/10-01-2021-preview": { + "post": { + "tags": [ + "Authentication Events", + "OnTokenIssuanceStart", + "10-01-2021-preview" + ], + "summary": "", + "operationId": "post-preview_10_01_2021", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$schema": "http://json-schema.org/draft-04/schema", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "onTokenIssuanceStartCustomExtension" + ] + }, + "apiSchemaVersion": { + "type": "string", + "enum": [ + "10-01-2021-preview" + ] + }, + "actions": { + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ProvideClaimsForToken" + ] + } + }, + "allOf": [ + { + "anyOf": [ + { + "not": { + "properties": { + "type": { + "enum": [ + "ProvideClaimsForToken" + ] + } + } + } + }, + { + "properties": { + "claims": { + "$ref": "#/components/schemas/claimsForToken" + } + }, + "required": [ + "claims" + ] + } + ] + } + ], + "required": [ + "type" + ] + } + } + }, + "required": [ + "type", + "apiSchemaVersion", + "actions" + ] + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$schema": "http://json-schema.org/draft-04/schema", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "onTokenIssuanceStartCustomExtension" + ], + "description": "OnTokenIssuanceStart Event" + }, + "apiSchemaVersion": { + "type": "string", + "enum": [ + "10-01-2021-preview" + ] + }, + "time": { + "type": "string" + }, + "eventListenerId": { + "type": "string" + }, + "customExtensionId": { + "type": "string" + }, + "context": { + "$ref": "#/components/schemas/context" + } + }, + "required": [ + "type", + "apiSchemaVersion", + "time", + "eventListenerId", + "customExtensionId", + "context" + ] + } + } + }, + "description": "OnTokenIssuanceStart payload" + }, + "description": "When a new token is issued" + }, + "parameters": [] + } + }, + "components": { + "schemas": { + "context": { + "type": "object", + "properties": { + "correlationId": { + "type": "string" + }, + "client": { + "$ref": "#/components/schemas/clientContext" + }, + "authProtocol": { + "$ref": "#/components/schemas/authProtocolContext" + }, + "clientServicePrincipal": { + "$ref": "#/components/schemas/servicePrincipalContext" + }, + "resourceServicePrincipal": { + "$ref": "#/components/schemas/servicePrincipalContext" + }, + "roles": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/userAppRole" + } + }, + "user": { + "$ref": "#/components/schemas/userPrincipalContext" + } + }, + "required": [ + "correlationId", + "client", + "authProtocol", + "clientServicePrincipal", + "resourceServicePrincipal", + "user" + ] + }, + "clientContext": { + "type": "object", + "properties": { + "ip": { + "type": "string" + }, + "locale": { + "type": "string" + }, + "market": { + "type": "string" + } + }, + "required": [ + "ip" + ] + }, + "authProtocolContext": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "tenantId": { + "type": "string" + } + }, + "required": [ + "type", + "tenantId" + ] + }, + "servicePrincipalContext": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "appId": { + "type": "string" + }, + "appDisplayName": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "servicePrincipalNames": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "appId", + "appDisplayName", + "displayName", + "servicePrincipalNames" + ] + }, + "userAppRole": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "id", + "value" + ] + }, + "userPrincipalContext": { + "type": "object", + "properties": { + "ageGroup": { + "type": [ + "string", + "null" + ] + }, + "companyName": { + "type": [ + "string", + "null" + ] + }, + "country": { + "type": [ + "string", + "null" + ] + }, + "createdDateTime": { + "type": [ + "string", + "null" + ] + }, + "creationType": { + "type": [ + "string", + "null" + ] + }, + "department": { + "type": [ + "string", + "null" + ] + }, + "displayName": { + "type": [ + "string", + "null" + ] + }, + "givenName": { + "type": [ + "string", + "null" + ] + }, + "id": { + "type": "string" + }, + "lastPasswordChangeDateTime": { + "type": [ + "string", + "null" + ] + }, + "mail": { + "type": [ + "string", + "null" + ] + }, + "onPremisesSamAccountName": { + "type": [ + "string", + "null" + ] + }, + "onPremisesSecurityIdentifier": { + "type": [ + "string", + "null" + ] + }, + "onPremiseUserPrincipalName": { + "type": [ + "string", + "null" + ] + }, + "preferredDataLocation": { + "type": [ + "string", + "null" + ] + }, + "preferredLanguage": { + "type": [ + "string", + "null" + ] + }, + "surname": { + "type": [ + "string", + "null" + ] + }, + "userPrincipalName": { + "type": "string" + }, + "userType": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "id", + "userPrincipalName" + ] + }, + "claimsForToken": { + "type": "array", + "items": { + "$ref": "#/components/schemas/claim" + } + }, + "claim": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "value": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + "required": [ + "id", + "value" + ] + } + } + }, + "tags": [ + { + "name": "10-01-2021-preview" + }, + { + "name": "Authentication Events" + }, + { + "name": "OnTokenIssuanceStart" + } + ] +} \ No newline at end of file diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/OpenApi/OpenApiPayloads.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/OpenApi/OpenApiPayloads.cs new file mode 100644 index 000000000000..411ec3504215 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/OpenApi/OpenApiPayloads.cs @@ -0,0 +1,27 @@ +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests.Payloads.OpenApi +{ + /// Test data for OpenApi + public static class OpenApiPayloads + { + /// Gets the open API document body. + /// The open API document. + public static string OpenApiDocument + { + get + { + return PayloadHelper.GetPayload("OpenApi.OpenApiDocument.json"); + } + } + + /// Gets the open API document merged sample payload. + /// The open API document merged. + public static string OpenApiDocumentMerge + { + get + { + return PayloadHelper.GetPayload("OpenApi.OpenApiDocumentMerge.json"); + + } + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/PayloadHelper.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/PayloadHelper.cs new file mode 100644 index 000000000000..2210253cf8a8 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/PayloadHelper.cs @@ -0,0 +1,47 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests.Payloads +{ + internal class PayloadHelper + { + public static string GetPayload(string name) + { + var defaultNS = typeof(PayloadHelper).Namespace; + return ReadResource(Assembly.GetExecutingAssembly(), string.Join(".", defaultNS, name)); + } + + private static string ReadResource(Assembly assembly, string resource, string defaultBody = null) + { + Stream stream = null; + StreamReader reader = null; + try + { + + if (!assembly.GetManifestResourceNames().Any(x => x == resource)) + { + return defaultBody != null ? defaultBody : throw new Exception($"Payload {resource} not found"); + } + stream = assembly.GetManifestResourceStream(resource); + + reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + finally + { + if (reader != null) + { + reader.Close(); + reader = null; + } + if (stream != null) + { + stream.Close(); + stream = null; + } + } + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/CloudEventActionResponse.json b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/CloudEventActionResponse.json new file mode 100644 index 000000000000..cec41052dcc2 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/CloudEventActionResponse.json @@ -0,0 +1,16 @@ +{ + "data": { + "actions": [ + { + "@odata.type": "microsoft.graph.provideClaimsForToken", + "claims": { + "DateOfBirth": "01/01/2000", + "CustomRoles": [ + "Writer", + "Editor" + ] + } + } + ] + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/ConversionPayload.json b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/ConversionPayload.json new file mode 100644 index 000000000000..9c2879597a46 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/ConversionPayload.json @@ -0,0 +1,14 @@ +{ + "actions": [ + { + "@odata.type": "microsoft.graph.provideClaimsForToken", + "claims": { + "DateOfBirth": "01/01/2000", + "CustomRoles": [ + "Writer", + "Editor" + ] + } + } + ] +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/ExpectedPayload.json b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/ExpectedPayload.json new file mode 100644 index 000000000000..eb87407d5e8d --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/ExpectedPayload.json @@ -0,0 +1,14 @@ +{ + "data": { + "@odata.type": "microsoft.graph.onTokenIssuanceStartResponseData", + "actions": [ + { + "@odata.type": "microsoft.graph.provideClaimsForToken", + "claims": { + "DateOfBirth": "01/01/2000", + "CustomRoles": [ "Writer", "Editor" ] + } + } + ] + } +} \ No newline at end of file diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/InvalidActionResponse.json b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/InvalidActionResponse.json new file mode 100644 index 000000000000..8d5cd30eb38d --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/InvalidActionResponse.json @@ -0,0 +1,20 @@ +{ + "actions": [ + { + "type": "ProvideClaims", + "claims": [ + { + "id": "DateOfBirth", + "value": "01/01/2000" + }, + { + "id": "CustomRoles", + "value": [ + "Writer", + "Editor" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/Legacy/ActionResponse.json b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/Legacy/ActionResponse.json new file mode 100644 index 000000000000..487e0f2fdadb --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/Legacy/ActionResponse.json @@ -0,0 +1,20 @@ +{ + "actions": [ + { + "type": "ProvideClaimsForToken", + "claims": [ + { + "id": "DateOfBirth", + "value": "01/01/2000" + }, + { + "id": "CustomRoles", + "value": [ + "Writer", + "Editor" + ] + } + ] + } + ] +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/Legacy/Cloud.json b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/Legacy/Cloud.json new file mode 100644 index 000000000000..074b1b59d60d --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/Legacy/Cloud.json @@ -0,0 +1,24 @@ +{ + "data": { + "@odata.type": "microsoft.graph.onTokenIssuanceStartResponseData", + "actions": [ + { + "type": "ProvideClaimsForToken", + "claims": [ + { + "id": "DateOfBirth", + "value": "01/01/2000" + }, + { + "id": "CustomRoles", + "value": [ + "Writer", + "Editor" + ] + } + ] + } + ] + }, + "type": "microsoft.graph.authenticationEvent.TokenIssuanceStart" +} \ No newline at end of file diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/Legacy/ExpectedPayload.json b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/Legacy/ExpectedPayload.json new file mode 100644 index 000000000000..9338a8b485ef --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/Legacy/ExpectedPayload.json @@ -0,0 +1,23 @@ +{ + "type": "onTokenIssuanceStartCustomExtension", + "apiSchemaVersion": "10-01-2021-preview", + "actions": [ + { + "type": "ProvideClaimsForToken", + "claims": [ + { + + "id": "DateOfBirth", + "value": "01/01/2000" + }, + { + "id": "CustomRoles", + "value": [ + "Writer", + "Editor" + ] + } + ] + } + ] +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/Legacy/RequestSchema.json b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/Legacy/RequestSchema.json new file mode 100644 index 000000000000..94544ba06c84 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/Legacy/RequestSchema.json @@ -0,0 +1,213 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ "onTokenIssuanceStartCustomExtension" ], + "description": "OnTokenIssuanceStart Event" + }, + "apiSchemaVersion": { + "type": "string", + "enum": [ "10-01-2021-preview" ] + }, + "time": { + "type": "string" + }, + "eventListenerId": { + "type": "string" + }, + "customExtensionId": { + "type": "string" + }, + "context": { "$ref": "#/definitions/context" } + }, + "required": [ + "type", + "apiSchemaVersion", + "time", + "eventListenerId", + "customExtensionId", + "context" + ], + + "definitions": { + + "context": { + "type": "object", + "properties": { + "correlationId": { + "type": "string" + }, + "client": { "$ref": "#/definitions/clientContext" }, + "authProtocol": { "$ref": "#/definitions/authProtocolContext" }, + "clientServicePrincipal": { "$ref": "#/definitions/servicePrincipalContext" }, + "resourceServicePrincipal": { "$ref": "#/definitions/servicePrincipalContext" }, + "roles": { + "type": [ "array", "null" ], + "items": { + "$ref": "#/definitions/userAppRole" + } + }, + "user": { "$ref": "#/definitions/userPrincipalContext" } + }, + "required": [ + "correlationId", + "client", + "authProtocol", + "clientServicePrincipal", + "resourceServicePrincipal", + "user" + ] + }, + + "clientContext": { + "type": "object", + "properties": { + "ip": { + "type": "string" + }, + "locale": { + "type": "string" + }, + "market": { + "type": "string" + } + }, + "required": [ + "ip" + ] + }, + + "authProtocolContext": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "tenantId": { + "type": "string" + } + }, + "required": [ + "type", + "tenantId" + ] + }, + + "servicePrincipalContext": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "appId": { + "type": "string" + }, + "appDisplayName": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "servicePrincipalNames": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "appId", + "appDisplayName", + "displayName", + "servicePrincipalNames" + ] + }, + + "userAppRole": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "id", + "value" + ] + }, + + "userPrincipalContext": { + "type": "object", + "properties": { + "ageGroup": { + "type": [ "string", "null" ] + }, + "companyName": { + "type": [ "string", "null" ] + }, + "country": { + "type": [ "string", "null" ] + }, + "createdDateTime": { + "type": [ "string", "null" ] + }, + "creationType": { + "type": [ "string", "null" ] + }, + "department": { + "type": [ "string", "null" ] + }, + "displayName": { + "type": [ "string", "null" ] + }, + "givenName": { + "type": [ "string", "null" ] + }, + "id": { + "type": "string" + }, + "lastPasswordChangeDateTime": { + "type": [ "string", "null" ] + }, + "mail": { + "type": [ "string", "null" ] + }, + "onPremisesSamAccountName": { + "type": [ "string", "null" ] + }, + "onPremisesSecurityIdentifier": { + "type": [ "string", "null" ] + }, + "onPremiseUserPrincipalName": { + "type": [ "string", "null" ] + }, + "preferredDataLocation": { + "type": [ "string", "null" ] + }, + "preferredLanguage": { + "type": [ "string", "null" ] + }, + "surname": { + "type": [ "string", "null" ] + }, + "userPrincipalName": { + "type": "string" + }, + "userType": { + "type": [ "string", "null" ] + } + }, + "required": [ + "id", + "userPrincipalName" + ] + } + } +} \ No newline at end of file diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/Legacy/ResponseSchema.json b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/Legacy/ResponseSchema.json new file mode 100644 index 000000000000..3c919a5af9bc --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/Legacy/ResponseSchema.json @@ -0,0 +1,88 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ "onTokenIssuanceStartCustomExtension" ] + }, + "apiSchemaVersion": { + "type": "string", + "enum": [ "10-01-2021-preview" ] + }, + "actions": { + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ "ProvideClaimsForToken" ] + } + }, + "allOf": [ + { + "anyOf": [ + { + "not": { + "properties": { "type": { "enum": [ "ProvideClaimsForToken" ] } } + } + }, + { + "properties": { "claims": { "$ref": "#/definitions/claimsForToken" } }, + "required": [ "claims" ] + } + ] + } + ], + "required": [ + "type" + ] + } + } + }, + "required": [ + "type", + "apiSchemaVersion", + "actions" + ], + + "definitions": { + + "claimsForToken": { + "type": "array", + "items": { + "$ref": "#/definitions/claim" + } + }, + + "claim": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "value": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + "required": [ + "id", + "value" + ] + } + } + +} \ No newline at end of file diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/Legacy/TokenIssuanceStartLegacy.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/Legacy/TokenIssuanceStartLegacy.cs new file mode 100644 index 000000000000..b193961dfae9 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/Legacy/TokenIssuanceStartLegacy.cs @@ -0,0 +1,55 @@ +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests.Payloads.TokenIssuanceStart.Legacy +{ + /// Test data for the Token Issuance Start Request version: Legacy + public static class TokenIssuanceStartLegacy + { + /// The expected response payload. + /// The expected payload. + public static string ExpectedPayload + { + get + { + return PayloadHelper.GetPayload("TokenIssuanceStart.Legacy.ExpectedPayload.json"); + } + } + + /// Mocks the data expected from the function execution. + /// The function response. + public static string ActionResponse + { + get + { + return PayloadHelper.GetPayload("TokenIssuanceStart.Legacy.ActionResponse.json"); + } + } + + /// Gets the request schema body. + /// The request schema. + public static string RequestSchema + { + get + { + return PayloadHelper.GetPayload("TokenIssuanceStart.Legacy.RequestSchema.json"); + + } + } + + /// Gets the response schema body. + /// The response schema. + public static string ResponseSchema + { + get + { + return PayloadHelper.GetPayload("TokenIssuanceStart.Legacy.ResponseSchema.json"); + + } + } + + /// Gets the version name space. + /// The version name space. + public static string VersionNameSpace + { + get { return "TokenIssuanceStart.preview_10_01_2021"; } + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/NoActionResponse.json b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/NoActionResponse.json new file mode 100644 index 000000000000..9d430603c4e4 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/NoActionResponse.json @@ -0,0 +1,3 @@ +{ + "actions": [] +} \ No newline at end of file diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/QueryParameters.json b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/QueryParameters.json new file mode 100644 index 000000000000..6cb89cb08bc7 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/QueryParameters.json @@ -0,0 +1,14 @@ +{ + "tokenClaims": {}, + "type": "", + "source": "", + "oDataType": "", + "response": null, + "payload": null, + "requestStatus": "Failed", + "statusMessage": "", + "queryParameters": { + "param1": "test1", + "param2": "test2" + } +} \ No newline at end of file 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 new file mode 100644 index 000000000000..db63a1fc699c --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/Payloads/TokenIssuanceStart/TokenIssuanceStart.cs @@ -0,0 +1,64 @@ +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests.Payloads.TokenIssuanceStart +{ + /// Test data for the Token Issuance Start + public static class TokenIssuanceStart + { + /// Gets the no action response. + /// The no action response. + public static string NoActionResponse + { + get + { + return PayloadHelper.GetPayload("TokenIssuanceStart.NoActionResponse.json"); + } + } + /// Gets the invalid action response. + /// The invalid action response. + public static string InvalidActionResponse + { + get + { + return PayloadHelper.GetPayload("TokenIssuanceStart.InvalidActionResponse.json"); + + } + } + /// Mocks the data expected from the function execution. + /// The function response. + public static string ActionResponse + { + get + { + return PayloadHelper.GetPayload("TokenIssuanceStart.CloudEventActionResponse.json"); + } + } + + /// Gets the expected payload. + /// The expected payload. + public static string ExpectedPayload + { + get + { + return PayloadHelper.GetPayload("TokenIssuanceStart.ExpectedPayload.json"); + } + } + + /// Mocks the data expected from the function execution that will be converted by yhe EventResponseHandler to an IActionResult. + /// The function response. + public static string ConversionPayload + { + get + { + return PayloadHelper.GetPayload("TokenIssuanceStart.ConversionPayload.json"); + } + } + /// Gets the token issuance start query parameter expected payload + /// The token issuance start query parameter. + public static string TokenIssuanceStartQueryParameter + { + get + { + return PayloadHelper.GetPayload("TokenIssuanceStart.QueryParameters.json"); + } + } + } +} diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/ResponseTypesTests.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/ResponseTypesTests.cs new file mode 100644 index 000000000000..ad5e0e5d105b --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/ResponseTypesTests.cs @@ -0,0 +1,135 @@ + +using Microsoft.AspNetCore.Http; +using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests.Payloads.TokenIssuanceStart.Legacy; +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading; +using Xunit; +using static Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests.TestHelper; +using payloads = Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests.Payloads.TokenIssuanceStart; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests +{ + /// Class for housing all tests pertaining to Response Type Casting + public class ResponseTypesTests + { + /// Available Types to test + public enum ResponseTypes + { + /// Function response is of type string + String, + /// Function response is of type HttpResponse + HttpResponse, + /// Function response is of type HttpResponseMessage + HttpResponseMessage, + /// An unknown type that raises failure + Unknown, + /// Function response is of type AuthEventResponse + AuthEventResponse + } + + /// Test the response value setting based on the response type expected. + /// The response type to test as a function return value. + [Theory] + [InlineData(ResponseTypes.String)] + [InlineData(ResponseTypes.HttpResponse)] + [InlineData(ResponseTypes.HttpResponseMessage)] + [InlineData(ResponseTypes.Unknown)] + [InlineData(ResponseTypes.AuthEventResponse)] + [Obsolete] + public async void Tests(ResponseTypes responseType) + { + var (code, payload) = GetExpected(responseType); + + HttpResponseMessage httpResponseMessage = await EventResponseBaseTest(eventsResponseHandler => + { + MemoryStream memoryStream = null; + StreamWriter streamWriter = null; + try + { + memoryStream = new MemoryStream(); + streamWriter = new StreamWriter(memoryStream); + eventsResponseHandler.SetValueAsync(GetResponseTypeObject(responseType, streamWriter), CancellationToken.None); + } + finally + { + if (streamWriter != null) + { + streamWriter.Close(); + streamWriter = null; + } + if (memoryStream != null) + { + memoryStream.Close(); + memoryStream = null; + } + } + }); + + Assert.Equal(httpResponseMessage.StatusCode, code); + Assert.True(DoesPayloadMatch(payload, httpResponseMessage.Content.ReadAsStringAsync().Result)); + } + + private object GetResponseTypeObject(ResponseTypes responseType, StreamWriter streamWriter) + { + switch (responseType) + { + case ResponseTypes.String: return TokenIssuanceStartLegacy.ActionResponse; + case ResponseTypes.HttpResponse: return CreateHttpResponse(streamWriter); + case ResponseTypes.HttpResponseMessage: return CreateHttpResponseMessage(); + case ResponseTypes.AuthEventResponse: return new TestAuthResponse(HttpStatusCode.OK, payloads.TokenIssuanceStart.ActionResponse); + case ResponseTypes.Unknown: return new object(); + default: return null; + } + } + + /// Get the expected HTTP status and payload. + /// Expected results for comparison based on Response Type + /// Returns the HttpStatusCode, if the expected HTTP response is json and if what the expected body of the response is. + private (HttpStatusCode code, string payload) GetExpected(ResponseTypes responseType) + { + switch (responseType) + { + case ResponseTypes.String: + case ResponseTypes.HttpResponse: + case ResponseTypes.HttpResponseMessage: + return (code: HttpStatusCode.OK, TokenIssuanceStartLegacy.ExpectedPayload); + case ResponseTypes.AuthEventResponse: + return (code: HttpStatusCode.OK, payloads.TokenIssuanceStart.ActionResponse); + case ResponseTypes.Unknown: + return (code: HttpStatusCode.BadRequest, @"{'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); + }; + } + + /// Creates a HttpResponse object based on default HTTP context. + /// The stream writer to use to right the HTTP response body. + /// A newly created HttpResponse with the a payload set. + private HttpResponse CreateHttpResponse(StreamWriter sw) + { + var response = new DefaultHttpContext().Response; + + sw.Write(TokenIssuanceStartLegacy.ActionResponse); + sw.Flush(); + + response.Body = sw.BaseStream; + response.Body.Position = 0; + return response; + } + + /// Creates the HTTP response message. + /// A newly created HttpResponseMessage with the payload set. + private HttpResponseMessage CreateHttpResponseMessage() + { + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(TokenIssuanceStartLegacy.ActionResponse) + }; + } + + } +} + diff --git a/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/TestHelper.cs b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/TestHelper.cs new file mode 100644 index 000000000000..8efe54185001 --- /dev/null +++ b/sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/tests/TestHelper.cs @@ -0,0 +1,364 @@ +//using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart; +using Microsoft.Azure.WebJobs.Host.Executors; +using Microsoft.Extensions.Logging; +using Moq; +using Newtonsoft.Json.Linq; +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.Tests +{ + /// Static Helper Methods for running tests. + public static partial class TestHelper + { + private const string DefaultNamespace = "Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents"; + private static readonly Assembly MainAssembly = Assembly.Load(DefaultNamespace); + + /// Enum for HTTP methods + public enum HttpMethods + { + /// + /// The post + /// + Post, + /// + /// The get + /// + Get + } + + /// The validating JSON schema type. + public enum TestSchemaType + { + /// Request Schema + Request, + + /// Response Schema + Response + } + + /// + /// This function will create action results based on incoming httpStatusCode + /// + /// + /// + internal static TestAuthResponse GetContentForHttpStatus(HttpStatusCode httpStatusCode) + { + + switch (httpStatusCode) + { + case HttpStatusCode.OK: return new TestAuthResponse(HttpStatusCode.OK, "{'hi':'bye'}"); + case HttpStatusCode.Unauthorized: return new TestAuthResponse(HttpStatusCode.Unauthorized); + default: return new TestAuthResponse(HttpStatusCode.BadRequest); + } + } + + /// + /// This function creates HttpRequestMessage using the incoming params + /// + /// + /// + /// + /// + public static HttpRequestMessage CreateHttpRequestMessage(HttpMethod method, string url, string body) + { + HttpRequestMessage requestMessage = new HttpRequestMessage(method, url); + requestMessage.Headers.Add("Authorization", "bearer 123123123123"); + requestMessage.Headers.Add("Accept", "*/*"); + requestMessage.Headers.Add("Connection", "keep-alive"); + requestMessage.Headers.Add("Accept-Encoding", "gzip, deflate, br"); + if (!string.IsNullOrEmpty(body)) + { + requestMessage.Content = new StringContent(body); + } + + return requestMessage; + } + + /// Sets up the boilerplate code for running end to end system tests.

Sets the HTTP methods as post and a default function URL called OnTokenIssuanceStart
+ /// Action to emulate the external function call. + /// A HttpResponseMessage containing the a result pertaining to the action expectations. + public static async Task BaseTest(Action action) + { + return await BaseTest(HttpMethods.Post, "http://test/mock?function=onTokenissuancestart", action); + } + + /// + /// Sets up the boilerplate code for running end to end system tests. Returning a valid EventResponseHandler in the action

Sets the HTTP methods as post and a default function URL called OnTokenIssuanceStart + ///
+ /// Action to emulate the external function call. + /// Defines the type of test + /// A HttpResponseMessage containing the a result pertaining to the action expectations. + [Obsolete] + public static async Task EventResponseBaseTest(Action action, TestTypes testTypes) + { + return await EventResponseBaseTest(HttpMethods.Post, "http://test/mock?function=onTokenissuancestart", action, testTypes); + } + + /// + /// Sets up the boilerplate code for running end to end system tests. Returning a valid EventResponseHandler in the action

Sets the HTTP methods as post and a default function URL called OnTokenIssuanceStart + ///
+ /// Action to emulate the external function call. + /// A HttpResponseMessage containing the a result pertaining to the action expectations. + [Obsolete] + public static async Task EventResponseBaseTest(Action action) + { + return await EventResponseBaseTest(HttpMethods.Post, "http://test/mock?function=onTokenissuancestart", action); + } + + /// + /// Sets up the boilerplate code for running end to end system tests. Returning a valid EventResponseHandler in the action

Sets the HTTP methods as post and a default function URL called OnTokenIssuanceStart + ///
+ /// Type of methods. i.e. Post/Get + /// The URL to use to create an inactive mock end point + /// Action to emulate the external function call. + /// defines the type of test + /// A HttpResponseMessage containing the a result pertaining to the action expectations. + /// + [Obsolete] + public static async Task EventResponseBaseTest(HttpMethods httpMethods, string url, Action action, TestTypes testTypes) + { + return await (BaseTest(httpMethods, url, t => + { + if (t.FunctionData.TriggerValue is HttpRequestMessage mockedRequest) + { + AuthenticationEventResponseHandler eventsResponseHandler = (AuthenticationEventResponseHandler)mockedRequest.Properties[AuthenticationEventResponseHandler.EventResponseProperty]; + eventsResponseHandler.Request = new TokenIssuanceStartRequest(t.RequestMessage) + { + Response = testTypes == TestTypes.ValidCloudEvent ? CreateTokenIssuanceStartResponse() : CreateIssuanceStartLegacyResponse(), + RequestStatus = RequestStatusType.Successful + }; + + action(eventsResponseHandler); + } + })); + } + + /// + /// Sets up the boilerplate code for running end to end system tests. Returning a valid EventResponseHandler in the action

Sets the HTTP methods as post and a default function URL called OnTokenIssuanceStart + ///
+ /// Type of methods. i.e. Post/Get + /// The URL to use to create an inactive mock end point + /// Action to emulate the external function call. + /// A HttpResponseMessage containing the a result pertaining to the action expectations. + [Obsolete] + public static async Task EventResponseBaseTest(HttpMethods httpMethods, string url, Action action) + { + return await (BaseTest(httpMethods, url, t => + { + if (t.FunctionData.TriggerValue is HttpRequestMessage mockedRequest) + { + AuthenticationEventResponseHandler eventsResponseHandler = (AuthenticationEventResponseHandler)mockedRequest.Properties[AuthenticationEventResponseHandler.EventResponseProperty]; + eventsResponseHandler.Request = new TokenIssuanceStartRequest(t.RequestMessage) + { + + Response = CreateIssuanceStartLegacyResponse(), + RequestStatus = RequestStatusType.Successful + }; + + action(eventsResponseHandler); + } + })); + } + + + /// Sets up the boilerplate code for running end to end system tests. + /// Type of methods. i.e. Post/Get + /// The URL to use to create an inactive mock end point + /// Action to emulate the external function call. + /// A HttpResponseMessage containing the a result pertaining to the action expectations. + public static async Task BaseTest(HttpMethods httpMethods, string url, Action action) + { + HttpRequestMessage requestMessage = CreateHttpRequestMessage(httpMethods == HttpMethods.Post ? HttpMethod.Post : HttpMethod.Get, url); + + AuthenticationEventsTriggerAttribute attr = CreateAuthenticationEventTriggerAttribute("Tenant", "App"); + + Mock mockObject = new Mock(); + + AuthenticationEventConfigProvider eventsTriggerConfigProvider = new AuthenticationEventConfigProvider(new LoggerFactory()); + + eventsTriggerConfigProvider.Listeners.Add("onTokenIssuanceStart", new AuthenticationEventListener(mockObject.Object, attr)); + + mockObject.Setup(m => m.TryExecuteAsync(It.IsAny(), It.IsAny())).Callback( + (t, x) => + { + action(new ActionParameters() + { + FunctionData = t, + RequestMessage = requestMessage + }); + }).ReturnsAsync(new FunctionResult(true)); + + return await eventsTriggerConfigProvider.ConvertAsync(requestMessage, new CancellationToken(false)); + } + + /// + /// This function creates HttpRequestMessage using the incoming params + /// + /// + /// + /// A newly created HttpRequestMessage + public static HttpRequestMessage CreateHttpRequestMessage(HttpMethod method, string url) + { + return CreateHttpRequestMessage(method, url, null); + } + + /// + /// This function creates AuthenticationEventTriggerAttribute using the incoming params + /// + /// Available version + /// Available Event type + /// A newly create AuthenticationEventTriggerAttribute + internal static AuthenticationEventsTriggerAttribute CreateAuthenticationEventTriggerAttribute(EventDefinition versions, EventType eventTypes) + { + return CreateAuthenticationEventTriggerAttribute(string.Empty, string.Empty); + } + + /// + /// This function creates AuthenticationEventTriggerAttribute using the incoming params + /// + /// + /// + /// A newly create AuthenticationEventTriggerAttribute + public static AuthenticationEventsTriggerAttribute CreateAuthenticationEventTriggerAttribute(string tenantId, string audienceAppId) + { + + return new AuthenticationEventsTriggerAttribute() + { + TenantId = tenantId, + AudienceAppId = audienceAppId + }; + } + + /// Reads the content of an embedded resource. + /// The assembly where the resource is embedded. + /// The resource Identifier + /// The content of the resource as a string. + /// + public static string ReadResource(Assembly assembly, string resource) + { + Stream stream = null; + StreamReader reader = null; + try + { + if (!assembly.GetManifestResourceNames().Any(x => x == resource)) + { + throw new MissingFieldException(); + } + + stream = assembly.GetManifestResourceStream(resource); + reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + finally + { + if (reader != null) + { + reader.Close(); + reader = null; + } + if (stream != null) + { + stream.Close(); + stream = null; + } + } + } + + /// Gets an attribute from an enumerator field + /// The Type of the attribute on the enumerator field. + /// The enum that the field is on + /// The Attribute if found. + public static TAttribute GetAttribute(this Enum value) where TAttribute : Attribute + { + var type = value.GetType(); + var name = Enum.GetName(type, value); + + return type.GetField(name) + .GetCustomAttributes(false) + .OfType() + .SingleOrDefault(); + } + + /// Creates the issuance start Legacy response. + /// A newly created TokenIssuanceStartResponse for version preview_10_01_2021 + public static TokenIssuanceStartResponse CreateIssuanceStartLegacyResponse() + { + JObject jBody = JObject.Parse(ReadResource(MainAssembly, String.Join(".", DefaultNamespace, "Templates", "ActionableTemplate.json"))); + (jBody["type"] as JValue).Value = "onTokenIssuanceStartCustomExtension"; + (jBody["apiSchemaVersion"] as JValue).Value = "10-01-2021-preview"; + + + return new TokenIssuanceStartResponse() + { + Body = jBody.ToString() + }; + } + + /// Creates the issuance start Legacy response. + /// 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"; + + + return new TokenIssuanceStartResponse() + { + Body = jBody.ToString() + }; + } + + /// Deep probes json to confirm if two pieces of Json are identical if not Json then normal Ordinal String comparison. + /// The actual. + /// The expected. + /// True if the actual and expected are identical. + public static bool DoesPayloadMatch(string expected, string actual) + { + if (IsJson(expected)) + { + var jExpected = JToken.Parse(expected); + var jActual = JToken.Parse(actual); + + return JToken.DeepEquals(jActual, jExpected); + } + else + { + return actual.Equals(expected, StringComparison.Ordinal); + } + } + /// Does the file payload match. + /// The expected payload. + /// The path to the file containing the payload + /// True if payloads match + public static bool DoesFilePayloadMatch(string expected, string path) + { + return File.Exists(path) ? DoesPayloadMatch(expected, File.ReadAllText(path)) : false; + } + + + /// Determines whether the specified input is json. + /// The input. + /// + /// true if the specified input is json; otherwise, false. + internal static bool IsJson(string input) + { + if (string.IsNullOrEmpty(input)) + { + return false; + } + + input = input.Trim(); + return (input.StartsWith("{", StringComparison.OrdinalIgnoreCase) && input.EndsWith("}", StringComparison.OrdinalIgnoreCase)) + || (input.StartsWith("[", StringComparison.OrdinalIgnoreCase) && input.EndsWith("]", StringComparison.OrdinalIgnoreCase)); + } + } +} diff --git a/sdk/entra/ci.yml b/sdk/entra/ci.yml new file mode 100644 index 000000000000..69a0bbe7fdd8 --- /dev/null +++ b/sdk/entra/ci.yml @@ -0,0 +1,31 @@ +# NOTE: Please refer to https://aka.ms/azsdk/engsys/ci-yaml before editing this file. + +trigger: + branches: + include: + - main + - hotfix/* + - release/* + paths: + include: + - sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/ + +pr: + branches: + include: + - main + - feature/* + - hotfix/* + - release/* + paths: + include: + - sdk/entra/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents/ + +extends: + template: /eng/pipelines/templates/stages/archetype-sdk-client.yml + parameters: + ServiceDirectory: entra + ArtifactName: packages + Artifacts: + - name: Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents + safeName: MicrosoftAzureWebJobsExtensionsAuthenticationEvents