AspNetCore: How to mock external authentication / Microsoft account for integration tests? - asp.net-core

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;
}

Related

ASP.NET Core : Return Json response on Unauthorized in a filter at the controller/action level

I am not using Identity.
I have this ASP.NET Core configuration enabling two authentication schemes, cookies and basic auth:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = "_auth";
options.Cookie.HttpOnly = true;
options.LoginPath = new PathString("/Account/Login");
options.LogoutPath = new PathString("/Account/LogOff");
options.AccessDeniedPath = new PathString("/Account/Login");
options.ExpireTimeSpan = TimeSpan.FromHours(4);
options.SlidingExpiration = true;
})
.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);
BasicAuthenticationHandler is a custom class inheriting from AuthenticationHandler and overriding HandleAuthenticateAsync to check the request headers for basic authentication challenge, and returns either AuthenticateResult.Fail() or AuthenticateResult.Success() with a ticket and the user claims.
It works fine as is:
Controllers/Actions with the [Authorize] tag will check the cookies and redirect to the login page is not present.
Controllers/Actions with the [Authorize(AuthenticationSchemes = "BasicAuthentication")] tag will check the header and reply a 401 Unauthorized HTTP code if not present.
Controllers/Actions with the [Authorize(AuthenticationSchemes = "BasicAuthentication,Cookies")] tag will allow both methods to access the page, but somehow use the Cookies redirection mechanism when failing both checks.
My goal is to have most of my project to use Cookies (hence why it is set as default), but have some API type of controllers to accept both methods. It should also be possible to tag the Controllers/Actions to return a specific Json body when desired (as opposed to the login redirect or base 401 response), but only for certain controllers.
I've spent the last 2 days reading different similar questions and answers here on StackOverflow, nothing seems to accommodate my need.
Here's a few methods I found:
The options under AddCookie allow you to set certain events, like OnRedirectToAccessDenied and change the response from there. This does not work because it applies to the whole project.
Under my BasicAuthenticationHandler class, the AuthenticationHandler class allow to override HandleChallengeAsync to change the response from there instead of replying 401. Unfortunately, again it applies globally to everywhere you use the scheme, not on a controller/action level. Not sure if it's applied when mixing multiple schemes either.
Many answers point to adding a Middleware to the solution, again, it impacts the whole project.
Many answers point to Policies, but it seems to be to control whether or not an user have access to the resource based on claims, not controlling the response when he do not.
Many answers suggest creating a class inheriting from AuthorizeAttribute, IAuthorizationFilter. Again, this allow to override the OnAuthorization method to decide if the user have the right or not to access the resource, but not to control the response AFTER the normal authentication scheme failed.
I'm thinking either there's a filter type I'm missing, or maybe I need to create a third authentication type that will mix the previous two and control the response from there. Finding a way to add a custom error message in the options would also be nice.
I managed to do it via a IAuthorizationMiddlewareResultHandler. Not my favorite approach because there can be only one per project and it intercepts all calls, but by checking if a specific (empty) attribute is set, I can control the flow:
public class JsonAuthorizationAttribute : Attribute
{
public string Message { get; set; }
}
public class MyAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler
{
private readonly AuthorizationMiddlewareResultHandler DefaultHandler = new AuthorizationMiddlewareResultHandler();
public async Task HandleAsync(RequestDelegate requestDelegate, HttpContext httpContext, AuthorizationPolicy authorizationPolicy, PolicyAuthorizationResult policyAuthorizationResult)
{
// if the authorization was forbidden and the resource had specific attribute, respond as json
if (policyAuthorizationResult.Forbidden)
{
var endpoint = httpContext.GetEndpoint();
var jsonHeader = endpoint?.Metadata?.GetMetadata<JsonAuthorizationAttribute>();
if (jsonHeader != null)
{
var message = "Invalid User Credentials";
if (!string.IsNullOrEmpty(jsonHeader.Message))
message = jsonHeader.Message;
httpContext.Response.StatusCode = 401;
httpContext.Response.ContentType = "application/json";
var jsonResponse = JsonSerializer.Serialize(new
{
error = message
});
await httpContext.Response.WriteAsync(jsonResponse);
return;
}
}
// Fallback to the default implementation.
await DefaultHandler.HandleAsync(requestDelegate, httpContext, authorizationPolicy, policyAuthorizationResult);
}
}
I was typing this on comment... but it's doesn't fit... so here is something we probably need to make clear before choosing a solution:
Authorization process happen at the upper middleware above controller
Yes, AuthorizationMiddleware was registered when we use app.UseAuthorization();, that quite far above controller layer, so it was returned long before the request can reach controller, so, any type of filter cannot be applied here.
Not specify an authentication scheme or policy would easily lead to un-stable behavior.
Imagine, Authentication process return an instance of User that stick with the request, but what would happen if the permission on cookie and basicAuth was difference, like cookie have myclaim, while basicAuth doens't ? Related process on both type of scheme was difference (like challenge on cookie would lead to /Account/Login and basicAuth to /Login ?). And various logic case that we could implement on each page.
I Know, this is not possible, but it would become a mess, not for the author of these code, but for those maintainers to come.
Json response for some specific process on client ?
This might sound detailed at first glance, but it would rather become burden soon, if some more authentication requirement raise after that (like Jwt). Covering each of these case on client would make user experience quite awkward (like, half-authentication and authorization).
And if It's un-avoidable in the project. Might I suggest create a default authentication scheme with ForwardDefaultSelector that would elected which authentication scheme to use for each request. And maintain a stable routing HashSet that would use to detect on which endpoint to set Json Response as wished on some upper level than AuthorizationMiddleware, by using middleware, ofcourse. Then, we narrow down to 2 centralize places to checkout the authorization.
Chaos came when we tried to make one thing to do somethings. At least in this case, I think we would breath easier when coming to debug phase.

What is Code Сhallenge in query param in authorization server like IdentityServer (from JS SPA client pov)?

When I do manual redirect, I'm getting an error from IdentityServer
invalid_request, code challenge required
However when I use oidc-client-js library for the same authorization request, I do not get that error. Library somehow sets code challenge under the hood.
Here is me JS code.
Set up:
const config = {
authority: "https://demo.identityserver.io",
client_id: "interactive.confidential",
redirect_uri: "http://localhost:3000/callback",
response_type: "code",
scope:"openid profile email api offline_access",
post_logout_redirect_uri : "http://localhost:3000/post_logout",
};
const url = `https://demo.identityserver.io/connect/authorize?
client_id=${config.client_id}&
redirect_uri=${config.redirect_uri}&
response_type=${config.response_type}&
scope=${config.scope}`;
My manual authorization redirect request that throws:
const onFormSubmit = async (ev: React.FormEvent) => {
ev.preventDefault();
window.location.replace(url); // I simply do replace
}
Code with the library that doesn't throw:
import Oidc from 'oidc-client';
const onFormSubmit = async (ev: React.FormEvent) => {
ev.preventDefault();
const mgr = new Oidc.UserManager(config);
mgr.signinRedirect(); // login redirect here, no errors
}
I want to understand what code challengem is. And how it gets generated. Give me a hint what to read about it.
I ca go on with the library, but I'd prefer not to import third-party libs into my app where possible.
Authorize Endpoint handle multiple grant types, the way you are sending your request, matched to Authorization Code Grant which needs code_challenge parameter during the request.
Try something simpler to make a request like:
GET /connect/authorize?
client_id=client1&
scope=openid email api1&
response_type=id_token token&
redirect_uri=https://myapp/callback&
state=abc&
nonce=xyz
Read Authorize Endpoint for more information.
Heres an example of generating a challenge code:
private string CreateCodeChallenge()
{
_codeVerifier = RandomNumberGenerator.CreateUniqueId();
var sha256 = HashAlgorithmProvider.OpenAlgorithm(HashAlgorithm.Sha256);
var challengeBuffer = sha256.HashData(
CryptographicBuffer.CreateFromByteArray(Encoding.UTF8.GetBytes(_codeVerifier)));
byte[] challengeBytes;
CryptographicBuffer.CopyToByteArray(challengeBuffer, out challengeBytes);
return Base64Url.Encode(challengeBytes);
}
Include the code and the method in the request querystring.
You can generate codes for testing here: https://tonyxu-io.github.io/pkce-generator/
That's as far I've gotten with it but I am shown the login screen.
It's a parameter required by the Proof Key for Code Exchange standard.
OAuth 2.0 public clients utilizing the Authorization Code Grant are susceptible to the authorization code interception attack. This specification describes the attack as well as a technique to mitigate against the threat through the use of Proof Key for Code Exchange (PKCE, pronounced "pixy").

