Use another auth method for external api calls

2019-06-26 03:36发布

I have a Web API application with MVC. When a user is using the website, the authentication and authorization is currently automatically handled by the global forms authentication I use, configured in the Web.config like so:

<authentication mode="Forms">
  <forms loginUrl="~/Login" slidingExpiration="true" timeout="1800" defaultUrl="/"></forms>
</authentication>
<authorization>
  <deny users="?" />
</authorization>

This makes sure only logged in users can access the site and call the API.

But I also have an external Windows client for which I would like to use another authentication method. In a test without the forms auth, I set up a custom AuthorizeAttribute that I can use in my controllers like this:

[ApiAuth]
public IEnumerable<string> Get() {
    // Return the resource
}

The AuthorizeAttribute looks something like this:

public class ApiAuthAttribute : AuthorizeAttribute {
    public override void OnAuthorization(HttpActionContext context) {
        // Authenticate the request with a HMAC-based approach
    }
}

This works fine in isolation but I cannot figure out how to allow both auth methods. I would like to the ApiAuth as a fallback if the form auth doesn't work (or the reverse, whatever works), but if I apply the [ApiAuth] attribute, only that will be used and normal users cannot access the api.

So, how can I use multiple auth methods, either by using one of them as a fallback if the other one fails, or configuring the server so the Windows client can call the API some other way then the MVC app, while still keeping the same API calls available to both type of clients?

Thank you.


Edit: One approach that I could probably take, is to let the Windows client authenticate using the forms auth (something like this), but it seems very much like a hack and I would much rather use some other approach.

2条回答
姐就是有狂的资本
2楼-- · 2019-06-26 03:56

FormAuthentication can be achieve multiple way. In old day, we use FormAuthentication Ticket.

Now, you can use claim-based authentication with Owin Middleware which basically is a strip down version of ASP.Net Identity.

After you authenticate a user inside ApiAuthAttribute, you create Principal object.

Web.config

You should not use <authorization> tag in ASP.Net MVC. Instead, you want to use Filter.

<authentication mode="Forms">
  <forms loginUrl="~/Account/Login" timeout="2880" />
</authentication>

ApiAuthAttribute

public class ApiAuthAttribute : AuthorizeAttribute
{
    public override void OnAuthorization(HttpActionContext context)
    {
        // Authenticate the request with a HMAC-based approach

        // Create FormAuthentication after custom authentication is successful
        if (!HttpContext.Current.User.Identity.IsAuthenticated)
        {
            User user = new User {Id = "1234", UserName = "johndoe", 
                 FirstName = "John", LastName = "Doe"};

            // This should be injected using IoC container. 
            var service = new OwinAuthenticationService(
                 new HttpContextWrapper(HttpContext.Current));
            service.SignIn(user);
        }
    }
}

Authentication

public class User
{
    public string Id { get; set; }
    public string UserName { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public interface IAuthenticationService
{
    void SignIn(User user);
    void SignOut();
}

public class OwinAuthenticationService : IAuthenticationService
{
    private readonly HttpContextBase _context;
    private const string AuthenticationType = "ApplicationCookie";

    public OwinAuthenticationService(HttpContextBase context)
    {
        _context = context;
    }

    public void SignIn(User user)
    {
        IList<Claim> claims = new List<Claim>
        {
            new Claim(ClaimTypes.Sid, user.Id),
            new Claim(ClaimTypes.Name, user.UserName),
            new Claim(ClaimTypes.GivenName, user.FirstName),
            new Claim(ClaimTypes.Surname, user.LastName),
        };

        /*foreach (Role role in user.Roles)
        {
            claims.Add(new Claim(ClaimTypes.Role, role.Name));
        }*/

        ClaimsIdentity identity = new ClaimsIdentity(claims, AuthenticationType);

        IOwinContext context = _context.Request.GetOwinContext();
        IAuthenticationManager authenticationManager = context.Authentication;

        authenticationManager.SignIn(identity);
    }

    public void SignOut()
    {
        IOwinContext context = _context.Request.GetOwinContext();
        IAuthenticationManager authenticationManager = context.Authentication;

        authenticationManager.SignOut(AuthenticationType);
    }
}

Startup.cs

[assembly: OwinStartup(typeof(YOUR_APPLICATION.Startup))]
namespace YOUR_APPLICATION
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = "ApplicationCookie",
                LoginPath = new PathString("/Account/Login")
            });
        }
    }
}
查看更多
一纸荒年 Trace。
3楼-- · 2019-06-26 04:07

I implemented something similar a while back. You may want to look at third party auth providers (as they have been tested). If you create your own mechanism make sure that whatever data you store to identify an authenticated user session will be removed based on some expiration value.

When I refer to a token below, please note that I am refering to a hash using a combination of :

  • Some user data
  • Some dynamic data such as tick count
  • Some data that represents what resource is being requested
  • Maybe the parameters

For example. You could hash the username/hh:mm:ss:ms/fully qualified path/enpoint/enpoint parameters into your user's token. Then you have to decide if the token will be valid on a sliding expiration, 30 minutes, or is it only valid per request.

I would add an anonymous endpoint for your test application to authenticate against. This endpoint should accept user credentials and return a token that matches an entry in Ticket table that represents the user with an expiration. Essentially, since you are not attaching a ticket to each request you will have to manage this yourself in some fashion as I have suggested using the http authorization header.

public ActionResult GetAuthententicationToken(Credentials credentials)
{
   //Authenticate the user
   //Insert a record into the Ticket database table and return hash key as token.
   //Return the token to the client.
}

Now the client ,your testing app, has been authenticated against an existing set of credentials and has a token representing that handshake.

Your test app now only has to sign the authorization http header with the value returned from get GetAuthententicationToken().

Now you can implement your AuthorizeAttribute in which case you want to validate the authorization header token with what was previously stored with a successful call to your anonymous GetAuthententicationToken method.

public class ApiAuthAttribute : AuthorizeAttribute {
    public override void OnAuthorization(HttpActionContext context) {
        //Get authorization token from header
        //if caching then get associated Ticket from cache else lookup in database
        //if not valid throw security exception
        //Apply principal to current user based on lookup above       
    }
}

So how to handle FormsAuthentication with the above scheme in mind?

Since Forms Authentication is handled earlier in the request processing than the MVC Authorize you have a perfect opportunity to add your custom authorization header to the incoming request when the user is authenticated via your forms method.

In the same place that you authenticate your forms authentication add something similar to below.

 public FormsAthentication.CreateAuthenticationTicket()
    {
        //Authenticate user
        //Insert a record into the Ticket database table and return hash key as token.
        //Add that token to ticket's data
    }

Next, you need to make sure the custom authorization header is applied per request. The best place to do this would be the Application_AuthenticateRequest in the Global.asax file.

protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
    //if FormsAuthentication.IsAuthenticated
    //Get the token saved in the ticket data   
    //Save the token value in the http authorization header 
}

NOTE : The Ticket database table mention above should save a valid authentication request with a datetime stamp for expiration date. You must ensure that you have a process that runs in the background to enforce the timeout by removing expired session records.

查看更多
登录 后发表回答