MSAL: AcquireTokenSilentAsync always interacts with the endpoint - authentication

I'm lookig at MSAL and I'm trying to understand what's the correct way to use it in a client app. In my case, I'd like to authenticate the user and then use the id token against a "private" web api app.
Now, I was under the impression that AcquireTokenSilentAsync would reuse an existing token from the cache (when available) without performing an extra call to the authentication endpoint if the token was still valid and the requestes scopes could be satisfied (this was my interpretation and it probably is wrong). However, this seems not to be the case. What I'm seeing with fiddler is that this method will always access the authorization endpoint.
Initially, I thought that my client service wrappers should always cal this method in order to get the id token, which would then be passed to the backend web site through the authentication bearer header. Here's an example of what I mean:
public async Task<string> GetAllWorkers() {
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", await GetToken());
var request = new HttpRequestMessage(HttpMethod.Get, _url);
var resposta = await _httpClient.SendAsync(request);
var content = await resposta.Content.ReadAsStringAsync();
return content;
}
GetToken is a method that wraps the typical code used for authenticating the user (uses a try/catch block for wrapping the AcquireTokenSilentAsync and, when that fails, redirects the user to the AcquireTokenAsync method for showing the login UI).
The question: is having this extra call before all my backend services really the way to go? Or should I cache the token and reuse it in all the internal web services call until I get a 401 (and only then should I call the GetToken method to refresh my id token?)
Editing to give more info
_clientApp = new PublicClientApplication(ClientId,
Authority,
TokenCacheHelper.GetUserCache());
TokenCacheHelper is the token cache helper that comes with most Azure AD samples. The GetToken method which returns the authentication header is a single liner that interacts with the helper that encapsulates the _clientApp field shown above:
return (await _helper.AuthenticateUser()).IdToken
And here is the AuthenticateUser method:
public async Task<AuthenticationResult> AuthenticateUser() {
try {
return await _clientApp.AcquireTokenSilentAsync(_scopes, _clientApp.Users.FirstOrDefault());
}
catch (MsalUiRequiredException ex) {
return await RetryWithGraphicalUI();
}
}
Now, the token cache helper is being hit. What I don't understand is why the AcquireTokenSilentAsync method ends up always calling the oauth2 endpoint (https://login.microsoftonline.com/{azure ad guid}/oauth2/v2.0/token)...
Meanwhile, I've changed the code making my helper class cache the AuthenticationResult. Now, AcquireTokenSilentAsync will only be called when one of the "internal" app's web api methods return 401 in response to a call performed with the bearer authorization header.

In the end, I've went along with caching the AuthenticationResult and it's ID Token. This seems to be the best option since it saves me a remote call. I'll only try to call AcquireTokenSilentAsync again when the web service returns 401.

Related

Correct way to retrigger loadTokens in Ktor client

In my mobile app, I have a singleton Ktor HttpClient with bearer authentication configured as such:
HttpClient(…) {
install(Auth) {
bearer {
sendWithoutRequest { requestBuilder -> some condition }
loadTokens(tokensStorage::getTokens)
refreshTokens {
performTokenRefresh(oldTokens.refreshToken)
}
}
}
}
Now consider the following flow:
The user opens the app while the tokens stored in my tokenStorage are not valid.
The HttpClient loads the tokens from tokenStorage and the request fails with a 401.
The HttpClient tries to perform a refresh but it fails too because the refresh token is invalid.
The user is redirected to the login page, logs in, and the new valid tokens are stored in tokenStorage.
Now from the app the user can retry the call that failed in points 2-3. However this will fail forever until the app is closed, because the HttpClient never tries to call loadTokens anymore. Indeed, as far as I can see from the source code, loadTokens is called only once and then never again.
I found out a couple of ways to solve the issue.
The first one is to manually retrieve BearerAuthProvider from the HttpClient and clear the token myself like in the following snippet, but it seems like a hacky workaround:
httpClient.plugin(Auth).providers
.filterIsInstance<BearerAuthProvider>()
.first().clearToken()
Another one is to manually load the current token from my tokenStorage in refreshToken and disregard what get passed in this#refreshTokens.oldTokens:
refreshTokens {
val currentRefreshToken = tokenStorage.getTokens().refreshToken
performTokenRefresh(currentRefreshToken)
}
However this means that the client will do an unnecessary call to the refresh API, while having already a valid token pair (obtained from the login).
So my question is: is there a cleaner way to handle the situation? Am I misusing Ktor?

AspNetCore: How to mock external authentication / Microsoft account for integration tests?

