My scenario
In my scenario I have a web app that calls some API endpoints to fetch data. On the API side the user is authenticated using a JWT token. In order to improve security, the JWT token is short-lived and the API provides a refresh-token that can be used along with the expired token to request a new pair of JWT and refresh-token.
The user is authenticated on the web app side using cookie authentication. In this authentication cookie I stored both the JWT token and refresh-token (with some other claims).
The client never makes calls directly to the API. The client makes a request to the web app, that when needed calls the API using the token extracted from the authentication cookie.
If the token is expired, it refreshes it using the refresh-token.
The problem
This works fine with a single request, but the problem is when I have a page that makes multiple calls in parallel. When the token is expired, the first call is made, the token is refreshed and so the cookie and all works fine. But the others requests, that have been sent before the updated cookie is returned, fail as the refresh token cannot be used again to ask for new tokens and this results in an error.
I used ASP.NET Core 6.0 for both API and web app.
How can this scenario be managed?
What I'm currently using
The following is the helper class that I've implemented to call the API. This automatically manages expired tokens.
For brevity I posted just the method that allows to make GET calls, but the workflow is the same for POST, PATCH, PUT, DELETE .
NOTE: When a call to the API fails because of an expired token, my API sets the header Token-Expired with value true in the response (in the code I used the const string CustomResponseHeaders.ExpiredTokenHeader).
public class MyApiCaller : IApiCaller
{
private readonly HttpClient _apiClient;
private readonly IHttpContextAccessor _httpContextAccessor;
private string _token;
private string _refreshToken;
public bool IsLoggedIn => _apiClient.DefaultRequestHeaders.Authorization != null;
public Uri BaseUri { get; }
public MyApiCaller (IConfiguration configuration, IHttpContextAccessor httpContextAccessor)
{
_apiClient = new HttpClient()
{
BaseAddress = new Uri(configuration.GetValue<string>("ApiRootUrl"))
};
_apiClient.DefaultRequestHeaders.Clear();
_apiClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
BaseUri = _apiClient.BaseAddress;
_httpContextAccessor = httpContextAccessor;
}
public void SetCredentials(string token, string refreshToken)
{
_token = token;
_refreshToken = refreshToken;
_apiClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
public void Logout()
{
_token = null;
_refreshToken = null;
_apiClient.DefaultRequestHeaders.Authorization = null;
}
public async Task<HttpResponseMessage> GetAsync(Uri uri)
{
var response = await _apiClient.GetAsync(uri);
if (response.IsSuccessStatusCode)
// All is good, return the response
return response;
else
{
// Check if the request failed because of an expired token
if (response.StatusCode == HttpStatusCode.Unauthorized && response.Headers.Contains(CustomResponseHeaders.ExpiredTokenHeader))
{
// The request failed due to an expired token
// Try to refresh the token
bool refreshed = await RefreshTokenAsync();
if (!refreshed)
// Failed to refresh so return the original response
return response;
// Repeat the original request with the new token
return await GetAsync(uri);
} else
{
// It is not a token-related error
// Return the unsuccessful response
return response;
}
}
}
private async Task<bool> RefreshTokenAsync()
{
var json = JsonConvert.SerializeObject(new RefreshTokenRequest
{
Token = _token,
RefreshToken = _refreshToken
});
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _apiClient.PostAsync("identity/refresh-token", content);
if (response.IsSuccessStatusCode)
{
SuccessfulAuthResponse successfulAuth = await response.ToModelAsync<SuccessfulAuthResponse>(); // Custom method to desirialize a JSON response
// Set new credentials
SetCredentials(successfulAuth.Token, successfulAuth.RefreshToken);
// Update the tokens in cookie
_httpContextAccessor.HttpContext.Response.UpdateTokensInCookie(successfulAuth.Token, successfulAuth.RefreshToken);
return true;
}
else
{
return false;
}
}
}
Related
I have code to allow us to use the NetSuite REST API using OAuth 1.0. Everything works fine, except one call. When trying to do /salesorder/{id}/!transform/itemFulfillment It fails with 401. All other calls work fine. When I execute the same call from Postman it works fine too. What am I missing?
Here is my Code:
private static async Task CreateItemFulFillmentsAsync(NetSuiteJob job, int id, Item item)
{
RestRequest request = new RestRequest($"{job.RecordUrl}/salesorder/{id}/!transform/itemFulfillment", Method.Post);
request.AddBody(item);
RestHelper restHelper = new RestHelper();
RestResponse response = await restHelper.ExecuteRestRequest(request, job);
if (response == null || !response.IsSuccessful)
{
throw new Exception($"Failed to create the Item Fulfillment for the Sales Order: {id}.\r\n" + response.Content);
}
}
And the Helper Class:
public async Task<RestResponse> ExecuteRestRequest(RestRequest request, NetSuiteJob job)
{
RestClient client = new RestClient(job.BaseUrl) { Authenticator = GetOAuth1Authenticator(job) };
RestResponse response = await client.ExecuteAsync(request);
return response;
}
private OAuth1Authenticator GetOAuth1Authenticator(NetSuiteJob job)
{
OAuth1Authenticator oAuth1 = OAuth1Authenticator.ForAccessToken(
consumerKey: job.ConsumerKey,
consumerSecret: job.ConsumerSecret,
token: job.TokenId,
tokenSecret: job.TokenSecret,
OAuthSignatureMethod.HmacSha256);
oAuth1.Realm = job.Realm;
return oAuth1;
}
The results are:
{"type":"https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2",
"title":"Unauthorized","status":401,
"o:errorDetails":[{"detail":"Invalid login attempt. For more details,
see the Login Audit Trail in the NetSuite UI at Setup > Users/Roles
> User Management > View Login Audit Trail.","o:errorCode":"INVALID_LOGIN"}]}
In NetSuite's Login Audit Trail, this call is logged as a failure and Role is blank, but the other calls using different action shows the Role like it should. The working routines use the same helper class but are doing it with a different URL and Body. I've verified the content being passed matches what I did manually in Postman too.
I want to authenticate against an external service in the background. So, whenever a user calls an API to fetch data exposed through my service and the external authentication service returns unauthorised, I want to in the background re-authenticate the user and return the data they were trying to fetch. What would be the best way to solve this? Is using a middleware feasible?
private readonly IHttpClientFactory _httpClientFactory;
private readonly AppSettings _appSettings;
public NotificationService(IHttpClientFactory httpClientFactory,
IOptions<AppSettings> appSettings)
{
_httpClientFactory = httpClientFactory;
_appSettings = appSettings.Value;
}
public async Task FetchNotifications(string projectId)
{
try
{
string notificationURL = _appSettings.BaseURL + "notifications";
HttpRequestMessage httpRequestMessage = new HttpRequestMessage(
HttpMethod.Get,
notificationURL)
{
Headers =
{
{ HeaderNames.Authorization, "Bearer " + _appSettings.bearerToken }
}
};
HttpClient? httpClient = _httpClientFactory.CreateClient();
HttpResponseMessage httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);
//If httpResponseMessage returns 401, re-authenticate in the background and re-run
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw;
}
}
This is the task I want to run whenever the external service returns 401
private async Task Authenticate()
{
try
{
HttpRequestMessage httpRequestMessage = new HttpRequestMessage(
HttpMethod.Post,
_authenticationSettings.AuthenticateURL)
{
Headers =
{
{ HeaderNames.Authorization, _authenticationSettings.Token }
}
};
HttpClient? httpClient = _httpClientFactory.CreateClient();
HttpResponseMessage httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);
if (httpResponseMessage.IsSuccessStatusCode)
{
await using Stream contentStream =
await httpResponseMessage.Content.ReadAsStreamAsync();
AuthToken? token = await JsonSerializer.DeserializeAsync
<AuthToken>(contentStream);
//To-Do: store token in session (?)
}
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
}
Using middleware is feasible.
Usually in our certification process, we log in to get the code, and then use the code to get the token. Token has an expiration date, when invalidated, use the old token to get the refreshed token.
In your scenario, it is possible to determine in the middleware whether the current token is within the validity period. Then effectively next directly to the next middleware, invalid to get the refreshed token, and then next to the next middleware.
I have the following code:
public async Task<TokenResponse> RefreshTokenAsync(string refreshToken)
{
HttpClient client = new();
var discoveryResponse = await client.GetDiscoveryDocumentAsync("https://localhost:44334");
var response = await client.RequestRefreshTokenAsync(new RefreshTokenRequest
{
Address = discoveryResponse.TokenEndpoint,
ClientId = "...",
ClientSecret = "...",
RefreshToken = refreshToken
});
return response;
}
And it always returns 400 Bad Request with invalid_client message. When I'm refreshing token in Postman it works well. Where is the problem?
The purpose of the refresh-token is: the user does not need to re-authenticate with the credentials (username/password) in the application every time the session expires. So your application needs to connect to the endpoint identity and consume a new refresh token before the token or refresh token times out. In asp dotnet core Identity and JwtToken always have a default timeout value; whatever: you need to capture the refresh token before this timeout expires, otherwise your identity understands the user who does not have the browser open or is not online. This may imply developing a routine that stays in Roudin-Robin always refreshing the application with the new Token while the browser is open.
I changed my code to this:
public async Task<TokenResponse> RefreshTokenAsync(string refreshToken)
{
HttpClient client = new();
var discoveryResponse = await client.GetDiscoveryDocumentAsync("https://localhost:44334");
var tokenClient = new TokenClient(client, new TokenClientOptions
{
Address = discoveryResponse.TokenEndpoint,
ClientId = "...",
ClientSecret = "...",
});
var response = await tokenClient.RequestRefreshTokenAsync(refreshToken);
response.HttpResponse.EnsureSuccessStatusCode();
return response;
}
And now it works as expected.
This question is continuation of my previous one: ASP.Net Identity 2 login using password from SMS - not using two-factor authentication
I've build my custom OAuthAuthorizationServerProvider to support custom grant_type.
My idea was to create grant_type of sms that will allow user to generate one-time access code that will be send to his mobile phone and then user as password when sending request with grant_type of password.
Now after generating, storing and sending via SMS that password I'd like to return custom response, not token from my GrantCustomExtension.
public override async Task GrantCustomExtension(OAuthGrantCustomExtensionContext context)
{
const string allowedOrigin = "*";
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] {allowedOrigin});
if (context.GrantType != "sms")
{
context.SetError("invalid_grant", "unsupported grant_type");
return;
}
var userName = context.Parameters.Get("username");
if (userName == null)
{
context.SetError("invalid_grant", "username is required");
return;
}
var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
ApplicationUser user = await userManager.FindByNameAsync(userName);
if (user == null)
{
context.SetError("invalid_grant", "user not found");
return;
}
var generator = new TotpSecurityStampBasedTokenProvider<ApplicationUser, string>();
await userManager.UpdateSecurityStampAsync(user.Id);
var accessCode = await generator.GenerateAsync("SMS", userManager, user);
var accessCodeExpirationTime = TimeSpan.FromMinutes(5);
var result = await userManager.AddAccessCode(user, accessCode, accessCodeExpirationTime);
if(result.Succeeded)
{
Debug.WriteLine("Login code:"+accessCode);
//here I'll send login code to user phone via SMS
}
//return 200 (OK)
//with content type="application/json; charset=utf-8"
//and custom json content {"message":"code send","expires_in":300}
//skip part below
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager, "SMS");
var ticket = new AuthenticationTicket(oAuthIdentity, null);
context.Validated(ticket);
}
How can I stop generating token and return custom response from OAuthAuthorizationServerProvider?
I'm aware of two methods: TokenEndpoint, TokenEndpointResponse, but I'd like to override whole response, not just token.
EDIT:
For now I'm creating temporary ClaimsIdentity in GrantCustomExtension using code below:
var ci = new ClaimsIdentity();
ci.AddClaim(new Claim("message","send"));
ci.AddClaim(new Claim("expires_in", accessCodeExpirationTime.TotalSeconds.ToString(CultureInfo.InvariantCulture)));
context.Validated(ci);
and I'm overriding TokenEndpointResponse:
public override Task TokenEndpointResponse(OAuthTokenEndpointResponseContext context)
{
if (context.TokenEndpointRequest.GrantType != "sms") return base.TokenEndpointResponse(context);
//clear response containing temporary token.
HttpContext.Current.Response.SuppressContent = true;
return Task.FromResult<object>(null);
}
This has two issues: when calling context.Validated(ci); I'm saying this is a valid user, but instead I'd like to response information that I've send access code via SMS.
HttpContext.Current.Response.SuppressContent = true; clears response, but I'd like to return something instead of empty response.
This is more of a workaround then a final solution, but I believe it is the most reliable way of solving your issue without rewriting tons of code from the default OAuthAuthorizationServerProvider implementation.
The approach is simple: use a Owin middleware to catch token requests, and overwrite the response if an SMS was sent.
[Edit after comments] Fixed the code to allow the response body to be buffered and changed as per this answer https://stackoverflow.com/a/36414238/965722
Inside your Startup.cs file:
public void Configuration(IAppBuilder app)
{
var tokenPath = new PathString("/Token"); //the same path defined in OAuthOptions.TokenEndpointPath
app.Use(async (c, n) =>
{
//check if the request was for the token endpoint
if (c.Request.Path == tokenPath)
{
var buffer = new MemoryStream();
var body = c.Response.Body;
c.Response.Body = buffer; // we'll buffer the response, so we may change it if needed
await n.Invoke(); //invoke next middleware (auth)
//check if we sent a SMS
if (c.Get<bool>("sms_grant:sent"))
{
var json = JsonConvert.SerializeObject(
new
{
message = "code send",
expires_in = 300
});
var bytes = Encoding.UTF8.GetBytes(json);
buffer.SetLength(0); //change the buffer
buffer.Write(bytes, 0, bytes.Length);
//override the response headers
c.Response.StatusCode = 200;
c.Response.ContentType = "application/json";
c.Response.ContentLength = bytes.Length;
}
buffer.Position = 0; //reset position
await buffer.CopyToAsync(body); //copy to real response stream
c.Response.Body = body; //set again real stream to response body
}
else
{
await n.Invoke(); //normal behavior
}
});
//other owin middlewares in the pipeline
//ConfigureAuth(app);
//app.UseWebApi( .. );
}
And inside your custom grant method:
// ...
var result = await userManager.AddAccessCode(user, accessCode, accessCodeExpirationTime);
if(result.Succeeded)
{
Debug.WriteLine("Login code:"+accessCode);
//here I'll send login code to user phone via SMS
}
context.OwinContext.Set("sms_grant:sent", true);
//you may validate the user or set an error, doesn't matter anymore
//it will be overwritten
//...
I would recommend to have a look at this answer :
https://stackoverflow.com/a/24090287/2508268
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
{
context.AdditionalResponseParameters.Add(property.Key, property.Value);
}
return Task.FromResult<object>(null);
}
I use dotnetOpenAuth. I want to request authorization to the user's gamil.
Do I need to use openId first?
Cannot find a decent tutorail. Can anyone help?
Tried this code unsuccesfully. Anyway I don't seems to ask for Gmail scope at the auth request, so I'm confused
public void PrepareAuthorizationRequest(Uri authCallbakUrl)
{
var consumer = new WebConsumer(GoogleConsumerConsts.ServiceDescription, mConsumerTokenManager);
// request access
consumer.Channel.Send(consumer.PrepareRequestUserAuthorization(authCallbakUrl, null, null));
throw new NoRedirectToAuthPageException();
}
public ProcessAuthorizationRequestResponse ProcessAuthorizationRequest()
{
ProcessAuthorizationRequestResponse response;
// Process result from the service provider
var consumer = new WebConsumer(GoogleConsumerConsts.ServiceDescription, mConsumerTokenManager);
var accessTokenResponse = consumer.ProcessUserAuthorization();
// If we didn't have an access token response, this wasn't called by the service provider
if (accessTokenResponse == null)
response = new ProcessAuthorizationRequestResponse
{
IsAuthorized = false
};
else
{
// Extract the access token
string accessToken = accessTokenResponse.AccessToken;
response = new ProcessAuthorizationRequestResponse
{
IsAuthorized = true,
Token = accessToken,
Secret = mConsumerTokenManager.GetTokenSecret(accessToken)
};
}
return response;
}
private string Test2()
{
// Process result from linked in
var google = new WebConsumer(GoogleConsumerConsts.ServiceDescription, mConsumerTokenManager);
// var accessToken = GetAccessTokenForUser();
var accessToken = String.Empty;
// Retrieve the user's profile information
var endpoint = GoogleConsumerConsts.GetGmailFeedsEndpoint;// new MessageReceivingEndpoint("http://api.linkedin.com/v1/people/~", HttpDeliveryMethods.GetRequest);
var request = google.PrepareAuthorizedRequest(endpoint, accessToken);
var response = request.GetResponse();
return (new StreamReader(response.GetResponseStream())).ReadToEnd();
}
No, you don't need to use OpenID if you just want to access the user's Gmail. OpenID is for when you want to authenticate the user. OAuth is for when you want to access the user's data.
You need to include the scope parameter in your authorization request as described in this question: Adding scopes to OAuth 1.0 authorization request with DotNetOpenAuth.