Authorization and Security¶
Setup¶
Maven¶
<dependency>
<groupId>de.whitefrog</groupId>
<artifactId>frogr-auth</artifactId>
<version>0.1.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:
open class BaseUser : Entity(), Principal {
@Unique @Fetch @Required
open var login: String? = null
@JsonView(Views.Hidden::class)
open var password: String? = null
@JsonView(Views.Hidden::class)
open var role: String? = null
@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 }
}
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> {
public MyApplication() {
// register the rest classes
register("de.whitefrog.frogr.auth.example");
// register repositories and models
serviceInjector().service().register("de.whitefrog.frogr.auth.example");
}
@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.