Securing resources

One of the most common use cases for using the Onegini Mobile Security Platform is the need of exposing sensitive data to mobile apps via APIs in a secure way. The Onegini SDK uses the Bearer Token Usage RFC (rfc6750) to talk to these protected API endpoints. This quick guide will show you how to easily setup a resource gateway according to the specification to protect your APIs.

The resource gateway has the following responsibilities:

This example code shows how to implement these responsibilities in a simple Spring Boot application. For a production setup you might consider using a third party product or a more scalable solution.

In the example the Token Server device API will be exposed. The scenario: A client application (Onegini SDK) will perform a GET request to /resources/devices with an access token in the Authorization header. The resource gateway validates the access token at the Token Server and gets back the assigned user and scopes. The read scope is required to perform the action. When the required scope is available the devices for the user assigned to the access token are fetched from the secured API (Token Server device API) and returned to the client application.

  +-------------+                           +------------------+                                       +-----------------+                               
  | Onegini SDK | ---- (1) get devices ---> | Resource Gateway |  ---- (2) validate access token --->  |  Token Server   |                               
  |             |                           |                  |                                       |                 |                               
  |             | <--- (6) user devices --- |                  |  <---- (3) validation response -----  |                 |                               
  +-------------+                           +------------------+                                       +-----------------+    
                                                    ^ |
                                                    | |                                                +-----------------+ 
                                                    | +------------------ (4) get user devices ----->  | Resource Server | 
                                                    |                                                  | (Token Server)  | 
                                                    +---------------------- (5) user devices --------  |                 |
                                                                                                       +-----------------+

To configure a resource gateway in the Token Server, please follow the topic guide Resource gateway configuration.

Extracting the access token from the request

The Onegini SDK provides the access token in the Authorization header with the Bearer prefix. The access token is included in a plain text so no encoding.

Example request:

GET /resources/devices HTTP/1.1
Authorization: Bearer 5F09FC2DD7DCDB72ABF1A6C026DF8FFB9D7D1F4AD069E34F0A6EC6206A593420
Host: www.onegini.com:9999
Connection: close

Code example:

@Service
public class AccessTokenExtractor {

  public String extractFromHeader(final String authorizationHeaderValue) {
    if (isInvalidAuthorizationHeaderFormat(authorizationHeaderValue)) {
      final String message = String.format("Authorization header value `%s` does not contain an access token.", authorizationHeaderValue);
      throw new NoAccessTokenProvidedException(message);
    }

    return StringUtils.removeStart(authorizationHeaderValue, BEARER_PREFIX);
  }

  private boolean isInvalidAuthorizationHeaderFormat(final String authorizationHeader) {
    return StringUtils.isBlank(authorizationHeader)
        || !StringUtils.startsWithIgnoreCase(authorizationHeader, BEARER_PREFIX)
        || authorizationHeader.length() == BEARER_PREFIX.length();
  }

}

Validation of the access token at the Token Server

The received access token should be send to the Token Server to be validated. The token validation endpoint API of the Token Server is described here.

In this case we only extract the user id (reference_id), the scopes, the app_identifier, app_version and app_platform from the response. In this example the execution of the request and the parsing of the result is done in separate classes.

@Service
public class TokenValidationService {

  @Resource
  private TokenValidationRequestExecutor tokenValidationRequestExecutor;
  @Resource
  private TokenValidationResultParser tokenValidationResultParser;

  public TokenValidationResult validateAccessToken(final String accessToken) {
    final ResponseEntity<String> response = tokenValidationRequestExecutor.execute(accessToken);
    return tokenValidationResultParser.parse(response);
  }

}
@Service
public class TokenValidationRequestExecutor {
  private static final String GRANT_TYPE = "grant_type";
  private static final String VALIDATE_BEARER = "urn:innovation-district.com:oauth2:grant_type:validate_bearer";
  private static final String TOKEN = "token";

  @Resource
  private TokenServerConfig tokenServerConfig;
  @Resource
  private RestTemplate restTemplate;

  public ResponseEntity<String> execute(final String accessToken) {
    final HttpEntity<?> entity = createRequestEntity(accessToken);
    final String url = tokenServerConfig.getBaseUri() + "/token";

    final ResponseEntity<String> response;
    try {
      response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
    } catch (final RestClientException exception) {
      throw new TokenServerException(exception);
    }
    return response;
  }

  private HttpEntity<?> createRequestEntity(final String accessToken) {
    final HttpHeaders headers = createTokenValidationRequestHeaders();

    final MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
    formData.add(GRANT_TYPE, VALIDATE_BEARER);
    formData.add(TOKEN, accessToken);
    return new HttpEntity<Object>(formData, headers);
  }

  private HttpHeaders createTokenValidationRequestHeaders() {
    final HttpHeaders headers = new HttpHeaders();
    final String authorizationHeaderValue = new BasicAuthenticationHeaderBuilder()
        .withUsername(tokenServerConfig.getClientId())
        .withPassword(tokenServerConfig.getClientSecret())
        .build();

    headers.add(AUTHORIZATION, authorizationHeaderValue);
    headers.add(CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE);
    return headers;
  }

}
@Service
public class TokenValidationResultParser {

  @Resource
  private ObjectMapper objectMapper;

