Introduction
Recently I faced a problem integrating OAuth2 authentication method into .NET Core MVC project, because the original Microsoft Owin libraries are not supported in the newest Microsoft .NET Core framework (the .NET Core app 1.0). I created a simple solution, to authenticate user for connecting to the Microsoft Graph API services. I'd like to share my experience with others, hopefully this would save someones time.
Background
When integrating Microsoft Graph API services into .NET Core MVC application, I faced a blocking issue, because Microsoft.Owin libraries, provided by Microsoft are incompatible with .NET core applications. That's why I've spent hours or even maybe several days to set out a proper solution. The solution is very simple and using calls to Microsoft OAuth2 authentication endpoint for authenticating usage of Graph API endpoints.
Using the code
In the proposed, verified solution, we will use only .NET Core framework libraries, from namespace System.Net.Http
. There is no need for any additional libraries to include. The OAuth2 in current case would be used for authneticating the user ( validating the identity ) and authorizing that user for Microsoft Graph API services ( any other Microsoft online service can be used, it's not restricted to Graph API ).
The main staps are following:
- redirecting ( sending, forwarding ) user to the Microsoft online site for typing username and password and confirming services usage. The result value is code and id_token. We would need token value to proceed in the authentication flow.
- Recieving results values in the Callback endpoint on the current web application. Params are sent back via Url parameters of the GET request.
- connecting to Microsoft authentication endpoint to request final access token and refresh token, which are needed to access Microsoft online services ( Graph API in particular )
- refreshing the expired access token ( in one hour after retrieving it ) using the refresh token
The first step is to authenticate the user on the Microsoft online site, where user has to type his password and confirm usage of particular Microsoft services. For authneticating on the Microsoft online site it needs to redirect user to specific address, which includes list of requested permissions ( Microsoft services ) - in this example I will show Microsoft Graph API for checking outlook mailbox. The sample finally formed authentication URL is provided below:
string authenticationUrl =
string.Format(@"https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id={0}" +
"&redirect_uri={1}" +
"&response_mode=form_post" +
"&response_type=code+id_token" +
"&scope={2}" +
"&state={3}" +
"&nonce={4}",
Uri.EscapeDataString(appId),
Uri.EscapeDataString(redirectUri),
"openid+email+profile+offline_access+" + Uri.EscapeDataString(scopes),
Uri.EscapeDataString(sessionId.ToString()),
secretNonce);
The first parameter is appId
- unique identifier of the customer authorized application, which should be retrieved (signed for using services) through Microsoft Azure management portal or another appropriate way.
RedirectUri
- callback address on the current website, which is called when user passed the online authentication step. The authentication code is submitted in case of successful result to this callback endpoint. Sample callback endpoint is provided in this article after authentication Uri parameters review.
scopes
- list of Microsoft servies which are going to be used by current web application. The permissions should be configured on the Azure portal first! If customer has not authorized current web application for using these services, the authentication would work, but when using desired services, API error is thrown. In this particular example, web application is going to use Microsoft Graph API outlook services - reading emails, sending, accessing user profile information. - User.Read, Mail.Read, Mail.Send - the requested service are called 'scopes'.
sessionId
- is unique identifier of current user session. In my case I just used randomly generated GUID, because session management in .NET Core MVC applications is not easy to integrate.
secretNonce
- the secret value associated with current authenticatino request. Can be used any GUID value, for eample: string secretNonce = Guid.NewGuid().ToString("N");
More details about parameters restrictions and purpose can be found here:
https://graph.microsoft.io/en-us/docs/authorization/app_authorization
or here
https://docs.microsoft.com/en-us/azure/active-directory/active-directory-protocols-oauth-code
When checking the documentation, please pay attention that OAuth2 v2.0 is used in the current example!
Next, in case user successfully passed the authentication step by typing username and passwork in browser, callback Url on your site is called with data being posted ( in this case we requested code and id_token response_type=code+id_token ). To read the post data code similar to fillowing can be used:
public async Task<IActionResult> AuthenticationCallback(string id_token, string code, string state, string session_state)
{
// ... requesting Microsoft access code and refresh token here.
}
When the valid code received, it's time to get the access token and refresh token for calling particular Microsoft service endpoint (in this case Graph API). Thats the code sample for retrieving final access and refresh tokens:
public static async Task<TokenDetails> RequestAccessToken(string code)
{
TokenDetails tokenDetails = null;
string endpointHost = "https://login.microsoftonline.com";
string currentSiteCallbackUri = "";
string grant_type = "authorization_code";
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(endpointHost);
using (var request = new HttpRequestMessage(HttpMethod.Post, "/common/oauth2/v2.0/token"))
{
StringContent contentParams = new StringContent(string.Format("grant_type={0}" +
"&redirect_uri={1}" +
"&client_id={2}" +
"&client_secret={3}" +
"&code={4}"
, grant_type,
Uri.EscapeDataString(currentSiteCallbackUri),
Uri.EscapeDataString(appId),
appSecret,
Uri.EscapeDataString(code)), Encoding.UTF8, "application/x-www-form-urlencoded");
request.Content = contentParams;
using (HttpResponseMessage response = await client.SendAsync(request))
{
if (response.StatusCode == HttpStatusCode.OK)
{
var resultJson = await response.Content.ReadAsStringAsync();
}
else if (response.Content != null)
{
var errorDetails = await response.Content.ReadAsStringAsync();
}
}
}
}
return tokenDetails;
}
There are few new paramters for this request:
grant_type
- type of the requested access codes, in this case we need access tockens for using Microsoft services ( Grpah API ) - which is called "authorization_code";
appSecret
- is application secret, which should be generated and retrieved thorugh Azure portal, or other appropriate way;
code
- is the code, retrieved during the first authentication call.
Detailed information about this endpoint params is provided in the official documentation: https://docs.microsoft.com/en-us/azure/active-directory/active-directory-protocols-oauth-code
When requesting access and refresh tokens, all paramteres are sent via POST request, as a request content. The callback endpoint Url SHOULD BE IGNORED, requested tokens are sent back in the request RESPONSE the access token and refresh token are required when using Microsoft online services.
The response content is simple:
{
"access_token": " eyJ0eXAiOiJKV1Q....................",
"token_type": "Bearer",
"expires_in": "3600",
"expires_on": "1388444763",
"resource": "https://service.contoso.com/",
"refresh_token": "YCAA......................................",
"scope": "https%3A%2F%2Fgraph.microsoft.com%2Fmail.read",
"id_token": " eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0..................."
}
The access token is valid about one hour, so you would need to use resresh token to get new access token, in case the last one expired. More details about access and resresh tokens expiration is provided below:
- Azure AD SSO Access-Token expires in 1 hour.
- You could use Azure AD Refresh Token to refresh your AccessToken.
- The Refresh Token expires in 14 days.
- Azure allows an access-token to be refreshed using the refresh-token for a maximum period of time of 90 days (from the initial date of issuing the token). This means after 90 days, Azure will authenticate the user to login again.
- At the time of writing this, all these settings are not configurable, and there is no plans to make them configurable to users.
To get a new access token, following code can be used:
public static async Task<TokenDetails> GetNewAccessTokenFromRefreshToken(string refreshToken)
{
TokenDetails tokenDetails = null;
string endpointHost = "https://login.microsoftonline.com";
string redirectUri = isLocal ? localRedirectUri : liveSiteRedirectUri;
string grant_type = "refresh_token";
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(endpointHost);
using (var request = new HttpRequestMessage(HttpMethod.Post, "/common/oauth2/v2.0/token"))
{
StringContent contentParams = new StringContent(string.Format("grant_type={0}" +
"&redirect_uri={1}" +
"&client_id={2}" +
"&client_secret={3}" +
"&refresh_token={4}"
, grant_type,
Uri.EscapeDataString(redirectUri),
Uri.EscapeDataString(appId),
appSecret,
Uri.EscapeDataString(refreshToken)), Encoding.UTF8, "application/x-www-form-urlencoded");
request.Content = contentParams;
using (HttpResponseMessage response = await client.SendAsync(request))
{
if (response.StatusCode == HttpStatusCode.OK)
{
var resultJson = await response.Content.ReadAsStringAsync();
}
else if (response.Content != null)
{
var errorDetails = await response.Content.ReadAsStringAsync();
}
}
}
}
return tokenDetails;
}
The TokenDetails
class is a c# definition of the authentication response content to simpify access in the code:
public class TokenDetails
{
public string token_type { get; set; }
public string scope { get; set; }
public int expires_in { get; set; }
public int ext_expires_in { get; set; }
public string access_token { get; set; }
public string refresh_token { get; set; }
public string id_token { get; set; }
}
Points of Interest
This simple approach can be used as a starting point for retrieving authenticated users profile information, accessing outlook messages, documents, calendar and all other Microsoft services through Graph API. This article is based on OAuth v2.0. For more details about Microsoft OAuth v2.0 authentication flow please visit https://docs.microsoft.com/en-us/azure/active-directory/active-directory-protocols-oauth-code
History
First publish of the article Sem Shekhovtsov on 06 Jan 2017