Accessing an Azure AD secured Web App View with Token - c#

We have a requirement to allow a third party to authenticate to an Azure Web App and display an Azure AD secured Web App View non interactively.
The problem I am encountering is I can get a token, but when I try to request the required resource in Azure Web App with the token, I am getting a Page saying to Sign into my Account instead of the HTML content from Azure Web App.
I had developed the code following steps from the below picture
string aadInstance = "https://login.microsoftonline.com/{0}";
string tenant = "xxxx.onmicrosoft.com";
string clientId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
Uri redirectUri = new Uri(#"http://xxxxxDaemonAppDev");
string resourcePath = #"https://xxxxx.azurewebsites.net/Customer/CashSummary?term=xxxxxx";
string appIdURI = #"https://xxxxx.onmicrosoft.com/WebApp-xxxxx.azurewebsites.net";
AuthenticationContext authContext = null;
AuthenticationResult result = null;
authContext = new AuthenticationContext(authority, new FileCache());
UserCredential uc = new UserPasswordCredential("xxxx#jkintranet.com", "xxx#xxxx");
try
{
//I am getting the Token here.
result = authContext.AcquireTokenAsync(appIdURI, clientId, uc).Result;
#region Call Web APP
HttpClient httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
HttpResponseMessage response = httpClient.GetAsync(resourcePath).Result;
if (response.IsSuccessStatusCode)
{
//I am not getting the HTML Content here
string rezstring = response.Content.ReadAsStringAsync().Result;
var todoArray = JArray.Parse(rezstring);
Console.ForegroundColor = ConsoleColor.Green;
foreach (var todo in todoArray)
{
Console.WriteLine(todo["Title"]);
}
}
#endregion
}
catch (Exception ee)
{
MessageBox.Show(ee.Message);
return;
}
Tools and Technologies followed:
Client App is a Daemon or Server Application to Web API
Server App is a Web Azure Web App Secured with Azure AD authentication
Both Server as Web APP and Client as Native are registered in Azure AD
The Architecture I followed:
Following the steps, I have written the code
The Web App's StartupAuth.cs has this:
public void ConfigureAuth(IAppBuilder app)
{
ApplicationDbContext db = new ApplicationDbContext();
AppUserModelContext appUserDB = new AppUserModelContext();
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
//Changed this from
//app.UseCookieAuthentication(new CookieAuthenticationOptions());
//Changed this to
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies",
//added this
CookieSecure = CookieSecureOption.SameAsRequest,
CookieManager = new Microsoft.Owin.Host.SystemWeb.SystemWebChunkingCookieManager()
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = Authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications()
{
// If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away.
AuthorizationCodeReceived = (context) =>
{
var code = context.Code;
ClientCredential credential = new ClientCredential(clientId, appKey);
string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
AuthenticationContext authContext = new AuthenticationContext(Authority, new ADALTokenCache(signedInUserID));
//AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
//code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId);
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId);
return Task.FromResult(0);
},
SecurityTokenValidated = (context) =>
{
var identity = context.AuthenticationTicket.Identity;
var identityName = context.AuthenticationTicket.Identity.FindFirst("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name").Value;
var aIdentity = identityName.Split('#');
var appUser = appUserDB.Find(aIdentity[0]);
if (appUser == null)
{
context.AuthenticationTicket.Properties.RedirectUri = "/Account/SignOut";
}
//Add Claims-Company
context.AuthenticationTicket.Identity.AddClaim(
new System.Security.Claims.Claim(
"http://com.jksb.org/claims/customclaims/company",
"JKB",
null,
"LOCAL AUTHORITY"));
//Add Claims-Business Unit
context.AuthenticationTicket.Identity.AddClaim(
new System.Security.Claims.Claim(
"http://com.jksb.org/claims/customclaims/buid",
appUser.AppBuID,
null,
"LOCAL AUTHORITY"));
return Task.FromResult(0);
},
//added this
AuthenticationFailed = (context) =>
{
if (context.Exception.Message.StartsWith("OICE_20004") || context.Exception.Message.Contains("IDX10311"))
{
context.SkipToNextMiddleware();
return Task.FromResult(0);
}
return Task.FromResult(0);
}
},
}

Related

Creating a user for Azure AD B2C app, how to acquire token correctly? Failing with "parsing_wstrust_response_failed"