  public TokenValidationResult parse(final ResponseEntity<String> response) {

    if (response.getStatusCode() != HttpStatus.OK) {
      throw new InvalidAccessTokenException("Token validation request return status code: " + response.getStatusCode());
    }

    final Map<String, Object> jsonMap = mapJsonResponse(response.getBody());

    final String userId = (String) jsonMap.get("reference_id");
    final String scopes = (String) jsonMap.get("scope");
    final String applicationVersion = (String) jsonMap.get("app_version");
    final String applicationIdentifier = (String) jsonMap.get("app_identifier");
    final String applicationPlatform = (String) jsonMap.get("app_platform");

    return new TokenValidationResultBuilder()
        .withUserId(userId)
        .withScopes(scopes)
        .withApplicationVersion(applicationVersion)
        .withApplicationIdentifier(applicationIdentifier)
        .withApplicationPlatform(applicationPlatform)
        .build();
  }

  private Map<String, Object> mapJsonResponse(final String responseBody) {
    final Map<String, Object> jsonMap;
    final TypeReference<Map<String,Object>> mapTypeReference = new TypeReference<Map<String,Object>>() {};
    try {
      jsonMap = objectMapper.readValue(responseBody, mapTypeReference);
    } catch (final IOException exception) {
      throw new TokenServerException(exception);
    }
    return jsonMap;
  }

}

Validation if required access level is met

To check if the required access level is met, in other words if the end user gave consent for certain scopes to the application, the scopes need to be validated. In this case the read scope is required. This is example also includes a method to validate if the application-details scope is granted.

@Service
public class ScopeValidationService {

  public static final String SCOPE_READ = "read";
  public static final String SCOPE_APPLICATION_DETAILS = "application-details";

  public void validateReadScopeGranted(final String grantedScopes) {
    validateScopeGranted(grantedScopes, SCOPE_READ);
  }

  public void validateApplicationDetailsScopeGranted(final String grantedScopes) {
    validateScopeGranted(grantedScopes, SCOPE_APPLICATION_DETAILS);
  }

  private void validateScopeGranted(final String grantedScopes, final String scope) {
    if (StringUtils.isBlank(grantedScopes)) {
      throw new ScopeNotGrantedException("No scopes granted to access token");
    }

    final String[] scopes = StringUtils.split(grantedScopes, SPACE);
    final boolean scopeNotGranted = !ArrayUtils.contains(scopes, scope);
    if(scopeNotGranted) {
      final String message = String.format("Scope %s not granted to provided access token.", scope);
      throw new ScopeNotGrantedException(message);
    }
  }

}

Providing access to the secured resource

After all checks are passed successfully the devices for the user in the access token can be fetched and returned to the original caller. The device api can be reached via uri /api/v2/users/{user_id}/devices.

@Service
public class DeviceApiRequestService {

  public static final String DEVICE_API_PATH = "/api/v2/users/{user_id}/devices";

  @Resource
  private RestTemplate restTemplate;
  @Resource
  private DeviceApiConfig deviceApiConfig;

  public ResponseEntity<Devices> getDevices(final String userId) {
    final HttpEntity<?> requestEntity = createRequestEntity();
    final String uri = deviceApiConfig.getServerRoot() + DEVICE_API_PATH;

    return restTemplate.exchange(uri, HttpMethod.GET, requestEntity, Devices.class, userId);
  }

  private HttpEntity<?> createRequestEntity() {
    final HttpHeaders headers = new HttpHeaders();
    final String authorizationHeaderValue = new BasicAuthenticationHeaderBuilder()
        .withUsername(deviceApiConfig.getUsername())
        .withPassword(deviceApiConfig.getPassword())
        .build();
    headers.add(AUTHORIZATION, authorizationHeaderValue);

    return new HttpEntity<>(headers);
  }
}

Alternatively a resource can be requested that does not require a user to be logged in. For example you have some content you only want to share via a mobile app and not being publicly available on the web. For that an anonymous resource call can be performed. This call uses an access token which does not have a user assigned. An application needs to have allowed function client_credentials enabled to be able to receive such an access token.

The example resource gateway exposes an endpoint /resources/application-details which returns the details of an application like the application version. This information is fetched from the access token validation result and can be used with a user and a anonymous access token. A prerequisite is that the application-details scope is granted to the access token that is used.

Error handling according to RFC

The RFC defines specific errors for certain error scenarios, see Bearer Token Usage Error Codes RFC (rfc6750 par 3.1).

public class ErrorResponseBuilder {

  public static ResponseEntity<ErrorResponse> buildBadRequestResponse() {
    final String error = "invalid_request";
    final String errorDescription = "The request is missing a required parameter";

    final ErrorResponse errorResponse = new ErrorResponse(error, errorDescription);
    return ResponseEntity.badRequest().body(errorResponse);
  }

  public static ResponseEntity<ErrorResponse> buildInvalidScopeResponse() {
    final String error = "insufficient_scope";
    final String errorDescription = "The request requires higher privileges than provided by the access token.";

    final ErrorResponse errorResponse = new ErrorResponse(error, errorDescription);
    return ResponseEntity.status(FORBIDDEN).body(errorResponse);
  }

  public static ResponseEntity<ErrorResponse> buildInvalidAccessTokenResponse() {
    final String error = "invalid_token";
    final String errorDescription = "The access token provided is expired, revoked, malformed, or invalid for other reasons.";

    final ErrorResponse errorResponse = new ErrorResponse(error, errorDescription);
    final String authenticateHeaderValue = BEARER_PREFIX + "error=\"" + error + "\", error_description=\"" + errorDescription + "\"";
    return ResponseEntity.status(UNAUTHORIZED).header(WWW_AUTHENTICATE, authenticateHeaderValue).body(errorResponse);
  }
}

The code

The complete code can be found on github: https://github.com/Onegini/example-resource-gateway