Skip to content

Commit 2a53819

Browse files
author
Keegan Caruso
committed
Add authorization header endpoint, minor cleanup
1 parent 9ed0eb9 commit 2a53819

File tree

11 files changed

+233
-69
lines changed

11 files changed

+233
-69
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,3 +357,4 @@ MigrationBackup/
357357
/.SharedData
358358
/out/
359359
objd/
360+
/src/Microsoft.Identity.Web.Sidecar/http-client.env.json
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.AspNetCore.Http.HttpResults;
5+
using Microsoft.AspNetCore.Mvc;
6+
using Microsoft.Identity.Abstractions;
7+
using Microsoft.Identity.Web.Sidecar.Models;
8+
9+
namespace Microsoft.Identity.Web.Sidecar.Endpoints;
10+
11+
internal static class DownstreamApiRequestEndpoints
12+
{
13+
public static void AddDownstreamApiRequestEndpoints(this WebApplication app)
14+
{
15+
app.MapPost("/AuthorizationHeader/{apiName}", AuthorizationHeaderAsync).
16+
WithName("Authorization header").
17+
RequireAuthorization().
18+
WithOpenApi().
19+
ProducesProblem(401);
20+
}
21+
22+
private static async Task<Results<Ok<AuthorizationHeaderResult>, ProblemHttpResult>> AuthorizationHeaderAsync(
23+
HttpContext httpContext,
24+
[FromRoute] string apiName,
25+
[FromQuery] string? agentIdentity,
26+
[FromQuery] string? agentUsername,
27+
[FromQuery] string? tenant,
28+
[FromBody] DownstreamApiOptions? optionsOverride,
29+
[FromServices] IAuthorizationHeaderProvider headerProvider,
30+
[FromServices] IConfiguration configuration)
31+
{
32+
DownstreamApiOptions? options = configuration.GetSection($"DownstreamApi:{apiName}").Get<DownstreamApiOptions>();
33+
34+
if (options is null)
35+
{
36+
return TypedResults.Problem(
37+
detail: $"Not able to resolve '{apiName}'.",
38+
statusCode: StatusCodes.Status400BadRequest);
39+
}
40+
41+
if (optionsOverride is not null)
42+
{
43+
MergeDownstreamApiOptionsOverrides(options, optionsOverride);
44+
}
45+
46+
if (options.Scopes is null)
47+
{
48+
return TypedResults.Problem(
49+
detail: $"No scopes found for the API '{apiName}'. 'scopes' needs to be either a single ",
50+
statusCode: StatusCodes.Status400BadRequest);
51+
}
52+
53+
if (!string.IsNullOrEmpty(agentIdentity) && !string.IsNullOrEmpty(agentUsername))
54+
{
55+
options.WithAgentUserIdentity(agentIdentity, agentUsername);
56+
}
57+
else if (!string.IsNullOrEmpty(agentIdentity))
58+
{
59+
options.WithAgentIdentity(agentIdentity);
60+
}
61+
62+
if (!string.IsNullOrEmpty(tenant))
63+
{
64+
options.AcquireTokenOptions.Tenant = tenant;
65+
}
66+
67+
var result = await headerProvider.CreateAuthorizationHeaderAsync(
68+
options.Scopes,
69+
options,
70+
httpContext.User,
71+
httpContext.RequestAborted);
72+
73+
return TypedResults.Ok(new AuthorizationHeaderResult(result));
74+
}
75+
76+
private static DownstreamApiOptions MergeDownstreamApiOptionsOverrides(DownstreamApiOptions left, DownstreamApiOptions right)
77+
{
78+
if (right is null)
79+
{
80+
return left;
81+
}
82+
83+
var res = left.Clone();
84+
85+
if (right.Scopes is not null && right.Scopes.Any())
86+
{
87+
res.Scopes = right.Scopes;
88+
}
89+
90+
if (!string.IsNullOrEmpty(right.AcquireTokenOptions.Tenant))
91+
{
92+
res.AcquireTokenOptions.Tenant = right.AcquireTokenOptions.Tenant;
93+
}
94+
95+
if (!string.IsNullOrEmpty(right.AcquireTokenOptions.Claims))
96+
{
97+
res.AcquireTokenOptions.Claims = right.AcquireTokenOptions.Claims;
98+
}
99+
100+
if (!string.IsNullOrEmpty(right.AcquireTokenOptions.AuthenticationOptionsName))
101+
{
102+
res.AcquireTokenOptions.AuthenticationOptionsName = right.AcquireTokenOptions.AuthenticationOptionsName;
103+
}
104+
105+
if (!string.IsNullOrEmpty(right.AcquireTokenOptions.FmiPath))
106+
{
107+
res.AcquireTokenOptions.FmiPath = right.AcquireTokenOptions.FmiPath;
108+
}
109+
110+
if (!string.IsNullOrEmpty(right.RelativePath))
111+
{
112+
res.RelativePath = right.RelativePath;
113+
}
114+
115+
res.AcquireTokenOptions.ForceRefresh = right.AcquireTokenOptions.ForceRefresh;
116+
117+
if (right.AcquireTokenOptions.ExtraParameters is not null)
118+
{
119+
if (res.AcquireTokenOptions.ExtraParameters is null)
120+
{
121+
res.AcquireTokenOptions.ExtraParameters = new Dictionary<string, object>();
122+
}
123+
foreach (var extraParameter in right.AcquireTokenOptions.ExtraParameters)
124+
{
125+
if (!res.AcquireTokenOptions.ExtraParameters.ContainsKey(extraParameter.Key))
126+
{
127+
res.AcquireTokenOptions.ExtraParameters.Add(extraParameter.Key, extraParameter.Value);
128+
}
129+
}
130+
}
131+
132+
return res;
133+
}
134+
}

