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-2017 ForgeRock AS.
015 */
016
017package org.forgerock.oauth2.core;
018
019import javax.inject.Inject;
020import java.io.IOException;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.Locale;
024import java.util.Map;
025import java.util.Set;
026
027import com.google.inject.assistedinject.Assisted;
028import org.forgerock.guava.common.collect.ClassToInstanceMap;
029import org.forgerock.guava.common.collect.MutableClassToInstanceMap;
030import org.forgerock.json.JsonValue;
031import org.forgerock.openam.rest.representations.JacksonRepresentationFactory;
032import org.restlet.Request;
033import org.restlet.data.Form;
034import org.restlet.data.MediaType;
035import org.restlet.data.Method;
036import org.restlet.ext.jackson.JacksonRepresentation;
037import org.restlet.ext.servlet.ServletUtils;
038import org.slf4j.Logger;
039import org.slf4j.LoggerFactory;
040
041/**
042 * An abstraction of the actual request so as to allow the core of the OAuth2 provider to be agnostic of the library
043 * used to translate the HTTP request.
044 *
045 * @since 12.0.0
046 * @supported.all.api
047 */
048public class OAuth2Request {
049
050    private final ClassToInstanceMap<Token> tokens = MutableClassToInstanceMap.create();
051    private final Logger logger = LoggerFactory.getLogger("OAuth2Provider");
052    private final Request request;
053    private final JacksonRepresentationFactory jacksonRepresentationFactory;
054    private String sessionId;
055    private String ops = null;
056    private JsonValue body;
057
058    /**
059     * Constructs a new RestletOAuth2Request.
060     *
061     * @param request The Restlet request.
062     */
063    @Inject
064    public OAuth2Request(JacksonRepresentationFactory jacksonRepresentationFactory, @Assisted Request request) {
065        this.jacksonRepresentationFactory = jacksonRepresentationFactory;
066        this.request = request;
067    }
068
069    /**
070     * Gets the actual underlying request.
071     *
072     * @return The underlying request.
073     */
074    public Request getRequest() {
075        return request;
076    }
077
078    /**
079     * Gets the specified parameter from the request.
080     * <br/>
081     * Used to call the overloaded getParameter method with checkQueryParam set to true.
082     * The checkQueryParam enables us to prevent checking the query parameters from the request.
083     *
084     * @param name The name of the parameter.
085     * @param <T> The type of the parameter.
086     * @return The parameter value or null if no parameter found.
087     */
088    public <T> T getParameter(String name) {
089        return (T) getParameter(name, true);
090    }
091
092    /**
093     * Gets the specified parameter from the request.
094     * <br/>
095     * This method will first check the attributes then query string if checkQueryParam == true, then move onto check
096     * entity values if it is a post.
097     *
098     * @param name The name of the parameter.
099     * @param checkQueryParam boolean value indicating if the query parameter should be checked.
100     * @param <T> The type of the parameter.
101     * @return The parameter value or null if no parameter found.
102     */
103    public <T> T getParameter(String name, boolean checkQueryParam) {
104        Object value = getAttribute(request, name);
105        if (value != null) {
106            return (T) value;
107        }
108
109        //query param priority over body
110        if ((checkQueryParam == true) && (getQueryParameter(request, name) != null)){
111            return (T) getQueryParameter(request, name);
112        }
113
114        if (request.getMethod().equals(Method.POST)) {
115            if (request.getEntity() != null) {
116                if (MediaType.APPLICATION_WWW_FORM.equals(request.getEntity().getMediaType())) {
117                    Form form = new Form(request.getEntity());
118                    // restore the entity body
119                    request.setEntity(form.getWebRepresentation());
120                    return (T) form.getValuesMap().get(name);
121                } else if (MediaType.APPLICATION_JSON.equals(request.getEntity().getMediaType())) {
122                    return (T) getBody().get(name).getObject();
123                }
124            }
125        }
126        return null;
127    }
128
129    /**
130     * Gets the count of the parameter present in the request with the given name
131     *
132     * @param name The name of the parameter
133     * @return  The count of the the parameter with the given name
134     */
135    public int getParameterCount(String name) {
136        return request.getResourceRef().getQueryAsForm().subList(name).size();
137    }
138
139    /**
140     *
141     * Gets the name of the parameters in the current request
142     *
143     *
144     * @return The parameter names in the request
145     */
146    public Set<String> getParameterNames() {
147
148        if (request.getMethod().equals(Method.GET)) {
149            return request.getResourceRef().getQueryAsForm().getNames();
150        } else if (request.getMethod().equals(Method.POST)) {
151            if (request.getEntity() != null) {
152                if (MediaType.APPLICATION_WWW_FORM.equals(request.getEntity().getMediaType())) {
153                    Form form = new Form(request.getEntity());
154                    // restore the entity body
155                    request.setEntity(form.getWebRepresentation());
156                    return  form.getNames();
157                } else if (MediaType.APPLICATION_JSON.equals(request.getEntity().getMediaType())) {
158                    return getBody().keys();
159                }
160            }
161        }
162        return Collections.emptySet();
163    }
164
165    /**
166     * Gets the value for an attribute from the request with the specified name.
167     *
168     * @param request The request.
169     * @param name The name.
170     * @return The attribute value, may be {@code null}
171     */
172    private Object getAttribute(Request request, String name) {
173        final Object value = request.getAttributes().get(name);
174        return value;
175    }
176
177    /**
178     * Gets the value for a query parameter from the request with the specified name.
179     *
180     * @param request The request.
181     * @param name The name.
182     * @return The query parameter value, may be {@code null}.
183     */
184    private String getQueryParameter(Request request, String name) {
185        return request.getResourceRef().getQueryAsForm().getValuesMap().get(name);
186    }
187
188    /**
189     * Gets the body of the request.
190     * <br/>
191     * Note: reading of the body maybe a one time only operation, so the implementation needs to cache the content
192     * of the body so multiple calls to this method do not behave differently.
193     * <br/>
194     * This method should only ever be called for access and refresh token request and requests to the userinfo and
195     * tokeninfo endpoints.
196     *
197     * @return The body of the request.
198     */
199    public JsonValue getBody() {
200        if (body == null) {
201            final JacksonRepresentation<Map> representation =
202                    jacksonRepresentationFactory.create(request.getEntity(), Map.class);
203            try {
204                body = new JsonValue(representation.getObject());
205            } catch (IOException e) {
206                logger.error(e.getMessage());
207                return JsonValue.json(JsonValue.object());
208            }
209        }
210        return body;
211    }
212
213    /**
214     * Set a Token that is in play for this request.
215     * @param tokenClass The token type.
216     * @param token The token instance.
217     * @param <T> The type of token.
218     */
219    public <T extends Token> void setToken(Class<T> tokenClass, T token) {
220        tokens.putInstance(tokenClass, token);
221    }
222
223    /**
224     * Get a Token that is in play for this request.
225     * @param tokenClass The token type.
226     * @param <T> The type of token.
227     * @return The token instance.
228     */
229    public <T extends Token> T getToken(Class<T> tokenClass) {
230        return tokens.getInstance(tokenClass);
231    }
232
233    /**
234     * Get all the tokens that have been used in this request.
235     * @return The token instances.
236     */
237    public Collection<Token> getTokens() {
238        return tokens.values();
239    }
240
241    /**
242     * Sets the user's session for this request.
243     *
244     * @param sessionId The user's session.
245     */
246    public void setSession(String sessionId) {
247        this.sessionId = sessionId;
248    }
249
250    /**
251     * Gets the user's session for this request.
252     *
253     * @return The user's session.
254     */
255    public String getSession() {
256        return sessionId;
257    }
258
259    /**
260     * Sets the user's session public reference for this request.
261     *
262     * @param ops The user's session public reference.
263     */
264    public void setOps(String ops) {
265        this.ops = ops;
266    }
267
268    /**
269     * Gets the user's session public reference for this request.
270     *
271     * @return The user's session public reference.
272     */
273    public String getOps() {
274        return ops;
275    }
276
277    /**
278     * Get the request locale.
279     * @return The Locale object.
280     */
281    public Locale getLocale() {
282        return ServletUtils.getRequest(request).getLocale();
283    }
284
285    /**
286     * Creates an {@code OAuth2Request} which holds the provided realm only.
287     *
288     * @param realm The request realm.
289     * @return An {@code OAuth2Request}.
290     */
291    public static OAuth2Request forRealm(String realm) {
292        return new RealmOnlyOAuth2Request(realm);
293    }
294
295    private static class RealmOnlyOAuth2Request extends OAuth2Request {
296
297        private final String realm;
298
299        private RealmOnlyOAuth2Request(String realm) {
300            super(null, null);
301            this.realm = realm;
302        }
303
304        @Override
305        public Request getRequest() {
306            throw new UnsupportedOperationException("Realm parameter only OAuth2Request");
307        }
308
309        @Override
310        public <T> T getParameter(String name) {
311            if ("realm".equals(name)) {
312                return (T) realm;
313            }
314            throw new UnsupportedOperationException("Realm parameter only OAuth2Request");
315        }
316
317        @Override
318        public JsonValue getBody() {
319            return null;
320        }
321
322        @Override
323        public int getParameterCount(String name)  { throw new UnsupportedOperationException(); }
324
325        @Override
326        public Set<String> getParameterNames() { throw new UnsupportedOperationException(); }
327
328        @Override
329        public java.util.Locale getLocale() {
330            throw new UnsupportedOperationException();
331        }
332    }
333}