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 2014-2015 ForgeRock AS. 015 */ 016 017package org.forgerock.openig.filter.oauth2.resolver; 018 019import static java.lang.String.format; 020import static org.forgerock.openig.http.Responses.blockingCall; 021import static org.forgerock.util.Utils.closeSilently; 022 023import java.io.IOException; 024import java.net.URI; 025import java.net.URISyntaxException; 026 027import org.forgerock.http.Handler; 028import org.forgerock.http.protocol.Entity; 029import org.forgerock.http.protocol.Form; 030import org.forgerock.http.protocol.Request; 031import org.forgerock.http.protocol.Response; 032import org.forgerock.http.protocol.Status; 033import org.forgerock.json.JsonValue; 034import org.forgerock.openig.filter.oauth2.AccessToken; 035import org.forgerock.openig.filter.oauth2.AccessTokenResolver; 036import org.forgerock.openig.filter.oauth2.OAuth2TokenException; 037import org.forgerock.services.context.Context; 038import org.forgerock.util.time.TimeService; 039 040/** 041 * An {@link OpenAmAccessTokenResolver} knows how to resolve a given token identifier against an OpenAm instance. 042 */ 043public class OpenAmAccessTokenResolver implements AccessTokenResolver { 044 045 private final Handler client; 046 private final String tokenInfoEndpoint; 047 private final OpenAmAccessToken.Builder builder; 048 049 /** 050 * Creates a new {@link OpenAmAccessTokenResolver} configured to access the given {@literal /oauth2/tokeninfo} 051 * OpenAm endpoint. 052 * 053 * @param client 054 * Http client handler used to perform the request 055 * @param time 056 * Time service used to compute the token expiration time 057 * @param tokenInfoEndpoint 058 * full URL of the {@literal /oauth2/tokeninfo} endpoint 059 */ 060 public OpenAmAccessTokenResolver(final Handler client, 061 final TimeService time, 062 final String tokenInfoEndpoint) { 063 this(client, new OpenAmAccessToken.Builder(time), tokenInfoEndpoint); 064 } 065 066 /** 067 * Creates a new {@link OpenAmAccessTokenResolver} configured to access the given {@literal /oauth2/tokeninfo} 068 * OpenAm endpoint. 069 * 070 * @param client 071 * Http client handler used to perform the request 072 * @param builder 073 * AccessToken builder 074 * @param tokenInfoEndpoint 075 * full URL of the {@literal /oauth2/tokeninfo} endpoint 076 */ 077 public OpenAmAccessTokenResolver(final Handler client, 078 final OpenAmAccessToken.Builder builder, 079 final String tokenInfoEndpoint) { 080 this.client = client; 081 this.builder = builder; 082 this.tokenInfoEndpoint = tokenInfoEndpoint; 083 } 084 085 086 @Override 087 public AccessToken resolve(Context context, final String token) throws OAuth2TokenException { 088 try { 089 Request request = new Request(); 090 request.setMethod("GET"); 091 request.setUri(new URI(tokenInfoEndpoint)); 092 093 // Append the access_token as a query parameter (automatically performs encoding) 094 Form form = new Form(); 095 form.add("access_token", token); 096 form.toRequestQuery(request); 097 098 // Call the client handler 099 Response response = null; 100 try { 101 response = blockingCall(client, context, request); 102 } catch (InterruptedException e) { 103 throw new OAuth2TokenException("AccessToken loading has been interrupted", e); 104 } 105 106 if (isResponseEmpty(response)) { 107 throw new OAuth2TokenException("Authorization Server did not return any AccessToken"); 108 } 109 110 JsonValue content = asJson(response.getEntity()); 111 if (isOk(response)) { 112 return builder.build(content); 113 } 114 115 if (content.isDefined("error")) { 116 String error = content.get("error").asString(); 117 String description = content.get("error_description").asString(); 118 throw new OAuth2TokenException(format("Authorization Server returned an error " 119 + "(error: %s, description: %s)", 120 error, 121 description)); 122 } 123 124 throw new OAuth2TokenException("AccessToken returned by the AuthorizationServer has a problem"); 125 } catch (URISyntaxException e) { 126 throw new OAuth2TokenException( 127 format("The token_info endpoint %s could not be accessed because it is a malformed URI", 128 tokenInfoEndpoint), 129 e); 130 } 131 } 132 133 private boolean isResponseEmpty(final Response response) { 134 // response.entity is NEVER null !!! 135 return (response == null) || (response.getEntity() == null); 136 } 137 138 private boolean isOk(final Response response) { 139 return Status.OK.equals(response.getStatus()); 140 } 141 142 /** 143 * Parse the response's content as a JSON structure. 144 * @param entity stream response's content 145 * @return {@link JsonValue} representing the JSON content 146 * @throws OAuth2TokenException if there was some errors during parsing 147 */ 148 private JsonValue asJson(final Entity entity) throws OAuth2TokenException { 149 try { 150 return new JsonValue(entity.getJson()); 151 } catch (IOException e) { 152 // Do not use Entity.toString(), we probably don't want to fully output the content here 153 throw new OAuth2TokenException("Cannot read response content as JSON", e); 154 } finally { 155 closeSilently(entity); 156 } 157 } 158 159 160}