Edit Page

Authentication and Authorization tutorial

This tutorial aims to provide a thorough understanding of securing RESTHeart applications. RESTHeart offers robust security features, with Authentication and Authorization being central to these. This guide will explore the basic authentication. We’ll also delve into authorization, focusing on Access Control Lists (ACLs) and their role in defining permissions.

By the end of this tutorial, you’ll have a solid foundation in securing RESTHeart applications, ensuring data security and controlled access.

🔧 Configuration

Sets localhost:8080 with admin:secret
Values are saved in your browser

What you need

  1. Docker: For running RESTHeart.

  2. HTTP Client: Choose from cURL (usually pre-installed), HTTPie (download at httpie.io/cli), or use the JavaScript examples directly in your browser’s console.

Starting RESTHeart and MongoDB

To begin, create a directory for RESTHeart and use Docker Compose to start both RESTHeart and MongoDB:

$ mkdir restheart && cd restheart
$ curl https://raw.githubusercontent.com/SoftInstigate/restheart/master/docker-compose.yml --output docker-compose.yml
$ docker compose up --attach restheart

The Admin User

Upon first launch, mongoRealmAuthenticator creates an admin user with default password secret and the admin role. This role is configured as the root role in mongoAclAuthorizer, granting full permissions.

Warning
Always change the admin user’s password to maintain security.

To change the admin password:

cURL

curl -i -u admin:secret -X PATCH [INSTANCE-URL]/users/admin \
  -H "Content-Type: application/json" \
  -d '{"password": "my-strong-password"}'

HTTPie

http -a admin:secret PATCH [INSTANCE-URL]/users/admin password="my-strong-password"

JavaScript

fetch("[INSTANCE-URL]/users/admin", {
  method: "PATCH",
  headers: {
    "Authorization": "Basic [BASIC-AUTH]",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    password: "my-strong-password"
  })
})
.then(response => {
  if (response.ok) {
    console.log("Password updated successfully");
  } else {
    console.error("Failed to update password:", response.status);
  }
})
.catch(error => console.error("Error:", error));

Creating Collection /secrets

Using admin, create the /secrets collection:

cURL

curl -i -u admin:secret -X PUT [INSTANCE-URL]/secrets

HTTPie

http -a admin:secret PUT [INSTANCE-URL]/secrets

JavaScript

fetch("[INSTANCE-URL]/secrets", {
  method: "PUT",
  headers: {
    "Authorization": "Basic [BASIC-AUTH]"
  }
})
.then(response => {
  if (response.ok) {
    console.log("Collection /secrets created successfully");
  } else {
    console.error("Failed to create collection:", response.status);
  }
})
.catch(error => console.error("Error:", error));

Creating Users alice and bob

Next, create two users, alice and bob, each with the user role:

cURL

curl -i -u admin:secret -X POST [INSTANCE-URL]/users \
  -H "Content-Type: application/json" \
  -d '{"_id": "alice", "password": "secret", "roles": ["user"]}'

curl -i -u admin:secret -X POST [INSTANCE-URL]/users \
  -H "Content-Type: application/json" \
  -d '{"_id": "bob", "password": "secret", "roles": ["user"]}'

HTTPie

http -a admin:secret POST [INSTANCE-URL]/users _id=alice password=secret roles:='["user"]'
http -a admin:secret POST [INSTANCE-URL]/users _id=bob password=secret roles:='["user"]'

JavaScript

// Create alice
fetch("[INSTANCE-URL]/users", {
  method: "POST",
  headers: {
    "Authorization": "Basic [BASIC-AUTH]",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    _id: "alice",
    password: "secret",
    roles: ["user"]
  })
})
.then(response => {
  if (response.ok) {
    console.log("User alice created successfully");

    // Create bob
    return fetch("[INSTANCE-URL]/users", {
      method: "POST",
      headers: {
        "Authorization": "Basic [BASIC-AUTH]",
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        _id: "bob",
        password: "secret",
        roles: ["user"]
      })
    });
  } else {
    throw new Error("Failed to create alice: " + response.status);
  }
})
.then(response => {
  if (response.ok) {
    console.log("User bob created successfully");
  } else {
    console.error("Failed to create bob:", response.status);
  }
})
.catch(error => console.error("Error:", error));

Understanding Status Codes

The /secrets endpoint helps verify credentials. For example, using incorrect credentials for alice:

cURL

curl -i -u alice:wrong -X GET [INSTANCE-URL]/secrets
# HTTP/1.1 401 Unauthorized

HTTPie

http -a alice:wrong GET [INSTANCE-URL]/secrets
# HTTP/1.1 401 Unauthorized

JavaScript

