Azure Functions OAuth2 from email / password stored in database? - c#

I have a database that contains emails and password hashes.
I would like to secure http trigger's from Azure Functions to allow only authorized call thanks to the Authorization header with a BEARER token.
I think I will need
an http trigger that will generate the token from email/password
Authorize and authenticate the user based on the Authorization header
Can someone get me started on how to create a custom authentication provider or use an existing one and configure Azure Functions to work with it?

Microsoft identity platform supports the OAuth 2.0 Resource Owner Password Credentials (ROPC) grant, which allows an application to sign in the user by directly handling their password.
Get the email(username) and password from database, and send the following request to receive the access token.
POST {tenant}/oauth2/v2.0/token
Host: login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded
client_id=6731de76-14a6-49ae-97bc-6eba6914391e
&scope=user.read%20openid%20profile%20offline_access
&username=MyUsername#myTenant.com
&password=SuperS3cret
&grant_type=password

You could have look following code snippet, I have tested on azure portal , Azure Function V2:
#r "Newtonsoft.Json"
using Newtonsoft.Json;
using System.Net;
using System.Net.Http.Headers;
public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
try
{
//Parse query parameter
log.LogInformation("C# HTTP trigger function processed a request.");
//Read Request Body
var content = await new StreamReader(req.Body).ReadToEndAsync();
//Extract Request Body and Parse To Class
UserAuthentication objUserInfo = JsonConvert.DeserializeObject<UserAuthentication>(content);
//Message Container
dynamic validationMessage;
//Validate required param
if (string.IsNullOrEmpty(objUserInfo.UserName.Trim()))
{
validationMessage = new OkObjectResult("User name is required!");
return (IActionResult)validationMessage;
}
if (string.IsNullOrEmpty(objUserInfo.Password.Trim()))
{
validationMessage = new OkObjectResult("Password is required!");
return (IActionResult)validationMessage;
}
// Authentication Token Request format
string tokenUrl = $"https://login.microsoftonline.com/common/oauth2/token";
var tokenRequest = new HttpRequestMessage(HttpMethod.Post, tokenUrl);
tokenRequest.Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "password",
["client_id"] = "YourApplicationId",
["client_secret"] = "YourApplicationPassword",
["resource"] = "https://graph.microsoft.com",
["username"] = "" + objUserInfo.UserName + "",
["password"] = "" + objUserInfo.Password + ""
});
// Request For Token Endpoint
using (var _client = new HttpClient())
{
var tokenResponse = await _client.SendAsync(tokenRequest);
AccessTokenClass objAccessToken = JsonConvert.DeserializeObject<AccessTokenClass>(await tokenResponse.Content.ReadAsStringAsync());
// When Token Request Null
if (objAccessToken.access_token == null)
{
validationMessage = new OkObjectResult("Invalid Authentication! Please Check Your Credentials And Try Again!");
return (IActionResult)validationMessage;
}
else
{
return new OkObjectResult(objAccessToken.access_token);
}
}
}
catch (Exception ex)
{
validationMessage = new OkObjectResult("Sorry something went wrong! Please check your given information and try again! {0}" + ex.Message);
return (IActionResult)validationMessage;
}
}
Class I have Used:
UserAuthentication Class
public class UserAuthentication
{
public string UserName { get; set; }
public string Password { get; set; }
}
public class AzureFunctionCreateUserClass
{
public string access_token { get; set; }
public string expires_in { get; set; }
public string token_type { get; set; }
public string resource { get; set; }
}
Note: This an sample for azure portal which I have written on azure function . So try to run on there.
Hope this would help.

Related

c# WebAPI API Issue - Swagger - Angular Axios

