2

Thank you and will appreciate some help. I am making a REST api call from a custom policy in Azure B2C. When I call the azure function in a browser, test/run in the auzre portal or via PowerShell Invoke-RestMehod, I get expected results. I get a 200 status OK when i look at the azure function in the portal (via the Monitor blade) or in Application Insights. B2C however keeps complaining with the error "Message: The claims exchange 'GetUserGroups' specified in step '7' returned HTTP error response that could not be parsed."

Here is the Claim Type in the TFExtensions.xml

<ClaimsSchema>
  <ClaimType Id="groupsMy">
    <DisplayName>Comma delimited list of group names</DisplayName>
    <DataType>stringCollection</DataType>
    <UserInputType>Readonly</UserInputType>
  </ClaimType>
</ClaimsSchema>

Here is the REST technical profile

    <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://functionapp.azurewebsites.net/api/GetGroupsFromGraph</Item>
        <Item Key="AuthenticationType">None</Item>
        <Item Key="SendClaimsIn">QueryString</Item>
        <Item Key="AllowInsecureAuthInProduction">true</Item>
      </Metadata>
      <InputClaims>
        <InputClaim Required="true" ClaimTypeReferenceId="objectId" />
      </InputClaims>
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="groupsMy" PartnerClaimType="groupsMy" />
      </OutputClaims>
      <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
    </TechnicalProfile>

Here is the orchestration step that calls the technical profile

    <OrchestrationStep Order="7" Type="ClaimsExchange">
      <ClaimsExchanges>
        <ClaimsExchange Id="GetUserGroups" TechnicalProfileReferenceId="GetUserGroups" />
      </ClaimsExchanges>
    </OrchestrationStep>

Here is the PowerShell Azure Function

using namespace System.Net

# Input bindings are passed in via param block.
param($Request, $TriggerMetadata)

# Write to the Azure Functions log stream.
Write-Host "PowerShell HTTP trigger function processed a request."


function getAccessTokenMicrosoftGraph($clientid, $tenant, $ClientSecret) {

      
    $loginURL = "https://login.microsoftonline.com"
    $resource = "https://graph.microsoft.com" #Microsoft Graph API

    # Get an OAuth 2 access token based on client id, secret and tenant
    $body = @{grant_type="client_credentials";client_id=$ClientID;client_secret=$ClientSecret;resource=$resource}
    $oauth = Invoke-RestMethod -Method Post -Uri $loginURL/$tenant/oauth2/token?api-version=1.0 -Body $body

    return $oauth

}


function returnUserGroups($objectId){
    
    $tenant = "tenant.onmicrosoft.com"
    $ClientID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx"
    $ClientSecret = "%%%%%%%%%%%%%%%%%%%%%%%%%"
    $body = @{grant_type="client_credentials";client_id=$ClientID;client_secret=$ClientSecret;resource=$resource}

    #get Microsoft Graph Token
    $oauthToken = getAccessTokenMicrosoftGraph -clientid $ClientID -tenant $tenant -ClientSecret $ClientSecret

    #Generate the authentication header and make the request
    $AuthHeader =  @{"Authorization"= $oauthToken.access_token;"Content-Type"="application/json";"ContentLength"=$body.length }

    #Get membership
    $MemberOf = Invoke-WebRequest -Headers $AuthHeader -Uri "https://graph.microsoft.com/beta/users/$objectId/transitiveMemberOf"
    #$MemberOf = Invoke-WebRequest -Headers $AuthHeader -Uri "https://graph.windows.net/$tenant/users/$objectId/$links/memberOf"

    #The query will return a JSON object, which you can convert to an array via the following:
    $result = ($MemberOf.Content | ConvertFrom-Json).Value

    #parse into comma-separated
    #$groups = ($result | Where-Object {$_.'@odata.type' -eq '#microsoft.graph.group'} | Select-Object DisplayName).DisplayName -join ","
    $groups = ($result | Where-Object {$_.'@odata.type' -eq '#microsoft.graph.group'} | Select-Object @{name="groupsMy" ; expression={$_.DisplayName}})


    return $groups | ConvertTo-Json
    

}

# Interact with query parameters or the body of the request.
$objectId = $Request.Query.objectId
if (-not $objectId) {
    $objectId = $Request.Body.objectId
}

