428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts/*
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts * The contents of this file are subject to the terms of the Common Development and
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts * Distribution License (the License). You may not use this file except in compliance with the
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts * License.
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts *
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts * specific language governing permission and limitations under the License.
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts *
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts * When distributing Covered Software, include this CDDL Header Notice in each file and include
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts * Header, with the fields enclosed by brackets [] replaced by your own identifying
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts * information: "Portions copyright [year] [name of copyright owner]".
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts *
5b2db69e215078c268a3e690d144373baebbf17bJames Phillpotts * Copyright 2015-2016 ForgeRock AS.
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts */
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpottspackage org.forgerock.oauth2.restlet;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpottsimport static org.forgerock.oauth2.core.OAuth2Constants.Custom.*;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpottsimport static org.forgerock.oauth2.core.OAuth2Constants.DeviceCode.*;
0023e54360a1db8ce1e864ddf56bddb68f0ad040James Phillpottsimport static org.forgerock.oauth2.core.OAuth2Constants.Params.*;
9f855439c0665e1336c2aef8a33aec7a605b8418James Phillpottsimport static org.forgerock.openam.utils.StringUtils.isEmpty;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpottsimport java.util.HashMap;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpottsimport java.util.Map;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpottsimport javax.inject.Inject;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpottsimport javax.servlet.http.HttpServletRequest;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpottsimport org.forgerock.oauth2.core.ClientRegistrationStore;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpottsimport org.forgerock.oauth2.core.DeviceCode;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpottsimport org.forgerock.oauth2.core.OAuth2Constants;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpottsimport org.forgerock.oauth2.core.OAuth2ProviderSettings;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpottsimport org.forgerock.oauth2.core.OAuth2ProviderSettingsFactory;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpottsimport org.forgerock.oauth2.core.OAuth2Request;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpottsimport org.forgerock.oauth2.core.OAuth2RequestFactory;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpottsimport org.forgerock.oauth2.core.TokenStore;
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpottsimport org.forgerock.oauth2.core.exceptions.OAuth2Exception;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpottsimport org.forgerock.openam.oauth2.OAuth2Utils;
5b2db69e215078c268a3e690d144373baebbf17bJames Phillpottsimport org.forgerock.openam.rest.representations.JacksonRepresentationFactory;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpottsimport org.forgerock.openam.services.baseurl.BaseURLProviderFactory;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpottsimport org.forgerock.openam.utils.StringUtils;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpottsimport org.restlet.Request;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpottsimport org.restlet.ext.servlet.ServletUtils;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpottsimport org.restlet.representation.Representation;
0023e54360a1db8ce1e864ddf56bddb68f0ad040James Phillpottsimport org.restlet.resource.Post;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpottsimport org.restlet.resource.ServerResource;
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpottsimport org.slf4j.Logger;
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpottsimport org.slf4j.LoggerFactory;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts/**
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts * A Restlet resource for issuing new device codes.
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts * @since 13.0.0
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts */
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpottspublic class DeviceCodeResource extends ServerResource {
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts private final Logger logger = LoggerFactory.getLogger("OAuth2Provider");
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts private final TokenStore tokenStore;
5b2db69e215078c268a3e690d144373baebbf17bJames Phillpotts private final OAuth2RequestFactory<?, Request> requestFactory;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts private final ClientRegistrationStore clientRegistrationStore;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts private final OAuth2ProviderSettingsFactory providerSettingsFactory;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts private final BaseURLProviderFactory baseURLProviderFactory;
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts private final ExceptionHandler exceptionHandler;
5b2db69e215078c268a3e690d144373baebbf17bJames Phillpotts private final JacksonRepresentationFactory jacksonRepresentationFactory;
5b2db69e215078c268a3e690d144373baebbf17bJames Phillpotts private final OAuth2Utils oAuth2Utils;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts @Inject
5b2db69e215078c268a3e690d144373baebbf17bJames Phillpotts public DeviceCodeResource(TokenStore tokenStore, OAuth2RequestFactory<?, Request> requestFactory,
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts ClientRegistrationStore clientRegistrationStore, OAuth2ProviderSettingsFactory providerSettingsFactory,
5b2db69e215078c268a3e690d144373baebbf17bJames Phillpotts BaseURLProviderFactory baseURLProviderFactory, ExceptionHandler exceptionHandler,
5b2db69e215078c268a3e690d144373baebbf17bJames Phillpotts JacksonRepresentationFactory jacksonRepresentationFactory, OAuth2Utils oAuth2Utils) {
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts this.tokenStore = tokenStore;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts this.requestFactory = requestFactory;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts this.clientRegistrationStore = clientRegistrationStore;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts this.providerSettingsFactory = providerSettingsFactory;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts this.baseURLProviderFactory = baseURLProviderFactory;
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts this.exceptionHandler = exceptionHandler;
5b2db69e215078c268a3e690d144373baebbf17bJames Phillpotts this.jacksonRepresentationFactory = jacksonRepresentationFactory;
5b2db69e215078c268a3e690d144373baebbf17bJames Phillpotts this.oAuth2Utils = oAuth2Utils;
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts }
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts
0023e54360a1db8ce1e864ddf56bddb68f0ad040James Phillpotts @Post
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts public Representation issueCode(Representation body) throws OAuth2RestletException {
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts final Request restletRequest = getRequest();
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts OAuth2Request request = requestFactory.create(restletRequest);
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts String state = request.getParameter(STATE);
9f855439c0665e1336c2aef8a33aec7a605b8418James Phillpotts // Client ID, Response Type and Scope are required, all other parameters are optional
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts String clientId = request.getParameter(CLIENT_ID);
9f855439c0665e1336c2aef8a33aec7a605b8418James Phillpotts String scope = request.getParameter(SCOPE);
9f855439c0665e1336c2aef8a33aec7a605b8418James Phillpotts String responseType = request.getParameter(RESPONSE_TYPE);
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts try {
9f855439c0665e1336c2aef8a33aec7a605b8418James Phillpotts if (isEmpty(clientId) || isEmpty(scope) || isEmpty(responseType)) {
9f855439c0665e1336c2aef8a33aec7a605b8418James Phillpotts throw new OAuth2RestletException(400, "bad_request",
9f855439c0665e1336c2aef8a33aec7a605b8418James Phillpotts "client_id, scope and response_type are required parameters", state);
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts } else {
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts // check client_id exists
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts clientRegistrationStore.get(clientId, request);
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts }
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts if (scope == null) {
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts scope = "";
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts }
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts final String maxAge = request.getParameter(MAX_AGE);
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts DeviceCode code = tokenStore.createDeviceCode(
5b2db69e215078c268a3e690d144373baebbf17bJames Phillpotts oAuth2Utils.split(scope, " "),
155dc11b6b32401c0187512a86b85326d170e63bPhill Cunnington null,
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts clientId,
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts request.<String>getParameter(NONCE),
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts request.<String>getParameter(RESPONSE_TYPE),
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts request.<String>getParameter(STATE),
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts request.<String>getParameter(ACR_VALUES),
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts request.<String>getParameter(PROMPT),
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts request.<String>getParameter(UI_LOCALES),
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts request.<String>getParameter(LOGIN_HINT),
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts maxAge == null ? null : Integer.valueOf(maxAge),
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts request.<String>getParameter(CLAIMS),
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts request,
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts request.<String>getParameter(CODE_CHALLENGE),
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts request.<String>getParameter(CODE_CHALLENGE_METHOD));
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts Map<String, Object> result = new HashMap<>();
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts OAuth2ProviderSettings providerSettings = providerSettingsFactory.get(request);
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts result.put(DEVICE_CODE, code.getDeviceCode());
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts result.put(USER_CODE, code.getUserCode());
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts result.put(EXPIRES_IN, providerSettings.getDeviceCodeLifetime());
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts result.put(INTERVAL, providerSettings.getDeviceCodePollInterval());
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts String verificationUrl = providerSettings.getVerificationUrl();
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts if (StringUtils.isBlank(verificationUrl)) {
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts final HttpServletRequest servletRequest = ServletUtils.getRequest(restletRequest);
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts final String realm = request.getParameter(OAuth2Constants.Custom.REALM);
3a9e5adf8ed71f7841c483df0173964166267d9bJames Phillpotts verificationUrl = baseURLProviderFactory.get(realm).getRootURL(servletRequest) + "/oauth2/device/user";
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts }
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts result.put(VERIFICATION_URL, verificationUrl);
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts
5b2db69e215078c268a3e690d144373baebbf17bJames Phillpotts return jacksonRepresentationFactory.create(result);
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts } catch (OAuth2Exception e) {
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts throw new OAuth2RestletException(e.getStatusCode(), e.getError(), e.getMessage(), state);
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts }
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts }
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts @Override
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts protected void doCatch(Throwable throwable) {
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts if (!(throwable.getCause() instanceof OAuth2RestletException)) {
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts logger.error("Exception when issuing device tokens", throwable.getCause());
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts }
e44dca0ed39eba8c8b3096bc7b40bba5a18f61f0James Phillpotts exceptionHandler.handle(throwable, getResponse());
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts }
428d85cb974cd1b98284f023a69bc6af5fb94723James Phillpotts}