1
votes

I recently got started working with B2C. I managed to get their sample application/API using MSAL and an API working with my own tenant.

Now I wanted to:

  1. Figure out how I can run this sample without using an API. The sample uses Scopes to get read/write access to the API. If I remove the references to the API from the app, it no longer works. Surely there should be some way to authenticate to B2C without requiring an API? This is not really important to my application but I'm mostly curious if the webservice HAS to be there as part of the auth-process?
  2. Communicate with Graph Api (Windows or Microsoft Graph?). The sample MS provides uses ADAL and some console application. I cannot find a sample that uses MSAL, so I am having trouble incorporating it into my own application. Is it now possible to call Graph API using MSAL? If it is, is there some documentation on how to do this somewhere?

I tried simply following the docs above and registering an app/granting it permissions. Then putting the client ID/key into my own application (the MSAL one from the first sample), but then I just get a message from B2C that looks like:

Correlation ID: 01040e7b-846c-4f81-9a0f-ff515fd00398 Timestamp: 2018-01-30 10:55:37Z AADB2C90068: The provided application with ID '9cd938c6-d3ed-4146-aee5-a661cd7d984b' is not valid against this service. Please use an application created via the B2C portal and try again.

It's true that it's not registered via the B2C portal, but that is what the instructions say; to register it in the B2C tenant under App Registrations, not the B2c portal.

The Startup class where all the magic happens looks like:

public partial class Startup
{

    // App config settings
    public static string ClientId = ConfigurationManager.AppSettings["ida:ClientId"];
    public static string ClientSecret = ConfigurationManager.AppSettings["ida:ClientSecret"];
    public static string AadInstance = ConfigurationManager.AppSettings["ida:AadInstance"];
    public static string Tenant = ConfigurationManager.AppSettings["ida:Tenant"];
    public static string RedirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
    public static string ServiceUrl = ConfigurationManager.AppSettings["api:TaskServiceUrl"];

    public static string ApiIdentifier = ConfigurationManager.AppSettings["api:ApiIdentifier"];
    public static string ReadTasksScope = ApiIdentifier + ConfigurationManager.AppSettings["api:ReadScope"];
    public static string WriteTasksScope = ApiIdentifier + ConfigurationManager.AppSettings["api:WriteScope"];
    public static string[] Scopes = new string[] { ReadTasksScope, WriteTasksScope };

    // B2C policy identifiers
    public static string SignUpSignInPolicyId = ConfigurationManager.AppSettings["ida:SignUpSignInPolicyId"];
    public static string EditProfilePolicyId = ConfigurationManager.AppSettings["ida:EditProfilePolicyId"];
    public static string ResetPasswordPolicyId = ConfigurationManager.AppSettings["ida:ResetPasswordPolicyId"];

    public static string DefaultPolicy = SignUpSignInPolicyId;

    // OWIN auth middleware constants
    public const string ObjectIdElement = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";

    // Authorities
    public static string Authority = String.Format(AadInstance, Tenant, DefaultPolicy);

