Skip to content

Making SCIM requests

Jacob Childress edited this page Mar 7, 2019 · 18 revisions

Requests to a SCIM service provider are made through a ScimService instance. The ScimService class wraps a JAX-RS Client instance, providing methods for building and making SCIM requests.

Responses are deserialized to instances of GenericScimResource or instances derived from BaseScimResource. See working with SCIM resources for more information.

ScimService

A new ScimService instance requires a JAX-RS WebTarget that specifies the base URI of a SCIM service provider. A WebTarget instance is obtained through a JAX-RS Client instance.

Client restClient = ClientBuilder.newClient();
WebTarget target =
    restClient.target(new URI("https://example.com/scim/v2"));
ScimService scimService = new ScimService(target);

Note that the details of creating a JAX-RS Client will vary depending on your needs. See JAX-RS client examples for examples of JAX-RS Client configuration.

ScimService supports making all SCIM request types (resource retrieval, searches, resource creation, resource replacement, resource modification, and resource deletion), and it provides a few different ways to do each. These are described below.

SCIM service provider metadata

SCIM service providers advertise provider-specific metadata at three special endpoints; ScimService provides accessor methods for these.

Endpoint Description Accessor method(s)
/ServiceProviderConfig Describes the service provider configuration, including whether or not optional aspects of the SCIM standard are supported. ScimService.getServiceProviderConfig()
/Schemas Describes the schemas supported by the service provider. Schemas are named sets of attribute descriptors. ScimService.getSchemas(), ScimService.getSchema(String)
/ResourceTypes Describes the resource types supported by the service provider. Resource types represent entities such as users or groups and are composed of one or more schemas. ScimService.getSchemas(), ScimService.getSchema(String)

The following example illustrates calling the /ServiceProviderConfig endpoint to determine if the service provider supports partial modifications using PATCH:

ServiceProviderConfigResource providerConfig =
    scimService.getServiceProviderConfig();
if (providerConfig.getPatch().isSupported()) {
  // Etc.
}

Resource URIs and resource IDs

SCIM resources such as users can be identified by either a URI or the combination of a SCIM resource type endpoint and a unique resource identifier.

A URI could be a SCIM resource's complete absolute URI, such as "https://example.com/scim/v2/Users/2819c223-7f76-453a-919d-413861904646", or it could be relative to the base URI that was specified when creating the ScimService, such as "Users/2819c223-7f76-453a-919d-413861904646".

If specifying a SCIM resource by ID, then the resource type endpoint and the resource ID are needed. Using the examples above, the resource type endpoint is "Users", and the resource ID is "2819c223-7f76-453a-919d-413861904646".

Me

The ScimService class provides a ME_URI constant, which can be used in any request method that takes a URI. When used, this specifies the currently authenticated user. This is useful when you are authenticated on behalf of a logged-in user, but you don't necessarily know the user's resource type or unique ID in advance.

SCIM resource return types

All request methods return a class that implements the ScimResource interface. This can either be an instance of GenericScimResource or a concrete implementation of the BaseScimResource class.

GenericScimResource is a general-purpose interface for interacting with SCIM resources that exposes attributes as instances of Jackson JsonNode. Use this if you are comfortable with the Jackson API and cannot or do not wish to model your SCIM resource types as POJOs.

BaseScimResource is an abstract class that serves as a basis for creating POJOs that model SCIM resource types. This is useful if you know details in advance about the SCIM resource types that you'll be working with. The SCIM 2 SDK provides such POJOs for the resource types defined in RFC 7643, the SCIM Core Schema spec. One such example is UserResource, which you can use as a model for creating your own POJOs.

This topic is discussed in greater detail in the article working with SCIM resources.

Retrieving with GET

Resources are retrieved using the ScimService.retrieve(…) methods or the ScimService.retrieveRequest(…) method. The patterns shown here generally apply to the other request methods.

To retrieve a resource by URI:

URI uri = new URI("Users/2819c223-7f76-453a-919d-413861904646");
GenericScimResource scimUser =
  scimService.retrieve(uri, GenericScimResource.class);
  
// Or, using the Me endpoint:
GenericScimResource scimUser =
  scimService.retrieve(ScimService.ME_URI, GenericScimResource.class);

To retrieve a resource by resource type and unique ID:

GenericScimResource scimUser =
  scimService.retrieve("Users", "2819c223-7f76-453a-919d-413861904646", 
  GenericScimResource.class);

You can also call the retrieveRequest(…) method to obtain a ResourceRequestBuilder, which allows you to modify the request using a fluent API. The invoke(…) method is called to build and submit the request. For example:

GenericScimResource scimUser =
  scimService.retrieveRequest("Users", "2819c223-7f76-453a-919d-413861904646")
  .header("X-Custom-Header", "custom-header-value")
  .invoke(GenericScimResource.class);

This is a pattern in the ScimService class. For each request type, at least one method is provided that returns a RequestBuilder with the same fluent API.

Creating with POST

