001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2016 ForgeRock AS. 015 */ 016package org.forgerock.opendj.rest2ldap.authz; 017 018import static org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.asConditionalFilter; 019import static org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.newConditionalFilter; 020import static org.forgerock.util.promise.Promises.newResultPromise; 021 022import java.net.URI; 023import java.util.HashMap; 024import java.util.Map; 025import java.util.Set; 026 027import org.forgerock.openig.oauth2.AccessTokenInfo; 028import org.forgerock.openig.oauth2.AccessTokenResolver; 029import org.forgerock.openig.oauth2.OAuth2Context; 030import org.forgerock.openig.oauth2.ResourceAccess; 031import org.forgerock.openig.oauth2.ResourceServerFilter; 032import org.forgerock.http.Filter; 033import org.forgerock.http.Handler; 034import org.forgerock.http.filter.Filters; 035import org.forgerock.http.protocol.Headers; 036import org.forgerock.http.protocol.Request; 037import org.forgerock.http.protocol.Response; 038import org.forgerock.http.protocol.ResponseException; 039import org.forgerock.http.protocol.Status; 040import org.forgerock.opendj.ldap.Connection; 041import org.forgerock.opendj.ldap.ConnectionFactory; 042import org.forgerock.opendj.ldap.controls.ProxiedAuthV2RequestControl; 043import org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext; 044import org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.Condition; 045import org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.ConditionalFilter; 046import org.forgerock.services.context.Context; 047import org.forgerock.services.context.SecurityContext; 048import org.forgerock.util.Function; 049import org.forgerock.util.Pair; 050import org.forgerock.util.Reject; 051import org.forgerock.util.promise.NeverThrowsException; 052import org.forgerock.util.promise.Promise; 053import org.forgerock.util.time.TimeService; 054 055/** Factory methods to create {@link Filter} performing authentication and authorizations. */ 056public final class Authorization { 057 058 private static final String OAUTH2_AUTHORIZATION_HEADER = "Authorization"; 059 060 /** 061 * Creates a new {@link Filter} in charge of injecting an {@link AuthenticatedConnectionContext}. This 062 * {@link Filter} tries each of the provided filters until one can apply. If no filter can be applied, the last 063 * filter in the list will be applied allowing it to formulate a valid, implementation specific, error response. 064 * 065 * @param filters 066 * {@link Iterable} of authorization {@link ConditionalFilters} to try. If empty, the returned filter 067 * will always respond with 403 Forbidden. 068 * @return A new authorization {@link Filter} 069 */ 070 public static Filter newAuthorizationFilter(Iterable<? extends ConditionalFilter> filters) { 071 return new AuthorizationFilter(filters); 072 } 073 074 /** 075 * Creates a new {@link ConditionalFilter} performing authentication. If authentication succeed, it injects a 076 * {@link SecurityContext} with the authenticationId provided by the user. Otherwise, returns a HTTP 401 - 077 * Unauthorized response. The condition of this {@link ConditionalFilter} will return true if the supplied requests 078 * contains credentials information, false otherwise. 079 * 080 * @param authenticationStrategy 081 * {@link AuthenticationStrategy} to validate the user's provided credentials. 082 * @param credentialsExtractor 083 * Function to extract the credentials from the received request. 084 * @throws NullPointerException 085 * if a parameter is null. 086 * @return a new {@link ConditionalFilter} 087 */ 088 public static ConditionalFilter newConditionalHttpBasicAuthenticationFilter( 089 final AuthenticationStrategy authenticationStrategy, 090 final Function<Headers, Pair<String, String>, NeverThrowsException> credentialsExtractor) { 091 return newConditionalFilter( 092 new HttpBasicAuthenticationFilter(authenticationStrategy, credentialsExtractor), 093 new Condition() { 094 @Override 095 public boolean canApplyFilter(Context context, Request request) { 096 return credentialsExtractor.apply(request.getHeaders()) != null; 097 } 098 }); 099 } 100 101 /** 102 * Creates a {@link ConditionalFilter} injecting an {@link AuthenticatedConnectionContext} with a connection issued 103 * from the given connectionFactory. The condition is always true. 104 * 105 * @param connectionFactory 106 * The factory used to get the {@link Connection} to inject. 107 * @return A new {@link ConditionalFilter}. 108 * @throws NullPointerException 109 * if connectionFactory is null 110 */ 111 public static ConditionalFilter newConditionalDirectConnectionFilter(ConnectionFactory connectionFactory) { 112 return asConditionalFilter(new DirectConnectionFilter(connectionFactory)); 113 } 114 115 /** 116 * Creates a filter injecting an {@link AuthenticatedConnectionContext} given the information provided in the 117 * {@link SecurityContext}. The connection contained in the created {@link AuthenticatedConnectionContext} will add 118 * a {@link ProxiedAuthV2RequestControl} to each LDAP requests. 119 * 120 * @param connectionFactory 121 * The connection factory used to create the connection which will be injected in the 122 * {@link AuthenticatedConnectionContext} 123 * @return A new filter. 124 * @throws NullPointerException 125 * if connectionFactory is null 126 */ 127 public static Filter newProxyAuthorizationFilter(ConnectionFactory connectionFactory) { 128 return new ProxiedAuthV2Filter(connectionFactory); 129 } 130 131 /** 132 * Creates a new {@link AccessTokenResolver} as defined in the RFC-7662. 133 * <p> 134 * @see <a href="https://tools.ietf.org/html/rfc7662">RFC-7662</a> 135 * 136 * @param httpClient 137 * Http client handler used to perform the request 138 * @param introspectionEndPointURL 139 * Introspect endpoint URL to use to resolve the access token. 140 * @param clientAppId 141 * Client application id to use in HTTP Basic authentication header. 142 * @param clientAppSecret 143 * Client application secret to use in HTTP Basic authentication header. 144 * @return A new {@link AccessTokenResolver} instance. 145 */ 146 public static AccessTokenResolver newRfc7662AccessTokenResolver(final Handler httpClient, 147 final URI introspectionEndPointURL, 148 final String clientAppId, 149 final String clientAppSecret) { 150 return new Rfc7662AccessTokenResolver(httpClient, introspectionEndPointURL, clientAppId, clientAppSecret); 151 } 152 153 /** 154 * Creates a new CTS access token resolver. 155 * 156 * @param connectionFactory 157 * The {@link ConnectionFactory} to use to perform search against the CTS. 158 * @param ctsBaseDNTemplate 159 * The base DN template to use to resolve the access token DN. 160 * @return A new CTS access token resolver. 161 */ 162 public static AccessTokenResolver newCtsAccessTokenResolver(final ConnectionFactory connectionFactory, 163 final String ctsBaseDNTemplate) { 164 return new CtsAccessTokenResolver(connectionFactory, ctsBaseDNTemplate); 165 } 166 167 /** 168 * Creates a new file access token resolver which should only be used for test purpose. 169 * 170 * @param tokenFolder 171 * The folder where the access token to resolve must be stored. 172 * @return A new file access token resolver which should only be used for test purpose. 173 */ 174 public static AccessTokenResolver newFileAccessTokenResolver(final String tokenFolder) { 175 return new FileAccessTokenResolver(tokenFolder); 176 } 177 178 /** 179 * Creates a new OAuth2 authorization filter configured with provided parameters. 180 * 181 * @param realm 182 * The realm to displays in error responses. 183 * @param scopes 184 * Scopes that an access token must have to be access a resource. 185 * @param resolver 186 * The {@link AccessTokenResolver} to use to resolve an access token. 187 * @param authzIdTemplate 188 * Authorization ID template. 189 * @return A new OAuth2 authorization filter configured with provided parameters. 190 */ 191 public static Filter newOAuth2ResourceServerFilter(final String realm, 192 final Set<String> scopes, 193 final AccessTokenResolver resolver, 194 final String authzIdTemplate) { 195 return createResourceServerFilter(realm, scopes, resolver, authzIdTemplate); 196 } 197 198 /** 199 * Creates a new optional OAuth2 authorization filter configured with provided parameters. 200 * <p> 201 * This filter will be used only if an OAuth2 Authorization header is present in the incoming request. 202 * 203 * @param realm 204 * The realm to displays in error responses. 205 * @param scopes 206 * Scopes that an access token must have to be access a resource. 207 * @param resolver 208 * The {@link AccessTokenResolver} to use to resolve an access token. 209 * @param authzIdTemplate 210 * Authorization ID template. 211 * @return A new OAuth2 authorization filter configured with provided parameters. 212 */ 213 public static ConditionalFilter newConditionalOAuth2ResourceServerFilter(final String realm, 214 final Set<String> scopes, 215 final AccessTokenResolver resolver, 216 final String authzIdTemplate) { 217 return new ConditionalFilter() { 218 @Override 219 public Filter getFilter() { 220 return createResourceServerFilter(realm, scopes, resolver, authzIdTemplate); 221 } 222 223 @Override 224 public Condition getCondition() { 225 return new Condition() { 226 @Override 227 public boolean canApplyFilter(final Context context, final Request request) { 228 return request.getHeaders().containsKey(OAUTH2_AUTHORIZATION_HEADER); 229 } 230 }; 231 } 232 }; 233 } 234 235 private static Filter createResourceServerFilter(final String realm, 236 final Set<String> scopes, 237 final AccessTokenResolver resolver, 238 final String authzIdTemplate) { 239 Reject.ifTrue(realm == null || realm.isEmpty(), "realm must not be empty"); 240 Reject.ifNull(resolver, "Access token resolver must not be null"); 241 Reject.ifTrue(scopes == null || scopes.isEmpty(), "scopes set can not be empty"); 242 Reject.ifTrue(authzIdTemplate == null || authzIdTemplate.isEmpty(), "Authz id template must not be empty"); 243 244 final ResourceAccess scopesProvider = new ResourceAccess() { 245 @Override 246 public Set<String> getRequiredScopes(final Context context, final Request request) 247 throws ResponseException { 248 return scopes; 249 } 250 }; 251 252 return Filters.chainOf(new ResourceServerFilter(resolver, TimeService.SYSTEM, scopesProvider, realm), 253 createSecurityContextInjectionFilter(authzIdTemplate)); 254 } 255 256 private static Filter createSecurityContextInjectionFilter(final String authzIdTemplate) { 257 final AuthzIdTemplate template = new AuthzIdTemplate(authzIdTemplate); 258 259 return new Filter() { 260 @Override 261 public Promise<Response, NeverThrowsException> filter(final Context context, 262 final Request request, 263 final Handler next) { 264 final AccessTokenInfo token = context.asContext(OAuth2Context.class).getAccessToken(); 265 final Map<String, Object> authz = new HashMap<>(1); 266 try { 267 authz.put(template.getSecurityContextID(), template.formatAsAuthzId(token.asJsonValue())); 268 } catch (final IllegalArgumentException e) { 269 return newResultPromise(new Response().setStatus(Status.INTERNAL_SERVER_ERROR) 270 .setCause(e)); 271 } 272 final Context securityContext = new SecurityContext(context, token.getToken(), authz); 273 return next.handle(securityContext, request); 274 } 275 }; 276 } 277 278 private Authorization() { 279 // Prevent instantiation. 280 } 281}