AzureAD / azure-activedirectory-library-for-dotnet

ADAL authentication libraries for .net
http://aka.ms/aaddev
MIT License
358 stars 214 forks source link

UserPasswordCredential doesn't support .NET Core #482

Closed justinyoo closed 8 years ago

justinyoo commented 8 years ago

Hi, Team.

When I import this ADAL into my .NET Core library, it complains at the UserPasswordCredential class like:

image

It doesn't seem that I can create a custom class inheriting the UserCredential class for workaround. Is there any suggestion that I can use the UserPasswordCredential in .NET Core?

Cheers,

markolb81 commented 6 years ago

Maybe it helps if I just post the function I'm using:

public static async Task<string> AcquireTokenAsync(string appId, string tenantId, string username, string password)
{
    HttpClient client = new HttpClient();
    string tokenEndpoint = string.Format("https://login.microsoftonline.com/{0}/oauth2/token", tenantId);

    var body = $"resource={GRAPH_BASE_URL}&client_id={appId}&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>();
    return token;
}
ryanshane commented 6 years ago

@markolb81 Further to my previous comment, i have done Option 1 by porting this authentication process to a method that can produce the auth token silently in .NET Core without any dependence on the missing UserPasswordCredential class.

Please let me know if you spot any potential issues.

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;
}
macosta3 commented 5 years ago

Hey @psignoret I found your comment really helpful to develop the On-Behalf-Of scenario. It's 2018 and applied it to some ASP NET CORE project and it works fine!! but only when running in IIS Express :(

When deploying our app to an instance of IIS and invoking a call to some service using the on behalf-of we get below error:

System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception. ---> System.Security.Authentication.AuthenticationException: The remote certificate is invalid according to the validation procedure.

This is odd because as per log file we are getting the Bearer token fine but error happens when executing AsyncPost on the HttpClient

var response = await _httpClient.PostAsync(url, content, cancellationToken).ConfigureAwait(false);

Just would like to know if anyone has come across with same Issue in same scenario and if you know what kind of configuration may be missing here.

Thanks in advance

psignoret commented 5 years ago

@macosta3 Glad to hear this was helpful! It sounds like your issue is completely unrelated to the ADAL library, so I suggest asking this in a more targeted forum to .NET and IIS (e.g. on StackOverflow).

pksorensen commented 5 years ago

The graph api for office 365 Planner do not accept application accesss: image

So what is the advice when you really need to do headless access?

I am all interested in doing it right, but i just dont see a solution other then usename/password and using httpclient due to the removal of classes in .net core.

jmprieur commented 5 years ago

@pksorensen, what is your scenario? is it a Web app? Here is a page showing what you should use depending on your kind of app: scenarios

pksorensen commented 5 years ago

Its an internal application that will opperate based on customer activity in our product.

The application will update office 365 planner tasks to the team. But the api do not allow application accesss and only delegated work. But none of us (the people working in the organization) is signed in, the application is working headless.

Ofcause when we start the company, i could manually use a device code and authorize the application with my credentials by opening the devicelogin page. but then at some point it will break and need to reauth and things will stop working if i dont write code that handle this and put things on a queue until its again resigned in.

If office 365 planner supported application permissions, then the normal approach would be: 1) Create application secret 2) put in keyvault 3 Authenticate the application using its client credentials. (We all agree that the above flow is secure).

I am going to argument that 1) Create a user in office 365 2) put its password in keyvault 3) sign the application in with this password. It gives the exact same thing. I fail to see any arguments why that would be less secure than client credentials. it is all just a secret.

jmprieur commented 5 years ago

