Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System.Net.Mime;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
Expand All @@ -17,6 +18,7 @@ public static void AddAuthorizationHeaderRequestEndpoints(this WebApplication ap
{
app.MapPost("/AuthorizationHeader/{apiName}", AuthorizationHeaderAsync).
WithName("Authorization header").
AllowAnonymous().
Accepts<DownstreamApiOptions>(true, MediaTypeNames.Application.Json).
ProducesProblem(StatusCodes.Status400BadRequest).
ProducesProblem(StatusCodes.Status401Unauthorized);
Expand Down Expand Up @@ -47,7 +49,7 @@ private static async Task<Results<Ok<AuthorizationHeaderResult>, ProblemHttpResu
if (options.Scopes is null)
{
return TypedResults.Problem(
detail: $"No scopes found for the API '{apiName}'. 'scopes' needs to be either a single value or a list of values.",
detail: $"No scopes found for the API '{apiName}' or in optionsOverride. 'scopes' needs to be either a single value or a list of values.",
statusCode: StatusCodes.Status400BadRequest);
}

Expand All @@ -71,6 +73,13 @@ private static async Task<Results<Ok<AuthorizationHeaderResult>, ProblemHttpResu
httpContext.User,
httpContext.RequestAborted);
}
catch (MicrosoftIdentityWebChallengeUserException ex)
{
logger.AuthorizationHeaderAsyncError(ex);
return TypedResults.Problem(
detail: ex.InnerException?.Message ?? ex.Message,
statusCode: StatusCodes.Status401Unauthorized);
}
catch (Exception ex)
{
logger.AuthorizationHeaderAsyncError(ex);
Expand Down
1 change: 1 addition & 0 deletions src/Microsoft.Identity.Web.Sidecar/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Diagnostics;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web.Sidecar.Endpoints;
Expand Down
20 changes: 4 additions & 16 deletions tests/E2E Tests/Sidecar.Tests/AuthorizationHeaderEndpointTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Identity.Abstractions;
Expand All @@ -14,19 +15,6 @@ public class AuthorizationHeaderEndpointTests(SidecarApiFactory factory) : IClas
{
private readonly SidecarApiFactory _factory = factory;

[Fact]
public async Task AuthorizationHeader_WithoutAuthentication_ReturnsUnauthorized()
{
// Arrange
var client = _factory.CreateClient();

// Act
var response = await client.PostAsync("/AuthorizationHeader/test-api", null);

// Assert
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}

[Fact]
public async Task AuthorizationHeader_WithInvalidToken_ReturnsUnauthorized()
{
Expand All @@ -35,14 +23,14 @@ public async Task AuthorizationHeader_WithInvalidToken_ReturnsUnauthorized()
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "invalid-token");

// Act
var response = await client.PostAsync("/AuthorizationHeader/test-api", null);
var response = await client.PostAsync("/AuthorizationHeader/test-api", JsonContent.Create(new { Scopes = new string[] { "scopes" }}));

// Assert
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}

[Fact]
public async Task AuthorizationHeader_WithNonExistentApi_ReturnsBadRequest()
public async Task AuthorizationHeader_WithNonExistentApi_AndNoScope_OverrideReturnsBadRequest()
{
// Arrange
var mockHeaderProvider = new TestAuthorizationHeaderProvider();
Expand All @@ -65,7 +53,7 @@ public async Task AuthorizationHeader_WithNonExistentApi_ReturnsBadRequest()
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("Not able to resolve 'non-existent-api'", content, StringComparison.OrdinalIgnoreCase);
Assert.Contains("No scopes found", content, StringComparison.OrdinalIgnoreCase);
}

[Fact]
Expand Down
10 changes: 7 additions & 3 deletions tests/E2E Tests/Sidecar.Tests/MockedEndToEndTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.Sidecar.Models;
using Xunit;

Expand All @@ -32,11 +33,14 @@ public async Task MockedAuthorizationFlow_WithValidConfiguration_ReturnsAuthoriz
var client = _factory
.WithWebHostBuilder(builder =>
{
builder.UseSetting($"DownstreamApi:{apiName}:BaseUrl", "https://graph.microsoft.com");
builder.UseSetting($"DownstreamApi:{apiName}:Scopes", scope);
builder.ConfigureServices(services =>
{
services.AddSingleton<IAuthorizationHeaderProvider>(mock);
services.Configure<DownstreamApiOptions>(apiName, options =>
{
options.BaseUrl = "https://graph.microsoft.com";
options.Scopes = new[] { scope };
});
});
})
.CreateClient();
Expand All @@ -58,7 +62,7 @@ public async Task MockedAuthorizationFlow_WithValidConfiguration_ReturnsAuthoriz
}

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = JsonSerializer.Deserialize<AuthorizationHeaderResult>(content);
var result = JsonSerializer.Deserialize<AuthorizationHeaderResult>(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true});
Assert.NotNull(result);
Assert.Equal(expectedAuthHeader, result.AuthorizationHeader);
}
Expand Down
11 changes: 9 additions & 2 deletions tests/E2E Tests/Sidecar.Tests/SidecarIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
// Licensed under the MIT License.

using System.Net;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Web;
using Xunit;

Expand Down Expand Up @@ -33,16 +36,20 @@ public async Task OpenApiEndpoint_IsAvailable_InDevelopment()
}

[Fact]
public async Task AuthorizationEndpoint_ExistsAndRequiresAuth()
public async Task AuthorizationEndpoint_ExistsButDoesNotRequiresAuth()
{
// Arrange
var client = _factory.CreateClient();

// Act
var response = await client.PostAsync("/AuthorizationHeader/test", null);
// The API does not exist (which is fine) but Scopes are not provided in the
// override
var response = await client.PostAsync("/AuthorizationHeader/test", JsonContent.Create(new DownstreamApiOptions{ Scopes = ["scopes"] }));

// Assert
string content = await response.Content.ReadAsStringAsync();
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
Assert.Contains("No account", content, StringComparison.OrdinalIgnoreCase);
}

[Fact]
Expand Down