I am using Microsoft Graph API to call some endpoints. I am using the SDK for C#.
When opening a fiddler trace, I found out that my _graphClientService is issuing an authentication to get a new token at every call. Why would that happen and how to prevent it?
It is also causing this error in some calls.
AADSTS50196: The server terminated an operation because it encountered a client request loop
It looks like this piece of code works. It generates a GraphServiceClient that reuses the same token at every call, instead of generating a new one.
public GraphServiceClient GenerateGraphUserClient()
{
string userToken = GetUserAccessToken();
GraphServiceClient client= new GraphServiceClient("https://graph.microsoft.com/v1.0", new DelegateAuthenticationProvider(async (requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", userToken);
}));
return client;
}
public string GetUserAccessToken()
{
string[] scopes = new string[] { "https://graph.microsoft.com/.default" };
IPublicClientApplication publicClientApplication = PublicClientApplicationBuilder
.Create(_clientId)
.WithTenantId(_domain)
.Build();
var securePassword = new SecureString();
foreach (char c in _password)
securePassword.AppendChar(c);
var result = publicClientApplication.AcquireTokenByUsernamePassword(scopes, _userName, securePassword).ExecuteAsync().Result;
return result.AccessToken;
}
If you are using MSAL.NET you can cache a token. Public client applications (desktop/mobile apps) should try to get a token from the cache before acquiring a token by another method.
Acquisition methods on confidential client applications manage the cache themselves.
Resource:
Token cache serialization in MSAL.NET
Related
We're creating a small Blazor app which accesses a 3rd party api which we have no control over. The API is secured using OAuth with a client_id and client_secret on a machine to machine basis. There is no authentication from the user.
Initially we hit the token endpoint with the id and secret and it returns a token with an expiry date and time. Currently we just repeat the initial token request before every call to the api so that the token is always brand new and has never expired.
Is this practice accepted or should I be storing the bearer token and it's expiration date and time somewhere secure on the client to check the expiration before requesting a new token?
I'm conscious that if the app is widely used internally at our business we could start to reach the api usage limits, but also, if we store it locally on the client, could I mistakenly leave us vulnerable to XSS attacks or similar.
I'm looking for best practice guidance really, and, if the advice is to store the token and check against expiry, where and how should I be storing the token securely?
I found a nice solution at:
https://referbruv.com/blog/using-imemorycache-for-token-caching-in-an-aspnet-core-application/
The author gives an excellent demonstration of how to use IMemoryCache for my exact scenario. I implement a Token Service which checks the expiry of the cache item. If it's in date, I just used the cached token. If the token has expired, I fetch a new token from the API.
public class TokenService : ITokenService
{
private readonly IApiAccountService _apiAccountService;
private readonly IMemoryCache _cache;
public TokenService(IMemoryCache cache, IApiAccountService apiAccountService)
{
_cache = cache;
_apiAccountService = apiAccountService;
}
public async Task<string> FetchTokenAsync()
{
string token;
if (!_cache.TryGetValue("Token", out token))
{
var authResult = await _apiAccountService.GetToken();
var options = new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds((authResult.expires_at - DateTime.Now).TotalSeconds));
_cache.Set("Token", authResult.access_token, options);
token = authResult.access_token;
}
return token;
}
}
Then, when calling my API I just call the Token Service:
private readonly ITokenService _tokenService;
public ApiService(HttpClient httpClient, IConfiguration configuration, ITokenService tokenService)
{
_httpClient = httpClient;
_configuration = configuration;
_tokenService = tokenService;
}
...
private async Task<List<T>> FetchListAsync<T>(string uri)
{
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", await _tokenService.FetchTokenAsync());
var response = await _httpClient.GetAsync(uri);
response.EnsureSuccessStatusCode();
using var responseStream = await
response.Content.ReadAsStreamAsync();
var reader = new StreamReader(responseStream).ReadToEnd();
var responseObject = JsonConvert.DeserializeObject<List<T>>(reader);
return responseObject;
}
I'm trying to get the MS example application to work for the Graph Beta Webhooks API and it's currently crashing because I've had to to modify some of the example code to remove some obsolete code and I'm not sure what to replace it with.
This is the function:
public static GraphServiceClient GetAuthenticatedClient(string userId, string redirect)
{
var graphClient = new GraphServiceClient(
new DelegateAuthenticationProvider(
async (request) =>
{
var tokenCache = new SampleTokenCache(userId);
// Obsolete code
//var cca = new ConfidentialClientApplication(Startup.ClientId, redirect, new ClientCredential(Startup.ClientSecret), tokenCache.GetMsalCacheInstance(), null);
// New code
var cca2 = ConfidentialClientApplicationBuilder.Create(Startup.ClientId).WithClientSecret(Startup.ClientSecret).WithRedirectUri(redirect).Build();
// Question - how do I pass the tokenCache in here as the userTokenCache ?
// ERROR - With the new code this returns zero accounts presuambly because I haven't passed in a userTokenCache
var accounts = await cca2.GetAccountsAsync();
// Obsolete code
//var authResult = await cca.AcquireTokenSilentAsync(Startup.Scopes, accounts.First());
// New code
var authResult2 = await cca2.AcquireTokenSilent(Startup.Scopes, accounts.First()).ExecuteAsync();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authResult2.AccessToken);
}));
return graphClient;
}
If I use the ConfidentialClientApplicationBuilder then the GetAccountsAsync() returns an empty collection and I think it's because I haven't passed the tokenCache into the builder. Does anyone know how to fix this code or has anyone got the example App working ?
This is the link to the example App:
https://learn.microsoft.com/en-us/samples/microsoftgraph/aspnet-webhooks-rest-sample/microsoft-graph-aspnet-webhooks/
Thanks
Ed James
You need to login as user so that an account is added to the token cache which is available calling the tokenCache.GetMsalCacheInstance() method.
I'm trying to create an external login scheme for facebook, google and linkedin without using identity framework. I have an api that stores all users and do some authentication stuffs. Right now I'm kind of lost on how to get the information from the external login.
I'm issuing a challenge like this.
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult ExternalLogin(string provider)
{
//Issue a challenge to external login middleware to trigger sign in process
return new ChallengeResult(provider);
}
This works well, it redirects me to either google, facebook or linkedinn authentication.
Now on this part:
public async Task<IActionResult> ExternalLoginCallback()
{
//Extract info from externa; login
return Redirect("/");
}
All I want is to get the information that was provided by the external login.
I have tried what I found from my research,
var result = await HttpContext.AuthenticateAsync(provider);
if (result?.Succeeded != true)
{
return Redirect("/");
}
var externalUser = result.Principal;
var claims = externalUser.Claims.ToList();
First of all I I'm not sure if a simple ?provider=Google on my callback string will pass the provider name I specify so it can be used to check the sign in scheme. I guess this is incorrect. Secondly, I tried hard coding await HttpContext.AuthenticateAsync("Google") and when it reach this code, the debug stops. I'm not sure why.
I've seen the generated code when creating a project with single authentication.
var info = await _signInManager.GetExternalLoginInfoAsync();
Sadly, I'm won't be able to use identity since I don't have a user store and my application will be consuming an API.
First you need to create a custom cookie handler. I myself had problems with:
No IAuthenticationSignInHandler is configured to handle sign in for
the scheme: Bearer
I had to add a cookie handler that will temporarily store the outcome of the external authentication, e.g. the claims that got sent by the external provider. This is necessary, since there are typically a couple of redirects involved until you are done with the external authentication process.
Startup
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(o =>
{
o.TokenValidationParameters = tokenValidationParameters;
})
.AddCookie("YourCustomScheme")
.AddGoogle(googleOptions =>
{
googleOptions.SignInScheme = "YourCustomScheme";
googleOptions.ClientId = "x";//Configuration["Authentication:Google:ClientId"];
googleOptions.ClientSecret = "x";//Configuration["Authentication:Google:ClientSecret"];
//googleOptions.CallbackPath = "/api/authentication/externalauthentication/signin-google";
});
The important part here is "YourCustomScheme".
Now it's time to retrieve the user information from the claims provided by the external authentication in the callback action.
Controller
[AllowAnonymous]
[HttpPost(nameof(ExternalLogin))]
public IActionResult ExternalLogin(ExternalLoginModel model)
{
if (model == null || !ModelState.IsValid)
{
return null;
}
var properties = new AuthenticationProperties { RedirectUri = _authenticationAppSettings.External.RedirectUri };
return Challenge(properties, model.Provider);
}
[AllowAnonymous]
[HttpGet(nameof(ExternalLoginCallback))]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
//Here we can retrieve the claims
var result = await HttpContext.AuthenticateAsync("YourCustomScheme");
return null;
}
VoilĂ ! We now have some user information to work with!
Helpful link
http://docs.identityserver.io/en/release/topics/signin_external_providers.html
I too had this issue and see if the below code works for you.
I wanted to extract the full name after Google/FB authentication.
var info = await _signInManager.GetExternalLoginInfoAsync();
TempData["fullname"] = info.Principal.FindFirstValue(ClaimTypes.Name);
I have built MVC app using OAuth 2.0 to Access Google APIs.
On thirst call I receive an Access token + Refresh token.
Next calls come without a Refresh token, its ok, I saved it on a first call.
After 1 hour Access token expires and I need to get a new one, using previously saved refresh token.
How do I check that Access token expired? Didnt see any IsExpired properties.
What is the proper syntax to acquire a new Access token using Refresh token (for MVC app)? Couldnt find any reference or documentation how to do that.
Should I write any new code or call existing API to do that?
Where should I do that, in my HomeController's Index action or before calling any Google API?
My app is built as described in here (basically the same code), but no code to acquire a new Access token: https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth#web-applications-aspnet-mvc
Thank you
For more details I added here how I wrote the code.
HomeController:
public async Task<ActionResult> Index(CancellationToken cancellationToken)
{
if (result == null || result.Credential == null)
{
result = await new AuthorizationCodeMvcApp(this, new AppFlowMetadata()).AuthorizeAsync(cancellationToken);
if (result.Credential == null) return new RedirectResult(result.RedirectUri);
if (!string.IsNullOrEmpty(result.Credential.Token.RefreshToken))
{
SaveRefreshTocken(result.Credential.Token.RefreshToken);
}
}
return View();
}
SaveRefreshTocken - just saves a Refresh token in web.config.
public ActionResult Gmail()
{
if (result == null || result.Credential == null) throw new Exception("expired_credential");
return PartialView(GmailManager.GetGmail(result.Credential));
}
And, simplified GmailManager class:
public static class GmailManager
{
public static List<Message> GetGmail(UserCredential credential)
{
var mygmail = new MyGmail();
var service = new GmailService(new BaseClientService.Initializer { HttpClientInitializer = credential });
var request = service.Users.Messages.List("me");
request.Q = "in:inbox is:unread";
var messages = request.Execute().Messages;
return messages;
}
}
Question - where and how should I USE refresh token?
If I saved it, I would have to use it when Access token expires to get a new Access token, right?
However it doesnt seem like its trying to acquire a new access token automatically:
[This question relates to ASP.NET MVC4, and it is about best-practice approach - so please, don't suggest hacks.]
I want to authenticate users using an auth token sent in the request URL. It works similarly to a password reset token, except in this case it does not go to a reset page but instead grants access to some portion of the site. The idea is to send the URL with the auth token to a verified email address of the user. Users can click the link and perform some actions without typing their password.
Out-of-the-box, ASP.NET has the [Authorize] attribute and the SimpleMembershipProvider - these seem to work great, but they do some voodoo magic under the hood (like auto-generating database tables), so I don't know how to extend them to add this link-based auth token.
I don't expect an exact answer, but please do point me to the right direction.
Thanks!
Uf, broad question. But I will try at least to direct you to a right direction.
So first if suggest that you use Forms Authentication as a base, but you will have to customize using of it. And I presume that you do not want to use cookies for the authentication as this is native behaviour of the Forms Authentication.
The most important point you should consider to have it you custom query string token based authentication.
Create a login action and in this action you will authorize the user, if he have granted access you ask FormsAuthentication to create AuthCookie. For the further on you just take the httpCookie.Value as your auth token that you will carry in query string.
You need to implement the Application_BeginRequest in the Global.asax that will handle this query string tokens and translate it into the cookie. With this approach you can leverage all the ASP.NET Forms Authentication infrastructure.
This is quite high level picture w/o code. If you need more detail help I can also provide it to you.
You should just use a regular Action that accepts HttpGet.
Upon receiving the token, immediately invalid it so it can't be used again.
Also, only accept tokens that are within your pre-defined range of time period, like 24 or 72 hours.
Thank you Peter for idea.
If smb need to create JWT token authorization for old ASP.NET MVC5.I wrote small example. I don't serialize cookie to JWT. I create a JWT and after I am checking it in the BeginRequest. If everything is ok, I create a cookie and set it to the httpContext.Request. I used authentication mode="Forms" for application and it require cookies.
For create JWT token:
const string secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";
[AllowAnonymous]
[HttpPost]
public ActionResult LoginJWT(LoginViewModel model)
{
ActionResult response = null;
if (ModelState.IsValid)
{
if (true) //todo: check user login&password
{
var payload = new Dictionary<string, object>
{
{ "iss", "subject" },
{ "sub", "api" },
{ "exp", DateTimeOffset.UtcNow.AddHours(2).ToUnixTimeSeconds()},
{ "iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds()},
{ "jti", Guid.NewGuid() },
{ "uid", "64" } //custom field for identificate user
};
IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); // symmetric
IJsonSerializer serializer = new JsonNetSerializer();
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
var token = encoder.Encode(payload, secret);
response = Content(token);
}
else
{
response = new HttpStatusCodeResult(System.Net.HttpStatusCode.BadRequest, "Login or password are not found");
}
}
else
{
response = new HttpStatusCodeResult(System.Net.HttpStatusCode.BadRequest, "Errors in Model");
}
return response;
}
For check JWT token in Global.asax:
public override void Init()
{
this.BeginRequest += this.BeginRequestHandler;
base.Init();
}
private void BeginRequestHandler(object sender, EventArgs e)
{
var bearerToken = this.Context.Request.Headers["Authorization"];
if (bearerToken != null)
{
var token = bearerToken.StartsWith("Bearer ") ? bearerToken.Substring(7) : bearerToken;
const string secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";
int userId = 0;
try
{
IJsonSerializer serializer = new JsonNetSerializer();
var provider = new UtcDateTimeProvider();
IJwtValidator validator = new JwtValidator(serializer, provider);
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); // symmetric
IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder, algorithm);
var json = decoder.DecodeToObject<IDictionary<string, string>>(token, secret, verify: true);
if (json.TryGetValue("uid", out var uid))
{
userId = Convert.ToInt32(uid);
}
}
catch (TokenExpiredException)
{
Console.WriteLine("Token has expired");
}
catch (SignatureVerificationException)
{
Console.WriteLine("Token has invalid signature");
}
if (userId != 0)
{
// check user by id, if found create cookie.
}
}
}
I used:
jwt-dotnet/jwt library 7.2.1