0

I am wanting to implement two websites that need to communicate with each other. (Since one of the sites has a separate deployment for each customer, and is spread across many servers, sharing a database or communicating privately is not an option.) So I've been looking into RESTful APIs.

Unfortunately, I'm running into a lot of information that I'm not familiar with. One issue is security. We don't need anything fancy--we're not a bank or anything. I think we can just get away with HTTPS and a basic username and password.

Questions:

  1. How would I pass the username and password to the API? Would they just be passed as bare arguments in the URL?

  2. Does .NET provide any mechanism for authorizing such username and passwords, or do I just manually see if the password is in our database on each and every request? (I would hash for security.)

ruffin
  • 16,507
  • 9
  • 88
  • 138
Jonathan Wood
  • 65,341
  • 71
  • 269
  • 466
  • I think looking into the authorization header would be a good start. Doing some hashing of a username|password combination with a salt and putting it in that header would be a good start. – Jesse Moreland Jan 06 '17 at 16:59
  • @JesseMoreland Well, I was hoping for some more step by step information related to the questions I'm asking. But why do you think it's good to combine the username and password, and what good is a salt if the data is hashed and not encrypted? – Jonathan Wood Jan 06 '17 at 17:03
  • Hm I guess you're right. Should have said encryption and not hashing. I think that if you give it a time value, then the authorization token will eventually expire right? If you decrypt it and get back some time value, and say you want to make it expire within 10 minutes, just make sure that the generated header's time value was within 10 minutes. Salting is REALLY necessary, just an added precaution. – Jesse Moreland Jan 06 '17 at 17:24
  • Put on hold because I'm looking for a tutorial or book? You must be kidding me. I've got two specific questions, and then I ask if anyone can point me to some additional information. What's wrong with my two questions? It wasn't even voted to close, just one overzealous moderator and, apparently, I have no way to voice an objection! Thanks to those who answered. – Jonathan Wood Jan 10 '17 at 16:21
  • @JonathanWood: Your post was flagged because the community can't vote to close posts with a bounty, which is when moderators step in. Your question is also *too broad*. Stick to *one question at a time*. – Martijn Pieters Jan 10 '17 at 16:42
  • 1
    @JonathanWood: you are free to edit your question and narrow down the scope. The normal re-open review queue procedure then applies, and if reopened you can re-apply your bounty. – Martijn Pieters Jan 10 '17 at 16:44
  • Note that by voting to re-open you put the post into the review queue early, and the community voted to leave it closed: https://stackoverflow.com/posts/41510443/timeline. Next time, edit the post first. – Martijn Pieters Jan 10 '17 at 16:56
  • @MartijnPieters I've removed the explicit request for a book/article. Was there something else that needed editing to reopen? I get the "don't ask two questions", but I think these "two" questions are so closely related that they reduce to two different angles of viewing the same focused domain -- "_What's the best way for the interrelated services to manage passing username and password if I'm not a bank/super concerned about security?_" The question did produce good, reasonably succinct, high-value answers. – ruffin Sep 22 '20 at 14:08
  • @MartijnPieters: BTW, I believe this sort of restricted nature about what can be asked or discussed is why stackoverflow is on the decline. – Jonathan Wood Sep 22 '20 at 14:17

3 Answers3

2

How would I pass the username and password to the API? Would they just be passed as bare arguments in the URL?

It can be either in the URL or in the header. If you are using HTTPS, it will all be encrypted so it will not be bare. Please see this for more details.

Does .NET provide any mechanism for authorizing such username and passwords, or do I just manually see if the password is in our database on each and every request? (I would hash for security.)

No you do not need to check the database on every request. You can check once, create a token with an expiry and the client can keep sending you the token. This way you do not have to keep checking the database every single time.

Please see see this answer for some helpful information.

I think basic authentication with base64 encoding will be sufficient. If not you can always change it. Here are the different ways to apply it to your backend code:

To apply an authentication filter to a controller, decorate the controller class with the filter attribute. The following code sets the [IdentityBasicAuthentication] filter on a controller class, which enables Basic Authentication for all of the controller's actions.

[IdentityBasicAuthentication] // Enable Basic authentication for this controller.
[Authorize] // Require authenticated requests.
public class HomeController : ApiController
{
    public IHttpActionResult Get() { . . . }
    public IHttpActionResult Post() { . . . }
}

To apply the filter to one action, decorate the action with the filter. The following code sets the [IdentityBasicAuthentication] filter on the controller's Post method.

[Authorize] // Require authenticated requests.
public class HomeController : ApiController
{
    public IHttpActionResult Get() { . . . }

    [IdentityBasicAuthentication] // Enable Basic authentication for this action.
    public IHttpActionResult Post() { . . . }
}

To apply the filter to all Web API controllers, add it to GlobalConfiguration.Filters.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Filters.Add(new IdentityBasicAuthenticationAttribute());

        // Other configuration code not shown...
    }
}

Finally here is an example of the implementation, you may change it as you need:

using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Principal;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.Filters;
using BasicAuthentication.Results;

namespace BasicAuthentication.Filters
{
    public abstract class BasicAuthenticationAttribute : Attribute, IAuthenticationFilter
    {
        public string Realm { get; set; }

