I'm attempting to access an API App I have hosted on Azure and secured with Azure AD.
For the API App I've set App Service Authentication = Azure Active Directory "Express" management mode.
In the "classic" portal I've created a couple of applications under AD. One for the API App and another for the Web App. And for the Web App I've added an entry under "permissions to other applications" for the API App (though I'm not sure I need this as "user assignment required to access app" is off for the API App). I've also generated a key for the Web App.
Following the example code given here - https://github.com/Azure-Samples/active-directory-dotnet-webapp-webapi-oauth2-appidentity ...
I can successfully obtain a bearer token using the following code:
private static string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
private static string tenant = ConfigurationManager.AppSettings["ida:Tenant"];
private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
private static string appKey = ConfigurationManager.AppSettings["ida:AppKey"];
static string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
private static string ApiId = ConfigurationManager.AppSettings["ApiId"];
private static AuthenticationContext authContext = new AuthenticationContext(authority);
private static ClientCredential clientCredential = new ClientCredential(clientId, appKey);
...
AuthenticationResult result = null;
int retryCount = 0;
bool retry = false;
do
{
retry = false;
try
{
// ADAL includes an in memory cache, so this call will only send a message to the server if the cached token is expired.
result = await authContext.AcquireTokenAsync(ApiId, clientCredential);
}
catch (AdalException ex)
{
if (ex.ErrorCode == "temporarily_unavailable")
{
retry = true;
retryCount++;
Thread.Sleep(3000);
}
}
} while ((retry == true) && (retryCount < 3));
if (result == null)
return Request.CreateResponse(HttpStatusCode.InternalServerError, "Could not authenticate against API.");
But when I use the bearer token with with the request from the Web App to the API App I always get a 401 unauthorized response:
StatusCode: 401, ReasonPhrase: 'Unauthorized', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
Date: Wed, 13 Jul 2016 08:43:09 GMT
Server: Microsoft-IIS/8.0
WWW-Authenticate: Bearer realm="MY-API-APP-ID-IS-HERE"
X-Powered-By: ASP.NET
Content-Length: 58
Content-Type: text/html
}
This is the code I'm using to make the request that's failing with a 401:
var apiUri = new Uri(ConfigurationManager.AppSettings["ApiUrl"] + "/api/MethodImCalling");
var client = new RestClient(apiUri.GetLeftPart(UriPartial.Authority));
var request = new RestRequest(apiUri, Method.GET);
request.AddHeader("Authorization", "Bearer " + result.AccessToken);
request.AddParameter("something", somevalue);
var response = client.Execute(request);
if (response.StatusCode != HttpStatusCode.OK)
return Request.CreateResponse(response.StatusCode); // Relay non-successful response
Any ideas what I might be doing wrong or am missing? Thanks in advance!
I already have Logic App in Azure accessing the API App without issue, but I note that the authentication credentials in the logic app json include an "audience" parameter. The code above does not use an "audience" so could this be the missing part of the puzzle, and if so, how do I add it?
Screenshot showing how Web App has been configured to access API App:
The reason you are getting a 401 response is that you've only granted your application Delegated Permissions, yet you are using the client credentials flow which requires Application Permissions.
You can either change your code to use the Authorization Code flow or grant application permissions from your web app to your web API.
To use the Authorization Code flow you'd need to change your code to use AcquireTokenByAuthorizationCodeAsync instead.
You can find more information about these two different approaches here:
https://azure.microsoft.com/en-us/documentation/articles/active-directory-authentication-scenarios/#web-application-to-web-api
Related
We have two separeate dotnet core apis(API1 & API2) that are protected using azure ad b2c. Both these apis are registered on the b2c tenant and have their scopes exposed.
We have a client web applicaiton that is to access the above protected apis. This web app has been registered as a applicaiton in b2c tenant and has api permissions set for the above apis with proper scopes defined.
We use MSAL.net with a signinpolicy to sign the user in to the web app.
the authentication call requires scopes to mentioned. So we add API1's scope in the call.
(note : one scope of a single resource can be added in a auth call shown below)
public void ConfigureAuth(IAppBuilder app)
{
// Required for Azure webapps, as by default they force TLS 1.2 and this project attempts 1.0
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
// ASP.NET web host compatible cookie manager
CookieManager = new SystemWebChunkingCookieManager()
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
// Generate the metadata address using the tenant and policy information
MetadataAddress = String.Format(Globals.WellKnownMetadata, Globals.Tenant, Globals.DefaultPolicy),
// These are standard OpenID Connect parameters, with values pulled from web.config
ClientId = Globals.ClientId,
RedirectUri = Globals.RedirectUri,
PostLogoutRedirectUri = Globals.RedirectUri,
// Specify the callbacks for each type of notifications
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = OnRedirectToIdentityProvider,
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthenticationFailed,
},
// Specify the claim type that specifies the Name property.
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
ValidateIssuer = false
},
// Specify the scope by appending all of the scopes requested into one string (separated by a blank space)
Scope = $"openid profile offline_access {Globals.ReadTasksScope} {Globals.WriteTasksScope}",
// ASP.NET web host compatible cookie manager
CookieManager = new SystemWebCookieManager()
}
);
}
The OnAuthorizationCodeRecieved method in Startup.Auth.cs recieved the code recieved as a result of above auth call and uses it to get a access token based on the scopes provided and stores it in the cache. shown below
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
{
try
{
/*
The `MSALPerUserMemoryTokenCache` is created and hooked in the `UserTokenCache` used by `IConfidentialClientApplication`.
At this point, if you inspect `ClaimsPrinciple.Current` you will notice that the Identity is still unauthenticated and it has no claims,
but `MSALPerUserMemoryTokenCache` needs the claims to work properly. Because of this sync problem, we are using the constructor that
receives `ClaimsPrincipal` as argument and we are getting the claims from the object `AuthorizationCodeReceivedNotification context`.
This object contains the property `AuthenticationTicket.Identity`, which is a `ClaimsIdentity`, created from the token received from
Azure AD and has a full set of claims.
*/
IConfidentialClientApplication confidentialClient = MsalAppBuilder.BuildConfidentialClientApplication(new ClaimsPrincipal(notification.AuthenticationTicket.Identity));
// Upon successful sign in, get & cache a token using MSAL
AuthenticationResult result = await confidentialClient.AcquireTokenByAuthorizationCode(Globals.Scopes, notification.Code).ExecuteAsync();
}
catch (Exception ex)
{
throw new HttpResponseException(new HttpResponseMessage
{
StatusCode = HttpStatusCode.BadRequest,
ReasonPhrase = $"Unable to get authorization code {ex.Message}.".Replace("\n", "").Replace("\r", "")
});
}
}
This access token is then used in the TasksController to call AcquireTokenSilent which retrieves the access token from the cache, which is then used in the api call.
public async Task<ActionResult> Index()
{
try
{
// Retrieve the token with the specified scopes
var scope = new string[] { Globals.ReadTasksScope };
IConfidentialClientApplication cca = MsalAppBuilder.BuildConfidentialClientApplication();
var accounts = await cca.GetAccountsAsync();
AuthenticationResult result = await cca.AcquireTokenSilent(scope, accounts.FirstOrDefault()).ExecuteAsync();
HttpClient client = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, apiEndpoint);
// Add token to the Authorization header and make the request
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
HttpResponseMessage response = await client.SendAsync(request);
// Handle the response
switch (response.StatusCode)
{
case HttpStatusCode.OK:
String responseString = await response.Content.ReadAsStringAsync();
JArray tasks = JArray.Parse(responseString);
ViewBag.Tasks = tasks;
return View();
case HttpStatusCode.Unauthorized:
return ErrorAction("Please sign in again. " + response.ReasonPhrase);
default:
return ErrorAction("Error. Status code = " + response.StatusCode + ": " + response.ReasonPhrase);
}
}
catch (MsalUiRequiredException ex)
{
/*
If the tokens have expired or become invalid for any reason, ask the user to sign in again.
Another cause of this exception is when you restart the app using InMemory cache.
It will get wiped out while the user will be authenticated still because of their cookies, requiring the TokenCache to be initialized again
through the sign in flow.
*/
return new RedirectResult("/Account/SignUpSignIn?redirectUrl=/Tasks");
}
catch (Exception ex)
{
return ErrorAction("Error reading to do list: " + ex.Message);
}
}
The issue is the code recieved by the OnAuthorizationCodeRecieved method can only be used to get the access token for API1 since its scope was mentioned in auth call. When trying to get access token for API2 it returns null.
Question : How to configure the web app so that it is able to access multiple protected apis?
Please suggest.
The code can be found from the sample https://github.com/Azure-Samples/active-directory-b2c-dotnet-webapp-and-webapi
A single access token can only contain scopes for a single audience.
You have 2 options:
Combine both services into a single app registration and expose different scopes.
Request multiple tokens - one per service. If your SSO policy is configured correctly in B2C, this should happen silently unbeknownst to the user.
I recommend using option 1 if you own both services (which it sounds like you do). A few tips related to this option.
When declaring the scopes in the combined app registration, use the dot-syntax {LogicalService}.{Operation}. If you do this, the scopes will be grouped by logical service within the Azure portal.
Make sure you are validating scopes in your service. Validating only the audience is not good enough and would allow an attacker to make lateral movements with a token bound for another service.
I'm working on implementing integrating Azure AD login authentication to my web app. I have created an account in azure development portal and registered my app details.
my app URL -> https://my-sample-app/my.dashboard/
my redirect url is ->https://my-sample-app/my.dashboard/ws/aad/callback/
Note : ws that comes after my app url is the servlet adapter configured
my web app is a java app and i'm using ADAL java SDK
I have referred this article Authenticate to an Azure API App from Java and did the similar way
this is the code logic written under web path "aad/callback"
String appIdUri = System.getProperty("azure.app.id.uri", "https://login.microsoftonline.com/");
String authority = System.getProperty("azure.authority.url", "https://login.microsoftonline.com/my-sample-app.onmicrosoft.com");
String clientId = System.getProperty("azure.client.id", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
String clientSecret = System.getProperty("azure.client.secret", "xxxxxxxxxxxxxxxxxxxxxxxx");
AuthenticationContext context = null;
AuthenticationResult result = null;
ExecutorService service = null;
UserVO userVO = null;
try {
HttpsURLConnection conn = (HttpsURLConnection) new URL(appIdUri).openConnection();
service = Executors.newFixedThreadPool(1);
context = new AuthenticationContext(authority, false, service);
ClientCredential credential = new ClientCredential(clientId, clientSecret);
Future<AuthenticationResult> future = context.acquireToken(appIdUri, credential, null);
result = future.get();
HttpSession session = request.getSession();
LOGGER.info("session :{}",session);
String accessToken = null;
if (result == null) {
throw new ServiceUnavailableException("authentication result was null");
} else {
accessToken = result.getAccessToken();
}
String data = "{\"access_token\": \"" + accessToken + "\"}";
LOGGER.info("access_token :{}", data);
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.addRequestProperty("Content-Length", data.length() + "");
new DataOutputStream(conn.getOutputStream()).writeBytes(data);
String authTokenResp = IOUtils.toString(conn.getInputStream());
Gson gson = new Gson();
Map<String, Object> map = gson.fromJson(authTokenResp, Map.class);
String authenticationToken = (String) map.get("authenticationToken");
System.out.println("Authentication Token: "+authenticationToken);
I'm able to see the access token value in the log statement but the authTokenResp output value that i received from authTokenResp = IOUtils.toString(conn.getInputStream()); looks like some html page (probably the login page response of portal.office.com ) doesn't has key authenticationToken in it.
I think I have made mistake by mentioning wrong URL for the appIdUri.
please can i someone tell me what URL should be given for appIdUri ? where can i find this URL value in azure portal ?
This sample is just a client credential flow to get access token.
please can i someone tell me what URL should be given for appIdUri ?
where can i find this URL value in azure portal ?
The first parameter of acquireToken method is the value of a resource which you want to access.It is the App ID URI of the target web API (secured resource). To find the App ID URI, in the Azure Portal, click Azure Active Directory, click Application registrations, open the application's Settings page, then click Properties. It may also be an external resource like https://graph.microsoft.com. This is required in one of either the authorization or token requests.
Is my-sample-app.onmicrosoft.com your tenant name?
String authority = System.getProperty("azure.authority.url", "https://login.microsoftonline.com/{your_tenant_name}");
If you want to integrate Azure AD login authentication to your web app, you should refer to this sample.
I am using the following approach as the basis of this (https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-devquickstarts-webapi-dotnet).
I got all this example working after setting up azure. But now we need to port it to an actual existing mobile app and web api app. The mobile app can get the Bearer token, but when we pass it to the web api, we pass this in a CSOM request as follows, but we still get a 401 Unauthroised response.
public static ClientContext GetSharepointBearerClientContext(this JwtTokenDetails tokenDetails)
{
var context = new ClientContext(tokenDetails.SiteUrl);
//context.AuthenticationMode = ClientAuthenticationMode.Anonymous;
context.ExecutingWebRequest += new EventHandler<WebRequestEventArgs>((s, e) =>
{
e.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + tokenDetails.BearerToken;
});
return context;
}
Our web api doesn't use any of the tech as in the example above, as I presume that we should just be able to pass the token through the CSOM request in the header, but this is not working, what else could I look at?
I have assigned the Office 365 Sharepoint Online (Microsoft.Sharepoint) permission and set the following
I have also done the same for the app registration, which we don't really use! Still not sure how the app registration comes into it)...
So this was possible, it was just microsoft telling us to put in an incorrect value. All the documentation says put the APP ID URI in the Resource. But in our case it needed to be the sharepoint url.
So we have the tenant name which on azure id the domain name e.g. srmukdev.onmicrosoft.com
Tenant: srmukdev.onmicrosoft.com
Application Id: This is the guid for the app registered in azure active directory.
RedirectUri: This can be any url(URI), its not actually used as a url for a mobile app as far as I can see.
ResourceUrl: srmukdev.sharepoint.com
The code I am using to get a token is as follows for a WPF example. The aadInstance is https://login.microsoftonline.com/{0}
private static string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
public async void CheckForCachedToken(PromptBehavior propmptBehavior)
{
//
// As the application starts, try to get an access token without prompting the user. If one exists, populate the To Do list. If not, continue.
//
AuthenticationResult result = null;
try
{
result = await authContext.AcquireTokenAsync(resourceUrl, applicationId, redirectUri, new PlatformParameters(propmptBehavior));
TokenTextBox.Text = result.AccessToken;
// A valid token is in the cache - get the To Do list.
GetTokenButton.Content = "Clear Cache";
}
catch (AdalException ex)
{
if (ex.ErrorCode == "user_interaction_required")
{
// There are no tokens in the cache. Proceed without calling the To Do list service.
}
else
{
// An unexpected error occurred.
string message = ex.Message;
if (ex.InnerException != null)
{
message += "Inner Exception : " + ex.InnerException.Message;
}
MessageBox.Show(message);
}
return;
}
}
I am working on an ASP.NET MVC5 Web App that uses Azure ADAL libraries to authenticate users, it works fine, however, when I manually send requests to graph, ex: GET https://graph.microsoft.com/v1.0/me or GET https://graph.microsoft.com/v1.0/groups?$filter=from/displayName eq 'whatever'.
I have tried updating the App Registration in Azure as to add the required Graph permissions, and I have also tried creating new app registrations, no matter what I do my requests will always respond 401 Unauthorized, is there anything I am missing?
EDIT: Example response from Postman
{
"error": {
"code": "InvalidAuthenticationToken",
"message": "Access token validation failure.",
"innerError": {
"request-id": "a142576b-acce-4e59-8a8d-adede61aaf59",
"date": "2017-04-05T13:27:36"
}
}
}
EDIT: C# Request Example
public async Task<GroupGraph> GetGroupIdByDisplayName(string displayName)
{
var accessToken = await authenticationService.GetTokenUserOnly();
GroupGraph groupGraphResponse = null;
using (var client = new HttpClient())
{
using (var request = new HttpRequestMessage(HttpMethod.Get, $"https://graph.microsoft.com/v1.0/groups?$filter=from/displayName eq '{displayName}'"))
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
using (var response = client.SendAsync(request).Result)
{
if (response.IsSuccessStatusCode)
{
using (var content = response.Content)
{
var result = await content.ReadAsStringAsync();
groupGraphResponse = JsonConvert.DeserializeObject<GroupGraph>(result);
}
}
}
}
}
return groupGraphResponse;
}
EDIT: The way I obtain the token
public async Task<string> GetTokenUserOnly()
{
string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
string tenantID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
string userObjectID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
// get a token for the Graph without triggering any user interaction (from the cache, via multi-resource refresh token, etc)
ClientCredential clientcred = new ClientCredential(clientId, appKey);
// initialize AuthenticationContext with the token cache of the currently signed in user, as kept in the app's database
AuthenticationContext authenticationContext = new AuthenticationContext(aadInstance + tenantID, new TableTokenCache(signedInUserID));
//AuthenticationResult authenticationResult = await authenticationContext.AcquireTokenSilentAsync(graphResourceID, clientcred, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));
AuthenticationResult authenticationResult = authenticationContext.AcquireToken(graphResourceID, clientcred);
return authenticationResult.AccessToken;
}
You can't use ADAL to get tokens for graph.microsoft.com. ADAL is for graph.windows.net.
In order to get tokens for the Graph library (graph.windows.com) look into the Nuget Package Microsoft.Graph. Microsoft also has some documentation on how to pull user info using Graph.
Be forewarned though, using Graph Libraries and ADAL libraries side by side can lead to some weird side effects, such as the credential cache being cleared.
It seems you are using the client credential grant flow to acquire the access token for graph api(graphResourceID is https://graph.microsoft.com ?) :
AuthenticationResult authenticationResult = authenticationContext.AcquireToken(graphResourceID, clientcred);
So you need to grant app permission in azure ad portal :
For error "Access token validation failure" , you could use online tool like http://jwt.calebb.net/ to decode your access token , check the audience or lifetime of the access token .
To obtain a valid token for Microsoft Graph API you can use Azure.Identity.
To use any implementation of TokenCredential we need to build our own IAuthenticationProvider.
public class TokenCredentialAuthenticationProvider : IAuthenticationProvider
{
private readonly TokenCredential _tokenCredential;
public TokenCredentialAuthenticationProvider(TokenCredential tokenCredential)
{
_tokenCredential = tokenCredential;
}
public async Task AuthenticateRequestAsync(HttpRequestMessage request)
{
var accessToken = await _tokenCredential.GetTokenAsync(new TokenRequestContext(new[] { "https://graph.microsoft.com" }), CancellationToken.None);
request.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken.Token);
}
}
Now we can for instance use AzureCliCredential to acquire an access token.
Open Powershell and type in az login in order to login with your Azure AD account.
In Azure you could also use Managed Identity to get a token based on a Azure resource e.g. Azure App Service. Here need to use ManagedIdentityToken.
Usage:
var client = new GraphServiceClient(new TokenCredentialAuthenticationProvider(new AzureCliCredential()));
var user = await client.Me.Request().GetAsync();
I created a basic project using Visual Studio 2015 Update 3 for Web API (nothing custom, bare bone) and deployed it to Azure (Free Account) following the instruction here.
Then I created a Console client with the following code.
public static async Task<bool> ReadValues()
{
try
{
// Authenticate the user and get a token from Azure AD
//AuthenticationResult authResult = await AuthContext.AcquireTokenSilentAsync(Resource, ClientId);
AuthenticationResult authResult = AuthContext.AcquireToken(Resource, ClientId, RedirectUri);
// Create an HTTP client and add the token to the Authorization header
HttpClient httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
//"Bearer"
authResult.AccessTokenType
, authResult.AccessToken);
// Call the Web API to get the values
var requestUri = new Uri(WebApiUri, "api/values");
Console.WriteLine("Reading values from '{0}'.", requestUri);
HttpResponseMessage httpResponse = await httpClient.GetAsync(requestUri);
Console.WriteLine("HTTP Status Code: '{0}'", httpResponse.StatusCode.ToString());
//Console.WriteLine("HTTP Header: '{0}'", httpClient.DefaultRequestHeaders.Authorization.ToString());
if (httpResponse.IsSuccessStatusCode)
{
//
// Code to do something with the data returned goes here.
//
var s = await httpResponse.Content.ReadAsStringAsync();
Console.WriteLine(s);
}
else
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(httpResponse.ReasonPhrase);
}
return (httpResponse.IsSuccessStatusCode);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
return false;
}
It works fine when I run the WEB API locally from the Visual Studio in debug, but when I deploy it to the Azure, it returns Unauthorized.
Few common things that I might get asked:
I do receive a valid bearer token
I have created the App registrations in the Azure AD for bot hthe WEB API and the client
The client and WEB API are using the correct redirect, resource uri
The account I am using to login is the same as the one used to create the Azure account and it has full privileges in the domain/AD/API
On the API side, this is whole of the startup.auth.cs
using System.Configuration;
using System.IdentityModel.Tokens;
using Microsoft.Owin;
using Microsoft.Owin.Security.ActiveDirectory;
using Owin;
using WebApi;
[assembly: OwinStartup("default", typeof(Startup))]
namespace WebApi
{
public partial class Startup
{
// For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
public void ConfigureAuth(IAppBuilder app)
{
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
TokenValidationParameters = new TokenValidationParameters {
ValidAudience = ConfigurationManager.AppSettings["ida:Audience"]
},
});
}
}
}
What else should I check?
Other references
https://www.simple-talk.com/cloud/security-and-compliance/azure-active-directory-part-3-developing-native-client-applications/
Thanks for help from Juunas who provided me with a working copy, I was able to narrow down the cause. When I attached a debugger to the Azure instance of the Web API I was able to see a exception for Bad Audience. On trying to retrace my steps, I found that while deployment from Visual Studio, I was selection Enterprise Authentication in settings that was causing the web.config to change in way that lead to the problem. Not selecting that option, I was able to access the API through bearer token.