I have an OpenID Connect / OAuth2 server (IdP) in my application stack. IdP allows both local and external authentication.
I have integration tests covering most scenarios, but struggle to create a end-to-end test for an external authentication scenario. There are multiple external providers, but from my application perspective they are all using the same workflow over OpenID Connect, only have slight difference (parameters, ie. redirect uri, scheme name, etc.). So it is enough to test one of them. One of them is Microsoft Account (aka. Azure AD)
Integration test is based on WebApplicationFactory (in-memory server with corresponding HttpClient). Local authentication is quite easy, because the whole part runs in my application domain, have access to full source code, etc. I simply create a request to the authorization endpoint and post back user credentials when prompted (I still need to parse the login page to retrieve the antiforgery token, but that's doable)
But when it comes to external, for example Microsoft Account, login involves multiple steps via AJAX and the final post with over 10 parameters, which I unable to reverse engenineer. Other provider has also same level of difficulty.
Since external providers are just blackboxes, from my IdP's perspective, it's just issuing a challenge (redirect to external authorization) and pick up after redirect. Is there a good way to mock the "in between" part?
My solution was to create a middleware, which will mock the external authentication. And then re-configure options for the external authentication scheme to direct to the path middleware is handling. You may also want to overwrite the signingkey (or turn of signature validation). So this code goes to WebApplicationFactory's ConfigureServices/ConfigureTestServices (etc., depending on your setup), to override original setup:
services.AddTransient<IStartupFilter, FakeExternalAuthenticationStartupFilter>();
services.Configure(AuthenticationSchemes.ExternalMicrosoft, (OpenIdConnectOptions options) =>
{
options.Configuration = new OpenIdConnectConfiguration
{
AuthorizationEndpoint = FakeExternalAuthenticationStartupFilter.AuthorizeEndpoint,
};
options.TokenValidationParameters.IssuerSigningKey = FakeExternalAuthenticationStartupFilter.SecurityKey;
});
Remark: WebApplicationFactory does not provide a way to override IApplicationBuilder (middleware) stack, so need to add IStartupFilter
The middleware then needs to issue a token with the security key and issue a form post back to the redirect uri. The usual way to achieve this to return simple HTML page with a form which will submit itself once loaded. This works fine in browsers, but HttpClient won't do anything, so the test have to parse the response and create a post request manually.
While this is doable, I wanted to spare this extra step, having to parse respond and re-send it, and make it a single step. Difficulties were:
redirect is not possible (starts as GET request, should ended as POST, need also form data)
cookies issued by OpenIdConnectHandler before redirecting (correlation and nonce) necessary to restore state, only available at redirect uri path (Set-Cookie with path=)
My solution was creating a middleware handling authorization (GET) requests at the same path as the redirect uri is set up, issue token and rewrite request so that OpenIdConnectHandler would pick up. Here's middleware's Invoke method:
public async Task Invoke(HttpContext httpContext)
{
if (!HttpMethods.IsGet(httpContext.Request.Method) || !httpContext.Request.Path.StartsWithSegments(AuthorizeEndpoint))
{
await _next(httpContext);
return;
}
// get and validate query parameters
// Note: these are absolute minimal, might need to add more depending on your flow logic
var clientId = httpContext.Request.Query["client_id"].FirstOrDefault();
var state = httpContext.Request.Query["state"].FirstOrDefault();
var nonce = httpContext.Request.Query["nonce"].FirstOrDefault();
if (clientId is null || state is null || nonce is null)
{
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
return;
}
var token = CreateToken(clientId, state, nonce); // CreateToken implementation omitted, use same signing key as used above
httpContext.Request.Method = HttpMethods.Post;
httpContext.Request.QueryString = QueryString.Empty;
httpContext.Request.ContentType = "application/x-www-form-urlencoded";
var content = new FormUrlEncodedContent(new Dictionary<string, string>()
{
["id_token"] = token,
["token_type"] = "Bearer",
["expires_in"] = "3600",
["state"] = state,
});
using var buffer = new MemoryStream();
await content.CopyToAsync(buffer, httpContext.RequestAborted);
buffer.Seek(offset: 0, loc: SeekOrigin.Begin);
var oldBody = httpContext.Request.Body;
httpContext.Request.Body = buffer;
await _next(httpContext);
httpContext.Request.Body = oldBody;
}

How to call StateHasChanged() / NotifyAuthenticationStateChanged from a class in Blazor WASM

In my project I created a class to handle http POST. The main method in the class first check if a JWT Token is available in local storage, check exp date, decide if a new toekn is needed and if so use the refresh token and finally do the POST.
I inject this class with builder.Services.AddTransient<IMyHttp, MyHttp>();
Now I would like to notify the UI (StateHasChanged() or NotifyAuthenticationStateChanged) in case the refresh token is not valid so to log out the user immediately.
The point is that I do not know how to raise the event from my http class (while from a controller is just a matter of calling this.StateHasChanged()).
As suggested here you are the (pseudo)code:
Index controller call the WebAPI to check weather:
(HttpResponseMessage, IEnumerable<WeatherForecast>) result = await MyHttp.PostPFAsync<IEnumerable<WeatherForecast>>("WeatherForecast/GetWeather", null);
This is MyHttp.PostPFAsync injected with builder.Services.AddTransient<IMyHttp, MyHttp>(); in Program.cs
public async Task<(HttpResponseMessage, T)> PostPFAsync<T>(string requestUri, object data)
{
// I get my JWT Token from localstorage, set up auth headers, create StreamContent content serializing data and then
HttpResponseMessage response = await _http.PostAsync(requestUri, content);
if (response.IsSuccessStatusCode)
{
return (response, "Happiness");
}
else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
// Here I clear tokens from localstorage and I'd also like to logout user and update UI
}
else return (response, default(T));
}
Obviosuly I could logout and update the UI in the Index controller but this would mean to make the check everywhere I call the WebAPI via MyHttp.PostPFAsync while I would like to centralize it as soon as I get a 401 (actually I'll do this if I fail to use the refresh token but keep things simple in this example).

Refresh Token request returning new refresh token?

I'm using Oauth2 on my own Web API and ASP.NET C# to consume that API on a web app. On my web app, I'm making HttpWebRequests. When my access token expires, I'm calling a method "RefreshToken" that makes a request to get a new access token. This works beautifully without issue...except that the response I get back contains a new refresh token??? I was expecting just the new access token. I didn't even think this was possible without passing credentials again, but my grant_type=refresh_token is somehow generating a new refresh token, and it has me concerned.
Please see this post by Taiseer Joudeh (which is a phenomenal series of posts by the way).
You will find in the SimpleRefreshTokenProvider's CreateAsync method, the refresh token is deleted and re-created which provides "sliding expiration". If you don't want a new refresh token each time don't delete/recreate.
Here is the line of code I'm talking about:
var result = await tokenRepository.AddRefreshToken(token);
AddRefreshToken actually deletes and re-creates the token as seen here:
public async Task<bool> AddRefreshToken(AspNetRefreshToken token)
{
var existingToken = _context.AspNetRefreshTokens.SingleOrDefault(r => r.Subject == token.Subject && r.ClientId == token.ClientId);
if (existingToken != null)
{
await RemoveRefreshToken(existingToken);
}
_context.AspNetRefreshTokens.Add(token);
return await _context.SaveChangesAsync() > 0;
}
So again, without seeing your code I would say its working as expected. If you don't want sliding expiration, don't have the provider re-create the refresh token each time.

ember simple auth session, ember data, and passing a Authorization header

I have a working oauth2 authentication process where I get an access token (eg from facebook) using ember simple auth, send it to the back end which calls fb.me() and then uses JWT to create a token. This token is then sent back to the ember app, which then has to send it with every server request, include those requests made by ember-data.
I also need to have this token available after a browser reload.
I have tried many options, where I set a property 'authToken' on the session - I believe that this uses local storage to persist the authenticated session.
But I always seem to have trouble with coordinating the retrieval of this token - either I don't have access to the session, or the token is no longer on the session, or I can't change the ember data headers.
Does anyone have a working simple example of how this can be done - I think it should be easy, but I'm obviously missing something!
Thanks.
Update
The only thing I've been able to get working is to use torii as shown below, but the session content is still lost on refresh - I can see its still authenticated, but its lost the token I set here. So I'm still looking for a real solution.
authenticateWithGooglePlus: function () {
var self = this;
this.get('session').authenticate('simple-auth-authenticator:torii', 'google-oauth2')
.then(function () {
resolveCodeToToken(self.get('session'), self);
});
}
resolveCodeToToken gets the bearer token from the server, sets it on the session and then transitions to the protected page:
function resolveCodeToToken(session, route) {
var authCode = session.content.authorizationCode;
var type = session.content.provider.split('-')[0];
$.ajax({
url: 'http://localhost:4200/api/1/user/auth/' + type,
data: {authCode: authCode}
}).done(function (response) {
// todo handle invalid cases - where user is denied access eg user is disabled
session.set('authToken', response.token);
route.transitionTo('activity', moment().format('DDMMYYYY'));
});
}
And I have a custom authorizer for putting the token (stored in the session) on every request:
import Base from 'simple-auth/authorizers/base';
export default Base.extend({
authorize: function(jqXHR, requestOptions) {
var accessToken = this.get('session.content.authToken');
if (this.get('session.isAuthenticated') && !Ember.isEmpty(accessToken)) {
jqXHR.setRequestHeader('Authorization', accessToken);
}
}
});
I'm not sure why this.get('session.content.authToken') would be undefined after a refresh, I thought by default the session was persisted in local storage. The fact that it is authenticated is persisted, but thats useless without the token since the server will reject calls to protected endpoints.
You'd want to implement your own custom authenticator that first gets a token from Facebook and then sends that to your own server to exchange it for a token for your app. Once you have that you get authorization of ember-data requests as well as session persistence etc. for free.
Have a look at this example: https://github.com/simplabs/ember-simple-auth/blob/master/examples/7-multiple-external-providers.html