Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP Core 3.0 API Token Custom Token Authentication (not jwt!)

We have a ASP CORE 3 API Project that we need to secure with an API Token. These API Tokens will be provisioned and loaded from the database, but as proof of concept we will hardcode for testing. Everything we have looked at for token authorization refers to JWTs. We do not want to use JWTs. We simply provision API Keys that allow access to our API - and then users can call API methods by passing the token in the headers e.g. X-CUSTOM-TOKEN: abcdefg.

How can I modify startup.cs and the pipeline so that this X-CUSTOM-TOKEN header is checked on EVERY request? A simple point in the right direction would be great.

EDIT: Okay this looks like a great start! Thank you so much!

Your example seems to indicate that the user API token is the User Token. Our requirements are that we need an API Key to use the API, and then also a User Token to call certain controllers.

Example: myapi.com/Auth/SSO (pass API Token and User Information to login, returns User information + User Token)

myapi.com/Schedule/Create (requires both an API Token header & a header with user token)

Could you suggest how to modify your code to support this?

like image 926
Scott Moniz Avatar asked Dec 10 '22 01:12

Scott Moniz


1 Answers

You could create a custom authentication scheme for this scenario because there's already a builtin Authenticationmiddleware. Also, custom Authentication Scheme allows you to integrate with the built-in authentication/authorization subsystem. You don't have to implement your own challenge/forbid logic.

For example, create a handler & options as below:

public class MyCustomTokenAuthOptions : AuthenticationSchemeOptions
{
    public const string DefaultScemeName= "MyCustomTokenAuthenticationScheme";
    public string  TokenHeaderName{get;set;}= "X-CUSTOM-TOKEN";
}

public class MyCustomTokenAuthHandler : AuthenticationHandler<MyCustomTokenAuthOptions>
{
    public MyCustomTokenAuthHandler(IOptionsMonitor<MyCustomTokenAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) 
        : base(options, logger, encoder, clock) { }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        if (!Request.Headers.ContainsKey(Options.TokenHeaderName))
            return Task.FromResult(AuthenticateResult.Fail($"Missing Header For Token: {Options.TokenHeaderName}"));

        var token = Request.Headers[Options.TokenHeaderName];
        // get username from db or somewhere else accordining to this token
        var username= "Username-From-Somewhere-By-Token";
        var claims = new[] {
            new Claim(ClaimTypes.NameIdentifier, username),
            new Claim(ClaimTypes.Name, username),
            // add other claims/roles as you like
        };
        var id = new ClaimsIdentity(claims, Scheme.Name);
        var principal = new ClaimsPrincipal(id);
        var ticket = new AuthenticationTicket(principal, Scheme.Name);
        return Task.FromResult(AuthenticateResult.Success(ticket));
    }
}

And then configure this authentication scheme in your startup:

services.AddAuthentication(MyCustomTokenAuthOptions.DefaultScemeName)
    .AddScheme<MyCustomTokenAuthOptions,MyCustomTokenAuthHandler>(
        MyCustomTokenAuthOptions.DefaultScemeName,
        opts =>{
            // you can change the token header name here by :
            //     opts.TokenHeaderName = "X-Custom-Token-Header";
        }
    );

Also don't forget to enable the Authentication middleware in the Configure(IApplicationBuilder app, IWebHostEnvironment env) method:

    app.UseRouting();

    app.UseAuthentication();       // add this line, the order is important
    app.UseAuthorization(); 

    app.UseEndpoints(endpoints =>{  ... });

Finally, protect your endpoints like:

[Authorize(AuthenticationSchemes=MyCustomTokenAuthOptions.DefaultScemeName)]
public IActionResult ScretApi()
{
    return new JsonResult(...);
}

or use Authorize() directly because we've set the MyCustomTokenAuth Scheme as the default authentication scheme:

[Authorize()]
public IActionResult ScretApi()
{
    return new JsonResult(...);
}

[Edit]:

Our requirements are that we need an API Key to use the API, and then also a User Token to call certain controllers.

Ok. Assume we have had a TokenChecker that checks the api key and the token is correct(Since I don't know the concrete business logic, I just return true here):

public static class TokenChecker{
    public static Task<bool> CheckApiKey(StringValues apiKey) {
        return Task.FromResult(true);// ... return true/false according to the business
    }

    public static Task<bool> CheckToken(StringValues userToken) {
        return Task.FromResult(true);// ... return true/false according to the business
    }
}

And change the above authentication scheme to check the ApiKey & UserToken header as below:

public class MyCustomTokenAuthOptions : AuthenticationSchemeOptions
{
    public const string DefaultScemeName= "MyCustomTokenAuthenticationScheme";
    public string  ApiKeyHeaderName{get;set;}= "X-Api-Key";
    public string  UserTokenHeaderName{get;set;}= "X-User-Token";
}

public class MyCustomTokenAuthHandler : AuthenticationHandler<MyCustomTokenAuthOptions>
{
    public MyCustomTokenAuthHandler(IOptionsMonitor<MyCustomTokenAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) 
        : base(options, logger, encoder, clock) { }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        if (!Request.Headers.ContainsKey(Options.ApiKeyHeaderName))
            return AuthenticateResult.Fail($"Missing Header For Token: {Options.ApiKeyHeaderName}");
        if (!Request.Headers.ContainsKey(Options.UserTokenHeaderName))
            return AuthenticateResult.Fail($"Missing Header For Token: {Options.UserTokenHeaderName}");

        var apiKey= Request.Headers[Options.ApiKeyHeaderName];
        var userToken = Request.Headers[Options.UserTokenHeaderName];
        var succeeded= await TokenChecker.CheckToken(userToken) && await TokenChecker.CheckApiKey(apiKey);
        if(!succeeded ){ return AuthenticateResult.Fail("incorrect ApiKey or UserToken"); }

        var username = "the-username-from-user-token"; //e.g. decode the userToken header
        var claims = new[] {
            new Claim(ClaimTypes.NameIdentifier, username),
            new Claim(ClaimTypes.Name, username),
            // add other claims/roles as you like
        };
        var id = new ClaimsIdentity(claims, Scheme.Name);
        var principal = new ClaimsPrincipal(id);
        var ticket = new AuthenticationTicket(principal, Scheme.Name);
        return AuthenticateResult.Success(ticket);
    }
}

And Change your Auth/SSO endpoint to return a user token:

public class AuthController: Controller
{
    private readonly MyCustomTokenAuthOptions _myCustomAuthOpts;
    // inject the options so that we can know the actual header name
    public AuthController(IOptionsMonitor<MyCustomTokenAuthOptions> options)
    {
        this._myCustomAuthOpts= options.CurrentValue;
    }

    [HttpPost("/Auth/SSO")]
    public async System.Threading.Tasks.Task<IActionResult> CreateUserTokenAsync()
    {
        var apiKeyHeaderName =_myCustomAuthOpts.ApiKeyHeaderName ;
        if (!Request.Headers.ContainsKey(apiKeyHeaderName))
            return BadRequest($"Missing Header For Token: {apiKeyHeaderName}");

        // check key
        var succeeded = await TokenChecker.CheckApiKey(Request.Headers[apiKeyHeaderName]);
        if(!succeeded)
            return BadRequest($"Incorrect Api Key");
        return Json(... {userInfo, apiKey} ... );
    }
}
like image 125
itminus Avatar answered Dec 12 '22 22:12

itminus