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;
}
Related
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 am making the below request from a WPF app to an MVC Core app that acts as an API:
var formContent = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("userName", userName),
new KeyValuePair<string, string>("password", password)
});
////var content = new StringContent($"{{\"username\": {userName}, \"password\": {password}}}", Encoding.UTF8, "application/json");
var resp = await _client.PostAsync("api/Token", formContent);
var json = await resp.Content.ReadAsStringAsync();
var tw = JsonConvert.DeserializeObject<TokenWrapper>(json);
return tw.Token;
When I inspect resp with a breakpoint, after the PostAsync call, I see a 415 - Unsupported Media Type error. A breakpoint on the first line of the action isn't event hit, so I think the request isn't even reaching the controller.
The controller action looks like this:
public async Task<string> LoginAsync(string userName, string password)
{
var formContent = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("userName", userName),
new KeyValuePair<string, string>("password", password)
});
////var content = new StringContent($"{{\"username\": {userName}, \"password\": {password}}}", Encoding.UTF8, "application/json");
var resp = await _client.PostAsync("api/Token", formContent);
var tempContent = await resp.Content.ReadAsStringAsync();
var json = await resp.Content.ReadAsStringAsync();
var tw = JsonConvert.DeserializeObject<TokenWrapper>(json);
return tw.Token;
}
I would expect FormUrlEncodedContent to imply the content type and work, as in the high number of examples I have seen like this. Why am I getting this 415 error?
Try by setting the Media Typeas below:
var content = new StringContent(formContent.ToString(), Encoding.UTF8, "application/json");
var result = await _client.PostAsync("http://example.com/api/Token", content);
Also PostAsync accepts parameters of requestUri and content , your code is the missing Absolute requestUri.
I created both a MVC 5 web app hosted on Azure and a WPF client. My short term purpose (as if I can achieve that I'll be able to implement all my uses case) is the following:
Enforce Azure Ad authentification on the WPF client
Have the MVC web app to check through Azure Graph API the AD group membership of the user authentified in the client
Send back Graph API object to the client (IUser, Group...)
Use group membership to define Authorization on controllers
My actual issue is the following:
The user launch the app, and is prompted for authentication. I guess it work as I can display the user's mail and I have an access token.
The user tries to access a web api controller and it works fine
The user tries to access another web api controller decorated with [Authorize] and i get in return some HTML page stating this : "We can't sign you in.Your browser is currently set to block JavaScript. You need to allow JavaScript to use this service."
From what I've found searching on the web it seems that it could be related to my web app that is not configured properly (I already tried to add my webapp url in trusted sites and I'm sure that my controller URL is Ok). i cannot find much doc on native client + AAD + MVC so I don't really know how to correct it.
Here's my startup.auth.cs from the webapp :
public partial class Startup
{
private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
private static string appKey = ConfigurationManager.AppSettings["ida:AppKey"];
private static string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
private static string tenant = ConfigurationManager.AppSettings["ida:Tenant"];
private static string tenantId = ConfigurationManager.AppSettings["ida:TenantId"];
private static string postLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];
private static string certName = ConfigurationManager.AppSettings["ida:CertName"];
public static readonly string Authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
string graphResourceId = ConfigurationManager.AppSettings["ida:GraphUrl"];
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = Authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
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;
#region Certs (not used)
if (certName.Length != 0)
{
// Create a Client Credential Using a Certificate
//
// Initialize the Certificate Credential to be used by ADAL.
// First find the matching certificate in the cert store.
//
X509Certificate2 cert = null;
X509Store store = new X509Store(StoreLocation.CurrentUser);
try
{
store.Open(OpenFlags.ReadOnly);
// Place all certificates in an X509Certificate2Collection object.
X509Certificate2Collection certCollection = store.Certificates;
// Find unexpired certificates.
X509Certificate2Collection currentCerts = certCollection.Find(X509FindType.FindByTimeValid, DateTime.Now, false);
// From the collection of unexpired certificates, find the ones with the correct name.
X509Certificate2Collection signingCert = currentCerts.Find(X509FindType.FindBySubjectDistinguishedName, certName, false);
if (signingCert.Count == 0)
{
// No matching certificate found.
return Task.FromResult(0);
}
// Return the first certificate in the collection, has the right name and is current.
cert = signingCert[0];
}
finally
{
store.Close();
}
// Then create the certificate credential.
ClientAssertionCertificate credential = new ClientAssertionCertificate(clientId, cert);
string userObjectID = context.AuthenticationTicket.Identity.FindFirst(
"http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
AuthenticationContext authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectID));
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId);
AuthenticationHelper.token = result.AccessToken;
}
#endregion
else
{
// Create a Client Credential Using an Application Key
ClientCredential credential = new ClientCredential(clientId, appKey);
string userObjectID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
AuthenticationContext authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectID));
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId);
AuthenticationHelper.token = result.AccessToken;
}
return Task.FromResult(0);
}
}
});
}
}
Here's the controller which can be acceded when not decorated with [Authorize] but in that case the action throw a null exception (but if I can't get it fixed i'll post another question):
[System.Web.Http.Authorize]
public class UserADGraphController : ApiController
{
[ResponseType(typeof(IUser))]
[System.Web.Http.Route("api/UserADGraphController/GetMyInformations")]
public IHttpActionResult GetMyInformations()
{
try
{
string uID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
if (uID == null)
return Ok("UId null");
ActiveDirectoryClient client = AuthenticationHelper.GetActiveDirectoryClient();
if (client == null)
return Ok("Client null");
IUser adUser = client.Users.Where(u => u.ObjectId == uID).ExecuteAsync().Result.CurrentPage.SingleOrDefault();
if (adUser == null)
{
return NotFound();
}
return Ok(adUser);
}
catch (Exception e)
{
return Ok(e.Message + " " + e.StackTrace);
}
and finally here are relevant parts of the client:
In the mainviewmodel class:
#region Azure AD auth properties
private static string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
private static string tenant = ConfigurationManager.AppSettings["ida:Tenant"];
private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
Uri redirectUri = new Uri(ConfigurationManager.AppSettings["ida:RedirectUri"]);
private static string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
private static string AppServiceResourceId = ConfigurationManager.AppSettings["todo:AppServiceResourceId"];
private static string AppServiceBaseAddress = ConfigurationManager.AppSettings["todo:AppServiceBaseAddress"];
private HttpClient httpClient;
private AuthenticationContext authContext = null;
#endregion
In the mainviewmodel constructor:
authContext = new AuthenticationContext(authority);
httpClient = new HttpClient();
My sign in method:
{
AuthenticationResult result = null;
try
{
result = authContext.AcquireToken(AppServiceResourceId, clientId, redirectUri, PromptBehavior.Auto);
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
SignInLabelContent = "Connected to azure AD as " + result.UserInfo.DisplayableId;
}
catch (AdalException ex)
{
if (ex.ErrorCode == "user_interaction_required")
{
}
else
{
// An unexpected error occurred.
string message = ex.Message;
if (ex.InnerException != null)
{
message += "Inner Exception : " + ex.InnerException.Message;
}
Messenger.Default.Send<NotificationMessage>(new NotificationMessage(message));
//MessageBox.Show(message);
}
return;
}
}
The method that access the protected controller:
IUser me = null;
AuthenticationResult result = null;
result = authContext.AcquireToken(AppServiceResourceId, clientId, redirectUri, PromptBehavior.Auto);
string authHeader = result.CreateAuthorizationHeader();
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
//HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, AppServiceBaseAddress + "/api/UserADGraphController/GetMyInformations");
//request.Headers.TryAddWithoutValidation("Authorization", authHeader);
//HttpResponseMessage response = await client.SendAsync(request);
//string responseString = await response.Content.ReadAsStringAsync();
//LogManager.log(responseString);
//Messenger.Default.Send<NotificationMessage>(new NotificationMessage(responseString));
HttpResponseMessage response = await httpClient.GetAsync(AppServiceBaseAddress + "/api/UserADGraphController/GetMyInformations");
if (response.IsSuccessStatusCode)
{
var jsonString = await response.Content.ReadAsStringAsync();
LogManager.log(jsonString);
me = JsonConvert.DeserializeObject<IUser>(jsonString);
//Messenger.Default.Send<NotificationMessage>(new NotificationMessage(jsonString));
}
In my case response has status code 200 but the jsonString contains the web page telling me about javascript disabled.
If someone has an idea it would be great !
Thanks !
If someone gets into this issue, I managed to solve it by changing my configureAuth method this way :
var azureADBearerAuthOptions = new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Tenant = tenant
};
azureADBearerAuthOptions.TokenValidationParameters = new TokenValidationParameters()
{
ValidAudience = audience
};
app.UseWindowsAzureActiveDirectoryBearerAuthentication(azureADBearerAuthOptions);
This error message is very misleading. I was getting the same problem, and found that my issue was actually mismatched Client Secret/AppURI settings.
From the error message, I assumed it was something I was doing in the ConfigureAuth method. Turns out I was mixing up dev and test settings.
Maybe this will help others who end up on this confusing error message.
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);
}
My windows phone client sends a piece of JSON that represents the Name and Score... Should I be using the Class from the Web API instead ? What is the code to send the object to the server rather than the raw json?
private void btnCreateSampleScore_Click(object sender, RoutedEventArgs e)
{
using (var client = new HttpClient())
{
client.BaseAddress = new Uri("http://punkoutersoftware.azurewebsites.net");
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("Bob", "2.65")
});
var result = client.PostAsync("/api/DrunkMeterScore", content).Result;
string resultContent = result.Content.ReadAsStringAsync().Result;
Console.WriteLine(resultContent);
//DrunkMeterScore dms = new DrunkMeterScore();
//dms.Name = "Bob";
//dms.Score = 2.42;
}
}
The server is using the plain Web API template
// POST api/DrunkMeterScore
public HttpResponseMessage PostDrunkMeterScore(DrunkMeterScore drunkmeterscore)
{
if (ModelState.IsValid)
{
db.DrunkMeterScores.Add(drunkmeterscore);
db.SaveChanges();
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.Created, drunkmeterscore);
response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = drunkmeterscore.DrunkMeterScoreId }));
return response;
}
else
{
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
}
}
edit1:
I tried both of these.. They both hang and never come back during the PostAsync calls :(
var client = new HttpClient();
client.BaseAddress = new Uri("http://punkoutersoftware.azurewebsites.net");
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
var score = new DrunkMeterScore() { Name = "Joe", Score = 2.67 };
//MediaTypeFormatter jsonFormatter = new JsonMediaTypeFormatter();
//HttpContent content = new ObjectContent<DrunkMeterScore>(score, jsonFormatter);
//var resp = client.PostAsync("api/DrunkMeterScore", content).Result;
//Uri scoreUri = null;
//HttpResponseMessage response = client.PostAsJsonAsync("api/DrunkMeterScore", score).Result;
//if (response.IsSuccessStatusCode)
//{
// scoreUri = response.Headers.Location;
//}
//else
//{
// Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase);
//}
To serialize objects you need the ObjectContent class which for the wp8 is only available as an alpha release.
Use the package console to do,
Install-Package Microsoft.AspNet.WebApi.Client -Pre
Should be something like this. I did this from memory so YMMV.
DrunkMeterScore dms = new DrunkMeterScore();
dms.Name = "Bob";
dms.Score = 2.42;
var content = new ObjectContent(dms, new JsonMediaTypeFormatter());
var result = client.PostAsync("/api/DrunkMeterScore", content).Result;