    public void ConfigureAuth(IAppBuilder app)
    {
        app.UseCookieAuthentication(new CookieAuthenticationOptions());
        app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

        app.UseOpenIdConnectAuthentication(
            new OpenIdConnectAuthenticationOptions
            {
            // Generate the metadata address using the tenant and policy information
            MetadataAddress = String.Format(AadInstance, Tenant, DefaultPolicy),

            // These are standard OpenID Connect parameters, with values pulled from web.config
            ClientId = ClientId,
                RedirectUri = RedirectUri,
                PostLogoutRedirectUri = RedirectUri,

            // Specify the callbacks for each type of notifications
            Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    RedirectToIdentityProvider = OnRedirectToIdentityProvider,
                    AuthorizationCodeReceived = OnAuthorizationCodeReceived,
                    AuthenticationFailed = OnAuthenticationFailed,
                },

            // Specify the claims to validate
            TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = "name"
                },

            // Specify the scope by appending all of the scopes requested into one string (seperated by a blank space)
            Scope = $"{OpenIdConnectScopes.OpenId} {ReadTasksScope} {WriteTasksScope}"
            }
        );

    }

    /*
     *  On each call to Azure AD B2C, check if a policy (e.g. the profile edit or password reset policy) has been specified in the OWIN context.
     *  If so, use that policy when making the call. Also, don't request a code (since it won't be needed).
     */
    private Task OnRedirectToIdentityProvider(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
    {
        var policy = notification.OwinContext.Get<string>("Policy");

        if (!string.IsNullOrEmpty(policy) && !policy.Equals(DefaultPolicy))
        {
            notification.ProtocolMessage.Scope = OpenIdConnectScopes.OpenId;
            notification.ProtocolMessage.ResponseType = OpenIdConnectResponseTypes.IdToken;
            notification.ProtocolMessage.IssuerAddress = notification.ProtocolMessage.IssuerAddress.ToLower().Replace(DefaultPolicy.ToLower(), policy.ToLower());
        }

        return Task.FromResult(0);
    }

    /*
     * Catch any failures received by the authentication middleware and handle appropriately
     */
    private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
    {
        notification.HandleResponse();

        // Handle the error code that Azure AD B2C throws when trying to reset a password from the login page 
        // because password reset is not supported by a "sign-up or sign-in policy"
        if (notification.ProtocolMessage.ErrorDescription != null && notification.ProtocolMessage.ErrorDescription.Contains("AADB2C90118"))
        {
            // If the user clicked the reset password link, redirect to the reset password route
            notification.Response.Redirect("/Account/ResetPassword");
        }
        else if (notification.Exception.Message == "access_denied")
        {
            notification.Response.Redirect("/");
        }
        else
        {
            notification.Response.Redirect("/Home/Error?message=" + notification.Exception.Message);
        }

        return Task.FromResult(0);
    }


    /*
     * Callback function when an authorization code is received 
     */
    private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
    {
        // Extract the code from the response notification
        var code = notification.Code;

        string signedInUserID = notification.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
        TokenCache userTokenCache = new MSALSessionCache(signedInUserID, notification.OwinContext.Environment["System.Web.HttpContextBase"] as HttpContextBase).GetMsalCacheInstance();
        ConfidentialClientApplication cca = new ConfidentialClientApplication(ClientId, Authority, RedirectUri, new ClientCredential(ClientSecret), userTokenCache, null);
        try
        {
            AuthenticationResult result = await cca.AcquireTokenByAuthorizationCodeAsync(code, Scopes);
        }
        catch (Exception ex)
        {
            //TODO: Handle
            throw;
        }
    }

}

2

2 Answers

1
votes

With regards to #2, you can only use the Azure AD Graph API with an Azure AD B2C directory, as noted in the "Azure AD B2C: Use the Azure AD Graph API" article.

Here is how (which I have copied from a previous answer)...

Azure AD B2C issues tokens using the Azure AD v2.0 endpoint:

https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize
https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token

The Azure AD Graph API requires tokens that are issued using the Azure AD v1.0 endpoint:

https://login.microsoftonline.com/{tenant}/oauth2/token

At design-time:

  1. Register the web application using the Azure AD B2C portal.
  2. Register the web application using the Azure AD portal and grant the Read directory data permission.

At runtime:

  1. The web application redirects the end user to the Azure AD B2C v2.0 endpoint for sign-in. Azure AD B2C issues an ID token containing the user identifier.
  2. The web application acquires an access token from the Azure AD v1.0 endpoint using the application credentials that were created at design-time in step 2.
  3. The web application invokes the Azure AD Graph API, passing the user identifier that was received in step 1, with the access token that was issued in step 2, and queries the user object, etc.
0
votes

This answer is just addressing question #1, I am unsure on #2 other than it is not publicly documented as far as I know.

B2C does not require you to use an API/service when building your app. In the sample you're looking at it's using 2 libraries to do slightly different (but related) things.

First, it's using an OWIN middleware module to help facilitate the authentication piece. This helps your app identify who a user is, but does not authorize your app to do perform actions or access data on their behalf. This library will net you a session with the end user and basic information about them as well as a authorization code you can use later on.

The other library being used is MSAL. MSAL is a client library for token acquisition and management. In this case after the initial authentication takes place using the aforementioned middleware, MSAL will use the authorization code to get access and refresh tokens that your app can use to call APIs. This is able to take place without end user interaction because they would have already consented to the app (and you would've configured the permissions your app needed). MSAL then manages the refresh and caching of these tokens, and makes them accessible via AcquireTokenSilent().

In order to remove the API functionality from the sample app, you'd need to do a bit more than just remove the scope. Specifically, eliminating the code in the TaskController.cs that is trying to call APIs, remove most usage of MSAL, and likely a few more things. This sample implements a Web App only architecture (Warning: it's for Azure AD not Azure AD B2C. The code is very similar, but would require a bit of modification).