How to silently get access token to user subscription Azure Batch? - azure

i am working on project, where we have service that run computation on Azure Batch in user subscription mode (because we are using custom image). I have now my code fully working, but it requires every launch to provide user credentials to log into Azure Active Directory app before it can create Batch pools and so on. Because it will run as background service, i need to log in silently with some provided user without popup asking user to log in.
I have registered native app in Azure and set its access to Azure Batch service, created Azure AD user, and got all ids and names from it.
Here is my code i am using now.
private const string AuthorityUri = "https://login.microsoftonline.com/common";
private const string BatchResourceUri = "https://batch.core.windows.net";
private const string BatchAccountEndpoint = "https://<BATCH SERVICE NAME>.westeurope.batch.azure.com";
private const string ClientId = "<AZURE APP GUID ID>";
...
public static async Task<string> GetAuthenticationTokenAsync()
{
var authContext = new AuthenticationContext(AuthorityUri);
//here it will throw exception about no token found in cache and to call AquireToken
var authResult = await authContext.AcquireTokenSilentAsync(BatchResourceUri, ClientId, new UserIdentifier("<AD USER GUID ID>", UserIdentifierType.UniqueId));
//this works fine, but show popup dialog for login
/*var authResult = await authContext.AcquireTokenAsync(BatchResourceUri,
ClientId,
new Uri(RedirectUri),
new PlatformParameters(PromptBehavior.Auto));*/
return authResult.AccessToken;
}
...
Func<Task<string>> tokenProvider = () => GetAuthenticationTokenAsync();
using (BatchClient batchClient = await BatchClient.OpenAsync(new BatchTokenCredentials(BatchAccountEndpoint, tokenProvider)))
{
...
}
Classic way with AquireToken with popup for login is working fine. I have tried to use AquireTokenSilent (as is shown in code), but i am getting error about no token cache and need to call AquireToken.
Id used in UserIdentifier is user id guid taken from Azure Active Directory user blade.
Does anybody know, how to update my code so i will be able to silently log into Azure Batch with specified user and is this even possible?
Thanks for help.

AcquireTokenSilent is not meant for this use case. It will try to get the token from the cache where it was previously stored by AcquireTokenAsync.
And AcquireTokenAsync will pop up a login dialog, so you can't use that in your batch app either.
Take a look at either authenticating with a certificate or with username/password.
In the first sample, you need to create a ClientAssertionCertificate with
certCred = new ClientAssertionCertificate(clientId, cert);
this is then used for AcquireTokenAsync:
result = await authContext.AcquireTokenAsync(todoListResourceId, certCred);
The other sample creates a UserPasswordCredential with
var uc = new UserPasswordCredential(user, password);
and then also uses it with AcquireTokenAsync in a slightly different way:
authContext.AcquireTokenAsync(todoListResourceId, clientId, uc);
There are some limitations as to what you can do with the tokens that are based on the two different authentication methods. For example, using the access token for EWS Impersonation requires using the certificate method.

Related

Accessing Google Drive through Azure Function