// Using incorrect credentials
fetch("[INSTANCE-URL]/secrets", {
  method: "GET",
  headers: {
    "Authorization": "Basic " + btoa("alice:wrong")
  }
})
.then(response => {
  console.log("Status:", response.status); // 401 Unauthorized
})
.catch(error => console.error("Error:", error));
Important
A 401 Unauthorized response indicates failed authentication due to incorrect credentials. RESTHeart blocks requests to secure services without proper authentication.

Attempting access with correct credentials:

cURL

curl -i -u alice:secret -X GET [INSTANCE-URL]/secrets
# HTTP/1.1 403 Forbidden

HTTPie

http -a alice:secret GET [INSTANCE-URL]/secrets
# HTTP/1.1 403 Forbidden

JavaScript

// Using correct credentials but no permissions
fetch("[INSTANCE-URL]/secrets", {
  method: "GET",
  headers: {
    "Authorization": "Basic " + btoa("alice:secret")
  }
})
.then(response => {
  console.log("Status:", response.status); // 403 Forbidden
})
.catch(error => console.error("Error:", error));
Important
A 403 Forbidden response means authentication succeeded, but the client lacks permission to access the resource.

RESTHeart’s default authorizer, mongoAclAuthorizer, enforces permissions based on user roles and ACL configurations.

Configuring Access for user Role on /secrets

We aim to allow user role to create and access their own documents in /secrets, and to modify only their documents.

1) Allow GET on /secrets:

Users can only access documents they created.

{
    "_id": "userCanAccessOwnSecret",
    "roles": [ "user" ],
    "predicate": "method(GET) and path('/secrets')",
    "priority": 100,
    "mongo": { "readFilter": "{ author: @user._id }" }
}

2) Allow POST on /secrets:

Users can create new documents, setting the author to their _id.

{
    "_id": "userCanCreateOwnSecret",
    "roles": [ "user" ],
    "predicate": "method(POST) and path('/secrets')",
    "priority": 100,
    "mongo": { "mergeRequest": { "author": "@user._id" } }
}

3) Allow PATCH on /secrets/{id}:

Users can modify only their documents.

{
    "_id": "userCanModifyOwnSecret",
    "roles": [ "user" ],
    "predicate": "method(PATCH) and path-template('/secrets/{id}')",
    "priority": 100,
    "mongo": { "writeFilter": { "author": "@user._id" } }
}

To create these permissions, use the following commands:

1. Allow GET on /secrets:

cURL

curl -i -u admin:secret -X POST [INSTANCE-URL]/acl \
  -H "Content-Type: application/json" \
  -d '{
    "_id": "userCanAccessOwnSecret",
    "roles": ["user"],
    "priority": 100,
    "predicate": "method(GET) and path('\''/secrets'\'')",
    "mongo": {
      "readFilter": {"author": "@user._id"}
    }
  }'

HTTPie

http -a admin:secret POST [INSTANCE-URL]/acl \
  _id=userCanAccessOwnSecret \
  roles:='["user"]' \
  priority:=100 \
  predicate="method(GET) and path('/secrets')" \
  mongo.readFilter:='{"author": "@user._id"}'

JavaScript

fetch("[INSTANCE-URL]/acl", {
  method: "POST",
  headers: {
    "Authorization": "Basic [BASIC-AUTH]",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    _id: "userCanAccessOwnSecret",
    roles: ["user"],
    priority: 100,
    predicate: "method(GET) and path('/secrets')",
    mongo: {
      readFilter: {"author": "@user._id"}
    }
  })
})
.then(response => response.ok ? console.log("ACL created") : console.error("Failed"))
.catch(error => console.error("Error:", error));

2. Allow POST on /secrets:

cURL

curl -i -u admin:secret -X POST [INSTANCE-URL]/acl \
  -H "Content-Type: application/json" \
  -d '{
    "_id": "userCanCreateOwnSecret",
    "roles": ["user"],
    "priority": 100,
    "predicate": "method(POST) and path('\''/secrets'\'')",
    "mongo": {
      "mergeRequest": {"author": "@user._id"}
    }
  }'

HTTPie

http -a admin:secret POST [INSTANCE-URL]/acl \
  _id=userCanCreateOwnSecret \
  roles:='["user"]' \
  priority:=100 \
  predicate="method(POST) and path('/secrets')" \
  mongo.mergeRequest:='{"author": "@user._id"}'

JavaScript

fetch("[INSTANCE-URL]/acl", {
  method: "POST",
  headers: {
    "Authorization": "Basic [BASIC-AUTH]",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    _id: "userCanCreateOwnSecret",
    roles: ["user"],
    priority: 100,
    predicate: "method(POST) and path('/secrets')",
    mongo: {
      mergeRequest: {"author": "@user._id"}
    }
  })
})
.then(response => response.ok ? console.log("ACL created") : console.error("Failed"))
.catch(error => console.error("Error:", error));

3. Allow PATCH on /secrets/{id}:

cURL