if ($objectId) {
    Write-Host "ObjectID successfully received by fxn"
    $body = $objectId
    $pshell = returnUserGroups -objectId $body  
}
else {
    $status = [HttpStatusCode]::BadRequest
    $body = "Please pass a name on the query string or in the request body."
}

if ($pshell){
    $status = [HttpStatusCode]::OK
    Write-Host "groups from graph API loaded"
}
else {
    $status = [HttpStatusCode]::BadRequest
    $body = "groups not successfully received from Graph API."
}  


# Associate values to output bindings by calling 'Push-OutputBinding'.


Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
    StatusCode = $status
    Body = $pshell | ConvertTo-Json
})

Finally, my relying party technical profile (SAML)

<RelyingParty>
    <DefaultUserJourney ReferenceId="SignUpOrSignIn" />
      <TechnicalProfile Id="PolicyProfile">
        <DisplayName>PolicyProfile</DisplayName>
        <Protocol Name="SAML2"/>
        <OutputClaims>
          <OutputClaim ClaimTypeReferenceId="displayName" />
          <OutputClaim ClaimTypeReferenceId="givenName" />
          <OutputClaim ClaimTypeReferenceId="surname" />
          <OutputClaim ClaimTypeReferenceId="groupsMy" PartnerClaimType="groupsMy"/>
          <OutputClaim ClaimTypeReferenceId="email" DefaultValue="" />
          <OutputClaim ClaimTypeReferenceId="identityProvider" DefaultValue="" />
          <OutputClaim ClaimTypeReferenceId="objectId"/>
        </OutputClaims>
         <SubjectNamingInfo ClaimType="email" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" ExcludeAsClaim="true"/>
      </TechnicalProfile>
  </RelyingParty>
Bandz
  • 253
  • 4
  • 15
  • did you get a chance to look at this similar [issue](https://stackoverflow.com/questions/62863830/return-simple-string-claim-from-custom-policies-in-adb2c) – Raghavendra beldona Jul 21 '20 at 20:46
  • Yes, i did. However, getting the payload was a challenge. I was using application insights and couldn't seem to be able to find the actual payload going back to b2c. Any ideas? – Bandz Jul 27 '20 at 00:46
  • Hi @Bandz sorry for the delay are you still looking for any help on this – Raghavendra beldona Aug 20 '20 at 09:52
  • @Raghavendra-MSFTIdentity I am experiencing the same exact issue. It does not seem the logs from Application Insights provide any more detail about the response from the REST API, and I do not see any errors in the logs of the API itself – ctj232 Aug 26 '20 at 17:45
  • Hello @Raghavendra-MSFTIdentity, thanks for checking back! Yes, I am still experiencing this issue. – Bandz Aug 27 '20 at 01:02
  • Use POSTMan and get the raw payload so we have an idea of what your APIs response actually looks like. – Jas Suri - MSFT Sep 29 '20 at 13:20
  • @JasSuri-MSFT My apologies. My attention moved away from this but never got it resolved. Here is the response payload, Appreciate your help. [ "User", "Admin" ] – Bandz May 01 '22 at 22:03
  • I was able to figure this out. The API is an Azure PowerShell function. I simply had to use the ConvertTo-Json to convert my System.Collections.Generic.List[string] to JSON. And then, I further parsed as follows: $res = @" { "roleNames": [ $json-variable ] } "@ Stopped getting the error from the B2C REST TP after that. – Bandz May 01 '22 at 22:44

3 Answers3

2
using Microsoft.AspNetCore.Mvc;
[HttpGet]
        public async Task<JsonResult> Groups(string objectId)
        { 
            List<string> groups = [your list of groups];
            
            //!!!!!This is the trick if I sent plain json I got the error
            
            JsonResult o = new JsonResult(
                new
                {
                    groups
                });

            return o;
}  
 
1

the correct, by Azure B2C rest-interface expected JSON Format, for a stringCollection is that one:


{
    "groups": [
        "Ford",
        "BMW",
        "Fiat"
    ]
}

Tobi
  • 109
  • 1
  • 1
  • 12
0

I was able to figure this out. The API is an Azure PowerShell function. I simply had to use the ConvertTo-Json to convert my System.Collections.Generic.List[string] to JSON. And then, I further parsed as follows: $res = @" { "roleNames": [ $json-variable ] } "@ Stopped getting the error from the B2C REST TP after that.

Bandz
  • 253
  • 4
  • 15