[SOLVED, see the edits]
I am working in Linqpad 6, running a script that I made based on the following articles:
https://learn.microsoft.com/en-us/graph/api/user-post-users?view=graph-rest-1.0&tabs=csharp
https://learn.microsoft.com/en-us/graph/sdks/choose-authentication-providers?tabs=CS
Here is my script:
void Main()
{
Debug.WriteLine("yo");
UserCreator creator = new();
creator.CreateUser();
}
public class UserCreator
{
public async void CreateUser()
{
var scopes = new[] { "User.ReadWriteAll" };
// Multi-tenant apps can use "common",
// single-tenant apps must use the tenant ID from the Azure portal
var tenantId = "<MY_TENANT_ID>";
// Value from app registration
var clientId = "<MY_APPLICATION_ID>";
var pca = PublicClientApplicationBuilder
.Create(clientId)
.WithTenantId(tenantId)
.Build();
// DelegateAuthenticationProvider is a simple auth provider implementation
// that allows you to define an async function to retrieve a token
// Alternatively, you can create a class that implements IAuthenticationProvider
// for more complex scenarios
var authProvider = new DelegateAuthenticationProvider(async (request) =>
{
// Use Microsoft.Identity.Client to retrieve token
var result = await pca.AcquireTokenByIntegratedWindowsAuth(scopes).ExecuteAsync();
request.Headers.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", result.AccessToken);
});
GraphServiceClient graphClient = new GraphServiceClient(authProvider);
var user = new User
{
AccountEnabled = true,
DisplayName = "John",
MailNickname = "John",
UserPrincipalName = "john#mail.com",
PasswordProfile = new PasswordProfile
{
ForceChangePasswordNextSignIn = true,
Password = "xWwvJ]6NMw+bWH-d"
}
};
await graphClient.Users
.Request()
.AddAsync(user);
}
}
I am trying to add a new user to an Azure AD B2C app, but the request is failing with an InnerException of:
"The system cannot contact a domain controller to service the authentication request."
I am suspecting I need more info for the script, such as the name of the registered App, but I cannot find anything about it in the documentation. I find it likely that the request is not returning the correct auth token.
Below is a screenshot of the error:
Updated code
This is my final, working result. Originally, I tried to create a user through my own account, but MFA got in the way. The actual way to do it, is through an app registration.
void Main()
{
UserCreator creator = new();
creator.CreateUser();
}
public class UserCreator
{
public async void CreateUser()
{
var clientId = "<CLIENT_ID>";
var scopes = new[] { "https://graph.microsoft.com/.default" };
var tenantId = "<TENANT_ID>";
var clientSecret = "<CLIENT_SECRET>";
// using Azure.Identity;
var options = new TokenCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
};
// https://learn.microsoft.com/dotnet/api/azure.identity.clientsecretcredential
var clientSecretCredential = new ClientSecretCredential(
tenantId, clientId, clientSecret, options);
var graphClient = new GraphServiceClient(clientSecretCredential, scopes);
var user = new{
Email = "test#mail.dk",
DisplayName = "TestUser",
Username = "Someusername",
};
var invitation = new Invitation
{
InvitedUserEmailAddress = user.Email,
InvitedUser = new User
{
AccountEnabled = true,
DisplayName = "TestUser",
CreationType = "LocalAccount",
PasswordPolicies = "DisableStrongPassword",
PasswordProfile = new PasswordProfile
{
ForceChangePasswordNextSignIn = true,
Password = "Test123456",
}
},
InvitedUserType = "member",
SendInvitationMessage = true,
InviteRedirectUrl = "someurl.com"
};
await graphClient.Invitations
.Request()
.AddAsync(invitation);
Console.Write("completed");
}
}
Try to set Azure AD authority by .WithAuthority instead of WithTenantId.
There is a typo in your scopes. Required permission is User.ReadWrite.All not User.ReadWriteAll.
var scopes = new[] { "User.ReadWrite.All" };
...
var pca = PublicClientApplicationBuilder
.Create(clientId)
.WithAuthority($"https://login.microsoftonline.com/{tenantId}")
.WithDefaultRedirectUri()
.Build();

Enable SSO for specific domain users