@pksorensen I think that the reason why planner does not allow application access, is it accesses data on behalf of a given user (with access controlled on the planner side), and does not allow to access data for it the app itself (that is all user's data).

I get your point for your app operating based on customer activity. Here are a few possible things to try:

Did you try the following?

  1. have your application (I'm guessing a Web API?) process the customer activity using the OBO flow that is:

    • you would call your Web API once (from a Web App or a desktop app, even device code flow), with an access token you would get for this Web API and the signed-in user.
    • the Web API would cache the access token that it would have got using the on-behalf-of flow.
    • then the access token would be in the cache - along with a refresh token. This means that whenever customer activity happens, the Web API can access planner on behalf of a user - using AcquireTokenSilentAsync (which does the necessary refresh). This is illustrated with ADAL.NET's sample: https://github.com/Azure-Samples/active-directory-dotnet-webapi-onbehalfof Here is the same even better with aspnet.core 2.1, (but for MSAL.NET): https://github.com/Azure-Samples/active-directory-aspnetcore-webapi-tutorial-v2
  2. Use MSAL.NET (instead of ADAL.NET). It supports username/password in .NET core (but this approach, although simpler in appearance is very discouraged, as what happens when the password changes?). See https://aka.ms/msal-net-up

In any case, I'd advise everybody, at that point to update to MSAL.NET. See https://github.com/AzureAD/azure-activedirectory-library-for-dotnet/wiki/adalnet4-0-preview#do-you-want-to-migrate-from-adalnet-to-msalnet-

pksorensen commented 5 years ago

Its our company, dont we get to decide if our apps can access our planer data :)

The signed in user is not related to Azure AD in any way. Signed in users are our customers that do not use office365 and we dont want them to see anything related to azure ad/office 365.

So we are back at, it is not less secure to use a password flow over client flow and decided to do the protocol requests ourself (with out any caching).

Password resets, its all the same as when an application key expires (except there is not "rolling keys" feature). The admin can also go and wipe keys/passwords for application credentials as they could four the user id :)

One last comment would be, that due to you guys trying to make us not make stupid mistakes when using the password flow, you actually is pushing us to do mistakes when we implement our own protocol messages with httpclient, think about that :)

Thanks for providing all the resources / links and trying to make us do the right thing.

jmprieur commented 5 years ago

Thanks for the context, @pksorensen I'm confused now if the signed-in user is not related to Azure AD, of why you'd want to use ADAL.

password reset was an example, and the links explains others (MFA etc...)

I'm well aware that not implementing U/P pushes you to re-implement it, which is really not a good idea given all the aspects to take into account. That's the reason why MSAL.NET implements it in .NET Framework and .NET core. For ADAL.NET: we don't want to invest more in ADAL.NET, the future being MSAL. We just want to let you update to MSAL to your own pace. Given your requirement it's probably the right moment for you, and we are committed to helping you in this update.

pksorensen commented 5 years ago

I was using ADAL to authorize the dummy user / application when supported to talk with graph api (to alter groups, plans and such). Everything for our internal opperations

MichelZ commented 5 years ago

@jmprieur I think it would be a good idea to deprecate this library for new development (if it isn't already), and make a prominent notice at least on the readme page, possibly on the nuget description, too? Would help stop confusion with this.

jmprieur commented 5 years ago

@MichelZ : we are working hard on moving MSAL.NET to General Availability (GA). Until this is done we cannot really deprecate ADAL.NET (would most people consume a preview nuget package). This is coming, though ...

pksorensen commented 5 years ago

Adal works, proven and been working for year, leave it be.

If people need MSAL.Net features they will move :)

jmprieur commented 5 years ago

Indeed, @pksorensen : we are not going to remove ADAL.NET. As I wrote, people will update if they want at their own pace. But we are not going to add more features in ADAL.NET than there are already

ShijuSamuel commented 5 years ago

When the standard list "Resource Owner Credential Flow" why the ADAL doesn't support this? AAD implements the standard but ADAL decides against it. I can see multiple scenarios where this flow is mandatory going with workarounds have more security implications. Let me list two -

  1. Open AAD App Registration Pane for an Application. If you want to add Owner access to another application can you do that? In that case I have to add a service account, probably I want to manage my key rotation through Graph.
  2. Graph Calendar API, I want to write to a shared calendar between a specific user and application. Oh I cannot share a calendar with an application but I can create a service account and act on behalf of application. Now, you can give OAuth2 Client application but that will expose every users calendar.

I would say not allowing developers to use this flow has more security implications by they implementing it via http or resorting to other workarounds. I don't buy this design decision.

jmprieur commented 5 years ago

Hello @ShijuSamuel ADAL does support ROPC in .NET Framework, but not .NET Core. We are not doing any addition any longer in ADAL which was now superceded by MSAL.NET, which is where all the innovatio happens.

Therefore we recommend that you move to MSAL.NET which supports ROPC in all platforms: https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-desktop-acquire-token#username--password

BTW if you want to access the Microsoft Graph you would have to use MSAL, not ADAL.