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}