I am using OpenID Connect authentication in my app. I have registered my app in Microsoft App Registration Portal and received a Client Id and secret from there.
private static string appId = ConfigurationManager.AppSettings["ida:AppId"];
private static string appSecret = ConfigurationManager.AppSettings["ida:AppSecret"];
private static string redirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
private static string graphScopes = ConfigurationManager.AppSettings["ida:GraphScopes"];
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = appId,
Authority = "https://login.microsoftonline.com/common/v2.0",
PostLogoutRedirectUri = redirectUri,
RedirectUri = redirectUri,
Scope = "openid email profile offline_access " + graphScopes,
TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
// In a real application you would use IssuerValidator for additional checks,
// like making sure the user's organization has signed up for your app.
// IssuerValidator = (issuer, token, tvp) =>
// {
// if (MyCustomTenantValidation(issuer))
// return issuer;
// else
// throw new SecurityTokenInvalidIssuerException("Invalid issuer");
// },
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async(context) =>
{
var code = context.Code;
string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
TokenCache userTokenCache = new SessionTokenCache(signedInUserID,
context.OwinContext.Environment["System.Web.HttpContextBase"] as HttpContextBase).GetMsalCacheInstance();
ConfidentialClientApplication cca = new ConfidentialClientApplication(
appId,
redirectUri,
new ClientCredential(appSecret),
userTokenCache,
null);
string[] scopes = graphScopes.Split(new char[] { ' ' });
AuthenticationResult result = await cca.AcquireTokenByAuthorizationCodeAsync(code, scopes);
},
AuthenticationFailed = (context) =>
{
context.HandleResponse();
context.Response.Redirect("/Error?message=" + context.Exception.Message);
return Task.FromResult(0);
}
}
});
}
This code enables SSO but from any Microsoft account as I have used common authority. But I want users from specific directory or domain to login into my application.
I have tried this
Authority = "https://login.microsoftonline.com/{tenant_id}",
instead of
Authority = "https://login.microsoftonline.com/common/v2.0",
But it is not working and Microsoft Login page is not displayed in the browser.
You are close, but you're missing the /v2.0 at the end.
For multi-tenant apps (AAD and MSA accounts) you use:
https://login.microsoftonline.com/common/v2.0
For single-tenant apps (AAD only) you need to use:
https://login.microsoftonline.com/{tenant_id}/v2.0
The /v2.0 signifies that your app uses Azure AD's "v2.0 Application Model" (aka "v2 Endpoint").

Identity Server 4 Authorization Code Flow example

