简体   繁体   English

Azure B2c 安全组授权通过自定义策略使用 Rest 后调用

[英]Azure B2c security groups authorization through custom policy using Rest Post call

I'm trying to use custom policies where I was able to deployed REST API to get groups claims我正在尝试使用能够部署 REST API 的自定义策略来获取组声明


    <ClaimsSchema>
          <ClaimType Id="groups">
            <DisplayName>B2C-user-test,B2C-admin-test</DisplayName>
            <DataType>stringCollection</DataType>
          </ClaimType>
    
        <!--Demo: List of permitted  security groups user can sign-in.
            Null or empty means, user any user can sign-in.
            This claim sends to REST API-->
        <ClaimType Id="onlyMembersOf">
            <DisplayName>onlyMembersOf</DisplayName>
            <DataType>string</DataType>
          </ClaimType>
    
        </ClaimsSchema>
        </BuildingBlocks>
          
      <ClaimsProviders>
        <ClaimsProvider>
          <DisplayName>Local Account</DisplayName>
          <TechnicalProfiles>
            <TechnicalProfile Id="SelfAsserted-LocalAccountSignin-Email">
              <OutputClaims>
                <!--Demo: Add the groups claim type to the output claim collection-->
                <OutputClaim ClaimTypeReferenceId="groups" />
              </OutputClaims>
              <ValidationTechnicalProfiles>
                <!-- Demo: Make sure you first call the login-NonInteractive technical profile, to get the user ID.
                     Then call the role-based access control REST API to get adn validate user's groups -->
                <ValidationTechnicalProfile ReferenceId="login-NonInteractive" />
                <ValidationTechnicalProfile ReferenceId="REST-RBAC" />
              </ValidationTechnicalProfiles>        
            </TechnicalProfile>
          </TechnicalProfiles>
        </ClaimsProvider>
    
        <!-- Local account Sign-In claims provider -->
        <ClaimsProvider>
          <DisplayName>Local Account SignIn</DisplayName>
          <TechnicalProfiles>
             <TechnicalProfile Id="login-NonInteractive">
              <Metadata>
                <Item Key="client_id">444b09a2-0f8b-4f05-b454-54495b5ef601</Item>
                <Item Key="IdTokenAudience">bd80807b-81d0-4732-a517-1132b128206c</Item>
              </Metadata>
              <InputClaims>
                <InputClaim ClaimTypeReferenceId="client_id" DefaultValue="444b09a2-0f8b-4f05-b454-54495b5ef601" />
                <InputClaim ClaimTypeReferenceId="resource_id" PartnerClaimType="resource" DefaultValue="bd80807b-81d0-4732-a517-1132b128206c" />
              </InputClaims>
            </TechnicalProfile>
          </TechnicalProfiles>
        </ClaimsProvider>
    
    <ClaimsProvider>
          <DisplayName>REST APIs</DisplayName>
          <TechnicalProfiles>
            <TechnicalProfile Id="REST-RBAC">
              <DisplayName>Read and validate user's groups</DisplayName>
              <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
              <Metadata>
                <!--Demo: Change the service URL with your REST API location-->
                <Item Key="ServiceUrl">https://test.azurewebsites.net/api/Identity/IsMemberOf</Item>
                
                <!--Demo: Change the AuthenticationType to basic or ClientCertificate.
                For more information, see: https://docs.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-custom-rest-api-netfw-secure-cert-->
                <Item Key="AuthenticationType">None</Item>
                <Item Key="SendClaimsIn">Body</Item>
                <Item Key="AllowInsecureAuthInProduction">true</Item>
              </Metadata>
              <CryptographicKeys>
                <Key Id="BasicAuthenticationUsername" StorageReferenceId="B2C_1A_RestApiUsername" />
                <Key Id="BasicAuthenticationPassword" StorageReferenceId="B2C_1A_RestApiPassword" />
              </CryptographicKeys>
              <InputClaims>
                <InputClaim ClaimTypeReferenceId="objectId" />
                <!--Demo: set the DefaultValue to empty string or comma delimiter list 
                of security groups to validate-->
                <InputClaim ClaimTypeReferenceId="onlyMembersOf" DefaultValue="B2C-user-test" />
              </InputClaims>
              <OutputClaims>
                <OutputClaim ClaimTypeReferenceId="groups" />
              </OutputClaims>
              <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
            </TechnicalProfile>
          </TechnicalProfiles>
        </ClaimsProvider>
      </ClaimsProviders>