I have an issue and I got no idea what to do, the issue is that on my C# WebApi app with swagger enabled.
i have a few apis but here is an example 1 of them.
[HttpPost]
[Route("/api/user/register")]
public UserSession Register(string email, string password, string confirm_password)
{
if (password != confirm_password)
{
return new UserSession()
{
Success = false,
Message = "Error Passwords don't match",
SessionKey = "",
};
}
// success code here
}
here is the angular API.
import axios from 'axios';
export class API {
private static base_api:string = "http://localhost:51019/";
static register(email:string, password:any, confirm_password:any) {
let url = this.base_api + "api/user/register";
let data = {
email: email,
password: password,
confirm_password: confirm_password,
};
const headers = {
'Content-Type': 'application/json'
};
let result = axios.post(url, data, {headers}).then(x=>{return x}).catch(x=>{return false;});
console.log(result);
}
}
even when I provide an email and/or a password, it's like the API isn't receiving the data?
to fix the cor issue i had i added this to the api controller
[HttpOptions]
[Route("/api/user/register")]
[Route("/api/user/login")]
[Route("/api/user/logout")]
public HttpResponseMessage Options()
{
var response = new HttpResponseMessage();
response.StatusCode = HttpStatusCode.OK;
return response;
}
if i access the api via the swagger ui via (http://localhost:51019/swagger/index.html)
then when i perform the api via the UI it works correctly.
TIA
Typically you'd create a model and set it as the body:
public sealed class RegisterModel
{
[JsonPropertyName("email")]
public string Email { get; set; }
[JsonPropertyName("password")]
public string Password { get; set; }
[JsonPropertyName("confirm_password")]
public string ConfirmPassword { get; set; }
}
[HttpPost, Route("/api/user/register")]
public UserSession Register([FromBody] RegisterModel register)
{
// ...
}
That will get it to work in all scenarios

WebAPI Token Authorization Bearer Getting Error: The operation was canceled

I am working on .NET Core WebAPI (Core 3.1) project where I have to make call to outside third party API's using OAuth2 Toekn based implementation.
Basically it is 2-Step process. First step is to get Token using Token API Call and then make call to Export API by passing this Token-AccessCode and Input JSON.
I am sucessfully able to get Token using below API Controller Code, but second API call(Export API Method) is throwing error.
Where as when i tested in Postman using Bearer Authorization, it worked fine.
Step(1) In Postman, Authorization tab is set to "No Auth".
Step(2) Header Tab has two settings as below (Authorization and Content-Type).
Step(3) From Body Tab, I am sending below JSON.
{
"oneOf": {
"schedule_date": "08/05/2020",
"request_date": null
},
"start_time": "",
"end_time": "",
"order_number": "",
"status": ""
}
After the above 3 settings, when i call POST method in Postman, it is returning response.
Where as below APICode is throwing error at Line exportClient.PostAsync() call.
Error:
**Exception:**The operation was canceled.
InnerException: Unable to read data from the transport connection: The I/O operation has been aborted because of either a thread exit or an application request..
What am i doing wrong?
Appreciate your resposes.
This is the APIController Code:
[HttpGet]
[Route("PullOrders")]
public async Task<IActionResult> PullOrders(
)
{
try
{
var token = await GetElibilityToken();
string exportUrl = "https://XXXXXXX.com/api/v1/export";
var exportClient = new HttpClient();
//exportClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + token.AccessToken);
exportClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken);
exportClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var oneData = new OneOf() { schedule_date = "08/05/2020" };
var orderReq = new PullOrderInputRequest() { oneOf = oneData };
var json = JsonConvert.SerializeObject(orderReq, Newtonsoft.Json.Formatting.Indented);
var stringContent = new StringContent(json);
HttpResponseMessage orderResponse = await **exportClient.PostAsync**(exportUrl, stringContent);
}
catch (Exception ex)
{
throw ex;
}
return Ok();
}
private static async Task<Token> GetElibilityToken()
{
var tokenClient = new HttpClient();
string baseAddress = #"https:// XXXXXXX/api/v1/oauth2/token";
string grant_type = "client_credentials";
string client_id = " XXXXXXX";
string client_secret = " XXXXXXXXXXXXXX ";
var form = new Dictionary<string, string>
{
{"grant_type", grant_type},
{"client_id", client_id},
{"client_secret", client_secret},
};
HttpResponseMessage tokenResponse = await tokenClient.PostAsync(baseAddress, new FormUrlEncodedContent(form));
var jsonContent = await tokenResponse.Content.ReadAsStringAsync();
Token tok = JsonConvert.DeserializeObject<Token>(jsonContent);
return tok;
}
internal class Token
{
[JsonProperty("access_token")]
public string AccessToken { get; set; }
[JsonProperty("token_type")]
public string TokenType { get; set; }
[JsonProperty("expires_in")]
public int ExpiresIn { get; set; }
[JsonProperty("refresh_token")]
public string RefreshToken { get; set; }
}

Calling an Microsoft Graph API for token gives error "AADSTS900144: The request body must contain the following parameter: 'grant_type'

