Have you ever had your browser pop up a native looking window asking for a username and password? No pretty web page, no validation, just a dull looking dialogue box asking for credentials?
This no frills approach to authentication is often used by modems, routers and other consumer electronic devices.
In this article I'll explain how it works and how to implement it with the BedSheet application server.
In the latter section ...with a Middleware Service we show how easy it is to convert an existing class to an IoC service and make it configurable.
Security Brief
Basic HTTP Authorisation does not encrypt the username and password. They are sent, essentially, in clear text to the server. Anybody sniffing your HTTP packets can read the credentials.
That said, if someone has gone to the trouble of tapping your Internet connection to sniff HTTP data, you probably have bigger problems than insecure authorisation!
Credentials may also be embedded in the URL itself, allowing the whole link to be bookmarked for easy access. It has the following handy notation:
http://username:password@www.example.com/admin/secret.zip
This can either be a good or a bad thing, depending on how lazy or paranoid you are!
Browsers happily convert the given userdata into relevant HTTP header fields.
As Basic HTTP Authorisation is so insecure, you may be asking...
Why Use It?
Well, it can be good for hiding data. It's one step further than having a secret URL. For as well as knowing the URL, a person also needs to know a valid username and password.
It won't deter a decent hacker so it's useless for protecting sensitive data. But it is a rough and ready solution for hobbyists to hide their shopping list from Joe Public!
How It Works
Basic HTTP Authorisation is detailed in RFC2617 section 2 Basic Authentication Scheme, but in layman's terms...
- A browser sends a HTTP request to a URL, for example:
http://www.example.com/admin/secret.zip
- The server, realising the URL is protected, challenges the request by returning a
WWW-Authenticate
HTTP header. - The browser, on receipt of a
WWW-Authenticate
HTTP header, pops up a dialogue box asking the user for a username and password. - Once entered, the browser re-issues the same request, but with an additional
Authorization
HTTP header containing the given user credentials. - The server validates the
Authorization
HTTP header and processes the request.
When a URL contains userdata then the browser sends this with the first request, skipping straight to Step 4.
Protect a File
Let's protect the single file secret.zip
at the URL /admin/secret.zip
. First, create a Route in AppModule
that maps to our response handler:
using afIoc using afBedSheet const class AppModule { @Contribute { serviceType=Routes# } Void contributeRoutes(OrderedConfig config) { config.add(Route(`/admin/secret.zip`, BasicHttpAuth#protectFile)) } }
The response handler, BasicHttpAuth
, looks like:
using afIoc using afBedSheet const class BasicHttpAuth { @Inject private const HttpRequest request @Inject private const HttpResponse response new make(|This|in) { in(this) } Obj protectFile() { authorise()// request authorised - return the secret!return `file:/C:/mydata/secret.zip`.toFile } Void authorise() { auth := request.headers["Authorization"]// if no credentials are given, challenge the requestif (auth == null) { response.headers["WWW-Authenticate"] = "Basic realm=\"What's the magic word?\"" throw HttpStatus.makeErr(401, "Credentials required") }// check the Authorization method is 'Basic'if (!auth.lower.startsWith("basic ")) throw HttpStatus.makeErr(400, "Only 'Basic' authorisation supported")// decode the credentialscredentials = Buf.fromBase64(auth[6..-1]).readAllStr.trim// check the username and passwordif (credentials != "admin:1234") { response.headers["WWW-Authenticate"] = "Basic realm=\"What's the magic word?\"" throw HttpStatus.makeErr(401, "Credentials invalid") } } }
The authorise()
method is used in all the examples. It does the Basic HTTP Authorisation, setting response headers and throwing a HttpStatus
to communicate to the browser.
authorise()
does the following:
First it checks if an Authorization
header was sent and if not we challenge the request by returning a HTTP status code of 401 Unauthorized.
The Authorization
header should look like this:
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
If it doesn't exist we set the following HTTP response header:
WWW-Authenticate: Basic realm="What's the magic word?"
The realm
attribute is the message displayed in the browser's dialogue box. It usually describes the domain that's being protect, such as "Alien-Factory Admin". Browsers often cache credentials against the domain, so you don't have to re-type them for each page.
Next we check the type of Authorization
. If it is not Basic
then we reject it as a Bad Request.
The gobbledygook that follows is the username and password, combined with a :
and then Base64 encoded. So we decode the credentials and do a basic String comparison. Here we check for a username of admin
and a password of 1234
.
If no errors were thrown then protectFile()
simply returns the secret file!
Protect a Directory
A more common usage is to protect all URLs under a directory, such as /admin
. First we'll configure BedSheet to serve up the secret documents:
using afBedSheet const class AppModule { @Contribute { serviceType=FileHandler# } Void contributeFileHandler(Configuration config) { config[`/admin/`] = `file:/C:/mydata/secret-documents/` } }
We can now protect the secret files...
...with Middleware
Http requests are passed through a stack of Middleware classes until one of them handles it, that is, returns true
or throws an Err. The AssetsMiddleware
is responsible for returning files (as configured by FileHandler
), so we want to make sure our authorisation Middleware is processed before it.
In AppModule
we contribute to the MiddlewarePipeline
service:
@Contribute { serviceType=MiddlewarePipeline# } Void contributeMiddleware(Configuration config) {// ensure our middleware is called *before* any file handler routesconfig.set("BasicAuth", config.autobuild(BasicHttpAuth#)).before("afBedSheet.assets") }
The Middleware
class is surprisingly simple; and note how BasicHttpAuth
extends Middleware
:
using afIoc using afBedSheet const class BasicHttpAuth : Middleware { @Inject private const HttpRequest request @Inject private const HttpResponse response new make(|This|in) { in(this) } override Void service(MiddlewarePipeline pipeline) { if (request.url.toStr.startsWith("/admin/")) authorise() pipeline.service } Void authorise() {// ...same as before} }
If the request URI starts with /admin/
then we authorise()
. If it passed (no Errs) then we let processing continue down the pipeline.
...with a Middleware Service
With a bit more effort we can take the example further and convert our BasicHttpAuth
middleware class into an IoC service. We can even protect multiple directory URLs by making the service configurable, and contributing to it from our own AppModule
!
If you were to build an authentication library then this is the preferred design pattern.
We'll start by converting BasicHttpAuth
into an IoC service. Do this by simply declaring it in the AppModule
bind method:
Void bind(RegistryBuilder bob) { bob.addService(BasicHttpAuth#) }
When we configure MiddlewarePipeline
be sure to contribute the actual BasicHttpAuth
service instance and not some autobuilt class. Do this by declaring it as a method parameter, IoC will then inject the real service when it calls the method:
@Contribute { serviceType=MiddlewarePipeline# } Void contributeMiddleware(Configuration config, BasicHttpAuth basicHttpAuth) {// ensure our middleware is called *before* any file handler routesconfig.set("BasicAuth", basicHttpAuth).before("afBedSheet.assets") }
The changes to BasicHttpAuth
are slight. We pass a list of protected URLs into the ctor, saving them to a field. When service()
is called we do a simple loop to check if the request URL is protected, and call authorise()
as usual:
using afIoc using afBedSheet const class BasicHttpAuth : Middleware { @Inject private const HttpRequest request @Inject private const HttpResponse response const Uri[] protectedUrls new make(Uri[] protectedUrls, |This|in) { in(this) this.protectedUrls = protectedUrls } override Void service(MiddlewarePipeline pipeline) { reqUrl := request.url.toStr// check if the req url is inside *any* of the given protected urlif (protectedUrls.any { reqUrl.startsWith(it.toStr) }) authorise() pipeline.service } Void authorise() {// ...same as before} }
Because our service ctor now takes a List
as the first parameter, we can contribute to it from our AppModule
:
@Contribute { serviceType=BasicHttpAuth# } Void contributeBasicHttpAuth(Configuration config) { config.add(`/admin/`) }
Ta daa!!!
You now have a configurable IoC service that provides Basic HTTP Authentication!
As an exercise, try updating the example so you can contribute both a protected URL and an associated realm.
Complete Examples
Following are complete BedSheet examples for the tutorial. Each one is a runnable script, so if you save it as Example.fan
you can run it with:
C:\> fan Example.fan
Then visit http://localhost:8080/admin/secret.zip in your browser to see the challenge.
CAUTION! Once entered correctly, most browsers will automatically cache your username and password! Clear your browsing history to see the challenge again or try the examples in privacy mode.
Ensure you have the BedSheet pod installed by running:
C:\> fanr install -r http://eggbox.fantomfactory.org/fanr "afBedSheet 1.5"
Note that all code examples have been tested with:
- Fantom 1.0.68
- IoC 3.0.2
- BedSheet 1.5.2
1. How to protect a single file
using afIoc using afBedSheet** How to protect a single file.const class AppModule { @Contribute { serviceType=Routes# } Void contributeRoutes(Configuration config) { config.add(Route(`/admin/secret.zip`, BasicHttpAuth#protectFile)) } } const class BasicHttpAuth { @Inject private const HttpRequest request @Inject private const HttpResponse response new make(|This|in) { in(this) } Obj protectFile() { authorise()// request authorised - return the secret!// You may return a File here, in place of the Text objectreturn Text.fromPlain("Here are the secret blueprints... Damn, lost them!") } Void authorise() { auth := request.headers["Authorization"]// if no credentials are given, challenge the requestif (auth == null) { response.headers["WWW-Authenticate"] = "Basic realm=\"What's the magic word?\"" throw HttpStatus.makeErr(401, "Credentials required") }// check the Authorization method is 'Basic'if (!auth.lower.startsWith("basic ")) throw HttpStatus.makeErr(400, "Only 'Basic' authorisation supported")// decode the credentialscredentials := Buf.fromBase64(auth[6..-1]).readAllStr.trim// check the username and passwordif (credentials != "admin:1234") { response.headers["WWW-Authenticate"] = "Basic realm=\"What's the magic word?\"" throw HttpStatus.makeErr(401, "Credentials invalid") } } } class Example { Int main() { BedSheetBuilder(AppModule#.qname).startWisp(8080) } }
2. How to protect a directory with Middleware
using afIoc using afBedSheet** How to protect a directory with Middleware.const class AppModule { @Contribute { serviceType=FileHandler# } Void contributeFileHandler(Configuration config) {// TODO: make sure this directory exists - use `secret-documents/` for a relative dirconfig[`/admin/`] = `file:/C:/mydata/secret-documents/` } @Contribute { serviceType=MiddlewarePipeline# } Void contributeMiddleware(Configuration config) {// ensure our middleware is called *before* any file handler routesconfig.set("BasicAuth", config.autobuild(BasicHttpAuth#)).before("afBedSheet.assets") } } const class BasicHttpAuth : Middleware { @Inject private const HttpRequest request @Inject private const HttpResponse response new make(|This|in) { in(this) } override Void service(MiddlewarePipeline pipeline) { if (request.url.toStr.startsWith("/admin/")) authorise() pipeline.service } Void authorise() { auth := request.headers["Authorization"]// if no credentials are given, challenge the requestif (auth == null) { response.headers["WWW-Authenticate"] = "Basic realm=\"What's the magic word?\"" throw HttpStatus.makeErr(401, "Credentials required") }// check the Authorization method is 'Basic'if (!auth.lower.startsWith("basic ")) throw HttpStatus.makeErr(400, "Only 'Basic' authorisation supported")// decode the credentialscredentials := Buf.fromBase64(auth[6..-1]).readAllStr.trim// check the username and passwordif (credentials != "admin:1234") { response.headers["WWW-Authenticate"] = "Basic realm=\"What's the magic word?\"" throw HttpStatus.makeErr(401, "Credentials invalid") } } } class Example { Int main() { BedSheetBuilder(AppModule#.qname).startWisp(8080) } }
3. How to protect a directory with a Middleware service
using afIoc using afBedSheet** How to protect a directory with a Middleware service.const class AppModule { Void defineServices(RegistryBuilder bob) { bob.addService(BasicHttpAuth#) } @Contribute { serviceType=FileHandler# } Void contributeFileHandler(Configuration config) {// TODO: make sure this directory exists - use `secret-documents/` for a relative dirconfig[`/admin/`] = `file:/C:/mydata/secret-documents/` } @Contribute { serviceType=MiddlewarePipeline# } Void contributeMiddleware(Configuration config, BasicHttpAuth basicHttpAuth) {// ensure our middleware is called *before* any file handler routesconfig.set("BasicAuth", basicHttpAuth).before("afBedSheet.assets") } @Contribute { serviceType=BasicHttpAuth# } Void contributeBasicHttpAuth(Configuration config) { config.add(`/admin/`) } } const class BasicHttpAuth : Middleware { @Inject private const HttpRequest request @Inject private const HttpResponse response const Uri[] protectedUrls new make(Uri[] protectedUrls, |This|in) { in(this) this.protectedUrls = protectedUrls } override Void service(MiddlewarePipeline pipeline) { reqUrl := request.url.toStr// check if the req url is inside *any* of the given protected dirsif (protectedUrls.any { reqUrl.startsWith(it.toStr) }) authorise() pipeline.service } Void authorise() { auth := request.headers["Authorization"]// if no credentials are given, challenge the requestif (auth == null) { response.headers["WWW-Authenticate"] = "Basic realm=\"What's the magic word?\"" throw HttpStatus.makeErr(401, "Credentials required") }// check the Authorization method is 'Basic'if (!auth.lower.startsWith("basic ")) throw HttpStatus.makeErr(400, "Only 'Basic' authorisation supported")// decode the credentialscredentials := Buf.fromBase64(auth[6..-1]).readAllStr.trim// check the username and passwordif (credentials != "admin:1234") { response.headers["WWW-Authenticate"] = "Basic realm=\"What's the magic word?\"" throw HttpStatus.makeErr(401, "Credentials invalid") } } } class Example { Int main() { BedSheetBuilder(AppModule#.qname).startWisp(8080) } }
Have fun!
Edits
- 31 July 2016 - Updated to use BedSheet 1.5 and IoC 3.0.
- 3 April 2014 - Original article.