16

In ADAL.Net 3.x UserPasswordCredential is introduced on top of UserCredential from 2.x. But the same UserPasswordCredential is not exposed in the .Net Core under the same nuget package?

UserCredential class has only one property UserName

namespace Microsoft.IdentityModel.Clients.ActiveDirectory
{
    //
    // Summary:
    //     Credential used for integrated authentication on domain-joined machines.
    public class UserCredential
    {
        //
        // Summary:
        //     Constructor to create user credential. Using this constructor would imply integrated
        //     authentication with logged in user and it can only be used in domain joined scenarios.
        public UserCredential();
        //
        // Summary:
        //     Constructor to create credential with client id and secret
        //
        // Parameters:
        //   userName:
        //     Identifier of the user application requests token on behalf.
        public UserCredential(string userName);

        //
        // Summary:
        //     Gets identifier of the user.
        public string UserName { get; }
    }
}

Since UserPasswordCredential is not available in .NetCore and UserCredential takes only one parameter username, how to input the password of the user and implement below code in .Net Core?

authContext.AcquireTokenAsync(WebAPIResourceId, ClientId, userPasswordCredential);

I am using ADAL 3.13.4 version specifically in .Net Core 1.0 version

Philippe Signoret
  • 13,299
  • 1
  • 40
  • 58
