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