I'm trying to implement Identity Server 4 with AspNet Core using Authorization Code Flow.
The thing is, the IdentityServer4 repository on github have several samples, but none with Authorization Code Flow.
Does anyone have a sample on how to implement Authorization Code Flow with Identity Server 4 and a Client in MVC consuming it?
Here's an implementation of an Authorization Code Flow with Identity Server 4 and an MVC client to consume it.
IdentityServer4 can use a client.cs file to register our MVC client, it's ClientId, ClientSecret, allowed grant types (Authorization Code in this case), and the RedirectUri of our client:
public class Clients
{
public static IEnumerable<Client> Get()
{
var secret = new Secret { Value = "mysecret".Sha512() };
return new List<Client> {
new Client {
ClientId = "authorizationCodeClient2",
ClientName = "Authorization Code Client",
ClientSecrets = new List<Secret> { secret },
Enabled = true,
AllowedGrantTypes = new List<string> { "authorization_code" }, //DELTA //IdentityServer3 wanted Flow = Flows.AuthorizationCode,
RequireConsent = true,
AllowRememberConsent = false,
RedirectUris =
new List<string> {
"http://localhost:5436/account/oAuth2"
},
PostLogoutRedirectUris =
new List<string> {"http://localhost:5436"},
AllowedScopes = new List<string> {
"api"
},
AccessTokenType = AccessTokenType.Jwt
}
};
}
}
This class is referenced in the ConfigurationServices method of the Startup.cs in the IdentityServer4 project:
public void ConfigureServices(IServiceCollection services)
{
////Grab key for signing JWT signature
////In prod, we'd get this from the certificate store or similar
var certPath = Path.Combine(PlatformServices.Default.Application.ApplicationBasePath, "SscSign.pfx");
var cert = new X509Certificate2(certPath);
// configure identity server with in-memory stores, keys, clients and scopes
services.AddDeveloperIdentityServer(options =>
{
options.IssuerUri = "SomeSecureCompany";
})
.AddInMemoryScopes(Scopes.Get())
.AddInMemoryClients(Clients.Get())
.AddInMemoryUsers(Users.Get())
.SetSigningCredential(cert);
services.AddMvc();
}
For reference, here are the Users and Scopes classes referenced above:
public static class Users
{
public static List<InMemoryUser> Get()
{
return new List<InMemoryUser> {
new InMemoryUser {
Subject = "1",
Username = "user",
Password = "pass123",
Claims = new List<Claim> {
new Claim(ClaimTypes.GivenName, "GivenName"),
new Claim(ClaimTypes.Surname, "surname"), //DELTA //.FamilyName in IdentityServer3
new Claim(ClaimTypes.Email, "user#somesecurecompany.com"),
new Claim(ClaimTypes.Role, "Badmin")
}
}
};
}
}
public class Scopes
{
// scopes define the resources in your system
public static IEnumerable<Scope> Get()
{
return new List<Scope> {
new Scope
{
Name = "api",
DisplayName = "api scope",
Type = ScopeType.Resource,
Emphasize = false,
}
};
}
}
The MVC application requires two controller methods. The first method kicks-off the Service Provider (SP-Initiated) workflow. It creates a State value, saves it in cookie-based authentication middleware, and then redirects the browser to the IdentityProvider (IdP) - our IdentityServer4 project in this case.
public ActionResult SignIn()
{
var state = Guid.NewGuid().ToString("N");
//Store state using cookie-based authentication middleware
this.SaveState(state);
//Redirect to IdP to get an Authorization Code
var url = idPServerAuthUri +
"?client_id=" + clientId +
"&response_type=" + response_type +
"&redirect_uri=" + redirectUri +
"&scope=" + scope +
"&state=" + state;
return this.Redirect(url); //performs a GET
}
For reference, here are the constants and SaveState method utilized above:
//Client and workflow values
private const string clientBaseUri = #"http://localhost:5436";
private const string validIssuer = "SomeSecureCompany";
private const string response_type = "code";
private const string grantType = "authorization_code";
//IdentityServer4
private const string idPServerBaseUri = #"http://localhost:5000";
private const string idPServerAuthUri = idPServerBaseUri + #"/connect/authorize";
private const string idPServerTokenUriFragment = #"connect/token";
private const string idPServerEndSessionUri = idPServerBaseUri + #"/connect/endsession";
//These are also registered in the IdP (or Clients.cs of test IdP)
private const string redirectUri = clientBaseUri + #"/account/oAuth2";
private const string clientId = "authorizationCodeClient2";
private const string clientSecret = "mysecret";
private const string audience = "SomeSecureCompany/resources";
private const string scope = "api";
//Store values using cookie-based authentication middleware
private void SaveState(string state)
{
var tempId = new ClaimsIdentity("TempCookie");
tempId.AddClaim(new Claim("state", state));
this.Request.GetOwinContext().Authentication.SignIn(tempId);
}
The second MVC action method is called by IdenityServer4 after the user enters their credentials and checks any authorization boxes. The action method:
Grabs the Authorization Code and State from the query string
Validates State
POSTs back to IdentityServer4 to exchange the Authorization Code for an Access Token
Here's the method:
[HttpGet]
public async Task<ActionResult> oAuth2()
{
var authorizationCode = this.Request.QueryString["code"];
var state = this.Request.QueryString["state"];
//Defend against CSRF attacks http://www.twobotechnologies.com/blog/2014/02/importance-of-state-in-oauth2.html
await ValidateStateAsync(state);
//Exchange Authorization Code for an Access Token by POSTing to the IdP's token endpoint
string json = null;
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(idPServerBaseUri);
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grant_type", grantType)
,new KeyValuePair<string, string>("code", authorizationCode)
,new KeyValuePair<string, string>("redirect_uri", redirectUri)
,new KeyValuePair<string, string>("client_id", clientId) //consider sending via basic authentication header
,new KeyValuePair<string, string>("client_secret", clientSecret)
});
var httpResponseMessage = client.PostAsync(idPServerTokenUriFragment, content).Result;
json = httpResponseMessage.Content.ReadAsStringAsync().Result;
}
//Extract the Access Token
dynamic results = JsonConvert.DeserializeObject<dynamic>(json);
string accessToken = results.access_token;
//Validate token crypto
var claims = ValidateToken(accessToken);
//What is done here depends on your use-case.
//If the accessToken is for calling a WebAPI, the next few lines wouldn't be needed.
//Build claims identity principle
var id = new ClaimsIdentity(claims, "Cookie"); //"Cookie" matches middleware named in Startup.cs
//Sign into the middleware so we can navigate around secured parts of this site (e.g. [Authorized] attribute)
this.Request.GetOwinContext().Authentication.SignIn(id);
return this.Redirect("/Home");
}
Checking that the State received is what you expected helps defend against CSRF attacks: http://www.twobotechnologies.com/blog/2014/02/importance-of-state-in-oauth2.html
This ValidateStateAsync method compares the received State to what was saved off in the cookie middleware:
private async Task<AuthenticateResult> ValidateStateAsync(string state)
{
//Retrieve state value from TempCookie
var authenticateResult = await this.Request
.GetOwinContext()
.Authentication
.AuthenticateAsync("TempCookie");
if (authenticateResult == null)
throw new InvalidOperationException("No temp cookie");
if (state != authenticateResult.Identity.FindFirst("state").Value)
throw new InvalidOperationException("invalid state");
return authenticateResult;
}
This ValidateToken method uses Microsoft's System.IdentityModel and System.IdentityModel.Tokens.Jwt libraries to check that JWT is properly signed.
private IEnumerable<Claim> ValidateToken(string token)
{
//Grab certificate for verifying JWT signature
//IdentityServer4 also has a default certificate you can might reference.
//In prod, we'd get this from the certificate store or similar
var certPath = Path.Combine(Server.MapPath("~/bin"), "SscSign.pfx");
var cert = new X509Certificate2(certPath);
var x509SecurityKey = new X509SecurityKey(cert);
var parameters = new TokenValidationParameters
{
RequireSignedTokens = true,
ValidAudience = audience,
ValidIssuer = validIssuer,
IssuerSigningKey = x509SecurityKey,
RequireExpirationTime = true,
ClockSkew = TimeSpan.FromMinutes(5)
};
//Validate the token and retrieve ClaimsPrinciple
var handler = new JwtSecurityTokenHandler();
SecurityToken jwt;
var id = handler.ValidateToken(token, parameters, out jwt);
//Discard temp cookie and cookie-based middleware authentication objects (we just needed it for storing State)
this.Request.GetOwinContext().Authentication.SignOut("TempCookie");
return id.Claims;
}
A working solution containing these source files resides on GitHub at https://github.com/bayardw/IdentityServer4.Authorization.Code
Here's a sample - it is using hybrid flow instead of code flow. But hybrid flow is more recommended anyways if you client library supports it (and the aspnetcore middleware does).
https://github.com/IdentityServer/IdentityServer4/tree/master/samples/Quickstarts/5_HybridFlowAuthenticationWithApiAccess

