Skip to main content

Security

The REST expanders provide authentication and authorization stubs which can be used to implement these concepts depending on the needs of the project. This can be done using some expanded infrastructure through injected features or they can be implemented custom by a developer.

Some default implementations for authentication and authorization can be enabled using model options. These mainly provide integration with the account base component. For obvious security reasons, by default all access into the API is blocked, so an implementation is needed to provide access in some way.

Authentication

Authentication is the step which validates user credentials and builds the UserContext object for the application.

The AuthenticationProvider class is generated as a stub in every Component which provides a REST API. By default an anonymous UserContext is created in this stub and added to the Context. This method takes in the Authorization header, which is typically used for access control through HTTP. If the header has an invalid format, a MalformedHeaderException is thrown.

Authentication can be implemented for the parsed Authorization header in the filter-authorization-header anchor, or if something other than the Authorization is not used, the filter anchors can be implemented instead.

Default expanded implementation of the filter method in the AuthenticationProvider class.
@Override
public void filter(final ContainerRequestContext requestContext) throws IOException {
ApiComponentUserContext userContext = ApiComponentUserContext.anonymous();

final String authorizationHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
if (authorizationHeader != null) {
int separatorPosition = authorizationHeader.indexOf(' ');
if (separatorPosition == -1) {
throw new MalformedHeaderException(HttpHeaders.AUTHORIZATION, "Header must have format '<key> <credentials>'.");
}
String authorizationKey = authorizationHeader.substring(0, separatorPosition);
String authorizationCredentials = authorizationHeader.substring(separatorPosition + 1);
if (authorizationKey.isEmpty() || authorizationCredentials.isEmpty()) {
throw new MalformedHeaderException(HttpHeaders.AUTHORIZATION, "Header must not contain empty key or credentials values.");
}

// @anchor:filter-authorization-header:start
// @anchor:filter-authorization-header:end
// anchor:custom-filter-authorization-header:start
// anchor:custom-filter-authorization-header:end
}

// @anchor:filter:start
// @anchor:filter:end
// anchor:custom-filter:start
// anchor:custom-filter:end

JaxNsContext.updateContext(requestContext, userContext);
}

Anonymous access

caution

Unless there is a very good reason to do so, never do this on any application that publicly accessible.

Though not recommended, the most basic form of access to an API is anonymous access. This simply means that the API will have absolutely no user authentication and will allow all incoming calls.

Option
jaxrs.auth.anonymous.enable Component

Enables anonymous access to the API.

<options>
<jaxrs.auth.anonymous.enable/>
</options>

Basic authentication

If you are using the account base-component, you can enable basic HTTP authentication with the rest-expanders.

Option
jaxrs.auth.basic.enable Component

Available with the rest-account-security-expanders bundle.

Adds (stateless) basic authentication to the JAX-RS REST API, which uses the account base component to authenticate users where they can be authenticated using a username/password combination.

<options>
<jaxrs.auth.basic.enable/>
</options>

This will add a feature to the AuthenticationProvider, which processes the Authorization header if its value is formatted as Basic <credentials>. The credentials in basic authentication are encoded with a base64 encoding. The username and password that are extracted are passed to the account component, which performs authentication and returns a UserContext.

Expanded implementation in the authentication provider class for absic authentication
@Override
public void filter(final ContainerRequestContext requestContext) throws IOException {
MyComponentUserContext userContext = MyComponentUserContext.anonymous();

final String authorizationHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
if (authorizationHeader != null) {
int separatorPosition = authorizationHeader.indexOf(' ');
if (separatorPosition == -1) {
throw new MalformedHeaderException(HttpHeaders.AUTHORIZATION, "Header must have format '<key> <credentials>'.");
}
String authorizationKey = authorizationHeader.substring(0, separatorPosition);
String authorizationCredentials = authorizationHeader.substring(separatorPosition + 1);
if (authorizationKey.isEmpty() || authorizationCredentials.isEmpty()) {
throw new MalformedHeaderException(HttpHeaders.AUTHORIZATION, "Header must not contain empty key or credentials values.");
}

// @anchor:filter-authorization-header:start
if (authorizationKey.equals("Basic")) {
final String credentials = new String(DatatypeConverter.parseBase64Binary(authorizationCredentials),
StandardCharsets.US_ASCII);
final int colonPos = credentials.indexOf(":");
if (colonPos == -1) {
throw new MalformedHeaderException(HttpHeaders.AUTHORIZATION, "Failed to parse basic authentication credentials.");
}

final String username = credentials.substring(0, colonPos);
final String password = credentials.substring(colonPos + 1);
if (username.isEmpty() || password.isEmpty()) {
throw new MalformedHeaderException(HttpHeaders.AUTHORIZATION, "Basic authentication credentials are malformed.");
}
final UserInput userInput = new UserInput();
userInput.setName(username);
userInput.setPassword(password);
String paramString = "";
// anchor:custom-before-basic-authenticate:start
// anchor:custom-before-basic-authenticate:end

final AuthenticationAgent authenticationAgent = AuthenticationAgent.getAuthenticationAgent(Context.emptyContext());
final TaskResult<UserContext> authenticationResult = authenticationAgent.perform(userInput, paramString);
if (authenticationResult.isSuccess()) {
userContext.update(authenticationResult.getValue());
}
}
// @anchor:filter-authorization-header:end
// anchor:custom-filter-authorization-header:start
// anchor:custom-filter-authorization-header:end
}

// @anchor:filter:start
// @anchor:filter:end
// anchor:custom-filter:start
// anchor:custom-filter:end

JaxNsContext.updateContext(requestContext, userContext);
}

OpenID Connect authentication

If you are using the account base-component with the option baseComponents.authentication.enableOIDC, you can enable OpenID Connect authentication with the rest-expanders.

Option
jaxrs.auth.oidc.enable Component

Available with the rest-account-security-expanders bundle.

This option enables OpenID Connect authentication to the REST API, which authenticates against the users in the account component. Unlike with the base-components, only one OpenID Connect provider can be used currently, so the option value should be the identifier of the provider you have configured in the account component.

<options>
<jaxrs.auth.oidc.enable>google</jaxrs.auth.oidc.enable>
</options>
<options>
<jaxrs.auth.oidc.enable>keycloak</jaxrs.auth.oidc.enable>
</options>
caution

The baseComponents.authentication.enableOIDC option must be present, but the provider used for the rest-expander does not necessarily have to be enabled in general for the base-components, though of course there must always be matching configuration for the provider in the account.ns.properties file.

In the AuthenticationProvider code is added which processes the Authorization header if its value is formatted as Bearer <accessToken>. As indicated, this takes in an OAuth2 access token as a bearer token from the REST API.

Expanded implementation in the authentication provider class for OpenID Connect integration
@Override
public void filter(final ContainerRequestContext requestContext) throws IOException {
MyComponentUserContext userContext = MyComponentUserContext.anonymous();

final String authorizationHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
if (authorizationHeader != null) {
int separatorPosition = authorizationHeader.indexOf(' ');
if (separatorPosition == -1) {
throw new MalformedHeaderException(HttpHeaders.AUTHORIZATION, "Header must have format '<key> <credentials>'.");
}
String authorizationKey = authorizationHeader.substring(0, separatorPosition);
String authorizationCredentials = authorizationHeader.substring(separatorPosition + 1);
if (authorizationKey.isEmpty() || authorizationCredentials.isEmpty()) {
throw new MalformedHeaderException(HttpHeaders.AUTHORIZATION, "Header must not contain empty key or credentials values.");
}

// @anchor:filter-authorization-header:start
if (authorizationKey.equals("Bearer")) {
final OIDCAccessInput accessInput = new OIDCAccessInput();
accessInput.setProvider("google");
accessInput.setAccessToken(authorizationCredentials);
String paramString = "";
// anchor:custom-before-oidc-authenticate:start
// anchor:custom-before-oidc-authenticate:end

final UserAgent userAgent = UserAgent.getUserAgent(Context.emptyContext());
final AuthenticationResult authenticationResult = userAgent.authenticate(AuthenticationDetails.from(accessInput));
if (authenticationResult.isSuccess()) {
userContext.update(authenticationResult.getUserContext());
}
}
// @anchor:filter-authorization-header:end
// anchor:custom-filter-authorization-header:start
// anchor:custom-filter-authorization-header:end
}

// @anchor:filter:start
// @anchor:filter:end
// anchor:custom-filter:start
// anchor:custom-filter:end

JaxNsContext.updateContext(requestContext, userContext);
}

