Skip to content
This repository has been archived by the owner on Jun 30, 2023. It is now read-only.

UserPasswordCredential doesn't support .NET Core #482

Closed
justinyoo opened this issue Jul 7, 2016 · 67 comments
Closed

UserPasswordCredential doesn't support .NET Core #482

justinyoo opened this issue Jul 7, 2016 · 67 comments

Comments

@justinyoo
Copy link

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,

justinyoo added a commit to aliencube/azure-activedirectory-library-for-dotnet that referenced this issue Jul 11, 2016
- so that UserCredential can ba overridden in .NET Core library, eg) UserPasswordCredential
- regarding to the issue AzureAD#482
@kpanwar
Copy link
Contributor

kpanwar commented Jul 25, 2016

This is not supported by design.

@kpanwar kpanwar closed this as completed Jul 25, 2016
@justinyoo
Copy link
Author

@kpanwar Thanks for getting back to me.

Is there any timeline that we can use UserPasswordCredential in .NET Core? I don't understand "not supported by design". Does that mean UserPasswordCredential won't support .NET Core? If not, could you provide a workaround?

@kpanwar
Copy link
Contributor

kpanwar commented Jul 25, 2016

Yes, we will not add support for UserPasswordCredential in .NET core. There are other ways to authenticate as well. See https://github.com/Azure-Samples/active-directory-dotnet-deviceprofile

@novogeek
Copy link

We have a daemon service that was authenticating to an API as a user and we were using UserPasswordCredential method to fetch access token (by passing username and password). But when we upgraded the solution to .Net Core (and the latest ADAL package), this has broken. If UserPasswordCredential is no more supported, what is the alternate approach to authenticate non-interactively for the aforementioned scenario?

@Squiggle
Copy link

Ditto. I'm trying to authenticate against a 3rd party API in an Azure WebJob, which was previously working fine using UserPasswordCredential.

What is now the recommended approach here?

@talynone
Copy link

So how would one write a headless client for lets say CRM dynamics which requires an Oath 2.0 authentication to Active Directory to access the web api in .NET core?

@psignoret
Copy link

psignoret commented Nov 22, 2016

The idea here is that for apps to collect and store a user's username and password is not the correct approach for non-interactive authentication. Here are some details on why you shouldn't do this:

When you have a process running with no user interaction whatsoever, you should refer to the "Application Identity with OAuth 2.0 Client Credentials Grant" case of the "Daemon or Server Application to Web API" scenario. The following ADAL constructs will obtain an access token based on the app's identity only, and authenticate with either a client secret, a client assertion (i.e. a signed JWT token), or a client certificate (which will be used to create a signed JWT token). The resulting access token will not identify a user, and is only suitable when the token is for apps/APIs that have defined app-only permissions:

  • AcquireTokenAsync(string resource, ClientCredential clientCredential)
  • AcquireTokenAsync(string resource, ClientAssertion clientAssertion)
  • AcquireTokenAsync(string resource, IClientAssertionCertificate clientCertificate)

In scenarios where user interaction is possible, you can use the AcquireToken constructs that will pop up an authentication prompt (if applicable to your platform), or obtain the authorization code on your own and using AcquireTokenByAuthorizationCode. In both cases, a real live human user is expected to be present during the initial token acquisition, which will take place through a browser-based sign-in page.

If your device or platform is incapable of displaying Azure AD's browser-based sign in page (e.g. a device with limited input capabilities), you can use the device flow that @kpanwar has linked to.

After the first token acquisition, your app can use the cached refresh token to get a new access token without any further user actions or interactions (as long as nothing has changed that might invalidate the refresh token).

@talynone
Copy link

talynone commented Nov 22, 2016

@psignoret My app works as an api proxy to the crm dynamics online web api (which is protected via AD oauth and application id). That's just the way web api in CRM works, I can't control its auth mechanism. The proxy is fully headless and has no concept or care of a logged in user. Other apps/services leverage this proxy api. There 100% no user interaction whatsoever in any way. Because of this I am forced to use the full .Net stack which supports this CRM web api headless login scenario. None of the links you provided have an acceptable alternative as they all rely on the concept of a logged in user. Obviously this was included in the full .net stack for a reason. This is the only feature holding back my entire solution from using .net core fully. It makes no sense to make the decision that .NET core never needs this and full stack may need it. If you're / Microsoft are so positive this scenario is never needed why is it in the full stack?

