Goal

Last time I pondered on how to implement remote storage in Obnam. I landed on having an Obnam server that guards access to the actual remote storage, which will be on a server with the S3 API. Alternatively, the Obnam serve can store the backups itself. I didn't say that very explicitly in the note for last time, but I think it's important to not require using an S3 instance, for operational simplicity.

The goal for today is to start work on the Obnam server API. The first stage is to get a grip on authentication. I'll do some research and planning, and if time permits, prototype some code. I'll do any prototyping in a command line environment only. Today is not when I start work on the actual HTTP API.

Plan

  • Draft an approach for authentication in the Obnam server API, with justification.

  • Prototype an implementation of the authentication, as a new repository. The prototype should be able to produce an authentication token and verify it, with tests to give me confidence its working right.

Notes

HTTP API authentication high level view

  • Based on my experience so far, I take it as a given that the very high level architecture for Obnam server HTTP API authentication will be using bearer tokens. This means the API client will add an Authorization: Bearer TOKEN header to every HTTP request and the server will grant access only if TOKEN is valid.

  • From the client point of view, TOKEN is an opaque string that it is given in some way. The client does not try to parse the token in any way.

  • It is important to remember that the Obnam API will only be used by batch mode clients, not interactively by users. This affects the design.

  • At this stage I do not worry about how the client gets the token. I'll assume I can design a secure way for that later. Possibly the server admin gives it to them in a private meeting in a parking garage in a sealed envelope. Possibly there will be a hosting provider for Obnam backups with a web application for signing up and paying for the service. The web application can provide the token to the user who then adds it to the Obnam client configuration. Or something.

Summary: client has an API token and includes it in each API request. Server verifies the token is valid and if it is, grants access.

Token type possibilities

  • The web tech sphere has many kinds of tokens. The undisputed most popular one is JSON web token (JWT). JWT has some problems, such as being too easy to use in an insecure way. However, being full of hubris, I'll use JWT unless there's a clearly better option.

  • I will not design my own. I am full of hubris, but I'm not entirely foolhardy.

  • JWT problems are discussed in, for example:

  • If I use JWT I will lock down any risky aspects I can:

    • only use signatures, not encryption
    • pick a strong algorithm with a large key size
    • set the algorithm without trusting the alg claim
    • verify against expiration
    • verify against revocation
    • verify iss, aud claims
  • PASETO does not have a Wikipedia page, which immediately makes me wonder why. The pasetors crate seems reasonably popular and easy to use.

  • Macaroons) are interesting for applications where a token holder needs to be able to grant access to a subset of its own access to another party. That's not relevant for Obnam, I think.

  • Client side TLS certificates are interesting, mostly because they remove the need to communicate a token in secret. I may want to switch to certificates later, but for now, tokens seem simpler.

  • There are also session cookies, but those seem inappropriate for Obnam. If nothing else, you can't issue a session cookie to be used for authorization unless the client has a way to authenticate, which is what I'm working on now.

  • Of these options, JWT seem good enough, but I'll try PASETO first. The good thing is that the only party that needs to care about how the token are implemented, so it'll be reasonably easy to migrate to a better token option. Migrating to client side certificates is going to be trickier, but not too tricky.

Summary: I'll use PASETO tokens to start with. If I can't make them work to my satisfaction, I'll fall back to JWT.

Implementation

  • I will implement a new Rust crate (to avoid any cruft from the Obnam crate), with types for tokens, token generation and token validation.

  • I did that, but didn't take notes, because I got into a deep hack state and didn't think about notes.

  • I've used pasetors, which seems nice so far.

  • I first created a mess of a main that did everything, then broke that up into helper functions.

  • I added a small Obnam specific layer on top. I don't want to mess with PASETO directly, I want to have a convenient layer that is specific to the things the Obnam server needs to do with the tokens.

  • After some flailing I ended up with the following (just the public API):

#[derive(Copy, Clone, PartialEq, Eq)]
enum ObnamClaim { Read, Write, Delete }

struct TokenFactory { }

impl TokenFactory {
    fn new<S: AsRef<str>>(server_id: S) -> Result<Self, TokenFactoryError> {}
    fn create_token(&self, extra: &[ObnamClaim]) -> Result<String, TokenFactoryError> {}
    fn verify(&self, untrusted: &str) -> Result<ObnamTrustedToken, TokenFactoryError> {}
}

#[derive(Debug, Default)]
struct ObnamTrustedToken {
    read_allowed: bool,
    write_allowed: bool,
    delete_allowed: bool,
}

impl ObnamTrustedToken {
    fn read_allowed(&self) -> bool {
        self.read_allowed
    }

    fn write_allowed(&self) -> bool {
        self.write_allowed
    }

    fn delete_allowed(&self) -> bool {
        self.delete_allowed
    }
}

impl From<&Claims> for ObnamTrustedToken {
    fn from(value: &Claims) -> Self {
        fn is_true(value: &Claims, claim: ObnamClaim) -> bool {
            value.get_claim(claim.as_str()) == Some(&Value::Bool(true))
        }
        Self {
            read_allowed: is_true(value, ObnamClaim::Read),
            write_allowed: is_true(value, ObnamClaim::Write),
            delete_allowed: is_true(value, ObnamClaim::Delete),
        }
    }
}
  • Basically, the factory creates tokens. All tokens have the standard PASETO fields set to known values: issuer and audience is the Obnam server instance, etc. Additionally, Obnam specific claims can be set. The factory also validates tokens, and if valid, creates a trusted Obnam token that has exactly the the authorization information needed by the Obnam server.

  • I have this in a local Git repository. When I start actually writing the Obnam server, I'll add the code there as a module.

Summary

  • Authentication and authorization is an interesting area. I chose to use PASETO tokens, at least for now.

Comments?

If you have feedback on this development session, please use the following Fediverse thread: https://toot.liw.fi/@liw/115955186773127161.