I must be missing something obvious. When posting to the beta API https://graph.microsoft.com/beta/invitations (api ref: https://graph.microsoft.io/en-us/docs/api-reference/beta/api/invitation_post), I get
{
"error": {
"code": "UnknownError",
"message": "",
"innerError": {
"request-id": "e41b0eab-c39c-4cf8-9034-341c81fc722c",
"date": "2017-01-14T19:26:55"
}
}
}
Here's my code:
namespace ConsoleApplication1
{
public class Program
{
static void Main(string[] args)
{
GraphClient g = new GraphClient();
Console.WriteLine(g.SendPost(g.authContext, g.credential).Result);
}
}
public class GraphClient
{
public AuthenticationContext authContext;
public ClientCredential credential;
public GraphClient()
{
this.authContext = new AuthenticationContext("https://login.microsoftonline.com/MYTENANT.onmicrosoft.com");
this.credential = new ClientCredential("MYCLIENTID", "MYCLIENTSECRET");
}
public async Task<string> SendPost(AuthenticationContext authContext, ClientCredential credential)
{
AuthenticationResult result = await authContext.AcquireTokenAsync("https://graph.microsoft.com", credential);
HttpClient http = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "https://graph.microsoft.com/beta/invitations");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
request.Content = new StringContent("{\"invitedUserEmailAddress\": \"MYEMAIL#MYDOMAIN.COM\",\"inviteRedirectUrl\": \"https://MYWEBSITE.COM\"}", Encoding.UTF8, "application/json");
HttpResponseMessage response = await http.SendAsync(request);
return await response.Content.ReadAsStringAsync();
}
}
}
Thanks! I can do other /beta commands just fine. For example GETting https://graph.microsoft.com/beta/users returns a user list in my tenant, as expected.
-Dan
Based on the logs, you are receiving a 401 error response, which means that the caller was not granted the required permissions to call the API. You'll need to follow the guidance here: https://graph.microsoft.io/en-us/docs/api-reference/beta/api/invitation_post which indicates that you need Directory.ReadWrite.All or User.ReadWrite.All (although I'm not sure the latter one works or is available right now).
We've also filed a bug to fix this error message (sorry about this - we do need to do much better here).
Hope this helps,
Related
I've got a cloud migration coming up and we previously did a lot of work transferring data between on-prem databases. The cloud provider is building API's for us to interface with the db once it's in the cloud, so I'm trying to write a Windows Service that will periodically suck some data out of our other on-prem db and post it to the cloud db. I'm having trouble figuring out how to implement OpenID Connect authentication in c# in a windows service. All the examples I've seen are web applications. Has anyone tried this?
If anyone has a better idea than a windows service, I'm open to it.
What you should look for is the client credentials flow (part of OpenID Connect). It is the flow you use when you have no user involved when requesting tokens.
To get a token, you can use the IdentityModel helper library and you can find sample code here:
If you don't want to use a windows service, then the alternative is to create a console application that you schedule to be called using the windows built in task scheduler.
Alternatively, take a look at HangFire.
I've got my service talking with the API. The basic flow is that you request a Bearer token, then pass the token in corresponding requests. Here's an example of the function I'm using to request tokens:
public static async Task<string> RequestTokenAsync()
{
var client = new HttpClient();
string token;
var con = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("client_id", "XXXXX"),
new KeyValuePair<string, string>("client_secret", "XXXXX"),
new KeyValuePair<string, string>("grant_type", "password"),
new KeyValuePair<string, string>("username", "XXXXX"),
new KeyValuePair<string, string>("password", "XXXXX"),
new KeyValuePair<string, string>("device_id", "XXXXX"),
new KeyValuePair<string, string>("company", "XXXXX")
};
var request = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("https://whatever/connect/token"),
Content = new FormUrlEncodedContent(con)
{
Headers =
{
ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded")
}
}
};
try
{
using (var response = await client.SendAsync(request))
{
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
token = body;
}
}
catch (Exception ex)
{
return "ERROR: " + ex.Message;
}
Dictionary<string, object> values =
JsonConvert.DeserializeObject<Dictionary<string, object>>(token);
values.TryGetValue("access_token", out object extractedToken);
return extractedToken.ToString();
}
And here's an example of using the token:
public static async Task<List<Location>> GetLocations()
{
List<Location> result = new List<Location>();
string jsonResponse;
var client = new HttpClient();
string token = await RequestTokenAsync();
var request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri("https://whatever/v1/Locations"),
};
request.Headers.TryAddWithoutValidation("Authorization", "Bearer " + token);
try
{
using (var response = await client.SendAsync(request))
{
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
jsonResponse = body.ToString();
try
{
result = JsonConvert.DeserializeObject<List<Location>>(jsonResponse);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message + ex.StackTrace);
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message + ex.StackTrace);
}
return result;
}
Calling microsoft graph API https://graph.microsoft.com/v1.0/subscribedSkus fails with
"code": "Authorization_RequestDenied",
"message": "Insufficient privileges to complete the operation.",
This is happening if we create a new user in the tenant who is non admin. But while calling this with Admin user it works just fine. Even it works for any microsoft user in the tenant.
This is the below code I used to try.
public static async Task TestAadGraph()
{
// using obo token of the user.
var graphToken = await GetTokenAsync(UserId, Token, "https://graph.microsoft.com");
var aadGraphClient = new AadGraphClient(new HttpClient());
var licenseResponse = await aadGraphClient.GetTenantLicenseDetailAsync(graphToken);
foreach (var license in licenseResponse)
{
Console.WriteLine("Sku ID: {0}", license.SkuId);
Console.WriteLine("Sku Part Number: {0}", license.SkuPartNumber);
foreach (var plan in license.ServicePlans)
{
Console.WriteLine("Plan Id: {0}", plan.ServicePlanId);
Console.WriteLine("Plan Name: {0}", plan.ServicePlanName);
}
}
}
public async Task<SubscribedSku[]> GetTenantLicenseDetailAsync(string accessToken)
{
var request = new RequestMessage
{
BearerToken = accessToken,
Endpoint = new Uri("http://graph.microsoft.com/v1.0/subscribedSkus"),
};
var response = await _httpClient.FetchAsync<SubscribedSkusResponse>(request);
return response.Value;
}
public static async Task<T> FetchAsync<T>(this HttpClient httpClient, RequestMessage request, Action<HttpResponseMessage, string> responseCallback) where T : class
{
request.Method = request.Method ?? HttpMethod.Get;
request.MediaType = request.MediaType ?? "application/json";
using (HttpRequestMessage message = new HttpRequestMessage(request.Method,
UrlHelper.AppendParameters(request.Params, request.Endpoint)))
{
if (!string.IsNullOrEmpty(request.BearerToken))
{
message.Headers.Authorization = new AuthenticationHeaderValue("Bearer",
request.BearerToken);
}
if (request.Headers != null)
{
foreach (KeyValuePair<string, string> header in request.Headers)
{
message.Headers.Add(header.Key, header.Value);
}
}
if (!string.IsNullOrEmpty(request.Content))
{
message.Content = new StringContent(request.Content, Encoding.UTF8,
request.MediaType);
}`
using (HttpResponseMessage response = await httpClient.SendAsync(message))
{
string json = await response.Content.ReadAsStringAsync();
if (responseCallback != null)
{
responseCallback?.Invoke(response, json);
}
if (response.IsSuccessStatusCode)
{
if (predictor != null)
{
json = predictor(JToken.Parse(json)).ToString();
}
return JsonConvert.DeserializeObject<T>(json);
}
else
{
throw new WebRequestException(response, json);
}
}
}
}
Firstly, try the same call for the new created user in Microsoft Graph Explorer to see if the same scene exists. If not, it means the there is nothing wrong with the new user.
Then debug your code and copy the graphToken into https://jwt.io/ and see if the Decoded result has one of the required Delegated permissions:Organization.Read.All, Directory.Read.All, Organization.ReadWrite.All, Directory.ReadWrite.All, Directory.AccessAsUser.All. Note that the "upn" should be the username of the new created user.
If the required permissions do not exist, you will need to assign permissions in the Azure AD app. See API permissions.
Perfect. Adding permission to the first party app has actually worked.
I have to implement three-legged authentication in ASP.NET MVC. I have followed the steps according to the Blackboard documentation, especially the link https://community.blackboard.com/docs/DOC-3976-three-legged-oauth
I have received authorization code by calling the REST API /learn/api/public/v1/oauth2/authorizationcode.After that according to the documentation (I followed the documentation exactly but I don't know what am I have been missing ), I built a POST request to /learn/api/public/v1/oauth2/token to get access_token but I am unable to get access_token.
Instead, access_token, I have been receiving a BadRequest. This means I am making a mistake to build my second request but I am unable to fix the problem. I haven't found any code sample in .NET to implement three legged authentication for Blackboard Learn. Could you please help me to resolve the issue?
This is my code to call both APIs to receive access_token.
public class HomeController : Controller
{
public ActionResult Index()
{
// GET /learn/api/public/v1/oauth2/authorizationcode
Guid stateId = Guid.NewGuid();
string applicationKey = "Application key goes here";
string redirectUrl = string.Format("https://Blackboard Learn URL goes here/learn/api/public/v1/oauth2/authorizationcode" +
"?redirect_uri=https://localhost:44300/Home/OAuth2Response&response_type=code&client_id={0}&scope=read&state={1}",
applicationKey, stateId);
Response.Redirect(redirectUrl, true);
return View();
}
public async Task<bool> OAuth2Response(string code = null, string state = null, string error = null, string error_description = null)
{
bool success = true;
string json = string.Empty;
string urlCommand = string.Format("/learn/api/public/v1/oauth2/token?code={0}&redirect_url=https://localhost:44300/Home/OAuth2Response", code);
try
{
using (HttpClient client = new HttpClient())
{
var endpoint = new Uri("Blackboard Learn URL goes here" + urlCommand);
var postData = new List<KeyValuePair<string, string>>();
postData.Add(new KeyValuePair<string, string>("grant_type", "authorization_code"));
HttpContent body = new FormUrlEncodedContent(postData);
// POST /learn/api/public/v1/oauth2/token
using (HttpResponseMessage response = await client.PostAsync(endpoint, body)) // Problem is here
{
if (response.IsSuccessStatusCode)
{
json = await response.Content.ReadAsStringAsync();
}
else
{
success = false;
}
}
}
}
catch (Exception err)
{
//hopefully we never end up here, log this exception for forensics
success = false;
}
return success;
}
}
NOTE: I can successfully receive an access_token in Postman tool.
Finally, the below code works perfectly for 3 legged authentications in ASP.NET MVC.
public class HomeController : Controller
{
//https://blackboard.jiveon.com/docs/DOC-3976-three-legged-oauth
public ActionResult Index()
{
// GET /learn/api/public/v1/oauth2/authorizationcode
Guid stateId = Guid.NewGuid();
string applicationKey = "Application key goes here";
string redirectUrl = string.Format("Blackboard Learn URL goes here/learn/api/public/v1/oauth2/authorizationcode" +
"?redirect_uri=https://localhost:44300/Home/OAuth2Response&response_type=code&client_id={0}&scope=read&state={1}",
applicationKey, stateId);
Response.Redirect(redirectUrl, true);
return View();
}
public async Task<bool> OAuth2Response(string code = null, string state = null, string error = null, string error_description = null)
{
bool success = true;
string json = string.Empty;
string urlCommand = string.Format("/learn/api/public/v1/oauth2/token?code={0}&redirect_uri=https://localhost:44300/Home/OAuth2Response", code);
try
{
using (HttpClient client = new HttpClient())
{
var endpoint = new Uri("Blackboard Learn URL goes here" + urlCommand);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes("client_id:client_secret")));
var postData = new List<KeyValuePair<string, string>>();
postData.Add(new KeyValuePair<string, string>("grant_type", "authorization_code"));
HttpContent body = new FormUrlEncodedContent(postData);
using (HttpResponseMessage response = await client.PostAsync(endpoint, body))
{
if (response.IsSuccessStatusCode)
{
json = await response.Content.ReadAsStringAsync();
dynamic oauth2Result = Newtonsoft.Json.JsonConvert.DeserializeObject(json);
string access_token = oauth2Result.access_token;
string refresh_token = oauth2Result.refresh_token; }
else
{
success = false;
}
}
}
}
catch (Exception err) {
//hopefully we never end up here, log this exception for forensics
success = false;
}
return success;
}
}
I'm using Microsoft.Azure.Mobile SDK for implementing Server code.
Code in OWIN startup is as below:
public void ConfigureAuth(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
new MobileAppConfiguration().ApplyTo(config);
app.UseAppServiceAuthentication(new AppServiceAuthenticationOptions
{
SigningKey = ConfigurationManager.AppSettings["SigningKey"],
ValidAudiences = new[] { ConfigurationManager.AppSettings["ValidAudience"] },
ValidIssuers = new[] { ConfigurationManager.AppSettings["ValidIssuer"] },
TokenHandler = config.GetAppServiceTokenHandler()
});
app.UseWebApi(config);
}
Token generation code:
Claim[] claims = new Claim[]
{
new Claim("sub", "SampleSubject"),
new Claim("Id", Convert.ToString(Users[0].user_id)),
new Claim("name", Users[0].name),
new Claim("surname", Users[0].surname),
new Claim(ClaimTypes.Role, "user")
};
var token = AppServiceLoginHandler.CreateToken(claims, ConfigurationManager.AppSettings["SigningKey"], ConfigurationManager.AppSettings["ValidAudience"], ConfigurationManager.AppSettings["ValidIssuer"], TimeSpan.FromDays(30));
return token.RawData;
A sample JWT token is
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJHcnViZXJBUEkiLCJJZCI6IjMyODkwIiwibmFtZSI6IkRhdmlkZSIsInN1cm5hbWUiOiJCb25ldHRhIiwicm9sZSI6InVzZXIiLCJ2ZXIiOiIzIiwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6MjM1MzEvIiwiYXVkIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6MjM1MzIvIiwiZXhwIjoxNTAyNDYyNDQzLCJuYmYiOjE0OTk4NzA0NDN9.b5VhWzvkaEumutPZpLzImcAy4NotXCSgUIqLltVUQWI
The token is valid per below screenshot
And for below code,
[Authorize]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/values/5
public string Get(int id)
{
try
{
ClaimsPrincipal claims;
AppServiceTokenHandler s = new AppServiceTokenHandler(new HttpConfiguration());
s.TryValidateLoginToken(Request.Headers.Authorization.Parameter, ConfigurationManager.AppSettings["SigningKey"], new[] { ConfigurationManager.AppSettings["ValidAudience"] }, new[] { ConfigurationManager.AppSettings["ValidIssuer"] }, out claims);
AppServiceTokenHandler.ValidateToken(Request.Headers.Authorization.Parameter, ConfigurationManager.AppSettings["SigningKey"], ConfigurationManager.AppSettings["ValidAudience"], ConfigurationManager.AppSettings["ValidIssuer"]);
}
catch (Exception ex)
{
throw;
}
return "value";
}
'/Get' request fails with HTTP 401.
BUT For the same JWT token 'Get/5' returns HTTP 200 (validates token manually).
The problem is, when I use Authorize attribute, api returns 401.
According to your description, I checked this issue on my side.
Access the protected Web API
In summary, you could refer to the above screen shoot and check with your api endpoint. Additionally, you could refer to AppServiceTokenHandler.cs and HmacSigningCredentials.cs for implementing your custom TokenHandler to troubleshoot this issue. Moreover, you could refer to adrian hall's book about Custom Authentication
.
I am consuming an external web api through mvc controller with HttpClient. My web api do return json-formatted content.
How do i return the same json-formatted content of web api response in my mvc controller while consuming the web api? I am expecting something like this.
public async JsonResult GetUserMenu()
{
using (HttpClient client = new HttpClient())
{
client.BaseAddress = new Uri(url);
HttpResponseMessage response = await client.GetAsync(url);
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadAsJsonAsync();
}
}
}
Using Json.Net you could do something like this:
public async Task<JsonResult> GetUserMenu()
{
string result = string.Empty;
using (HttpClient client = new HttpClient())
{
client.BaseAddress = new Uri(url);
HttpResponseMessage response = await client.GetAsync(url);
if (response.IsSuccessStatusCode)
{
result = await response.Content.ReadAsStringAsync();
}
}
return Json(Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(result));
}
Here below an example of inserting log for action.
[HttpPost]
public async System.Threading.Tasks.Task<ActionResult> ChangePassword(ChangePasswordInfo model)
{
var omodelPwd = loginContext.UsersChangePasswordRequest(objAuthModel.oUsers.iID);
TempData[LKTransferDashboardCommon.Notification] = JsonConvert.SerializeObject(new Response { Success = false, ResponseString = "Invalid Old Password!" });
var auditLog = LKTransferDashboardCommon.PrepareAuditLogData(
"ChangePassword-Fail",
objAuthModel.oUsers.iID,
nameof(ChangePassword),
Request.ServerVariables["REMOTE_ADDR"],
"AdministrationController",
objAuthModel.oUsers.Name
);
await AuditLogHelper.ExecuteAsync(auditLog, null, null, null, null, null).ConfigureAwait(false);
return View(model);
}