The task is to download google sheet in excel format and store it in Azure blob storage on timely basics using the Azure time trigger function.
Access Method to users google drive - OAuth Client ID.
I have created an Azure function locally and it works fine as expected and performs the task but when I deploy azure function I get this error.
Code for DriveService where the error occurs according to stack trace when deployed
public string[] Scopes = { DriveService.Scope.Drive, DriveService.Scope.DriveReadonly };
public DriveService GetService()
{
UserCredential _credential;
//Error Occurs at line below
Google.Apis.Auth.OAuth2.Flows.GoogleAuthorizationCodeFlow googleAuthFlow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer()
{
ClientSecrets = new ClientSecrets
{
ClientId = _config[Constant.ClientId],
ClientSecret = _config[Constant.ClientSecret],
}
});
string FilePath = Path.GetDirectoryName(_driveCredentialsPath);
_credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
googleAuthFlow.ClientSecrets,
Scopes,
"user",
CancellationToken.None,
new FileDataStore(FilePath, true)).Result;
DriveService service = new DriveService(new BaseClientService.Initializer()
{
HttpClientInitializer = _credential,
ApplicationName = Constant.ApplicationName,
});
return service;
}
I think there are two situations where it can go wrong but I am not sure about it.
When I am running the application locally a consent screen appears and gives permission to access the drive.
When this same function is running on azure who and how it will grant permission to access the drive.
I have provided my Azure App URL on Google OAuth Consent Screen as mentioned below to overcome this situation.
When I am running locally after giving permission to access drive it creates a TOKENRESPONSE-USER file a which consists of the access token, expiry date refresh token, and scope.
Is this possible that when the function is deployed it is unable to create a TOKENRESPONSE-USER file on azure function?
Please let me know why I am getting this error or do I need to change something in my process.
You can configure your function app to use Google login for authentication purposes when running on Azure. To achieve this you have to generate client id and client secret using the Google sign-in for server-side apps, using this connection you can store the tokens obtained in the token store. Please refer to this document to configure your function app to use Google Login, refer to this document regarding the token store and how to retrieve and refresh the token obtained.

How to login to a Azure Active Directory as a User in Test-Code?

I'm new to Azure and struggle a little in learning all the functionalities of the Azure Active Directory (AAD), so I hope you can clear some things up for me. Here is what I already did:
I registered a web app which serves as a resource provider and offers different APIs behind a API management service.
The web app has several users and roles in the AAD. Plus, more detailed permissions are set on App-Level. So the AAD doesn't control all permissions of my users.
Users are authenticated by using OAuth 2.0. In practice, this means if a new user tries to login to my app he gets redirected to Microsofts login page, enters username and password and then gets a JWT token from Microsofts authentication server.
Now what I want to do:
I want to write an app running on my build server which tests the user permissions. The app has to be written in C# .NET Core. Now I'm struggling on how to log in as a user from my code, so my question is:
How can i log in as a user from code to AAD and get the JWT token to test the user permissions? Can I do this by just using username / password, or do I need to register my test app in the AAD? What are the best solutions to reach my goals?
Thank you in advance
Juunas' comment already covered most of what is required. Just putting a bit more detail behind it.
You can use MSAL (link) to write a .NET Core application that accesses your API.
Within MSAL, you need to use username password authentication (Resource Owner Password Credentials grant) to acquire a JWT token. Please never use this grant outside your testing application.
Depending on how your app is configured, using just the clientId of the API could be enough. It would however be best practice to register a separate native app.
Some wording to help you along:
ClientId: The id of the client application which is requesting the token.
Scope: The scope of the API you acquire the token for. Should already be configured somewhere in your API. Usually something with the AppId URI. Possible examples could look like:
https://<yourtenant>.onmicrosoft.com/<yourapi>/user_impersonation
https://<clientId-of-API>/.default
...
Authority: Your AAD, e.g. https://login.microsoftonline.com/yourtenant.onmicrosoft.com
Code example for the password grant from the wiki (more examples there):
static async Task GetATokenForGraph()
{
string authority = "https://login.microsoftonline.com/contoso.com";
string[] scopes = new string[] { "user.read" };
PublicClientApplication app = new PublicClientApplication(clientId, authority);
try
{
var securePassword = new SecureString();
foreach (char c in "dummy") // you should fetch the password
securePassword.AppendChar(c); // keystroke by keystroke
result = await app.AcquireTokenByUsernamePasswordAsync(scopes, "joe#contoso.com",
securePassword);
}
catch(MsalException)
{
// See details below
}
Console.WriteLine(result.Account.Username);
}
I actually find out a way to do it in "pure" C# without using the MSAL library, which I had some trouble with. So if you're looking for a solution w/o MSAL, you can do it the way described below.
Prerequisites
A user must exist in the AAD and must not use a Microsoft Account (source in Active Directory must not be "Microsoft Account").
A client application must be registered in the Azure Active Directory. The client app must be granted permissions to the app you want to test. If the client app is of type "Native", no client secret must be provided. If the client app is of type "Web app / api", a client secret must be provided. For testing purposes, its recommended to use an app of type "Native" without a client secret.
There must be no two factor authentication.
C# Code
You can than create a class "JwtFetcher" and use code like this:
public JwtFetcher(string tenantId, string clientId, string resource)
{
this.tenantId = !string.IsNullOrEmpty(tenantId) ? tenantId : throw new ArgumentNullException(nameof(tenantId));
this.clientId = !string.IsNullOrEmpty(clientId) ? clientId : throw new ArgumentNullException(nameof(clientId));
this.resource = !string.IsNullOrEmpty(resource) ? resource : throw new ArgumentNullException(nameof(resource));
}
public async Task<string> GetAccessTokenAsync(string username, string password)
{
var requestContent = this.GetRequestContent(username, password);
var client = new HttpClient
{
BaseAddress = new Uri(ApplicationConstant.Endpoint.BaseUrl)
};
var message = await client.PostAsync(this.tenantId + "/oauth2/token", requestContent).ConfigureAwait(false);
message.EnsureSuccessStatusCode();
var jsonResult = await message.Content.ReadAsStringAsync().ConfigureAwait(false);
dynamic objectResult = JsonConvert.DeserializeObject(jsonResult);
return objectResult.access_token.Value;
}
private FormUrlEncodedContent GetRequestContent(string username, string password)
{
List<KeyValuePair<string, string>> requestParameters = new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>(ApplicationConstant.RequestParameterName.GrantType, ApplicationConstant.RequestParameterValue.GrantTypePassword),
new KeyValuePair<string, string>(ApplicationConstant.RequestParameterName.Username, username),
new KeyValuePair<string, string>(ApplicationConstant.RequestParameterName.Password, password),
new KeyValuePair<string, string>(ApplicationConstant.RequestParameterName.ClientId, this.clientId),
new KeyValuePair<string, string>(ApplicationConstant.RequestParameterName.Resource, this.resource)
};
var httpContent = new FormUrlEncodedContent(requestParameters);
return httpContent;
}
The grant type for this is just "password".

