About

Fathom-REST-Security integrates Fathom-REST with Fathom-Security to provide your Routes and Controllers with flexible authentication and authorization.

This is the recommended security module for Fathom-REST.

Installation

Add the Fathom-REST-Security artifact.

<dependency>
    <groupId>com.gitblit.fathom</groupId>
    <artifactId>fathom-rest-security</artifactId>
    <version>${fathom.version}</version>
</dependency>

Configuration

See Fathom-Security.

Authenication

Basic Authentication (Routes)

Basic Authentication is suitable for simple webpages and RESTful APIs.

Usage

public class Routes extends RoutesModule {

  @Inject
  SecurityManager securityManager;

  @Override
  protected void setup() {
    // Require BASIC authentication for all requests to /employees/
    ALL("/employees/.*", new BasicAuthenticationHandler(securityManager, getSettings()));

    // Prefer but do not require BASIC authentication for all requests to /contractors/
    boolean createSessions = true;
    boolean isPassive = true; // allow request to continue if basic-auth header is absent
    String realm = getSettings().getApplicationName();
    ALL("/contractors/.*", new BasicAuthenticationHandler(securityManager, createSessions, isPassive, realm));
  }
}

Form Authentication (Routes)

Form Authentication is suitable for complex webpages rendered with a browser. Form authentication requires maintaining a Session.

Usage

public class Routes extends RoutesModule {

  @Inject
  SecurityManager securityManager;

  @Override
  protected void setup() {
    // Define a handler for the login process.
    // A template named 'login' will be rendered on unauthenticated GET requests.
    // The SecurityManager will authenticate unauthenticated POST requests and a
    // new Session will be created on successful authentication.
    FormAuthenticationHandler formAuthHandler = new FormAuthenticationHandler(securityManager);
    GET("/login", formAuthHandler);
    POST("/login", formAuthHandler);

    // Define the logout URL and handler.
    // Any request to this URL will invalidate the Session.
    ALL("/logout", new LogoutHandler());

    // Specify the URLs to guard with form authentication.
    // Unauthenticated requests will be redirected to "/login".
    FormAuthenticationGuard guard = new FormAuthenticationGuard("/login");
    GET("/employees/.*", guard);
    POST("/employees/.*", guard);
  }
}

Keycloak Authentication (Routes)

Keycloak authentication is a little different from the other realms since Keycloak is based on Oauth/OpenIDConnect.

public class Routes extends RoutesModule {

  @Inject
  KeycloakGuard keycloakGuard;

  @Override
  protected void setup() {
    // Define a handler for the login process.
    // Unauthenticated requests to login will be redirected to your Keycloak server.
    // Authenticated requests will be redirected to the root page.    
    ALL("/login", keycloakGuard);
    ALL("/login", ctx -> ctx.redirect("/"));

    // Define the logout URL and handler.
    // Any request to this URL will invalidate the Session.
    ALL("/logout", new KeycloakLogoutHandler());

    // Specify the URLs to guard with Keycloak authentication.
    // Unauthenticated requests will be redirected to your Keycloak server.    
    GET("/employees/.*", keycloakGuard);
    POST("/employees/.*", keycloakGuard);
  }
}

Authorization

Form Submission Authorization or Cross-Site Request Forgery Protection

Cross-Site Request Forgery, or CSRF, is a technique to coerce a user to submit a form which executes an unauthorized action on a server using the user’s cookies or current session. From the perspective of the server, the submitted form originated from the user and was an intended action. From the perspective of the user, they did not authorize the action even though they did submit the forged form.

To protect the user from this kind of forgery we will insert a temporary token into the generated forms and require the same token to be in the submitted POST request. This token is only valid for the duration of the user’s Session and not otherwise accessible from malicious forms.

Guarding against this kind of attack is a two-step operation. First we must add a guard which will validate that submitted forms contain the correct token and then we must ensure we are including the token in the generated forms to be submitted.

In this example we will guard all the /employees/ URLs.

CSRFHandler csrfHandler = new CSRFHandler();
ALL("/employees/.*)", csrfHandler).named("CSRF handler");

And we must also update our page generation to include a hidden form field named _csrf_token. The csrfToken value is available to all template engines.

<form method="post" action="/employees/rename/5">
  <input type="hidden" name="_csrf_token" value="${csrfToken}">
  <input placeholder="Employee name" name="employeeName">
  <input type="submit" value="Rename">
</form>

CSRF and Controllers

If your design uses controllers and you need CSRF protection you may annotate the controller or the specific controller methods with @CSRF. This will automatically make the csrfToken available to the template engine for GET and POST requests and it will automatically validate POST requests.

@Path("/employees")
@CSRF
public class EmployeesController extends Controller {
}

Controller Method Account Argument Extractor

