Packages

Basic HTTP Authentication Scheme for Wisp

Current section

Files

Jump to
wisp_basic_auth src wisp_basic_auth.gleam
Raw

src/wisp_basic_auth.gleam

import gleam/bit_array
import gleam/http/request
import gleam/list
import gleam/string
import wisp
// Tuple of client id and password
pub type ClientAuth =
#(String, String)
/// Middleware that validates an `Authorization: Basic` header
/// against a known list of client ids and passwords within a realm.
///
/// The basic authentication scheme is based on the model that the
/// user agent must authenticate itself with a user-ID and a password
/// for each realm.
///
/// The realm value should be considered an opaque string which can
/// only be compared for equality with other realms on that server.
///
/// Example header: `Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==`
///
/// Using `curl` to set the header:
///
/// ```bash
/// curl -X POST -u "client:password" ... https://example.com
/// ```
///
/// Set the middleware in your router:
///
/// ```gleam
/// let validate_basic_auth = wisp_basic_auth.validate_basic_auth(realm, known_clients)
/// use _ <- validate_basic_auth(request)
/// ```
///
pub fn validate_basic_auth(
realm: String,
known_clients: List(ClientAuth),
) -> fn(wisp.Request, fn(wisp.Request) -> wisp.Response) -> wisp.Response {
let authorized_clients = precalculate_credentials(known_clients)
fn(request, handler) {
case request.get_header(request, "Authorization") {
Error(_) -> unauthorized_response(realm)
Ok(auth_header) -> {
case list.contains(authorized_clients, auth_header) {
True -> handler(request)
False -> forbidden_response()
}
}
}
}
}
/// Parse a list of credentials in the format `client:password`
/// separated by semi-colons.
///
/// ```
/// parse_credentials("a:A;b:B")
/// // -> [#("a", "A"), #("b", "B")]
/// ```
pub fn parse_credentials(credentials: String) -> List(ClientAuth) {
let split_credential = fn(credential) {
case string.split_once(credential, on: ":") {
Ok(credential) -> [credential]
Error(_) -> []
}
}
credentials
|> string.split(on: ";")
|> list.flat_map(split_credential)
}
fn precalculate_credentials(known_clients) {
list.map(known_clients, fn(id_secret: ClientAuth) {
encode_creds(id_secret.0, id_secret.1)
})
}
fn encode_creds(id, secret) -> String {
let expected_creds = id <> ":" <> secret
let encoded_creds =
bit_array.base64_encode(bit_array.from_string(expected_creds), True)
"Basic " <> encoded_creds
}
fn unauthorized_response(realm: String) {
let realm = "Basic realm=\"" <> realm <> "\""
401
|> wisp.response()
|> wisp.string_body("Unauthorized")
|> wisp.set_header("WWW-Authenticate", realm)
}
fn forbidden_response() {
403 |> wisp.response() |> wisp.string_body("Forbidden")
}