src/Microsoft.Identity.Web.Sidecar/ValidateRequestEndpoints.cs renamed to src/Microsoft.Identity.Web.Sidecar/Endpoints/ValidateRequestEndpoints.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,31 @@ internal static class ValidateRequestEndpoints
1414
public static void AddValidateRequestEndpoints(this WebApplication app)
1515
{
1616
app.MapGet("/Validate", ValidateEndpoint).
17-
WithName("Validate Authorization header")
18-
.RequireAuthorization()
19-
.WithOpenApi()
20-
.ProducesProblem(401);
17+
WithName("Validate Authorization header").
18+
RequireAuthorization().
19+
WithOpenApi().
20+
ProducesProblem(401);
2121
}
2222

2323
private static Results<Ok<ValidateAuthorizationHeaderResult>, ProblemHttpResult> ValidateEndpoint(HttpContext httpContext, IConfiguration configuration)
2424
{
25-
string scopeRequiredByApi = configuration["AzureAd:Scopes"] ?? "";
25+
string scopeRequiredByApi = configuration["AzureAd:Scopes"] ?? string.Empty;
2626
httpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
2727

2828
var claimsPrincipal = httpContext.User;
2929
var token = claimsPrincipal.GetBootstrapToken() as JsonWebToken;
3030

3131
if (token is null)
3232
{
33-
return TypedResults.Problem("No token found", statusCode: 400);
33+
return TypedResults.Problem("No token found", statusCode: StatusCodes.Status400BadRequest);
3434
}
3535

3636
var decodedBody = Base64Url.DecodeFromChars(token.EncodedPayload);
3737
var jsonDoc = JsonSerializer.Deserialize<JsonNode>(decodedBody);
3838

3939
if (jsonDoc is null)
4040
{
41-
return TypedResults.Problem("Failed to decode token claims", statusCode: 400);
41+
return TypedResults.Problem("Failed to decode token claims", statusCode: StatusCodes.Status400BadRequest);
4242
}
4343

4444
var result = new ValidateAuthorizationHeaderResult(

src/Microsoft.Identity.Web.Sidecar/Microsoft.Identity.Web.Sidecar.csproj

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,32 @@
88
<GenerateDocumentationFile>False</GenerateDocumentationFile>
99
<IsPackable>false</IsPackable>
1010
<EnablePackageValidation>false</EnablePackageValidation>
11-
<NoWarn>$(NoWarn); RS0016</NoWarn>
11+
<NoWarn>
12+
$(NoWarn);
13+
<!--RS0016: Add public types and members to the declared API-->
14+
RS0016;
15+
<!--RS0036: Annotate nullability of public types and members in the declared API-->
16+
RS0036;
17+
<!--RS0037: Enable tracking of nullability of reference types in the declared API-->
18+
RS0037;
19+
<!--RS0051: Add internal types and members to the declared API-->
20+
RS0051
21+
</NoWarn>
1222
</PropertyGroup>
1323

14-
<PropertyGroup>
15-
<!--RS0016: Add public types and members to the declared API-->
16-
<NoWarn>RS0016</NoWarn>
17-
<!--RS0036: Annotate nullability of public types and members in the declared API-->
18-
<NoWarn>RS0036</NoWarn>
19-
<!--RS0037: Enable tracking of nullability of reference types in the declared API-->
20-
<NoWarn>RS0037</NoWarn>
21-
<!--RS0051: Add internal types and members to the declared API-->
22-
<NoWarn>RS0051</NoWarn>
23-
</PropertyGroup>
24-
25-
2624
<ItemGroup>
2725
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.9" NoWarn="NU1605" />
2826
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.9" NoWarn="NU1605" />
2927
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.9" />
28+
<PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="9.0.9">
29+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
30+
<PrivateAssets>all</PrivateAssets>
31+
</PackageReference>
3032
</ItemGroup>
3133

3234

3335
<ItemGroup>
36+
<ProjectReference Include="..\Microsoft.Identity.Web.AgentIdentities\Microsoft.Identity.Web.AgentIdentities.csproj" />
3437
<ProjectReference Include="..\Microsoft.Identity.Web.DownstreamApi\Microsoft.Identity.Web.DownstreamApi.csproj" />
3538
<ProjectReference Include="..\Microsoft.Identity.Web\Microsoft.Identity.Web.csproj" />
3639
</ItemGroup>
Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
@Microsoft.Identity.Web.Sidecar_HostAddress = http://localhost:5178
22

3+
GET {{Microsoft.Identity.Web.Sidecar_HostAddress}}/openapi/v1.json
4+
Accept: application/json
5+
6+
###
7+
38
GET {{Microsoft.Identity.Web.Sidecar_HostAddress}}/Validate/
49
Accept: application/json
510
Authorization: Bearer {{AccessToken}}
611

712
###
813

9-
GET {{Microsoft.Identity.Web.Sidecar_HostAddress}}/openapi/v1.json
10-
Accept: application/json
14+
POST {{Microsoft.Identity.Web.Sidecar_HostAddress}}/AuthorizationHeader/me
15+
Authorization: Bearer {{AccessToken}}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
namespace Microsoft.Identity.Web.Sidecar.Models;
5+
6+
internal record AuthorizationHeaderResult(string AuthorizationHeader);
Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,62 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
3-
#pragma warning disable RS0016 // Public API
43

54
using System.Diagnostics;
65
using Microsoft.AspNetCore.Authentication.JwtBearer;
76
using Microsoft.Identity.Web;
7+
using Microsoft.Identity.Web.Sidecar.Endpoints;
88
using Microsoft.IdentityModel.JsonWebTokens;
99

10-
var builder = WebApplication.CreateBuilder(args);
10+
namespace Microsoft.Identity.Web.Sidecar;
1111

12-
// Add services to the container.
13-
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
14-
.AddMicrosoftIdentityWebApi(
15-
configureJwtBearerOptions: jwtBearerOptions =>
16-
{
17-
builder.Configuration.GetSection("AzureAd").Bind(jwtBearerOptions);
18-
jwtBearerOptions.Events ??= new();
19-
jwtBearerOptions.Events.OnTokenValidated = context =>
12+
public class Program
13+
{
14+
public static void Main(string[] args)
15+
{
16+
var builder = WebApplication.CreateBuilder(args);
17+
18+
// Add services to the container.
19+
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
20+
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
21+
.EnableTokenAcquisitionToCallDownstreamApi()
22+
.AddInMemoryTokenCaches();
23+
24+
builder.Services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme,
25+
options =>
2026
{
21-
Debug.Assert(context.SecurityToken is JsonWebToken, "Token should always be JsonWebToken");
22-
var token = (JsonWebToken)context.SecurityToken;
23-
24-
if (context.Principal?.Identities.FirstOrDefault() is null)
27+
options.Events ??= new();
28+
options.Events.OnTokenValidated = context =>
2529
{
26-
context.Fail("No principal or no identity");
27-
return Task.FromResult(context);
28-
}
29-
30-
context.Principal.Identities.First().BootstrapContext = token!.InnerToken is not null ? token.InnerToken: token;
31-
return Task.FromResult(context);
32-
};
33-
},
34-
configureMicrosoftIdentityOptions: msidOptions => builder.Configuration.GetSection("AzureAd").Bind(msidOptions));
30+
Debug.Assert(context.SecurityToken is JsonWebToken, "Token should always be JsonWebToken");
31+
var token = (JsonWebToken)context.SecurityToken;
3532

36-
builder.Services.AddAuthorization();
33+
if (context.Principal?.Identities.FirstOrDefault() is null)
34+
{
35+
context.Fail("No principal or no identity");
36+
return Task.FromResult(context);
37+
}
3738

38-
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
39-
builder.Services.AddOpenApi();
39+
context.Principal.Identities.First().BootstrapContext = token.InnerToken is not null ? token.InnerToken : token;
40+
return Task.FromResult(context);
41+
};
42+
});
4043

41-
var app = builder.Build();
44+
builder.Services.AddAuthorization();
4245

43-
// Configure the HTTP request pipeline.
44-
if (app.Environment.IsDevelopment())
45-
{
46-
app.MapOpenApi();
47-
}
46+
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
47+
builder.Services.AddOpenApi();
4848

49-
app.AddValidateRequestEndpoints();
49+
var app = builder.Build();
5050

51-
app.Run();
51+
// Configure the HTTP request pipeline.
52+
if (app.Environment.IsDevelopment())
53+
{
54+
app.MapOpenApi();
55+
}
5256

57+
app.AddValidateRequestEndpoints();
58+
app.AddDownstreamApiRequestEndpoints();
5359

54-
// Added for test project WebApplicationFactory discovery
55-
public partial class Program { }
60+
app.Run();
61+
}
62+
}