To create a SCIM resource, first create an instance of GenericScimResource or BaseScimResource, and provide it to one of the create(…) methods, along with the desired resource type endpoint.

The following example uses the UserResource class, which is a concrete implementation of BaseScimResource.

UserResource userResource = new UserResource();
userResource.setUserName("user.1");
userResource.setPassword("secret");
Name name = new Name()
    .setGivenName("Joe")
    .setFamilyName("Public");
userResource.setName(name);
Email email = new Email()
    .setType("home")
    .setPrimary(true)
    .setValue("joe.public@example.com");
userResource.setEmails(Collections.singletonList(email));

userResource = scimService.create("Users", userResource);

Replacing with PUT

The replace(…) method updates a SCIM resource by replacing all of its attributes.

The following example retrieves a resource, performs an in-memory modification, and then replaces the existing resource with the modified copy.

UserResource userResource = 
    scimService.retrieve("Users", "2819c223-7f76-453a-919d-413861904646", 
    UserResource.class);
    
Email newEmail = userResource.getEmails().get(0);
newEmail.setValue("joe@example.com");
userResource.setEmails(Collections.singletonList(newEmail));

userResource = scimService.replace(userResource);

Note that a URI or unique ID did not have to be specified when performing the replace, because that information is already available in the UserResource object.

Updating with PATCH

If the SCIM service provider supports partial modifications using PATCH, then you can use the modifyRequest(…) methods. These provide a powerful and expressive means of making specific changes to a SCIM resource.

Fundamentally, a PATCH request consists of one or more operations (add, replace, or remove), a path specifying the target attribute, and the value to set. Paths can be as simple as attribute names or can include filters that select one or more attributes. Values can be expressed as a Jackson JsonNode or as one of several basic types, such as strings, integers, booleans, dates, or URIs.

The following examples of using PATCH are not exhaustive.

To replace a single-valued attribute:

UserResource userResource =
  scimService.modifyRequest("Users", "2819c223-7f76-453a-919d-413861904646")
    .replaceValue("displayName", "Joe Public")
    .invoke(UserResource.class);

// Or:
UserResource userResource =
  scimService.modifyRequest("Users", "2819c223-7f76-453a-919d-413861904646")
    .addOperation(PatchOperation.replace("displayName", "Joe Public"))
    .invoke(UserResource.class);

// Or:
UserResource userResource =
  scimService.modifyRequest("Users", "2819c223-7f76-453a-919d-413861904646")
    .addOperation(PatchOperation.replace("displayName",
                                         JsonUtils.valueToNode("Joe Public")))
    .invoke(UserResource.class);

// Or:
UserResource userResource =
  scimService.modifyRequest("Users", "2819c223-7f76-453a-919d-413861904646")
    .addOperation(PatchOperation.replace("displayName",
                                         TextNode.valueOf("Joe Public")))
    .invoke(UserResource.class);

To replace a single-valued complex attribute:

Name name = new Name()
    .setGivenName("Joseph")
    .setFamilyName("Public");
scimService.modifyRequest("Users", "2819c223-7f76-453a-919d-413861904646")
    .replaceValue("name", name)
    .invoke(UserResource.class);

// Or:
scimService.modifyRequest("Users", "2819c223-7f76-453a-919d-413861904646")
    .addOperation(PatchOperation.replace(Path.fromString("name"),
                                         JsonUtils.valueToNode(name)))
    .invoke(UserResource.class);

To replace a sub-attribute:

scimService.modifyRequest("Users", "2819c223-7f76-453a-919d-413861904646")
    .replaceValue("name.givenName", "Joe")
    .invoke(UserResource.class);

To add members to a multivalued attribute:

Email email1 = new Email()
    .setType("work")
    .setPrimary(false)
    .setValue("joe.public@work.com");

Email email2 = new Email()
    .setType("other")
    .setPrimary(false)
    .setValue("joe.public@gmail.com");

scimService.modifyRequest("Users", "2819c223-7f76-453a-919d-413861904646")
    .addValues("emails", email1, email2)
    .invoke(UserResource.class);

// Or:
List<Email> emails = 
  new ArrayList<Email>(Arrays.asList(email1, email2));
scimService.modifyRequest("Users", "2819c223-7f76-453a-919d-413861904646")
    .addOperation(PatchOperation.add(Path.fromString("emails"),
                                     JsonUtils.valueToNode(emails)))
    .invoke(UserResource.class);

To remove an attribute:

// Single-valued attribute
scimService.modifyRequest("Users", "2819c223-7f76-453a-919d-413861904646")
    .removeValues("displayName")
    .invoke(UserResource.class);

// Multivalued attribute; removes all members
scimService.modifyRequest("Users", "2819c223-7f76-453a-919d-413861904646")
    .removeValues("emails")
    .invoke(UserResource.class);

Multiple operations may be provided in a single PATCH request:

scimService.modifyRequest("Users", "2819c223-7f76-453a-919d-413861904646")
    .replaceValue("displayName", "Joe Public")
    .replaceValue("active", false)
    .invoke(UserResource.class);

