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?

A browser challenge

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

HTTP Lockdown

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...

  1. A browser sends a HTTP request to a URL, for example: http://www.example.com/admin/secret.zip
  2. The server, realising the URL is protected, challenges the request by returning a WWW-Authenticate HTTP header.
  3. The browser, on receipt of a WWW-Authenticate HTTP header, pops up a dialogue box asking the user for a username and password.
  4. Once entered, the browser re-issues the same request, but with an additional Authorization HTTP header containing the given user credentials.
  5. 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:

select all
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:

select all
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 request
        if (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 credentials
        credentials = Buf.fromBase64(auth[6..-1]).readAllStr.trim

        // check the username and password
        if (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 routes
    config.set("BasicAuth", config.autobuild(BasicHttpAuth#)).before("afBedSheet.assets")
}

The Middleware class is surprisingly simple; and note how BasicHttpAuth extends Middleware:

select all
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 routes
    config.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:

select all
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 url
        if (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:

1. How to protect a single file

select all
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 object
        return Text.fromPlain("Here are the secret blueprints... Damn, lost them!")
    }

    Void authorise() {
        auth := request.headers["Authorization"]

        // if no credentials are given, challenge the request
        if (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 credentials
        credentials := Buf.fromBase64(auth[6..-1]).readAllStr.trim

        // check the username and password
        if (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

select all
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 dir
        config[`/admin/`] = `file:/C:/mydata/secret-documents/`
    }

    @Contribute { serviceType=MiddlewarePipeline# }
    Void contributeMiddleware(Configuration config) {
        // ensure our middleware is called *before* any file handler routes
        config.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 request
        if (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 credentials
        credentials := Buf.fromBase64(auth[6..-1]).readAllStr.trim

        // check the username and password
        if (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

select all
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 dir
        config[`/admin/`] = `file:/C:/mydata/secret-documents/`
    }

    @Contribute { serviceType=MiddlewarePipeline# }
    Void contributeMiddleware(Configuration config, BasicHttpAuth basicHttpAuth) {
        // ensure our middleware is called *before* any file handler routes
        config.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 dirs
        if (protectedUrls.any { reqUrl.startsWith(it.toStr) })
            authorise()

        pipeline.service
    }

    Void authorise() {
        auth := request.headers["Authorization"]

        // if no credentials are given, challenge the request
        if (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 credentials
        credentials := Buf.fromBase64(auth[6..-1]).readAllStr.trim

        // check the username and password
        if (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


Discuss