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 java.util.Collections.emptyList; 020import static org.forgerock.http.protocol.Status.BAD_REQUEST; 021import static org.forgerock.http.protocol.Status.OK; 022import static org.forgerock.http.protocol.Status.UNAUTHORIZED; 023import static org.forgerock.openig.filter.oauth2.client.OAuth2Error.E_SERVER_ERROR; 024import static org.forgerock.openig.filter.oauth2.client.OAuth2Utils.getJsonContent; 025import static org.forgerock.openig.heap.Keys.CLIENT_HANDLER_HEAP_KEY; 026import static org.forgerock.openig.http.Responses.blockingCall; 027import static org.forgerock.openig.util.JsonValues.evaluate; 028import static org.forgerock.openig.util.JsonValues.firstOf; 029 030import java.nio.charset.Charset; 031import java.util.List; 032 033import org.forgerock.http.Handler; 034import org.forgerock.http.protocol.Form; 035import org.forgerock.http.protocol.Request; 036import org.forgerock.http.protocol.Response; 037import org.forgerock.http.protocol.ResponseException; 038import org.forgerock.http.protocol.Status; 039import org.forgerock.json.JsonValue; 040import org.forgerock.openig.heap.GenericHeaplet; 041import org.forgerock.openig.heap.HeapException; 042import org.forgerock.services.context.Context; 043import org.forgerock.util.encode.Base64; 044 045/** 046 * A configuration for an OpenID Connect Provider. Options: 047 * 048 * <pre> 049 * {@code 050 * { 051 * "clientId" : expression, [REQUIRED] 052 * "clientSecret" : expression, [REQUIRED] 053 * "issuer" : String / Issuer [REQUIRED - the issuer name, or its inlined declaration, 054 * "scopes" : [ expressions ], [OPTIONAL - specific scopes to use for this client 055 * registration. ] 056 * "registrationHandler" : handler [OPTIONAL - by default it uses the 'ClientHandler' 057 * provided in heap.] 058 * "tokenEndpointUseBasicAuth" : boolean [OPTIONAL - default is true, use Basic Authentication.] 059 * } 060 * } 061 * </pre> 062 * 063 * Example of use: 064 * 065 * <pre> 066 * {@code 067 * { 068 * "name": "MyClientRegistration", 069 * "type": "ClientRegistration", 070 * "config": { 071 * "clientId": "OpenIG", 072 * "clientSecret": "password", 073 * "scopes": [ 074 * "openid", 075 * "profile" 076 * ], 077 * "issuer": "OpenAM" 078 * } 079 * } 080 * } 081 * </pre> 082 * 083 * or, with inlined Issuer declaration: 084 * 085 * <pre> 086 * {@code 087 * { 088 * "name": "MyClientRegistration", 089 * "type": "ClientRegistration", 090 * "config": { 091 * "clientId": "OpenIG", 092 * "clientSecret": "password", 093 * "scopes": [ 094 * "openid", 095 * "profile" 096 * ], 097 "tokenEndpointUseBasicAuth": true, 098 * "issuer": { 099 * "name": "myIssuer", 100 * "type": "Issuer", 101 * "config": { 102 * "wellKnownEndpoint": "http://server.com:8090/openam/oauth2/.well-known/openid-configuration" 103 * } 104 * } 105 * } 106 * } 107 * } 108 * </pre> 109 * 110 * @see <a 111 * href="https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata"> 112 * OpenID Connect Dynamic Client Registration 1.0 </a> 113 */ 114public final class ClientRegistration { 115 /** The key used to store this client registration in the context. */ 116 static final String CLIENT_REG_KEY = "registration"; 117 118 private final String name; 119 private final String clientId; 120 private final String clientSecret; 121 private final Issuer issuer; 122 private final List<String> scopes; 123 private boolean tokenEndpointUseBasicAuth; 124 private final Handler registrationHandler; 125 126 /** 127 * Creates a Client Registration. 128 * 129 * @param name 130 * The name of this client registration. Can be {@code null}. If 131 * it is {@code null} the name is extracted from the 132 * configuration. 133 * @param config 134 * The configuration of the client registration. 135 * @param issuer 136 * The {@link Issuer} of this Client. 137 * @param registrationHandler 138 * The handler used to send request to the AS. 139 */ 140 public ClientRegistration(final String name, 141 final JsonValue config, 142 final Issuer issuer, 143 final Handler registrationHandler) { 144 this.name = name != null 145 ? name 146 : firstOf(config, "client_name", "client_id").asString(); 147 this.clientId = firstOf(config, "clientId", "client_id").required().asString(); 148 this.clientSecret = firstOf(config, "clientSecret", "client_secret").required().asString(); 149 this.scopes = config.get("scopes").defaultTo(emptyList()).required().asList(String.class); 150 if (config.isDefined("token_endpoint_auth_method") 151 && config.get("token_endpoint_auth_method").asString().equals("client_secret_post")) { 152 this.tokenEndpointUseBasicAuth = false; 153 } else { 154 this.tokenEndpointUseBasicAuth = config.get("tokenEndpointUseBasicAuth").defaultTo(true).asBoolean(); 155 } 156 this.issuer = issuer; 157 this.registrationHandler = registrationHandler; 158 } 159 160 /** 161 * Returns the name of this client registration. 162 * 163 * @return the name of this client registration. 164 */ 165 public String getName() { 166 return name; 167 } 168 169 /** 170 * Exchanges the authorization code for an access token and optional ID 171 * token, and then update the session state. 172 * 173 * @param context 174 * The current context. 175 * @param code 176 * The authorization code. 177 * @param callbackUri 178 * The callback URI. 179 * @return The json content of the response if status return code of the 180 * response is 200 OK. Otherwise, throw an OAuth2ErrorException. 181 * @throws OAuth2ErrorException 182 * If an error occurs when contacting the authorization server 183 * or if the returned response status code is different than 200 184 * OK. 185 */ 186 public JsonValue getAccessToken(final Context context, 187 final String code, 188 final String callbackUri) throws OAuth2ErrorException { 189 final Request request = createRequestForAccessToken(code, callbackUri); 190 final Response response = httpRequestToAuthorizationServer(context, request); 191 checkResponseStatus(response, false); 192 return getJsonContent(response); 193 } 194 195 /** 196 * Returns the client ID of this client registration. 197 * 198 * @return the client ID. 199 */ 200 public String getClientId() { 201 return clientId; 202 } 203 204 /** 205 * Returns the {@link Issuer} for this client registration. 206 * 207 * @return the {@link Issuer} for this client registration. 208 */ 209 public Issuer getIssuer() { 210 return issuer; 211 } 212 213 /** 214 * Refreshes the actual access token, making a refresh request to the token 215 * end-point. 216 * 217 * @param context 218 * The current context. 219 * @param session 220 * The current session. 221 * @return The JSON content of the response if status return code of the 222 * response is 200 OK. Otherwise, throw an OAuth2ErrorException. 223 * @throws ResponseException 224 * If an exception occurs that prevents handling of the request 225 * or if the creation of the request for a refresh token fails. 226 * @throws OAuth2ErrorException 227 * If an error occurs when contacting the authorization server 228 * or if the returned response status code is different than 200 229 * OK. 230 */ 231 public JsonValue refreshAccessToken(final Context context, 232 final OAuth2Session session) throws ResponseException, OAuth2ErrorException { 233 234 final Request request = createRequestForTokenRefresh(session); 235 final Response response = httpRequestToAuthorizationServer(context, request); 236 checkResponseStatus(response, true); 237 return getJsonContent(response); 238 } 239 240 /** 241 * Returns the list of scopes of this client registration. 242 * 243 * @return the the list of scopes of this client registration. 244 */ 245 public List<String> getScopes() { 246 return scopes; 247 } 248 249 /** 250 * Returns the json value of the user info obtained from the authorization 251 * server if the response from the authorization server has a status code of 252 * 200. Otherwise, it throws an exception, meaning the access token may have 253 * expired. 254 * 255 * @param context 256 * The current context. 257 * @param session 258 * The current session to use. 259 * @return A JsonValue containing the requested user info. 260 * @throws ResponseException 261 * If an exception occurs that prevents handling of the request 262 * or if the creation of the request for getting user info 263 * fails. 264 * @throws OAuth2ErrorException 265 * If an error occurs when contacting the authorization server 266 * or if the returned response status code is different than 200 267 * OK. May signify that the access token has expired. 268 */ 269 public JsonValue getUserInfo(final Context context, 270 final OAuth2Session session) throws ResponseException, OAuth2ErrorException { 271 final Request request = createRequestForUserInfo(session.getAccessToken()); 272 final Response response = httpRequestToAuthorizationServer(context, request); 273 if (!Status.OK.equals(response.getStatus())) { 274 /* 275 * The access token may have expired. Trigger an exception, 276 * catch it and react later. 277 */ 278 final OAuth2BearerWWWAuthenticateHeader header = OAuth2BearerWWWAuthenticateHeader.valueOf(response); 279 final OAuth2Error error = header.getOAuth2Error(); 280 final OAuth2Error bestEffort = OAuth2Error.bestEffortResourceServerError(response.getStatus(), error); 281 throw new OAuth2ErrorException(bestEffort); 282 } 283 return getJsonContent(response); 284 } 285 286 /** 287 * Sets the authentication method the token end-point should use. 288 * {@code true} for 'client_secret_basic', {@code false} for 289 * 'client_secret_post' (not recommended). 290 * 291 * @param useBasicAuth 292 * {@code true} if the token end-point should use Basic 293 * authentication, {@code false} if it should use client secret 294 * POST. 295 * @return This client registration. 296 * @see <a href="https://tools.ietf.org/html/rfc6749#section-2.3.1">RFC 6749, Section 2.3.1</a> 297 */ 298 public ClientRegistration setTokenEndpointUseBasicAuth(final boolean useBasicAuth) { 299 this.tokenEndpointUseBasicAuth = useBasicAuth; 300 return this; 301 } 302 303 private Request createRequestForAccessToken(final String code, 304 final String callbackUri) { 305 final Request request = new Request(); 306 request.setMethod("POST"); 307 request.setUri(issuer.getTokenEndpoint()); 308 final Form form = new Form(); 309 form.add("grant_type", "authorization_code"); 310 form.add("redirect_uri", callbackUri); 311 form.add("code", code); 312 addClientIdAndSecret(request, form); 313 form.toRequestEntity(request); 314 return request; 315 } 316 317 private Request createRequestForTokenRefresh(final OAuth2Session session) throws ResponseException { 318 final Request request = new Request(); 319 request.setMethod("POST"); 320 request.setUri(issuer.getTokenEndpoint()); 321 final Form form = new Form(); 322 form.add("grant_type", "refresh_token"); 323 form.add("refresh_token", session.getRefreshToken()); 324 addClientIdAndSecret(request, form); 325 form.toRequestEntity(request); 326 return request; 327 } 328 329 private Request createRequestForUserInfo(final String accessToken) throws ResponseException { 330 final Request request = new Request(); 331 request.setMethod("GET"); 332 request.setUri(issuer.getUserInfoEndpoint()); 333 request.getHeaders().add("Authorization", "Bearer " + accessToken); 334 return request; 335 } 336 337 private void addClientIdAndSecret(final Request request, 338 final Form form) { 339 final String user = getClientId(); 340 final String pass = getClientSecret(); 341 if (!tokenEndpointUseBasicAuth) { 342 form.add("client_id", user); 343 form.add("client_secret", pass); 344 } else { 345 final String userpass = Base64.encode((user + ":" + pass).getBytes(Charset.defaultCharset())); 346 request.getHeaders().add("Authorization", "Basic " + userpass); 347 } 348 } 349 350 private String getClientSecret() { 351 return clientSecret; 352 } 353 354 private Response httpRequestToAuthorizationServer(final Context context, 355 final Request request) 356 throws OAuth2ErrorException { 357 try { 358 return blockingCall(registrationHandler, context, request); 359 } catch (final InterruptedException e) { 360 // FIXME Changed IOException to InterruptedException, not very sure about that 361 throw new OAuth2ErrorException(E_SERVER_ERROR, 362 "Authorization failed because an error occurred while trying " 363 + "to contact the authorization server"); 364 } 365 } 366 367 private void checkResponseStatus(final Response response, 368 final boolean isRefreshToken) throws OAuth2ErrorException { 369 final Status status = response.getStatus(); 370 if (!OK.equals(status)) { 371 if (BAD_REQUEST.equals(status) || UNAUTHORIZED.equals(status)) { 372 final JsonValue errorJson = getJsonContent(response); 373 throw new OAuth2ErrorException(OAuth2Error.valueOfJsonContent(errorJson.asMap())); 374 } else { 375 final String errorMessage = format("Unable to %s access token [status=%d]", 376 isRefreshToken ? "refresh" : "exchange", 377 status.getCode()); 378 throw new OAuth2ErrorException(E_SERVER_ERROR, errorMessage); 379 } 380 } 381 } 382 383 /** Creates and initializes a Client Registration object in a heap environment. */ 384 public static class Heaplet extends GenericHeaplet { 385 @Override 386 public Object create() throws HeapException { 387 final Handler registrationHandler = heap.resolve(config.get("registrationHandler") 388 .defaultTo(CLIENT_HANDLER_HEAP_KEY), 389 Handler.class); 390 final Issuer issuer = heap.resolve(config.get("issuer"), Issuer.class); 391 return new ClientRegistration(this.name, 392 evaluate(config, logger), 393 issuer, 394 registrationHandler); 395 } 396 } 397}