so above TrustFrameworkExtensions policy calls Rest method but it is returning catch statement exception所以上面的 TrustFrameworkExtensions 策略调用 Rest 方法,但它返回 catch 语句异常


    catch (Exception ex)
                {
                    if (ex.Message.Contains("Request_ResourceNotFound"))
                    {
                        return StatusCode((int)HttpStatusCode.Conflict, new B2CResponseModel("Can not read user groups, user not found", HttpStatusCode.Conflict));
                    }
    
                    return StatusCode((int)HttpStatusCode.Conflict, new B2CResponseModel("Can not read user groups", HttpStatusCode.Conflict));
                }"

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using AADB2C.RBAC.Sample.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using System.Net.Http;


namespace AADB2C.RBAC.Sample.Controllers
{
    [Route("api/[controller]/[action]")]
    public class IdentityController : Controller
    {
        private readonly AppSettingsModel AppSettings;

        // Demo: Inject an instance of an AppSettingsModel class into the constructor of the consuming class, 
        // and let dependency injection handle the rest
        public IdentityController(IOptions<AppSettingsModel> appSettings)
        {
            this.AppSettings = appSettings.Value;
        }

        [HttpPost(Name = "IsMemberOf")]
        public async Task<ActionResult> IsMemberOf()
        {
            string input = null;

            // If not data came in, then return
            if (this.Request.Body == null)
            {
                return StatusCode((int)HttpStatusCode.Conflict, new B2CResponseModel("Request content is null", HttpStatusCode.Conflict));
            }

            //Read the input claims from the request body
            using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8))
            {
                input = await reader.ReadToEndAsync();
            }

            //string input = Request.Content.ReadAsStringAsync().Result;

            //string content = "";
            //System.Web.HttpContext.Current.Request.InputStream.Position = 0;
            //using (var reader = new StreamReader(
            //         Request.InputStream, System.Text.Encoding.UTF8, true, 4096, true))
            //{
            //    content = reader.ReadToEnd();
            //}
            ////Rest
            //System.Web.HttpContext.Current.Request.InputStream.Position = 0;

            // Check input content value
            if (string.IsNullOrEmpty(input))
            {
                return StatusCode((int)HttpStatusCode.Conflict, new B2CResponseModel("Request content is empty", HttpStatusCode.Conflict));
            }

            // Convert the input string into InputClaimsModel object
            InputClaimsModel inputClaims = InputClaimsModel.Parse(input);

            if (inputClaims == null)
            {
                return StatusCode((int)HttpStatusCode.Conflict, new B2CResponseModel("Can not deserialize input claims", HttpStatusCode.Conflict));
            }

            if (string.IsNullOrEmpty(inputClaims.objectId))
            {
                return StatusCode((int)HttpStatusCode.Conflict, new B2CResponseModel("User 'objectId' is null or empty", HttpStatusCode.Conflict));
            }


            try
            {
                AzureADGraphClient azureADGraphClient = new AzureADGraphClient(this.AppSettings.Tenant, this.AppSettings.ClientId, this.AppSettings.ClientSecret);

                // Demo: Get user's groups
                GraphGroupsModel groups = await azureADGraphClient.GetUserGroup(inputClaims.objectId);

                // Demo: Add the groups to string collections
                List<string> groupsList = new List<string>();
                foreach (var item in groups.value)
                {
                    groupsList.Add(item.displayName);
                }

                // Demo: Set the output claims
                OutputClaimsModel output = new OutputClaimsModel() { groups = groupsList };

                // Demo: Check if user needs to be a member of a security group
                if (!string.IsNullOrEmpty(inputClaims.onlyMembersOf))
                {
                    List<string> onlyMembersOf = inputClaims.onlyMembersOf.ToLower().Split(',').ToList<string>();
                    bool isMemberOf = false;
                    foreach (var item in output.groups)
                    {
                        if (onlyMembersOf.Contains(item.ToLower()))
                        {
                            isMemberOf = true;
                            break;
                        }
                    }

                    // Demo: Throw error if user is not member of one of the security groups
                    if (isMemberOf == false)
                    {
                        return StatusCode((int)HttpStatusCode.Conflict, new B2CResponseModel("You are not authorized to sign-in to this application.", HttpStatusCode.Conflict));
                    }
                }

                // Demo: Return the groups collection
                return Ok(output);
            }
            catch (Exception ex)
            {
                if (ex.Message.Contains("Request_ResourceNotFound"))
                {
                    return StatusCode((int)HttpStatusCode.Conflict, new B2CResponseModel("Can not read user groups, user not found", HttpStatusCode.Conflict));
                }

                return StatusCode((int)HttpStatusCode.Conflict, new B2CResponseModel("Can not read user groups", HttpStatusCode.Conflict));
            }

        }

    }
}

So I have reached point where custom policy uses REST POST method to get the groups details but this code mentioned doesn't provide me group details as it goes to catch statement and throws can not read user groups.The problem here is I cannot use localhost to run the Rest API and hit the breakpoints of API through custom policy as it is not supported,I tried using ngrox.exe but it gives me bad request in return.所以我已经达到了自定义策略使用 REST POST 方法来获取组详细信息的地步,但是提到的这段代码没有提供组详细信息,因为它去 catch 语句并且抛出无法读取用户组。这里的问题是我不能使用 localhost运行 Rest API 并通过自定义策略命中 API 的断点,因为它不受支持,我尝试使用返回。