src/Microsoft.Identity.Web.Sidecar/appsettings.Development.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"Logging": {
33
"LogLevel": {
44
"Default": "Information",
5-
"Microsoft.AspNetCore": "Warning"
5+
"Microsoft.AspNetCore": "Information"
66
}
77
}
88
}

src/Microsoft.Identity.Web.Sidecar/appsettings.json

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"$schema": "https://raw.githubusercontent.com/AzureAD/microsoft-identity-web/refs/heads/master/JsonSchemas/microsoft-identity-web.json",
23
/*
34
The following identity settings need to be configured
45
before the project can be successfully executed.
@@ -11,15 +12,29 @@ For more info see https://aka.ms/dotnet-template-ms-identity-platform
1112

1213
"Scopes": "access_as_user",
1314

14-
"ClientCertificates": [
15+
"ClientCredentials": [
16+
{
17+
"SourceType": "StoreWithDistinguishedName",
18+
"CertificateStorePath": "LocalMachine/My",
19+
"CertificateDistinguishedName": "CN=LabAuth.MSIDLab.com"
20+
}
1521
],
1622

1723
"EnablePiiLogging": false
1824
},
25+
26+
"DownstreamApi": {
27+
"me": {
28+
"BaseUrl": "https://graph.microsoft.com/v1.0/",
29+
"RelativePath": "me",
30+
"Scopes": [ "User.Read" ]
31+
}
32+
},
33+
1934
"Logging": {
2035
"LogLevel": {
2136
"Default": "Information",
22-
"Microsoft.AspNetCore": "Information"
37+
"Microsoft.AspNetCore": "Warning"
2338
}
2439
},
2540
"AllowedHosts": "*"

0 commit comments

Comments
 (0)