Skip to content

Commit

Permalink
Cherry pick #2857 and 2859 (#2860)
Browse files Browse the repository at this point in the history
* Adding a base class for AuthorizationHeaderProvider for extensibility (#2857)

* Adding a base class for AuthorizationHeaderProvider usable by
extensions that want to leverage IdWeb for Bearer and Pop and process
their own protocols

* Update src/Microsoft.Identity.Web.TokenAcquisition/BaseAuthorizationHeaderProvider.cs

Co-authored-by: jennyf19 <[email protected]>

---------

Co-authored-by: jennyf19 <[email protected]>

* Fixes #2855 (#2859)

Tested with End to end test

---------

Co-authored-by: jennyf19 <[email protected]>
  • Loading branch information
jmprieur and jennyf19 authored May 27, 2024
1 parent 3d33ee3 commit 8601843
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Abstractions;
Expand Down Expand Up @@ -59,6 +60,7 @@ public async Task AuthenticateRequestAsync(

scopes = authenticationOptions?.Scopes ?? _defaultAuthenticationOptions.Scopes;
graphServiceClientOptions = authenticationOptions ?? _defaultAuthenticationOptions;
ClaimsPrincipal? user = authenticationOptions?.User;

// Remove the authorization header if it exists
if (request.Headers.ContainsKey(AuthorizationHeaderKey))
Expand Down Expand Up @@ -88,13 +90,16 @@ public async Task AuthenticateRequestAsync(
if (authorizationHeaderProviderOptions!.RequestAppToken)
{
authorizationHeader = await _authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync("https://graph.microsoft.com/.default",
authorizationHeaderProviderOptions);
authorizationHeaderProviderOptions,
cancellationToken);
}
else
{
authorizationHeader = await _authorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync(
scopes!,
authorizationHeaderProviderOptions);
authorizationHeaderProviderOptions,
claimsPrincipal: user,
cancellationToken);
}
request.Headers.Add(AuthorizationHeaderKey, authorizationHeader);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Microsoft.Identity.Abstractions;
using System.Collections.Generic;
using System.Security.Claims;

namespace Microsoft.Identity.Web
{
Expand All @@ -28,5 +29,11 @@ public GraphServiceClientOptions()
/// should end in "./default")
/// </summary>
public IEnumerable<string> Scopes { get; set; }

/// <summary>
/// When calling Microsoft graph with delegated permissions offers a way to override the
/// user on whose behalf the call is made.
/// </summary>
public ClaimsPrincipal? User { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using Microsoft.Kiota.Abstractions;

namespace Microsoft.Identity.Web
Expand Down Expand Up @@ -106,6 +107,25 @@ public static IList<IRequestOption> WithAuthenticationScheme(this IList<IRequest
graphAuthenticationOptions.AcquireTokenOptions.AuthenticationOptionsName = authenticationScheme;
return options;
}
#endif
#endif

/// <summary>
/// Specifies to use app only permissions for Graph.
/// </summary>
/// <param name="options">Options to modify.</param>
/// <param name="user">Overrides the user on behalf of which Microsoft Graph is called
/// (for delegated permissions in some specific scenarios)</param>
/// <returns></returns>
public static IList<IRequestOption> WithUser(this IList<IRequestOption> options, ClaimsPrincipal user)
{
GraphAuthenticationOptions? graphAuthenticationOptions = options.OfType<GraphAuthenticationOptions>().FirstOrDefault();
if (graphAuthenticationOptions == null)
{
graphAuthenticationOptions = new GraphAuthenticationOptions();
options.Add(graphAuthenticationOptions);
}
graphAuthenticationOptions.User = user;
return options;
}
}
}
15 changes: 15 additions & 0 deletions src/Microsoft.Identity.Web.MicrosoftGraph/BaseRequestExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System;
using System.Security.Claims;
using Microsoft.Graph;
using Microsoft.Identity.Abstractions;

Expand Down Expand Up @@ -95,5 +96,19 @@ private static T SetParameter<T>(T baseRequest, Action<TokenAcquisitionAuthentic

return baseRequest;
}

/// <summary>
/// Overrides authentication options for a given request.
/// </summary>
/// <typeparam name="T">Request</typeparam>
/// <param name="baseRequest">Request.</param>
/// <param name="user">Delegate to override
/// the authentication options</param>
/// <returns>Base request</returns>
public static T WithUser<T>(this T baseRequest,
ClaimsPrincipal user) where T : IBaseRequest
{
return SetParameter(baseRequest, options => options.User = user );
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@

using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Graph;
using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Client;

namespace Microsoft.Identity.Web
{
Expand Down Expand Up @@ -37,13 +36,15 @@ public async Task AuthenticateRequestAsync(HttpRequestMessage request)
bool appOnly = _initialOptions.AppOnly ?? false;
string? tenant = _initialOptions.Tenant ?? null;
string? scheme = _initialOptions.AuthenticationScheme ?? null;
ClaimsPrincipal? user = null;
// Extract per-request options from the request if present
TokenAcquisitionAuthenticationProviderOption? msalAuthProviderOption = GetMsalAuthProviderOption(request);
if (msalAuthProviderOption != null) {
scopes = msalAuthProviderOption.Scopes ?? scopes;
appOnly = msalAuthProviderOption.AppOnly ?? appOnly;
tenant = msalAuthProviderOption.Tenant ?? tenant;
scheme = msalAuthProviderOption.AuthenticationScheme ?? scheme;
user = msalAuthProviderOption.User ?? user;
}

if (!appOnly && scopes == null)
Expand Down Expand Up @@ -71,7 +72,8 @@ public async Task AuthenticateRequestAsync(HttpRequestMessage request)
{
authorizationHeader = await _authorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync(
scopes!,
downstreamOptions).ConfigureAwait(false);
downstreamOptions,
claimsPrincipal: user).ConfigureAwait(false);
}

// add or replace authorization header
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System;
using System.Security.Claims;
using Microsoft.Graph;
using Microsoft.Identity.Abstractions;

Expand All @@ -14,5 +15,6 @@ internal class TokenAcquisitionAuthenticationProviderOption : IAuthenticationPro
public string? Tenant { get; set; }
public string? AuthenticationScheme { get; set; }
public Action<AuthorizationHeaderProviderOptions>? AuthorizationHeaderProviderOptions { get; set; }
public ClaimsPrincipal? User { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Identity.Abstractions;

namespace Microsoft.Identity.Web.Extensibility
{
/// <summary>
/// Base class for custom implementations of <see cref="IAuthorizationHeaderProvider"/> that
/// would still want to leverage the default implementation for the bearer and Pop protocols.
/// </summary>
public class BaseAuthorizationHeaderProvider : IAuthorizationHeaderProvider
{
/// <summary>
/// Constructor from a service provider
/// </summary>
/// <param name="serviceProvider"></param>
public BaseAuthorizationHeaderProvider(IServiceProvider serviceProvider)
{
// We, intentionally, use a locator pattern here, because we don't want to expose ITokenAcquisition
// in the public API as it's going to be deprecated in future versions of IdWeb. Here this
// is an implementation detail.
var _tokenAcquisition = serviceProvider.GetRequiredService<ITokenAcquisition>();
implementation = new DefaultAuthorizationHeaderProvider(_tokenAcquisition);
}

private IAuthorizationHeaderProvider implementation;

/// <inheritdoc/>
public virtual Task<string> CreateAuthorizationHeaderForUserAsync(IEnumerable<string> scopes, AuthorizationHeaderProviderOptions? authorizationHeaderProviderOptions = null, ClaimsPrincipal? claimsPrincipal = null, CancellationToken cancellationToken = default)
{
return implementation.CreateAuthorizationHeaderForUserAsync(scopes, authorizationHeaderProviderOptions, claimsPrincipal, cancellationToken);
}

/// <inheritdoc/>
public virtual Task<string> CreateAuthorizationHeaderForAppAsync(string scopes, AuthorizationHeaderProviderOptions? downstreamApiOptions = null, CancellationToken cancellationToken = default)
{
return implementation.CreateAuthorizationHeaderForAppAsync(scopes, downstreamApiOptions, cancellationToken);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ public IndexModel(ILogger<IndexModel> logger, GraphServiceClient graphServiceCli

public async Task OnGet()
{
var user = await _graphServiceClient.Me.GetAsync(r => r.Options.WithScopes("user.read"));
var user = await _graphServiceClient.Me.GetAsync(r =>
r.Options.WithScopes("user.read")
//.WithUser(User)
);

try
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Client;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.Extensibility;
using Xunit;

namespace TokenAcquirerTests
{
public class BaseAuthorizationHeaderProviderTest
{
public BaseAuthorizationHeaderProviderTest()
{
TokenAcquirerFactory.ResetDefaultInstance(); // Test only
}

// Example of extension
class CustomAuthorizationHeaderProvider : BaseAuthorizationHeaderProvider
{
public CustomAuthorizationHeaderProvider(IServiceProvider serviceProvider) : base(serviceProvider)
{
}
public override Task<string> CreateAuthorizationHeaderForAppAsync(string scopes, AuthorizationHeaderProviderOptions? downstreamApiOptions = null, CancellationToken cancellationToken = default)
{
if (downstreamApiOptions?.ProtocolScheme == "Custom")
return Task.FromResult("Custom");
else
return base.CreateAuthorizationHeaderForAppAsync(scopes, downstreamApiOptions, cancellationToken);
}

public override Task<string> CreateAuthorizationHeaderForUserAsync(IEnumerable<string> scopes, AuthorizationHeaderProviderOptions? authorizationHeaderProviderOptions = null, ClaimsPrincipal? claimsPrincipal = null, CancellationToken cancellationToken = default)
{
if (authorizationHeaderProviderOptions?.ProtocolScheme == "Custom")
return Task.FromResult("Custom");
else
return base.CreateAuthorizationHeaderForUserAsync(scopes, authorizationHeaderProviderOptions, claimsPrincipal, cancellationToken);
}
}

// Mock for ITokenAcquisition
class CustomTokenAcquisition : ITokenAcquisition
{
public Task<string> GetAccessTokenForAppAsync(string scope, string? authenticationScheme, string? tenant = null, TokenAcquisitionOptions? tokenAcquisitionOptions = null)
{
throw new NotImplementedException();
}

public Task<string> GetAccessTokenForUserAsync(IEnumerable<string> scopes, string? authenticationScheme, string? tenantId = null, string? userFlow = null, ClaimsPrincipal? user = null, TokenAcquisitionOptions? tokenAcquisitionOptions = null)
{
throw new NotImplementedException();
}

public Task<AuthenticationResult> GetAuthenticationResultForAppAsync(string scopes, string? authenticationOptionsName = null, string? tenant = null, TokenAcquisitionOptions? tokenAcquisitionOptions = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}

public Task<AuthenticationResult> GetAuthenticationResultForAppAsync(string scope, string? authenticationScheme, string? tenant = null, TokenAcquisitionOptions? tokenAcquisitionOptions = null)
{
throw new NotImplementedException();
}

public Task<AuthenticationResult> GetAuthenticationResultForUserAsync(IEnumerable<string> scopes, string? authenticationOptionsName = null, string? tenant = null, string? userFlow = null, ClaimsPrincipal? claimsPrincipal = null, TokenAcquisitionOptions? tokenAcquisitionOptions = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}

public Task<AuthenticationResult> GetAuthenticationResultForUserAsync(IEnumerable<string> scopes, string? authenticationScheme, string? tenantId = null, string? userFlow = null, ClaimsPrincipal? user = null, TokenAcquisitionOptions? tokenAcquisitionOptions = null)
{
return Task.FromResult(new AuthenticationResult("eXY", false, null, DateTimeOffset.Now, DateTimeOffset.Now, null, null, null, null, Guid.Empty));
}

public string GetEffectiveAuthenticationScheme(string? authenticationScheme)
{
throw new NotImplementedException();
}

public void ReplyForbiddenWithWwwAuthenticateHeader(IEnumerable<string> scopes, MsalUiRequiredException msalServiceException, string? authenticationScheme, HttpResponse? httpResponse = null)
{
throw new NotImplementedException();
}

public Task ReplyForbiddenWithWwwAuthenticateHeaderAsync(IEnumerable<string> scopes, MsalUiRequiredException msalServiceException, HttpResponse? httpResponse = null)
{
throw new NotImplementedException();
}
}

[Fact]
public async Task TestBaseAuthorizationHeaderProvider()
{
TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();
// Test the extensibility
tokenAcquirerFactory.Services.AddSingleton<IAuthorizationHeaderProvider, CustomAuthorizationHeaderProvider>();

// Mock the token acquisition
tokenAcquirerFactory.Services.AddSingleton<ITokenAcquisition, CustomTokenAcquisition>();
var serviceProvider = tokenAcquirerFactory.Build();

IAuthorizationHeaderProvider authorizationHeaderProvider = serviceProvider.GetRequiredService<IAuthorizationHeaderProvider>();
string result = await authorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync(["scope"],
new AuthorizationHeaderProviderOptions { ProtocolScheme = "Custom" }, null, CancellationToken.None);
Assert.Equal("Custom", result);

result = await authorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync(["scope"],
new AuthorizationHeaderProviderOptions { }, null, CancellationToken.None);
Assert.Equal("Bearer eXY", result);

}
}
}

0 comments on commit 8601843

Please sign in to comment.