Simple Directory Lookup in Azure Active Directory

I am writing a simple desktop application that needs to retrieve some basic properties about a user from Microsoft’ directory. Specifically:
I am writing a single tenant native LOB application.
The application runs on my desktop.
The application runs as my logged on domain account.
The organization' domain accounts are synced to AAD.
I am not trying to secure a native web app or a Web API or anything like that. I do not need users to sign in.
I have email addresses of folks in my organization from an external event management tool. I need to lookup the AAD account profile data (address book info - specifically job title) from AAD based on the email address. I will only be reading AAD data.
So far, I have done the following:-
It appears that the Azure AD Graph API is the right way to fetch the profile information. In particular, the information is available at the endpoint: https://graph.windows.net/{tenant}/users/{email}?api-version=1.6
When registering the native application in AAD, no key was provided. So I don't have a client secret.
Looked at the sample in GitHub here: https://github.com/Azure-Samples/active-directory-dotnet-graphapi-console. The instructions here seem to be wrong because no Keys section is available [see (2)].
Based on the sample above, I wrote a simple function. Code is below:
private static async Task PrintAADUserData(string email)
{
string clientId = "0a202b2c-6220-438d-9501-036d4e05037f";
Uri redirectUri = new Uri("http://localhost:4000");
string resource = "https://graph.windows.net/{tenant}";
string authority = "https://login.microsoftonline.com/{tenant}/oauth2/authorize";
AuthenticationContext authContext = new AuthenticationContext(authority);
AuthenticationResult authResult = await authContext.AcquireTokenAsync(resource, clientId, redirectUri, new PlatformParameters(PromptBehavior.Auto));
string api = String.Format("https://graph.windows.net/{tenant}/users/{0}?api-version=1.6", email);
LOG.DebugFormat("Using API URL {0}", api);
// Create an HTTP client and add the token to the Authorization header
HttpClient httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(authResult.AccessTokenType, authResult.AccessToken);
HttpResponseMessage response = await httpClient.GetAsync(api);
string data = await response.Content.ReadAsStringAsync();
LOG.Debug(data);
}
Questions
The application when run was able to bring up the authentication page. Why do I need that? The application already runs as my domain account. Is an additional authentication necessary? If I were to run this application in Azure as a worker process, then I would not want to use my domain credentials.
The primary problem seems to be the resource URL which is wrong. What resource do I need to specify to access the Azure AD Graph API?
Thanks,
Vijai.
EDITS
Based on the comments from #Saca, the code and application has been edited.
Code
string clientId = ConfigurationManager.AppSettings["AADClientId"];
string clientSecret = ConfigurationManager.AppSettings["AADClientSecret"];
string appIdUri = ConfigurationManager.AppSettings["AADAppIdURI"];
string authEndpoint = ConfigurationManager.AppSettings["AADGraphAuthority"];
string graphEndpoint = ConfigurationManager.AppSettings["AADGraphEndpoint"];
AuthenticationContext authContext = new AuthenticationContext(authEndpoint, false);
AuthenticationResult authResult = await authContext.AcquireTokenAsync("https://graph.windows.net", new ClientCredential(clientId, clientSecret));
ExistingTokenWrapper wrapper = new ExistingTokenWrapper(authResult.AccessToken);
ActiveDirectoryClient client = new ActiveDirectoryClient(new Uri(graphEndpoint), async () => await wrapper.GetToken());
IUser user = client.Users.Where(_ => _.UserPrincipalName.Equals(email.ToLowerInvariant())).Take(1).ExecuteSingleAsync().Result;
App
Error
Unhandled Exception: System.AggregateException: One or more errors occurred. ---> System.AggregateException: One or more errors occurred. ---> Microsoft.Data.OData.ODataErrorException: Insufficient privileges to complete the operation. ---> System.Data.Services.Client.DataServiceQueryException: An error occurred while processing this request. ---> System.Data.Services.Client.DataServiceClientException: {"odata.error":{"code":"Authorization_RequestDenied","message":{"lang":"en","value":"Insufficient privileges to complete the operation."}}}
It appears that despite giving the right permissions, the correct resource and being able to acquire a token, there is still something missing.
The key thing to consider here is if your application will be a headless client run from a secure server or desktop client run by users on their machines.
If the former, then your application is considered a confidential client and can be trusted with secrets, i.e. the keys. If this is your scenario, which is the scenario covered by the sample, then you need to use clientId and clientSecret.
The most likely reason you are not seeing a Keys section in the your application's Configure page is that, instead of selecting Web Application and/or Web API as per step #7 in the sample, you selected Native Client Application when first creating the application. This "type" can't be changed, so you'll need to create a new application.
If your scenario is the latter, then your application is considered a public client and can't be trusted with secrets, in which case, your only options is to prompt the user for credentials. Otherwise, even if your app has it's own authorization layer, it can easily be decompiled and the secret extracted and used.
Your resource URL is correct by the way.
Turns out the real issue was not with the code. I am not an AAD administrator. It appears that any application needing to perform authentication against AAD in our tenant needs to have permissions enabled by the AAD administrators. Once they enabled permissions for my application (and took ownership of the AAD registration as well), this started working.
Hope help some one that are using GraphClient:
var userPriNam = "johndoe#cloudalloc.com";
var userLookupTask = activeDirectoryClient.Users.Where(
user => user.UserPrincipalName.Equals(userPriNam, StringComparison.CurrentCultureIgnoreCase)).ExecuteSingleAsync();
User userJohnDoe = (User)await userLookupTask;
from https://www.simple-talk.com/cloud/security-and-compliance/azure-active-directory-part-5-graph-api/

