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 2015 ForgeRock AS. 015 */ 016 017package org.forgerock.openig.openam; 018 019import static org.forgerock.http.protocol.Response.newResponsePromise; 020import static org.forgerock.http.protocol.Status.FORBIDDEN; 021import static org.forgerock.json.JsonValue.json; 022import static org.forgerock.json.JsonValue.object; 023import static org.forgerock.openig.el.Bindings.bindings; 024import static org.forgerock.openig.http.Responses.newInternalServerError; 025import static org.forgerock.util.Reject.checkNotNull; 026import static org.forgerock.util.promise.Promises.newResultPromise; 027 028import java.io.IOException; 029import java.net.URI; 030import java.util.Map; 031 032import org.forgerock.http.Filter; 033import org.forgerock.http.Handler; 034import org.forgerock.http.protocol.Request; 035import org.forgerock.http.protocol.Response; 036import org.forgerock.http.session.SessionContext; 037import org.forgerock.openig.el.Bindings; 038import org.forgerock.openig.el.Expression; 039import org.forgerock.openig.heap.GenericHeapObject; 040import org.forgerock.services.context.Context; 041import org.forgerock.util.AsyncFunction; 042import org.forgerock.util.Function; 043import org.forgerock.util.annotations.VisibleForTesting; 044import org.forgerock.util.promise.NeverThrowsException; 045import org.forgerock.util.promise.Promise; 046 047/** 048 * Provides an OpenAM SSO Token in the given header name for downstream components. 049 * 050 * <p>The SSO Token is stored in the session to avoid DOS on OpenAM endpoints. 051 * 052 * <p>If the request failed, a unique attempt to refresh the SSO token is tried. 053 */ 054public class SsoTokenFilter extends GenericHeapObject implements Filter { 055 056 static final String SSO_TOKEN_KEY = "SSOToken"; 057 static final String BASE_ENDPOINT = "json"; 058 static final String AUTHENTICATION_ENDPOINT = "/authenticate"; 059 static final String DEFAULT_HEADER_NAME = "iPlanetDirectoryPro"; 060 061 private final Handler ssoClientHandler; 062 private final URI openamUrl; 063 private final String realm; 064 private final String headerName; 065 private final Expression<String> username; 066 private final Expression<String> password; 067 068 SsoTokenFilter(final Handler ssoClientHandler, 069 final URI openamUrl, 070 final String realm, 071 final String headerName, 072 final Expression<String> username, 073 final Expression<String> password) { 074 this.ssoClientHandler = checkNotNull(ssoClientHandler); 075 this.openamUrl = checkNotNull(openamUrl); 076 this.realm = startsWithSlash(realm); 077 this.headerName = headerName != null ? headerName : DEFAULT_HEADER_NAME; 078 this.username = username; 079 this.password = password; 080 } 081 082 private static String startsWithSlash(final String realm) { 083 String nonNullRealm = realm != null ? realm : "/"; 084 return nonNullRealm.startsWith("/") ? nonNullRealm : "/" + nonNullRealm; 085 } 086 087 @Override 088 public Promise<Response, NeverThrowsException> filter(final Context context, 089 final Request request, 090 final Handler next) { 091 092 final AsyncFunction<String, Response, NeverThrowsException> executeRequestWithToken = 093 new AsyncFunction<String, Response, NeverThrowsException>() { 094 095 @Override 096 public Promise<Response, NeverThrowsException> apply(String token) { 097 if (token != null) { 098 request.getHeaders().put(headerName, token); 099 return next.handle(context, request); 100 } else { 101 return newResponsePromise(newInternalServerError("Unable to retrieve SSO Token")); 102 } 103 } 104 }; 105 106 final AsyncFunction<Response, Response, NeverThrowsException> checkResponse = 107 new AsyncFunction<Response, Response, NeverThrowsException>() { 108 109 @Override 110 public Promise<Response, NeverThrowsException> apply(Response response) { 111 if (response.getStatus().equals(FORBIDDEN)) { 112 final SessionContext sessionContext = context.asContext(SessionContext.class); 113 sessionContext.getSession().remove(SSO_TOKEN_KEY); 114 return createSsoToken(context, request) 115 .thenAsync(executeRequestWithToken); 116 } 117 return newResponsePromise(response); 118 } 119 }; 120 121 return findSsoToken(context, request) 122 .thenAsync(executeRequestWithToken) 123 .thenAsync(checkResponse); 124 } 125 126 private Promise<String, NeverThrowsException> findSsoToken(final Context context, final Request request) { 127 final SessionContext sessionContext = context.asContext(SessionContext.class); 128 if (sessionContext.getSession().containsKey(SSO_TOKEN_KEY)) { 129 return newResultPromise((String) sessionContext.getSession().get(SSO_TOKEN_KEY)); 130 } else { 131 return createSsoToken(context, request); 132 } 133 } 134 135 private Promise<String, NeverThrowsException> createSsoToken(final Context context, final Request request) { 136 return ssoClientHandler.handle(context, authenticationRequest(bindings(context, request))) 137 .then(extractSsoToken(context)); 138 } 139 140 private Function<Response, String, NeverThrowsException> extractSsoToken(final Context context) { 141 return new Function<Response, String, NeverThrowsException>() { 142 @Override 143 public String apply(Response response) { 144 String token = null; 145 try { 146 @SuppressWarnings("unchecked") 147 final Map<String, String> result = (Map<String, String>) response.getEntity().getJson(); 148 token = result.get("tokenId"); 149 context.asContext(SessionContext.class).getSession().put(SSO_TOKEN_KEY, token); 150 } catch (IOException e) { 151 logger.warning("Couldn't parse as JSON the OpenAM authentication response"); 152 logger.warning(e); 153 } 154 return token; 155 } 156 }; 157 } 158 159 @VisibleForTesting 160 Request authenticationRequest(final Bindings bindings) { 161 final Request request = new Request(); 162 request.setMethod("POST"); 163 request.setUri(openamUrl.resolve(BASE_ENDPOINT + realm + AUTHENTICATION_ENDPOINT)); 164 request.setEntity(json(object()).asMap()); 165 request.getHeaders().put("X-OpenAM-Username", username.eval(bindings)); 166 request.getHeaders().put("X-OpenAM-Password", password.eval(bindings)); 167 return request; 168 } 169}