If you want easy access to the account associated with a request you may specify @Auth Account account as a method parameter and Fathom-REST will inject the account on execution. If the request has no associated account then the Guest account is supplied to avoid NullPointerExceptions.

@Path("/employees")
public class EmployeesController extends Controller {

  @GET("/{id: [0-9]+}")
  @Produces(Produces.JSON)
  @Metered
  public void getEmployee(int id, @Auth Account account) {
    // authorize the request
    account.checkPermission("employee:view:" + id);

    // The method parameter name "id" matches the url parameter name "id"
    Employee employee = employeeDao.get(id);
    if (employee == null) {
      getResponse().notFound().send("Failed to find employee {}!", id);
    } else {
      getResponse().ok().json().send(employee);
    }
  }
}

Controller Method Basic Authentication

If you want to quickly guard a controller or controller method with HTTP BASIC authentication, annotate it with @BasicAuth to require authentication.

@Path("/employees")
public class EmployeesController extends Controller {

  @GET("/{id: [0-9]+}")  
  @Produces({Produces.JSON})  
  @BasicAuth
  public void getEmployee(int id, @Auth Account account) {
    // authorize the request
    account.checkPermission("employee:view:" + id);

    // The method parameter name "id" matches the url parameter name "id"
    Employee employee = employeeDao.get(id);
    if (employee == null) {
      getResponse().notFound().send("Failed to find employee {}!", id);
    } else {
      getResponse().ok().json().send(employee);
    }
  }
}

Controller Method Basic Authentication (Passive)

If you want to quickly guard a controller or controller method with HTTP BASIC authentication, annotate it with @PassiveBasicAuth to authenticate the request if there are credentials, and otherwise let it pass-through. This may be useful if you are controlling the quantity or detail of returned content, rather than policing the request url.

@Path("/employees")
public class EmployeesController extends Controller {

  @GET("/{id: [0-9]+}")  
  @Produces({Produces.JSON, Produces.XML})  
  @PassiveBasicAuth
  public void getEmployee(int id, @Auth Account account) {
    // The method parameter name "id" matches the url parameter name "id"
    Employee employee = employeeDao.get(id);
    if (employee == null) {
      getResponse().notFound().send("Failed to find employee {}!", id);
    } else {
      // authorize the request
      if (!account.hasPermission("employee:password:" + id)) {
        employee.setPassword(null);
      }

      getResponse().ok().json().send(employee);
    }
  }
}

Controller Method Form Authentication

If you want to quickly guard a controller or controller method with HTTP FORM authentication, annotate it with @FormAuth.

@Path("/employees")
public class EmployeesController extends Controller {

  @GET("/{id: [0-9]+}")  
  @Produces(Produces.HTML)  
  @FormAuth
  public void viewEmployee(int id, @Auth Account account) {
    // authorize the request
    account.checkPermission("employee:view:" + id);

    // The method parameter name "id" matches the url parameter name "id"
    Employee employee = employeeDao.get(id);
    if (employee == null) {
      getResponse().notFound().send("Failed to find employee {}!", id);
    } else {
      getResponse().bind("employee", employee).render("view/employee");
    }
  }
}

Controller Authorization

Instead of manually injecting and checking the account for a specific permission, you may use an annotation to specify the role or permission to require. If the request has no associated account then the Guest account is supplied to avoid NullPointerExceptions.

If the account is unauthorized an AuthorizationException will be thrown which will be intercepted and an Unauthorized (403) error code will be returned to the client.

Annotation Use-Case
@RequireToken Action requires a valid Realm Account token
@RequireGuest Action requires a Guest account
@RequireAuthenticated Action requires an Authenticated account (not a Guest account)
@RequireAdministrator Action requires administrator permissions
@RequireRole("role") Action requires the specified role
@RequirePermission("permission") Action requires the specified permission

In the following examples we are enforcing employee:view and employee:delete permissions on the controller methods. Additionally, the deleteEmployee method requires the presence of a token in the request. This token may be a query parameter or a header but it must correlate to an Account in your Realm configuration.

@Path("/employees")
public class EmployeesController extends Controller {

  @GET("/{id: [0-9]+}")
  @RequirePermission("employee:view")
  @Produces({Produces.JSON, Produces.XML})
  @Metered  
  public void getEmployee(int id, @Auth Account account) {
    // The method parameter name "id" matches the url parameter name "id"
    Employee employee = employeeDao.get(id);
    if (employee == null) {
      getResponse().notFound().send("Failed to find employee {}!", id);
    } else {
      getResponse().ok().send(employee);
    }
  }

  @DELETE("/employees/{id: [0-9]+}")
  @RequireToken
  @RequirePermission("employee:delete")
  public void deleteEmployee(int id) {
    boolean success = employeeDao.remove(id);
    if (success) {
      getResponse().ok();
    } else {
      getResponse().notFound();
    }
  }
}