Azure B2C - Failed to acquire token silently

I'm building an application with ASP.NET MVC and WebAPI using this template : Azure AD B2C WebApp / WepAPI. I've configured my Azure B2C AD through the web.config files and when i click "Sign in" i see my identity providers. Login works so far (i see my username on the top right corner) and i'm able to execute the "To-Do List"-Action.
But a soon as i stop the debugger and restart the Application by pressing F5, i get an error when i click on "To-Do List"-Action again.
Failed to acquire token silently. Call method AcquireToken text --> Code
This happens, cause the user is still authenticated, but the NaiveSessionCache is empty after the applications restart. A possible solution would be, to store the token in the OnAuthorizationCodeReceived Handler, but i looks a little bit weird to me
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
{
string userObjectID = notification.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant, string.Empty, string.Empty);
ClientCredential credential = new ClientCredential(clientId, clientSecret);
string mostRecentPolicy = notification.AuthenticationTicket.Identity.FindFirst(Startup.AcrClaimType).Value;
AuthenticationContext authContext = new AuthenticationContext(authority);
AuthenticationResult result = await authContext.AcquireTokenByAuthorizationCodeAsync(notification.Code, new Uri(redirectUri), credential, new string[] { clientId }, mostRecentPolicy);
// Store token in ClaimsIdentity
notification.AuthenticationTicket.Identity.AddClaim(new System.Security.Claims.Claim("Token", result.Token));
}
How do i correct retrieve the bearer token using AuthenticationContext-class for further use in my Angular-SPA client?
Is it a good idea to store the token as a claim within the OnAuthorizationCodeReceived Handler?
The solution uses the Microsoft.Experimental.IdentityModel.Clients.ActiveDirectory package. Is Microsoft.IdentityModel.Clients.ActiveDirectory still not supporting Azure B2C ?
Your cache is empty because it is not being persisted anywhere. Check out http://www.cloudidentity.com/blog/2014/07/09/the-new-token-cache-in-adal-v2/. Search for EFADALTokenCache and you will find the implementation that will help you persist the cache to some storage.
Azure B2C will only be supported via the new library called MSAL available at https://www.nuget.org/packages/Microsoft.Identity.Client. This library is still under preview.

