I have created a ServiceStack service on top of Asp.Net that implements Basic authentication. Everything is working fine on the service routes. I am able to login and I get the session cookies which are validated on subsequent calls. I'm using an HttpClient for those requests.
I also have a SignalR Hub that runs on the same Asp.Net service, but the Principal is not authenticated on my Hub methods.
Basically what I need is for ServiceStack to intercept calls into my Hub and validate the session cookie and populate the Context.User.Identity and mark it as authenticated. If I can get that set up, a simple [Authorize] attribute on my hub will do the rest.
Here is a sample of my code:
// set up a HttpClient with a cookie container to hold the session cookie
var cookieJar = new CookieContainer();
var handler = new HttpClientHandler { CookieContainer = cookieJar, UseCookies = true, UseDefaultCredentials = false };
var client = new HttpClient(handler) { BaseAddress = _baseUri };
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Basic",
Convert.ToBase64String(Encoding.ASCII.GetBytes(string.Format("{0}:{1}", userName, password))));
// do client login and get response with session cookie...
var response = client.PostAsync(...);
// add the cookies to the SignalR hub connection
var responseCookies = cookieJar.GetCookies(_baseUri);
var cookieContainer = new CookieContainer();
foreach (Cookie cookie in responseCookies)
{
cookieContainer.Add(cookie);
}
_hubConnection = new HubConnection(_baseUri.ToString()) { CookieContainer = cookieContainer };
After this setup, my session cookies are sent to the Hub on each invocation. Somehow I need for ServiceStack to intercept those requests and set the authenticated user.
Let ServiceStack do the authenication and persisting the user session. Then in the SignalR hub endpoints that need authentication put this code:
var cache = AppHostBase.Resolve<ICacheClient>();
var sess = cache.SessionAs<AuthUserSession>();
if (!sess.IsAuthenticated)
throw new AuthenticationException();
Johan's answer works but it is not very convenient to put this code to every method and if you put it in the hub constructor, it will fail in the web on page refresh with "Only ASP.NET Requests accessible via Singletons are supported" exception.
Instead, I have chosen to create a custom attribute that gives you more control on the hub and method call authorization.
Simplest attribute would look like this:
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class AuthorizeServiceStack : AuthorizeAttribute
{
public override bool AuthorizeHubConnection(HubDescriptor hubDescriptor, IRequest request)
{
return CheckAuthorization();
}
public override bool AuthorizeHubMethodInvocation(IHubIncomingInvokerContext hubIncomingInvokerContext, bool appliesToMethod)
{
return CheckAuthorization();
}
private static bool CheckAuthorization()
{
var cache = AppHostBase.Resolve<ICacheClient>();
var sess = cache.SessionAs<AuthUserSession>();
return sess.IsAuthenticated;
}
}
As you can see, the code is the same as in Johan's answer but it will work in the web as well, ensuring that HttpContext.Current is not null when you are calling cache.SessionAs
Related
I have an application where the backend is an asp.net web api and the front-end is a Blazor server side. Both projects are using net6.0.
I have implemented jwt token authentication, so users can register and login from the front-end.
My problem is that if the user refreshes a page, he automatically gets logged out. My understanding is that this can be solved using refresh token (I'm not sure if this understanding is correct).
I have tried to follow this guide: Refresh Token with Blazor WebAssembly and ASP.NET Core Web API
However since I'm using Blazor server side I cannot intercept HTTP Requests using the approach in the article.
My question is: in my Blazor server side application how can I prevent users automatically getting logged out due to page refresh and how can I intercept the http request?
UPDATE: Notice I already have everything working in regards to token and authentication between the back and frontend. The part that I'm missing is inside the blazor server side application in the program.cs file. I basically want to intercept all http request and call a method.
In program.cs I have:
builder.Services.AddScoped<IRefreshTokenService, RefreshTokenService>();
I want RefreshTokenService to be called on every http request. I have tried creating a middleware (which calls the RefreshTokenService), inside the program.cs like:
app.UseMyMiddleware();
But this only get called once.
Here's a very simplified version of an API client I'm using in my app that's also split into an ASP.NET Core API backend and a Blazor Server frontend.
The way it works is that the accessToken gets retreived from local storage and added as an authentication header to the HttpRequestMessage in my API client before each API call.
MyApiClient.cs
public class MyApiClient
{
private readonly IHttpClientFactory _clientFactory;
private readonly IMyApiTokenProvider _myApiTokenProvider;
public MyApiClient(IHttpClientFactory clientFactory, IMyApiTokenProvider myApiTokenProvider)
{
_clientFactory = clientFactory;
_myApiTokenProvider = myApiTokenProvider;
}
public async Task<ApiResponse<CustomerListResponse>> GetCustomersAsync()
{
//create HttpClient
var client = _clientFactory.CreateClient("MyApiHttpClient");
//create HttpRequest
var request = CreateRequest(HttpMethod.Get, "/getCustomers");
//call the API
var response = await client.SendAsync(request);
//if Unauthorized, refresh access token and retry
if(response.StatusCode == HttpStatusCode.Unauthorized)
{
var refreshResult = await RefreshAccessToken(client);
if (refreshResult.IsSuccess)
{
//save new token
await _backendTokenProvider.SetAccessToken(refreshResult.NewAccessToken);
//create request again, with new access token
var retryRequest = await CreateRequest(HttpMethod.Get, "/getCustomers");
//retry
response = await client.SendAsync(retryRequest);
}
else
{
//refresh token request failed
return ApiResponse<CustomerListResponse>.Error("Token invalid");
}
}
//parse response
var customers = await response.Content.ReadFromJsonAsync<ApiResponse<CustomerListResponse>>();
return customers;
}
private HttpRequestMessage CreateRequest<TRequest>(string command, HttpMethod method, TRequest requestModel = null) where TRequest : class
{
//create HttpRequest
var request = new HttpRequestMessage(method, command);
//add body if not empty
if (requestModel is not null)
{
request.Content = JsonContent.Create(requestModel);
}
//set the Auth header to the Access Token value taken from Local Storage
var accessToken = await _myApiTokenProvider.GetAccessToken();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
return request;
}
private async Task<ApiResponse<RefreshTokenResponse>> RefreshAccessToken(HttpClient client)
{
var refreshToken = await _backendTokenProvider.GetRefreshToken();
if (refreshToken is null)
{
return ApiResponse<RefreshTokenResponse>.Error("Refresh token is null, cannot refresh access token");
}
var refreshRequest = CreateRequest(HttpMethod.Post, "/refreshToken", new RefreshTokenRequest(refreshToken));
var refreshResponse = await client.SendAsync(refreshRequest);
var refreshResult = await response.Content.ReadFromJsonAsync<ApiResponse<RefreshTokenResponse>>();
return refreshResult;
}
}
MyApiTokenProvider.cs
public class MyApiTokenProvider : IMyApiTokenProvider
{
private readonly ProtectedLocalStorage _protectedLocalStorage;
public MyApiTokenProvider(ProtectedLocalStorage protectedLocalStorage)
{
_protectedLocalStorage = protectedLocalStorage;
}
public async Task<string> GetAccessToken()
{
var result = await _protectedLocalStorage.GetAsync<string>("accessToken");
return result.Success ? result.Value : null;
}
public async Task<string> GetRefreshToken()
{
var result = await _protectedLocalStorage.GetAsync<string>("refreshToken");
return result.Success ? result.Value : null;
}
public async Task SetAccessToken(string newAccessToken)
{
await _protectedLocalStorage.SetAsync("accessToken", newAccessToken);
}
public async Task SetRefreshToken(string newRefreshToken)
{
await _protectedLocalStorage.SetAsync("refreshToken", newRefreshToken);
}
}
I'm in the process of trying to put together a small project which uses Asp.Net Core Identity, Identity Server 4 and a Web API project.
I've got my MVC project authenticating correctly with IdS4 from which I get a JWT which I can then add to the header of a request to my Web API project, this all works as expected.
The issue I have is how I'm actually adding the token to the HttpClient, basically I'm setting it up for every request which is obviously wrong otherwise I'd have seen other examples online, but I haven't been able to determine a good way to refactor this. I've read many articles and I have found very little information about this part of the flow, so I'm guessing it could be so simple that it's never detailed in guides, but I still don't know!
Here is an example MVC action that calls my API:
[HttpGet]
[Authorize]
public async Task<IActionResult> GetFromApi()
{
var client = await GetHttpClient();
string testUri = "https://localhost:44308/api/TestItems";
var response = await client.GetAsync(testUri, HttpCompletionOption.ResponseHeadersRead);
var data = await response.Content.ReadAsStringAsync();
GetFromApiViewModel vm = new GetFromApiViewModel()
{
Output = data
};
return View(vm);
}
And here is the GetHttpClient() method which I call (currently residing in the same controller):
private async Task<HttpClient> GetHttpClient()
{
var client = new HttpClient();
var expat = HttpContext.GetTokenAsync("expires_at").Result;
var dataExp = DateTime.Parse(expat, null, DateTimeStyles.RoundtripKind);
if ((dataExp - DateTime.Now).TotalMinutes < 10)
{
//SNIP GETTING A NEW TOKEN IF ITS ABOUT TO EXPIRE
}
var accessToken = await HttpContext.GetTokenAsync("access_token");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
return client;
}
My StartUp classes are pretty standard from what I gather, but if they could be useful, then I'll add them in.
I've read many articles and I have found very little information about this part of the flow, so I'm guessing it could be so simple that it's never detailed in guides, but I still don't know!
The problem is that the docs are really spread all over, so it's hard to get a big picture of all the best practices. I'm planning a blog series on "Modern HTTP API Clients" that will collect all these best practices.
First, I recommend you use HttpClientFactory rather than new-ing up an HttpClient.
Next, adding an authorization header is IMO best done by hooking into the HttpClient's pipeline of message handlers. A basic bearer-token authentication helper could look like this:
public sealed class BackendApiAuthenticationHttpClientHandler : DelegatingHandler
{
private readonly IHttpContextAccessor _accessor;
public BackendApiAuthenticationHttpClientHandler(IHttpContextAccessor accessor)
{
_accessor = accessor;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var expat = await _accessor.HttpContext.GetTokenAsync("expires_at");
var dataExp = DateTime.Parse(expat, null, DateTimeStyles.RoundtripKind);
if ((dataExp - DateTime.Now).TotalMinutes < 10)
{
//SNIP GETTING A NEW TOKEN IF ITS ABOUT TO EXPIRE
}
var token = await _accessor.HttpContext.GetTokenAsync("access_token");
// Use the token to make the call.
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
return await base.SendAsync(request, cancellationToken);
}
}
This can be hooked up via DI:
services.AddTransient<BackendApiAuthenticationHttpClientHandler>();
services.AddHttpClient<MyController>()
.ConfigureHttpClient((provider, c) => c.BaseAddress = new Uri("https://localhost:44308/api"))
.AddHttpMessageHandler<BackendApiAuthenticationHttpClientHandler>();
Then you can inject an HttpClient into your MyController, and it will magically use the auth tokens:
// _client is an HttpClient, initialized in the constructor
string testUri = "TestItems";
var response = await _client.GetAsync(testUri, HttpCompletionOption.ResponseHeadersRead);
var data = await response.Content.ReadAsStringAsync();
GetFromApiViewModel vm = new GetFromApiViewModel()
{
Output = data
};
return View(vm);
This pattern seems complex at first, but it separates the "how do I call this API" logic from "what is this action doing" logic. And it's easier to extend with retries / circuit breakers / etc, via Polly.
You can use HttpRequestMessage
// Create this instance once on stratup
// (preferably you want to keep an instance per base url to avoid waiting for socket fin)
HttpClient client = new HttpClient();
Then create an instance of HttpRequestMessage:
HttpRequestMessage request = new HttpRequestMessage(
HttpMethod.Get,
"https://localhost:44308/api/TestItems");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "ey..");
await client.SendAsync(request);
I have a simple api controller with a method "GetDashboard" that contains an Authorize attribute like so...
[RoutePrefix("api/Dashboard")]
public class DashboardController : ApiController
{
[HttpGet]
[Authorize]
[Route("GetDashboard")]
public HttpResponseMessage GetDashboard()
{
//Do stuff...
}
}
I'm using Owin pipeline and bearer tokens for my api authorization and in my Owin configuration i have created two authorization providers using the app.Map() functionality to select the correct mechanism for authorizing users depending your on your entry point to the api like so...
app.Map("/RouteOne", app1 =>
{
appAteb.UseCookieAuthentication(new CookieAuthenticationOptions());
appAteb.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
PublicClientId = "app1";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Authenticate1"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
AllowInsecureHttp = false
};
app1.UseOAuthBearerTokens(OAuthOptions);
});
app.Map("/RouteTwo", app2 =>
{
app2.UseCookieAuthentication(new CookieAuthenticationOptions());
app2.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
PublicClientId = "app2";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Authenticate2"),
Provider = new AnotherAuthProvider(PublicClientId),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
AllowInsecureHttp = false
};
app2.UseOAuthBearerTokens(OAuthOptions);
});
Both mechanisms authenticate correctly, the bearer token is generated and passed back to the browser however when then use the token for authorization on my dashboard controller it returns a 401 Unorthorized?
I suspect its something to do with the app.map because if i remove it and just have one mechanism calls to my dashboard controller work fine however i need to be able to use both mechanisms of authorization and my api controllers to accept the tokens.
Any help on solving this issue would be greatly appreciated.
Thanks
The RoutePrefix of your action GetDashboard() should include the route of one of the maps. For example you could redefine your maps as api/RouteOne and api/RouteTwo and then the route prefix could be for example [RoutePrefix("api/RouteOne/Dashboard")].
I hope it helps.
We are using Embedded STS locally to test our ASP.Net web application. I'm creating a console app to call some of the WebAPI methods and to do some load testing on our app. I'd like to test using a set of users with certain permissions. For our local instance, that means authenticating against EmbeddedSTS.
How do I write an HttpClient to authenticate against EmbeddedSTS to receive this token and auth against my WebAPI endpoints?
Edit: bonus points if I can get the SAML Token while running the app in HTTP mode (not HTTPS).
I figured out how to do this.
Caveat: this is just used for a one off console app that lets us auth against EmbeddedSTS and do the WebAPI calls for stress testing purposes.
Essentially, we simulate what would happen on the browser. This uses the HttpClient and HtmlAgilityPack to parse through the HTML responses, select a user, POST it back to EmbeddedSTS, then POSTs the WS Fed token result and finally receives the FedAuth cookies. After that, the HTTP Client can be used to call any WebAPI or MVC pages in the app.
public static Task<HttpClient> BuildClient(string authurl, string username)
{
var task = Task.Run<HttpClient>(async () =>
{
// setup http client an cookie handler
var handler = new HttpClientHandler();
handler.AllowAutoRedirect = true;
handler.CookieContainer = new System.Net.CookieContainer();
handler.UseCookies = true;
var client = new HttpClient(handler);
client.MaxResponseContentBufferSize = 256000;
client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0)");
client.DefaultRequestHeaders.Add("Connection", "Keep-Alive");
client.DefaultRequestHeaders.ExpectContinue = false;
// this is the html of the page that has the user dropdown
var userSelectionPage = await client.GetStringAsync(authurl);
string actionPathAndQuery = GetAction(userSelectionPage);
// for the purposes of this sample, we just choose the user called admin
var postData = new List<KeyValuePair<string, string>>() {
new KeyValuePair<string, string>("username", username)
};
// now we post the user name and expect to get the ws fed response
var wsfedresponse = await client.PostAsync(authurl + actionPathAndQuery, new FormUrlEncodedContent(postData));
var wsfedcontent = await wsfedresponse.Content.ReadAsStringAsync();
var namevaluepairs = GetHiddenInputNameValues(wsfedcontent);
var finalpost = await client.PostAsync(authurl, new FormUrlEncodedContent(namevaluepairs));
// at this point, the fedauth cookie is set, we are good to go
return client;
});
return task;
}
private static string GetAction(string htmlContent)
{
var d = new HtmlDocument();
d.LoadHtml(htmlContent);
var node = d.DocumentNode.SelectSingleNode("//form[#action]");
var result = node.GetAttributeValue("action", string.Empty);
return result;
}
private static IEnumerable<KeyValuePair<string, string>> GetHiddenInputNameValues(string htmlContent)
{
var d = new HtmlDocument();
d.LoadHtml(htmlContent);
var nodes = d.DocumentNode.SelectNodes("//input[#type='hidden']");
return nodes.Select(p =>
new KeyValuePair<string, string>(
p.GetAttributeValue("name", string.Empty),
System.Web.HttpUtility.HtmlDecode(p.GetAttributeValue("value", string.Empty))
));
}
EmbeddedSts does ws-federation. This is not designed for web Apis. You rather want Oauth2.
I've spent several days attempting to get to grips with ServiceStack and it seems great. Only issue is with authentication which seems to be a lot of friction, hard work and tears.
I want MonoTouch to register users, authenticate against ServiceStack/authenticate against OAuth and generally minimise calls to the database when authenticating.
So far I have got this:
var client = new JsonServiceClient(newbaseUri);
// register a new user:
var registration = new Registration {
FirstName = "john"
UserName = "user" ,
Password = "pass",
Email = "john#john.com",
};
var registerResponse = client.Send<RegistrationResponse>(registration);
--------------------------------
// user registered...later on I authenticate:
var authResponse = client.Send<AuthResponse>(new Auth {
UserName = "user",
Password = "pass",
RememberMe = true
});
var authResponse = clientlogin.Send<AuthResponse>(auth);
--------------------------------
// somehow I need to store 'authresponse' for later calls, not sure how without a browser
// tried manually setting the cookies and credentials parameters later but no joy
// but now I need to call a secured ([Authenticate] decorated) service method:
var client = new JsonServiceClient(newbaseUri);
var response = client.Send<HelloResponse>(new Hello { Name = "World!" });
return response.Result;
-----------------------------------------
// heres the configuration
var appSettings = new AppSettings();
//Default route: /auth/{provider}
Plugins.Add(new AuthFeature(() => new CustomUserSession(),
new IAuthProvider[] {
new CredentialsAuthProvider(appSettings), // never seems to get called
//new FacebookAuthProvider(appSettings), // not sure how to get this to work on monotouch
//new TwitterAuthProvider(appSettings), // same issue as facebook
new BasicAuthProvider(appSettings) // works but what about caching/tokens/cookies?
}));
//Default route: /register
Plugins.Add(new RegistrationFeature()); // how do i send extra params to this as created in mongodb collection
var mongoClient = new MongoClient("mongodb://localhost");
var server = mongoClient.GetServer();
var db = server.GetDatabase("users");
container.Register<ICacheClient>(new MemoryCacheClient());
container.Register<IUserAuthRepository>(new MongoDBAuthRepository(db, true));
My question is:
1) How do I enable extra fields to be passed in along with registration (as the mongodb [Servicestack.Authentication.Mongodb] has lots of empty fields i.e. birthdate, firstline, city, timezone, etc) that are not present in ServiceStack.Common.ServiceClient.Web.Registration object?
2) How can I transfer the cookie (or even maybe a token system) sent in the 'authresponse' to subsequent calls in order to allow ServiceStack to match against the session for ongoing authentication rather than more ongoing database calls that what seems to be issue with 'basic authentication' method (i.e CredentialsAuthProvider doesnt get called on server side)?
Please help...I've read documentation, run tests, examined social bootstrap and now I'm seriously losing days over this and thinking of integrating SS with simplemembership or even throwing ServiceStack away completely for old skool soap/wcf which is far easier to implement by the looks of it :(
1) If you want to use the Registration Plugin I don't think you can add additional fields since the Registration request/class is already defined. You could make your own registration Service and call into the RegistrationService/Plugin. Also, this post might be helpful.
[Route("/myregistration")]
public class MyRegistration : Registration //Add Additional fields for registration
{
public DateTime? BirthDate { get; set; }
public string Gender { get; set; }
}
public class MyRegisterService : Service
{
public IUserAuthRepository UserAuthRepo { get; set; }
public object Post(MyRegistration request)
{
using (var registrationService = base.ResolveService<RegistrationService>())
{
//handle the registration
var response = registrationService.Post(request.TranslateTo<Registration>());
}
//save the additional data
var userAuth = request.TranslateTo<UserAuth>();
UserAuthRepo.SaveUserAuth(userAuth);
//can make your own response or grab response from RegistrationService above
return new MyRegistrationResponse();
}
}
2) You can authenticate your JsonServiceClient and reuse it to make multiple requests.
var client = new JsonServiceClient(newbaseUri);
var authResponse = client.Send<AuthResponse>(new Auth {
UserName = "user",
Password = "pass",
RememberMe = true
}); //if successful your 'client' will have a populated CookieContainer with 'ss-id' and 'ss-pid' values
//reusing 'client' (after successful authentication) to make a request
//to a service requiring authentication
var response = client.Send<HelloResponse>(new Hello { Name = "World!" });
If reusing your 'client' is not an option you can try to store the ss-id. I don't know much about MonoTouch and how it stores 'browser sessions' so I'm not sure how you would accomplish this. After you authenticate and store the ss-id you can add it to the client using a Request Filter
//Get ss-id value
foreach(Cookie cookie in previousAuthenticatedClient.GetCookies(new Uri(newbaseUri)))
{
if (cookie.Name == "ss-id")
{
//store ss-id
}
}
var newClient = new JsonServiceClient(newbaseUri)
{
LocalHttpWebRequestFilter = (req) =>
{
req.CookieContainer.Add(new Uri("http://localhost:56006"), new System.Net.Cookie("ss-id", ssId));
}
};