More details about this is documented here:

https://debajmecrm.com/2016/06/21/knowhow-authentication-with-dynamics-crm-online-web-api-without-user-login-screen-where-headless-authentication-works-and-where-not/

@Ro3A
Copy link

Ro3A commented Nov 22, 2016

I'd like to present another similar use case just to highlight the obvious need for headless auth / user password credntials.

We have a customer portal where our users (construction contractors) can view their quotes (stored in CRM) as well other other business related documents like invoices (Dynamics AX) etc. This portal talks to our backend Web API app service that is only privately accessible. This API needs to call out to CRM's Web API.

Our users have no clue about AD, they don't have Microsoft accounts, and barely remember their logins for our site let alone any other. If another login screen came up just to access their quote information, they'd run and never come back.

We are a single tenant that wants to display quote information to our customers and we control all access to that data. We have created a non-interactive service account that "should" handle this.

I've tried every single workflow I've found. Client Credentials with generated Key, User Password, pure OAuth without ADAL library and using an older version of ADAL that still had UserPasswordCredential.

The latter got me very close to successfully accessing the Web API via a fixed service account but received an error claiming consent had not been granted and I found no way to grant this consent.

I got in touch with @dstrockis at Azure-Samples/active-directory-dotnet-native-headless#12 (comment) who presented this solution to grant consent:

http://www.cloudidentity.com/blog/2016/10/04/provision-an-app-created-on-portal-azure-com-in-your-own-tenant/

All in all, like the others, I'm having to use the full framework and unfortunately the old OrganizationService to access data which I'm able to do very quickly and easily. I've not tried the solution mentioned above yet but it looks promising.

@psignoret
Copy link

@talynone:

Your scenario looks like a perfect candidate for the on-behalf-of flow (which is the only approach I didn't mention in my earlier comment). This flow is briefly described in the Daemon or Server Application to Web API scenario. It is an implementation of part of MS-OAPX (OAuth 2.0 Protocol Extensions), which is an extension to OAuth 2.0 (developed by Microsoft for AD FS).

To illustrate how this works, let's suppose you have the following:

  1. A client application that the end-user interacts with: ClientApp.
  2. A backend API used by your client app(s): BackendAPI (which has no user interface whatsoever, but is configured to use Azure AD for authentication).
  3. A third-party API that you have no control over, that requires user authentication via Azure AD: DynamicsAPI (which also has no user interface whatsoever).

With the on-behalf-of flow, the following would happen:

  1. The user signs in to ClientApp, by using any of the flows that include the user using the normal browser-based login flows. If the user needs to do MFA, provide consent, change their password, or any other sign-in interruption, they can do that. At the end of the sign-in, ClientApp will have an access tokenin the name of the authenticated user, destined for BackendAPI.
  2. ClientApp will present this access token to BackendAPI (e.g. in the Authorization HTTP header).
  3. BackendAPI will validate this token (e.g. check that its signature is good, that the claims are valid, etc.).
  4. Now, BackendAPI can't call DynamicsAPI and provide the same access token it was given (since it is destined for BackendAPI, and DynamicsAPI will reject it). Instead, BackendAPI needs to get a new access token, on behalf of the signed in user. To do this, BackendAPI makes an authenticated token request (meaning BackendAPI authenticates itself with a client secret or certificate) to Azure AD. This request includes:
    • An indication that the resulting access token should be for DynamicsAPI.
    • An indication that this is an "on-behalf-of" (OBO) request.
    • The original access token that BackendAPI received (remember, to get this token, ClientApp put the user through a sign-in, so it includes all the details about the user who signed in).
  5. If everything checks out, Azure AD will respond with an access token for DynamicsAPI, on behalf of the signed in user. BackendAPI can now call DynamicsAPI with that token.

Step 4, above, is very easily invoked with ADAL:

  • AcquireTokenAsync(string resource, ClientCredential clientCredential, UserAssertion userAssertion)

In the signature above, resource would be the ID of DynamicsAPI (e.g. "https://contoso.crm.dynamics.com"), clientCredential is how BackendAPI authenticates, and userAssertion is what contains the access token that ClientApp originally obtained for BackendAPI.

Here's a quick diagram illustrating the token and API requests happening:

image

You can see a working example of this in the active-directory-dotnet-webapi-onbehalfof sample. The on-behalf-of token request is executed in CallGraphAPIOnBehalfOfUser(). In that sample, one app (TodoListClient, a native client app) obtains an access token to a back-end API (TodoListService). TodoListService then uses that access token (which identified the authenticated user) to obtain an new access token to a different API (the Azure AD Graph API).

@Ro3A
Copy link

Ro3A commented Nov 24, 2016

@talynone did not specify if his Web API was authenticated using AD or not. However, in my case, our end users are NOT registered in AD. We use AspNet Identity for our client, and the Web API is "internal only" protected via networking rather than user authentication.

If the Web API proxy is not AD integrated and the end users themselves are not AD users, I'm fairly certain this flow does not work. Is that accurate?

@psignoret
Copy link

@Ro3A hold on, I haven't addressed your scenario. I can't type that fast. 😄.

@psignoret
Copy link

psignoret commented Nov 24, 2016

Edit (2017-04-08): This response mentions several times that Dynamics 365 (online) does not expose app-only permissions. This is only partly true, and it is possible to call Dynamics 365 with only a an application identity. See comments below for more details. I'm leaving the bulk of this response intact, because it is the case of some services/APIs.

@Ro3A:

Though I encourage you to read through my last response to @talynone, it's not going to help you, since the end-users accessing your app are not necessarily users that can sign in to your Azure AD tenant, let alone users that themselves have permissions to Dynamics CRM Online. (And @talynone, if this is also the case for you, then you should also read this.)

@dstrockis' link to @vibronet's blog post with the "consent invoker" tool will indeed work. (I just tried it myself.)

The root of your issue is that the resource that you are trying to access, Dynamics CRM Online, itself does not support the concept of app-only access. It always expects all requests will come from an authenticated user (directly or indirectly as with the on-behalf-of flow). You can see in the image below that the only permissions exposed are delegated permissions, which assume/require an authenticated user.

Dynamics CRM Online only exposes delegated permissions

So, let's walk through your options so far:

  • Use a browser-based interface to authenticate your users. Can't because this a backend API.
  • Use app-only authentication. Can't because Dynamics CRM Online only exposes delegated permissions.
  • Use delegated permissions with the on-behalf-of flow: Can't because end-users don't sign in with Azure AD, and themselves don't have access to Dynamics CRM Online.

Edit (2017-04-08): You can use app-only authentication to Dynamics 365 (online). If you're here because of some other resource which doesn't have a work-around like Dynamics does, then continue to read...

Since we've exhausted all other options, then I agree that your only choice here is what you're trying to do. This option was removed from ADAL for .NET Core to strongly discourage transporting and persisting user credentials. However, the service (Azure AD) does support the Resource Owner Password Credentials Grant OAuth 2.0 flow, so you can skip using ADAL altogether for getting the token (though of course, you lose all the advantages of ADAL, particularly the token cache). You will need to make the following POST request (line breaks for display only):

POST https://login.microsoftonline.com/{tenant-id}/oauth2/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

grant_type=password
&resource={resource}
&username={username}
&password={password}
&client_id={client-id}

This request can be made with a simple HttpClient (make sure you do it right). The response will include the access token which you can use to access Dynamics CRM Online.

You would, of course, ensure that the user account that is being used as a "service account" has a complex password, that the password is stored securely (e.g. in Key Vault), that you are continuously rotating this password, and that the account has the least amount of permissions it needs (not just to CRM Online, but to everything else).

I can't say this enough: Only use this approach if you really, truly can't do what you need in any other way. "It's too hard", or "I'm in a hurry" are not a valid reasons for reducing the security of your systems and your data.

(Edited to remove reference invoking the Resource Owner Password Credential Grant flow as a confidential client, leaving it only as it would be invoked by a public client.)

@kpanwar
Copy link
Contributor

kpanwar commented Nov 24, 2016

@psignoret - resource owner grant for confidential clients was exposed as an error. We are going to disable that feature soon.

@psignoret
Copy link

Ah, thanks @kpanwar. Updated to show instead how a public client would invoke it.

@Ro3A
Copy link

Ro3A commented Nov 24, 2016

So all that said, since what you're recommending is a last resort (and nearly a hack since the library doesn't support it), one must assume this use case is very small. From what I've seen across dozens of stack overflow articles and blogs, I believe it's a much larger use case and warrants a better solution.

