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 */ 016package org.forgerock.openig.filter.oauth2.client; 017 018import static java.lang.String.format; 019import static org.forgerock.http.protocol.Status.CREATED; 020import static org.forgerock.json.JsonValue.field; 021import static org.forgerock.json.JsonValue.json; 022import static org.forgerock.json.JsonValue.object; 023import static org.forgerock.openig.filter.oauth2.client.ClientRegistration.CLIENT_REG_KEY; 024import static org.forgerock.openig.filter.oauth2.client.Issuer.ISSUER_KEY; 025import static org.forgerock.openig.filter.oauth2.client.OAuth2Utils.getJsonContent; 026import static org.forgerock.openig.http.Responses.blockingCall; 027import static org.forgerock.openig.http.Responses.newInternalServerError; 028import static org.forgerock.util.Reject.checkNotNull; 029import static org.forgerock.util.promise.Promises.newResultPromise; 030 031import java.net.URI; 032 033import org.forgerock.http.Filter; 034import org.forgerock.http.Handler; 035import org.forgerock.http.protocol.Request; 036import org.forgerock.http.protocol.Response; 037import org.forgerock.json.JsonValue; 038import org.forgerock.openig.heap.Heap; 039import org.forgerock.openig.heap.HeapException; 040import org.forgerock.services.context.AttributesContext; 041import org.forgerock.services.context.Context; 042import org.forgerock.util.promise.NeverThrowsException; 043import org.forgerock.util.promise.Promise; 044 045/** 046 * The client registration filter is the way to dynamically register an OpenID 047 * Connect Relying Party with the End-User's OpenID Provider. 048 * <p> 049 * All OpenID metadata must be included in the <b>{@link OAuth2ClientFilter}</b> configuration, 050 * in the <b>"metadata" attribute</b>. Note that for dynamic client registration, 051 * only the "redirect_uris" attribute is mandatory. 052 * </p> 053 * 054 * Note: When using OpenAM, the "scopes" may be specified to this configuration but 055 * it must be defined as: "scopes"(array of string), which differs from 056 * the OAuth2 metadata "scope" (a string containing a space separated list of scope values). 057 * 058 * <br> 059 * Note for developers: The suffix is added to the issuer name to compose the 060 * client registration name in the current heap. When automatically called by 061 * the {@link OAuth2ClientFilter}, this name is {@literal IssuerName} + {@literal OAuth2ClientFilterName} 062 * This is required in order to retrieve the Client Registration when performing 063 * dynamic client registration. 064 * 065 * @see <a href="https://openid.net/specs/openid-connect-registration-1_0.html"> 066 * OpenID Connect Dynamic Client Registration 1.0</a> 067 * @see <a 068 * href="https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata"> 069 * OpenID Connect Dynamic Client Registration 1.0 </a> 070 * @see <a 071 * href="https://tools.ietf.org/html/draft-ietf-oauth-dyn-reg-30#section-2"> 072 * OAuth 2.0 Dynamic Client Registration Protocol </a> 073 */ 074public class ClientRegistrationFilter implements Filter { 075 private final Handler registrationHandler; 076 private final Heap heap; 077 private final JsonValue config; 078 private final String suffix; 079 080 /** 081 * Creates a new dynamic registration filter. 082 * 083 * @param registrationHandler 084 * The handler to perform the dynamic registration to the AS. 085 * @param config 086 * Can contain any client metadata attributes that the client 087 * chooses to specify for itself during the registration. Must 088 * contains the 'redirect_uris' attributes. 089 * @param heap 090 * A reference to the current heap. 091 * @param suffix 092 * The name of the client registration in the heap will be 093 * {@literal IssuerName} + {@literal suffix}. Must not be {@code null}. 094 */ 095 public ClientRegistrationFilter(final Handler registrationHandler, 096 final JsonValue config, 097 final Heap heap, 098 final String suffix) { 099 this.registrationHandler = registrationHandler; 100 this.config = config; 101 this.heap = heap; 102 this.suffix = checkNotNull(suffix); 103 } 104 105 @Override 106 public Promise<Response, NeverThrowsException> filter(Context context, 107 Request request, 108 Handler next) { 109 try { 110 AttributesContext attributesContext = context.asContext(AttributesContext.class); 111 final Issuer issuer = (Issuer) attributesContext.getAttributes().get(ISSUER_KEY); 112 if (issuer != null) { 113 ClientRegistration cr = heap.get(issuer.getName() + suffix, ClientRegistration.class); 114 if (cr == null) { 115 if (!config.isDefined("redirect_uris")) { 116 throw new RegistrationException( 117 "Cannot perform dynamic registration: 'redirect_uris' should be defined"); 118 } 119 if (issuer.getRegistrationEndpoint() == null) { 120 throw new RegistrationException(format("Registration is not supported by the issuer '%s'", 121 issuer)); 122 } 123 final JsonValue registeredClientConfiguration = performDynamicClientRegistration(context, config, 124 issuer.getRegistrationEndpoint()); 125 cr = heap.resolve( 126 createClientRegistrationDeclaration(registeredClientConfiguration, issuer.getName()), 127 ClientRegistration.class); 128 } 129 attributesContext.getAttributes().put(CLIENT_REG_KEY, cr); 130 } else { 131 throw new RegistrationException("Cannot retrieve issuer from the context"); 132 } 133 } catch (RegistrationException e) { 134 return newResultPromise(newInternalServerError(e)); 135 } catch (HeapException e) { 136 return newResultPromise( 137 newInternalServerError("Cannot inject inlined Client Registration declaration to heap", e)); 138 } 139 return next.handle(context, request); 140 } 141 142 private JsonValue createClientRegistrationDeclaration(final JsonValue configuration, final String issuerName) { 143 configuration.put("issuer", issuerName); 144 return json(object( 145 field("name", issuerName + suffix), 146 field("type", "ClientRegistration"), 147 field("config", configuration))); 148 } 149 150 JsonValue performDynamicClientRegistration(final Context context, 151 final JsonValue clientRegistrationConfiguration, 152 final URI registrationEndpoint) throws RegistrationException { 153 final Request request = new Request(); 154 request.setMethod("POST"); 155 request.setUri(registrationEndpoint); 156 request.setEntity(clientRegistrationConfiguration.asMap()); 157 158 final Response response; 159 try { 160 response = blockingCall(registrationHandler, context, request); 161 } catch (InterruptedException e) { 162 throw new RegistrationException(format("Interrupted while waiting for '%s' response", request.getUri()), e); 163 } 164 if (!CREATED.equals(response.getStatus())) { 165 throw new RegistrationException("Cannot perform dynamic registration: this can be caused " 166 + "by the distant server(busy, offline...) " 167 + "or a malformed registration response."); 168 } 169 try { 170 return getJsonContent(response); 171 } catch (OAuth2ErrorException e) { 172 throw new RegistrationException("Cannot perform dynamic registration: invalid response JSON content."); 173 } 174 } 175}