From c8585baebc9fc35ed12a3321acf47730c967b5d3 Mon Sep 17 00:00:00 2001
From: Gaetan Boismal <gaetan.boismal@forgerock.com>
Date: Tue, 24 May 2016 15:45:03 +0000
Subject: [PATCH] OPENDJ-2880 Rest2Ldap as an OAuth2 Resource Server

---
 opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/TestUtils.java                            |   27 
 opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/Rfc7662AccessResolverTestCase.java  |  124 ++++
 opendj-rest2ldap/pom.xml                                                                                |   17 
 opendj-server-legacy/resource/config/http-config.json                                                   |   99 +++
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java             |  141 ++++
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AuthzIdTemplate.java                |  152 +++--
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/CtsAccessTokenResolver.java         |  115 +++
 opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2FilterTest.java        |    2 
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2Filter.java            |    4 
 opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json                   |  107 +++
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/Authorizations.java                 |  176 +++++
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/Rfc7662AccessTokenResolver.java     |  135 ++++
 opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/AuthzIdTemplateTest.java            |   39 
 opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/CtsAccessTokenResolverTestCase.java |  138 ++++
 opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/OAuth2JsonConfigurationTestCase.java      |  238 ++++++++
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/FileAccessTokenResolver.java        |   64 ++
 opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/AuthorizationsTestCase.java               |  131 ++++
 17 files changed, 1,607 insertions(+), 102 deletions(-)

diff --git a/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json
index e352407..dc79d68 100644
--- a/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json
+++ b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json
@@ -70,7 +70,7 @@
         }
     },
 
-    "authorization": { 
+    "authorization": {
 		// The authorization policies to use. Supported policies are "anonymous", "basic" and "oauth2".
         "policies": [ "basic" ],
 
@@ -114,7 +114,7 @@
 
 				// Authentication identity template containing a single %s which will be replaced by the authenticating
                 // user's name. (i.e: u:%s)
-				"authcIdTemplate": "u:%s"
+				"authzIdTemplate": "u:%s"
 			},
 			
 			// Bind to the LDAP server using the resulting DN of a search request. 
@@ -134,7 +134,108 @@
                 "filterTemplate" : "(&(uid=%s)(objectClass=inetOrgPerson))"
 			}
 			// TODO: support for HTTP sessions?
-		}
+		},
+
+        // Use OAuth2 authorization method. If used, LDAP requests will be performed with proxied authorization control.
+        // This field is optional.
+        "oauth2": {
+			// Access tokens associated realm.
+			// This attribute is optional and has a string syntax.
+			"realm": "myrealm",
+
+            // Defines the list of required scopes required to access the service.
+            // This field is required and cannot be empty.
+            "requiredScopes": [ "read", "write", "uid" ],
+
+			// Specify the resolver to use to resolve OAuth2 access token.
+			// This attribute is required and its value must be one of "openam", "rfc7662", "cts".
+			// Note that the JSON object corresponding to this attribute value must be present
+			// and well formed in the "oauth2" JSON attribute.
+			"resolver": "openam",
+
+            // The default authzIdTemplate demonstrates how an authorization DN may be constructed
+			// from the "uid" field in the following example OAuth2 token introspection response:
+            // {
+            //     "token": "access_token_string",
+            //     "uid" : "user.2",
+			//     "userName" : [ "user.2" ]
+            // }
+			// This attribute is required and has a string syntax.
+			// It must start with either 'dn:' or 'u:'.
+			// Note: For the CTS resolver, the following placeholder "{userName/0}" must be part of the template string,
+			//       e.g "authzIdTemplate": "dn:uid={userName/0},ou=People,dc=example,dc=com"
+            "authzIdTemplate": "dn:uid={uid},ou=People,dc=example,dc=com",
+
+            // Configures caching of access token introspection results.
+            // This attribute is optional, if it is not present, no token caching will be performed.
+            "accessTokenCache": {
+                // Indicates whether the access token caching should be used.
+                // This attribute is optional (default value is false) and must have a boolean syntax.
+                "enabled": false,
+
+                // Specifies the maximal caching duration for an access token.
+                // Once this delay is over, token will be refreshed from an access token resolver (see "oauth2/resolver")
+                // This attribute is optional, its default value is "5 minutes".
+                // The duration syntax supports all human readable notations from day ("days", "day", "d")
+                // to nanosecond ("nanoseconds", "nanosecond", "nanosec", "nanos", "nano", "ns")
+                // Any negative or zero values are incorrect.
+                "cacheExpiration": "5 minutes"
+            },
+
+            // The OpenAM access token resolver configuration.
+            // This attribute must be present if the "oauth2/resolver" is equal to "openam".
+            // If "oauth2/resolver" is set to another resolver, this attribute will be ignored.
+            "openam": {
+                // Defines the OpenAM endpoint URL where the request should be sent.
+                // This attribute is required and must have a string syntax.
+                "endpointURL": "http://openam.example.com:8080/openam/oauth2/tokeninfo"
+            },
+
+            // The RFC-7662 (see https://tools.ietf.org/html/rfc7662) access token resolver configuration.
+            // This attribute must be present if the "oauth2/resolver" is equal to "rfc7662".
+            // If "oauth2/resolver" is set to another resolver, this attribute will be ignored.
+            "rfc7662": {
+                // Defines the token introspection endpoint URL where the request should be sent.
+                // This attribute is required and must have a string syntax.
+                "endpointURL": "http:/example.com/introspect",
+
+				// Token introspect endpoint requires authentication.
+				// It should support HTTP basic authorization (a base64-encoded string of clientId:clientSecret)
+				// These attributes are mandatory.
+				"clientId": "client_id",
+				"clientSecret": "client_secret"
+            },
+
+            // The CTS access token resolver.
+            // This attribute must be present if the "oauth2/resolver" is equal to "cts".
+            // If "oauth2/resolver" is set to another resolver, this attribute will be ignored.
+			// Note: You can use {userName/0} in authzIdTemplate configuration to access
+			//       user id from the default CTS access token content config.
+            "cts": {
+				// The connection factory to use to access CTS.
+				// This value is only used in gateway mode.
+				// This attribute must reference a connection factory defined in the "ldapConnectionFactories" section.
+				// Default value: "root" (i.e the root connection factory will be used to access the CTS).
+				"ldapConnectionFactory": "cts",
+
+                // The access token base DN.
+                // This attribute is required and must have a string syntax.
+                "baseDN": "ou=famrecords,ou=openam-session,ou=tokens,dc=example,dc=com"
+            },
+
+            // ONLY FOR TEST PURPOSE: A File based access token resolver
+            // This attribute must be present if the "oauth2/resolver" is equal to "file".
+            // If "oauth2/resolver" is set to another resolver, this attribute will be ignored.
+            "file": {
+                // Directory containing token files.
+                // You can test the rest2ldap OAuth2 authorization support by providing some json token files under
+                // the directory set in the configuration below.
+                // File names must be equal to the token strings.
+				// The file content must a JSON object with the following attributes:
+				// 'scope', 'expireTime' and all the field(s) needed to resolve the authzIdTemplate.
+                "folderPath": "/path/to/test/folder"
+            }
+        }
     },
 
 
diff --git a/opendj-rest2ldap/pom.xml b/opendj-rest2ldap/pom.xml
index 842b3f2..f293562 100644
--- a/opendj-rest2ldap/pom.xml
+++ b/opendj-rest2ldap/pom.xml
@@ -34,6 +34,7 @@
             org.forgerock.opendj.*;provide:=true,
             org.forgerock.json.*;provide:=true
         </opendj.osgi.import.additional>
+        <openig.version>5.0.0-SNAPSHOT</openig.version>
     </properties>
 
 
@@ -54,6 +55,22 @@
         </dependency>
 
         <dependency>