In my case, it seems like AspNet Identity is a no go if the app needs access to AD protected resources. So in the long run, am I better off integrating our app with AD B2C and using the on-behalf-of flow? Even then, I'm not even sure B2C solution would work for us since we have user related tables in our Identity database. Can B2C store user related data?

@gsacavdm
Copy link
Contributor

gsacavdm commented Dec 3, 2016

@Ro3A, B2C does support storing user related data.
Here's a link that explains how you'd do this:
https://docs.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-reference-custom-attr

@Ro3A
Copy link

Ro3A commented Dec 3, 2016

I'll look into moving that direction then. Thanks for the guidance on this thread guys.

@psignoret
Copy link

@Ro3A: Careful though, you can't sign up for services such as CRM Online with an Azure AD B2C tenant. So though you can (and probably should) use Azure AD B2C as the identity service for your application, you won't be able to use the on-behalf-of flow to get access to the CRM Online API in the context of these users. (Because CRM Online won't recognize the user as a user who has permissions in CRM Online.)

@Ro3A
Copy link

Ro3A commented Dec 5, 2016

That negates the reason to switch then because we'd be back at square one. I'm hoping you guys see the frustration at this point. This is definitely the most trouble I've ever had interacting with an API especially when the older version of the service is a breeze to connect to.

@psignoret
Copy link

@Ro3A: I do see your frustration. Like I said above, unfortunately this is a side-effect of the fact that CRM Online service itself does not currently support the concept of app-only API access, which is what your scenario calls for.

Each API or service that is secured by Azure AD declares which permissions it exposes, and whether these permissions are "app-only", or "app+user" permissions. The permissions will generally mirror the service's own authorization strategy. Since, CRM Online currently only exposes app+user permissions, this is a likely indication that the CRM Online service itself also only supports doing authorization based on a user identity.

The Resource Owner Password Credentials flow I mentioned above is a supported flow. Support for this was not included in ADAL v3 for .NET Core because it often leads to insecure practices (e.g. hard-coding username/password in a PowerShell script, etc.), but the flow itself is supported by the Azure AD service.

(I'll inquire with the Dynamic CRM team if there are any plans to support your scenario in a manner that does not require you to use a "service" user account.)

@justin-cook
Copy link

This post was written a couple days ago http://phuocle.net/crm/dynamics-365-online-s2s-authentication-full-explain.aspx and says he got it Dynamics CRM API working using client credentials flow.

@psignoret
Copy link

@justin-cook Thanks! Indeed, it looks like Dynamics 365 (online) now provides a mechanism to associate an "application user" (and that user's privileges within Dynamics 365) with an app ID. The client can then request an app-only access token to Dynamics 365, and Dynamics 365 will apply authorization based on it's own table (of application user to app ID), rather than OAuth 2.0 scopes and permissions.

This approach, while perfectly valid, is particular to Dynamics 365, so I'll update my comments above to clarify with the people of the future that there is a way to do this with Dynamics 365.

@osca2000
Copy link

I like to have finger print authentication for my universal app. how can I do that without UserPasswordCredential? Most iPhone banking apps seem to have pwd and id saved but you are saying 'not recommended'. Can you add the feature to .net core and developers will decide if it is used? 'cached refresh token' may work with finger print authentication (not really authentication but lauching)?

@markolb81
Copy link

markolb81 commented Oct 5, 2018

Thanks for the idea @jmprieur , but MSAL.NET only supports User/Password for .NET Framework. Unfortunately I'm forced to use Azure Functions V2 which runs on .NET Standard 2.0.

@ryanshane
Copy link

ryanshane commented Oct 10, 2018

Some options tested:

  • Option 1: Disassemble & fiddler trace what AcquireTokenAsync is doing and replicate it so it can be done in .NET Core. This has been done -- code sample in my comment below.
  • Option 2: the example mentioned above mentions making a call to get a temporary code, then asking the user to open http://aka.ms/devicelogin on their device and enter that code, then sign into their account, at which time your application is then able to get the token using AcquireTokenByDeviceCodeAsync. You could programmatically perform these steps on behalf of the user, but would have to base it on how the devicelogin page works. MS may change that page or how the login screen works and your app would break.
  • Option 3: create an azure function or API using .NET to use the missing class to get a token. You would have to call that function and send it the username and password. It's over HTTPS so that should be fine but anyone who has access to the function can easily insert something to harvest accounts. There's no guarantee that MS may in future view this as suspicious (a session token used somewhere else other than where it was obtained)

@markolb81
Copy link

Hi ryanshane, thanks for the answer. Your option 3 is the way to go for me now - I already hat that Azure function anyway, so I acquire the token with a raw http-Request to https://login.microsoftonline.com/{TENANT_ID}/oauth2/token which still accepts the request parameters "resource={RESOURCE_URI}&client_id={CLIENT_ID}&grant_type=password&username={USERNAME}&password={PASSWORD}". This way, I do not use any library at all.

So, for the moment I am fine, but I understood that it's not very elegant. If there's any possibility to get rid of a service user (and therefore stored credentials), I would do it. Although I think I have a good overview about the possibilities, I am convinced that this is (for now) the one and only feasible solution for my use case. To explain, I will describe it here shortly if anyone is interested:

We developed a touchscreen device, allowing the user to book a meeting room via speech recognition on behalf of the user that is being recognized via face recognition. The touchscreen runs an UWP-App in "kiosk-mode" and is publicly usable by any employee. The user interface is restricted to simple touch input (even on-screen keyboard is not an option), face- and voice recognition. To actually access information, the touchscreen accesses an Azure Function which is used as a Proxy to the MS Graph and an SQL Database. Therefore one might understand, that even authentication by remote device (oauth device profile flow) is not feasible, because it will totally blow up the user experience.

The easiest technical solution is using app-only permissions (Calendars.ReadWrite), but since they are not granular and immediately give access to all private meetings, I consider that a no-go, even if I manually filter out sensitive information within the proxy azure function.

So, the only way to go is operating in a user-context, and since login by the actual user is not possible, a service user is the way to go. That, indeed, causes the next issue when trying to create a meeting on behalf of another user: It requires that every user in the organization which potentially uses that device creates a delegate permission on his calendar to that service user. Let's see if our IT dept. will accept a powershell solution here :)

Thanks however for your hints and help!

@jmprieur
Copy link
Contributor

@markolb81 : Is it an option for you to migrate to MSAL.NET 2.x? it supports U/P in .NET Core?

@ryanshane
Copy link

Hey @markolb81 can you elaborate on that endpoint you're using? It only accepts POST? I tried putting the parameters in the posted data but receive 400 bad request.

@markolb81
Copy link

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
Copy link

ryanshane commented Oct 16, 2018

@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
Copy link

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
Copy link

@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
Copy link

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
Copy link
Contributor

@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
Copy link

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
Copy link
Contributor

@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
Copy link

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
Copy link
Contributor

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
Copy link

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
Copy link

MichelZ commented Dec 14, 2018

@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
Copy link
Contributor

@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
Copy link

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

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

@jmprieur
Copy link
Contributor

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
Copy link

ShijuSamuel commented Sep 13, 2019

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
Copy link
Contributor

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.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests