Why couldn't the polar bear access the fish database?
It kept getting the message: "You do not have the bear-er token!"

Now that we have that out of the way, let’s get to the real stuff. In this post, we will see how to use the OSO Authorization Framework with SpringBoot and SpringSecurity. We will be using Kotlin for this example, but the same can be done with Java as well.

Note

As of the end of 2023 OsoHQ has decided to deprecate the open-source version of the Oso Authorization Framework. Parts of this post were written before this announcement and therefore I’ve decided to publish it as it might help someone who’s still looking to use OSS version of Oso.

In the future I might either write my own simple authorization framework or leverage the Oso in its commercial form. Currently my project is a Modular Monolith and doesn’t really require anything complex.

What is Oso?

Oso (in its open-source form) is a library that allows you to define authorization rules in a simple, declarative language called Polar. It is a powerful language that allows you to define complex authorization rules in a very simple way. It is also very easy to integrate with your application. We can use this language to model authorization rules in RBAC, ReBAC, ABAC and other models.

Here’s a short example of a Polar rule:

actor User {}

resource Organization {
  roles = ["owner"];
}


resource Repository {
  permissions = ["read", "push"];
  roles = ["contributor", "maintainer"];
  relations = { parent: Organization };

  "read" if "contributor";
  "push" if "maintainer";

  "contributor" if "maintainer";
}

...

Here we have a rule that says that a User can read a Repository if they have the contributor role. Similarly, they can push to a Repository if they have the maintainer role.

The public interface is also very easy to use. Here’s an example of how to use the above rule:

oso.authorize(user, "read", repo);

Here, we are asking Oso to check if the user has the read permission on the repo. If the user has the permission, true is returned, otherwise we get a false back.

Oso has a great getting started guide that you can follow to get a better understanding of how it works. You can find it here. We’ll be skipping the basics and focus on integrating it with SpringSecurity.

SpringSecurity with OSO

There’s no out of the box way to integrate Oso with SpringSecurity, but we can leverage the extensibility of Spring’s MethodSecurity to integrate both.

Prerequisites

Let’s start by setting up a simple landscape application model. In our case we’ll have User, Organization and Repository. A User can be a member of multiple Organizations and a Repository can belong to only one Organization. A User can have different roles in different Organizations.

Registering Java/Kotlin classes with Oso

Oso requires us to register the Java classes that we want to use in the Polar rules. We can do this by using the registerClass method on the Oso object. Let’s create a simple configuration class to do this and also return an instance of Oso when needed:

@Configuration
class SecurityConfiguration {
    @Bean
    fun oso(): Oso {
        val oso = Oso()
        oso.registerClass(UserPrincipal::class.java, "User")

        return oso
    }
}

We can register all the classes manually by calling the registerClass method for each class. But this can get tedious if we have a lot of classes. If you want to be fancier, we can define a custom and use it to discover and register all the classes automatically.

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class Securable(val name: String)

And to register them:

private fun registerSecurables(oso: Oso) {
    // scan all the classes with @Securable annotation and register them
    val scanner = ClassPathScanningCandidateComponentProvider(false)
    scanner.addIncludeFilter(AnnotationTypeFilter(Securable::class.java))

    for (beanDefinition in scanner.findCandidateComponents("com.example")) {
        val clazz = Class.forName(beanDefinition.beanClassName)
        if (clazz.isAnnotationPresent(Securable::class.java)) {
            oso.registerClass(clazz, clazz.getAnnotation(Securable::class.java).name)
        } else {
            oso.registerClass(clazz, clazz.simpleName)
        }
    }
}

Oso will use these bindings in order to resolve the classes in the Polar rules as well as call the methods on them for role, permission adn relation resolution.

Defining the models

We’ll start by defining the models for our application. We’ll just use simple classes here, but in the real app you would probably use JPA entities as your base.

If you’re subscribing to the DDD philosophy, you can separate the models into the domain and the data models.

Organization

@Securable("Organization")
data class Organization(val id: UUID)

User

We model the user as a collection of OrganizationMemberships. Each OrganizationMembership contains the Organization and the Role of the user in that Organization.

@Securable("User")
data class UserPrincipal(val memberships: List<OrganizationMembership>) : UserDetails

Repository

@Securable("Repository")
data class Repository(val id: UUID, val organization: Organization)

SpringSecurity Configuration

By convention, Oso follow the simple model of actor, action and resource. We can use this to define a custom PermissionEvaluator that can be used by SpringSecurity to evaluate permissions.

class OsoPermissionEvaluator(private val oso: Oso) : PermissionEvaluator {
    override fun hasPermission(authentication: Authentication?, targetDomainObject: Any?, permission: Any?): Boolean {
        if (authentication == null) return false
        if (targetDomainObject == null) return false
        if (permission == null || permission !is String) return false
        return oso.isAllowed(authentication.principal, permission, targetDomainObject)
    }
}

The above implementation uses targetDomainObject which allows us to write the rules that use the resources we already have as objects. In case you want to use the id of the resource, you can use the hasPermission(authentication: Authentication?, targetId: Serializable?, targetType: String?, permission: Any?): Boolean method instead. This one would be useful when securing the controllers.

override fun hasPermission(
    authentication: Authentication?,
    targetId: Serializable?,
    targetType: String?,
    permission: Any?
): Boolean {
    if (authentication == null) return false
    if (targetId == null) return false
    if (targetType == null) return false
    if (permission == null || permission !is String) return false

    when (targetType) {
        "YourType" -> {
            val loadedType = yourRepository.findById(targetId)
            return hasPermission(authentication, database, permission)
        }
        else -> {
            logger.warn("SECURITY: Unknown target type: {}", targetType)
            return false
        }
    }
}

As a last step before we can start using Oso, we need to configure SpringSecurity to use our custom PermissionEvaluator. We can do this by extending the MethodSecurityExpressionHandler:

@Configuration
@EnableMethodSecurity
internal class MethodSecurityConfiguration(private val oso: Oso) {
    @Bean
    fun expressionHandler(): MethodSecurityExpressionHandler {
        val handler = DefaultMethodSecurityExpressionHandler()
        handler.setPermissionEvaluator(OsoPermissionEvaluator(oso))

        return handler
    }
}

Securing the endpoints with SpringSecurity

Now that we have everything in place, we can start securing our endpoints. We can either secure the Controllers or the Service methods. In this example, we’ll secure the Service methods first using the @PreAuthorize annotation.

@Service
class RepositoryService {
    @PreAuthorize("hasPermission(#repo, 'push')")
    fun push(repo: Repository, content: Content): Repository {
        return repo
    }
}

Or, if you want to use the id of the resource to secure reading the repository:

@Controler
class RepositoryController(private val repositoryService: RepositoryService) {
    @GetMapping("/{id}")
    @PreAuthorize("hasPermission(#id, 'Repository', 'read')")
    fun getRepository(@PathVariable id: UUID): Repository {
        return repositoryService.getRepository(id)
    }
}

Conclusion

In this post, we saw how to integrate the Oso Authorization Framework with SpringSecurity. It’s also possible to use the same approach to integrate other authorization frameworks with SpringSecurity due to its extensibility.

The complete code for this example will be published later on my GitHub. I’ll update this post with the link once it’s published.