Authorization and Security

Setup

Maven

<dependency>
    <groupId>de.whitefrog</groupId>
    <artifactId>frogr-auth</artifactId>
    <version>0.2.2</version>
</dependency>

User Model

The User model has to extend BaseUser and defines our user, which can be passed in Service methods using the @Auth annotation.

class User : BaseUser() {
  @JsonView(Views.Secure::class)
  @RelatedTo(type = RelationshipTypes.FriendWith)
  var friends: ArrayList<User> = ArrayList()
}

As you can see the annotation @JsonView(Views.Secure::class) is used on the friends field. These views can be used on Service methods too, and describe what can be seen by the user. The default is Views.Public::class, so any field annotated with that @JsonView is visible to everyone. Fields without @JsonView annotation are always visible.

BaseUser provides some commonly used fields describing a user in an authentication environment:

/**
 * Base user class required for Authentication.
 */
open class BaseUser : Entity(), Principal {
  /**
   * Unique and required field.
   */
  @Unique @Fetch @Required 
  open var login: String? = null

  /**
   * The password as string.
   */
  @JsonView(Views.Hidden::class)
  open var password: String? = null

  /**
   * [Role] in which the user is
   */
  @JsonView(Views.Hidden::class)
  open var role: String? = null

  /**
   * Used for oAuth user authentication.
   */
  @Indexed @JsonView(Views.Secure::class)
  var accessToken: String? = null

  override fun getName(): String? {
    return login
  }

  override fun implies(subject: Subject?): Boolean {
    return subject!!.principals.any { p -> (p as BaseUser).id == id }
  }

  override fun equals(other: Any?): Boolean {
    return super<Entity>.equals(other)
  }

  companion object {
    const val Login = "login"
    const val Password = "password"
    const val AccessToken = "accessToken"
    const val Roles = "role"
  }
}

You can write your own User class, but then you’ll have to create your own oAuth implementation.

Warning: Be cautious with Views.Secure on Service methods, as it could reveal sensitive data. So it’s best to have custom methods like findFriendsOfFriends for example to get all friends of the users friends instead of the common search function.

User Repository

Next, we’ll have to define a repository for our users, extending BaseUserRepository:

public class UserRepository extends BaseUserRepository<User> {
  public String init() {
    String token;
    PersonRepository persons = service().repository(Person.class);
    if(search().count() == 0) {
      Person rick = new Person("Rick Sanchez");
      Person beth = new Person("Beth Smith");
      Person jerry = new Person("Jerry Smith");
      Person morty = new Person("Morty Smith");
      Person summer = new Person("Summer Smith");
      // we need to save the people first, before we can create relationships
      persons.save(rick, beth, jerry, morty, summer);

      rick.setChildren(Arrays.asList(beth));
      beth.setChildren(Arrays.asList(morty, summer));
      beth.setMarriedWith(jerry);
      jerry.setChildren(Arrays.asList(morty, summer));
      jerry.setMarriedWith(beth);
      persons.save(rick, beth, jerry, morty, summer);

      User user = new User();
      user.setLogin("justin_roiland");
      user.setPassword("rickandmorty");
      user.setRole(Role.Admin);
      save(user);
      
      // login to create and print an access token - for test purposes only
      user = login("justin_roiland", "rickandmorty");
      token = "access_token=" + user.getAccessToken();
      System.out.println("User created. Authorization: Bearer " + user.getAccessToken());
    } else {
      User user = login("justin_roiland", "rickandmorty");
      token = "access_token=" + user.getAccessToken();
      System.out.println("Authorization: Bearer " + user.getAccessToken());
    }
    return token;
  }
}

The extended class BaseUserRepository provides some basic functionality and security.

register(user)
Registration of a new user, passwords will be encrypted by default.
login(login, password)
Login method, encrypts the password automatically for you.
validateModel(context)
Overridden to ensure a password and a role is set on new users.

Application

In our applications run method, we need to set up some authentication configurations:

public class MyApplication extends Application<Configuration> {
  private MyApplication() {
    // register the rest classes
    register("de.whitefrog.frogr.example.oauth");
    // register repositories and models
    serviceInjector().service().register("de.whitefrog.frogr.example.oauth");
    
  }

  @Override
  @SuppressWarnings("unchecked")
  public void run(Configuration configuration, Environment environment) throws Exception {
    super.run(configuration, environment);

    Authorizer authorizer = new Authorizer(service().repository(User.class));
    AuthFilter oauthFilter = new OAuthCredentialAuthFilter.Builder<User>()
      .setAuthenticator(new Authenticator(service().repository(User.class)))
      .setAuthorizer(authorizer)
      .setPrefix("Bearer")
      .buildAuthFilter();

    environment.jersey().register(RolesAllowedDynamicFeature.class);
    environment.jersey().register(new AuthValueFactoryProvider.Binder<>(User.class));
    environment.jersey().register(new AuthDynamicFeature(oauthFilter));
  }

  @Override
  public String getName() {
    return "frogr-auth-example-rest";
  }

  public static void main(String[] args) throws Exception {
    new MyApplication().run("server", "config/example.yml");
  }
}

Inside the run method, we set up RolesAllowedDynamicFeature, which activates the previously used @RolesAllowed annotation. We also set up AuthValueFactoryProvider.Binder which activates the later described @Auth injection annotation and AuthDynamicFeature which activates the actual oAuth authentication.

Services

Here’s a simple service, that can only be called when the user is authenticated. The user will be passed as argument to the method:

@Path("person")
public class Persons extends AuthCRUDService<PersonRepository, Person, User> {
  @GET
  @Path("find-morty")
  @RolesAllowed(Role.User)
  public Person findMorty(@Auth User user) {
    try(Transaction ignored = service().beginTx()) {
      return repository().findMorty();
    }
  }
}

See how the @RolesAllowed(Role.User) annotation is used, to only allow this method to registered users. You can always extend the Role class and use your own roles. Predefined roles are Admin, User and Public.

The first (and only) parameter on findMorty is annotated with @Auth and has the type of our User class created before. This will inject the currently authenticated user and also tells the application that this method is only allowed for authenticated users.

The extended class AuthCRUDService provides some convenient methods for authentication and sets some default @RolesAllowed annotations on the basic CRUD methods. All predefined methods are only allowed for registered and authenticated users.

authorize
Override to implement your access rules for specific models. Is used by default in create and update methods.
authorizeDelete
Override to implement your rules to who can delete specific models. Is used by default in delete method.