Authorization

Authorization in the JAX-RS stack is handled by deciding if a call is allowed to proceed to the API based on the UserContext. Authorization is handled in the AuthorizationProvider class, which is executed after the AuthenticationProvider that creates the UserContext.

By default all access to the REST API is disabled. The reason for this approach is to prevent potential security issues if an API is left exposed by accident. This is achieved by preventing any access to the API for an anonymous UserContext. It is possible to enable anonymous access if needed, but it is not recommended for use outside of development.

Calls with the OPTIONS method are always permitted given that it does not expose any data, only information about the API itself.

Implementation

Authorization can be implemented in the filter anchor of the provider. This is intended for fully custom authorization implementations. If authorization passes the method should return, whereas for unauthorized users, the NotAuthorizedException should be thrown.

@Override
public void filter(final ContainerRequestContext requestContext) throws IOException {
Context context = JaxNsContext.getContext(requestContext);
ApiComponentUserContext userContext = ApiComponentUserContext.from(context);
String path = requestContext.getUriInfo().getPath();

// @anchor:before-filter:start
// @anchor:before-filter:end
// anchor:custom-before-filter:start
// anchor:custom-before-filter:end

try {
if (HttpMethod.OPTIONS.equals(requestContext.getMethod())) {
return;
}

if (!isExcluded(requestContext)) {
SecurityRight securityRight = getSecurityRight(path);
if (securityRight == null) {
// @anchor:default-security:start
// @anchor:default-security:end
// anchor:custom-default-security:start
// anchor:custom-default-security:end
} else if (securityRight.disabled()) {
// @anchor:disabled-security:start
// @anchor:disabled-security:end
// anchor:custom-disabled-security:start
// anchor:custom-disabled-security:end

return;
}

// @anchor:filter:start
// @anchor:filter:end
// anchor:custom-filter:start
// anchor:custom-filter:end

if (userContext.isAnonymous()) {
throw new NotAuthorizedException("User is not authorized to access this resource.");
}
}
} finally {
JaxNsContext.updateContext(requestContext, userContext);
}
}

Exclusions

It is possible to define exclusions where specific requested endpoints do not require authorization. The provider has a method for this, where depending on the path this can be handled.

This exclusion mechanism should be used with care and is intended primarily for paths which are not part of the API itself or custom exclusion mechanisms. For endpoints which are to be excluded, it is preferred to use the SecurityRight annotation instead.

private boolean isExcluded(final ContainerRequestContext containerRequestContext) {
final String path = containerRequestContext.getUriInfo().getPath();

// @anchor:exclude:start
// @anchor:exclude:end
// anchor:custom-exclude:start
// anchor:custom-exclude:end

return false;
}

SecurityRight

Authorization comes with a predefined stub mechanism which is intended to handle security though the definition of rights on the endpoints. The mechanism is stubbed in such a way that the implementation of these rights can be customized.

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SecurityRight {

boolean disabled() default false;

// @anchor:methods:start
// @anchor:methods:end
// anchor:custom-methods:start
// anchor:custom-methods:end

}

The @SecurityRight annotation can be defined on the method of any JAX-RS REST endpoint in the API. The AuthorizationProvider will automatically retrieve it if present for every call, so it can be processed for authorization.

The idea behind the security rights is that in some form specific rights can be attached to a user. These rights, typically identified by a name can be added to an endpoint through the annotation. If they are identified by a name, a name field could be added to the SecurityRight annotation for example. The AuthorizationProvider would then determine based on the UserContext if the user has the right(s) defined on this endpoint to access it.

One mechanism that is available in the stubbed implementation is the disabled field on the SecurityRight annotation, which is set to true, will disable authorization for that specific endpoint.

note

An expanded implementation that leverages SecurityRight is available for the DataAccessRight element which is part of the account base-component.

DataAccessRight integration