        public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
        {
            HttpRequestMessage request = context.Request;
            AuthenticationHeaderValue authorization = request.Headers.Authorization;

            if (authorization == null)
            {
                // No authentication was attempted (for this authentication method).
                // Do not set either Principal (which would indicate success) or ErrorResult (indicating an error).
                return;
            }

            if (authorization.Scheme != "Basic")
            {
                // No authentication was attempted (for this authentication method).
                // Do not set either Principal (which would indicate success) or ErrorResult (indicating an error).
                return;
            }

            if (String.IsNullOrEmpty(authorization.Parameter))
            {
                // Authentication was attempted but failed. Set ErrorResult to indicate an error.
                context.ErrorResult = new AuthenticationFailureResult("Missing credentials", request);
                return;
            }

            Tuple<string, string> userNameAndPasword = ExtractUserNameAndPassword(authorization.Parameter);

            if (userNameAndPasword == null)
            {
                // Authentication was attempted but failed. Set ErrorResult to indicate an error.
                context.ErrorResult = new AuthenticationFailureResult("Invalid credentials", request);
                return;
            }

            string userName = userNameAndPasword.Item1;
            string password = userNameAndPasword.Item2;

            IPrincipal principal = await AuthenticateAsync(userName, password, cancellationToken);

            if (principal == null)
            {
                // Authentication was attempted but failed. Set ErrorResult to indicate an error.
                context.ErrorResult = new AuthenticationFailureResult("Invalid username or password", request);
            }
            else
            {
                // Authentication was attempted and succeeded. Set Principal to the authenticated user.
                context.Principal = principal;
            }
        }

        protected abstract Task<IPrincipal> AuthenticateAsync(string userName, string password,
            CancellationToken cancellationToken);

        private static Tuple<string, string> ExtractUserNameAndPassword(string authorizationParameter)
        {
            byte[] credentialBytes;

            try
            {
                credentialBytes = Convert.FromBase64String(authorizationParameter);
            }
            catch (FormatException)
            {
                return null;
            }

            // The currently approved HTTP 1.1 specification says characters here are ISO-8859-1.
            // However, the current draft updated specification for HTTP 1.1 indicates this encoding is infrequently
            // used in practice and defines behavior only for ASCII.
            Encoding encoding = Encoding.ASCII;
            // Make a writable copy of the encoding to enable setting a decoder fallback.
            encoding = (Encoding)encoding.Clone();
            // Fail on invalid bytes rather than silently replacing and continuing.
            encoding.DecoderFallback = DecoderFallback.ExceptionFallback;
            string decodedCredentials;

            try
            {
                decodedCredentials = encoding.GetString(credentialBytes);
            }
            catch (DecoderFallbackException)
            {
                return null;
            }

            if (String.IsNullOrEmpty(decodedCredentials))
            {
                return null;
            }

            int colonIndex = decodedCredentials.IndexOf(':');

            if (colonIndex == -1)
            {
                return null;
            }

            string userName = decodedCredentials.Substring(0, colonIndex);
            string password = decodedCredentials.Substring(colonIndex + 1);
            return new Tuple<string, string>(userName, password);
        }

        public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
        {
            Challenge(context);
            return Task.FromResult(0);
        }

        private void Challenge(HttpAuthenticationChallengeContext context)
        {
            string parameter;

            if (String.IsNullOrEmpty(Realm))
            {
                parameter = null;
            }
            else
            {
                // A correct implementation should verify that Realm does not contain a quote character unless properly
                // escaped (precededed by a backslash that is not itself escaped).
                parameter = "realm=\"" + Realm + "\"";
            }

            context.ChallengeWith("Basic", parameter);
        }

        public virtual bool AllowMultiple
        {
            get { return false; }
        }
    }
}

If you still want to read more then here is a great article which goes into details. I have copied the above code from this article. It has lots of great information.

Community
  • 1
  • 1
CodingYoshi
  • 25,467
  • 4
  • 62
  • 64
1

If you control or exert significant influence on both sides of the connection, client ssl certificates is a really strong and powerful way of doing this. It's attractive to me in this case because it only requires distributing a trusted CA certificate which can be done before the client certificates are created. It's far more secure than any username and password could ever be ( because the password doesn't need to go across the wire).

Any other solution with authentication I can think of, you're going to have to have some sort of data source to verify the credentials. But x509 solves this problem for you. We've done it at work between applications and other than managing the certificates it works really, really well. And it's basically the most secure thing available.

I don't know much about .net in general, but ( not to lmgtfy ) https://support.microsoft.com/en-us/kb/315588 seems like the step by step format you are looking for.

erik258
  • 14,701
  • 2
  • 25
  • 31
  • I'll read up on the linked article. If it's much more complicated that a username and password, it might not be worth it as we're not a bank or anything like that. We just want to avoid easy access to the data by other parties. – Jonathan Wood Jan 06 '17 at 17:08
  • Any other method you're going to have to both store and distribute the secrets. But yeah, it's basically the most complicated way to do it - up front. When you can add clients without having to update code on every server, you may find you are willing to pay for that complexity up front, so to speak. Good luck! – erik258 Jan 06 '17 at 17:11
  • The other website is the only client, so I'll weigh that in my decision. Thanks. – Jonathan Wood Jan 06 '17 at 17:14
0

Just a thought, and it really depends on what you meant by "username/password". If this means "authorization"/access to some API call and you want to ensure that the client is "authorized" to make a call to your API (only apps A, B can make api calls to API - and it seems this is what you're looking for based on your comment above):

As in the comment above, authorization header, using JWT. There is an great/easy JWT library in Nuget

  • it's pretty much something like a "shared secret" used to sign a "payload" (the JWT)

  • the "sender" will build the JWT and sign it (and add to header or whatever protocol you want - it can be body if prefer it over headers)

  • the "receiver" will verify the JWT sent

    • this includes handling/mitigating "replays" - the JWT spec has an "expire" field (exp) that you can have the library validate as well (or not, it's up to you)

The project site is on Github with samples.

Hth.

EdSF
  • 11,753
  • 6
  • 42
  • 83