Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f693fbe
initial commit
Sep 15, 2025
97ef1d6
Refactor, add basic error handling
Sep 15, 2025
0e0d405
Adding E2E test (#3476)
jmprieur Sep 15, 2025
4598585
Add authorization header endpoint, minor cleanup
Sep 15, 2025
cd71afb
more end to end tests for sidecar (#3477)
jmprieur Sep 16, 2025
d2497ad
Add more tests
Sep 16, 2025
187a563
Improvements (#3478)
jmprieur Sep 16, 2025
c3dd0fc
Fix model bindng (#3479)
keegan-caruso Sep 16, 2025
933010a
Update tests to new behavior + 401 on AuthorizationHeader when we are…
jmprieur Sep 16, 2025
f96f4e7
add downstream api (#3483)
keegan-caruso Sep 22, 2025
e0ef196
Enable container workflow (#3496)
keegan-caruso Sep 26, 2025
706e717
Add more e2e tests (#3504)
keegan-caruso Sep 29, 2025
08279fa
Authorization Header and downstream API endpoint updates (#3507)
keegan-caruso Sep 30, 2025
7ed6be6
Sidecar endpoint descriptions (#3510)
keegan-caruso Oct 1, 2025
4ec1187
Current implementation of Open API description generation is not trim…
Oct 1, 2025
fb6897a
Add agentuserid to list of params (#3514)
keegan-caruso Oct 2, 2025
e56faa3
Add windows container (#3516)
keegan-caruso Oct 2, 2025
42a69dc
Sidecar python adapter devapp (#3508)
keegan-caruso Oct 3, 2025
ece3055
Add readme (#3517)
keegan-caruso Oct 3, 2025
c4eadfe
Make the sidecar trim friendlier (#3518)
keegan-caruso Oct 3, 2025
1423c38
Update python adapter (#3519)
keegan-caruso Oct 3, 2025
a2272f0
Don't use R2R (#3523)
keegan-caruso Oct 3, 2025
37115b9
Exclude EndpointsE2ETests when ran from GH action
Oct 4, 2025
ebac5a2
include FROM_GITHUB_ACTION in sidecar tests
Oct 4, 2025
9313bc4
Use in-memory config for sidecar e2e test
Oct 6, 2025
e0d86b1
Apply suggestions from code review
keegan-caruso Oct 6, 2025
a31b2f7
Move SidecarApiFactory to separate file
Oct 6, 2025
c6546c3
Also needs test filter
Oct 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added .dockerignore
Empty file.
2 changes: 1 addition & 1 deletion .github/workflows/dotnetcore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
run: dotnet test --no-restore --no-build Microsoft.Identity.Web.sln -f net8.0 -v m -p:FROM_GITHUB_ACTION=true --configuration Release --collect "Xplat Code Coverage" --filter "(FullyQualifiedName!~Microsoft.Identity.Web.Test.Integration)&(FullyQualifiedName!~WebAppUiTests)&(FullyQualifiedName!~IntegrationTests)&(FullyQualifiedName!~TokenAcquirerTests)&(FullyQualifiedName!~AgentApplicationsTests)"

- name: Test with .NET 9.0.x
run: dotnet test --no-restore --no-build Microsoft.Identity.Web.sln -f net9.0 -v m -p:FROM_GITHUB_ACTION=true --configuration Release --collect "Xplat Code Coverage" --filter "(FullyQualifiedName!~Microsoft.Identity.Web.Test.Integration)&(FullyQualifiedName!~WebAppUiTests)&(FullyQualifiedName!~IntegrationTests)&(FullyQualifiedName!~TokenAcquirerTests)&(FullyQualifiedName!~AgentApplicationsTests)"
run: dotnet test --no-restore --no-build Microsoft.Identity.Web.sln -f net9.0 -v m -p:FROM_GITHUB_ACTION=true --configuration Release --collect "Xplat Code Coverage" --filter "(FullyQualifiedName!~Microsoft.Identity.Web.Test.Integration)&(FullyQualifiedName!~WebAppUiTests)&(FullyQualifiedName!~IntegrationTests)&(FullyQualifiedName!~TokenAcquirerTests)&(FullyQualifiedName!~AgentApplicationsTests)&(FullyQualifiedName!~SidecarEndpointsE2ETests)"



Expand Down
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -357,3 +357,12 @@ MigrationBackup/
/.SharedData
/out/
objd/
/src/Microsoft.Identity.Web.Sidecar/http-client.env.json

# Generated data protection keys

key*.xml

# bin files

*.bin
14 changes: 14 additions & 0 deletions Microsoft.Identity.Web.sln
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Identity.Web.Agen
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "daemon-app-msi", "tests\DevApps\daemon-app\daemon-app-msi\daemon-app-msi.csproj", "{A8181404-23E0-D38B-454C-D16ECDB18B9F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Identity.Web.Sidecar", "src\Microsoft.Identity.Web.Sidecar\Microsoft.Identity.Web.Sidecar.csproj", "{55C81F88-0FFA-491C-A1D7-0ACA7212B59C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sidecar.Tests", "tests\E2E Tests\Sidecar.Tests\Sidecar.Tests.csproj", "{946E6BED-2A06-4FF4-3E39-22ACEB44A984}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -405,6 +409,14 @@ Global
{A8181404-23E0-D38B-454C-D16ECDB18B9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A8181404-23E0-D38B-454C-D16ECDB18B9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A8181404-23E0-D38B-454C-D16ECDB18B9F}.Release|Any CPU.Build.0 = Release|Any CPU
{55C81F88-0FFA-491C-A1D7-0ACA7212B59C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{55C81F88-0FFA-491C-A1D7-0ACA7212B59C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{55C81F88-0FFA-491C-A1D7-0ACA7212B59C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{55C81F88-0FFA-491C-A1D7-0ACA7212B59C}.Release|Any CPU.Build.0 = Release|Any CPU
{946E6BED-2A06-4FF4-3E39-22ACEB44A984}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{946E6BED-2A06-4FF4-3E39-22ACEB44A984}.Debug|Any CPU.Build.0 = Debug|Any CPU
{946E6BED-2A06-4FF4-3E39-22ACEB44A984}.Release|Any CPU.ActiveCfg = Release|Any CPU
{946E6BED-2A06-4FF4-3E39-22ACEB44A984}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -482,6 +494,8 @@ Global
{DD56CDF7-E6B3-4304-B8DF-3AC610C35623} = {45B20A78-91F8-4DD2-B9AD-F12D3A93536C}
{C14780ED-5756-2A09-C6A7-5DDA433D1E86} = {1DDE1AAC-5AE6-4725-94B6-A26C58D3423F}
{A8181404-23E0-D38B-454C-D16ECDB18B9F} = {E37CDBC1-18F6-4C06-A3EE-532C9106721F}
{55C81F88-0FFA-491C-A1D7-0ACA7212B59C} = {1DDE1AAC-5AE6-4725-94B6-A26C58D3423F}
{946E6BED-2A06-4FF4-3E39-22ACEB44A984} = {45B20A78-91F8-4DD2-B9AD-F12D3A93536C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {104367F1-CE75-4F40-B32F-F14853973187}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<Title>Microsoft Identity Web Agentic Identity support</Title>
<Product>Microsoft Identity Web for Agent Identities</Product>
<Description>Helper methods for Agent applications to act as the agent identities.</Description>
<Description>Helper methods for Agent identity blueprint to act as the agent identities.</Description>
<PackageReadmeFile>README.md</PackageReadmeFile>

<!-- The package is new in 3.10.0.-->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.OidcFic;

namespace Microsoft.Extensions.DependencyInjection
Expand All @@ -20,6 +21,7 @@ public static class OidcFicSignedAssertionProviderExtensions
/// <returns>the service collection for chaining.</returns>
public static IServiceCollection AddOidcFic(this IServiceCollection services)
{
services.AddTokenAcquisition(true);
services.TryAddEnumerable(ServiceDescriptor.Singleton<ICustomSignedAssertionProvider, OidcIdpSignedAssertionLoader>());
return services;
}
Expand Down
51 changes: 51 additions & 0 deletions src/Microsoft.Identity.Web.Sidecar/AgentOverrides.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Diagnostics.CodeAnalysis;
using Microsoft.Identity.Abstractions;

namespace Microsoft.Identity.Web.Sidecar;

public class AgentOverrides
{
/// <summary>
/// Applies agent identity overrides to <see cref="DownstreamApiOptions"/>.
/// Precedence:
/// 1. If an agent identity and a username (UPN) are provided, use agent user identity (username wins over userId).
/// 2. Else if an agent identity and a userId (OID) are provided, use agent user identity with the OID.
/// 3. Else if only an agent identity is provided, use agent identity.
/// To override the tenant, set options.AcquireTokenOptions.Tenant separately.
/// </summary>
/// <param name="options">Downstream API options to mutate.</param>
/// <param name="agentIdentity">Agent identity (client/application ID) to act as.</param>
/// <param name="agentUsername">Agent user identity UPN.</param>
/// <param name="agentUserId">Agent user identity object id (GUID string).</param>
public static void SetOverrides(
DownstreamApiOptions options,
string? agentIdentity,
string? agentUsername,
[StringSyntax(StringSyntaxAttribute.GuidFormat)]
string? agentUserId)
{
if (options is null || string.IsNullOrWhiteSpace(agentIdentity))
{
return;
}

// Username (UPN) takes precedence if both UPN and OID are supplied.
if (!string.IsNullOrWhiteSpace(agentUsername))
{
options.WithAgentUserIdentity(agentIdentity, agentUsername);
}
else if (!string.IsNullOrWhiteSpace(agentUserId) &&
Guid.TryParse(agentUserId, out Guid userGuid))
{
options.WithAgentUserIdentity(agentIdentity, userGuid);
}
else
{
// Fallback to plain agent identity.
options.WithAgentIdentity(agentIdentity);
}
}
}
15 changes: 15 additions & 0 deletions src/Microsoft.Identity.Web.Sidecar/AppJsonSerializerContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Text.Json.Serialization;
using Microsoft.Identity.Web.Sidecar.Models;

namespace Microsoft.Identity.Web.Sidecar;

[JsonSerializable(typeof(AuthorizationHeaderResult))]
[JsonSerializable(typeof(DownstreamApiResult))]
[JsonSerializable(typeof(ValidateAuthorizationHeaderResult))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{

}
37 changes: 37 additions & 0 deletions src/Microsoft.Identity.Web.Sidecar/CacheControl.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Net.Http.Headers;

namespace Microsoft.Identity.Web.Sidecar;

public static class CacheControl
{
private readonly static string s_cacheControlHeader = $"{CacheControlHeaderValue.NoCacheString}, {CacheControlHeaderValue.NoStoreString}, {CacheControlHeaderValue.MustRevalidateString}";


public static void SetNoCachingMiddleware(this WebApplication app)
{
app.Use(async (context, next) =>
{
context.Response.OnStarting(() =>
{
if (context.Response.StatusCode is >= 200 and < 300)
{
CacheControl.SetNoCaching(context.Response);
}
return Task.CompletedTask;
});

await next();
});
}

private static void SetNoCaching(HttpResponse response)
{
// using Microsoft.Net.Http.Headers
response.Headers[HeaderNames.CacheControl] = s_cacheControlHeader;
response.Headers[HeaderNames.Expires] = "0";
response.Headers[HeaderNames.Pragma] = CacheControlHeaderValue.NoCacheString;
}
}
14 changes: 14 additions & 0 deletions src/Microsoft.Identity.Web.Sidecar/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
<PropertyGroup>
<TargetFrameworks>net9.0</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<AspDependencyVersion>9.0.9</AspDependencyVersion>
<MicrosoftAspNetCoreAuthenticationJwtBearerVersion>$(AspDependencyVersion)</MicrosoftAspNetCoreAuthenticationJwtBearerVersion>
<MicrosoftAspNetCoreAuthenticationOpenIdConnectVersion>$(AspDependencyVersion)</MicrosoftAspNetCoreAuthenticationOpenIdConnectVersion>
<MicrosoftAspNetCoreOpenApiVersion>$(AspDependencyVersion)</MicrosoftAspNetCoreOpenApiVersion>
<MicrosoftExtensionsApiDescriptionServerVersion>$(AspDependencyVersion)</MicrosoftExtensionsApiDescriptionServerVersion>
<MicrosoftVisualStudioAzureContainersToolsTargetsVersion>1.22.1</MicrosoftVisualStudioAzureContainersToolsTargetsVersion>
</PropertyGroup>
</Project>
9 changes: 9 additions & 0 deletions src/Microsoft.Identity.Web.Sidecar/DockerFile.NanoServer
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# This docker file is used for building the container image from a published set of files.
# This is used when for workflows where the files need to be signed in CI/CD.

FROM mcr.microsoft.com/dotnet/aspnet:9.0-nanoserver-ltsc2022 AS base
WORKDIR /app

COPY app/publish .
USER app
ENTRYPOINT ["dotnet", "Microsoft.Identity.Web.Sidecar.dll"]
29 changes: 29 additions & 0 deletions src/Microsoft.Identity.Web.Sidecar/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# This file is used for local builds and can be used with the Visual Studio tools to publish a container.

# See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.

# This stage is used when running from VS in fast mode (Default for Debug configuration)
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
USER $APP_UID
WORKDIR /app

# This stage is used to build the service project
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["Microsoft.Identity.Web.Sidecar.csproj", "."]
RUN dotnet restore "./Microsoft.Identity.Web.Sidecar.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet build "./Microsoft.Identity.Web.Sidecar.csproj" -c $BUILD_CONFIGURATION -o /app/build

# This stage is used to publish the service project to be copied to the final stage
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./Microsoft.Identity.Web.Sidecar.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration)
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Microsoft.Identity.Web.Sidecar.dll"]
11 changes: 11 additions & 0 deletions src/Microsoft.Identity.Web.Sidecar/Dockerfile.AzureLinux
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# This docker file is used for building the container image from a published set of files.
# This is used when for workflows where the files need to be signed in CI/CD.

FROM mcr.microsoft.com/dotnet/aspnet:9.0-azurelinux3.0-distroless AS base
WORKDIR /app

COPY app/publish .

USER app

ENTRYPOINT ["dotnet", "Microsoft.Identity.Web.Sidecar.dll"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Identity.Abstractions;

namespace Microsoft.Identity.Web.Sidecar;

public static class DownstreamApiOptionsMerger
{
public static DownstreamApiOptions MergeOptions(DownstreamApiOptions left, DownstreamApiOptions right)
{
var res = left.Clone();

if (right is null)
{
return res;
}

if (right.Scopes is not null && right.Scopes.Any())
{
res.Scopes = right.Scopes;
}

if (!string.IsNullOrEmpty(right.AcquireTokenOptions.Tenant))
{
res.AcquireTokenOptions.Tenant = right.AcquireTokenOptions.Tenant;
}

if (!string.IsNullOrEmpty(right.AcquireTokenOptions.Claims))
{
res.AcquireTokenOptions.Claims = right.AcquireTokenOptions.Claims;
}

if (!string.IsNullOrEmpty(right.AcquireTokenOptions.AuthenticationOptionsName))
{
res.AcquireTokenOptions.AuthenticationOptionsName = right.AcquireTokenOptions.AuthenticationOptionsName;
}

if (!string.IsNullOrEmpty(right.AcquireTokenOptions.FmiPath))
{
res.AcquireTokenOptions.FmiPath = right.AcquireTokenOptions.FmiPath;
}

if (!string.IsNullOrEmpty(right.RelativePath))
{
res.RelativePath = right.RelativePath;
}

res.AcquireTokenOptions.ForceRefresh = right.AcquireTokenOptions.ForceRefresh;

if (right.AcquireTokenOptions.ExtraParameters is not null)
{
if (res.AcquireTokenOptions.ExtraParameters is null)
{
res.AcquireTokenOptions.ExtraParameters = new Dictionary<string, object>();
}
foreach (var extraParameter in right.AcquireTokenOptions.ExtraParameters)
{
if (!res.AcquireTokenOptions.ExtraParameters.ContainsKey(extraParameter.Key))
{
res.AcquireTokenOptions.ExtraParameters.Add(extraParameter.Key, extraParameter.Value);
}
}
}

return res;
}
}
Loading
Loading