Like other security features, it is possible to enable some default integration with the account component for authorization.

Option
jaxrs.account.accessControl.enable Component

Available with the rest-account-security-expanders bundle.

This option provides an authorization implementation which integrates with the authorization task in the account component. Through this system, rules for access control can be defined in the account::DataAccess element.

<options>
<jaxrs.account.accessControl.enable/>
</options>

Additional code is injected in the AuthorizationProvider.filter() method, which processes the authorization through the account component, based on the @DataAccessRight annotations. If any of the rights configured in the annotations are granted to the user in the account component, the call will be authorized.

Authorization filter implementation to integrate with account component
@Override
public void filter(final ContainerRequestContext requestContext) throws IOException {
Context context = JaxNsContext.getContext(requestContext);
MyComponentUserContext userContext = MyComponentUserContext.from(context);
String path = requestContext.getUriInfo().getPath();

// @anchor:before-filter:start
// @anchor:before-filter:end
// anchor:custom-before-filter:start
// anchor:custom-before-filter:end

try {
if (HttpMethod.OPTIONS.equals(requestContext.getMethod())) {
return;
}

if (!isExcluded(requestContext)) {
SecurityRight securityRight = getSecurityRight(path);
if (securityRight == null) {
// @anchor:default-security:start
// @anchor:default-security:end
// anchor:custom-default-security:start
// anchor:custom-default-security:end
} else if (securityRight.disabled()) {
// @anchor:disabled-security:start
// @anchor:disabled-security:end
// anchor:custom-disabled-security:start
// anchor:custom-disabled-security:end

return;
}

// @anchor:filter:start
if (securityRight != null) {
AuthorizationManagerAgent authorizationAgent = AuthorizationManagerAgent.getAuthorizationManagerAgent(context);
for (DataAccessRight dataAccessRight : securityRight.dataAccessRights()) {
DataAccessQuery dataAccessQuery = new AccessQueryBuilder()
.setComponent(dataAccessRight.component())
.setElement(dataAccessRight.dataElement())
.setFunctionality(dataAccessRight.functionality())
.createAccessQuery();

TaskResult<DataAccessRights> dataAccessRights = authorizationAgent.getDataAccessRights(dataAccessQuery);
if (dataAccessRights.isError()) {
throw new NotAuthorizedException("Could not retrieve data access rights for '" + dataAccessQuery + "'.");
}

if (dataAccessRights.getValue().isAllowed(dataAccessRight.functionality())) {
return;
}
}
throw new NotAuthorizedException("User is not authorized to access this resource.");
}
// @anchor:filter:end
// anchor:custom-filter:start
// anchor:custom-filter:end

if (userContext.isAnonymous()) {
throw new NotAuthorizedException("User is not authorized to access this resource.");
}
}
} finally {
JaxNsContext.updateContext(requestContext, userContext);
}
}

DataAccessRight annotation

An array field dataAccessRights is added to the @SecurityRight annotation and provisions it with a set of @DataAccessRight annotations which define new API-specific access rights that can be used in the DataAccess component to grant access to the endpoint.

EndpointRights
GET (list)apiAll, apiGetList, (status)
GET (resource)apiAll, apiGetResource, (status)
POSTapiAll, apiCreate, (create)
PUTapiAll, apiModify, (modify)
PATCHapiAll, apiUpdate, (modify)
REMOVEapiAll, apiRemove, (delete)
Option
jaxrs.account.accessControl.defaultRights Component

Available with the rest-account-security-expanders bundle.

Adds the default access rights (status, create, modify and delete) that are used in the standard struts-based API, which could make configuration easier if the access control should match between these interfaces.

The option will only have an effect when jaxrs.account.accessControl.enable is also present.

<options>
<jaxrs.account.accessControl.defaultRights/>
</options>
Example

The @SecurityRight annotation on a GETlist endpoint with the default rights option included:

@SecurityRight(dataAccessRights = {
@DataAccessRight(component = "testComp", dataElement = "city", functionality = "apiAll"),
@DataAccessRight(component = "testComp", dataElement = "city", functionality = "apiGetList"),
@DataAccessRight(component = "testComp", dataElement = "city", functionality = "status"),
})