I need to force re-authentication of Microsoft Graph within an MVC Core application.
The Graph object is obtained in ConfigureServices using the code segment:
var tokenAcquisition = context.HttpContext.RequestServices
.GetRequiredService<ITokenAcquisition>();
var graphClient = new GraphServiceClient(
new DelegateAuthenticationProvider(async (request) => {
var token = await tokenAcquisition
.GetAccessTokenForUserAsync(_scopes, user: context.Principal);
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", token);
})
);
The problem is the token goes stale and a later call to Graph fails. Easy to trap and to put in some reauthentication code except it also fails, with a "MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call" error. Plenty of reference to this scenario online but no definitive response that I can find.
Reauthentication code in the controller is:
if (ex.InnerException.InnerException is MsalUiRequiredException)
{
string[] _scopes = _config.GetValue<string>("AzureAd:GraphScopes")?.Split(' ');
var tokenAcquisition = _http.RequestServices.GetRequiredService<ITokenAcquisition>();
_graph = new GraphServiceClient(
new DelegateAuthenticationProvider(async (request) =>
{
var options = new TokenAcquisitionOptions() { ForceRefresh = true };
var token = await tokenAcquisition.GetAccessTokenForUserAsync(_scopes, user: User, tokenAcquisitionOptions: options);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
})
);
}
Question is how to successfully force reauthentication and obtain a new Graph client?
Answering my own question, turns out it's easily handled in the Controller:
try
{
string token = await _tokenAcquisition
.GetAccessTokenForUserAsync(GraphConstants.Scopes);
return View().WithInfo("Token acquired", token);
}
catch (MicrosoftIdentityWebChallengeUserException)
{
return Challenge();
}
This code segment is from https://learn.microsoft.com/en-us/graph/tutorials/aspnet-core?tutorial-step=3
I assume that your application is forcing the user to log in and you're using that identity to get a Graph token based on the use of context.Principal:
.GetAccessTokenForUserAsync(_scopes, user: context.Principal);
When the token expires I assume the original token that was used to get in has also expired about the same time. That means that there is no user and therefore calls fail with the error that you're describing. It makes me think you need to reauthenticate before you try to get a new graph token.
However, you should monitor the token and get a new one just before it expires - silently -using the refresh token rather than a new authentication.
https://learn.microsoft.com/en-us/advertising/guides/authentication-oauth-get-tokens?view=bingads-13#refresh-accesstoken
Related
I'm trying to build a feature where a client application retrieves the graph resources via WebAPI layer. The scenario has following applications:
Angular5 Client application
ASP.Net Core Web API
The Angular5 client application uses MSAL to authenticate against application (resisted as Converged application via apps.dev.microsoft.com registration application; AAD v2 endpoint).
The authentication flow defines the Web API as scope while login or getting access token
constructor() {
var logger = new Msal.Logger((logLevel, message, piiEnabled) =>
{
console.log(message);
},
{ level: Msal.LogLevel.Verbose, correlationId: '12345' });
this.app = new Msal.UserAgentApplication(
CONFIGSETTINGS.clientId,
null,
this.authCallback,
{
redirectUri: window.location.origin,
cacheLocation: 'localStorage',
logger: logger
}
);
}
public getAPIAccessToken() {
return this.app.acquireTokenSilent(CONFIGSETTINGS.scopes).then(
accessToken => {
return accessToken;
},
error => {
return this.app.acquireTokenSilent(CONFIGSETTINGS.scopes).then(
accessToken => {
return accessToken;
},
err => {
console.error(err);
}
);
}
);
}
Here scope is defined as scopes: ['api://<<guid of application>>/readAccess']. This is the exact value which was generated when I've registered the Web API in registeration portal. Also, the client application id is added as Pre-authorized applications .
The Web API layer (built in dotnet core -- and uses JwtBearer to validate the authentication), defines the API which internally fetches the graph resources (using HttpClient). To get the access token, I've used following code
public async Task<string> GetAccesToken(string resourceName)
{
var userAssertion = this.GetUserAssertion();
string upn = GetLoggedInUpn();
var userTokenCache = new SessionTokenCache(upn, new Microsoft.Extensions.Caching.Memory.MemoryCache(new MemoryCacheOptions())).GetCacheInstance();
string msGraphScope = "https://graph.microsoft.com/User.Read";
string authority = string.Format("https://login.microsoftonline.com/{0}/v2.0", this.authConfig.TenantId);
ConfidentialClientApplication clientApplication = new ConfidentialClientApplication(this.authConfig.ClientId, authority, new ClientCredential(this.authConfig.AppKey), userTokenCache, null);
var result = await clientApplication.AcquireTokenOnBehalfOfAsync(new string[] { msGraphScope }, userAssertion);
return result != null ? result.AccessToken : null;
}
private UserAssertion GetUserAssertion()
{
string token = this.httpContextAccessor.HttpContext.Request.Headers["Authorization"];
string upn = GetLoggedInUpn();
if (token.StartsWith("Bearer", true, CultureInfo.InvariantCulture))
{
token = token.Trim().Substring("Bearer".Length).Trim();
return new UserAssertion(token, "urn:ietf:params:oauth:grant-type:jwt-bearer");
}
else
{
throw new Exception($"ApiAuthService.GetUserAssertion() failed: Invalid Authorization token");
}
}
Note here, The method AcquireTokenOnBehalfOfAsync is used to get the access token using graph scope. However it throws the following exception:
AADSTS65001: The user or administrator has not consented to use the application with ID '<>' named '<>'. Send an interactive authorization request for this user and resource.
I'm not sure why the of-behalf flow for AAD v2 is not working even when client application uses the Web API as scope while fetching access token and Web API registers the client application as the pre-authorized application.
Note - I've tried using the other methods of ConfidentialClientApplication but even those did not work.
Can someone please point out how the above flow can work without providing the admin consent on Web API?
I've been trying to figure this out for weeks! My solution isn't great (it requires the user to go through the consent process again for the Web API), but I'm not sure that's entirely unexpected. After all, either the Admin has to give consent for the Web API to access the graph for the user, or the user has to give consent.
Anyway, the key was getting consent from the user, which of course the Web API can't do since it has no UI. However, ConfidentialClientApplication will tell you the URL that the user has to visit with GetAuthorizationRequestUrlAsync.
Here's a snippet of the code that I used to get it working (I'm leaving out all the details of propagating the url back to the webapp, but you can check out https://github.com/rlittletht/msal-s2s-ref for a working example.)
async Task<string> GetAuthenticationUrlForConsent(ConfidentialClientApplication cca, string []graphScopes)
{
// if this throws, just let it throw
Uri uri = await cca.GetAuthorizationRequestUrlAsync(graphScopes, "", null);
return uri.AbsoluteUri;
}
async Task<string> GetAccessTokenForGraph()
{
// (be sure to use the redirectUri here that matches the Web platform
// that you added to your WebApi
ConfidentialClientApplication cca =
new ConfidentialClientApplication(Startup.clientId,
"http://localhost/webapisvc/auth.aspx",
new ClientCredential(Startup.appKey), null, null);
string[] graphScopes = {"https://graph.microsoft.com/.default"};
UserAssertion userAssertion = GetUserAssertion();
AuthenticationResult authResult = null;
try
{
authResult = await cca.AcquireTokenOnBehalfOfAsync(graphScopes, userAssertion);
}
catch (Exception exc)
{
if (exc is Microsoft.Identity.Client.MsalUiRequiredException
|| exc.InnerException is Microsoft.Identity.Client.MsalUiRequiredException)
{
// We failed because we don't have consent from the user -- even
// though they consented for the WebApp application to access
// the graph, they also need to consent to this WebApi to grant permission
string sUrl = await GetAuthenticationUrlForConsent(cca, graphScopes);
// you will need to implement this exception and handle it in the callers
throw new WebApiExceptionNeedConsent(sUrl, "WebApi does not have consent from the user to access the graph on behalf of the user", exc);
}
// otherwise, just rethrow
throw;
}
return authResult.AccessToken;
}
One of the things that I don't like about my solution is that it requires that I add a "Web" platform to my WebApi for the sole purpose of being able to give it a redirectUri when I create the ConfidentialClientApplication. I wish there was some way to just launch the consent workflow, get the user consent, and then just terminate the flow (since I don't need a token to be returned to me -- all I want is consent to be granted).
But, I'm willing to live with the extra clunky step since it actually gets consent granted and now the API can call the graph on behalf of the user.
If someone has a better, cleaner, solution, PLEASE let us know! This was incredibly frustrating to research.
I have an app using MSAL connecting to MS Graph and have been able to use the API. There are couple Outlook APIs such as https://msdn.microsoft.com/en-us/office/office365/api/calendar-rest-operations#GetRoomLists, FindRoomLists currently not available in Graph.
I have the need to use both these APIs in a single app? I read a similar question on Stack Overflow and it mentioned, a Token cannot be used for both Graph and Outlook. I did try and it did not work.
Any suggestions? Is my path to quit using Graph and go to Outlook API?
MSAL will look up the cache and return any cached token which match with the requirement. If such access tokens are expired or no suitable access tokens are present, but there is an associated refresh token(need offline_access scope), MSAL will automatically use that to get a new access token and return it transparently.
For example, if you use MSAL to redeem the authorization code into an access token for microsoft graph, in openid connect owin middleware :
AuthorizationCodeReceived = async (context) =>
{
var code = context.Code;
string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
TokenCache userTokenCache = new MSALSessionCache(signedInUserID,
context.OwinContext.Environment["System.Web.HttpContextBase"] as HttpContextBase).GetMsalCacheInstance();
ConfidentialClientApplication cca =
new ConfidentialClientApplication(clientId, redirectUri, new ClientCredential(appKey), userTokenCache,null);
string[] scopes = { "Mail.Read" };
try
{
AuthenticationResult result = await cca.AcquireTokenByAuthorizationCodeAsync(code, scopes);
}
catch (Exception eee)
{
}
},
With scope Mail.Read , you could get an access token for Microsoft Graph, for the purpose of reading the user's mailbox .Now if you want to call outlook mail rest api in a controller/action , you could use scope: https://outlook.office.com/mail.read , MSAL will acquire token for outlook mail rest api using cached refresh token :
// try to get token silently
string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
TokenCache userTokenCache = new MSALSessionCache(signedInUserID, this.HttpContext).GetMsalCacheInstance();
ConfidentialClientApplication cca = new ConfidentialClientApplication(clientId, redirectUri,new ClientCredential(appKey), userTokenCache, null);
if (cca.Users.Count() > 0)
{
string[] scopes = { "https://outlook.office.com/mail.read" };
try
{
AuthenticationResult result = await cca.AcquireTokenSilentAsync(scopes,cca.Users.First());
}
catch (MsalUiRequiredException)
{
try
{// when failing, manufacture the URL and assign it
string authReqUrl = await WebApp.Utils.OAuth2RequestManager.GenerateAuthorizationRequestUrl(scopes, cca, this.HttpContext, Url);
ViewBag.AuthorizationRequest = authReqUrl;
}
catch (Exception ee)
{
}
}
}
else
{
}
Please refer to code sample : Integrate Microsoft identity and the Microsoft Graph into a web application using OpenID Connect.
I am pretty new to this.. so any help would be greatly appreciated.
I have a WebApi service that uses OAUTH token and refresh token authentication.
All works well at the moment:
step1: I send in the user and password and it generates an authentication token and a refresh token. The refresh token is saved in the DB.
step2. I can now use the refresh token and i receive the authentication token and a new refresh token. I want a way to use the same refresh token i sent and not reuse a new one.
This is my code for the refresh token:
public class SimpleRefreshTokenProvider : IAuthenticationTokenProvider
{
public async Task CreateAsync(AuthenticationTokenCreateContext context)
{
RefreshTokensRepository _repo = new RefreshTokensRepository();
var clientid = context.Ticket.Properties.Dictionary["as:client_id"];
//HERE I regenerate the token, but I have no idea how to retrieve the already sent one.
var refreshTokenId = Guid.NewGuid().ToString("n");
//saving in BD:
var refreshTokenLifeTime = context.OwinContext.Get<string>("as:clientRefreshTokenLifeTime");
var token = new RefreshTokens()
{
Id = Helper.GetHash(refreshTokenId),
ClientId = clientid,
Subject = context.Ticket.Identity.Name,
IssuedUtc = DateTime.UtcNow,
ExpiresUtc = DateTime.UtcNow.AddMinutes(Convert.ToDouble(refreshTokenLifeTime))
};
context.Ticket.Properties.IssuedUtc = DateTime.UtcNow;
context.Ticket.Properties.ExpiresUtc = DateTime.UtcNow.AddMinutes(Convert.ToDouble(refreshTokenLifeTime));
token.ProtectedTicket = context.SerializeTicket();
var result = _repo.Add(token);
if(!string.IsNullOrEmpty(result))
context.SetToken(refreshTokenId);
}
public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
{
var allowedOrigin = context.OwinContext.Get<string>("as:clientAllowedOrigin");
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
string hashedTokenId = Helper.GetHash(context.Token);
RefreshTokensRepository _repo = new RefreshTokensRepository();
var refreshToken = _repo.FindById(hashedTokenId);
if (refreshToken != null)
{
//Get protectedTicket from refreshToken class
context.DeserializeTicket(refreshToken.ProtectedTicket);
_repo.Remove(hashedTokenId);
}
}
void IAuthenticationTokenProvider.Create(AuthenticationTokenCreateContext context)
{
throw new NotImplementedException();
}
void IAuthenticationTokenProvider.Receive(AuthenticationTokenReceiveContext context)
{
throw new NotImplementedException();
}
}
My code is based on this samples:
http://bitoftech.net/2014/07/16/enable-oauth-refresh-tokens-angularjs-app-using-asp-net-web-api-2-owin/
I would like to use the same sent refresh token, but I have no idea how to use the already sent one in this context.
Any ideas?
Disclaimer: I don't condone reusing refresh tokens.
However, this does provide a good opportunity for everyone to improve knowledge of how this process works and there could be a good reason for reusing past refresh tokens in certain scenarios. I'm basing my answer upon:
Question: "I want a way to use the same refresh token i sent and not reuse a new one."
Code comment, "//HERE I regenerate the token, but I have no idea how to retrieve the already sent one."
PseudoCode Steps:
Store a user identifier as a property in AuthenticationProperties in the GrantResourceOwnerCredentials() method. From the sample code, it looks like you may already be doing this with "userName":
var props = new AuthenticationProperties(new Dictionary<string, string>
{
{
"as:client_id", (context.ClientId == null) ? string.Empty : context.ClientId
},{
"userName", context.UserName
}
});
Retrieve the user identifier in the CreateAsync() method of your IAuthenticationTokenProvider implementation (e.g. "SimpleRefreshTokenProvider" in your case). This would look something like:
public async Task CreateAsync(AuthenticationTokenCreateContext context)
{
var userName = context.Ticket.Properties.Dictionary["userName"];
...
Still in the CreateAsync() method use the user identifier to lookup the existing refresh token. This would look something like:
var existingRefreshToken = await _repo.FindRefreshTokenByUserNameAsync(userName);
Note: You would need to write the above method into your AuthRepository class from the example code. The "FindRefreshTokenByUserNameAsync(userName) implementation might include something like this if you're using Entity Framework and have a "RefreshToken" table that is being used to persist the granted refresh token:
var existingToken = RefreshToken.Where(r => r.UserName == userName).SingleOrDefault();
At this point, you have the existing token and should be able to re-use that refresh token value instead of Guid.NewGuid():
var refreshTokenId = existingToken.Token;
Taking a look at the tutorial's example code, however, indicates that a HashAlgorithm is being used to store the refresh token's value. That could complicate things a bit for you as storing a hash value is better security, but the process of hashing here is meant to be one-way.
If you really want to reuse the original token value when all you have persisted is the hashed token, would need to implement code that captures the non-hashed token value in the ReceiveAsync() method. It would have to temporarily persist the non-hashed value long enough for you to use it in the CreateAsync() method. In other words, you would have to save/persist the "context.Token" in ReceiveAsync(), associate it with your userName (from context.Ticket.Properties.Dictionary["userName"]), and use it later in the CreateAsync() method. It's hacky and I don't like it, but you would do it around this line of code in ReceiveAsync():
string hashedTokenId = Helper.GetHash(context.Token);
I am using OAuth in ASP.NET Web Api to return access token to the caller of the application.
I have inherited my OAuth provider class from OAuthAuthorizationServerProvider and once the user is authenticated inside the GrantResourceOwnerCredentials function, I want to read the generated access token, create it's hash with some salt value and then add the created hash into a cookie.
Below is the simplified definition of my GrantResourceOwnerCredentials function.
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
ApplicationUser user = await userManager.FindAsync(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager, OAuthDefaults.AuthenticationType);
ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager, CookieAuthenticationDefaults.AuthenticationType);
//Add claims required on client side.
AuthenticationProperties properties = CreateProperties(user.UserName);
AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
//Generate the token behind the scene for given ticket
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
SetCsrfCookie(context);
}
private void SetCsrfCookie(OAuthGrantResourceOwnerCredentialsContext context)
{
var accessToken = "<READ THE GENERATED ACCESS TOKEN HERE>"; //<------ How?
if(string.IsNullOrEmpty(accessToken)) return;
var csrfToken = Helper.GetHash(accessToken);
context.Response.Cookies.Append("XSRF-TOKEN", csrfToken, new CookieOptions {HttpOnly = false});
}
I am facing two issues here.
First one is how to read the generated access token in the SetCsrfCookie function in the code above.
Generated cookie is not received on the client side.
I know its possible to intercept the response in a some OwinMiddleware inherited class and then I may be able to generate the required cookie and attach to the response but first I have not tried that and secondly, it seems better option to handle this case inside my OAuth provider class as some people suggest that deriving from the OwinMiddleware is not a good practice.
I finally managed to fix the cookie issue by adding the below line of code on angular side
$httpProvider.defaults.withCredentials = true;
On the Web Api side I just set the Access-Control-Allow-Credentials response header to true inside the WebApiConfig.Register method like below:
var cors = new EnableCorsAttribute(ConfigurationManager.AppSettings["ALLOWED_ORIGIN"], "*", "*")
{
SupportsCredentials = true
};
config.EnableCors(cors);
This solved my cookie problem.
For accessing the generated access token I inherited a class from OwinMiddleware and inside the Invoke function I access the response body to read the access token like below:
public override async Task Invoke(IOwinContext context)
{
var path = context.Request.Path;
var stream = context.Response.Body;
var buffer = new MemoryStream();
context.Response.Body = buffer;
await Next.Invoke(context);
var reqStream = new StreamReader(context.Request.Body);
reqStream.BaseStream.Position = 0;
var data = reqStream.ReadToEnd();
if (path.Equals(new PathString("/token"),StringComparison.CurrentCultureIgnoreCase))
{
buffer.Seek(0, SeekOrigin.Begin);
var reader = new StreamReader(buffer);
var responseBody = await reader.ReadToEndAsync();
//check if the response body contains access token if so then do your processing
}
buffer.Seek(0, SeekOrigin.Begin);
await buffer.CopyToAsync(stream);
}
I am using Web API as my back-end and implemented the token security using the built in mechanism. In the template code, when issuing the access token, I can get the issued and expired dates of the token:
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
var issued = context.Properties.IssuedUtc;
var expired = context.Properties.ExpiresUtc;
.
.
.
}
Now when a request is made to a method that requires authorization I want to do something similar:
[Authorize]
public async Task<string> GetTokenInfo()
{
//var issued = GetCurrentTicket().Properties.ExpiresUtc;
//var issued = GetCurrentTicket().Properties.IssuedUtc;
.
.
.
}
So how can I get the information encrypted inside the token, more specifically the ExpireUtc and IssuedUtc ?
You can easily retrieve the AuthenticationProperties dictionary using IAuthenticationManager.AuthenticateAsync, which returns a AuthenticateResult object: https://msdn.microsoft.com/en-us/library/dn270674(v=vs.113).aspx
From a Web API controller, you'll need the GetOwinContext extension to get the OWIN context from the request message and use IOwinContext.Authentication: https://msdn.microsoft.com/en-us/library/system.net.http.owinhttprequestmessageextensions.getowincontext(v=vs.118).aspx
var context = Request.GetOwinContext();
var result = await context.Authentication.AuthenticateAsync(OAuthDefaults.AuthenticationType);
if (result == null) {
throw new InvalidOperationException();
}
var properties = result.Properties;
(of course, you also need to have a properly configured app.UseOAuthBearerAuthentication call in your Startup class, but I assume it's the case here).