Not receiving Email from Azure AD login using Owin OpenIdConnect

I have a MVC web application that uses Owin's OpenIdConnector OAuth provider to authenticate against a multi-tenant Azure AD directory.
I can redirect to the Microsoft login page and return to my app, but when I call the GetExternalLoginInfo method the Email property is always null.
I suspect this is because of the permissions I am setting on the application, but I can't find what permissions I should be requesting for email to come across properly.
The permissions I'm requesting:
My OpenIDConnect configuration in Startup.Auth.cs
string clientId = "ClientId";
string appKey = "Client Secret";
string graphResourceID = "https://graph.windows.net";
string Authority = "https://login.microsoftonline.com/common/";
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = Authority,
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = false,
},
Notifications = new OpenIdConnectAuthenticationNotifications()
{
AuthorizationCodeReceived = (context) =>
{
var code = context.Code;
ClientCredential credential = new ClientCredential(clientId, appKey);
string tenantID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
AuthenticationContext authContext = new AuthenticationContext(string.Format("https://login.microsoftonline.com/{0}", tenantID));
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceID);
return Task.FromResult(0);
},
RedirectToIdentityProvider = (context) =>
{
string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase;
context.ProtocolMessage.RedirectUri = appBaseUrl + "/";
context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl;
return Task.FromResult(0);
},
SecurityTokenValidated = (context) =>
{
// retriever caller data from the incoming principal
string issuer = context.AuthenticationTicket.Identity.FindFirst("iss").Value;
string UPN = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.Name).Value;
string tenantID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{ context.OwinContext.Response.Redirect("/Home/Error");
context.HandleResponse(); // Suppress the exception
return Task.FromResult(0);
}
}
});
You may need to specify the scope of the challenge when setting up the OpenIdConnectOptions. When a "challenge" is issued by your authentication scheme, the scope determines what claims to request/challenge.
var options = new OpenIdConnectOptions()
{
// .... Your existing options
};
//You'll need to check what sort of attributes you can request from azure
options.Scope.Add("profile")
app.UseOpenIdConnectAuthentication(options);

