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.OK; 020import static org.forgerock.openig.filter.oauth2.client.OAuth2Utils.getJsonContent; 021import static org.forgerock.openig.heap.Keys.CLIENT_HANDLER_HEAP_KEY; 022import static org.forgerock.openig.http.Responses.blockingCall; 023import static org.forgerock.openig.util.JsonValues.evaluate; 024import static org.forgerock.openig.util.JsonValues.firstOf; 025 026import java.net.URI; 027import java.net.URISyntaxException; 028import java.util.Collections; 029import java.util.LinkedList; 030import java.util.List; 031import java.util.regex.Pattern; 032import java.util.regex.PatternSyntaxException; 033 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.json.JsonValueException; 039import org.forgerock.openig.heap.GenericHeaplet; 040import org.forgerock.openig.heap.HeapException; 041import org.forgerock.services.context.AttributesContext; 042import org.forgerock.services.context.RootContext; 043import org.forgerock.util.Reject; 044 045/** 046 * A configuration for an OpenID Connect Issuer. Two approaches to create the 047 * Issuer: 048 * <p> 049 * With an OpenId well-known end-point: 050 * </p> 051 * 052 * <pre> 053 * {@code 054 * { 055 * "wellKnownEndpoint" : uriExpression, [REQUIRED] 056 * "issuerHandler" : handler [OPTIONAL - by default it uses the 'ClientHandler' 057 * provided in heap.] 058 * "supportedDomains" : [ patterns ] [OPTIONAL - if this issuer supports other domain names] 059 * } 060 * } 061 * </pre> 062 * 063 * The 'supportedDomains' are the other domain names supported by this issuer, 064 * their format can include use of regular-expression patterns. 065 * Nota: Declaring these domains in the configuration should be as simple as 066 * possible, without any schemes or end slash i.e.: 067 * 068 * <pre>{@code 069 * GOOD: [ "openam.com", "openam.com:8092", "register.server.com", "allopenamdomains.*" ] 070 * BAD : [ "http://openam.com", "openam.com:8092/", "http://openam.com/" ] 071 * } 072 * </pre> 073 * 074 * <p>For example, use this kind of configuration if the end-points are not known: 075 * 076 * <pre> 077 * {@code 078 * { 079 * "name": "openam", 080 * "type": "Issuer", 081 * "config": { 082 * "wellKnownEndpoint": "http://www.example.com:8081/openam/oauth2/.well-known/openid-configuration" 083 * "supportedDomains" : [ "openam.com", "openam.com:8092", "register.server.com" ] 084 * } 085 * } 086 * } 087 * </pre> 088 * 089 * <br> 090 * <p> 091 * Use this configuration if the end-points are known. The well-known end-point 092 * is optional as the value will be saved but no request will be performed on 093 * this end-point. 094 * </p> 095 * 096 * <pre> 097 * {@code 098 * { 099 * "authorizeEndpoint" : uriExpression, [REQUIRED] 100 * "tokenEndpoint" : uriExpression, [REQUIRED] 101 * "registrationEndpoint" : uriExpression, [OPTIONAL - allows dynamic client registration] 102 * "userInfoEndpoint" : uriExpression [OPTIONAL - default is no user info] 103 * "wellKnownEndpoint" : uriExpression [OPTIONAL] 104 * "supportedDomains" : [ patterns ] [OPTIONAL - if this issuer supports other domain names] 105 * } 106 * } 107 * </pre> 108 * 109 * For example: 110 * 111 * <pre> 112 * {@code 113 * { 114 * "name": "openam", 115 * "type": "Issuer", 116 * "config": { 117 * "authorizeEndpoint": "http://www.example.com:8081/openam/oauth2/authorize", 118 * "tokenEndpoint": "http://www.example.com:8081/openam/oauth2/access_token", 119 * "userInfoEndpoint": "http://www.example.com:8081/openam/oauth2/userinfo" 120 * } 121 * } 122 * } 123 * </pre> 124 */ 125public final class Issuer { 126 /** The key used to store this issuer in the context. */ 127 public static final String ISSUER_KEY = "issuer"; 128 129 private final String name; 130 private final URI authorizeEndpoint; 131 private final URI tokenEndpoint; 132 private final URI registrationEndpoint; 133 private final URI userInfoEndpoint; 134 private URI wellKnownEndpoint; 135 private final List<Pattern> supportedDomains; 136 137 /** 138 * Creates an issuer with the specified name and configuration. 139 * 140 * @param name 141 * The name of this Issuer. When the issuer is created by 142 * discovery, the issuer name is given by the metadata "issuer", 143 * not null. 144 * @param config 145 * The configuration of this issuer, not null. 146 */ 147 public Issuer(final String name, final JsonValue config) { 148 Reject.ifNull(name, config); 149 this.name = name; 150 this.authorizeEndpoint = firstOf(config, "authorizeEndpoint", "authorization_endpoint").required().asURI(); 151 this.tokenEndpoint = firstOf(config, "tokenEndpoint", "token_endpoint").required().asURI(); 152 this.registrationEndpoint = firstOf(config, "registrationEndpoint", "registration_endpoint").asURI(); 153 this.userInfoEndpoint = firstOf(config, "userInfoEndpoint", "userinfo_endpoint").asURI(); 154 this.wellKnownEndpoint = config.get("wellKnownEndpoint").asURI(); 155 this.supportedDomains = extractPatterns(config.get("supportedDomains").expect(List.class).asList(String.class)); 156 } 157 158 /** 159 * Returns the name of this issuer. 160 * 161 * @return the name of this issuer. 162 */ 163 public String getName() { 164 return name; 165 } 166 167 /** 168 * Returns the authorize end-point of this issuer. 169 * 170 * @return the authorize end-point of this issuer. 171 */ 172 public URI getAuthorizeEndpoint() { 173 return authorizeEndpoint; 174 } 175 176 /** 177 * Returns the token end-point of this issuer. 178 * 179 * @return the token end-point of this issuer. 180 */ 181 public URI getTokenEndpoint() { 182 return tokenEndpoint; 183 } 184 185 /** 186 * Returns the registration end-point of this issuer. 187 * 188 * @return the registration end-point of this issuer. 189 */ 190 public URI getRegistrationEndpoint() { 191 return registrationEndpoint; 192 } 193 194 /** 195 * Returns the user end-point of this issuer. 196 * 197 * @return the user end-point of this issuer. 198 */ 199 public URI getUserInfoEndpoint() { 200 return userInfoEndpoint; 201 } 202 203 /** 204 * Returns the well-known end-point of this issuer. 205 * 206 * @return the well-known end-point of this issuer. 207 */ 208 public URI getWellKnownEndpoint() { 209 return wellKnownEndpoint; 210 } 211 212 /** 213 * Returns {@code true} if this issuer has a user info end-point. 214 * 215 * @return {@code true} if this issuer has a user info end-point. 216 */ 217 public boolean hasUserInfoEndpoint() { 218 return userInfoEndpoint != null; 219 } 220 221 /** 222 * Returns the unmodifiable list of the supported domain names. 223 * 224 * @return A unmodifiable list of the supported domain names. 225 */ 226 public List<Pattern> getSupportedDomains() { 227 return Collections.unmodifiableList(supportedDomains); 228 } 229 230 /** 231 * Builds a new Issuer based on the given well-known URI. 232 * 233 * @param name 234 * The issuer's identifier. Usually, it's the host name or a 235 * given name. 236 * @param wellKnownUri 237 * The well-known URI of this issuer. 238 * @param supportedDomains 239 * List of the supported domains for this issuer. 240 * @param handler 241 * The issuer handler that does the call to the given well-known 242 * URI. 243 * @return An OpenID issuer. 244 * @throws DiscoveryException 245 * If an error occurred when retrieving the JSON content from 246 * the server response. 247 */ 248 public static Issuer build(final String name, 249 final URI wellKnownUri, 250 final List<String> supportedDomains, 251 final Handler handler) throws DiscoveryException { 252 final Request request = new Request(); 253 request.setMethod("GET"); 254 request.setUri(wellKnownUri); 255 256 Response response; 257 try { 258 response = blockingCall(handler, new AttributesContext(new RootContext()), request); 259 } catch (InterruptedException e) { 260 throw new DiscoveryException(format("Interrupted while waiting for '%s' response", wellKnownUri), e); 261 } 262 263 if (!OK.equals(response.getStatus())) { 264 throw new DiscoveryException("Unable to read well-known OpenID Configuration from '" 265 + wellKnownUri + "'", response.getCause()); 266 } 267 JsonValue config = null; 268 try { 269 config = getJsonContent(response); 270 } catch (OAuth2ErrorException | JsonValueException e) { 271 throw new DiscoveryException("Cannot read JSON", e); 272 } 273 return new Issuer(name, config.put("supportedDomains", supportedDomains)); 274 } 275 276 private static List<Pattern> extractPatterns(final List<String> from) { 277 final List<Pattern> patterns = new LinkedList<>(); 278 if (from != null) { 279 for (String s : from) { 280 try { 281 s = new StringBuilder("(http|https)://").append(s).append("/$").toString(); 282 patterns.add(Pattern.compile(s)); 283 } catch (final PatternSyntaxException ex) { 284 // Ignore 285 } 286 } 287 } 288 return patterns; 289 } 290 291 /** Creates and initializes an Issuer object in a heap environment. */ 292 public static class Heaplet extends GenericHeaplet { 293 @Override 294 public Object create() throws HeapException { 295 final Handler issuerHandler = heap.resolve(config.get("issuerHandler") 296 .defaultTo(CLIENT_HANDLER_HEAP_KEY), 297 Handler.class); 298 final List<String> supportedDomains = config.get("supportedDomains").asList(String.class); 299 if (config.isDefined("wellKnownEndpoint") 300 && !config.isDefined("authorizeEndpoint") 301 && !config.isDefined("tokenEndpoint")) { 302 try { 303 final URI wellKnownEndpoint = new URI(evaluate(config.get("wellKnownEndpoint"))); 304 return build(this.name, wellKnownEndpoint, supportedDomains, issuerHandler); 305 } catch (DiscoveryException | URISyntaxException e) { 306 throw new HeapException(e); 307 } 308 } 309 return new Issuer(this.name, evaluate(config, logger)); 310 } 311 } 312}