A path for a multivalued attribute can even include a filter, which will cause the modification to be applied to the matching member attribute(s):

Email newEmail = new Email()
    .setType("other")
    .setPrimary(false)
    .setValue("joe.public@hotmail.com");

Filter filter =
    Filter.hasComplexValue("emails",
                           Filter.eq("value", "joe.public@hotmail.com"));
// If path doesn't match, a BadRequestException will be thrown
scimService.modifyRequest("Users", "2819c223-7f76-453a-919d-413861904646")
    .replaceValue(filter.toString(), newEmail)
    .invoke(UserResource.class);

Deleting

Deleting a resource is straightforward. A delete method returns no value.

scimService.delete("Users", "2819c223-7f76-453a-919d-413861904646");

// If you have a resource instance
UserResource userResource =
    scimService.retrieve("Users", "2819c223-7f76-453a-919d-413861904646",
                        UserResource.class);
scimService.delete(userResource);

If you need to customize the request, you can use the deleteRequest(…) method to get a RequestBuilder, then use its fluent API to adjust the request before calling invoke().

scimService.deleteRequest("Users", "2819c223-7f76-453a-919d-413861904646")
    .header("X-Custom-Header", "custom-header-value")
    .invoke();

Searching

Searches are performed by calling searchRequest(…) to get a SearchRequestBuilder, chaining search directive methods, and finally calling invoke(…), which will return a ListResponse containing the type provided to invoke(…).

A ListResponse provides methods for getting the total number of results and paging information such as the number of resources per page and the start index. A ListResponse is iterable.

Filter filter = Filter.eq("lastName", "Jensen");
ListResponse<UserResource> searchResponse =
  scimService.searchRequest("Users")
    .filter(filter.toString())
    .invoke(UserResource);
for (UserResource userResource : searchResponse)
{
  // Etc.
}

Search filters

You can either specify a search filter as a string or build it using the Filter class, which is based on the operators defined by RFC 7644.

To check that an attribute value equals an expected value:

Filter.eq("userName", "user.1")

To check for the presence of an attribute value:

Filter.pr("userName")

To check that an attribute value starts with an expected value:

Filter.sw("name.givenName", "Barb")

To check that an attribute value ends with an expected value:

Filter.ew("name.familyName", "Jen")

To check that an attribute value is greater than or equal to a specific value:

Filter.gte("age", 18)

To negate a search condition:

Filter.not(Filter.gt("age", 18))

To search using AND:

Filter.and(
  Filter.eq("userName", "user.1"),
  Filter.eq("name.familyName", "Jensen")
)

To search based on matching sub-attributes of a multivalued attribute:

// Same as: addresses[(type eq "home" and country eq "US")]
Filter.hasComplexValue(
  "addresses", 
  Filter.and(
    Filter.eq("type", "home"), 
    Filter.eq("country", "US")
  )
)

Paging search results

A search filter may match many SCIM resources, so a SCIM service provider may limit the number of resources returned per response, requiring the client to page through results.

The client pages through search results by calling the page(int, int) method, providing the 1-based startIndex of the first query result and the number of resources to return in each response.

Filter filter = Filter.eq("lastName", "Jensen");
ListResponse<UserResource> searchResponse =
  scimService.searchRequest("Users")
    .filter(filter.toString())
    // Start at index 1 and return 10 results
    .page(1, 10)
    .invoke(UserResource);
// The search response will include up to 10 resources.
for (UserResource userResource : searchResponse)
{
  // Etc.
}

Specifying attributes to include or exclude

Not all clients may need or want to retrieve the full contents of a SCIM resource. For example, a help desk application may need to view a complete representation of a user, but an online ordering application may only need the user's name, address, phone number, and email address. This could be handled at the authorization level — for example, by using OAuth 2 scopes to constrain the data available to an application in the first place — but a client may also specify which attributes to retrieve or which attributes to not retrieve.

This is done through the SCIM 2 SDK by calling a request method that returns a RequestBuilder with the attributes(String…) method or the excludeAttributes(String…) method.

For example, the following would retrieve a user with only the name and emails attributes:

scimService.retrieveRequest("Users", "2819c223-7f76-453a-919d-413861904646")
  .attributes("name", "emails")
  .invoke(GenericScimResource.class);

And the following would a retrieve a user, returning all available attributes except the name and emails attributes:

scimService.retrieveRequest("Users", "2819c223-7f76-453a-919d-413861904646")
  .excludedAttributes("name", "emails")
  .invoke(GenericScimResource.class);

Note that some attributes are always returned. This is determined by the schema and therefore varies, but you should at least expect the id attribute to always be present.

Server-side errors

In the event of a server-side error, the SCIM 2 SDK will throw a ScimServiceException. This contains an ErrorResponse object with the HTTP status code and any available error messages for the failed request. Applications should be prepared to catch and log or present this information, if necessary.

Processing problems with the underlying JAX-RS client or lower-level networking errors such as connection timeouts will result in a javax.ws.rs.ProcessingException.