I am calling a Graph API URL
https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token
to get an access token but I am getting the following response.
{
"error": "invalid_request",
"error_description": "AADSTS900144: The request body must contain the following parameter: 'grant_type'.\r\nTrace ID: 5ff6b053-9011-4397-89ff-fdb6f31e4600\r\nCorrelation ID: 22509847-199d-4bd8-a083-b29d8bbf3139\r\nTimestamp: 2020-04-01 11:14:00Z",
"error_codes": [
900144
],
"timestamp": "2020-04-01 11:14:00Z",
"trace_id": "5ff6b053-9011-4397-89ff-fdb6f31e4600",
"correlation_id": "22509847-199d-4bd8-a083-b29d8bbf3139",
"error_uri": "https://login.microsoftonline.com/error?code=900144"
}
I have an active tenantid, I have an application registered, and I have an active user for the above application say user#tenant.onmicrosoft.com; that user has ALL the roles (Global Administrator).
Please find below Postman's request and Response.
PostmanSnap
Also I have given API permission as suggested in https://learn.microsoft.com/en-us/graph/api/group-post-members?view=graph-rest-1.0&tabs=http
Problem: I have successfully reproduced your error. As you seen below:
Solution:
You are trying in wrong way. You have to send required parameter in form-data on postman with key-value pairs like below format:
grant_type:client_credentials
client_id:b6695c7be_YourClient_Id_e6921e61f659
client_secret:Vxf1SluKbgu4PF0Nf_Your_Secret_Yp8ns4sc=
scope:https://graph.microsoft.com/.default
Code Snippet:
//Token Request End Point
string tokenUrl = $"https://login.microsoftonline.com/YourTenant.onmicrosoft.com/oauth2/v2.0/token";
var tokenRequest = new HttpRequestMessage(HttpMethod.Post, tokenUrl);
//I am Using client_credentials as It is mostly recommended
tokenRequest.Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "client_credentials",
["client_id"] = "b6695c7be_YourClient_Id_e6921e61f659",
["client_secret"] = "Vxf1SluKbgu4PF0Nf_Your_Secret_Yp8ns4sc=",
["scope"] = "https://graph.microsoft.com/.default"
});
dynamic json;
AccessTokenClass results = new AccessTokenClass();
HttpClient client = new HttpClient();
var tokenResponse = await client.SendAsync(tokenRequest);
json = await tokenResponse.Content.ReadAsStringAsync();
results = JsonConvert.DeserializeObject<AccessTokenClass>(json);
Class Used:
public class AccessTokenClass
{
public string token_type { get; set; }
public string expires_in { get; set; }
public string resource { get; set; }
public string access_token { get; set; }
}
You could refer to Official document
Hope that would help. If you still have any concern feel free to share.

Azure AD Access Token Request

In my WebAssembly Blazor App, I need to access an API that I'm developing and Microsoft.Graph.
As I understood, I cannot use the same bearer token for 2 different resources (my API and Graph).
I setup the access to my API with MSAL in Program.cs
builder.Services.AddBaseAddressHttpClient();
builder.Services.AddMsalAuthentication(options =>
{
var authentication = options.ProviderOptions.Authentication;
authentication.Authority = "https://login.microsoftonline.com/xxx";
authentication.ClientId = "xxx";
options.ProviderOptions.DefaultAccessTokenScopes.Add("xxx/user_impersonation");
});
And I'm trying to get the token for the Graph API directly when I need it (following this):
internal class Token
{
[JsonProperty("access_token")]
public string AccessToken { get; set; }
[JsonProperty("token_type")]
public string TokenType { get; set; }
[JsonProperty("expires_in")]
public int ExpiresIn { get; set; }
[JsonProperty("refresh_token")]
public string RefreshToken { get; set; }
}
private static async Task<Token> GetElibilityToken(HttpClient client)
{
string baseAddress = #"https://login.microsoftonline.com/xxx/oauth2/v2.0/token";
string grant_type = "authorization_code";
string client_id = "xxx";
string client_secret = "==xxx";
string scope = "https://graph.microsoft.com/.default";
var form = new Dictionary<string, string>
{
{"grant_type", grant_type},
{"client_id", client_id},
{"client_secret", client_secret},
{"scope", scope }
};
HttpResponseMessage tokenResponse = await client.PostAsync(baseAddress, new FormUrlEncodedContent(form));
var jsonContent = await tokenResponse.Content.ReadAsStringAsync();
Token tok = JsonConvert.DeserializeObject<Token>(jsonContent);
return tok;
}
Is the approach correct? Is there a better one?
Should I register 2 IAccessTokenProvider in Program.cs? How?
The problem I have is that I keep getting the error:
Access to fetch at 'https://login.microsoftonline.com/xxx/oauth2/v2.0/token' from origin 'https://localhost:xxx' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
dotnet.3.2.0-preview2.20159.2.js:1 POST https://login.microsoftonline.com/xxx/oauth2/v2.0/token net::ERR_FAILED
How do I setup CORS in my request?
Use the options to specify the scopes, each time you request a new token:
IAccessTokenProvider authService; /* inject your IAccessTokenProvider */
var tokenResult = await authService .RequestAccessToken(
new AccessTokenRequestOptions
{
ReturnUrl = "...",
Scopes = new string[] { "..." }
});

