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/main/java/org/forgerock/opendj/rest2ldap/authz/Authorizations.java |  176 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 171 insertions(+), 5 deletions(-)

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.
+    }
 }

--
Gitblit v1.10.0