diff --git a/RSK.IdentityServer4.AuditEventSink.sln b/RSK.IdentityServer4.AuditEventSink.sln
index 2a4c179..cfc15e4 100644
--- a/RSK.IdentityServer4.AuditEventSink.sln
+++ b/RSK.IdentityServer4.AuditEventSink.sln
@@ -21,6 +21,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rsk.Audit.Tests.Common", "t
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rsk.Audit.Tests.Integration", "tests\Rsk.Audit.Tests.Integration\Rsk.Audit.Tests.Integration.csproj", "{374C47E2-B685-47BD-AD1A-6E435623E055}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rsk.Open.IdentityServer.AuditEventSink", "src\Rsk.Open.IdentityServer.AuditEventSink\Rsk.Open.IdentityServer.AuditEventSink.csproj", "{535B7E62-8578-4B75-AB82-81E854A3F8DD}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rsk.Open.IdentityServer.AuditEventSink.Tests", "tests\Rsk.Open.IdentityServer.AuditEventSink.Tests\Rsk.Open.IdentityServer.AuditEventSink.Tests.csproj", "{CE817EDF-273F-4605-926D-A876DD1AA5BC}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -55,6 +59,14 @@ Global
{374C47E2-B685-47BD-AD1A-6E435623E055}.Debug|Any CPU.Build.0 = Debug|Any CPU
{374C47E2-B685-47BD-AD1A-6E435623E055}.Release|Any CPU.ActiveCfg = Release|Any CPU
{374C47E2-B685-47BD-AD1A-6E435623E055}.Release|Any CPU.Build.0 = Release|Any CPU
+ {535B7E62-8578-4B75-AB82-81E854A3F8DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {535B7E62-8578-4B75-AB82-81E854A3F8DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {535B7E62-8578-4B75-AB82-81E854A3F8DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {535B7E62-8578-4B75-AB82-81E854A3F8DD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CE817EDF-273F-4605-926D-A876DD1AA5BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CE817EDF-273F-4605-926D-A876DD1AA5BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CE817EDF-273F-4605-926D-A876DD1AA5BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CE817EDF-273F-4605-926D-A876DD1AA5BC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -67,6 +79,8 @@ Global
{3EA6C204-5D23-4844-B7D7-4C342CA401CD} = {4AD87BDC-20A4-4426-80E4-0706AA9D8294}
{6FAFBAE2-745C-4243-BDA9-F01A6973F8D6} = {4AD87BDC-20A4-4426-80E4-0706AA9D8294}
{374C47E2-B685-47BD-AD1A-6E435623E055} = {4AD87BDC-20A4-4426-80E4-0706AA9D8294}
+ {535B7E62-8578-4B75-AB82-81E854A3F8DD} = {0F18CE73-394B-4E63-8791-E6FDD93581AC}
+ {CE817EDF-273F-4605-926D-A876DD1AA5BC} = {4AD87BDC-20A4-4426-80E4-0706AA9D8294}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {741C58BE-23A1-4690-81D8-57D7789ACE7F}
diff --git a/azure-pipelines.open-identity-server.yml b/azure-pipelines.open-identity-server.yml
new file mode 100644
index 0000000..0331f37
--- /dev/null
+++ b/azure-pipelines.open-identity-server.yml
@@ -0,0 +1,91 @@
+trigger: none
+pr: none
+
+variables:
+- group: "NugetPackageRelease"
+- name: buildConfiguration
+ value: 'Release'
+- name: identityPackageVersion
+ value: '1.0.0.0'
+
+stages:
+- stage: Build
+ jobs:
+ - job: BuildOpenIdentityServer
+ strategy:
+ matrix:
+ linux:
+ imageName: 'ubuntu-latest'
+ shouldPack: true
+ mac:
+ imageName: 'macOS-latest'
+ shouldPack: true
+ windows:
+ imageName: 'windows-latest'
+ shouldPack: true
+ pool:
+ vmImage: $(imageName)
+ steps:
+ - task: UseDotNet@2
+ displayName: Install .NET Core sdk version 10.x
+ inputs:
+ packageType: sdk
+ version: 10.x
+ installationPath: $(Agent.ToolsDirectory)/dotnet
+ - task: NuGetToolInstaller@0
+ inputs:
+ versionSpec: 5.4.0
+ - task: DotNetCoreCLI@2
+ displayName: 'Dotnet restore'
+ inputs:
+ command: 'restore'
+ projects: 'src/Rsk.Open.IdentityServer.AuditEventSink/Rsk.Open.IdentityServer.AuditEventSink.csproj'
+ feedsToUse: 'config'
+ nugetConfigPath: $(System.DefaultWorkingDirectory)/NuGet.AzDO.config
+ - task: DotNetCoreCLI@2
+ displayName: dotnet build src/Rsk.Open.IdentityServer.AuditEventSink/Rsk.Open.IdentityServer.AuditEventSink.csproj
+ inputs:
+ command: 'build'
+ projects: 'src/Rsk.Open.IdentityServer.AuditEventSink/Rsk.Open.IdentityServer.AuditEventSink.csproj'
+ arguments: -c $(buildConfiguration) --no-restore /p:Version="$(identityPackageVersion)"
+ - task: DotNetCoreCLI@2
+ displayName: dotnet test - Run Open IdentityServer Tests
+ inputs:
+ command: 'test'
+ projects: 'tests/Rsk.Open.IdentityServer.AuditEventSink.Tests/Rsk.Open.IdentityServer.AuditEventSink.Tests.csproj'
+ arguments: -c $(buildConfiguration) --no-restore
+ - task: DotNetCoreCLI@2
+ displayName: Package Rsk.Open.IdentityServer.AuditEventSink.csproj for Nuget
+ inputs:
+ command: 'pack'
+ packagesToPack: 'src/Rsk.Open.IdentityServer.AuditEventSink/Rsk.Open.IdentityServer.AuditEventSink.csproj'
+ nobuild: true
+ includesymbols: true
+ versionEnvVar: identityPackageVersion
+ versioningScheme: 'byEnvVar'
+ verbosityPack: 'Normal'
+ outputDir: '$(Build.ArtifactStagingDirectory)'
+ - task: PublishBuildArtifacts@1
+ condition: and(succeeded(), eq(variables['shouldPack'], true))
+ displayName: Publish Open IdentityServer artifacts
+ inputs:
+ PathtoPublish: '$(Build.ArtifactStagingDirectory)'
+ ArtifactName: 'Rsk.Open.IdentityServer.AuditEventSink nupkg'
+ publishLocation: 'Container'
+
+- stage: Publish
+ dependsOn: Build
+ condition: succeeded()
+ jobs:
+ - deployment: PublishNuGet
+ environment: Release
+ strategy:
+ runOnce:
+ deploy:
+ steps:
+ - template: templates/publish-nuget.yml
+ parameters:
+ toLive: true
+ artifact: 'Rsk.Open.IdentityServer.AuditEventSink nupkg'
+ packageToPublish: '$(Pipeline.Workspace)/Rsk.Open.IdentityServer.AuditEventSink nupkg/*.nupkg'
+
diff --git a/src/Rsk.Audit.EF/Rsk.Audit.EF.csproj b/src/Rsk.Audit.EF/Rsk.Audit.EF.csproj
index 06d75e2..cf16ece 100644
--- a/src/Rsk.Audit.EF/Rsk.Audit.EF.csproj
+++ b/src/Rsk.Audit.EF/Rsk.Audit.EF.csproj
@@ -26,7 +26,7 @@
-
+
diff --git a/src/Rsk.DuendeIdentityServer.AuditEventSink/Rsk.DuendeIdentityServer.AuditEventSink.csproj b/src/Rsk.DuendeIdentityServer.AuditEventSink/Rsk.DuendeIdentityServer.AuditEventSink.csproj
index c11d6d8..afbfe8b 100644
--- a/src/Rsk.DuendeIdentityServer.AuditEventSink/Rsk.DuendeIdentityServer.AuditEventSink.csproj
+++ b/src/Rsk.DuendeIdentityServer.AuditEventSink/Rsk.DuendeIdentityServer.AuditEventSink.csproj
@@ -1,4 +1,4 @@
-
+
net10.0
diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/AdapterFactory.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/AdapterFactory.cs
new file mode 100644
index 0000000..d08c3ee
--- /dev/null
+++ b/src/Rsk.Open.IdentityServer.AuditEventSink/AdapterFactory.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+using Open.IdentityServer.Events;
+using RSK.Audit;
+using Rsk.Open.IdentityServer.AuditEventSink.Adapters;
+
+namespace Rsk.Open.IdentityServer.AuditEventSink
+{
+ public class AdapterFactory : IAdapterFactory
+ {
+ private readonly Dictionary> eventAdapters;
+
+ public AdapterFactory(IDictionary> customEventAdapters = null)
+ {
+ eventAdapters = CreateDefaultEventAdapters();
+
+ if (customEventAdapters == null) return;
+
+ foreach (var mapping in customEventAdapters)
+ {
+ eventAdapters[mapping.Key] = mapping.Value;
+ }
+ }
+
+ public IAuditEventArguments Create(Event evt)
+ {
+ if (evt == null)
+ {
+ return null;
+ }
+
+ return eventAdapters.TryGetValue(evt.GetType(), out var adapterFactory)
+ ? adapterFactory(evt)
+ : null;
+ }
+
+ private static Dictionary> CreateDefaultEventAdapters()
+ {
+ return new Dictionary>
+ {
+ [typeof(TokenIssuedSuccessEvent)] = e => new TokenIssuedSuccessEventAdapter((TokenIssuedSuccessEvent)e),
+ [typeof(UserLoginSuccessEvent)] = e => new UserLoginSuccessEventAdapter((UserLoginSuccessEvent)e),
+ [typeof(UserLoginFailureEvent)] = e => new UserLoginFailureEventAdapter((UserLoginFailureEvent)e),
+ [typeof(UserLogoutSuccessEvent)] = e => new UserLogoutSuccessEventAdapter((UserLogoutSuccessEvent)e),
+ [typeof(ConsentGrantedEvent)] = e => new ConsentGrantedEventAdapter((ConsentGrantedEvent)e),
+ [typeof(ConsentDeniedEvent)] = e => new ConsentDeniedEventAdapter((ConsentDeniedEvent)e),
+ [typeof(TokenIssuedFailureEvent)] = e => new TokenIssuedFailureEventAdapter((TokenIssuedFailureEvent)e),
+ [typeof(GrantsRevokedEvent)] = e => new GrantsRevokedEventAdapter((GrantsRevokedEvent)e),
+ [typeof(DeviceAuthorizationFailureEvent)] = e => new DeviceAuthorizationFailureEventAdapter((DeviceAuthorizationFailureEvent)e),
+ [typeof(DeviceAuthorizationSuccessEvent)] = e => new DeviceAuthorizationSuccessEventAdapter((DeviceAuthorizationSuccessEvent)e),
+ [typeof(TokenRevokedSuccessEvent)] = e => new TokenRevokedSuccessEventAdapter((TokenRevokedSuccessEvent)e),
+ [typeof(InvalidClientConfigurationEvent)] = e => new InvalidClientConfigurationEventAdapter((InvalidClientConfigurationEvent)e),
+ [typeof(TokenIntrospectionFailureEvent)] = e => new TokenIntrospectionFailureEventAdapter((TokenIntrospectionFailureEvent)e),
+ [typeof(TokenIntrospectionSuccessEvent)] = e => new TokenIntrospectionSuccessEventAdapter((TokenIntrospectionSuccessEvent)e),
+ [typeof(ClientAuthenticationFailureEvent)] = e => new ClientAuthenticationFailureEventAdapter((ClientAuthenticationFailureEvent)e),
+ [typeof(ClientAuthenticationSuccessEvent)] = e => new ClientAuthenticationSuccessEventAdapter((ClientAuthenticationSuccessEvent)e),
+ [typeof(ApiAuthenticationFailureEvent)] = e => new ApiAuthenticationFailureEventAdapter((ApiAuthenticationFailureEvent)e),
+ [typeof(ApiAuthenticationSuccessEvent)] = e => new ApiAuthenticationSuccessEventAdapter((ApiAuthenticationSuccessEvent)e),
+ [typeof(UnhandledExceptionEvent)] = e => new UnhandledExceptionEventAdapter((UnhandledExceptionEvent)e)
+ };
+ }
+ }
+}
diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ApiAuthenticationFailureEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ApiAuthenticationFailureEventAdapter.cs
new file mode 100644
index 0000000..bc3b528
--- /dev/null
+++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ApiAuthenticationFailureEventAdapter.cs
@@ -0,0 +1,21 @@
+using System;
+using Open.IdentityServer.Events;
+using RSK.Audit;
+
+namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters
+{
+ public class ApiAuthenticationFailureEventAdapter : IAuditEventArguments
+ {
+ private readonly ApiAuthenticationFailureEvent evt;
+
+ public ApiAuthenticationFailureEventAdapter(ApiAuthenticationFailureEvent evt)
+ {
+ this.evt = evt ?? throw new ArgumentNullException(nameof(evt));
+ }
+
+ public ResourceActor Actor => new ResourceActor(ResourceActor.MachineSubjectType, null, null);
+ public string Action => evt.Name;
+ public AuditableResource Resource => new AuditableResource("IdentityServer", evt.ApiName);
+ public FormattedString Description => evt.ToString().SafeForFormatted();
+ }
+}
diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ApiAuthenticationSuccessEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ApiAuthenticationSuccessEventAdapter.cs
new file mode 100644
index 0000000..39bcacc
--- /dev/null
+++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ApiAuthenticationSuccessEventAdapter.cs
@@ -0,0 +1,21 @@
+using System;
+using Open.IdentityServer.Events;
+using RSK.Audit;
+
+namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters
+{
+ public class ApiAuthenticationSuccessEventAdapter : IAuditEventArguments
+ {
+ private readonly ApiAuthenticationSuccessEvent evt;
+
+ public ApiAuthenticationSuccessEventAdapter(ApiAuthenticationSuccessEvent evt)
+ {
+ this.evt = evt ?? throw new ArgumentNullException(nameof(evt));
+ }
+
+ public ResourceActor Actor => new ResourceActor(ResourceActor.MachineSubjectType, null, null);
+ public string Action => evt.Name;
+ public AuditableResource Resource => new AuditableResource("IdentityServer", evt.ApiName);
+ public FormattedString Description => evt.ToString().SafeForFormatted();
+ }
+}
diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ClientAuthenticationFailureEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ClientAuthenticationFailureEventAdapter.cs
new file mode 100644
index 0000000..b7c13fb
--- /dev/null
+++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ClientAuthenticationFailureEventAdapter.cs
@@ -0,0 +1,21 @@
+using System;
+using Open.IdentityServer.Events;
+using RSK.Audit;
+
+namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters
+{
+ public class ClientAuthenticationFailureEventAdapter : IAuditEventArguments
+ {
+ private readonly ClientAuthenticationFailureEvent evt;
+
+ public ClientAuthenticationFailureEventAdapter(ClientAuthenticationFailureEvent evt)
+ {
+ this.evt = evt ?? throw new ArgumentNullException(nameof(evt));
+ }
+
+ public ResourceActor Actor => new ResourceActor(ResourceActor.MachineSubjectType, evt.ClientId, evt.ClientId);
+ public string Action => evt.Name;
+ public AuditableResource Resource => new AuditableResource("IdentityServer");
+ public FormattedString Description => evt.ToString().SafeForFormatted();
+ }
+}
diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ClientAuthenticationSuccessEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ClientAuthenticationSuccessEventAdapter.cs
new file mode 100644
index 0000000..dbf92f4
--- /dev/null
+++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ClientAuthenticationSuccessEventAdapter.cs
@@ -0,0 +1,21 @@
+using System;
+using Open.IdentityServer.Events;
+using RSK.Audit;
+
+namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters
+{
+ public class ClientAuthenticationSuccessEventAdapter : IAuditEventArguments
+ {
+ private readonly ClientAuthenticationSuccessEvent evt;
+
+ public ClientAuthenticationSuccessEventAdapter(ClientAuthenticationSuccessEvent evt)
+ {
+ this.evt = evt ?? throw new ArgumentNullException(nameof(evt));
+ }
+
+ public ResourceActor Actor => new ResourceActor(ResourceActor.MachineSubjectType, evt.ClientId, evt.ClientId);
+ public string Action => evt.Name;
+ public AuditableResource Resource => new AuditableResource("IdentityServer");
+ public FormattedString Description => evt.ToString().SafeForFormatted();
+ }
+}
diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ConsentDeniedEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ConsentDeniedEventAdapter.cs
new file mode 100644
index 0000000..54f9a1f
--- /dev/null
+++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ConsentDeniedEventAdapter.cs
@@ -0,0 +1,21 @@
+using System;
+using Open.IdentityServer.Events;
+using RSK.Audit;
+
+namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters
+{
+ public class ConsentDeniedEventAdapter : IAuditEventArguments
+ {
+ private readonly ConsentDeniedEvent evt;
+
+ public ConsentDeniedEventAdapter(ConsentDeniedEvent evt)
+ {
+ this.evt = evt ?? throw new ArgumentNullException(nameof(evt));
+ }
+
+ public ResourceActor Actor => new ResourceActor(ResourceActor.UserSubjectType, evt.SubjectId, evt.SubjectId);
+ public string Action => evt.Name;
+ public AuditableResource Resource => new AuditableResource("Client", evt.ClientId);
+ public FormattedString Description => evt.ToString().SafeForFormatted();
+ }
+}
diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ConsentGrantedEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ConsentGrantedEventAdapter.cs
new file mode 100644
index 0000000..6f495a2
--- /dev/null
+++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/ConsentGrantedEventAdapter.cs
@@ -0,0 +1,20 @@
+using Open.IdentityServer.Events;
+using RSK.Audit;
+
+namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters
+{
+ public class ConsentGrantedEventAdapter : IAuditEventArguments
+ {
+ private readonly ConsentGrantedEvent evt;
+
+ public ConsentGrantedEventAdapter(ConsentGrantedEvent evt)
+ {
+ this.evt = evt;
+ }
+
+ public ResourceActor Actor => new ResourceActor(ResourceActor.UserSubjectType, evt.SubjectId, evt.SubjectId);
+ public string Action => evt.Name;
+ public AuditableResource Resource => new AuditableResource("Client", evt.ClientId);
+ public FormattedString Description => evt.ToString().SafeForFormatted();
+ }
+}
diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/DeviceAuthorizationFailureEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/DeviceAuthorizationFailureEventAdapter.cs
new file mode 100644
index 0000000..465fe6b
--- /dev/null
+++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/DeviceAuthorizationFailureEventAdapter.cs
@@ -0,0 +1,21 @@
+using System;
+using Open.IdentityServer.Events;
+using RSK.Audit;
+
+namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters
+{
+ public class DeviceAuthorizationFailureEventAdapter : IAuditEventArguments
+ {
+ private readonly DeviceAuthorizationFailureEvent evt;
+
+ public DeviceAuthorizationFailureEventAdapter(DeviceAuthorizationFailureEvent evt)
+ {
+ this.evt = evt ?? throw new ArgumentNullException(nameof(evt));
+ }
+
+ public ResourceActor Actor => new ResourceActor(ResourceActor.MachineSubjectType, evt.ClientId, evt.ClientName);
+ public string Action => evt.Name;
+ public AuditableResource Resource => new AuditableResource("IdentityServer", evt.Endpoint);
+ public FormattedString Description => evt.ToString().SafeForFormatted();
+ }
+}
diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/DeviceAuthorizationSuccessEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/DeviceAuthorizationSuccessEventAdapter.cs
new file mode 100644
index 0000000..51b82b6
--- /dev/null
+++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/DeviceAuthorizationSuccessEventAdapter.cs
@@ -0,0 +1,21 @@
+using System;
+using Open.IdentityServer.Events;
+using RSK.Audit;
+
+namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters
+{
+ public class DeviceAuthorizationSuccessEventAdapter : IAuditEventArguments
+ {
+ private readonly DeviceAuthorizationSuccessEvent evt;
+
+ public DeviceAuthorizationSuccessEventAdapter(DeviceAuthorizationSuccessEvent evt)
+ {
+ this.evt = evt ?? throw new ArgumentNullException(nameof(evt));
+ }
+
+ public ResourceActor Actor => new ResourceActor(ResourceActor.MachineSubjectType, evt.ClientId, evt.ClientName);
+ public string Action => evt.Name;
+ public AuditableResource Resource => new AuditableResource("IdentityServer", evt.Endpoint);
+ public FormattedString Description => evt.ToString().SafeForFormatted();
+ }
+}
diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/GrantsRevokedEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/GrantsRevokedEventAdapter.cs
new file mode 100644
index 0000000..8c659d5
--- /dev/null
+++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/GrantsRevokedEventAdapter.cs
@@ -0,0 +1,20 @@
+using System;
+using Open.IdentityServer.Events;
+using RSK.Audit;
+
+namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters
+{
+ public class GrantsRevokedEventAdapter : IAuditEventArguments
+ {
+ private readonly GrantsRevokedEvent evt;
+
+ public GrantsRevokedEventAdapter(GrantsRevokedEvent evt)
+ {
+ this.evt = evt ?? throw new ArgumentNullException(nameof(evt));
+ }
+ public ResourceActor Actor => new ResourceActor(ResourceActor.UserSubjectType, evt.SubjectId, evt.SubjectId);
+ public string Action => evt.Name;
+ public AuditableResource Resource => new AuditableResource("Client", evt.ClientId);
+ public FormattedString Description => evt.ToString().SafeForFormatted();
+ }
+}
diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/InvalidClientConfigurationEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/InvalidClientConfigurationEventAdapter.cs
new file mode 100644
index 0000000..f6a4893
--- /dev/null
+++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/InvalidClientConfigurationEventAdapter.cs
@@ -0,0 +1,21 @@
+using System;
+using Open.IdentityServer.Events;
+using RSK.Audit;
+
+namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters
+{
+ public class InvalidClientConfigurationEventAdapter : IAuditEventArguments
+ {
+ private readonly InvalidClientConfigurationEvent evt;
+
+ public InvalidClientConfigurationEventAdapter(InvalidClientConfigurationEvent evt)
+ {
+ this.evt = evt ?? throw new ArgumentNullException(nameof(evt));
+ }
+
+ public ResourceActor Actor => new ResourceActor(ResourceActor.MachineSubjectType, null, null);
+ public string Action => evt.Name;
+ public AuditableResource Resource => new AuditableResource("IdentityServer", evt.ClientId);
+ public FormattedString Description => evt.ToString().SafeForFormatted();
+ }
+}
\ No newline at end of file
diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenIntrospectionFailureEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenIntrospectionFailureEventAdapter.cs
new file mode 100644
index 0000000..2466deb
--- /dev/null
+++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenIntrospectionFailureEventAdapter.cs
@@ -0,0 +1,21 @@
+using System;
+using Open.IdentityServer.Events;
+using RSK.Audit;
+
+namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters
+{
+ public class TokenIntrospectionFailureEventAdapter : IAuditEventArguments
+ {
+ private readonly TokenIntrospectionFailureEvent evt;
+
+ public TokenIntrospectionFailureEventAdapter(TokenIntrospectionFailureEvent evt)
+ {
+ this.evt = evt ?? throw new ArgumentNullException(nameof(evt));
+ }
+
+ public ResourceActor Actor => new ResourceActor(ResourceActor.MachineSubjectType, null, null);
+ public string Action => evt.Name;
+ public AuditableResource Resource => new AuditableResource("IdentityServer", evt.ApiName);
+ public FormattedString Description => evt.ToString().SafeForFormatted();
+ }
+}
diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenIntrospectionSuccessEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenIntrospectionSuccessEventAdapter.cs
new file mode 100644
index 0000000..8cfdfbf
--- /dev/null
+++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenIntrospectionSuccessEventAdapter.cs
@@ -0,0 +1,21 @@
+using System;
+using Open.IdentityServer.Events;
+using RSK.Audit;
+
+namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters
+{
+ public class TokenIntrospectionSuccessEventAdapter : IAuditEventArguments
+ {
+ private readonly TokenIntrospectionSuccessEvent evt;
+
+ public TokenIntrospectionSuccessEventAdapter(TokenIntrospectionSuccessEvent evt)
+ {
+ this.evt = evt ?? throw new ArgumentNullException(nameof(evt));
+ }
+
+ public ResourceActor Actor => new ResourceActor(ResourceActor.MachineSubjectType, null, null);
+ public string Action => evt.Name;
+ public AuditableResource Resource => new AuditableResource("IdentityServer", evt.ApiName);
+ public FormattedString Description => evt.ToString().SafeForFormatted();
+ }
+}
diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenIssuedFailureEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenIssuedFailureEventAdapter.cs
new file mode 100644
index 0000000..157f601
--- /dev/null
+++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenIssuedFailureEventAdapter.cs
@@ -0,0 +1,21 @@
+using System;
+using Open.IdentityServer.Events;
+using RSK.Audit;
+
+namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters
+{
+ public class TokenIssuedFailureEventAdapter : IAuditEventArguments
+ {
+ private readonly TokenIssuedFailureEvent evt;
+
+ public TokenIssuedFailureEventAdapter(TokenIssuedFailureEvent evt)
+ {
+ this.evt = evt ?? throw new ArgumentNullException(nameof(evt));
+ }
+
+ public ResourceActor Actor => new ResourceActor(ResourceActor.MachineSubjectType, evt.ClientId, evt.ClientName);
+ public string Action => evt.Name;
+ public AuditableResource Resource => new AuditableResource("IdentityServer", evt.Endpoint);
+ public FormattedString Description => evt.ToString().SafeForFormatted();
+ }
+}
diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenIssuedSuccessEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenIssuedSuccessEventAdapter.cs
new file mode 100644
index 0000000..eeee54d
--- /dev/null
+++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenIssuedSuccessEventAdapter.cs
@@ -0,0 +1,21 @@
+using System;
+using Open.IdentityServer.Events;
+using RSK.Audit;
+
+namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters
+{
+ public class TokenIssuedSuccessEventAdapter : IAuditEventArguments
+ {
+ private readonly TokenIssuedSuccessEvent evt;
+
+ public TokenIssuedSuccessEventAdapter(TokenIssuedSuccessEvent evt)
+ {
+ this.evt = evt ?? throw new ArgumentNullException(nameof(evt));
+ }
+
+ public ResourceActor Actor => new ResourceActor(ResourceActor.MachineSubjectType, evt.ClientId, evt.ClientName);
+ public string Action => evt.Name;
+ public AuditableResource Resource => new AuditableResource("IdentityServer", evt.Endpoint);
+ public FormattedString Description => evt.ToString().SafeForFormatted();
+ }
+}
diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenRevokedSuccessEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenRevokedSuccessEventAdapter.cs
new file mode 100644
index 0000000..43856f5
--- /dev/null
+++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/TokenRevokedSuccessEventAdapter.cs
@@ -0,0 +1,21 @@
+using System;
+using Open.IdentityServer.Events;
+using RSK.Audit;
+
+namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters
+{
+ public class TokenRevokedSuccessEventAdapter : IAuditEventArguments
+ {
+ private readonly TokenRevokedSuccessEvent evt;
+
+ public TokenRevokedSuccessEventAdapter(TokenRevokedSuccessEvent evt)
+ {
+ this.evt = evt ?? throw new ArgumentNullException(nameof(evt));
+ }
+
+ public ResourceActor Actor => new ResourceActor(ResourceActor.MachineSubjectType, evt.ClientId, evt.ClientName);
+ public string Action => evt.Name;
+ public AuditableResource Resource => new AuditableResource("IdentityServer");
+ public FormattedString Description => evt.ToString().SafeForFormatted();
+ }
+}
diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/UnhandledExceptionEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/UnhandledExceptionEventAdapter.cs
new file mode 100644
index 0000000..a165171
--- /dev/null
+++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/UnhandledExceptionEventAdapter.cs
@@ -0,0 +1,21 @@
+using System;
+using Open.IdentityServer.Events;
+using RSK.Audit;
+
+namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters
+{
+ public class UnhandledExceptionEventAdapter : IAuditEventArguments
+ {
+ private readonly UnhandledExceptionEvent evt;
+
+ public UnhandledExceptionEventAdapter(UnhandledExceptionEvent evt)
+ {
+ this.evt = evt ?? throw new ArgumentNullException(nameof(evt));
+ }
+
+ public ResourceActor Actor => new ResourceActor(ResourceActor.MachineSubjectType, null, null);
+ public string Action => evt.Name;
+ public AuditableResource Resource => new AuditableResource("IdentityServer");
+ public FormattedString Description => evt.ToString().SafeForFormatted();
+ }
+}
diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/UserLoginFailureEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/UserLoginFailureEventAdapter.cs
new file mode 100644
index 0000000..91b7ec2
--- /dev/null
+++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/UserLoginFailureEventAdapter.cs
@@ -0,0 +1,21 @@
+using System;
+using Open.IdentityServer.Events;
+using RSK.Audit;
+
+namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters
+{
+ public class UserLoginFailureEventAdapter : IAuditEventArguments
+ {
+ private readonly UserLoginFailureEvent evt;
+
+ public UserLoginFailureEventAdapter(UserLoginFailureEvent evt)
+ {
+ this.evt = evt ?? throw new ArgumentNullException(nameof(evt));
+ }
+
+ public ResourceActor Actor => new ResourceActor(ResourceActor.UserSubjectType, evt.Username, evt.Username);
+ public string Action => evt.Name;
+ public AuditableResource Resource => new AuditableResource("IdentityServer", evt.Endpoint);
+ public FormattedString Description => evt.ToString().SafeForFormatted();
+ }
+}
diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/UserLoginSuccessEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/UserLoginSuccessEventAdapter.cs
new file mode 100644
index 0000000..d1fc1e9
--- /dev/null
+++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/UserLoginSuccessEventAdapter.cs
@@ -0,0 +1,24 @@
+using System;
+using Open.IdentityServer.Events;
+using RSK.Audit;
+
+namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters
+{
+ public class UserLoginSuccessEventAdapter : IAuditEventArguments
+ {
+ private readonly UserLoginSuccessEvent evt;
+
+ public UserLoginSuccessEventAdapter(UserLoginSuccessEvent evt)
+ {
+ this.evt = evt ?? throw new ArgumentNullException(nameof(evt));
+ }
+
+ public ResourceActor Actor => new ResourceActor(ResourceActor.UserSubjectType, evt.SubjectId, evt.DisplayName);
+
+ public string Action => evt.Name;
+
+ public AuditableResource Resource => new AuditableResource("IdentityServer", evt.Endpoint);
+
+ public FormattedString Description => evt.ToString().SafeForFormatted();
+ }
+}
diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/UserLogoutSuccessEventAdapter.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/UserLogoutSuccessEventAdapter.cs
new file mode 100644
index 0000000..856ec01
--- /dev/null
+++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Adapters/UserLogoutSuccessEventAdapter.cs
@@ -0,0 +1,20 @@
+using Open.IdentityServer.Events;
+using RSK.Audit;
+
+namespace Rsk.Open.IdentityServer.AuditEventSink.Adapters
+{
+ public class UserLogoutSuccessEventAdapter : IAuditEventArguments
+ {
+ private readonly UserLogoutSuccessEvent evt;
+
+ public UserLogoutSuccessEventAdapter(UserLogoutSuccessEvent evt)
+ {
+ this.evt = evt;
+ }
+
+ public ResourceActor Actor => new ResourceActor(ResourceActor.UserSubjectType, evt.SubjectId, evt.DisplayName);
+ public string Action => evt.Name;
+ public AuditableResource Resource => new AuditableResource("IdentityServer", string.Empty);
+ public FormattedString Description => evt.ToString().SafeForFormatted();
+ }
+}
diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/AuditSink.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/AuditSink.cs
new file mode 100644
index 0000000..b737616
--- /dev/null
+++ b/src/Rsk.Open.IdentityServer.AuditEventSink/AuditSink.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using Open.IdentityServer.Events;
+using Open.IdentityServer.Services;
+using RSK.Audit;
+
+[assembly:InternalsVisibleTo("RSK.Open.IdentityServer.AuditEventSink.Tests")]
+
+namespace Rsk.Open.IdentityServer.AuditEventSink;
+
+public class AuditSink(
+ IRecordAuditableActions auditRecorder,
+ IDictionary> customEventAdapters = null)
+ : IEventSink
+{
+ private readonly IRecordAuditableActions auditRecorder = auditRecorder ?? throw new ArgumentNullException();
+
+ internal IAdapterFactory Factory { get; init; } = new AdapterFactory(customEventAdapters);
+
+ public Task PersistAsync(Event evt)
+ {
+ var auditArgument = Factory.Create(evt);
+
+ if (auditArgument != null)
+ {
+ if (evt.EventType == EventTypes.Success || evt.EventType == EventTypes.Information)
+ {
+ return auditRecorder.RecordSuccess(auditArgument);
+ }
+
+ return auditRecorder.RecordFailure(auditArgument);
+ }
+
+ return Task.CompletedTask;
+ }
+}
\ No newline at end of file
diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/EventSinkAgregator.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/EventSinkAgregator.cs
new file mode 100644
index 0000000..4125746
--- /dev/null
+++ b/src/Rsk.Open.IdentityServer.AuditEventSink/EventSinkAgregator.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Open.IdentityServer.Events;
+using Open.IdentityServer.Services;
+
+namespace Rsk.Open.IdentityServer.AuditEventSink
+{
+ public class EventSinkAggregator : IEventSink
+ {
+ private readonly ILogger logger;
+ public List EventSinks { get; set; } = new List();
+
+ public EventSinkAggregator(ILogger logger)
+ {
+ this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public Task PersistAsync(Event evt)
+ {
+ var eventSinkTasks = new List();
+
+ foreach (var eventSink in EventSinks)
+ {
+ eventSinkTasks.Add(ProtectedExecution(() => eventSink.PersistAsync(evt)));
+ }
+
+ return Task.WhenAll(eventSinkTasks);
+ }
+
+ private async Task ProtectedExecution(Func persistAsync)
+ {
+ try
+ {
+ await persistAsync();
+ }
+ catch (Exception e)
+ {
+ logger.Log(LogLevel.Error, e.Message);
+ }
+ }
+ }
+}
diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/IAdapterFactory.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/IAdapterFactory.cs
new file mode 100644
index 0000000..c0208a2
--- /dev/null
+++ b/src/Rsk.Open.IdentityServer.AuditEventSink/IAdapterFactory.cs
@@ -0,0 +1,10 @@
+using Open.IdentityServer.Events;
+using RSK.Audit;
+
+namespace Rsk.Open.IdentityServer.AuditEventSink
+{
+ public interface IAdapterFactory
+ {
+ IAuditEventArguments Create(Event evt);
+ }
+}
\ No newline at end of file
diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/Rsk.Open.IdentityServer.AuditEventSink.csproj b/src/Rsk.Open.IdentityServer.AuditEventSink/Rsk.Open.IdentityServer.AuditEventSink.csproj
new file mode 100644
index 0000000..14114c4
--- /dev/null
+++ b/src/Rsk.Open.IdentityServer.AuditEventSink/Rsk.Open.IdentityServer.AuditEventSink.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net10.0
+ Rock Solid Knowledge Ltd
+ Open.IdentityServer event sink to add audit records into AdminUI auditing
+ https://github.com/RockSolidKnowledge/Audit
+ Add event extensibility
+ Copyright 2026 (c) Rock Solid Knowledge Ltd. All rights reserved
+ Audit AdminUI Open.IdentityServer Events
+ true
+ icon.png
+ Apache-2.0
+ 1.0.0
+ Rsk.Open.IdentityServer.AuditEventSink
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Rsk.Open.IdentityServer.AuditEventSink/StringExtention.cs b/src/Rsk.Open.IdentityServer.AuditEventSink/StringExtention.cs
new file mode 100644
index 0000000..39c0a74
--- /dev/null
+++ b/src/Rsk.Open.IdentityServer.AuditEventSink/StringExtention.cs
@@ -0,0 +1,10 @@
+namespace Rsk.Open.IdentityServer.AuditEventSink
+{
+ public static class StringExtension
+ {
+ public static string SafeForFormatted(this string value)
+ {
+ return value.Replace("{", "{{").Replace("}", "}}");
+ }
+ }
+}
diff --git a/tests/Rsk.Open.IdentityServer.AuditEventSink.Tests/AdapterFactoryTests.cs b/tests/Rsk.Open.IdentityServer.AuditEventSink.Tests/AdapterFactoryTests.cs
new file mode 100644
index 0000000..9a0c4c9
--- /dev/null
+++ b/tests/Rsk.Open.IdentityServer.AuditEventSink.Tests/AdapterFactoryTests.cs
@@ -0,0 +1,324 @@
+using System.Collections.Generic;
+using System.Security.Claims;
+using Open.IdentityServer;
+using Open.IdentityServer.Events;
+using Open.IdentityServer.Models;
+using Open.IdentityServer.ResponseHandling;
+using Open.IdentityServer.Validation;
+using Rsk.Open.IdentityServer.AuditEventSink.Adapters;
+using Xunit;
+
+namespace Rsk.Open.IdentityServer.AuditEventSink.Tests
+{
+ public class AdapterFactoryTests
+ {
+ [Fact]
+ public void Create_WhenTokenIssuedSuccessEvent_WillReturnTokenIssuedSuccessEventAdapter()
+ {
+ // Arrange
+ var authResponse = new AuthorizeResponse()
+ {
+ Request = new ValidatedAuthorizeRequest()
+ {
+ Client = new Client(),
+ Subject = new ClaimsPrincipal(new ClaimsIdentity(new List()
+ {
+ new Claim(JwtClaimTypes.Subject, string.Empty)
+ }))
+ }
+ };
+
+ var evt = new TokenIssuedSuccessEvent(authResponse);
+
+ var sut = new AdapterFactory();
+
+ // Act
+ var adapter = sut.Create(evt);
+
+ // Assert
+ Assert.IsType(adapter);
+ }
+
+ [Fact]
+ public void Create_WhenConsentGrantedEvent_WillReturnConsentGrantedEventAdapter()
+ {
+ // Arrange
+ var evt = new ConsentGrantedEvent(string.Empty, string.Empty, new List(), new List(),
+ false);
+
+ var sut = new AdapterFactory();
+
+ // Act
+ var adapter = sut.Create(evt);
+
+ // Assert
+ Assert.IsType(adapter);
+ }
+
+ [Fact]
+ public void Create_WhenUserLoginFailureEvent_WillReturnUserLoginFailureAdapter()
+ {
+ // Arrange
+ var evt = new UserLoginFailureEvent(string.Empty, string.Empty);
+
+ var sut = new AdapterFactory();
+
+ // Act
+ var adapter = sut.Create(evt);
+
+ // Assert
+ Assert.IsType(adapter);
+ }
+
+ [Fact]
+ public void Create_WhenUserLoginSuccessEvent_WillReturnUserLoginSuccessAdapter()
+ {
+ // Arrange
+ var evt = new UserLoginSuccessEvent(string.Empty, string.Empty, string.Empty, string.Empty);
+
+ var sut = new AdapterFactory();
+
+ // Act
+ var adapter = sut.Create(evt);
+
+ // Assert
+ Assert.IsType(adapter);
+ }
+
+ [Fact]
+ public void Create_WhenUserLogoutSuccessEvent_WillReturnUserLogoutSuccessAdapter()
+ {
+ // Arrange
+ var evt = new UserLogoutSuccessEvent(string.Empty, string.Empty);
+
+ var sut = new AdapterFactory();
+
+ // Act
+ var adapter = sut.Create(evt);
+
+ // Assert
+ Assert.IsType(adapter);
+ }
+
+ [Fact]
+ public void Create_WhenConsentDeniedEvent_WillReturnConsentDeniedAdapter()
+ {
+ // Arrange
+ var evt = new ConsentDeniedEvent(string.Empty, string.Empty, new string[] { });
+
+ var sut = new AdapterFactory();
+
+ // Act
+ var adapter = sut.Create(evt);
+
+ // Assert
+ Assert.IsType(adapter);
+ }
+
+ [Fact]
+ public void Create_WhenTokenIssuedFailureEvent_WillReturnTokenIssuedFailureAdapter()
+ {
+ // Arrange
+ var request = new ValidatedAuthorizeRequest()
+ {
+ Client = new Client(),
+ Subject = new ClaimsPrincipal(new ClaimsIdentity(new List()
+ {
+ new Claim(JwtClaimTypes.Subject, string.Empty)
+ }))
+ };
+
+ var evt = new TokenIssuedFailureEvent(request, string.Empty, string.Empty);
+
+ var sut = new AdapterFactory();
+
+ // Act
+ var adapter = sut.Create(evt);
+
+ // Assert
+ Assert.IsType(adapter);
+ }
+
+ [Fact]
+ public void Create_WhenGrantsRevokedEvent_WillReturnGrantsRevokedAdapter()
+ {
+ // Arrange
+ var evt = new GrantsRevokedEvent(string.Empty, string.Empty);
+
+ var sut = new AdapterFactory();
+
+ // Act
+ var adapter = sut.Create(evt);
+
+ // Assert
+ Assert.IsType(adapter);
+ }
+
+ [Fact]
+ public void Create_WhenApiAuthenticationFailureEvent_WillReturnApiAuthenticationFailureEventAdapter()
+ {
+ // Arrange
+ var evt = new ApiAuthenticationFailureEvent(string.Empty, string.Empty);
+
+ var sut = new AdapterFactory();
+
+ // Act
+ var adapter = sut.Create(evt);
+
+ // Assert
+ Assert.IsType(adapter);
+ }
+
+ [Fact]
+ public void Create_WhenApiAuthenticationSuccessEvent_WillReturnApiAuthenticationSuccessEventAdapter()
+ {
+ // Arrange
+ var evt = new ApiAuthenticationSuccessEvent(string.Empty, string.Empty);
+
+ var sut = new AdapterFactory();
+
+ // Act
+ var adapter = sut.Create(evt);
+
+ // Assert
+ Assert.IsType(adapter);
+ }
+
+ [Fact]
+ public void Create_WhenClientAuthenticationFailureEvent_WillReturnClientAuthenticationFailureEventAdapter()
+ {
+ // Arrange
+ var evt = new ClientAuthenticationFailureEvent(string.Empty, string.Empty);
+
+ var sut = new AdapterFactory();
+
+ // Act
+ var adapter = sut.Create(evt);
+
+ // Assert
+ Assert.IsType(adapter);
+ }
+
+ [Fact]
+ public void Create_WhenClientAuthenticationSuccessEvent_WillReturnClientAuthenticationSuccessEventAdapter()
+ {
+ // Arrange
+ var evt = new ClientAuthenticationSuccessEvent(string.Empty, string.Empty);
+
+ var sut = new AdapterFactory();
+
+ // Act
+ var adapter = sut.Create(evt);
+
+ // Assert
+ Assert.IsType(adapter);
+ }
+
+ [Fact]
+ public void Create_WhenDeviceAuthorizationFailureEvent_WillReturnDeviceAuthorizationFailureEventAdapter()
+ {
+ // Arrange
+ var evt = new DeviceAuthorizationFailureEvent(new DeviceAuthorizationRequestValidationResult(null));
+
+ var sut = new AdapterFactory();
+
+ // Act
+ var adapter = sut.Create(evt);
+
+ // Assert
+ Assert.IsType(adapter);
+ }
+
+ [Fact]
+ public void Create_WhenDeviceAuthorizationSuccessEvent_WillReturnDeviceAuthorizationSuccessEventAdapter()
+ {
+ // Arrange
+ var evt = new DeviceAuthorizationSuccessEvent(new DeviceAuthorizationResponse(),
+ new DeviceAuthorizationRequestValidationResult(new ValidatedDeviceAuthorizationRequest()));
+
+ var sut = new AdapterFactory();
+
+ // Act
+ var adapter = sut.Create(evt);
+
+ // Assert
+ Assert.IsType(adapter);
+ }
+
+ [Fact]
+ public void Create_WhenInvalidClientConfigurationEvent_WillReturnInvalidClientConfigurationEventAdapter()
+ {
+ // Arrange
+ var evt = new InvalidClientConfigurationEvent(new Client(), string.Empty);
+
+ var sut = new AdapterFactory();
+
+ // Act
+ var adapter = sut.Create(evt);
+
+ // Assert
+ Assert.IsType(adapter);
+ }
+
+ [Fact]
+ public void Create_WhenTokenIntrospectionFailureEvent_WillReturnTokenIntrospectionFailureEventAdapter()
+ {
+ // Arrange
+ var evt = new TokenIntrospectionFailureEvent(string.Empty, string.Empty);
+
+ var sut = new AdapterFactory();
+
+ // Act
+ var adapter = sut.Create(evt);
+
+ // Assert
+ Assert.IsType(adapter);
+ }
+
+ [Fact]
+ public void Create_WhenTokenIntrospectionSuccessEvent_WillReturnTokenIntrospectionSuccessEventAdapter()
+ {
+ // Arrange
+ var evt = new TokenIntrospectionSuccessEvent(new IntrospectionRequestValidationResult
+ { Api = new ApiResource() });
+
+ var sut = new AdapterFactory();
+
+ // Act
+ var adapter = sut.Create(evt);
+
+ // Assert
+ Assert.IsType(adapter);
+ }
+
+ [Fact]
+ public void Create_WhenTokenRevokedSuccessEvent_WillReturnTokenRevokedSuccessEventAdapter()
+ {
+ // Arrange
+ var evt = new TokenRevokedSuccessEvent(new TokenRevocationRequestValidationResult(), new Client());
+
+ var sut = new AdapterFactory();
+
+ // Act
+ var adapter = sut.Create(evt);
+
+ // Assert
+ Assert.IsType(adapter);
+ }
+
+ [Fact]
+ public void Create_WhenUnhandledExceptionEvent_WillReturnUnhandledExceptionEventAdapter()
+ {
+ // Arrange
+ var evt = new UnhandledExceptionEvent(new System.Exception());
+
+ var sut = new AdapterFactory();
+
+ // Act
+ var adapter = sut.Create(evt);
+
+ // Assert
+ Assert.IsType(adapter);
+ }
+ }
+}
diff --git a/tests/Rsk.Open.IdentityServer.AuditEventSink.Tests/AuditSinkTests.cs b/tests/Rsk.Open.IdentityServer.AuditEventSink.Tests/AuditSinkTests.cs
new file mode 100644
index 0000000..045ba6e
--- /dev/null
+++ b/tests/Rsk.Open.IdentityServer.AuditEventSink.Tests/AuditSinkTests.cs
@@ -0,0 +1,128 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Moq;
+using Open.IdentityServer.Events;
+using RSK.Audit;
+using Xunit;
+
+namespace Rsk.Open.IdentityServer.AuditEventSink.Tests
+{
+ public class AuditSinkTests
+ {
+ [Fact]
+ public async Task PersistAsync_WhenSuccessEvent_WillCallSuccessAuditRecord()
+ {
+ // Arrange
+ var recorder = new Mock();
+ var factory = new Mock();
+ factory.Setup(x => x.Create(It.IsAny())).Returns(new Mock().Object);
+
+ var sut = new AuditSink(recorder.Object) {Factory = factory.Object};
+
+
+ var successfulEvent = new StubEvent(string.Empty, string.Empty, EventTypes.Success, -1);
+
+ // Act
+ await sut.PersistAsync(successfulEvent);
+
+ // Assert
+ recorder.Verify(x => x.RecordSuccess(It.IsAny()), Times.Once);
+ }
+
+ [Fact]
+ public async Task PersistAsync_WhenInformationEvent_WillCallSuccessAuditRecord()
+ {
+ // Arrange
+ var recorder = new Mock();
+ var factory = new Mock();
+ factory.Setup(x => x.Create(It.IsAny())).Returns(new Mock().Object);
+
+ var sut = new AuditSink(recorder.Object) { Factory = factory.Object };
+
+
+ var successfulEvent = new StubEvent(string.Empty, string.Empty, EventTypes.Information, -1);
+
+ // Act
+ await sut.PersistAsync(successfulEvent);
+
+ // Assert
+ recorder.Verify(x => x.RecordSuccess(It.IsAny()), Times.Once);
+ }
+
+ [Fact]
+ public async Task PersistAsync_WhenErrorEvent_WillCallFailureAuditRecord()
+ {
+ // Arrange
+ var recorder = new Mock();
+ var factory = new Mock();
+ factory.Setup(x => x.Create(It.IsAny())).Returns(new Mock().Object);
+
+ var sut = new AuditSink(recorder.Object) { Factory = factory.Object };
+
+
+ var successfulEvent = new StubEvent(string.Empty, string.Empty, EventTypes.Error, -1);
+
+ // Act
+ await sut.PersistAsync(successfulEvent);
+
+ // Assert
+ recorder.Verify(x => x.RecordFailure(It.IsAny()), Times.Once);
+ }
+
+ [Fact]
+ public async Task PersistAsync_WhenFailureEvent_WillCallFailureAuditRecord()
+ {
+ // Arrange
+ var recorder = new Mock();
+ var factory = new Mock();
+ factory.Setup(x => x.Create(It.IsAny())).Returns(new Mock().Object);
+
+ var sut = new AuditSink(recorder.Object) { Factory = factory.Object };
+
+
+ var successfulEvent = new StubEvent(string.Empty, string.Empty, EventTypes.Failure, -1);
+
+ // Act
+ await sut.PersistAsync(successfulEvent);
+
+ // Assert
+ recorder.Verify(x => x.RecordFailure(It.IsAny()), Times.Once);
+ }
+
+ [Fact]
+ public async Task PersistAsync_WhenCustomMappingIsProvided_WillUseCustomAdapter()
+ {
+ // Arrange
+ var recorder = new Mock();
+ var customAuditEventArguments = new Mock().Object;
+ var customMappings = new Dictionary>
+ {
+ [typeof(CustomStubEvent)] = _ => customAuditEventArguments
+ };
+
+ var sut = new AuditSink(recorder.Object, customMappings);
+ var evt = new CustomStubEvent(string.Empty, string.Empty, EventTypes.Success, -1);
+
+ // Act
+ await sut.PersistAsync(evt);
+
+ // Assert
+ recorder.Verify(x => x.RecordSuccess(customAuditEventArguments), Times.Once);
+ }
+
+ private class StubEvent : Event
+ {
+ public StubEvent(string category, string name, EventTypes type, int id, string message = null) : base(category, name, type, id, message)
+ {
+ }
+ }
+
+ private class CustomStubEvent : Event
+ {
+ public CustomStubEvent(string category, string name, EventTypes type, int id, string message = null) : base(category, name, type, id, message)
+ {
+ }
+ }
+ }
+}
diff --git a/tests/Rsk.Open.IdentityServer.AuditEventSink.Tests/EventSinkAggregatorTests.cs b/tests/Rsk.Open.IdentityServer.AuditEventSink.Tests/EventSinkAggregatorTests.cs
new file mode 100644
index 0000000..743ef8b
--- /dev/null
+++ b/tests/Rsk.Open.IdentityServer.AuditEventSink.Tests/EventSinkAggregatorTests.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Open.IdentityServer.Events;
+using Open.IdentityServer.Services;
+using Xunit;
+
+namespace Rsk.Open.IdentityServer.AuditEventSink.Tests
+{
+ public class EventSinkAggregatorTests
+ {
+ [Fact]
+ public async Task PersistAsync_WhenCalledWithMultiEventSinks_WillRaiseWithAll()
+ {
+ // Arrange
+ var sink1 = new StubSink();
+ var sink2 = new StubSink();
+
+ var sut = new EventSinkAggregator(new Mock().Object);
+
+ sut.EventSinks.Add(sink1);
+ sut.EventSinks.Add(sink2);
+
+ // Act
+ await sut.PersistAsync(new StubEvent());
+
+ // Assert
+ Assert.Equal(1, sink1.WasCalled);
+ Assert.Equal(1, sink2.WasCalled);
+ }
+
+ [Fact]
+ public async Task PersistAsync_WhenCalledWithMultiEventSinksAndOneThrowsAnException_WillRaiseToAllEventSinks()
+ {
+ // Arrange
+ var sink1 = new StubSink();
+ var sink2 = new StubSink();
+ var sink3 = new StubSinkThrowsException();
+
+ var logger = new StubLogger();
+
+ var sut = new EventSinkAggregator(logger);
+
+ sut.EventSinks.Add(sink1);
+ sut.EventSinks.Add(sink2);
+ sut.EventSinks.Add(sink3);
+
+ // Act
+ await sut.PersistAsync(new StubEvent());
+
+ // Assert
+ Assert.Equal(1, sink1.WasCalled);
+ Assert.Equal(1, sink2.WasCalled);
+ Assert.Equal(1, sink3.WasCalled);
+ Assert.Equal(1, logger.TimesErrored);
+ }
+
+ private class StubSink : IEventSink
+ {
+ public int WasCalled { get; private set; }
+
+ public Task PersistAsync(Event evt)
+ {
+ WasCalled++;
+ return Task.CompletedTask;
+ }
+ }
+
+ private class StubSinkThrowsException : IEventSink
+ {
+ public int WasCalled { get; private set; }
+
+ public Task PersistAsync(Event evt)
+ {
+ WasCalled++;
+ throw new Exception("Blah");
+ }
+ }
+
+ private class StubEvent : Event
+ {
+ public StubEvent() : base(string.Empty, string.Empty, EventTypes.Failure, 0)
+ {
+ }
+ }
+
+ private class StubLogger : ILogger
+ {
+ public int TimesErrored = 0;
+
+ public IDisposable BeginScope(TState state)
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool IsEnabled(LogLevel logLevel) => true;
+
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ {
+ if (logLevel == LogLevel.Error) TimesErrored++;
+ }
+ }
+ }
+}
diff --git a/tests/Rsk.Open.IdentityServer.AuditEventSink.Tests/Rsk.Open.IdentityServer.AuditEventSink.Tests.csproj b/tests/Rsk.Open.IdentityServer.AuditEventSink.Tests/Rsk.Open.IdentityServer.AuditEventSink.Tests.csproj
new file mode 100644
index 0000000..d6959a3
--- /dev/null
+++ b/tests/Rsk.Open.IdentityServer.AuditEventSink.Tests/Rsk.Open.IdentityServer.AuditEventSink.Tests.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net10.0
+ false
+ Rsk.Open.IdentityServer.AuditEventSink.Tests
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+