403 Forbidden from Azure Graph API

I get a 403 Forbidden response from Azure AD when trying to create an application using the Graph API:
private static void CreateApplicationViaPost(string tenantId, string clientId, string clientSecret)
{
var authContext = new AuthenticationContext(
string.Format("https://login.windows.net/{0}",
tenantId));
ClientCredential clientCred = new ClientCredential(clientId, clientSecret);
AuthenticationResult result = authContext.AcquireToken(
"https://graph.windows.net",
clientCred);
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
const string json = #"{ displayName: ""My test app"", logoutUrl: ""http://logout.net"", identifierUris: [ ""http://identifier1.com"" ], replyUrls: [ ""http://replyUrl.net"" ] }";
HttpResponseMessage response = client.PostAsync(
string.Format("https://graph.windows.net/{0}/applications?api-version=1.6", tenantId),
new StringContent(json, Encoding.UTF8, "application/json")).Result;
Console.WriteLine(response.ToString());
}
The client registered in Azure AD has all the permissions:
What am I missing?
EDIT:
I registered a native client in Azure AD and gave it permissions to write to Windows Azure Active Directory. This code create an application in Azure AD:
private static void CreateApplicationViaPost(string tenantId, string clientId, string redirectUri)
{
var authContext = new AuthenticationContext(
string.Format("https://login.windows.net/{0}",
tenantId));
AuthenticationResult result = authContext.AcquireToken("https://graph.windows.net", clientId, new Uri(redirectUri), PromptBehavior.Auto);
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
const string json = #"{ displayName: ""My test app1"", homepage: ""http://homepage.com"", logoutUrl: ""http://logout1.net"", identifierUris: [ ""http://identifier11.com"" ], replyUrls: [ ""http://replyUrl1.net"" ] }";
HttpResponseMessage response = client.PostAsync(
string.Format("https://graph.windows.net/{0}/applications?api-version=1.6", tenantId),
new StringContent(json, Encoding.UTF8, "application/json")).Result;
Console.WriteLine(response.ToString());
}
Modifying the directory requires consent from an admin user. So you'll need to acquire an access token from an user, e.g. through OAuth, instead of a token for the client.
There are quite a few of samples at GitHub that show the authorisation flow, e.g. https://github.com/AzureADSamples/WebApp-GraphAPI-DotNet.
Adding to #MrBrink's answer - you need to make sure the person adding the permissions in the Azure Active Directory UI is actually an administrator. If you have access to Azure Active Directory and are not an administrator it WILL still let you assign permissions - however they will only apply at a user scope.
An alternative would be to use the ActiveDirectoryClient from the Microsoft.Azure.ActiveDirectory.GraphClient NuGet package.
private static async Task CreateApplication(string tenantId, string clientId,
string redirectUri)
{
var graphUri = new Uri("https://graph.windows.net");
var serviceRoot = new Uri(graphUri, tenantId);
var activeDirectoryClient = new ActiveDirectoryClient(serviceRoot,
async () => AcquireTokenAsyncForUser("https://login.microsoftonline.com/" + tenantId,
clientId, redirectUri));
var app = new Application
{
Homepage = "https://localhost",
DisplayName = "My Application",
LogoutUrl = "https://localhost",
IdentifierUris = new List<string> { "https://tenant.onmicrosoft.com/MyApp" },
ReplyUrls = new List<string> { "https://localhost" }
};
await activeDirectoryClient.Applications.AddApplicationAsync(app);
Console.WriteLine(app.ObjectId);
}
private static string AcquireTokenAsyncForUser(string authority, string clientId,
string redirectUri)
{
var authContext = new AuthenticationContext(authority, false);
var result = authContext.AcquireToken("https://graph.windows.net",
clientId, new Uri(redirectUri), PromptBehavior.Auto);
return result.AccessToken;
}

Categories

Resources