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