I'm very newbie in .NET and trying to implemented Bearer Token in some WebAPI:
My Controller:
public class ClientController : Controller
{
[Route("/api")]
[AllowAnonymous]
[HttpGet("{clientKey}")]
public async Task<IActionResult> UserLogin(string clientKey)
{
SQLDataResult response = null;
List<SqlParameter> parameters = new List<SqlParameter>();
parameters.Add(new SqlParameter("#clientKey", clientKey));
response = await new SQLDataContext().StoredProcedureExecuteReaderAsync<SQLDataResult>("ClientCheck", parameters);
if (response == null)
{
response = new SQLDataResult();
return Ok(response);
}
else
{
var token = new JwtTokenBuilder()
.AddSecurityKey(JwtSecurityKey.Create("imarh-secret-key"))
.AddSubject("imarh")
.AddIssuer("imarh.Security.Bearer")
.AddAudience("imarh.Security.Bearer")
.AddClaim("MembershipId", "111")
.AddExpiry(1)
.Build();
return Ok(token.Value);
}
}
[Authorize]
[HttpPost]
[Route("/api/get/{clientID}")]
public async Task<IActionResult> ClientAssign(string clientID)
{
SQLDataResult response = null;
List<SqlParameter> parameters = new List<SqlParameter>();
parameters.Add(new SqlParameter("#clientID", clientID));
response = await new SQLDataContext().StoredProcedureExecuteReaderAsync<SQLDataResult>("ClientAssign", parameters);
if (response == null)
response = new SQLDataResult();
return Ok(response);
}
}
Token is getting correctly. When I remove [Authorize] attribute from second API I will get correct result from the database. But when return and pass generated token in Authorization header I always getting 401 Unauthorized with no any result.
Suppose this can happen in different cases, but in mine the only one that fixes the problem was:
// app.UseMvc();
in Startup.cs
Related
How can I persist user session between user usages of the application (closed browser/tab without logout)?
Can a Blazor WebAssembly app persist the user session in a cookie with some expiration date?
Right now I got this startup configuration, and everything related to authorization with IdentityServer works fine. The tokens in Identity Server have a 30 days expiration period.
// startup.cs
builder.Services.AddOidcAuthentication(options =>
{
builder.Configuration.Bind("Local", options.ProviderOptions);
options.UserOptions.RoleClaim = "RoleName";
});
// appsettings.json
"Local": {
"Authority": "https://idserver.url",
"ClientId": "Client",
"DefaultScopes": [
"openid",
"profile",
"email",
"roles",
"offline_access"
],
"ResponseType": "code",
"PostLogoutRedirectUri": "https://localhost:5004/authentication/logout-callback",
"RedirectUri": "https://localhost:5004/authentication/login-callback"
}
Is there a way to persist user session in cookies?
I will try my best to answer your question, because I had the same issue and figured it out.
So essentially in between sessions, the cookie is still there saved in the browser, and even though your AuthenticationStateProvider is not set to authenticated, if you try and execute a call against your API, the Cookie Handler will include the cookie in the request and it will authenticate.
So I was able to implement a solution based off an article that I found here:
https://www.learmoreseekmore.com/2022/04/blazorwasm-cookie-series-part-1-blazor-webassembly-cookie-authentication.html
I assume that you have a delegation handler that attaches the cookie to outgoing HTTP requests like so:
public class CookieHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);
return await base.SendAsync(request, cancellationToken);
}
}
This guy will continue to attach that cookie in between sessions.
The problem is that the AuthenticationStateProvider will not persist his state. So what I did was save a local variable into the local browser storage that allows me to remember if I am authenticated. When I check if I am logged in, I make the following checks:
I check if the AutenticationStateProvider is authenticated.
If not, I check if I have set a local variable in the local storage indicating if I am authenticated. If that local variable exists, then he will make an API call to my web service asking for my user information. If that call completes successfully, then I update my AuthenticationStateProvider
I have a dependency injection service I call ILoginService that has an implementation that looks a bit like so:
ILocalStorageService _storageService;
AuthenticationStateProvider _authStateProvider;
public UserModel User { get; private set; } = new UserModel();
public LoginService(ILocalStorageService storageService, IHttpClientFactory clientFactory, AuthenticationStateProvider authStateProvider) : base(clientFactory)
{
_authStateProvider = authStateProvider;
_storageService = storageService;
}
public async Task<bool> IsLoggedIn()
{
var authState = await _authStateProvider.GetAuthenticationStateAsync();
if (authState.User?.Identity?.IsAuthenticated == true)
{
return true;
}
var isauthenticated = await _storageService.GetItemAsync<string>("isauthenticated");
if (!string.IsNullOrWhiteSpace(isauthenticated))
{
using (var client = _clientFactory.CreateClient("API"))
{
var response = await client.GetAsync("/login");
if (response.IsSuccessStatusCode)
{
string jsonStr = await response.Content.ReadAsStringAsync();
UserModel? user = JsonConvert.DeserializeObject<UserModel>(jsonStr);
if (user == null)
{
await _storageService.RemoveItemAsync("isauthenticated");
return false;
}
else
{
(_authStateProvider as CustomAuthStateProvider)?.SetAuthInfo(user);
this.User = user;
return true;
}
}
}
}
await _storageService.RemoveItemAsync("isauthenticated");
return false;
}
public async Task<bool> Login(LoginCredentials credentials)
{
try
{
if (credentials == null) throw new ArgumentNullException(nameof(credentials));
using (var client = _clientFactory.CreateClient("API"))
{
StringContent body = new StringContent(JsonConvert.SerializeObject(credentials), System.Text.Encoding.UTF8, "application/json");
var response = await client.PostAsync("/login", body);
if (response.IsSuccessStatusCode)
{
string jsonStr = await response.Content.ReadAsStringAsync();
UserModel? user = JsonConvert.DeserializeObject<UserModel>(jsonStr);
if (user == null)
{
await _storageService.RemoveItemAsync("isauthenticated");
return false;
}
else
{
(_authStateProvider as CustomAuthStateProvider)?.SetAuthInfo(user);
this.User = user;
await _storageService.SetItemAsync<string>("isauthenticated", "true");
return true;
}
}
else
{
#if DEBUG
string responseStr = await response.Content.ReadAsStringAsync();
#endif
await _storageService.RemoveItemAsync("isauthenticated");
return false;
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex);
await _storageService.RemoveItemAsync("isauthenticated");
return false;
}
}
public async Task<bool> Logout()
{
try
{
using (var client = _clientFactory.CreateClient("API"))
{
var response = await client.DeleteAsync("/login");
if (!response.IsSuccessStatusCode)
{
#if DEBUG
string responseStr = await response.Content.ReadAsStringAsync();
#endif
}
}
(_authStateProvider as CustomAuthStateProvider)?.ClearAuthInfo();
await _storageService.RemoveItemAsync("isauthenticated");
return true;
}
catch (Exception ex)
{
Console.WriteLine(ex);
return false;
}
}
I hope this helps you fix your problem and anybody else that comes along.
I have a asp.net WebAPI service for user login that takes an email and password. The api method has the following signature. LoginDto has two fileds, Email and password.
public async Task<IActionResult> Login(LoginDto dto)
Once the user is authenticated, WebAPI returns an object that has token and Id:
return Ok(new { Token = GenerateJwtTokenFromClaims(claims), Id=user.Id });
On the client side (Blazor app), I used nswag command line tool by running nswag run and it "successfully" generated the Service and Contract files. Everything complies. nswag generated code is pasted below.
When I want to use the login nswag Service, I have the following method (I also have an overloaded method with CancellationToken but I only use this method):
public System.Threading.Tasks.Task Login2Async(LoginDto body)
{
return Login2Async(body, System.Threading.CancellationToken.None);
}
The question that I have is that how do I get the response out of the nswag-generated-code that the WebAPI login sent back to the client? When I try to assign a var to the method, I get Cannot assign void to an implicitly-typed variable which makes sense since I don't see a return type. I also don't see any logic in the nswag generated service file to return the response to the caller. How do I get the response back from the nswag generated API call? Is there an option I have to set in nswag run to get a response object back? Thanks in advance.
public async System.Threading.Tasks.Task Login2Async(LoginDto body, System.Threading.CancellationToken cancellationToken)
{
var urlBuilder_ = new System.Text.StringBuilder();
urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/api/Account/Login");
var client_ = _httpClient;
var disposeClient_ = false;
try
{
using (var request_ = new System.Net.Http.HttpRequestMessage())
{
var content_ = new System.Net.Http.StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(body, _settings.Value));
content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json");
request_.Content = content_;
request_.Method = new System.Net.Http.HttpMethod("POST");
PrepareRequest(client_, request_, urlBuilder_);
var url_ = urlBuilder_.ToString();
request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
PrepareRequest(client_, request_, url_);
var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
var disposeResponse_ = true;
try
{
var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
if (response_.Content != null && response_.Content.Headers != null)
{
foreach (var item_ in response_.Content.Headers)
headers_[item_.Key] = item_.Value;
}
ProcessResponse(client_, response_);
var status_ = (int)response_.StatusCode;
if (status_ == 200)
{
return;
}
else
if (status_ == 400)
{
var objectResponse_ = await ReadObjectResponseAsync<ProblemDetails>(response_, headers_).ConfigureAwait(false);
throw new ApiException<ProblemDetails>("Bad Request", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
}
else
{
var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
}
}
finally
{
if (disposeResponse_)
response_.Dispose();
}
}
}
finally
{
if (disposeClient_)
client_.Dispose();
}
}
Big thanks to the NSwag team, the issue is resolved. I was returning anonymous object from the WebAPI method. The correct way to do is the following. Notice that IActionResult was changed to ActionResult passing a concrete object to return to the caller.
public async Task<ActionResult<LoginDtoResponse>> Login(LoginDto dto)
then returning
return Ok(new LoginDtoResponse { Token = GenerateJwtTokenFromClaims(claims), Id=user.Id });
After that I did that, the following code was generated:
if (status_ == 200)
{
var objectResponse_ = await ReadObjectResponseAsync<LoginDtoResponse>(response_, headers_).ConfigureAwait(false);
return objectResponse_.Object;
}
I am trying to update my appsync client to authenticate with IAM credentials. In case of API_KEY I set the API_KEY_HEADER like so: request.addHeader(API_KEY_HEADER, this.apiKey); Is there a similar way to authenticate in a Java client with IAM credentials? Is there a header I can pass in to pass in the secret and access keys like here: https://docs.amplify.aws/lib/graphqlapi/authz/q/platform/js#iam? Or should I just be using a cognito user pool as a way to authenticate the request?
According to AWS Documentation we need to use sign requests using the process documented here: https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html and steps listed here: https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html.
I also found an implementation here: https://medium.com/#tridibbolar/aws-lambda-as-an-appsync-client-fbb0c1ce927d. Using the code above:
private void signRequest(final Request<AmazonWebServiceRequest> request) {
final AWS4Signer signer = new AWS4Signer();
signer.setRegionName(this.region);
signer.setServiceName("appsync");
signer.sign(request, this.appsyncCredentials);
}
private Request<AmazonWebServiceRequest> getRequest(final String data) {
final Request<AmazonWebServiceRequest> request =
new DefaultRequest<AmazonWebServiceRequest>("appsync");
request.setHttpMethod(HttpMethodName.POST);
request.setEndpoint(URI.create(this.appSyncEndpoint));
final byte[] byteArray = data.getBytes(Charset.forName("UTF-8"));
request.setContent(new ByteArrayInputStream(byteArray));
request.addHeader(AUTH_TYPE_HEADER, AWS_IAM_AUTH_TYPE);
request.addHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_GRAPHQL);
request.addHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(byteArray.length));
signRequest(request);
return request;
}
private HttpResponseHandler<String> getResponseHandler() {
final HttpResponseHandler<String> responseHandler = new HttpResponseHandler<String>() {
#Override
public String handle(com.amazonaws.http.HttpResponse httpResponse) throws Exception {
final String result = IOUtils.toString(httpResponse.getContent());
if(httpResponse.getStatusCode() != HttpStatus.SC_OK) {
final String errorText = String.format(
"Error posting request. Response status code was %s and text was %s. ",
httpResponse.getStatusCode(),
httpResponse.getStatusText());
throw new RuntimeException(errorText);
} else {
final ObjectMapper objectMapper = new ObjectMapper();
//custom class to parse appsync response.
final AppsyncResponse response = objectMapper.readValue(result, AppsyncResponse.class);
if(CollectionUtils.isNotEmpty(response.getErrors())){
final String errorMessages = response
.getErrors()
.stream()
.map(Error::getMessage)
.collect(Collectors.joining("\n"));
final String errorText = String.format(
"Error posting appsync request. Errors were %s. ",
errorMessages);
throw new RuntimeException(errorText);
}
}
return result;
}
#Override
public boolean needsConnectionLeftOpen() {
return false;
}
};
return responseHandler;
}
private Response<String> makeGraphQlRequest(final Request<AmazonWebServiceRequest> request) {
return this.httpClient.requestExecutionBuilder()
.executionContext(new ExecutionContext())
.request(request)
.execute(getResponseHandler());
}
my question is how to pass username and password from the C# client(xamarin forms) to server's API? if details are correct then the client will get whole product list from webapi(URL).and bind all the details to a listview.I want to get the member details after the success of response code.
the client will send username password from login page to server's API. if server's webapi check whether the details matched with the database, if not, don't let it get product list.
here is the code in loginservices for login(xamarin forms)
public async Task GetData(string username,string password)
{
//string detail = new UserDetails();
UserDetails userDetails = new UserDetails();
// List<UserDetails> detail = new List<UserDetails>();
try
{
var values = new List<KeyValuePair<string, string>>();
values.Add(new KeyValuePair<string, string>("Username", username));
values.Add(new KeyValuePair<string, string>("Password", password));
var content = new FormUrlEncodedContent(values);
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue("nl-NL"));
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var response = await client.PostAsync("http://192.168.1.50/Accounts/Authenticate", content);
return response.IsSuccessStatusCode;
};
}
catch (Exception ex)
{
throw ex;
}
}
here is the code for web api---
public async Task ValidateUser([FromBody] Credentials credentials)
{
using (DemoAPPEntities entities = new DemoAPPEntities())
{
var result = await entities.MemberDetails.Where(x => x.UserName == credentials.UserName && x.Password == credentials.Password).SingleOrDefaultAsync();
if (result == null)
{
return NotFound();
}
return Ok(entities.MemberDetails);
}
}
I have configured an ASOS OpenIdConnect Server using and an asp.net core mvc app that uses the "Microsoft.AspNetCore.Authentication.OpenIdConnect": "1.0.0 and "Microsoft.AspNetCore.Authentication.Cookies": "1.0.0". I have tested the "Authorization Code" workflow and everything works.
The client web app processes the authentication as expected and creates a cookie storing the id_token, access_token, and refresh_token.
How do I force Microsoft.AspNetCore.Authentication.OpenIdConnect to request a new access_token when it expires?
The asp.net core mvc app ignores the expired access_token.
I would like to have openidconnect see the expired access_token then make a call using the refresh token to get a new access_token. It should also update the cookie values. If the refresh token request fails I would expect openidconnect to "sign out" the cookie (remove it or something).
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
AuthenticationScheme = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
ClientId = "myClient",
ClientSecret = "secret_secret_secret",
PostLogoutRedirectUri = "http://localhost:27933/",
RequireHttpsMetadata = false,
GetClaimsFromUserInfoEndpoint = true,
SaveTokens = true,
ResponseType = OpenIdConnectResponseType.Code,
AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet,
Authority = http://localhost:27933,
MetadataAddress = "http://localhost:27933/connect/config",
Scope = { "email", "roles", "offline_access" },
});
It seems there is no programming in the openidconnect authentication for asp.net core to manage the access_token on the server after received.
I found that I can intercept the cookie validation event and check if the access token has expired. If so, make a manual HTTP call to the token endpoint with the grant_type=refresh_token.
By calling context.ShouldRenew = true; this will cause the cookie to be updated and sent back to the client in the response.
I have provided the basis of what I have done and will work to update this answer once all work as been resolved.
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
AuthenticationScheme = "Cookies",
ExpireTimeSpan = new TimeSpan(0, 0, 20),
SlidingExpiration = false,
CookieName = "WebAuth",
Events = new CookieAuthenticationEvents()
{
OnValidatePrincipal = context =>
{
if (context.Properties.Items.ContainsKey(".Token.expires_at"))
{
var expire = DateTime.Parse(context.Properties.Items[".Token.expires_at"]);
if (expire > DateTime.Now) //TODO:change to check expires in next 5 mintues.
{
logger.Warn($"Access token has expired, user: {context.HttpContext.User.Identity.Name}");
//TODO: send refresh token to ASOS. Update tokens in context.Properties.Items
//context.Properties.Items["Token.access_token"] = newToken;
context.ShouldRenew = true;
}
}
return Task.FromResult(0);
}
}
});
You must enable the generation of refresh_token by setting in startup.cs:
Setting values to AuthorizationEndpointPath = "/connect/authorize"; // needed for refreshtoken
Setting values to TokenEndpointPath = "/connect/token"; // standard token endpoint name
In your token provider, before validating the token request at the end of the HandleTokenrequest method, make sure you have set the offline scope:
// Call SetScopes with the list of scopes you want to grant
// (specify offline_access to issue a refresh token).
ticket.SetScopes(
OpenIdConnectConstants.Scopes.Profile,
OpenIdConnectConstants.Scopes.OfflineAccess);
If that is setup properly, you should receive a refresh_token back when you login with a password grant_type.
Then from your client you must issue the following request (I'm using Aurelia):
refreshToken() {
let baseUrl = yourbaseUrl;
let data = "client_id=" + this.appState.clientId
+ "&grant_type=refresh_token"
+ "&refresh_token=myRefreshToken";
return this.http.fetch(baseUrl + 'connect/token', {
method: 'post',
body : data,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json'
}
});
}
and that's it, make sure that your auth provider in HandleRequestToken is not trying to manipulate the request that is of type refresh_token:
public override async Task HandleTokenRequest(HandleTokenRequestContext context)
{
if (context.Request.IsPasswordGrantType())
{
// Password type request processing only
// code that shall not touch any refresh_token request
}
else if(!context.Request.IsRefreshTokenGrantType())
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "Invalid grant type.");
return;
}
return;
}
The refresh_token shall just be able to pass through this method and is handled by another piece of middleware that handles refresh_token.
If you want more in depth knowledge about what the auth server is doing, you can have a look at the code of the OpenIdConnectServerHandler:
https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server/blob/master/src/AspNet.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.Exchange.cs
On the client side you must also be able to handle the auto refresh of the token, here is an example of an http interceptor for Angular 1.X, where one handles 401 reponses, refresh the token, then retry the request:
'use strict';
app.factory('authInterceptorService',
['$q', '$injector', '$location', 'localStorageService',
function ($q, $injector, $location, localStorageService) {
var authInterceptorServiceFactory = {};
var $http;
var _request = function (config) {
config.headers = config.headers || {};
var authData = localStorageService.get('authorizationData');
if (authData) {
config.headers.Authorization = 'Bearer ' + authData.token;
}
return config;
};
var _responseError = function (rejection) {
var deferred = $q.defer();
if (rejection.status === 401) {
var authService = $injector.get('authService');
console.log("calling authService.refreshToken()");
authService.refreshToken().then(function (response) {
console.log("token refreshed, retrying to connect");
_retryHttpRequest(rejection.config, deferred);
}, function () {
console.log("that didn't work, logging out.");
authService.logOut();
$location.path('/login');
deferred.reject(rejection);
});
} else {
deferred.reject(rejection);
}
return deferred.promise;
};
var _retryHttpRequest = function (config, deferred) {
console.log('autorefresh');
$http = $http || $injector.get('$http');
$http(config).then(function (response) {
deferred.resolve(response);
},
function (response) {
deferred.reject(response);
});
}
authInterceptorServiceFactory.request = _request;
authInterceptorServiceFactory.responseError = _responseError;
authInterceptorServiceFactory.retryHttpRequest = _retryHttpRequest;
return authInterceptorServiceFactory;
}]);
And here is an example I just did for Aurelia, this time I wrapped my http client into an http handler that checks if the token is expired or not. If it is expired it will first refresh the token, then perform the request. It uses a promise to keep the interface with the client-side data services consistent. This handler exposes the same interface as the aurelia-fetch client.
import {inject} from 'aurelia-framework';
import {HttpClient} from 'aurelia-fetch-client';
import {AuthService} from './authService';
#inject(HttpClient, AuthService)
export class HttpHandler {
constructor(httpClient, authService) {
this.http = httpClient;
this.authService = authService;
}
fetch(url, options){
let _this = this;
if(this.authService.tokenExpired()){
console.log("token expired");
return new Promise(
function(resolve, reject) {
console.log("refreshing");
_this.authService.refreshToken()
.then(
function (response) {
console.log("token refreshed");
_this.http.fetch(url, options).then(
function (success) {
console.log("call success", url);
resolve(success);
},
function (error) {
console.log("call failed", url);
reject(error);
});
}, function (error) {
console.log("token refresh failed");
reject(error);
});
}
);
}
else {
// token is not expired, we return the promise from the fetch client
return this.http.fetch(url, options);
}
}
}
For jquery you can look a jquery oAuth:
https://github.com/esbenp/jquery-oauth
Hope this helps.
Following on from #longday's answer, I have had success in using this code to force a client refresh without having to manually query an open id endpoint:
OnValidatePrincipal = context =>
{
if (context.Properties.Items.ContainsKey(".Token.expires_at"))
{
var expire = DateTime.Parse(context.Properties.Items[".Token.expires_at"]);
if (expire > DateTime.Now) //TODO:change to check expires in next 5 mintues.
{
context.ShouldRenew = true;
context.RejectPrincipal();
}
}
return Task.FromResult(0);
}