Azure B2C: How do I get "group" claim in JWT token

7.6k views Asked by At

In the Azure B2C, I used to be able to get a "groups" claim in my JWT tokens by following Retrieving Azure AD Group information with JWT:

  • Open the old-school Azure manager (https://manage.windowsazure.com)
  • Register my application with B2C
  • Download the B2C manifest for the application
  • In the manifest, change the "groupMembershipClaims" entry to

    "groupMembershipClaims": "SecurityGroup",

  • Upload the changed B2C manifest again

The problem

This has worked well in the past (about a month ago, I believe...) but it doesn't anymore. See below for details...

What I've tried sofar

Plan A: Use Azure Manager

Follow the known-good recipe above.

Unfortunately that doesn't work anymore - I get the following error when this client tries to authenticate me with B2C:

AADB2C90068: The provided application with ID '032fe196-e17d-4287-9cfd-25386d49c0d5' is not valid against this service. Please use an application created via the B2C portal and try again"

OK, fair enough - they're moving us to the new Portal.

Plan B: Use Azure Portal

Follow the good old recipe, using the new Portal.

But that doesn't work either - when I get to the "download manifest" part, I cannot find any way to access the manifest (and Googling tells me it's probably gone for good...).

Plan C: Mix Azure Portal and manager

Getting a little desperate, I tried mixing plans A and B: register the app using the new Portal, then change the manifest using the old Azure Manager.

But no luck - when I try to upload the manifest, it fails with the message

ParameterValidationException=Invalid parameters provided; BadRequestException=Updates to converged applications are not allowed in this version.

Plan Z: Use the Graph API to retrieve group membership data

Just give up the "group" claim - instead, whenever I need group info, just query the B2C server using the Graph API.

I really, really don't want to do this - it would ruin the self-contained-ness of the access token, and make the system very "chatty".

But I've included it as a plan Z here, just to say: yes, I know the option exists, no I haven't tried it - and I'd prefer not to.

The question:

How do I get the "group" claim in my JWT token these days?

1

There are 1 answers

3
gfyans On BEST ANSWER

Plan Z it is I'm afraid. I don't know why they don't return it, but it's currently marked as planned on their Feedback Portal (it's the highest rated item).

This is how I'm doing it. Querying the groups when the user is authenticated, you can do it your way as well - just query as and when you need to. Depends on your use case.

public partial class Startup
{
    public void ConfigureAuth(IAppBuilder app)
    {
        app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
        app.UseKentorOwinCookieSaver();
        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            LoginPath = new PathString("/account/unauthorised"),
            CookieSecure = CookieSecureOption.Always,
            ExpireTimeSpan = TimeSpan.FromMinutes(20),
            SlidingExpiration = true,
            CookieHttpOnly = true
        });

        // Configure OpenID Connect middleware for each policy
        app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(Globals.SignInPolicyId));
    }

    private OpenIdConnectAuthenticationOptions CreateOptionsFromPolicy(string policy)
    {
        return new OpenIdConnectAuthenticationOptions
        {
            // For each policy, give OWIN the policy-specific metadata address, and
            // set the authentication type to the id of the policy
            MetadataAddress = string.Format(Globals.AadInstance, Globals.TenantName, policy),
            AuthenticationType = policy,
            AuthenticationMode = AuthenticationMode.Active,
            // These are standard OpenID Connect parameters, with values pulled from web.config
            ClientId = Globals.ClientIdForLogin,
            RedirectUri = Globals.RedirectUri,
            PostLogoutRedirectUri = Globals.RedirectUri,
            Notifications = new OpenIdConnectAuthenticationNotifications
            {
                AuthenticationFailed = AuthenticationFailed,
                SecurityTokenValidated = SecurityTokenValidated
            },
            Scope = "openid",
            ResponseType = "id_token",

            // This piece is optional - it is used for displaying the user's name in the navigation bar.
            TokenValidationParameters = new TokenValidationParameters
            {
                NameClaimType = "name",
            }
        };
    }

    private async Task SecurityTokenValidated(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> token)
    {
            var groups = await _metaDataService.GetGroups(token.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value);

            if (groups?.Value != null && groups.Value.Any())
            {
                foreach (IGroup group in groups.Value.ToList())
                {
                    token.AuthenticationTicket.Identity.AddClaim(
                        new Claim(ClaimTypes.Role, group.DisplayName, ClaimValueTypes.String, "GRAPH"));
                }
            }
    }

    // Used for avoiding yellow-screen-of-death
    private Task AuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
    {
        notification.HandleResponse();

        if (notification.Exception.Message == "access_denied")
        {
            notification.Response.Redirect("/");
        }
        else
        {
            notification.Response.Redirect("/error?message=" + notification.Exception.Message);
        }

        return Task.FromResult(0);
    }
}

My GetGroups method just queries the getMemberGroups method on the Users API

Then I have a simple helper method to determine whether the user is in a role:

public static bool UserIsInRole(IPrincipal user, string roleName)
{
    var claims = user.Identity as ClaimsIdentity;

    if (claims == null) return false;

    return claims.FindAll(x => x.Type == ClaimTypes.Role).Any(x => x.Value == roleName);
}