I've been bringing myself up to speed on HTTP Basic Authentication.
I understand that this is fundamentally an insecure access mechanism (even when used over HTTPS, which it always should be) but I recognise that HTTP Basic Auth is not entirely without utility and I would like to be familiar with it, even if situations seldom arise in which I might deploy it.
My understanding so far:
After some reading, I understand that:
- a server may request authorisation for a resource by returning
401 (unauthorised)
- a
WWW-Authenticate
response header determines that the authentication to be used to access this resource will beBasic
HTTP Basic Authentication
requires either that:- a) the user submit username and password via a browser-generated console; or that
- b) once successfully submitted manually, the same username and password combination will be automatically submitted via an
Authorization
request header prepended to each HTTP request
So far, so good.
Issues to be aware of:
I also understand that there are some issues with HTTP Basic Auth which have evolved over time, like:
- some browsers no longer accept URL syntax such as
https://mylogin:[email protected]/my-resource.html
- where PHP is being run through CGI or FastCGI then the submitted Authorization Credentials will not be passed to
$_SERVER['HTTP_AUTHORIZATION']
unless a hack is deployed - the most common recommendation being a URL rewrite via.htaccess mod_rewrite
and other issues which have persisted from the very beginning, like:
- it's a non-trivial problem to "log out" of HTTP Basic Auth since it was never intended or designed to have a log-out mechanism
The missing piece of the puzzle:
However, I'm still confused, because even where the user (or the Authorization
request header) has submitted valid authentication credentials... how does the server know they are valid?
In every document I have come across discussing the mechanics of HTTP Basic Authentication the discussion stops short of the point at which the credentials are actually authenticated.
Question:
How are the submitted credentials actually authenticated?
Where is the server comparing the submitted credentials to... anything?
Bonus Question:
N.B. This is related to my main question immediately above because my use of .htaccess
and queryString
parameters to convey credentials (see below) renders deployment of HTTP Basic Auth entirely redundant - if I go down this route, I can convey credentials using .htaccess
and queryString
parameters alone and I don't need to deploy HTTP Basic Auth at all.
As a way to circumvent the CGI/FastCGI issue, I often see variations of these .htaccess
lines cited:
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L]
or
RewriteRule .* - [env=HTTP_AUTHORIZATION:%{HTTP:Authorization},last]
or
SetEnvIf Authorization . HTTP_AUTHORIZATION=$0
though my attempts to get any of these methods to populate PHP Environment Variables with the credentials have all proven unsuccessful.
Instead, I have deployed the following (using queryString
parameters instead of Environment variables), successfully:
# WRITE HTTP BASIC AUTHENTICATION CREDENTIALS TO QUERY STRING
RewriteCond %{HTTP:Authorization} [NC]
RewriteCond %{QUERY_STRING} ^basicauth=login$ [NC]
RewriteRule ^my-document.php https://example.com/my-document.php?basicauth=login-submitted&credentials=%{HTTP:Authorization} [NC,L]
which appends the credentials as queryString
parameters.
I am not unhappy with my own mod_rewrite
solution above, but I am stumped that I cannot get Environment Variables working at all.
I'm idly wondering if there is something obvious I'm missing when it comes to the latter - like... might they be switched off in my PHP Configuration?
(And, if so, which entries would I need to check in PHPInfo
to confirm that they were actually switched on and receptive to values transferred to them via mod_rewrite
?)
CodePudding user response:
So this is only from my understanding but I'm pretty confident about it...: If you are sending HTTP Basic Authentication you are supplying a username and password which is automatically encrypted to base64 and sent in the header as for example: Basic ZG9kb3BhbmE6YXV0bw== , which when received in the header on the other side is in the headers authorization value and it still looks like: Basic YmFzaWM6YXV0bw== :) => a quick only base64 decode will return from this: dodopana:auto - which are the username:password I gave to the post.
So far so good?! :) now, the basic auth has been sent and received. the next step - is not a server step... it's the API or endpoint software itself that as it demands a token or a username and password... should validate the decrypted auth infront of the API suppliers credentials.
It's just passing through an encrypted string and the comparing and or validating of said header is now in the API software hands or should I say - the programmer should validate it...
CodePudding user response:
The explanation stops short of the actual authentication because how you do this is entirely up to you.
The username and password are sent as clear text* to you. So, same as when someone would submit a login form with a username
and password
field, you can then decide what to do with it.
*: Yes, they are base64-encoded. But my point was that they aren't hashed or encrypted or anything like that.
So, you may...
- Compare the username & password to some other clear-text value, for example from an environment variable
- Hash the password and compare the hash with something, for example a hashed password in a database
- Forward the credentials to some external authentication service
- ...?
Example of how that could look on the server (this example assumes the use of node.js, Koa, koa-router, Mongoose and bcrypt, and for simplification purposes it assumes neither username nor password are allowed to contain a colon):
router.get('/protectedPage', async ctx => {
const [authMethod, authData] = ctx.get('authorization')?.split(' ') ?? []
if (authMethod === 'Basic') {
try {
const decoded = Buffer.from(authData, 'base64').toString()
const [username, password] = decoded.split(':')
// Find user in database and verify password
const user = await User.findOne({ username })
if (user && await bcrypt.compare(password, user.encryptedPassword)) {
// User is authenticated now
ctx.state.user = user
}
} catch (e) {
console.error(`Failed to process auth header "${authData}"`, e)
}
}
if (ctx.state.user) {
return ctx.render('protectedPage')
} else {
return ctx.throw(401, null, {
headers: { 'WWW-Authenticate': 'Basic realm="Protected Page"' }
})
}
})
Essentially, how you use the credentials is not in the scope of the mechanism.
(About logging out: I usually redirect people to https://[email protected]
or something like that, so that from now on the requests will use this invalid username and empty password, causing a 401 again. Note that this won't work if the page is in cache because the cached version would be delivered anyway, so this aspect may need extra consideration in that case - probably authenticated pages should not be cached anyway.)
In regards to the "insecure" aspect: It's not really insecure if you use HTTPS, because the aspect of "the credentials are sent in every request in clear text" is not relevant anymore then. It does however present other issues outlined in this answer, most importantly that nowadays clear-text password validation should intentionally be designed as a slow operation to avoid brute-force attacks, but with a system like basic auth where this has to be performed on every request, this puts a heavy burden on the server which can easily be abused for denial-of-service attacks.