racha
  • 161
  • 1
  • 5
  • Possible duplicate of [ADAL.NET v3 does not support AcquireToken with UserCredential?](http://stackoverflow.com/questions/37465949/adal-net-v3-does-not-support-acquiretoken-with-usercredential) – blowdart Sep 09 '16 at 14:05
  • UserPasswordCredential is not available in .NET Core under same package and same version – racha Sep 09 '16 at 16:20

4 Answers4

17

To use the resource owner password credentials grant flow to get the access token for Azure AD, we can call the http request diectly using the HttpClient. Here is an example for your reference :

HttpClient client = new HttpClient();
string tokenEndpoint = "https://login.microsoftonline.com/{tenantId}/oauth2/token";
var body = "resource={resourceUrl}&client_id={clientId}&grant_type=password&username={userName}&password={password}";
var stringContent = new StringContent(body, Encoding.UTF8, "application/x-www-form-urlencoded");

var result=await client.PostAsync(tokenEndpoint, stringContent).ContinueWith<string>((response) =>
{
    return response.Result.Content.ReadAsStringAsync().Result;
});

JObject jobject = JObject.Parse(result);

var token = jobject["access_token"].Value<string>();
Fei Xue
  • 14,369
  • 1
  • 19
  • 27
  • 1
    It appears that client_secret is also required – Russell at ISC Dec 28 '16 at 17:25
  • 1
    It depends on the type of application. The web application requires the client_secret in this scenario. – Fei Xue Dec 29 '16 at 07:36
  • Gotcha, thanks. Also worth noting that all values should be URI-encoded in the body. – Russell at ISC Dec 30 '16 at 14:16
  • This worked for me. As @FeiXue-MSFT mentioned, the caveat is that the `client_id` you use must be configured as a Native Client App in Azure. – mellis481 Jun 05 '17 at 13:40
  • I can't seem to make this work. No matter what I try to do with the request, I can't get the endpoint to return anything other than a web page asking for creds. My endpoint in this case is https://login.windows.net/common/oauth2/authorize/. – James R. May 21 '18 at 13:30
  • @JamesR. The resource owner password credential grant flow requires using the token endpoint instead of authorization endpoint. You can refer the exact endpoint in my post. – Fei Xue May 22 '18 at 02:45
  • @FeiXue, I've actually changed directions to get around the issue and I'm using regular old .NET Framework instead for now. I will try what you say when I get the chance though. Thanks for pointing that out! – James R. May 23 '18 at 13:05
  • @FeiXue This does not work for federated account. It always gives an error saying username or password is incorrect. is there a workaround for that? – MAK Nov 14 '18 at 13:13
  • @FeiXue - can you please provide an answer to MAK's question? I am getting the same issue. MAK - how do I know that I am using a federated account? – Vijay V Feb 25 '19 at 23:54
8

You are correct, UserPasswordCredential is not available for .NET Core, and UserCredential no longer accepts username and password. This means ADAL v3 does not support the username/password flow on .NET Core.

Philippe Signoret
  • 13,299
  • 1
  • 40
  • 58
  • 2
    And bug is closed with comment ["This is not supported by design."](https://github.com/AzureAD/azure-activedirectory-library-for-dotnet/issues/482) – oderibas Nov 22 '16 at 13:12
  • @PhilippeSignoret So what is an alternative in `MSAL.NET`? For example, I used to get a token using [this](https://stackoverflow.com/a/48140094/1232087) code. But since I'm using `.NET Core 3.1` and `MSAL.NET`, I can't use that code since that is using `UserPasswordCredential` from `ADAL.NET`. – nam Oct 11 '20 at 04:22
  • You really shouldn't be using username/password flows. If for some reason you really think you do need this: https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/Username-Password-Authentication – Philippe Signoret Oct 11 '20 at 11:07
2

Below is what i have been doing to get around this problem. I replicated the same behaviour in a static method for use in .NET Core, since the UserPasswordCredential class is missing. This is based on fiddler traces of what happens when the UserPasswordCredential class is used in the .NET version. Since the .NET DLL seems to be obfuscated, this is a best attempt at capturing what it does.

public const string Saml11Bearer = "urn:ietf:params:oauth:grant-type:saml1_1-bearer";
public const string Saml20Bearer = "urn:ietf:params:oauth:grant-type:saml2-bearer";
public const string JwtBearer = "urn:ietf:params:oauth:grant-type:jwt-bearer";

/// <summary>
/// Acquire an AAD authentication token silently for an AAD App (Native) with an AAD account
/// 
/// NOTE: This process was ported from the Microsoft.IdentityModel.Clients.ActiveDirectory's
///  AuthenticationContext.AcquireTokenAsync method, which can silently authenticate using the UserPasswordCredential class.
///  Since this class is missing from .NET Core, this method can be used to perform the same without any dependencies.
/// </summary>
/// <param name="user">AAD login</param>
/// <param name="pass">AAD pass</param>
/// <param name="tenantId">Tenant ID</param>
/// <param name="resourceUrl">Resource ID: the Azure app that will be accessed</param>
/// <param name="clientId">The Application ID of the calling app. This guid can be obtained from Azure Portal > app auth setup > Advanced Settings</param>
public static string GetAuthTokenForAADNativeApp(string user, SecureString pass, string tenantId, string resourceUrl, string clientId)
{
    string tokenForUser = string.Empty;
    string authority = "https://login.microsoftonline.com/" + tenantId; // The AD Authority used for login
    string clientRequestID = Guid.NewGuid().ToString();

    // Discover the preferred openid / oauth2 endpoint for the tenant (by authority)
    string api = "https://login.microsoftonline.com/common/discovery/instance?api-version=1.1&authorization_endpoint=" + authority + "/oauth2/authorize";
    string openIdPreferredNetwork = string.Empty;
    var client = new HttpClient();
    client.DefaultRequestHeaders.Clear();
    client.DefaultRequestHeaders.Add("client-request-id", clientRequestID);
    client.DefaultRequestHeaders.Add("return-client-request-id", "true");
    client.DefaultRequestHeaders.Add("Accept", "application/json");

    var responseTask = client.GetAsync(api);
    responseTask.Wait();
    if (responseTask.Result.Content != null)
    {
        var responseString = responseTask.Result.Content.ReadAsStringAsync();
        responseString.Wait();
        try
        {
            dynamic json = JObject.Parse(responseString.Result);
            openIdPreferredNetwork = json.metadata[0].preferred_network; // e.g. login.microsoftonline.com
        }
        catch { }
    }
    if (string.IsNullOrEmpty(openIdPreferredNetwork))
        openIdPreferredNetwork = "login.microsoftonline.com";

    // Get the federation metadata url & federation active auth url by user realm (by user domain)
    responseTask = client.GetAsync("https://" + openIdPreferredNetwork + "/common/userrealm/" + user + "?api-version=1.0");
    responseTask.Wait();
    string federation_metadata_url = string.Empty;
    string federation_active_auth_url = string.Empty;
    if (responseTask.Result.Content != null)
    {
        var responseString = responseTask.Result.Content.ReadAsStringAsync();
        responseString.Wait();
        try
        {
            dynamic json = JObject.Parse(responseString.Result);
            federation_metadata_url = json.federation_metadata_url; // e.g. https://sts.{domain}.com.au/adfs/services/trust/mex
            federation_active_auth_url = json.federation_active_auth_url; // e.g. https://sts.{domain}.com.au/adfs/services/trust/2005/usernamemixed
        }
        catch { }
    }
    if(string.IsNullOrEmpty(federation_metadata_url) || string.IsNullOrEmpty(federation_active_auth_url))
        return string.Empty;

    // Get federation metadata
    responseTask = client.GetAsync(federation_metadata_url);
    responseTask.Wait();
    string federationMetadataXml = null;
    if (responseTask.Result.Content != null)
    {
        var responseString = responseTask.Result.Content.ReadAsStringAsync();
        responseString.Wait();
        try
        {
            federationMetadataXml = responseString.Result;
        }
        catch { }
    }
    if (string.IsNullOrEmpty(federationMetadataXml))
        return string.Empty;

    // Post credential to the federation active auth URL
    string messageId = Guid.NewGuid().ToString("D").ToLower();
    string postData = @"
<s:Envelope xmlns:s='http://www.w3.org/2003/05/soap-envelope' xmlns:a='http://www.w3.org/2005/08/addressing' xmlns:u='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd'>
<s:Header>
<a:Action s:mustUnderstand='1'>http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action>
<a:MessageID>urn:uuid:" + messageId + @"</a:MessageID>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand='1'>" + federation_active_auth_url + @"</a:To>
<o:Security s:mustUnderstand='1' xmlns:o='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'>
<u:Timestamp u:Id='_0'>
<u:Created>" + DateTime.Now.ToString("o") + @"</u:Created>
<u:Expires>" + DateTime.Now.AddMinutes(10).ToString("o") + @"</u:Expires>
</u:Timestamp>
<o:UsernameToken u:Id='uuid-" + Guid.NewGuid().ToString("D").ToLower() + @"'>
<o:Username>" + user + @"</o:Username>
<o:Password>" + FromSecureString(pass) + @"</o:Password>
</o:UsernameToken>
</o:Security>
</s:Header>
<s:Body>
<trust:RequestSecurityToken xmlns:trust='http://schemas.xmlsoap.org/ws/2005/02/trust'>
<wsp:AppliesTo xmlns:wsp='http://schemas.xmlsoap.org/ws/2004/09/policy'>
<a:EndpointReference>
  <a:Address>urn:federation:MicrosoftOnline</a:Address>
</a:EndpointReference>
</wsp:AppliesTo>
<trust:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</trust:KeyType>
<trust:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</trust:RequestType>
</trust:RequestSecurityToken>
</s:Body>
</s:Envelope>";
    var content = new StringContent(postData, Encoding.UTF8, "application/soap+xml");
    client.DefaultRequestHeaders.Clear();
    client.DefaultRequestHeaders.Add("SOAPAction", "http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue");
    client.DefaultRequestHeaders.Add("client-request-id", clientRequestID);
    client.DefaultRequestHeaders.Add("return-client-request-id", "true");
    client.DefaultRequestHeaders.Add("Accept", "application/json");

    responseTask = client.PostAsync(federation_active_auth_url, content);
    responseTask.Wait();
    XmlDocument xml = new XmlDocument();
    string assertion = string.Empty;
    string grant_type = string.Empty;
    if (responseTask.Result.Content != null)
    {
        HttpResponseMessage rseponse = responseTask.Result;
        Task<string> responseContentTask = rseponse.Content.ReadAsStringAsync();
        responseContentTask.Wait();
        try { xml.LoadXml(responseContentTask.Result); }
        catch { }
        var nodeList = xml.GetElementsByTagName("saml:Assertion");
        if (nodeList.Count > 0)
        {
            assertion = nodeList[0].OuterXml;
            // The grant type depends on the assertion value returned previously <saml:Assertion MajorVersion="1" MinorVersion="1"...>
            grant_type = Saml11Bearer;
            string majorVersion = nodeList[0].Attributes["MajorVersion"] != null ? nodeList[0].Attributes["MajorVersion"].Value : string.Empty;
            if (majorVersion == "1")
                grant_type = Saml11Bearer;
            if (majorVersion == "2")
                grant_type = Saml20Bearer;
            else
                grant_type = Saml11Bearer; // Default to Saml11Bearer
        }
    }

    // Post to obtain an oauth2 token to for the resource 
    // (*) Pass in the assertion XML node encoded to base64 in the post, as is done here https://blogs.msdn.microsoft.com/azuredev/2018/01/22/accessing-the-power-bi-apis-in-a-federated-azure-ad-setup/
    UserAssertion ua = new UserAssertion(assertion, grant_type, Uri.EscapeDataString(user));
    UTF8Encoding encoding = new UTF8Encoding();
    Byte[] byteSource = encoding.GetBytes(ua.Assertion);
    string base64ua = Uri.EscapeDataString(Convert.ToBase64String(byteSource));
    postData = "resource={resourceUrl}&client_id={clientId}&grant_type={grantType}&assertion={assertion}&scope=openid"
        .Replace("{resourceUrl}", Uri.EscapeDataString(resourceUrl))
        .Replace("{clientId}", Uri.EscapeDataString(clientId))
        .Replace("{grantType}", Uri.EscapeDataString(grant_type))
        .Replace("{assertion}", base64ua);
    content = new StringContent(postData, Encoding.UTF8, "application/x-www-form-urlencoded");
    client.DefaultRequestHeaders.Clear();
    client.DefaultRequestHeaders.Add("client-request-id", clientRequestID);
    client.DefaultRequestHeaders.Add("return-client-request-id", "true");
    client.DefaultRequestHeaders.Add("Accept", "application/json");

    responseTask = client.PostAsync("https://" + openIdPreferredNetwork + "/common/oauth2/token", content);
    responseTask.Wait();
    if (responseTask.Result.Content != null)
    {
        var responseString = responseTask.Result.Content.ReadAsStringAsync();
        responseString.Wait();
        try
        {
            dynamic json = JObject.Parse(responseString.Result);
            tokenForUser = json.access_token;
        }
        catch { }
    }
    if (string.IsNullOrEmpty(federationMetadataXml))
        return string.Empty;


    return tokenForUser;
}

private static string FromSecureString(SecureString value)
{
    string stringBSTR;
    IntPtr bSTR = Marshal.SecureStringToBSTR(value);
    if (bSTR == IntPtr.Zero)
    {
        return string.Empty;
    }
    try
    {
        stringBSTR = Marshal.PtrToStringBSTR(bSTR);
    }
    finally
    {
        Marshal.FreeBSTR(bSTR);
    }
    return stringBSTR;
}
Shane
  • 303
  • 2
  • 9
  • I tried to use this method but `federation_metadata_url` and `federation_active_auth_url` are missing from response where they expected to be. I get the following json: `{"ver":"1.0","account_type":"Managed","domain_name":"mydomain.onmicrosoft.com","cloud_instance_name":"microsoftonline.com","cloud_audience_urn":"urn:federation:MicrosoftOnline"}`. Do you have any idea what's wrong? – Alexey Merson Feb 17 '19 at 20:56
  • What URL are you normally redirected to when logging into portal.azure.com (https://sts...)? Try hard coding it (federation_active_auth_url) to see if it works (e.g. would be something like "https://sts.{domain}.com.au/adfs/...", so create a hard coded string like this: "https://sts.{domain}.com.au/adfs/services/trust/2005/usernamemixed")? You can comment out the parts that use federation_metadata_url, since the response from this query doesn't get fed into anything. My guess is it looks like your tenant doesn't have a domain name associated, so probably would default to a generic MS sts page. – Shane Feb 19 '19 at 02:02
0

Fast forward to 2020, with ADAL 3.19.8, you should be able to use the ClientCredential class for AAD authentication. It is working for me when integrating with a D365 CRM web API. I documented my experience in the following blog post. Hope you will find it useful.

Kelvin
  • 483
  • 4
  • 13
  • The question was asking how to use the username/password, e.g. [resource owner password credential flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc). Not the client credential flow. – Adi Unnithan Sep 11 '20 at 04:49