Allow anonymous SPA users to access secure resources

I don’t know how many of you have faced this dilemma where your SPA has to access protected resources outside its domain before the user has logged in? Let’s say, you need to display products to all users, whether signed-in or not. You have a Products API which is protected using OAuth2 Client Credentials flow. For security reasons you don’t want to publish this API with anonymous access, but still allow anonymous users to benefit from it. The client would be configured to use OAuth2 Implicit/Hybrid Flow.
Below is a standard flow of Client Credentials.

OAuth2 Client Credentials Flow

The SPA will have access token only when the user has logged in to the Authorisation server. The client can access the protected API using the access token which has the required scope. We are not speaking of token management here, but client secret management.

Now when a user is still anonymous, we still need to provide them with data from our API. Secret management on client side is not a viable option. The only place client secret can be safely stored is with a trusted client which runs on the server side.
There is an active doc by IETF which guides you on using OAuth with browser based apps here. Based on their recommendation at 6.2 we can move the authentication and token management to the ASP.NET Core app which shares the domain with the SPA. The token/session management would be done by Cookies with HttpOnly and Lax or Strict SameSite mode. Check out this blog for more details on SameSite Cookie.

This way we are moving all the authentication to the server and a machine to machine communication takes place through which a token is requested on behalf of the clients.
The BFF (Backend for front end) architecture is more common with Microservices and sometime also referred to as API Gateway. Personally, in this scenario I would lean more towards calling this as a BFF pattern since this nomenclature makes the intent more clearer that we are introducing this API specifically for a front-end, namely SPA.

Let us see what goes in to implement such an API.
In the same ASP.NET Core site which serves the static content you can setup session management and forwarding the requests to the backend API along with attaching a valid access token.
In Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
private const string Authority_TokenEndpoint = "http://localhost:62000/connect/token";
private const string token_cookie_name = "code.token";
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.HttpOnly = Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy.Always;
options.MinimumSameSitePolicy = SameSiteMode.Strict;
});
services.AddHttpClient();
services.AddProxy();
}

You might have noticed line AddProxy(). This is from ProxyKit. A light-weight, code-first HTTP reverse proxy. In this case it deals with the mundane task of forwarding requests to the back-end API with ease and style. It is very powerfull and you can do a lot more with it.
Let is see how we use it next.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseCookiePolicy();
string tokenForCookie = "";
app.Use(async (context, next) =>
{
// get access token
var factory = app.ApplicationServices.GetRequiredService<IHttpClientFactory>();
var client = factory.CreateClient();
var tokenResponse = await client.RequestClientCredentialsTokenAsync(
new ClientCredentialsTokenRequest
{
Address = Authority_TokenEndpoint,

ClientId = "securebff",
ClientSecret = "secret",
Scope = "externalapi"
});
// check if we have a valid response
tokenForCookie = tokenResponse.AccessToken;
var expiresin = DateTime.Now.AddSeconds(tokenResponse.ExpiresIn);
// save to cookie
CookieOptions options = new CookieOptions
{
IsEssential = true,
HttpOnly = true,
SameSite = SameSiteMode.Strict,
Secure = env.IsDevelopment() ? false : true,
Expires = expiresin
};
context.Response.Cookies.Append(token_cookie_name, tokenForCookie, options);
await next();
});
app.Map("/external", api =>
{
api.RunProxy(async context =>
{
var forwardContext = context.ForwardTo("http://localhost:5000/api/test");

var token = string.IsNullOrEmpty(tokenForCookie) 
? context.Request.Cookies[token_cookie_name] 
: tokenForCookie;

forwardContext.UpstreamRequest.SetBearerToken(token);
forwardContext.AddXForwardedHeaders();
// add retry on 401 or other conditions
var response = await forwardContext.Send();
return response;
});
});
app.UseStaticFiles();
}

That’s it! As we receive a request, it creates a new token. Here I am using IdentityModel.Client to retrieve an access token from the STS end-point. It creates a cookie with the provided options and saves the token. If the request is for an external API protected by the access token, then the map would attach the current access token and forward the request to the external API and return the response.
Of course I have omitted checks like cookie existence, token expiration and skipping token retrieval for every requests for brevity. Also the STS and external API are not shown here, but there is nothing different about them here.

For completeness here is the new flow.

OAuth2 Client Credentials Flow with BFF

Some parting notes to consider. The activation of BFF component need not be that long when the token is retrieved from the cookie for subsequent requests. The SameSite cookie helps in stopping CSRF attacks. I am not sure if this completely shields you in XSS attacks. You would be adding additional server round-trips. It’s for you to decide if this choice suits your architecture.