ADAL .Net Core nuget包不支持UserPasswordCredential。

16
在ADAL.Net 3.x中,在2.x的UserCredential的基础上引入了UserPasswordCredential。但是在相同的NuGet包下,.Net Core中没有暴露相同的UserPasswordCredential?
UserCredential类只有一个属性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; }
    }
}

由于 .Net Core 中不支持 UserPasswordCredential,而 UserCredential 只接受一个参数用户名,那么如何在 .Net Core 中输入用户密码并实现以下代码?

authContext.AcquireTokenAsync(WebAPIResourceId, ClientId, userPasswordCredential);

我正在特定使用 ADAL 3.13.4 版本在 .Net Core 1.0 版本中


在相同的包和版本下,UserPasswordCredential 在 .NET Core 中不可用。 - racha
4个回答

17

为了使用资源所有者密码凭据授权流来获取Azure AD的访问令牌,我们可以直接使用HttpClient调用HTTP请求。以下是一个示例供您参考:

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>();

1
似乎还需要client_secret。 - Russell at ISC
1
这取决于应用程序的类型。在这种情况下,Web应用程序需要client_secret。 - Fei Xue - MSFT
这对我有用。正如@FeiXue-MSFT所提到的,需要注意的是您使用的client_id必须在Azure中配置为本机客户端应用程序。 - mellis481
@JamesR。资源所有者密码凭证授权流需要使用令牌端点而不是授权端点。您可以在我的帖子中查看确切的端点。 - Fei Xue - MSFT
@FeiXue - 你能否回答MAK的问题?我也遇到了同样的问题。MAK - 我怎么知道我是否正在使用联合账户? - Vijay V
显示剩余4条评论

8
你是正确的,UserPasswordCredential 不适用于 .NET Core,并且UserCredential不再接受用户名和密码。这意味着ADAL v3在.NET Core上不支持用户名/密码流程。

2
错误已关闭,并附有评论"这不是设计支持的功能。" - oderibas
@PhilippeSignoret 那么,在“MSAL.NET”中有什么替代方案呢?例如,我曾经使用过这个代码来获取令牌。但由于我正在使用“.NET Core 3.1”和“MSAL.NET”,所以我不能使用该代码,因为它使用了“ADAL.NET”的UserPasswordCredential - nam
你真的不应该使用用户名/密码流程。如果出于某种原因,你确实认为需要这样做:https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/Username-Password-Authentication - Philippe Signoret

2

以下是我一直在做的解决此问题的方法。我在.NET Core中复制了相同的行为,用于静态方法,因为缺少UserPasswordCredential类。这是基于当UserPasswordCredential类在.NET版本中使用时发生的fiddler跟踪。由于.NET DLL似乎已被混淆,因此这是捕获其操作的最佳尝试。

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;
}

我尝试使用这种方法,但是响应中缺少了federation_metadata_urlfederation_active_auth_url,而它们应该在那里。我得到了以下JSON:{"ver":"1.0","account_type":"Managed","domain_name":"mydomain.onmicrosoft.com","cloud_instance_name":"microsoftonline.com","cloud_audience_urn":"urn:federation:MicrosoftOnline"}。你有任何想法出了什么问题吗? - Alexey Merson
当您登录portal.azure.com (https://sts...)时,通常会重定向到哪个URL?尝试硬编码它(federation_active_auth_url)以查看是否有效(例如,类似于“https://sts.{domain}.com.au/adfs/...”,因此创建一个这样的硬编码字符串:“https://sts.{domain}.com.au/adfs/services/trust/2005/usernamemixed”)?您可以注释掉使用federation_metadata_url的部分,因为此查询的响应不会被馈送到任何内容中。我猜测您的租户似乎没有关联域名,因此可能会默认为通用的MS sts页面。 - Shane

0

快进到2020年,使用ADAL 3.19.8,您应该能够使用ClientCredential类进行AAD身份验证。当我与D365 CRM Web API集成时,它对我有效。我在以下博客文章中记录了我的经验。希望您会发现它有用。


问题是关于如何使用用户名/密码,例如资源所有者密码凭据流,而不是客户端凭据流。 - Adi Unnithan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接