Any help or example would be very helpful任何帮助或示例都会非常有帮助

I've done this a bit differently, but had more luck with my method than using similar code to yours, which I also found in many samples.我的做法有点不同,但我的方法比使用与你的类似代码更幸运,我在许多示例中也发现了这种代码。 I also prefer this method over the often referenced guide at mrochon.azurewebsites.net as this does not require an app registration to be setup, or to store secrets anywhere, and works much better for multi-tenant applications.我也更喜欢这种方法,而不是 mrochon.azurewebsites.net 上经常引用的指南,因为这不需要设置应用程序注册,也不需要在任何地方存储机密,并且对于多租户应用程序效果更好。

What I've done is add the following OutputClaim to the technical profile that does the login against login.microsoftonline.com.我所做的是将以下 OutputClaim 添加到针对 login.microsoftonline.com 进行登录的技术配置文件中。 This gives us the user token supplied by the identity provider.这为我们提供了身份提供者提供的用户令牌。 Then we can later use this token to query the Graph API directly.然后我们以后可以使用这个token直接查询Graph API。

<OutputClaim ClaimTypeReferenceId="identityProviderAccessToken" PartnerClaimType="{oauth2:access_token}" />

I then use another OrchestrationStep to call my TechnicalProfile that does the REST POST.然后,我使用另一个 OrchestrationStep 调用我的 TechnicalProfile,它执行 REST POST。 My TechnicalProfile Looks like this:我的 TechnicalProfile 看起来像这样:

<TechnicalProfile Id="GetUserGroups">
  <DisplayName>Retrieves security groups assigned to the user</DisplayName>
  <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
  <Metadata>
    <Item Key="ServiceUrl">https://PATH-TO-YOUR-REST-API/groups</Item>
    <Item Key="AuthenticationType">None</Item>
    <Item Key="SendClaimsIn">Body</Item>
  </Metadata>
  <InputClaims>
    <InputClaim ClaimTypeReferenceId="objectId" /><!-- optional -->
    <InputClaim ClaimTypeReferenceId="tenantId" /><!-- optional -->
    <InputClaim ClaimTypeReferenceId="identityProviderAccessToken" />
  </InputClaims>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="groups" />
  </OutputClaims>
  <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>

The user token will be delivered to your REST API with the same name: identityProviderAccessToken用户令牌将传送到您的 REST API 同名: identityProviderAccessToken

This token can be used to look up the Graph API directly, so you only need code like this (roleGroups.Keys is used to limit the groups that we want returned):这个token可以用来直接查找Graph API,所以只需要这样的代码(roleGroups.Keys用来限制我们想要返回的组):

var graphService = await GraphService.CreateOnBehalfOfUserAsync(inputClaims.identityProviderAccessToken);
var memberGroups = await graphService.CheckMemberGroupsDelegateAsync(roleGroups.Keys);

And this is the GraphService I use above:这是我在上面使用的 GraphService:

public class GraphService
{
    private readonly IGraphServiceClient _client;

    private GraphService(IGraphServiceClient client)
    {
        _client = client;
    }

    public static async Task<GraphService> CreateOnBehalfOfUserAsync(string userToken)
    {

        GraphServiceClient graphClient = new GraphServiceClient(
            "https://graph.microsoft.com/v1.0",
            new DelegateAuthenticationProvider(async (requestMessage) =>
            {
            requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", userToken);
            }));

        return new GraphService(graphClient);
    }

    public async Task<IEnumerable<string>> CheckMemberGroupsDelegateAsync(IEnumerable<string> groupIds)
    {
        //You can check up to a maximum of 20 groups per request (see graph api doc).
        var batchSize = 20;

        var tasks = new List<Task<IDirectoryObjectCheckMemberGroupsCollectionPage>>();
        foreach (var groupsBatch in groupIds.Batch(batchSize))
        {
            tasks.Add(_client.Me.CheckMemberGroups(groupsBatch).Request().PostAsync());
        }
        await Task.WhenAll(tasks);

        return tasks.SelectMany(x => x.Result.ToList());
    }
}

If you need the extension to do batching of the results, here that is too:如果您需要扩展来对结果进行批处理,这也是:

public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> items,
    int maxItems)
{
    return items.Select((item, inx) => new { item, inx })
        .GroupBy(x => x.inx / maxItems)
        .Select(g => g.Select(x => x.item));
}

Good luck.祝你好运。 It's a wild ride inside these custom policies.在这些自定义政策中,这是一次疯狂的旅程。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM