3

I have a case where I need some controller methods to be accessible either by an authenticated user, or if the request contains a sort of "acccess token" in the url.

For example:

Either an authenticated user could make a call to: https://example.com/some/resource

Or a non authenticated user could make the same call, but add some kind of token to the url (or as a header): https://example.com/some/resource?token=123abc

The token does not have to be super secret, only something hard to guess.

[AllowSpecialToken]
[HttpGet]
[Route("some/resource")]
public async Task<string> GetSomeResource()
{
    return "some resource";
}

What I'm struggling with is how to write the AllowSpecialTokenAttribute. And how to get that to run before the authentication (using OpenIddict) we have in place now.

Is this a stupid use case? Should I find another solution?

To give some context: We have a SPA that calls our API. Some pages of the SPA can be shared with others (non user) just by sending a link. That link will contain the token. The content of those pages are not critical security wise, but they shouldn't be completely open.

Joel
  • 8,502
  • 11
  • 66
  • 115

2 Answers2

1

You need to make your own authentication attribute. I've done something like that in the past, here is my stub at it:

public class TokenAuthenticationAttribute : AuthorizeAttribute
{
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        // this will read `token` parameter from your URL
        ValueProviderResult valueProvided = filterContext.Controller.ValueProvider.GetValue("token");
        if (valueProvided == null)
        {
            filterContext.Result = new System.Web.Mvc.HttpStatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
            return;
        }

        var providedToken = valueProvided.AttemptedValue;

        var storedToken = "12345"; // <-- get your token value from DB or something

        if (storedToken != providedToken)
        {
            filterContext.Result = new System.Web.Mvc.HttpStatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
            return;
        }
    }
}

Then decorate your action with the attribute:

[TokenAuthentication]
[HttpGet]
[Route("some/resource")]
public async Task<string> GetSomeResource()
{
    return "some resource";
}    

And get your URI looking like https:\\www.example.com\api\some\resource?token=12345

trailmax
  • 34,305
  • 22
  • 140
  • 234
  • 1
    Is this for Asp.Net Core or Asp.Net? – Joel Nov 16 '17 at 13:48
  • That's for old asp. Apologies, I've missed that you tagged the question as Core – trailmax Nov 16 '17 at 14:28
  • Yeah, sorry. Guess I fooled you with the asp.net-identity tag. Removed that :) Do have any idea on how to do it using asp.net core? – Joel Nov 16 '17 at 14:29
  • Sorry, not had a chance to do the same in Core. I can only imagine the technique would be described in [this doc](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies) – trailmax Nov 16 '17 at 15:47
0

You could try the below and see if it works for you. Caveat: I have absolutely no idea if this is the "correct" way to do this. I just know it is a way that appears to work. Please test and downvote if you find problems. I still have an open question on another authentication handler I have written, but with no replies, so use with caution. It may be worth contacting blowdart (search users) at MS if you are going to pursue this use case.

Middleware Class

public class TokenCodeAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public const string DefaultSchemeName = "TokenAuthScheme";

    public TokenCodeAuthHandler(
        IOptionsMonitor<AuthenticationSchemeOptions> options, 
        ILoggerFactory logger, 
        UrlEncoder encoder, 
        ISystemClock clock)
        : base(options, logger, encoder, clock)
    {
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        AuthenticateResult result = await this.Context.AuthenticateAsync();
        if (result.Succeeded)
        {
            //User has supplied details
            return AuthenticateResult.Success(result.Ticket);
        }
        else if (Context.Request.Query["token"] == "123abc")   //TODO: Change hard-coded token
        {
            //User has supplied token
            string username = "Test";    //Get/set username here
            var claims = new[]
                {
                    new Claim(ClaimTypes.NameIdentifier, username, ClaimValueTypes.String, Options.ClaimsIssuer),
                    new Claim(ClaimTypes.Name, username, ClaimValueTypes.String, Options.ClaimsIssuer)
                };

            ClaimsPrincipal principal = new ClaimsPrincipal(new ClaimsIdentity(claims, Scheme.Name));
            AuthenticationTicket ticket = new AuthenticationTicket(principal, Scheme.Name);
            return AuthenticateResult.Success(ticket);
        }
        return AuthenticateResult.Fail("Unauthorized");
    }
}

Configure Services in Startup

services.AddAuthentication()
    .AddScheme<AuthenticationSchemeOptions, TokenCodeAuthHandler>(
        TokenCodeAuthHandler.DefaultSchemeName, 
        (o) => { });

Attribute Usage

Use on controller actions as follows: Note - I couldn't seem to override controller level authorize attributes).

[Authorize(AuthenticationSchemes = TokenCodeAuthHandler.DefaultSchemeName)]
[HttpGet]
[Route("some/resource")]
public async Task<string> GetSomeResource()
{
    return "some resource";
}  
SpruceMoose
  • 9,737
  • 4
  • 39
  • 53
  • Thank you! But I can't get it to work :/ . My "regular" authentication kicks in before the TokenCodeAuthHandler is even called... – Joel Nov 17 '17 at 09:24
  • I couldn't seem to get it to override controller level `[Authorize]` tags, so on the controllers I tested it on, I had to remove controller level tags and just have the appropriate Auth tag on each action. Maybe someone will come up with a better way. – SpruceMoose Nov 17 '17 at 09:26
  • Ah ok. I don't have it on each controller, but a global [Authorize] for everything. – Joel Nov 17 '17 at 09:32
  • Hmm, maybe you could use a [policy](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme?tabs=aspnetcore2x#selecting-the-scheme-with-policies) to select the scheme. Otherwise, this probably isn't the answer you are looking for :) – SpruceMoose Nov 17 '17 at 09:38
  • Yes, thank you! However, this still does not work when I want this to override a global auth-filter configured like `services .AddMvc(options => { var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); options.Filters.Add(new AuthorizeFilter(policy)); })` – Joel Nov 17 '17 at 11:56
  • OK, I see the problem. Have rolled back last edit. Only link I could find that is relevant is [this one](https://stackoverflow.com/questions/35825021/override-global-authorize-filter-in-asp-net-core-mvc-1-0#35863514) which applies to Core 1.0. Seems like a lot of effort just to override a global filter though. – SpruceMoose Nov 17 '17 at 13:29