How to acquire a user based token from Azure Graph API

I have an Azure Active Directory and in my Web Api I have a piece of code that I can get a token from Azure Graph Api using the Application that I have registered with Azure and a Client Certificate.
Here is the code that I use right now:
public static string AcquireServiceToken()
{
var authority = string.Format(_authority, "common");
var authContext = new AuthenticationContext(authority);
var result = authContext.AcquireToken(_serviceTokenResourceId, new ClientAssertionCertificate(_serviceTokenClientId, GetClientCertificate(_certThumbprint)));
return result.AccessToken;
}
This snippet of code works just fine, now what I need is a more specific token which has logged-in user's context, so basically I need to be able to pass in a username and password and get a Graph token back from Azure.
Any Ideas?
A web API can access another web API (in this case, the Graph) as the current user by obtaining a new token via the onbehalfof flow. See https://github.com/Azure-Samples/active-directory-dotnet-webapi-onbehalfof for an example. The direct use of username and password is not recommended, credentials should never be collected outside of Azure AD own pages, and in this specific case it would not work (or will stop working soon)
AcquireToken has another overload that takes in a UserCredential object and U assume you could use that (You will need the TenantId of the Active Directory that the users you need to acquire the tokens for)
Your function will look something like this: (Please fill in the _variables with your own application information)
public static string AcquireTokenWithoutUserCredentials(string userName, string password)
{
var authContext = new AuthenticationContext(string.Format(_authority, _userTokenTenantId));
var userCreds = new UserCredential(userName, password);
var result = authContext.AcquireToken(_resourceId, _userTokenClientId, userCreds);
return result.AccessToken;
}
Looking at your code, looks like you have a multi tenant scenario, in which you use "common" as TenatName, which I'm not sure how/if it will work using the code I pasted here but give it a try ...

Resources