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 java.lang.String.format; 020import static org.forgerock.http.protocol.Response.newResponsePromise; 021import static org.forgerock.json.JsonValue.field; 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.openig.util.JsonValues.asExpression; 026import static org.forgerock.openig.util.JsonValues.evaluateJsonStaticExpression; 027import static org.forgerock.openig.util.StringUtil.trailingSlash; 028import static org.forgerock.util.Reject.checkNotNull; 029 030import java.io.IOException; 031import java.net.URI; 032import java.net.URISyntaxException; 033import java.util.Map; 034 035import org.forgerock.http.Filter; 036import org.forgerock.http.Handler; 037import org.forgerock.http.handler.Handlers; 038import org.forgerock.http.protocol.Request; 039import org.forgerock.http.protocol.Response; 040import org.forgerock.http.protocol.Status; 041import org.forgerock.openig.el.Expression; 042import org.forgerock.openig.heap.GenericHeapObject; 043import org.forgerock.openig.heap.GenericHeaplet; 044import org.forgerock.openig.heap.HeapException; 045import org.forgerock.services.context.Context; 046import org.forgerock.util.AsyncFunction; 047import org.forgerock.util.promise.NeverThrowsException; 048import org.forgerock.util.promise.Promise; 049 050/** 051 * A {@link TokenTransformationFilter} is responsible to transform a token issued by OpenAM 052 * into a token of another type. 053 * 054 * <p>Currently only the OpenID Connect id_token to SAML 2.0 Token (Assertions) is supported, {@literal BEARER} mode. 055 * 056 * <pre> 057 * {@code { 058 * "type": "TokenTransformationFilter", 059 * "config": { 060 * "openamUri": "https://openam.example.com/openam/", 061 * "realm": "/my-realm", 062 * "username": "${attributes.username}", 063 * "password": "${attributes.password}", 064 * "idToken": "${attributes.id_token}", 065 * "target": "${attributes.saml_assertions}", 066 * "instance": "oidc-to-saml", 067 * "amHandler": "#Handler" 068 * } 069 * } 070 * } 071 * </pre> 072 * 073 * <p>The {@literal openamUri} attribute is the OpenAM base URI against which authentication 074 * and STS requests will be issued. 075 * 076 * <p>The {@literal realm} attribute is the OpenAM realm that contains both the subject 077 * (described through {@literal username} and {@literal password} attributes) and the STS 078 * instance (described with {@literal instance}). 079 * 080 * <p>The {@literal idToken} attribute is an {@link Expression} specifying where to get the JWT id_token. 081 * Note that the referenced value has to be a {@code String} (the JWT encoded value). 082 * 083 * <p>The {@literal target} attribute is an {@link Expression} specifying where to place the 084 * result of the transformation. Note that the pointed location will contains a {@code String}. 085 * 086 * <p>The {@literal instance} attribute is the name of an STS instance: a pre-configured transformation available 087 * under a specific REST endpoint. 088 * 089 * <p>The {@literal amHandler} attribute is a reference to a {@link Handler} heap object. That handler will be used 090 * for all REST calls to OpenAM (as opposed to the {@code next} Handler of the filter method that is dedicated to 091 * continue the execution flow through the chain). 092 * 093 * <p>If errors are happening during the token transformation, the error response is returned as-is to the caller, 094 * and informative messages are being logged for the administrator. 095 */ 096public class TokenTransformationFilter extends GenericHeapObject implements Filter { 097 098 private final Handler handler; 099 private final URI endpoint; 100 private final Expression<String> idToken; 101 private final Expression<String> target; 102 103 /** 104 * Constructs a new TokenTransformationFilter transforming the OpenID Connect id_token from {@code idToken} 105 * into a SAML 2.0 Assertions structure (into {@code target}). 106 * 107 * @param handler pipeline used to send the STS transformation request 108 * @param endpoint Fully qualified URI of the STS instance (including the {@literal _action=translate} query string) 109 * @param idToken Expression for reading OpenID Connect id_token (expects a {@code String}) 110 * @param target Expression for writing SAML 2.0 token (expects a {@code String}) 111 */ 112 public TokenTransformationFilter(final Handler handler, 113 final URI endpoint, 114 final Expression<String> idToken, 115 final Expression<String> target) { 116 this.handler = checkNotNull(handler); 117 this.endpoint = checkNotNull(endpoint); 118 this.idToken = checkNotNull(idToken); 119 this.target = checkNotNull(target); 120 } 121 122 @Override 123 public Promise<Response, NeverThrowsException> filter(final Context context, 124 final Request request, 125 final Handler next) { 126 127 final String resolvedIdToken = idToken.eval(bindings(context, request)); 128 if (resolvedIdToken == null) { 129 logger.error(format("OpenID Connect id_token expression (%s) has evaluated to null", idToken)); 130 return newResponsePromise(newInternalServerError()); 131 } 132 133 return handler.handle(context, transformationRequest(resolvedIdToken)) 134 .thenAsync(processIssuedToken(context, request, next)); 135 } 136 137 private AsyncFunction<Response, Response, NeverThrowsException> processIssuedToken(final Context context, 138 final Request request, 139 final Handler next) { 140 return new AsyncFunction<Response, Response, NeverThrowsException>() { 141 @Override 142 public Promise<Response, NeverThrowsException> apply(final Response response) { 143 try { 144 Map<String, Object> json = parseJsonObject(response); 145 if (response.getStatus() != Status.OK) { 146 logger.error(format("Server side error (%s, %s) while transforming id_token:%s", 147 response.getStatus(), 148 json.get("reason"), 149 json.get("message"))); 150 return newResponsePromise(new Response(Status.BAD_GATEWAY)); 151 } 152 153 String token = (String) json.get("issued_token"); 154 if (token == null) { 155 // Unlikely to happen, since this is an OK response 156 logger.error("STS issued_token is null"); 157 return newResponsePromise(newInternalServerError()); 158 } 159 target.set(bindings(context, request, response), token); 160 161 // Forward the initial request 162 return next.handle(context, request); 163 } catch (IOException e) { 164 logger.error(format("Can't get JSON back from %s", endpoint)); 165 logger.error(e); 166 return newResponsePromise(newInternalServerError(e)); 167 } 168 } 169 170 @SuppressWarnings("unchecked") 171 private Map<String, Object> parseJsonObject(final Response response) throws IOException { 172 return (Map<String, Object>) response.getEntity().getJson(); 173 } 174 }; 175 } 176 177 private Request transformationRequest(final String resolvedIdToken) { 178 return new Request().setUri(endpoint) 179 .setMethod("POST") 180 .setEntity(transformation(resolvedIdToken)); 181 } 182 183 private static Object transformation(String idToken) { 184 return object(field("input_token_state", object(field("token_type", "OPENIDCONNECT"), 185 field("oidc_id_token", idToken))), 186 field("output_token_state", object(field("token_type", "SAML2"), 187 field("subject_confirmation", "BEARER")))); 188 } 189 190 /** Creates and initializes a token transformation filter in a heap environment. */ 191 public static class Heaplet extends GenericHeaplet { 192 193 @Override 194 public Object create() throws HeapException { 195 Handler amHandler = heap.resolve(config.get("amHandler").required(), 196 Handler.class); 197 URI baseUri = getOpenamBaseUri(); 198 String realm = config.get("realm").defaultTo("/").asString(); 199 String ssoTokenHeader = config.get("ssoTokenHeader").asString(); 200 Expression<String> username = asExpression(config.get("username").required(), String.class); 201 Expression<String> password = asExpression(config.get("password").required(), String.class); 202 SsoTokenFilter ssoTokenFilter = new SsoTokenFilter(amHandler, 203 baseUri, 204 realm, 205 ssoTokenHeader, 206 username, 207 password); 208 209 Expression<String> idToken = asExpression(config.get("idToken").required(), String.class); 210 Expression<String> target = asExpression(config.get("target").required(), String.class); 211 212 String instance = evaluateJsonStaticExpression(config.get("instance").required()).asString(); 213 214 return new TokenTransformationFilter(Handlers.chainOf(amHandler, ssoTokenFilter), 215 transformationEndpoint(baseUri, realm, instance), 216 idToken, 217 target); 218 } 219 220 private URI getOpenamBaseUri() throws HeapException { 221 String baseUri = config.get("openamUri").required().asString(); 222 try { 223 return new URI(trailingSlash(baseUri)); 224 } catch (URISyntaxException e) { 225 throw new HeapException(format("Cannot append trailing '/' on %s", baseUri), e); 226 } 227 } 228 229 private static URI transformationEndpoint(final URI baseUri, final String realm, final String instance) 230 throws HeapException { 231 try { 232 StringBuilder sb = new StringBuilder("rest-sts"); 233 if (!realm.startsWith("/")) { 234 sb.append("/"); 235 } 236 sb.append(realm); 237 if (!realm.endsWith("/") && !instance.startsWith("/")) { 238 sb.append("/"); 239 } 240 sb.append(instance); 241 sb.append("?_action=translate"); 242 243 return baseUri.resolve(new URI(sb.toString())); 244 } catch (URISyntaxException e) { 245 throw new HeapException("Can't build STS endpoint URI", e); 246 } 247 } 248 } 249}