MSAL: AcquireTokenSilentAsync always interacts with the endpoint

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.

asp.net core JWT in uri query parameter?

I have an api that is protected by JWT and Authorize attribute and at the client I use jquery ajax call to deal with it.
This works fine, however I now need to be able to secure downloading of files so I can't set a header Bearer value, can it be done in the URI as an url parameter?
=-=-=-=-
UPDATE: This is what I ended up doing for my scenario which is an in-house project and very low volume but security is important and it might need to scale in future:
When user logs in I generate a random download key and put it in their user record in the db along with the expiry date of their JWT and return the download key to the client. The download route is protected to only allow a download if there is a query parameter that has the download key and that key exists in the user records and that expiry date has not passed. This way the dl key is unique per user, valid as long as the user's auth session is valid and can be revoked easily.
This is a common problem.
Whenever you want to reference images or other files directly from an API in a single page application's HTML, there isn't a way to inject the Authorization request header between the <img> or <a> element and the request to the API. You can sidestep this by using some fairly new browser features as described here, but you may need to support browsers that lack this functionality.
Fortunately, RFC 6750 specifies a way to do exactly what you're asking via the "URI Query Parameter" authentication approach. If you follow its convention, you would accept JWTs using the following format:
https://server.example.com/resource?access_token=mF_9.B5f-4.1JqM&p=q
As stated in another answer and in RFC 6750 itself, you should be doing this only when necessary. From the RFC:
Because of the security weaknesses associated with the URI method (see Section 5), including the high likelihood that the URL containing the access token will be logged, it SHOULD NOT be used unless it is impossible to transport the access token in the "Authorization" request header field or the HTTP request entity-body.
If you still decide to implement "URI Query Parameter" authentication, you can use the Invio.Extensions.Authentication.JwtBearer library and call AddQueryStringAuthentication() extension method on JwtBearerOptions. Or, if you want to do it manually, you can certainly do that as well. Here's a code sample that shows both ways as extensions of the Microsoft.AspNetCore.Authentication.JwtBearer library.
public void ConfigureServices(IServiceCollection services) {
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(
options => {
var authentication = this.configuration.GetSection("Authentication");
options.TokenValidationParameters = new TokenValidationParameters {
ValidIssuers = authentication["Issuer"],
ValidAudience = authentication["ClientId"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(authentication["ClientSecret"])
)
};
// OPTION 1: use `Invio.Extensions.Authentication.JwtBearer`
options.AddQueryStringAuthentication();
// OPTION 2: do it manually
options.Events = new JwtBearerEvents {
OnMessageReceived = (context) => {
StringValues values;
if (!context.Request.Query.TryGetValue("access_token", out values)) {
return Task.CompletedTask;
}
if (values.Count > 1) {
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
context.Fail(
"Only one 'access_token' query string parameter can be defined. " +
$"However, {values.Count:N0} were included in the request."
);
return Task.CompletedTask;
}
var token = values.Single();
if (String.IsNullOrWhiteSpace(token)) {
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
context.Fail(
"The 'access_token' query string parameter was defined, " +
"but a value to represent the token was not included."
);
return Task.CompletedTask;
}
context.Token = token;
return Task.CompletedTask;
}
};
}
);
}
You can use a middleware to set the authorization header from the query param:
public class SecureDownloadUrlsMiddleware
{
private readonly RequestDelegate next;
public SecureDownloadUrlsMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context /* other dependencies */)
{
// get the token from query param
var token = context.Request.Query["t"];
// set the authorization header only if it is empty
if (string.IsNullOrEmpty(context.Request.Headers["Authorization"]) &&
!string.IsNullOrEmpty(token))
{
context.Request.Headers["Authorization"] = $"Bearer {token}";
}
await next(context);
}
}
and then in Startup.cs use the middleware before the authentication middleware:
app.UseMiddleware(typeof(SecureDownloadUrlsMiddleware));
app.UseAuthentication();
Although it is technically possible to include a JWT in the URL, it is strongly discouraged. See the quote from here, which explains why it's a bad idea:
Don't pass bearer tokens in page URLs: Bearer tokens SHOULD NOT be
passed in page URLs (for example, as query string parameters).
Instead, bearer tokens SHOULD be passed in HTTP message headers or
message bodies for which confidentiality measures are taken. Browsers,
web servers, and other software may not adequately secure URLs in the
browser history, web server logs, and other data structures. If bearer
tokens are passed in page URLs, attackers might be able to steal them
from the history data, logs, or other unsecured locations.
However, if you have no choice or just don't care about security practices, see Technetium's answer.
If you still need it,you have to set jwt token on localStorage.After,you have to create a new header with the following code:
'functionName'():Headers{
let header =new Headers();
let token = localStorage.getItem('token')
header.append('Authorization',`Bearer ${token}`);
return header;
}
Add Hader to http requests.
return this.http.get('url',new RequestOptions({headers:this.'serviceName'.'functionName'()}))
Although this is a bit outside of the box, I would advice you to do the same as this is the best scalable solution when developing in the .NET environment.
Use Azure Storage! Or any other similar online cloud storage solution.
It makes sure your web app is separate from your files, so you don't have to worry about moving an application to a different web environment.
Web storage is mostly more expensive then azure storage (1GB with about 3000 operations (read/write/list) costs in total about $0.03.
When you scale your application where downtime is more critical, point 1 also applies when you use a swapping/staging technique.
Azure storage takes care of the expiry of so called Shared Access Tokens (SAS)
For the sake of simplicity for you, I will just include my code here so you don't have to google the rest
So what I do in my case, all my files are saved as Attachments within the database (not the actual file of course).
When someone requests an attachment, I do a quick check to see if the expire date has passed and if so we should generate a new url.
//where ever you want this to happen, in the controller before going to the client for example
private async Task CheckSasExpire(IEnumerable<AttachmentModel> attachments)
{
foreach (AttachmentModel attachment in attachments)
{
await CheckSasExpire(attachment);
}
}
private async Task CheckSasExpire(AttachmentModel attachment)
{
if (attachment != null && attachment.LinkExpireDate < DateTimeOffset.UtcNow && !string.IsNullOrWhiteSpace(attachment.AzureContainer))
{
Enum.TryParse(attachment.AzureContainer, out AzureStorage.ContainerEnum container);
string url = await _azureStorage.GetFileSasLocator(attachment.Filename, container);
attachment.FileUrl = url;
attachment.LinkExpireDate = DateTimeOffset.UtcNow.AddHours(1);
await _attachmentRepository.UpdateAsync(attachment.AttachmentId, attachment);
}
}
AzureStorage.ContainerEnum is just an internal enum to easily track the container certain files are stored in, but these can be strings of course
And my AzureStorage class:
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
public async Task<string> GetFileSasLocator(string filename, ContainerEnum container, DateTimeOffset expire = default(DateTimeOffset))
{
var cont = await GetContainer(container);
CloudBlockBlob blockBlob = cont.GetBlockBlobReference(filename);
DateTimeOffset expireDate = DateTimeOffset.UtcNow.AddHours(1);//default
if (expire != default(DateTimeOffset) && expire > expireDate)
{
expireDate = expire.ToUniversalTime();
}
SharedAccessBlobPermissions permission = SharedAccessBlobPermissions.Read;
var sasConstraints = new SharedAccessBlobPolicy
{
SharedAccessStartTime = DateTime.UtcNow.AddMinutes(-30),
SharedAccessExpiryTime = expireDate,
Permissions = permission
};
var sasToken = blockBlob.GetSharedAccessSignature(sasConstraints);
return blockBlob.Uri + sasToken;
}
private async Task<CloudBlobContainer> GetContainer(ContainerEnum container)
{
//CloudConfigurationManager.GetSetting("StorageConnectionString")
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(_config["StorageConnectionString"]);
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
string containerName = container.ToString().ToLower();
CloudBlobContainer cloudContainer = blobClient.GetContainerReference(containerName);
await cloudContainer.CreateIfNotExistsAsync();
return cloudContainer;
}
So this will produce url's like so: http://127.0.0.1:10000/devstoreaccount1/invoices/NL3_2002%20-%202019-04-12.pdf?sv=2018-03-28&sr=b&sig=gSiohA%2BGwHj09S45j2Deh%2B1UYP1RW1Fx5VGeseNZmek%3D&st=2019-04-18T14%3A16%3A55Z&se=2019-04-18T15%3A46%3A55Z&sp=r
Of course you have to apply your own authentication logic when retrieving the attachments, if the user is allowed to view the file or not. But that can all be done with the JWT token and in the controller or the repository. I wouldn't worry about the URL being a public url, if one is so mighty to get that URL... within one hour... well then reduce the expire date :D

Alternative to cookie based session/authentication

Is there an alternative to the session feature plugin in servicestack? In some scenarios I cannot use cookies to match the authorized session in my service implementation. Is there a possibility to resolve the session using a token in http header of the request? What is the preferred solution for that in case the browser is blocking cookies?
I'm using ServiceStack without the built-in auth and session providers.
I use a attribute as request filter to collect the user information (id and token), either from a cookie, request header or string parameter.
You can provide this information after the user takes login. You append a new cookie to the response and inject the id and token info on clientside when rendering the view, so you can use for http headers and query parameters for links.
public class AuthenticationAttribute : Attribute, IHasRequestFilter
{
public void RequestFilter(IHttpRequest request, IHttpResponse response, object dto)
{
var userAuth = new UserAuth { };
if(!string.IsNullOrWhiteSpace(request.GetCookieValue("auth"))
{
userAuth = (UserAuth)request.GetCookieValue("auth");
}
else if (!string.IsNullOrEmpty(request.Headers.Get("auth-key")) &&
!string.IsNullOrEmpty(request.Headers.Get("auth-id")))
{
userAuth.Id = request.Headers.Get("id");
userAuth.Token = request.Headers.Get("token");
}
authenticationService.Authenticate(userAuth.Id, userAuth.token);
}
public IHasRequestFilter Copy()
{
return new AuthenticationAttribute();
}
public int Priority { get { return -3; } } // negative are executed before global requests
}
If the user isn't authorized, i redirect him at this point.
My project supports SPA. If the user consumes the API with xmlhttprequests, the authentication stuff is done with headers. I inject that information on AngularJS when the page is loaded, and reuse it on all request (partial views, api consuming, etc). ServiceStack is powerful for this type of stuff, you can easily configure your AngularJS app and ServiceStack view engine to work side by side, validating every requests, globalizing your app, etc.
In case you don't have cookies and the requests aren't called by javascript, you can support the authentication without cookies if you always generate the links passing the id and token as query parameters, and pass them through hidden input on forms, for example.
#Guilherme Cardoso: In my current solution I am using a PreRequestFilters and the built-in session feature.
My workflow/workaround is the following:
When the user gets authorized I took the cookie and send it to the client by using an http header. Now the client can call services if the cookie is set in a http-header (Authorization) of the request.
To achieve this I redirect the faked authorization header to the cookie of the request using a PreRequestFilter. Now I am able to use the session feature. Feels like a hack but works for the moment ;-)
public class CookieRestoreFromAuthorizationHeaderPlugin : IPlugin
{
public void Register(IAppHost appHost)
{
appHost.PreRequestFilters.Add((req, res) =>
{
var cookieValue = req.GetCookieValue("ss-id");
if(!string.IsNullOrEmpty(cookieValue))
return;
var authorizationHeader = req.Headers.Get("Authorization");
if (!string.IsNullOrEmpty(authorizationHeader) && authorizationHeader.ToLower().StartsWith("basictoken "))
{
var cookie = Encoding.UTF8.GetString(Convert.FromBase64String(authorizationHeader.Split(' ').Last()));
req.Cookies.Add("ss-id",new Cookie("ss-id",cookie));
req.Items.Add("ss-id",cookie);
}
});
}
}