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}