curl -i -u admin:secret -X POST [INSTANCE-URL]/acl \
  -H "Content-Type: application/json" \
  -d '{
    "_id": "userCanModifyOwnSecret",
    "roles": ["user"],
    "priority": 100,
    "predicate": "method(PATCH) and path-template('\''/secrets/{id}'\'')",
    "mongo": {
      "writeFilter": {"author": "@user._id"}
    }
  }'

HTTPie

http -a admin:secret POST [INSTANCE-URL]/acl \
  _id=userCanModifyOwnSecret \
  roles:='["user"]' \
  priority:=100 \
  predicate="method(PATCH) and path-template('/secrets/{id}')" \
  mongo.writeFilter:='{"author": "@user._id"}'

JavaScript

fetch("[INSTANCE-URL]/acl", {
  method: "POST",
  headers: {
    "Authorization": "Basic [BASIC-AUTH]",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    _id: "userCanModifyOwnSecret",
    roles: ["user"],
    priority: 100,
    predicate: "method(PATCH) and path-template('/secrets/{id}')",
    mongo: {
      writeFilter: {"author": "@user._id"}
    }
  })
})
.then(response => response.ok ? console.log("ACL created") : console.error("Failed"))
.catch(error => console.error("Error:", error));

Creating Secret Documents

Let’s have alice and bob create their secrets:

cURL

curl -i -u bob:secret -X POST [INSTANCE-URL]/secrets \
  -H "Content-Type: application/json" \
  -d '{"message": "Bob loves Alice"}'

curl -i -u alice:secret -X POST [INSTANCE-URL]/secrets \
  -H "Content-Type: application/json" \
  -d '{"message": "Alice loves Bob"}'

HTTPie

http -a bob:secret POST [INSTANCE-URL]/secrets message="Bob loves Alice"
http -a alice:secret POST [INSTANCE-URL]/secrets message="Alice loves Bob"

JavaScript

// Bob creates his secret
fetch("[INSTANCE-URL]/secrets", {
  method: "POST",
  headers: {
    "Authorization": "Basic " + btoa("bob:secret"),
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    message: "Bob loves Alice"
  })
})
.then(response => {
  if (response.ok) {
    console.log("Bob's secret created");

    // Alice creates her secret
    return fetch("[INSTANCE-URL]/secrets", {
      method: "POST",
      headers: {
        "Authorization": "Basic " + btoa("alice:secret"),
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        message: "Alice loves Bob"
      })
    });
  } else {
    throw new Error("Failed to create Bob's secret");
  }
})
.then(response => {
  if (response.ok) {
    console.log("Alice's secret created");
  } else {
    console.error("Failed to create Alice's secret");
  }
})
.catch(error => console.error("Error:", error));

Reading Secret Documents

Viewing with admin (sees all secrets):

cURL

curl -i -u admin:secret -X GET [INSTANCE-URL]/secrets
# Output includes both Alice's and Bob's messages

HTTPie

http -a admin:secret -b GET [INSTANCE-URL]/secrets
# Output includes both Alice's and Bob's messages

JavaScript

fetch("[INSTANCE-URL]/secrets", {
  method: "GET",
  headers: {
    "Authorization": "Basic " + btoa("admin:secret")
  }
})
.then(response => response.json())
.then(data => {
  console.log("Admin sees all secrets:", data);
  // Output includes both Alice's and Bob's messages
})
.catch(error => console.error("Error:", error));
Note
The author property is correctly set for each document.

Accessing /secrets as alice (sees only her own secret):

cURL

curl -i -u alice:secret -X GET [INSTANCE-URL]/secrets
# Output includes only Alice's message

HTTPie

http -a alice:secret -b GET [INSTANCE-URL]/secrets
# Output includes only Alice's message

JavaScript

fetch("[INSTANCE-URL]/secrets", {
  method: "GET",
  headers: {
    "Authorization": "Basic " + btoa("alice:secret")
  }
})
.then(response => response.json())
.then(data => {
  console.log("Alice sees her secrets:", data);
  // Output includes only Alice's message
})
.catch(error => console.error("Error:", error));

Similarly, accessing as bob (sees only his own secret):

cURL

curl -i -u bob:secret -X GET [INSTANCE-URL]/secrets
# Output includes only Bob's message

HTTPie

http -a bob:secret -b GET [INSTANCE-URL]/secrets
# Output includes only Bob's message

JavaScript

fetch("[INSTANCE-URL]/secrets", {
  method: "GET",
  headers: {
    "Authorization": "Basic " + btoa("bob:secret")
  }
})
.then(response => response.json())
.then(data => {
  console.log("Bob sees his secrets:", data);
  // Output includes only Bob's message
})
.catch(error => console.error("Error:", error));

Let’s take a moment to acknowledge the story of Alice and Bob. These two characters are entwined in an 'impossible love' story that symbolizes the challenges of secure communication in the digital age. And RESTHeart is no exception keeping their love hidden in the /secrets collection.