+            <groupId>org.forgerock.commons</groupId>
+            <artifactId>forgerock-util</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.forgerock.openig</groupId>
+            <artifactId>openig-oauth2-resource-server-filter</artifactId>
+            <version>${openig.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.forgerock.http</groupId>
+            <artifactId>chf-client-apache-async</artifactId>
+        </dependency>
+
+        <dependency>
             <groupId>org.forgerock</groupId>
             <artifactId>forgerock-build-tools</artifactId>
             <scope>test</scope>
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java
index 6362c6a..b215dd5 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java
@@ -17,6 +17,7 @@
 package org.forgerock.opendj.rest2ldap;
 
 import static org.forgerock.http.util.Json.readJsonLenient;
+import static org.forgerock.json.JsonValueFunctions.duration;
 import static org.forgerock.json.JsonValueFunctions.enumConstant;
 import static org.forgerock.json.JsonValueFunctions.setOf;
 import static org.forgerock.opendj.rest2ldap.Rest2LDAP.configureConnectionFactory;
@@ -24,11 +25,17 @@
 import static org.forgerock.opendj.rest2ldap.authz.Authorizations.*;
 import static org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.*;
 import static org.forgerock.opendj.rest2ldap.authz.CredentialExtractors.*;
+import static org.forgerock.opendj.rest2ldap.authz.Authorizations.newCtsAccessTokenResolver;
+import static org.forgerock.opendj.rest2ldap.authz.Authorizations.newFileAccessTokenResolver;
+import static org.forgerock.opendj.rest2ldap.authz.Authorizations.newRfc7662AccessTokenResolver;
 import static org.forgerock.util.Reject.checkNotNull;
 import static org.forgerock.util.Utils.closeSilently;
+import static org.forgerock.opendj.rest2ldap.authz.Authorizations.newConditionalOAuth2ResourceServerFilter;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -36,15 +43,25 @@
 import java.util.Map;
 import java.util.Set;
 
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+
+import org.forgerock.authz.modules.oauth2.AccessTokenInfo;
+import org.forgerock.authz.modules.oauth2.AccessTokenException;
+import org.forgerock.authz.modules.oauth2.AccessTokenResolver;
+import org.forgerock.authz.modules.oauth2.cache.CachingAccessTokenResolver;
+import org.forgerock.authz.modules.oauth2.resolver.OpenAmAccessTokenResolver;
 import org.forgerock.http.Filter;
 import org.forgerock.http.Handler;
 import org.forgerock.http.HttpApplication;
 import org.forgerock.http.HttpApplicationException;
 import org.forgerock.http.filter.Filters;
 import org.forgerock.http.handler.Handlers;
+import org.forgerock.http.handler.HttpClientHandler;
 import org.forgerock.http.io.Buffer;
 import org.forgerock.http.protocol.Headers;
 import org.forgerock.json.JsonValue;
+import org.forgerock.json.JsonValueException;
 import org.forgerock.json.resource.RequestHandler;
 import org.forgerock.json.resource.Router;
 import org.forgerock.json.resource.http.CrestHttp;
@@ -59,7 +76,12 @@
 import org.forgerock.util.Factory;
 import org.forgerock.util.Function;
 import org.forgerock.util.Pair;
+import org.forgerock.util.PerItemEvictionStrategyCache;
+import org.forgerock.util.annotations.VisibleForTesting;
 import org.forgerock.util.promise.NeverThrowsException;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.time.Duration;
+import org.forgerock.util.time.TimeService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -68,6 +90,18 @@
     private static final String DEFAULT_ROOT_FACTORY = "root";
     private static final String DEFAULT_BIND_FACTORY = "bind";
 
+    /** Keys for json oauth2 configuration. */
+    private static final String RESOLVER_CONFIG_OBJECT = "resolver";
+    private static final String REALM = "realm";
+    private static final String SCOPES = "requiredScopes";
+    private static final String AUTHZID_TEMPLATE = "authzIdTemplate";
+    private static final String CACHE_EXPIRATION_DEFAULT = "5 minutes";
+
+    /** Keys for json oauth2 access token cache configuration. */
+    private static final String CACHE_CONFIG_OBJECT = "accessTokenCache";
+    private static final String CACHE_ENABLED = "enabled";
+    private static final String CACHE_EXPIRATION = "cacheExpiration";
+
     private static final Logger LOG = LoggerFactory.getLogger(Rest2LDAPHttpApplication.class);
 
     /** URL to the JSON configuration file. */
@@ -77,8 +111,19 @@
     protected final Schema schema;
 
     private final Map<String, ConnectionFactory> connectionFactories = new HashMap<>();
+    /** Used for token caching. */
+    private ScheduledExecutorService executorService;
 
-    private enum Policy { BASIC, ANONYMOUS }
+    /** Define the method which should be used to resolve an OAuth2 access token. */
+    private enum OAuth2ResolverType {
+        RFC7662,
+        OPENAM,
+        CTS,
+        FILE
+    }
+
+    @VisibleForTesting
+    enum Policy { OAUTH2, BASIC, ANONYMOUS }
 
     private enum BindStrategy { SIMPLE, SEARCH, SASL_PLAIN }
 
@@ -107,6 +152,7 @@
     public final Handler start() throws HttpApplicationException {
         try {
             final JsonValue configuration = readJson(configurationUrl);
+            executorService = Executors.newSingleThreadScheduledExecutor();
             configureConnectionFactories(configuration.get("ldapConnectionFactories"));
             return Handlers.chainOf(
                     CrestHttp.newHttpHandler(configureRest2Ldap(configuration)),
@@ -155,11 +201,18 @@
             closeSilently(factory);
         }
         connectionFactories.clear();
+        if (executorService != null) {
+            executorService.shutdown();
+            executorService = null;
+        }
     }
 
-    private Filter buildAuthorizationFilter(final JsonValue config) {
+    private Filter buildAuthorizationFilter(final JsonValue config) throws HttpApplicationException {
         final Set<Policy> policies = config.get("policies").as(setOf(enumConstant(Policy.class)));
         final List<ConditionalFilter> filters = new ArrayList<>(policies.size());
+        if (policies.contains(Policy.OAUTH2)) {
+            filters.add(buildOAuth2Filter(config.get("oauth2")));
+        }
         if (policies.contains(Policy.BASIC)) {
             filters.add(buildBasicFilter(config.get("basic")));
         }
@@ -169,6 +222,88 @@
         return newAuthorizationFilter(filters);
     }
 
+    @VisibleForTesting
+    ConditionalFilter buildOAuth2Filter(final JsonValue config) throws HttpApplicationException {
+        final String realm = config.get(REALM).defaultTo("no_realm").asString();
+        final Set<String> scopes = config.get(SCOPES).required().asSet(String.class);
+        final AccessTokenResolver resolver =
+                createCachedTokenResolverIfNeeded(config, parseUnderlyingResolver(config));
+        final ConditionalFilter oAuth2Filter = newConditionalOAuth2ResourceServerFilter(
+                realm, scopes, resolver, config.get(AUTHZID_TEMPLATE).required().asString());
+        return newConditionalFilter(
+                Filters.chainOf(oAuth2Filter.getFilter(),
+                                newProxyAuthzFilter(getConnectionFactory(DEFAULT_ROOT_FACTORY))),
+                oAuth2Filter.getCondition());
+    }
+
+    @VisibleForTesting
+    AccessTokenResolver createCachedTokenResolverIfNeeded(
+            final JsonValue config, final AccessTokenResolver resolver) {
+        final JsonValue cacheConfig = config.get(CACHE_CONFIG_OBJECT);
+        if (cacheConfig.isNull() || !cacheConfig.get(CACHE_ENABLED).defaultTo(Boolean.FALSE).asBoolean()) {
+            return resolver;
+        }
+        final Duration expiration = parseCacheExpiration(
+                cacheConfig.get(CACHE_EXPIRATION).defaultTo(CACHE_EXPIRATION_DEFAULT));
+
+        final PerItemEvictionStrategyCache<String, Promise<AccessTokenInfo, AccessTokenException>> cache =
+                new PerItemEvictionStrategyCache<>(executorService, expiration);
+        cache.setMaxTimeout(expiration);
+        return new CachingAccessTokenResolver(TimeService.SYSTEM, resolver, cache);
+    }
+
+    @VisibleForTesting
+    AccessTokenResolver parseUnderlyingResolver(final JsonValue configuration) throws HttpApplicationException {
+        final JsonValue resolver = configuration.get(RESOLVER_CONFIG_OBJECT).required();
+        switch (resolver.as(enumConstant(OAuth2ResolverType.class))) {
+        case RFC7662:
+            return parseRfc7662Resolver(configuration);
+        case OPENAM:
+            return new OpenAmAccessTokenResolver(new HttpClientHandler(),
+                                                 TimeService.SYSTEM,
+                                                 configuration.get("openam").get("endpointURL").required().asString());
+        case CTS:
+            final JsonValue cts = configuration.get("cts").required();
+            return newCtsAccessTokenResolver(
+                getConnectionFactory(cts.get("ldapConnectionFactory").defaultTo(DEFAULT_ROOT_FACTORY).asString()),
+                                     cts.get("baseDN").required().asString());
+        case FILE:
+            return newFileAccessTokenResolver(configuration.get("file").get("folderPath").required().asString());
+        default:
+            throw new JsonValueException(resolver, "is not a supported access token resolver");
+        }
+    }
+
+    private AccessTokenResolver parseRfc7662Resolver(final JsonValue configuration) throws HttpApplicationException {
+        final JsonValue rfc7662 = configuration.get("rfc7662").required();
+        final String introspectionEndPointURL = rfc7662.get("endpointURL").required().asString();
+        try {
+            return newRfc7662AccessTokenResolver(new HttpClientHandler(),
+                                                 new URI(introspectionEndPointURL),
+                                                 rfc7662.get("clientId").required().asString(),
+                                                 rfc7662.get("clientSecret").required().asString());
+        } catch (final URISyntaxException e) {
+            throw new IllegalArgumentException("The token introspection endpoint '"
+                    + introspectionEndPointURL + "' URL has an invalid syntax: " + e.getLocalizedMessage(), e);
+        }
+    }
+
+    private Duration parseCacheExpiration(final JsonValue expirationJson) {
+        try {
+            final Duration expiration = expirationJson.as(duration());
+            if (expiration.isZero() || expiration.isUnlimited()) {
+                throw new JsonValueException(expirationJson, "The cache expiration duration cannot be "
+                        + (expiration.isZero() ? "zero" : "unlimited."));
+            }
+            return expiration;
+        } catch (final Exception e) {
+            throw new JsonValueException(expirationJson,
+                      "Malformed duration value '" + expirationJson.toString() + "' for cache expiration. "
+                    + "The duration syntax supports all human readable notations from day ('days'', 'day'', 'd'') "
+                    + "to nanosecond ('nanoseconds', 'nanosecond', 'nanosec', 'nanos', 'nano', 'ns')");
+        }
+    }
+
     /**
      * Creates a new {@link Filter} in charge of injecting {@link AuthenticatedConnectionContext}.
      *
@@ -263,7 +398,7 @@
     private AuthenticationStrategy buildSASLBindStrategy(JsonValue config) {
         return newSASLPlainStrategy(
                 getConnectionFactory(config.get("ldapConnectionFactory").defaultTo(DEFAULT_BIND_FACTORY).asString()),
-                schema, config.get("authcIdTemplate").defaultTo("u:%s").asString());
+                schema, config.get(AUTHZID_TEMPLATE).defaultTo("u:%s").asString());
     }
 
     private AuthenticationStrategy buildSearchThenBindStrategy(JsonValue config) {
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/Authorizations.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/Authorizations.java
index 96a7d36..56f8c25 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/Authorizations.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/Authorizations.java
@@ -17,12 +17,27 @@
 
 import static org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.asConditionalFilter;
 import static org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.newConditionalFilter;
+import static org.forgerock.util.promise.Promises.newResultPromise;
 
+import java.net.URI;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
+import org.forgerock.authz.modules.oauth2.AccessTokenInfo;
+import org.forgerock.authz.modules.oauth2.AccessTokenResolver;
+import org.forgerock.authz.modules.oauth2.OAuth2Context;
+import org.forgerock.authz.modules.oauth2.ResourceAccess;
+import org.forgerock.authz.modules.oauth2.ResourceServerFilter;
 import org.forgerock.http.Filter;
+import org.forgerock.http.Handler;
+import org.forgerock.http.filter.Filters;
 import org.forgerock.http.protocol.Headers;
 import org.forgerock.http.protocol.Request;
+import org.forgerock.http.protocol.Response;
+import org.forgerock.http.protocol.ResponseException;
+import org.forgerock.http.protocol.Status;
 import org.forgerock.opendj.ldap.Connection;
 import org.forgerock.opendj.ldap.ConnectionFactory;
 import org.forgerock.opendj.ldap.controls.ProxiedAuthV2RequestControl;
@@ -33,15 +48,15 @@
 import org.forgerock.services.context.SecurityContext;
 import org.forgerock.util.Function;
 import org.forgerock.util.Pair;
+import org.forgerock.util.Reject;
 import org.forgerock.util.promise.NeverThrowsException;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.time.TimeService;
 
-/**
- * Factory methods to create {@link Filter} performing authentication and authorizations.
- */
+/** Factory methods to create {@link Filter} performing authentication and authorizations. */
 public final class Authorizations {
 
-    private Authorizations() {
-    }
+    private static final String OAUTH2_AUTHORIZATION_HEADER = "Authorization";
 
     /**
      * Creates a new {@link Filter} in charge of injecting an {@link AuthenticatedConnectionContext}. This
@@ -113,4 +128,155 @@
     public static Filter newProxyAuthorizationFilter(ConnectionFactory connectionFactory) {
         return new ProxiedAuthV2Filter(connectionFactory);
     }
+
+    /**
+     * Creates a new {@link AccessTokenResolver} as defined in the RFC-7662.
+     * <p>
+     * @see <a href="https://tools.ietf.org/html/rfc7662">RFC-7662</a>
+     *
+     * @param httpClient
+     *          Http client handler used to perform the request
+     * @param introspectionEndPointURL
+     *          Introspect endpoint URL to use to resolve the access token.
+     * @param clientAppId
+     *          Client application id to use in HTTP Basic authentication header.
+     * @param clientAppSecret
+     *          Client application secret to use in HTTP Basic authentication header.
+     * @return A new {@link AccessTokenResolver} instance.
+     */
+    public static AccessTokenResolver newRfc7662AccessTokenResolver(final Handler httpClient,
+                                                                    final URI introspectionEndPointURL,
+                                                                    final String clientAppId,
+                                                                    final String clientAppSecret) {
+        return new Rfc7662AccessTokenResolver(httpClient, introspectionEndPointURL, clientAppId, clientAppSecret);
+    }
+
+    /**
+     * Creates a new CTS access token resolver.
+     *
+     * @param connectionFactory
+     *          The {@link ConnectionFactory} to use to perform search against the CTS.
+     * @param ctsBaseDNTemplate
+     *          The base DN template to use to resolve the access token DN.
+     * @return A new CTS access token resolver.
+     */
+    public static AccessTokenResolver newCtsAccessTokenResolver(final ConnectionFactory connectionFactory,
+                                                                final String ctsBaseDNTemplate) {
+        return new CtsAccessTokenResolver(connectionFactory, ctsBaseDNTemplate);
+    }
+
+    /**
+     * Creates a new file access token resolver which should only be used for test purpose.
+     *
+     * @param tokenFolder
+     *          The folder where the access token to resolve must be stored.
+     * @return A new file access token resolver which should only be used for test purpose.
+     */
+    public static AccessTokenResolver newFileAccessTokenResolver(final String tokenFolder) {
+        return new FileAccessTokenResolver(tokenFolder);
+    }
+
+    /**
+     * Creates a new OAuth2 authorization filter configured with provided parameters.
+     *
+     * @param realm
+     *          The realm to displays in error responses.
+     * @param scopes
+     *          Scopes that an access token must have to be access a resource.
+     * @param resolver
+     *          The {@link AccessTokenResolver} to use to resolve an access token.
+     * @param authzIdTemplate
+     *          Authorization ID template.
+     * @return A new OAuth2 authorization filter configured with provided parameters.
+     */
+    public static Filter newOAuth2ResourceServerFilter(final String realm,
+                                                                  final Set<String> scopes,
+                                                                  final AccessTokenResolver resolver,
+                                                                  final String authzIdTemplate) {
+        return createResourceServerFilter(realm, scopes, resolver, authzIdTemplate);
+    }
+
+    /**
+     * Creates a new optional OAuth2 authorization filter configured with provided parameters.
+     * <p>
+     * This filter will be used only if an OAuth2 Authorization header is present in the incoming request.
+     *
+     * @param realm
+     *          The realm to displays in error responses.
+     * @param scopes
+     *          Scopes that an access token must have to be access a resource.
+     * @param resolver
+     *          The {@link AccessTokenResolver} to use to resolve an access token.
+     * @param authzIdTemplate
+     *          Authorization ID template.
+     * @return A new OAuth2 authorization filter configured with provided parameters.
+     */
+    public static ConditionalFilter newConditionalOAuth2ResourceServerFilter(final String realm,
+                                                                             final Set<String> scopes,
+                                                                             final AccessTokenResolver resolver,
+                                                                             final String authzIdTemplate) {
+        return new ConditionalFilter() {
+            @Override
+            public Filter getFilter() {
+                return createResourceServerFilter(realm, scopes, resolver, authzIdTemplate);
+            }
+
+            @Override
+            public Condition getCondition() {
+                return new Condition() {
+                    @Override
+                    public boolean canApplyFilter(final Context context, final Request request) {
+                        return request.getHeaders().containsKey(OAUTH2_AUTHORIZATION_HEADER);
+                    }
+                };
+            }
+        };
+    }
+
+    private static Filter createResourceServerFilter(final String realm,
+                                                     final Set<String> scopes,
+                                                     final AccessTokenResolver resolver,
+                                                     final String authzIdTemplate) {
+        Reject.ifTrue(realm == null || realm.isEmpty(), "realm must not be empty");
+        Reject.ifNull(resolver, "Access token resolver must not be null");
+        Reject.ifTrue(scopes == null || scopes.isEmpty(), "scopes set can not be empty");
+        Reject.ifTrue(authzIdTemplate == null || authzIdTemplate.isEmpty(), "Authz id template must not be empty");
+
+        final ResourceAccess scopesProvider = new ResourceAccess() {
+            @Override
+            public Set<String> getRequiredScopes(final Context context, final Request request)
+                    throws ResponseException {
+                return scopes;
+            }
+        };
+
+        return Filters.chainOf(new ResourceServerFilter(resolver, TimeService.SYSTEM, scopesProvider, realm),
+                               createSecurityContextInjectionFilter(authzIdTemplate));
+    }
+
+    private static Filter createSecurityContextInjectionFilter(final String authzIdTemplate) {
+        final AuthzIdTemplate template = new AuthzIdTemplate(authzIdTemplate);
+
+        return new Filter() {
+            @Override
+            public Promise<Response, NeverThrowsException> filter(final Context context,
+                                                                  final Request request,
+                                                                  final Handler next) {
+                final AccessTokenInfo token = context.asContext(OAuth2Context.class).getAccessToken();
+                final Map<String, Object> authz = new HashMap<>(1);
+                try {
+                    authz.put(template.getSecurityContextID(), template.formatAsAuthzId(token.asJsonValue()));
+                } catch (final IllegalArgumentException e) {
+                    return newResultPromise(
+                            new Response().setStatus(Status.FORBIDDEN).setCause(e).setEntity("Invalid configuration"));
+                }
+                final Context securityContext = new SecurityContext(context, token.getToken(), authz);
+                return next.handle(securityContext, request);
+            }
+        };
+    }
+
+    private Authorizations() {
+        // Prevent instantiation.
+    }
 }
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AuthzIdTemplate.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AuthzIdTemplate.java
index 6040d0a..589bb5b 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AuthzIdTemplate.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AuthzIdTemplate.java
@@ -15,15 +15,19 @@
  */
 package org.forgerock.opendj.rest2ldap.authz;
 
+import static org.forgerock.util.Utils.joinAsString;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
-import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import org.forgerock.json.JsonPointer;
+import org.forgerock.json.JsonValue;
 import org.forgerock.opendj.ldap.DN;
 import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.services.context.SecurityContext;
 
 /**
  * An authorization ID template used for mapping security context principals to
@@ -32,56 +36,77 @@
  * <code>u:{uid}@{realm}.example.com</code>.
  */
 final class AuthzIdTemplate {
-    private static interface Impl {
-        String formatAsAuthzId(AuthzIdTemplate t, Object[] templateVariables, Schema schema);
+
+    private interface Impl {
+        String formatAsAuthzId(AuthzIdTemplate t, Object[] templateVariables);
     }
 
-    private static final Impl DN_IMPL = new Impl() {
-
-        @Override
-        public String formatAsAuthzId(final AuthzIdTemplate t, final Object[] templateVariables,
-                final Schema schema) {
-            final String authzId = String.format(Locale.ENGLISH, t.formatString, templateVariables);
-            try {
-                // Validate the DN.
-                DN.valueOf(authzId.substring(3), schema);
-            } catch (final IllegalArgumentException e) {
-                throw new IllegalArgumentException(
-                        "The request could not be authorized because the required security principal "
-                        + "was not a valid LDAP DN");
-            }
-            return authzId;
-        }
-    };
-
-    private static final Pattern DN_PATTERN = Pattern.compile("^dn:\\{[^}]+\\}$");
-
     private static final Impl DN_TEMPLATE_IMPL = new Impl() {
-
         @Override
-        public String formatAsAuthzId(final AuthzIdTemplate t, final Object[] templateVariables,
-                final Schema schema) {
-            return "dn:" + DN.format(t.dnFormatString, schema, templateVariables);
+        public String formatAsAuthzId(final AuthzIdTemplate t, final Object[] templateVariables) {
+            // We're not interested in matching and place-holder attribute types can be tolerated,
+            // so we can just use the core schema.
+            return DN.format(t.formatString, Schema.getCoreSchema(), templateVariables).toString();
         }
-
     };
 
-    private static final Pattern KEY_RE = Pattern.compile("\\{([^}]+)\\}");
-
     private static final Impl UID_TEMPLATE_IMPL = new Impl() {
-
         @Override
-        public String formatAsAuthzId(final AuthzIdTemplate t, final Object[] templateVariables,
-                final Schema schema) {
+        public String formatAsAuthzId(final AuthzIdTemplate t, final Object[] templateVariables) {
             return String.format(Locale.ENGLISH, t.formatString, templateVariables);
         }
-
     };
 
-    private final String dnFormatString;
+    private static final Pattern TEMPLATE_KEY_RE = Pattern.compile("\\{([^}]+)\\}");
+
+    private enum TemplateType {
+        DN ("dn:", SecurityContext.AUTHZID_DN, DN_TEMPLATE_IMPL),
+        UID ("u:", SecurityContext.AUTHZID_ID, UID_TEMPLATE_IMPL);
+
+        private final String key;
+        private final String securityContextId;
+        private final Impl impl;
+
+        TemplateType(final String key, final String securityContextId, final Impl impl) {
+            this.key = key;
+            this.securityContextId = securityContextId;
+            this.impl = impl;
+        }
+
+        private String getSecurityContextId() {
+            return securityContextId;
+        }
+
+        private Impl getImpl() {
+            return impl;
+        }
+
+        private static TemplateType parseTemplateType(final String template) {
+            for (final TemplateType type : TemplateType.values()) {
+                if (template.startsWith(type.key)) {
+                    return type;
+                }
+            }
+            throw new IllegalArgumentException("Invalid authorization ID template: '" + template + "'. Templates must "
+                       + "start with one of the following elements: " + joinAsString(",", getSupportedStartKeys()));
+        }
+
+        private static List<String> getSupportedStartKeys() {
+            final List<String> startKeys = new ArrayList<>();
+            for (final TemplateType type : TemplateType.values()) {
+                startKeys.add(type.key);
+            }
+            return startKeys;
+        }
+
+        private String removeTemplateKey(final String formattedString) {
+            return formattedString.substring(key.length()).trim();
+        }
+    }
+
+    private final TemplateType type;
     private final String formatString;
     private final List<String> keys = new ArrayList<>();
-    private final Impl pimpl;
     private final String template;
 
     /**
@@ -92,29 +117,22 @@
      * @throws IllegalArgumentException
      *             if template doesn't start with "u:" or "dn:"
      */
-    public AuthzIdTemplate(final String template) {
-        if (!template.startsWith("u:") && !template.startsWith("dn:")) {
-            throw new IllegalArgumentException("Invalid authorization ID template: " + template);
-        }
+    AuthzIdTemplate(final String template) {
+        this.type = TemplateType.parseTemplateType(template);
+        this.formatString = formatTemplate(template);
+        this.template = template;
+    }
 
+    private String formatTemplate(final String template) {
         // Parse the template keys and replace them with %s for formatting.
-        final Matcher matcher = KEY_RE.matcher(template);
+        final Matcher matcher = TEMPLATE_KEY_RE.matcher(template);
         final StringBuffer buffer = new StringBuffer(template.length());
         while (matcher.find()) {
             matcher.appendReplacement(buffer, "%s");
             keys.add(matcher.group(1));
         }
         matcher.appendTail(buffer);
-        this.formatString = buffer.toString();
-        this.template = template;
-
-        if (template.startsWith("dn:")) {
-            this.pimpl = DN_PATTERN.matcher(template).matches() ? DN_IMPL : DN_TEMPLATE_IMPL;
-            this.dnFormatString = formatString.substring(3);
-        } else {
-            this.pimpl = UID_TEMPLATE_IMPL;
-            this.dnFormatString = null;
-        }
+        return type.removeTemplateKey(buffer.toString());
     }
 
     @Override
@@ -122,41 +140,45 @@
         return template;
     }
 
+    String getSecurityContextID() {
+        return this.type.getSecurityContextId();
+    }
+
     /**
      * Return the template with all the variable replaced.
      *
      * @param principals
      *            Value to use to replace the variables.
-     * @param schema
-     *            Schema to perform validation.
      * @return The template with all the variable replaced.
      */
-    public String formatAsAuthzId(final Map<String, Object> principals, final Schema schema) {
+    String formatAsAuthzId(final JsonValue principals) {
         final String[] templateVariables = getPrincipalsForFormatting(principals);
-        return pimpl.formatAsAuthzId(this, templateVariables, schema);
+        return type.getImpl().formatAsAuthzId(this, templateVariables);
     }
 
-    private String[] getPrincipalsForFormatting(final Map<String, Object> principals) {
+    private String[] getPrincipalsForFormatting(final JsonValue principals) {
         final String[] values = new String[keys.size()];
         for (int i = 0; i < values.length; i++) {
             final String key = keys.get(i);
-            final Object value = principals.get(key);
-            if (isJSONPrimitive(value)) {
-                values[i] = String.valueOf(value);
-            } else if (value != null) {
-                throw new IllegalArgumentException(String.format(
-                        "The request could not be authorized because the required "
-                                + "security principal '%s' had an invalid data type", key));
-            } else {
+            final JsonValue value = principals.get(new JsonPointer(key));
+            if (value == null) {
                 throw new IllegalArgumentException(String.format(
                         "The request could not be authorized because the required "
                                 + "security principal '%s' could not be determined", key));
             }
+
+            final Object object = value.getObject();
+            if (!isJSONPrimitive(object)) {
+                throw new IllegalArgumentException(String.format(
+                        "The request could not be authorized because the required "
+                                + "security principal '%s' had an invalid data type", key));
+            }
+            values[i] = String.valueOf(object);
         }
         return values;
     }
 
-    static boolean isJSONPrimitive(final Object value) {
+    private boolean isJSONPrimitive(final Object value) {
         return value instanceof String || value instanceof Boolean || value instanceof Number;
     }
 }
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/CtsAccessTokenResolver.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/CtsAccessTokenResolver.java
new file mode 100644
index 0000000..253726a
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/CtsAccessTokenResolver.java
@@ -0,0 +1,115 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap.authz;
+
+import static org.forgerock.opendj.ldap.requests.Requests.newSingleEntrySearchRequest;
+import static org.forgerock.opendj.rest2ldap.authz.Utils.close;
+import static org.forgerock.util.Reject.checkNotNull;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.forgerock.authz.modules.oauth2.AccessTokenInfo;
+import org.forgerock.authz.modules.oauth2.AccessTokenException;
+import org.forgerock.authz.modules.oauth2.AccessTokenResolver;
+import org.forgerock.http.util.Json;
+import org.forgerock.json.JsonValue;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.services.context.Context;
+import org.forgerock.util.AsyncFunction;
+import org.forgerock.util.Function;
+import org.forgerock.util.promise.Promise;
+
+/**
+ * This class resolves an access token in order to get the {@link AccessTokenInfo}
+ * by performing a request to an OpenDJ server.
+ */
+final class CtsAccessTokenResolver implements AccessTokenResolver {
+
+    private static final Filter FR_CORE_TOKEN_OC_FILTER = Filter.equality("objectclass", "frCoreToken");
+
+    private final ConnectionFactory connectionFactory;
+    private final DN ctsBaseDN;
+
+    CtsAccessTokenResolver(final ConnectionFactory connectionFactory, final String ctsBaseDN) {
+        this.connectionFactory = checkNotNull(connectionFactory, "connectionFactory cannot be null");
+        this.ctsBaseDN = DN.valueOf(checkNotNull(ctsBaseDN, "ctsBaseDN cannot be null"));
+    }
+
+    @Override
+    public Promise<AccessTokenInfo, AccessTokenException> resolve(final Context context, final String token) {
+        final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
+
+        return connectionFactory
+            .getConnectionAsync()
+            .thenAsync(new AsyncFunction<Connection, SearchResultEntry, LdapException>() {
+                @Override
+                public Promise<SearchResultEntry, LdapException> apply(final Connection connection)
+                        throws LdapException {
+                    connectionHolder.set(connection);
+                    return connection.searchSingleEntryAsync(newSingleEntrySearchRequest(
+                            ctsBaseDN.child("coreTokenId", token),
+                            SearchScope.BASE_OBJECT, FR_CORE_TOKEN_OC_FILTER, "coreTokenObject"));
+                }
+            }).then(new Function<SearchResultEntry, AccessTokenInfo, AccessTokenException>() {
+                @Override
+                public AccessTokenInfo apply(final SearchResultEntry entry) throws AccessTokenException {
+                    final JsonValue accessToken = parseJson(
+                            entry.getAttribute("coreTokenObject").firstValueAsString(), token);
+
+                    final String tokenName = getRequiredFirstValue(accessToken.get("tokenName"));
+                    if (!tokenName.equals("access_token")) {
+                        throw new AccessTokenException(
+                                "The token '" + token + "' must be an access token, but it is a \"" + tokenName + "\"");
+                    }
+
+                    return new AccessTokenInfo(accessToken, token,
+                            accessToken.get("scope").required().asSet(String.class),
+                            Long.parseLong(getRequiredFirstValue(accessToken.get("expireTime"))));
+                }
+            }, new Function<LdapException, AccessTokenInfo, AccessTokenException>() {
+                @Override
+                public AccessTokenInfo apply(final LdapException e) throws AccessTokenException {
+                    throw new AccessTokenException("Unable to find the token '" + token + "' in the CTS because: "
+                            + e.getMessage(), e);
+                }
+            }).thenCatchRuntimeException(new Function<RuntimeException, AccessTokenInfo, AccessTokenException>() {
+                @Override
+                public AccessTokenInfo apply(final RuntimeException e) throws AccessTokenException {
+                    throw new AccessTokenException("Unable to resolve access token '" + token
+                            + "' due to the following reason: " + e.getMessage(), e);
+                }
+            }).thenFinally(close(connectionHolder));
+    }
+
+    private String getRequiredFirstValue(final JsonValue list) {
+        return list.required().asList(String.class).get(0);
+    }
+
+    private JsonValue parseJson(final String accessTokenJson, final String token) throws AccessTokenException {
+        try {
+            return new JsonValue(Json.readJson(accessTokenJson));
+        } catch (final IOException e) {
+            throw new AccessTokenException("Json of token '" + token + "' is malformed");
+        }
+    }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/FileAccessTokenResolver.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/FileAccessTokenResolver.java
new file mode 100644
index 0000000..dca7987
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/FileAccessTokenResolver.java
@@ -0,0 +1,64 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap.authz;
+
+import static org.forgerock.util.Reject.checkNotNull;
+import static org.forgerock.util.promise.Promises.newExceptionPromise;
+import static org.forgerock.util.promise.Promises.newResultPromise;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.forgerock.authz.modules.oauth2.AccessTokenInfo;
+import org.forgerock.authz.modules.oauth2.AccessTokenException;
+import org.forgerock.authz.modules.oauth2.AccessTokenResolver;
+import org.forgerock.http.util.Json;
+import org.forgerock.json.JsonValue;
+import org.forgerock.json.JsonValueException;
+import org.forgerock.services.context.Context;
+import org.forgerock.util.promise.Promise;
+
+/** A file access token resolver which should only be used for test purpose.*/
+final class FileAccessTokenResolver implements AccessTokenResolver {
+
+    private final String folderPath;
+
+    FileAccessTokenResolver(final String folderPath) {
+        this.folderPath = checkNotNull(folderPath);
+    }
+
+    @Override
+    public Promise<AccessTokenInfo, AccessTokenException> resolve(final Context context, final String token) {
+        final JsonValue accessToken;
+        try (final InputStream stream = new FileInputStream(new File(folderPath, token))) {
+            accessToken = new JsonValue(Json.readJsonLenient(stream));
+        } catch (final IOException e) {
+            return newExceptionPromise(new AccessTokenException("Unable to find token file '" + token + "'", e));
+        }
+
+        try {
+            final AccessTokenInfo result = new AccessTokenInfo(accessToken, token,
+                    accessToken.get("scope").required().asSet(String.class),
+                    accessToken.get("expireTime").required().asLong());
+            return newResultPromise(result);
+        } catch (final JsonValueException e) {
+            return newExceptionPromise(
+                    new AccessTokenException("Malformed token file '" + token + "': '" + e.getMessage() + "'.", e));
+        }
+    }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2Filter.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2Filter.java
index db76fb9..b8d3451 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2Filter.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2Filter.java
@@ -71,12 +71,10 @@
      *
      * @param connectionFactory
      *            Factory used to get the {@link Connection}
-     * @param authzIdProvider
-     *            Function in charge of providing the authzid to use for the ProxiedAuth control
      * @throws NullPointerException
      *             If a parameter is null
      */
-    public ProxiedAuthV2Filter(final ConnectionFactory connectionFactory) {
+    ProxiedAuthV2Filter(final ConnectionFactory connectionFactory) {
         this.connectionFactory = checkNotNull(connectionFactory, "connectionFactory cannot be null");
     }
 
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/Rfc7662AccessTokenResolver.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/Rfc7662AccessTokenResolver.java
new file mode 100644
index 0000000..4277291
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/Rfc7662AccessTokenResolver.java
@@ -0,0 +1,135 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap.authz;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.forgerock.util.Reject.checkNotNull;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.forgerock.authz.modules.oauth2.AccessTokenInfo;
+import org.forgerock.authz.modules.oauth2.AccessTokenException;
+import org.forgerock.authz.modules.oauth2.AccessTokenResolver;
+import org.forgerock.http.Handler;
+import org.forgerock.http.protocol.Responses;
+import org.forgerock.http.protocol.Entity;
+import org.forgerock.http.protocol.Form;
+import org.forgerock.http.protocol.Headers;
+import org.forgerock.http.protocol.Request;
+import org.forgerock.http.protocol.Response;
+import org.forgerock.http.protocol.Status;
+import org.forgerock.json.JsonValue;
+import org.forgerock.json.JsonValueException;
+import org.forgerock.services.context.Context;
+import org.forgerock.util.Function;
+import org.forgerock.util.encode.Base64;
+import org.forgerock.util.promise.Promise;
+
+/**
+ * An {@link AccessTokenResolver} which is RFC 7662 compliant.
+ * <p>
+ * @see <a href="https://tools.ietf.org/html/rfc7662">RFC-7662</a>
+ */
+final class Rfc7662AccessTokenResolver implements AccessTokenResolver {
+    /** RFC 7662 defined fields for token introspection request. */
+    private static final String RFC_7662_FORM_TOKEN_FIELD = "token";
+    private static final String RFC_7662_FORM_TOKEN_TYPE_HINT_FIELD = "token_type_hint";
+    private static final String RFC_7662_FORM_TOKEN_TYPE_HINT_ACCESS_TOKEN = "access_token";
+
+    /** RFC 7662 defined fields for token introspection response. */
+    private static final String RFC_7662_RESPONSE_SCOPE_FIELD = "scope";
+    private static final String RFC_7662_RESPONSE_EXPIRE_TIME_FIELD = "exp";
+
+    /** Rest2Ldap fields name for the RFC 7662 access token resolver. */
+    private static final String RFC_7662_RESPONSE_ACTIVE_FIELD = "active";
+
+    private final Handler httpClient;
+    private final URI introspectionEndPointURI;
+    private final String clientAppId;
+    private final String clientAppSecret;
+
+    Rfc7662AccessTokenResolver(final Handler httpClient,
+                               final URI introspectionEndPointURI,
+                               final String clientAppId,
+                               final String clientAppSecret) {
+        this.httpClient = checkNotNull(httpClient);
+        this.introspectionEndPointURI = checkNotNull(introspectionEndPointURI);
+        this.clientAppId = checkNotNull(clientAppId);
+        this.clientAppSecret = checkNotNull(clientAppSecret);
+    }
+
+    @Override
+    public Promise<AccessTokenInfo, AccessTokenException> resolve(final Context context, final String token) {
+        final Request request = new Request().setUri(introspectionEndPointURI);
+
+        final Headers headers = request.getHeaders();
+        headers.put("Accept", "application/json");
+        headers.put("Authorization", "Basic " + Base64.encode((clientAppId + ":" + clientAppSecret).getBytes()));
+
+        final Form form = new Form();
+        form.add(RFC_7662_FORM_TOKEN_FIELD, token);
+        form.add(RFC_7662_FORM_TOKEN_TYPE_HINT_FIELD, RFC_7662_FORM_TOKEN_TYPE_HINT_ACCESS_TOKEN);
+        form.toRequestEntity(request);
+
+        return httpClient.handle(context, request)
+                         .then(buildAccessToken(token),
+                               Responses.<AccessTokenInfo, AccessTokenException> noopExceptionFunction());
+    }
+
+    private Function<Response, AccessTokenInfo, AccessTokenException> buildAccessToken(final String tokenSent) {
+        return new Function<Response, AccessTokenInfo, AccessTokenException>() {
+            @Override
+            public AccessTokenInfo apply(final Response response) throws AccessTokenException {
+                final Status status = response.getStatus();
+                if (!Status.OK.equals(status)) {
+                    throw new AccessTokenException(
+                            "Authorization server returned an error: " + status, response.getCause());
+                }
+
+                try (final Entity entity = response.getEntity()) {
+                    final JsonValue jsonResponse = asJson(entity);
+                    if (!jsonResponse.get(RFC_7662_RESPONSE_ACTIVE_FIELD).defaultTo(Boolean.FALSE).asBoolean()) {
+                        throw new AccessTokenException(
+                                "Access token returned by authorization server is not currently active");
+                    }
+                    return buildAccessTokenFromJson(jsonResponse, tokenSent);
+                } catch (final JsonValueException e) {
+                    throw new AccessTokenException("Invalid or malformed access token: " + e.getMessage(), e);
+                }
+            }
+        };
+    }
+
+    private AccessTokenInfo buildAccessTokenFromJson(final JsonValue jsonToken, final String token) {
+        final Set<String> tokenScopes = new HashSet<>(Arrays.asList(
+                jsonToken.get(RFC_7662_RESPONSE_SCOPE_FIELD).required().asString().trim().split(" +")));
+        final Long expiresAt = jsonToken.get(RFC_7662_RESPONSE_EXPIRE_TIME_FIELD).required().asLong();
+        return new AccessTokenInfo(jsonToken, token, tokenScopes, SECONDS.toMillis(expiresAt));
+    }
+
+    private JsonValue asJson(final Entity entity) throws AccessTokenException {
+        try {
+            return new JsonValue(entity.getJson());
+        } catch (final IOException e) {
+            // Do not use Entity.toString(), we probably don't want to fully output the content here
+            throw new AccessTokenException("Cannot read response content as JSON", e);
+        }
+    }
+}
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/AuthorizationsTestCase.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/AuthorizationsTestCase.java
new file mode 100644
index 0000000..71b1cea
--- /dev/null
+++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/AuthorizationsTestCase.java
@@ -0,0 +1,131 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.forgerock.authz.modules.oauth2.AccessTokenInfo;
+import org.forgerock.authz.modules.oauth2.AccessTokenException;
+import org.forgerock.authz.modules.oauth2.AccessTokenResolver;
+import org.forgerock.http.Handler;
+import org.forgerock.http.protocol.Request;
+import org.forgerock.json.JsonValue;
+import org.forgerock.opendj.rest2ldap.authz.Authorizations;
+import org.forgerock.opendj.rest2ldap.authz.ConditionalFilters;
+import org.forgerock.services.context.AttributesContext;
+import org.forgerock.services.context.Context;
+import org.forgerock.services.context.RootContext;
+import org.forgerock.services.context.SecurityContext;
+import org.forgerock.testng.ForgeRockTestCase;
+import org.forgerock.util.promise.Promises;
+import org.forgerock.util.time.TimeService;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+@Test
+@SuppressWarnings("javadoc")
+public final class AuthorizationsTestCase extends ForgeRockTestCase {
+
+    private static final String TEST_TOKEN = "2YotnFZFEjr1zCsicMWpAA";
+
+    private Context context;
+
+    @Mock
+    private AccessTokenResolver resolver;
+
+    @Mock
+    private Handler next;
+
+    @Captor
+    private ArgumentCaptor<Request> requestCapture;
+
+    @Captor
+    private ArgumentCaptor<Context> contextCapture;
+
+    @BeforeMethod
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @DataProvider
+    private Object[][] basicUseCaseData() {
+        return new Object[][] {
+            { "dn:uid={user/id},dc=com", "uid=bjensen,dc=com", SecurityContext.AUTHZID_DN },
+            { "u:{user/id}", "bjensen", SecurityContext.AUTHZID_ID }
+        };
+    }
+
+    @Test(dataProvider = "basicUseCaseData")
+    public void testBasicUseCase(final String template,
+                                 final String resolvedTemplate,
+                                 final String securityContextKey) throws Exception {
+        newContextChain();
+        prepareResolverResponse();
+
+        final ConditionalFilters.ConditionalFilter filter = Authorizations.newConditionalOAuth2ResourceServerFilter(
+                "realm",
+                new HashSet<>(Arrays.asList("read", "write", "dolphin")),
+                resolver,
+                template
+        );
+        final Request request = new Request();
+        request.getHeaders().add("Authorization", "Bearer " + TEST_TOKEN);
+        filter.getFilter().filter(context, request, next);
+
+        verify(next).handle(contextCapture.capture(), requestCapture.capture());
+        assertThat(requestCapture.getValue()).isEqualTo(request);
+
+        final Context responseContext = contextCapture.getValue();
+        assertThat(responseContext.asContext(AttributesContext.class).getAttributes().get("test")).isEqualTo("value");
+        final String resolvedDn = responseContext.asContext(SecurityContext.class).getAuthorization()
+                                                                                  .get(securityContextKey)
+                                                                                  .toString();
+        assertThat(resolvedDn).isEqualTo(resolvedTemplate);
+    }
+
+    private void prepareResolverResponse() {
+        final Map<String, Object> jsonResponse = new HashMap<>();
+        final Set<String> scopes = new HashSet<>(Arrays.asList("read", "write", "dolphin"));
+        jsonResponse.put("access_token", TEST_TOKEN);
+        jsonResponse.put("expires_in", 120000);
+        jsonResponse.put("scope", scopes);
+        jsonResponse.put("user", Collections.singletonMap("id", "bjensen"));
+        final AccessTokenInfo token = new AccessTokenInfo(
+                new JsonValue(jsonResponse), TEST_TOKEN, scopes, TimeService.SYSTEM.now() + 120000);
+        when(resolver.resolve(eq(context), eq(TEST_TOKEN)))
+                .thenReturn(Promises.<AccessTokenInfo, AccessTokenException> newResultPromise(token));
+    }
+
+    private void newContextChain() {
+        context = new AttributesContext(new RootContext());
+        context.asContext(AttributesContext.class).getAttributes().put("test", "value");
+    }
+}
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/OAuth2JsonConfigurationTestCase.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/OAuth2JsonConfigurationTestCase.java
new file mode 100644
index 0000000..72c29b4
--- /dev/null
+++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/OAuth2JsonConfigurationTestCase.java
@@ -0,0 +1,238 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.forgerock.opendj.rest2ldap.TestUtils.parseJson;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import org.forgerock.authz.modules.oauth2.AccessTokenResolver;
+import org.forgerock.json.JsonValueException;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.testng.ForgeRockTestCase;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+@Test
+@SuppressWarnings("javadoc")
+public class OAuth2JsonConfigurationTestCase extends ForgeRockTestCase {
+
+    @Mock
+    private AccessTokenResolver resolver;
+
+    @Spy
+    private Rest2LDAPHttpApplication fakeApp;
+
+    @BeforeMethod
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        fakeApp = spy(Rest2LDAPHttpApplication.class);
+    }
+
+    @DataProvider
+    public Object[][] invalidOAuth2Configurations() {
+        // @Checkstyle:off
+        return new Object[][] {
+                // Missing 'realm'
+                {
+                        "{}",
+                },
+                // Missing 'authzIdTemplate'
+                {
+                        "{'realm': 'example.com'}",
+                },
+                // Missing 'requiredScopes'
+                {
+                        "{'realm': 'example.com', "
+                                + "'authzIdTemplate': 'dn: ou={/user/id},dc=example,dc=com'}",
+                },
+                // Missing 'resolver'
+                {
+                        "{'realm': 'example.com',"
+                                + "'authzIdTemplate': 'dn: ou={/user/id},dc=example,dc=com',"
+                                + "'requiredScopes': ['read', 'write', 'dolphin']}",
+                },
+                // Missing 'openam/endpointURL'
+                {
+                        "{'realm': 'example.com',"
+                                + "'authzIdTemplate': 'dn: ou={/user/id},dc=example,dc=com',"
+                                + "'requiredScopes': ['read', 'write', 'dolphin'],"
+                                + "'resolver': 'openam',"
+                                + "'openam': {}}",
+                },
+                // Invalid 'authzIdTemplate' content
+                {
+                        "{'realm': 'example.com',"
+                                + "'authzIdTemplate': 'userName: ou={/user/id},dc=example,dc=com',"
+                                + "'requiredScopes': ['read', 'write', 'dolphin'],"
+                                + "'resolver': 'openam',"
+                                + "'openam': {'endpointURL': 'http://www.example.com/token-info'},"
+                                + "'accessTokenCache': {'enabled': true, 'cacheExpiration': '42'}}",
+                },
+                // Invalid 'accessTokenCache/expiration' duration
+                {
+                        "{'realm': 'example.com',"
+                                + "'authzIdTemplate': 'dn: ou={/user/id},dc=example,dc=com',"
+                                + "'requiredScopes': ['read', 'write', 'dolphin'],"
+                                + "'resolver': 'openam',"
+                                + "'openam': {'endpointURL': 'http://www.example.com/token-info'},"
+                                + "'accessTokenCache': {'enabled': true, 'cacheExpiration': '42'}}",
+                }
+        };
+        // @Checkstyle:on
+    }
+
+    @Test(dataProvider = "invalidOAuth2Configurations", expectedExceptions = JsonValueException.class)
+    public void testInvalidOauth2Configurations(final String rawJson) throws Exception {
+        fakeApp.buildOAuth2Filter(parseJson(rawJson));
+    }
+
+    @Test(expectedExceptions = IllegalArgumentException.class,
+          expectedExceptionsMessageRegExp = ".*scopes set can not be empty.*")
+    public void testOAuth2FilterWithEmptyScopes() throws Exception {
+        final String config =
+            "{'realm': 'example.com',"
+                    + "'authzIdTemplate': 'dn: ou={/user/id},dc=example,dc=com',"
+                    + "'requiredScopes': [],"
+                    + "'resolver': 'openam',"
+                    + "'openam': {'endpointURL': 'http://www.example.com/token-info'}}";
+        fakeApp.buildOAuth2Filter(parseJson(config));
+    }
+
+    @DataProvider
+    public Object[][] invalidResolverConfigurations() {
+        // @Checkstyle:off
+        return new Object[][] {
+            {
+                    "{}",
+            },
+            {
+                    "{'resolver': 'rfc7662',"
+                            + "'rfc7662': {}}",
+            },
+            {
+                    "{'resolver': 'rfc7662',"
+                            + "'rfc7662': { 'endpointURL': 'http:/example.com/introspect'}}",
+            },
+            {
+                    "{'resolver': 'rfc7662',"
+                            + "'rfc7662': { 'endpointURL': 'http:/example.com/introspect',"
+                            + "               'clientId': 'client_app_id'}}",
+            },
+            {
+                    "{'resolver': 'openam',"
+                            + "'openam': {}}",
+            },
+            {
+                    "{'resolver': 'cts',"
+                            + "'cts': {}}",
+            },
+            {
+                    "{'resolver': 'file',"
+                            + "'file': {}}",
+            }
+        };
+        // @Checkstyle:on
+    }
+
+    @Test(dataProvider = "invalidResolverConfigurations", expectedExceptions = JsonValueException.class)
+    public void testInvalidResolverConfigurations(final String rawJson) throws Exception {
+        fakeApp.parseUnderlyingResolver(parseJson(rawJson));
+    }
+
+    @DataProvider
+    public Object[][] invalidCacheResolverConfigurations() {
+        // @Checkstyle:off
+        return new Object[][] {
+                {
+                        "{'accessTokenCache': {"
+                                + "'enabled': true,"
+                                + "'cacheExpiration': '0 minutes'}}",
+                },
+                {
+                        "{'accessTokenCache': {"
+                                + "'enabled': true,"
+                                + "'cacheExpiration': 'lorem ipsum'}}",
+                }
+        };
+        // @Checkstyle:on
+    }
+
+    @Test(dataProvider = "invalidCacheResolverConfigurations", expectedExceptions = JsonValueException.class)
+    public void testInvalidCacheResolverConfigurations(final String rawJson) throws Exception {
+        fakeApp.createCachedTokenResolverIfNeeded(parseJson(rawJson), resolver);
+    }
+
+    @DataProvider
+    public Object[][] ingnoredCacheResolverConfigurations() {
+        // @Checkstyle:off
+        return new Object[][] {
+                {
+                        "{}"
+                },
+                {
+                        "{'accessTokenCache': {"
+                                + "'enabled': false,"
+                                + "'cacheExpiration': '5 minutes'}}"
+                }
+        };
+        // @Checkstyle:on
+    }
+
+    @Test(dataProvider = "ingnoredCacheResolverConfigurations")
+    public void testNoCacheFallbackOnResolver(final String rawJson) throws Exception {
+        assertThat(fakeApp.createCachedTokenResolverIfNeeded(parseJson(rawJson), resolver)).isEqualTo(resolver);
+    }
+
+    @DataProvider
+    public Object[][] validResolverConfigurations() {
+        // @Checkstyle:off
+        return new Object[][] {
+                {
+                        "{'resolver': 'rfc7662',"
+                                + "'rfc7662': { 'endpointURL': 'http:/example.com/introspect',"
+                                + "               'clientId': 'client_app_id',"
+                                + "               'clientSecret': 'client_app_secret'}}"
+                },
+                {
+                        "{'resolver': 'openam',"
+                                + "'openam': { 'endpointURL': 'http:/example.com/tokeninfo'}}"
+                },
+                {
+                        "{'resolver': 'cts',"
+                                + "'cts': { 'baseDN': 'coreTokenId={token},dc=com' }}"
+                },
+                {
+                        "{'resolver': 'file',"
+                                + "'file': { 'folderPath': '/path/to/test/folder'}}"
+                }
+        };
+        // @Checkstyle:on
+    }
+
+    @Test(dataProvider = "validResolverConfigurations")
+    public void testValidResolverConfiguration(final String rawJson) throws Exception {
+        when(fakeApp.getConnectionFactory(eq("root"))).thenReturn(mock(ConnectionFactory.class));
+        assertThat(fakeApp.parseUnderlyingResolver(parseJson(rawJson))).isNotNull();
+    }
+}
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/TestUtils.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/TestUtils.java
index f436de2..fa39d16 100644
--- a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/TestUtils.java
+++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/TestUtils.java
@@ -11,15 +11,18 @@
  * Header, with the fields enclosed by brackets [] replaced by your own identifying
  * information: "Portions copyright [year] [name of copyright owner]".
  *
- * Copyright 2013-2015 ForgeRock AS.
+ * Copyright 2013-2016 ForgeRock AS.
  */
 package org.forgerock.opendj.rest2ldap;
 
 import static org.forgerock.json.JsonValue.json;
 
+import java.io.IOException;
+import java.io.StringReader;
 import java.util.ArrayList;
 import java.util.List;
 
+import org.forgerock.http.util.Json;
 import org.forgerock.json.JsonPointer;
 import org.forgerock.json.JsonValue;
 import org.forgerock.json.resource.ResourceResponse;
@@ -89,6 +92,28 @@
         return result;
     }
 
+    /**
+     * Return {@link JsonValue} corresponding to the provided json blob.
+     *
+     * @param jsonStr
+     *          JSON blob.
+     * @return A {@link JsonValue} corresponding to the provided json blob.
+     */
+    public static JsonValue parseJson(final String jsonStr) throws IOException {
+        return new JsonValue(Json.readJsonLenient(new StringReader(toValidJson(jsonStr))));
+    }
+
+    /**
+     * Allows usage of single quote character in json string used in unit tests.
+     *
+     * @param jsonStr
+     *          The json string to convert to valid json.
+     * @return A Json compliant string.
+     */
+    public static String toValidJson(final String jsonStr) {
+        return jsonStr.replace("'", "\"");
+    }
+
     private TestUtils() {
         // Prevent instantiation.
     }
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/AuthzIdTemplateTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/AuthzIdTemplateTest.java
index 3d7e099..bfec765 100644
--- a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/AuthzIdTemplateTest.java
+++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/AuthzIdTemplateTest.java
@@ -20,7 +20,7 @@
 import java.util.LinkedHashMap;
 import java.util.Map;
 
-import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.json.JsonValue;
 import org.forgerock.testng.ForgeRockTestCase;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
@@ -35,46 +35,47 @@
     @DataProvider
     public Object[][] templateData() {
         // @formatter:off
+        // [template as set in json configuration file]
+        // [excepted result after placeholders resolution]
+        // [Values to use to resolve the placeholders]
         return new Object[][] {
             {
                 "dn:uid={uid},ou={realm},dc=example,dc=com",
-                "dn:uid=test.user,ou=acme,dc=example,dc=com",
+                "uid=test.user,ou=acme,dc=example,dc=com",
                 map("uid", "test.user", "realm", "acme")
             },
             {
                 // Should perform DN quoting.
                 "dn:uid={uid},ou={realm},dc=example,dc=com",
-                "dn:uid=test.user,ou=test\\+cn=quoting,dc=example,dc=com",
+                "uid=test.user,ou=test\\+cn=quoting,dc=example,dc=com",
                 map("uid", "test.user", "realm", "test+cn=quoting")
             },
             {
-                // Should not perform DN quoting.
-                "dn:{dn}",
-                "dn:uid=test.user,ou=acme,dc=example,dc=com",
-                map("dn", "uid=test.user,ou=acme,dc=example,dc=com")
-            },
-            {
                 "u:{uid}@{realm}.example.com",
-                "u:test.user@acme.example.com",
+                "test.user@acme.example.com",
                 map("uid", "test.user", "realm", "acme")
             },
             {
                 // Should not perform any DN quoting.
                 "u:{uid}@{realm}.example.com",
-                "u:test.user@test+cn=quoting.example.com",
+                "test.user@test+cn=quoting.example.com",
                 map("uid", "test.user", "realm", "test+cn=quoting")
             },
+            {
+                // Should resolve boolean and numbers
+                "u:{uid}.{numericid}.{testboolean}@{realm}.example.com",
+                "test.42.true@test.example.com",
+                map("uid", "test", "numericid", 42, "testboolean", true, "realm", "test")
+            }
         };
         // @formatter:on
 
     }
 
     @Test(dataProvider = "templateData")
-    public void testTemplates(final String template, final String expected,
-            Map<String, Object> principals) throws Exception {
-        assertThat(
-                new AuthzIdTemplate(template)
-                        .formatAsAuthzId(principals, Schema.getDefaultSchema()))
+    public void testTemplates(final String template, final String expected, Map<String, Object> principals)
+            throws Exception {
+        assertThat(new AuthzIdTemplate(template).formatAsAuthzId(new JsonValue(principals)))
                 .isEqualTo(expected);
     }
 
@@ -103,7 +104,7 @@
     @Test(dataProvider = "invalidTemplateData", expectedExceptions = IllegalArgumentException.class)
     public void testInvalidTemplateData(final String template, Map<String, Object> principals)
             throws Exception {
-        new AuthzIdTemplate(template).formatAsAuthzId(principals, Schema.getDefaultSchema());
+        new AuthzIdTemplate(template).formatAsAuthzId(new JsonValue(principals));
     }
 
     @DataProvider
@@ -122,10 +123,10 @@
         new AuthzIdTemplate(template);
     }
 
-    private Map<String, Object> map(String... keyValues) {
+    private Map<String, Object> map(Object... keyValues) {
         Map<String, Object> map = new LinkedHashMap<>();
         for (int i = 0; i < keyValues.length; i += 2) {
-            map.put(keyValues[i], keyValues[i + 1]);
+            map.put(keyValues[i].toString(), keyValues[i + 1]);
         }
         return map;
     }
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/CtsAccessTokenResolverTestCase.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/CtsAccessTokenResolverTestCase.java
new file mode 100644
index 0000000..ee9959f
--- /dev/null
+++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/CtsAccessTokenResolverTestCase.java
@@ -0,0 +1,138 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap.authz;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+import static org.forgerock.opendj.ldap.spi.LdapPromises.newSuccessfulLdapPromise;
+import static org.forgerock.opendj.rest2ldap.TestUtils.toValidJson;
+import static org.forgerock.opendj.rest2ldap.authz.Authorizations.newCtsAccessTokenResolver;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.concurrent.ExecutionException;
+
+import org.forgerock.authz.modules.oauth2.AccessTokenInfo;
+import org.forgerock.authz.modules.oauth2.AccessTokenResolver;
+import org.forgerock.json.JsonPointer;
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.spi.LdapPromises;
+import org.forgerock.services.context.Context;
+import org.forgerock.testng.ForgeRockTestCase;
+import org.forgerock.util.promise.Promises;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+@Test
+@SuppressWarnings("javadoc")
+public class CtsAccessTokenResolverTestCase extends ForgeRockTestCase {
+
+    @Mock
+    private ConnectionFactory connectionFactory;
+
+    @Mock
+    private Connection connection;
+
+    @Captor
+    private ArgumentCaptor<SearchRequest> requestCapture;
+
+    private final Context context = null;
+
+    private AccessTokenResolver resolver;
+
+    @BeforeMethod
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        resolver = newCtsAccessTokenResolver(connectionFactory, "dc=cts,dc=example,dc=com");
+    }
+
+    @Test
+    public void basicUseCase() throws Exception {
+        final SearchResultEntry entry = mock(SearchResultEntry.class);
+        final Attribute attribute = mock(Attribute.class);
+        when(entry.getAttribute("coreTokenObject")).thenReturn(attribute);
+        when(attribute.firstValueAsString()).thenReturn(toValidJson(
+                  "{ 'tokenName': ['access_token'],"
+                + "'scope': ['read', 'write', 'dolphin'],"
+                + "'expireTime': ['1461057000'],"
+                + "'an info': 'info value',"
+                + "'user': {'id': 'bjensen'}}"));
+        when(connection.searchSingleEntryAsync(requestCapture.capture()))
+                .thenReturn(newSuccessfulLdapPromise(entry));
+        when(connectionFactory.getConnectionAsync()).thenReturn(
+                Promises.<Connection, LdapException> newResultPromise(connection));
+
+        final String testToken = "test-token";
+        final AccessTokenInfo accessToken = resolver.resolve(context, testToken).get();
+
+        final SearchRequest request = requestCapture.getValue();
+        assertThat(request.getName().toString()).isEqualTo("coreTokenId=test-token,dc=cts,dc=example,dc=com");
+        assertThat(request.getScope()).isEqualTo(SearchScope.BASE_OBJECT);
+        assertThat(request.getAttributes()).containsExactly("coreTokenObject");
+
+        assertThat(accessToken.getExpiresAt()).isEqualTo(1461057000);
+        assertThat(accessToken.getScopes()).containsOnly("dolphin", "read", "write");
+        assertThat(accessToken.getInfo().get("an info")).isEqualTo("info value");
+        assertThat(accessToken.asJsonValue().get(new JsonPointer("/user/id")).asString()).isEqualTo("bjensen");
+    }
+
+    @Test(expectedExceptions = ExecutionException.class,
+          expectedExceptionsMessageRegExp = ".*Unable to find the token 'test-token' in the CTS because:.*")
+    public void testConnectionFactoryError() throws Exception {
+        when(connectionFactory.getConnectionAsync()).thenReturn(
+                Promises.<Connection, LdapException> newExceptionPromise(
+                        newLdapException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS)));
+        resolver.resolve(context, "test-token").get();
+    }
+
+    @Test(expectedExceptions = ExecutionException.class,
+            expectedExceptionsMessageRegExp = ".*Unable to find the token 'test-token' in the CTS because:.*")
+    public void testConnectionSearchError() throws Exception {
+        when(connectionFactory.getConnectionAsync()).thenReturn(
+                Promises.<Connection, LdapException> newResultPromise(connection));
+        when(connection.searchSingleEntryAsync(any(SearchRequest.class))).thenReturn(
+                LdapPromises.<SearchResultEntry, LdapException> newFailedLdapPromise(
+                        newLdapException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS)));
+        resolver.resolve(context, "test-token").get();
+    }
+
+    @Test(expectedExceptions = ExecutionException.class, expectedExceptionsMessageRegExp =
+                  ".*The token 'test-token' must be an access token, but it is a \"refresh_token\"")
+    public void testInvalidTokenType() throws Exception {
+        final SearchResultEntry entry = mock(SearchResultEntry.class);
+        final Attribute attribute = mock(Attribute.class);
+        when(entry.getAttribute("coreTokenObject")).thenReturn(attribute);
+        when(attribute.firstValueAsString()).thenReturn(toValidJson(
+                "{ 'tokenName': ['refresh_token'],"
+                + "'expireTime': ['1461057000']}"));
+        when(connectionFactory.getConnectionAsync()).thenReturn(
+                Promises.<Connection, LdapException> newResultPromise(connection));
+        when(connection.searchSingleEntryAsync(any(SearchRequest.class))).thenReturn(newSuccessfulLdapPromise(entry));
+        resolver.resolve(context, "test-token").get();
+    }
+}
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2FilterTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2FilterTest.java
index d0747a2..24383f3 100644
--- a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2FilterTest.java
+++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2FilterTest.java
@@ -78,7 +78,7 @@
     }
 
     @Test
-    public void testConnectionIsUsingProxiedAuthControlOnRequests() throws LdapException {
+    public void testConnectionIsUsingProxiedAuthControlOnRequests() throws Exception {
         final ConnectionFactory connectionFactory = mock(ConnectionFactory.class);
         when(connectionFactory.getConnectionAsync())
                 .thenReturn(Promises.<Connection, LdapException> newResultPromise(new CheckConnection() {
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/Rfc7662AccessResolverTestCase.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/Rfc7662AccessResolverTestCase.java
new file mode 100644
index 0000000..f4dc1b7
--- /dev/null
+++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/Rfc7662AccessResolverTestCase.java
@@ -0,0 +1,124 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap.authz;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.forgerock.opendj.rest2ldap.authz.Authorizations.newRfc7662AccessTokenResolver;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+import org.forgerock.authz.modules.oauth2.AccessTokenInfo;
+import org.forgerock.authz.modules.oauth2.AccessTokenException;
+import org.forgerock.authz.modules.oauth2.AccessTokenResolver;
+import org.forgerock.http.Handler;
+import org.forgerock.http.MutableUri;
+import org.forgerock.http.protocol.Request;
+import org.forgerock.http.protocol.Response;
+import org.forgerock.http.protocol.Status;
+import org.forgerock.services.context.AttributesContext;
+import org.forgerock.services.context.Context;
+import org.forgerock.services.context.RootContext;
+import org.forgerock.testng.ForgeRockTestCase;
+import org.forgerock.util.encode.Base64;
+import org.forgerock.util.promise.Promise;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+@Test
+@SuppressWarnings("javadoc")
+public final class Rfc7662AccessResolverTestCase extends ForgeRockTestCase {
+
+    @Mock
+    private Handler client;
+
+    @Captor
+    private ArgumentCaptor<Request> requestCapture;
+
+    private AccessTokenResolver resolver;
+
+    private Context context;
+
+    @BeforeMethod
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        resolver = newRfc7662AccessTokenResolver(
+                client, new URI("http://www.example.com/introspect"), "client_id", "client_secret");
+        context = new AttributesContext(new RootContext());
+    }
+
+    @Test
+    public void testBasicUseCase() throws Exception {
+        final AccessTokenInfo token = createFakeTokenResponse(
+                "active-access-token", true, "  read    write dolphin ", 1461057).get();
+
+        assertThat(token.getExpiresAt()).isEqualTo(1461057000);
+        assertThat(token.getScopes()).containsOnly("dolphin", "read", "write");
+        assertThat(token.getInfo().get("an info")).isEqualTo("info value");
+    }
+
+    @Test(expectedExceptions = ExecutionException.class,
+          expectedExceptionsMessageRegExp =
+                  ".*AccessTokenException.*Access token returned by authorization server is not currently active.*")
+    public void testInactiveTokenInRefused() throws Exception {
+        createFakeTokenResponse("inactive-access-token", false, " read  write dolphin ", 1461057).get();
+    }
+
+    @Test(expectedExceptions = ExecutionException.class,
+          expectedExceptionsMessageRegExp = ".*AccessTokenException.*Authorization server returned an error:.*")
+    public void testErrorResponse() throws Exception {
+        when(client.handle(eq(context), any(Request.class))).thenReturn(
+                Response.newResponsePromise(new Response().setStatus(Status.UNAUTHORIZED)));
+        resolver.resolve(context, "fake-access-token").get();
+    }
+
+    private Promise<AccessTokenInfo, AccessTokenException> createFakeTokenResponse(
+            final String token, final boolean active, final String scopes, final long expiresAt) throws Exception {
+        final Map<String, Object> jsonResponse = new HashMap<>();
+        jsonResponse.put("active", active);
+        jsonResponse.put("scope", scopes);
+        jsonResponse.put("exp", expiresAt);
+        jsonResponse.put("an info", "info value");
+
+        when(client.handle(eq(context), any(Request.class))).thenReturn(
+                Response.newResponsePromise(new Response().setStatus(Status.OK).setEntity(jsonResponse)));
+        final Promise<AccessTokenInfo, AccessTokenException> promise = resolver.resolve(context, token);
+        ensureRequestIsCorrect(token);
+        return promise;
+    }
+
+    private void ensureRequestIsCorrect(final String token) throws Exception {
+        verify(client).handle(eq(context), requestCapture.capture());
+        final Request request = requestCapture.getValue();
+        assertThat(request.getMethod()).isEqualTo("POST");
+        assertThat(request.getUri()).isEqualTo(new MutableUri("http://www.example.com/introspect"));
+        assertThat(request.getForm().get("token").get(0)).isEqualTo(token);
+        assertThat(request.getForm().get("token_type_hint").get(0)).isEqualTo("access_token");
+        assertThat(request.getHeaders().get("Accept").getFirstValue()).isEqualTo("application/json");
+        final String credentials = request.getHeaders().getFirst("Authorization").substring("basic".length() + 1);
+        assertThat(new String(Base64.decode(credentials)).split(":")).containsExactly("client_id", "client_secret");
+    }
+}
diff --git a/opendj-server-legacy/resource/config/http-config.json b/opendj-server-legacy/resource/config/http-config.json
index 3c0311b..274b65b 100644
--- a/opendj-server-legacy/resource/config/http-config.json
+++ b/opendj-server-legacy/resource/config/http-config.json
@@ -40,7 +40,7 @@
 			    
 				// Authentication identity template containing a single %s which will be replaced by the authenticating
                 // user's name. (i.e: u:%s)
-				"authcIdTemplate": "u:%s"
+				"authzIdTemplate": "u:%s"
 			},
 			
 			// Bind to the LDAP server using the resulting DN of a search request. 
@@ -60,7 +60,102 @@
                 "filterTemplate" : "(&(uid=%s)(objectClass=inetOrgPerson))"
 			}
 	        // TODO: support for HTTP sessions?
-	    }
+	    },
+
+        // Use OAuth2 authorization method. If used, LDAP requests will be performed with proxied authorization control.
+        // This field is optional.
+        "oauth2": {
+            // Access tokens associated realm.
+            // This attribute is optional and has a string syntax.
+            "realm": "myrealm",
+
+            // Defines the list of required scopes required to access the service.
+            // This field is required and cannot be empty.
+            "requiredScopes": [ "read", "write", "uid" ],
+
+            // Specify the resolver to use to resolve OAuth2 access token.
+            // This attribute is required and its value must be one of "openam", "rfc7662", "cts".
+            // Note that the JSON object corresponding to this attribute value must be present
+            // and well formed in the "oauth2" JSON attribute.
+            "resolver": "openam",
+
+            // The default authzIdTemplate demonstrates how an authorization DN may be constructed
+            // from the "uid" field in the following example OAuth2 token introspection response:
+            // {
+            //     "token": "access_token_string",
+            //     "uid" : "user.2",
+            //     "userName" : [ "user.2" ]
+            // }
+            // This attribute is required and has a string syntax.
+            // It must start with either 'dn:' or 'u:'.
+            // Note: For the CTS resolver, the following placeholder "{userName/0}" must be part of the template string,
+            //       e.g "authzIdTemplate": "dn:uid={userName/0},ou=People,dc=example,dc=com"
+            "authzIdTemplate": "dn:uid={uid},ou=People,dc=example,dc=com",
+
+            // Configures caching of access token introspection results.
+            // This attribute is optional, if it is not present, no token caching will be performed.
+            "accessTokenCache": {
+                // Indicates whether the access token caching should be used.
+                // This attribute is optional (default value is false) and must have a boolean syntax.
+                "enabled": false,
+
+                // Specifies the maximal caching duration for an access token.
+                // Once this delay is over, token will be refreshed from an access token resolver (see "oauth2/resolver")
+                // This attribute is optional, its default value is "5 minutes".
+                // The duration syntax supports all human readable notations from day ("days", "day", "d")
+                // to nanosecond ("nanoseconds", "nanosecond", "nanosec", "nanos", "nano", "ns")
+                // Any negative or zero values are incorrect.
+                "cacheExpiration": "5 minutes"
+            },
+
+            // The OpenAM access token resolver configuration.
+            // This attribute must be present if the "oauth2/resolver" is equal to "openam".
+            // If "oauth2/resolver" is set to another resolver, this attribute will be ignored.
+            "openam": {
+                // Defines the OpenAM endpoint URL where the request should be sent.
+                // This attribute is required and must have a string syntax.
+                "endpointURL": "http://openam.example.com:8080/openam/oauth2/tokeninfo"
+            },
+
+            // The RFC-7662 (see https://tools.ietf.org/html/rfc7662) access token resolver configuration.
+            // This attribute must be present if the "oauth2/resolver" is equal to "rfc7662".
+            // If "oauth2/resolver" is set to another resolver, this attribute will be ignored.
+            "rfc7662": {
+                // Defines the token introspection endpoint URL where the request should be sent.
+                // This attribute is required and must have a string syntax.
+                "endpointURL": "http:/example.com/introspect",
+
+                // Token introspect endpoint requires authentication.
+                // It should support HTTP basic authorization (a base64-encoded string of clientId:clientSecret)
+                // These attributes are mandatory.
+                "clientId": "client_id",
+                "clientSecret": "client_secret"
+            },
+
+            // The CTS access token resolver.
+            // This attribute must be present if the "oauth2/resolver" is equal to "cts".
+            // If "oauth2/resolver" is set to another resolver, this attribute will be ignored.
+            // Note: You can use {userName/0} in authzIdTemplate configuration to access
+            //       user id from the default CTS access token content config.
+            "cts": {
+                // The access token base DN.
+                // This attribute is required and must have a string syntax.
+                "baseDN": "ou=famrecords,ou=openam-session,ou=tokens,dc=example,dc=com"
+            },
+
+            // ONLY FOR TEST PURPOSE: A File based access token resolver
+            // This attribute must be present if the "oauth2/resolver" is equal to "file".
+            // If "oauth2/resolver" is set to another resolver, this attribute will be ignored.
+            "file": {
+                // Directory containing token files.
+                // You can test the rest2ldap OAuth2 authorization support by providing some json token files under
+                // the directory set in the configuration below.
+                // File names must be equal to the token strings.
+                // The file content must a JSON object with the following attributes:
+                // 'scope', 'expireTime' and all the field(s) needed to resolve the authzIdTemplate.
+                "folderPath": "/path/to/test/folder"
+            }
+        }
 	},
 
     // The REST APIs and their LDAP attribute mappings.

--
Gitblit v1.10.0