Closed justinyoo closed 8 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;
}
@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;
}
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
@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).
The graph api for office 365 Planner do not accept application accesss:
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.
@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
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.
@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?
have your application (I'm guessing a Web API?) process the customer activity using the OBO flow that is:
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-v2Use 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-
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.
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.
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
@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.
@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 ...
Adal works, proven and been working for year, leave it be.
If people need MSAL.Net features they will move :)
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
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 -
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.
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.
Hi, Team.
When I import this ADAL into my .NET Core library, it complains at the
UserPasswordCredential
class like: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 theUserPasswordCredential
in .NET Core?Cheers,