How to store the token received in AcquireTokenAsync with Active Directory

Problem Statement
I am using .NET Core, and I'm trying to make a web application talk to a web API. Both require authentication using the [Authorize] attribute on all of their classes. In order to be able to talk between them server-to-server, I need to retrieve the validation token. I've been able to do that thanks to a Microsoft tutorial.
Problem
In the tutorial, they use a call to AcquireTokenByAuthorizationCodeAsync in order to save the token in the cache, so that in other places, the code can just do a AcquireTokenSilentAsync, which doesn't require going to the Authority to validate the user.
This method does not lookup token cache, but stores the result in it, so it can be looked up using other methods such as AcquireTokenSilentAsync
The issue comes in when the user is already logged in. The method stored at OpenIdConnectEvents.OnAuthorizationCodeReceived never gets called, since there is no authorization being received. That method only gets called when there's a fresh login.
There is another event called: CookieAuthenticationEvents.OnValidatePrincipal when the user is only being validated via a cookie. This works, and I can get the token, but I have to use AcquireTokenAsync, since I don't have the authorization code at that point. According to the documentation, it
Acquires security token from the authority.
This makes calling AcquireTokenSilentAsync fail, since the token has not been cached. And I'd rather not always use AcquireTokenAsync, since that always goes to the Authority.
Question
How can I tell the token gotten by AcquireTokenAsync to be cached so that I can use AcquireTokenSilentAsync everywhere else?
Relevant code
This all comes from the Startup.cs file in the main, Web Application project.
This is how the event handling is done:
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
Events = new CookieAuthenticationEvents()
{
OnValidatePrincipal = OnValidatePrincipal,
}
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
ClientId = ClientId,
Authority = Authority,
PostLogoutRedirectUri = Configuration["AzureAd:PostLogoutRedirectUri"],
ResponseType = OpenIdConnectResponseType.CodeIdToken,
CallbackPath = Configuration["Authentication:AzureAd:CallbackPath"],
GetClaimsFromUserInfoEndpoint = false,
Events = new OpenIdConnectEvents()
{
OnRemoteFailure = OnAuthenticationFailed,
OnAuthorizationCodeReceived = OnAuthorizationCodeReceived,
}
});
And these are the events behind:
private async Task OnValidatePrincipal(CookieValidatePrincipalContext context)
{
string userObjectId = (context.Principal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value;
ClientCredential clientCred = new ClientCredential(ClientId, ClientSecret);
AuthenticationContext authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectId, context.HttpContext.Session));
AuthenticationResult authResult = await authContext.AcquireTokenAsync(ClientResourceId, clientCred);
// How to store token in authResult?
}
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
{
// Acquire a Token for the Graph API and cache it using ADAL. In the TodoListController, we'll use the cache to acquire a token to the Todo List API
string userObjectId = (context.Ticket.Principal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value;
ClientCredential clientCred = new ClientCredential(ClientId, ClientSecret);
AuthenticationContext authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectId, context.HttpContext.Session));
AuthenticationResult authResult = await authContext.AcquireTokenByAuthorizationCodeAsync(
context.ProtocolMessage.Code, new Uri(context.Properties.Items[OpenIdConnectDefaults.RedirectUriForCodePropertiesKey]), clientCred, GraphResourceId);
// Notify the OIDC middleware that we already took care of code redemption.
context.HandleCodeRedemption();
}
// Handle sign-in errors differently than generic errors.
private Task OnAuthenticationFailed(FailureContext context)
{
context.HandleResponse();
context.Response.Redirect("/Home/Error?message=" + context.Failure.Message);
return Task.FromResult(0);
}
Any other code can be found in the linked tutorial, or ask and I will add it to the question.
(Note: I had been struggling with this exact issue for several days. I followed the same Microsoft Tutorial as the one linked in the question, and tracked various problems like a wild goose chase; it turns out the sample contains a whole bunch of seemingly unnecessary steps when using the latest version of the Microsoft.AspNetCore.Authentication.OpenIdConnect package.).
I eventually had a breakthrough moment when I read this page:
http://docs.identityserver.io/en/release/quickstarts/5_hybrid_and_api_access.html
The solution essentially involves letting OpenID Connect auth put the various tokens (access_token, refresh_token) into the cookie.
Firstly, I'm using a Converged Application created at https://apps.dev.microsoft.com and v2.0 of the Azure AD endpoint. The App has an Application Secret (password/public key) and uses Allow Implicit Flow for a Web platform.
(For some reason it seems as if v2.0 of the endpoint doesn't work with Azure AD only applications. I'm not sure why, and I'm not sure if it really matters anyway.)
Relevant lines from the Startup.Configure method:
// Configure the OWIN pipeline to use cookie auth.
app.UseCookieAuthentication(new CookieAuthenticationOptions());
// Configure the OWIN pipeline to use OpenID Connect auth.
var openIdConnectOptions = new OpenIdConnectOptions
{
ClientId = "{Your-ClientId}",
ClientSecret = "{Your-ClientSecret}",
Authority = "http://login.microsoftonline.com/{Your-TenantId}/v2.0",
ResponseType = OpenIdConnectResponseType.CodeIdToken,
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
},
GetClaimsFromUserInfoEndpoint = true,
SaveTokens = true,
};
openIdConnectOptions.Scope.Add("offline_access");
app.UseOpenIdConnectAuthentication(openIdConnectOptions);
And that's it! No OpenIdConnectOptions.Event callbacks. No calls to AcquireTokenAsync or AcquireTokenSilentAsync. No TokenCache. None of those things seem to be necessary.
The magic seems to happen as part of OpenIdConnectOptions.SaveTokens = true
Here's an example where I'm using the access token to send an e-mail on behalf of the user using their Office365 account.
I have a WebAPI controller action which obtains their access token using HttpContext.Authentication.GetTokenAsync("access_token"):
[HttpGet]
public async Task<IActionResult> Get()
{
var graphClient = new GraphServiceClient(new DelegateAuthenticationProvider(async requestMessage =>
{
var accessToken = await HttpContext.Authentication.GetTokenAsync("access_token");
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
}));
var message = new Message
{
Subject = "Hello",
Body = new ItemBody
{
Content = "World",
ContentType = BodyType.Text,
},
ToRecipients = new[]
{
new Recipient
{
EmailAddress = new EmailAddress
{
Address = "email#address.com",
Name = "Somebody",
}
}
},
};
var request = graphClient.Me.SendMail(message, true);
await request.Request().PostAsync();
return Ok();
}
Side Note #1
At some point you might also need to get hold of the refresh_token too, in case the access_token expires:
HttpContext.Authentication.GetTokenAsync("refresh_token")
Side Note #2
My OpenIdConnectOptions actually includes a few more things which I've omitted here, for example:
openIdConnectOptions.Scope.Add("email");
openIdConnectOptions.Scope.Add("Mail.Send");
I've used these for working with the Microsoft.Graph API to send an e-mail on behalf of the currently logged in user.
(Those delegated permissions for Microsoft Graph are set up on the app too).
Update - How to 'silently' Refresh the Azure AD Access Token
So far, this answer explains how to use the cached access token but not what to do when the token expires (typically after 1 hour).
The options seem to be:
Force the user to sign in again. (Not silent)
POST a request to the Azure AD service using the refresh_token to obtain a new access_token (silent).
How to Refresh the Access Token using v2.0 of the Endpoint
After more digging, I found part of the answer in this SO Question:
How to handle expired access token in asp.net core using refresh token with OpenId Connect
It seems like the Microsoft OpenIdConnect libraries do not refresh the access token for you. Unfortunately the answer in the question above is missing the crucial detail about precisely how to refresh the token; presumably because it depends on specific details about Azure AD which OpenIdConnect doesn't care about.
The accepted answer to the above question suggests sending a request directly to the Azure AD Token REST API instead of using one of the Azure AD libraries.
Here's the relevant documentation (Note: this covers a mix of v1.0 and v2.0)
https://developer.microsoft.com/en-us/graph/docs/concepts/rest
https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-protocols-oauth-code#refreshing-the-access-tokens
Here's a proxy based on the API docs:
public class AzureAdRefreshTokenProxy
{
private const string HostUrl = "https://login.microsoftonline.com/";
private const string TokenUrl = $"{Your-Tenant-Id}/oauth2/v2.0/token";
private const string ContentType = "application/x-www-form-urlencoded";
// "HttpClient is intended to be instantiated once and re-used throughout the life of an application."
// - MSDN Docs:
// https://msdn.microsoft.com/en-us/library/system.net.http.httpclient(v=vs.110).aspx
private static readonly HttpClient Http = new HttpClient {BaseAddress = new Uri(HostUrl)};
public async Task<AzureAdTokenResponse> RefreshAccessTokenAsync(string refreshToken)
{
var body = $"client_id={Your-Client-Id}" +
$"&refresh_token={refreshToken}" +
"&grant_type=refresh_token" +
$"&client_secret={Your-Client-Secret}";
var content = new StringContent(body, Encoding.UTF8, ContentType);
using (var response = await Http.PostAsync(TokenUrl, content))
{
var responseContent = await response.Content.ReadAsStringAsync();
return response.IsSuccessStatusCode
? JsonConvert.DeserializeObject<AzureAdTokenResponse>(responseContent)
: throw new AzureAdTokenApiException(
JsonConvert.DeserializeObject<AzureAdErrorResponse>(responseContent));
}
}
}
The AzureAdTokenResponse and AzureAdErrorResponse classes used by JsonConvert:
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
public class AzureAdTokenResponse
{
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "token_type", Required = Required.Default)]
public string TokenType { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "expires_in", Required = Required.Default)]
public int ExpiresIn { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "expires_on", Required = Required.Default)]
public string ExpiresOn { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "resource", Required = Required.Default)]
public string Resource { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "access_token", Required = Required.Default)]
public string AccessToken { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "refresh_token", Required = Required.Default)]
public string RefreshToken { get; set; }
}
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
public class AzureAdErrorResponse
{
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "error", Required = Required.Default)]
public string Error { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "error_description", Required = Required.Default)]
public string ErrorDescription { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "error_codes", Required = Required.Default)]
public int[] ErrorCodes { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "timestamp", Required = Required.Default)]
public string Timestamp { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "trace_id", Required = Required.Default)]
public string TraceId { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "correlation_id", Required = Required.Default)]
public string CorrelationId { get; set; }
}
public class AzureAdTokenApiException : Exception
{
public AzureAdErrorResponse Error { get; }
public AzureAdTokenApiException(AzureAdErrorResponse error) :
base($"{error.Error} {error.ErrorDescription}")
{
Error = error;
}
}
Finally, my modifications to Startup.cs to refresh the access_token
(Based on the answer I linked above)
// Configure the OWIN pipeline to use cookie auth.
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = OnValidatePrincipal
},
});
The OnValidatePrincipal handler in Startup.cs (Again, from the linked answer above):
private async Task OnValidatePrincipal(CookieValidatePrincipalContext context)
{
if (context.Properties.Items.ContainsKey(".Token.expires_at"))
{
if (!DateTime.TryParse(context.Properties.Items[".Token.expires_at"], out var expiresAt))
{
expiresAt = DateTime.Now;
}
if (expiresAt < DateTime.Now.AddMinutes(-5))
{
var refreshToken = context.Properties.Items[".Token.refresh_token"];
var refreshTokenService = new AzureAdRefreshTokenService();
var response = await refreshTokenService.RefreshAccessTokenAsync(refreshToken);
context.Properties.Items[".Token.access_token"] = response.AccessToken;
context.Properties.Items[".Token.refresh_token"] = response.RefreshToken;
context.Properties.Items[".Token.expires_at"] = DateTime.Now.AddSeconds(response.ExpiresIn).ToString(CultureInfo.InvariantCulture);
context.ShouldRenew = true;
}
}
}
Finally, a solution with OpenIdConnect using v2.0 of the Azure AD API.
Interestingly, it seems that v2.0 does not ask for a resource to be included in the API request; the documentation suggests it's necessary, but the API itself simply replies that resource is not supported. This is probably a good thing - presumably it means that the access token works for all resources (it certainly works with the Microsoft Graph API)

Categories

Resources