Securing resources
One of the most common use cases for using the OneWelcome Identity Platform is to use APIs to securely expose sensitive data to mobile apps. The OneWelcome Identity Platform Mobile SDK uses the Bearer Token Usage RFC (rfc6750) to talk to these protected API endpoints. To protect your APIs, you can set up a resource gateway according to the specification.
The resource gateway has the following responsibilities:
- Extract the access token from a request
- Introspect the access token
- Verify the access level through the introspection result
- Verify the authentication method through the introspection result
- Fetch and return the secured resource to the client
- Handle errors according to the RFC
Example code
The 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. The complete example resource gateway is available as a public repository on GitHub.
The example code exposes three endpoints:
-
The
/resource/devices
endpoint returns a list of the registered user's devices from the OneWelcome Identity Platform. -
The
/resource/application-details
endpoint returns some general information about the client application coming from the OneWelcome Identity Platform. -
The
/resource/user-id-decorated
endpoint returns the current user's ID with some added affixes.
Request the devices endpoint
In the following scenario, the client requests the devices endpoint:
-
The client first performs a GET request to
/resources/devices
with an access token in theAuthorization
header. -
The resource gateway introspects the access token with the OneWelcome Identity Platform.
-
The OneWelcome Identity Platform responds to this request with information about the access token, including the user ID (
sub
) and the scopes associated with the token. Theread
scope is required to access the devices endpoint. The OneWelcome Identity Platform also provides an Authentication Method Reference (amr
) field. You can use theamr
field to view how a user authenticated. To deny access to users who are implicitly authenticated, check theamr
field for the device's endpoint. -
and 5. If the token introspection result confirmed that the required scope is present, the devices for the user assigned to the access token are fetched from the secured API, which is the OneWelcome Identity Platform device API.
-
Finally, the list of devices is returned to the client application.
+----------------+ +------------------+ +--------------------+
| Mobile SDK | ---- (1) get devices ---> | Resource Gateway | --- (2) introspect access token ---> | the OneWelcome Identity Platform |
| (Client) | | | | |
| | <--- (6) user devices --- | | <--- (3) introspection response ---- | |
+----------------+ +------------------+ +--------------------+
^ |
| | +--------------------+
| +------------------ (4) get user devices ---------> | Resource Server |
| | (the OneWelcome Identity Platform)|
+---------------------- (5) user devices ------------ | |
+--------------------+
Extract the access token from a request
The Mobile SDK provides the access token in the Authorization
header with the Bearer
prefix. The access token is included in plain text, which means that it has no encoding.
Example request:
GET /resources/devices HTTP/1.1
Authorization: Bearer 5F09FC2DD7DCDB72ABF1A6C026DF8FFB9D7D1F4AD069E34F0A6EC6206A593420
Host: www.onewelcome.com:9999
Connection: close
The following example code extracts the access token from the HTTP header of the incoming request:
@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();
}
}
Introspect the access token
Send the received access token to the OneWelcome Identity Platform for introspection. For the API description, see the introspection API reference.
The following code executes the introspection requests and checks if the the token is valid according to the OneWelcome Identity Platform:
@Service
public class TokenIntrospectionService {
@Resource
private TokenIntrospectionRequestExecutor tokenIntrospectionRequestExecutor;
public TokenIntrospectionResult introspectAccessToken(final String accessToken) {
final TokenIntrospectionResult tokenIntrospectionResult = tokenIntrospectionRequestExecutor.execute(accessToken).getBody();
final boolean tokenInvalid = !tokenIntrospectionResult.isActive();
if (tokenInvalid) {
throw new InvalidAccessTokenException("Token introspection result: invalid token");
}
return tokenIntrospectionResult;
}
}
The TokenIntrospectionRequestExecutor
executes the actual HTTP request:
@Service
public class TokenIntrospectionRequestExecutor {
private static final String TOKEN = "token";
private static final String ENDPOINT_INTROSPECT = "/api/v2/token/introspect";
@Resource
private TokenServerConfig tokenServerConfig;
@Resource
private RestTemplate restTemplate;
public ResponseEntity<TokenIntrospectionResult> execute(final String accessToken) {
final HttpEntity<?> entity = createRequestEntity(accessToken);
final String url = tokenServerConfig.getBaseUri() + ENDPOINT_INTROSPECT;
final ResponseEntity<TokenIntrospectionResult> response;
try {
response = restTemplate.exchange(url, HttpMethod.POST, entity, TokenIntrospectionResult.class);
} catch (final RestClientException exception) {
throw new TokenServerException(exception);
}
return response;
}
private HttpEntity<?> createRequestEntity(final String accessToken) {
final HttpHeaders headers = createTokenIntrospectionRequestHeaders();
final MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add(TOKEN, accessToken);
return new HttpEntity<Object>(formData, headers);
}
private HttpHeaders createTokenIntrospectionRequestHeaders() {
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;
}
}
Verify the access level
To check whether the required access level is met, which means whether the user gave consent for certain scopes to the application, the scopes need to be validated. In this case the read
scope is required. This example also includes a method to validate whether 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);
}
}
}
Verify the authentication method
The TokenTypeValidationService
class verifies whether the user authenticated using implicit authentication.
@Service
public class TokenTypeValidationService {
public void validateImplicitAuthenticationToken(final TokenType[] tokenTypes) {
if (isNoImplicitAuthenticationToken(tokenTypes)) {
throw new InvalidAccessTokenException("Token is not an implicit authentication access token");
}
}
public void validateNoImplicitAuthenticationToken(final TokenType[] tokenTypes) {
if (isImplicitAuthenticationToken(tokenTypes)) {
throw new InvalidAccessTokenException("Token is an implicit authentication access token");
}
}
private boolean isImplicitAuthenticationToken(final TokenType[] tokenTypes) {
return ArrayUtils.contains(tokenTypes, IMPLICIT_AUTHENTICATION);
}
private boolean isNoImplicitAuthenticationToken(final TokenType[] tokenTypes) {
return !isImplicitAuthenticationToken(tokenTypes);
}
}
In this scenario, we do not want implicitly authenticated users to access the devices API. To check whether the user is not implicitly authenticated, the controller can pass the Authentication Method Reference (amr) values from the introspection result:
tokenTypeValidationService.validateNoImplicitAuthenticationToken(tokenIntrospectionResult.getAmr());
Fetch and return 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 using the URI: /api/v4/users/{userId}/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, you can request a resource that is not user-specific. For example, this might be useful if you have some content that you don't want to be publicly available on the web, and want to share only via a mobile app. To accomplish this, perform an anonymous resource call. This call uses an access token that does not have a user assigned. For an application to be able to receive such an access token, it needs to have the Anonymous resource calls
flow enabled in the OneWelcome Identity Platform.
The example resource gateway exposes an /resources/application-details
endpoint that returns the details of an application like the application version. This information is fetched from the access token introspection result and can be used with a user and an anonymous access token. A prerequisite is that the application-details
scope is granted to the access token that is used.
Handle errors according to the RFC
The RFC defines specific errors for certain error scenarios, see the 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);
}
}