简体   繁体   中英

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


    <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


    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.

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.

What I've done is add the following OutputClaim to the technical profile that does the login against 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.

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

I then use another OrchestrationStep to call my TechnicalProfile that does the REST POST. My TechnicalProfile Looks like this:

<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

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):

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

And this is the GraphService I use above:

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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