001/*******************************************************************************
002 * Copyright 2017 The MIT Internet Trust Consortium
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *   http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *******************************************************************************/
016
017package org.mitre.oauth2.web;
018
019import java.util.Collection;
020import java.util.Date;
021import java.util.HashMap;
022import java.util.LinkedHashSet;
023import java.util.Map;
024import java.util.Set;
025import java.util.UUID;
026
027import javax.servlet.http.HttpSession;
028
029import org.mitre.oauth2.exception.DeviceCodeCreationException;
030import org.mitre.oauth2.model.ClientDetailsEntity;
031import org.mitre.oauth2.model.DeviceCode;
032import org.mitre.oauth2.model.SystemScope;
033import org.mitre.oauth2.service.ClientDetailsEntityService;
034import org.mitre.oauth2.service.DeviceCodeService;
035import org.mitre.oauth2.service.SystemScopeService;
036import org.mitre.oauth2.token.DeviceTokenGranter;
037import org.mitre.openid.connect.config.ConfigurationPropertiesBean;
038import org.mitre.openid.connect.view.HttpCodeView;
039import org.mitre.openid.connect.view.JsonEntityView;
040import org.mitre.openid.connect.view.JsonErrorView;
041import org.slf4j.Logger;
042import org.slf4j.LoggerFactory;
043import org.springframework.beans.factory.annotation.Autowired;
044import org.springframework.http.HttpStatus;
045import org.springframework.http.MediaType;
046import org.springframework.security.access.prepost.PreAuthorize;
047import org.springframework.security.core.Authentication;
048import org.springframework.security.oauth2.common.exceptions.InvalidClientException;
049import org.springframework.security.oauth2.common.util.OAuth2Utils;
050import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
051import org.springframework.security.oauth2.provider.AuthorizationRequest;
052import org.springframework.security.oauth2.provider.OAuth2Authentication;
053import org.springframework.security.oauth2.provider.OAuth2Request;
054import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
055import org.springframework.stereotype.Controller;
056import org.springframework.ui.ModelMap;
057import org.springframework.web.bind.annotation.RequestMapping;
058import org.springframework.web.bind.annotation.RequestMethod;
059import org.springframework.web.bind.annotation.RequestParam;
060
061import com.google.common.collect.Sets;
062
063/**
064 * Implements https://tools.ietf.org/html/draft-ietf-oauth-device-flow
065 *
066 * @see DeviceTokenGranter
067 *
068 * @author jricher
069 *
070 */
071@Controller
072public class DeviceEndpoint {
073
074        public static final String URL = "devicecode";
075        public static final String USER_URL = "device";
076
077        public static final Logger logger = LoggerFactory.getLogger(DeviceEndpoint.class);
078
079        @Autowired
080        private ClientDetailsEntityService clientService;
081
082        @Autowired
083        private SystemScopeService scopeService;
084
085        @Autowired
086        private ConfigurationPropertiesBean config;
087
088        @Autowired
089        private DeviceCodeService deviceCodeService;
090
091        @Autowired
092        private OAuth2RequestFactory oAuth2RequestFactory;
093
094        @RequestMapping(value = "/" + URL, method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
095        public String requestDeviceCode(@RequestParam("client_id") String clientId, @RequestParam(name="scope", required=false) String scope, Map<String, String> parameters, ModelMap model) {
096
097                ClientDetailsEntity client;
098                try {
099                        client = clientService.loadClientByClientId(clientId);
100
101                        // make sure this client can do the device flow
102
103                        Collection<String> authorizedGrantTypes = client.getAuthorizedGrantTypes();
104                        if (authorizedGrantTypes != null && !authorizedGrantTypes.isEmpty()
105                                        && !authorizedGrantTypes.contains(DeviceTokenGranter.GRANT_TYPE)) {
106                                throw new InvalidClientException("Unauthorized grant type: " + DeviceTokenGranter.GRANT_TYPE);
107                        }
108
109                } catch (IllegalArgumentException e) {
110                        logger.error("IllegalArgumentException was thrown when attempting to load client", e);
111                        model.put(HttpCodeView.CODE, HttpStatus.BAD_REQUEST);
112                        return HttpCodeView.VIEWNAME;
113                }
114
115                if (client == null) {
116                        logger.error("could not find client " + clientId);
117                        model.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND);
118                        return HttpCodeView.VIEWNAME;
119                }
120
121                // make sure the client is allowed to ask for those scopes
122                Set<String> requestedScopes = OAuth2Utils.parseParameterList(scope);
123                Set<String> allowedScopes = client.getScope();
124
125                if (!scopeService.scopesMatch(allowedScopes, requestedScopes)) {
126                        // client asked for scopes it can't have
127                        logger.error("Client asked for " + requestedScopes + " but is allowed " + allowedScopes);
128                        model.put(HttpCodeView.CODE, HttpStatus.BAD_REQUEST);
129                        model.put(JsonErrorView.ERROR, "invalid_scope");
130                        return JsonErrorView.VIEWNAME;
131                }
132
133                // if we got here the request is legit
134
135                try {
136                        DeviceCode dc = deviceCodeService.createNewDeviceCode(requestedScopes, client, parameters);
137        
138                        Map<String, Object> response = new HashMap<>();
139                        response.put("device_code", dc.getDeviceCode());
140                        response.put("user_code", dc.getUserCode());
141                        response.put("verification_uri", config.getIssuer() + USER_URL);
142                        if (client.getDeviceCodeValiditySeconds() != null) {
143                                response.put("expires_in", client.getDeviceCodeValiditySeconds());
144                        }
145        
146                        model.put(JsonEntityView.ENTITY, response);
147        
148        
149                        return JsonEntityView.VIEWNAME;
150                } catch (DeviceCodeCreationException dcce) {
151                        
152                        model.put(HttpCodeView.CODE, HttpStatus.BAD_REQUEST);
153                        model.put(JsonErrorView.ERROR, dcce.getError());
154                        model.put(JsonErrorView.ERROR_MESSAGE, dcce.getMessage());
155                        
156                        return JsonErrorView.VIEWNAME;
157                }
158
159        }
160
161        @PreAuthorize("hasRole('ROLE_USER')")
162        @RequestMapping(value = "/" + USER_URL, method = RequestMethod.GET)
163        public String requestUserCode(ModelMap model) {
164
165                // print out a page that asks the user to enter their user code
166                // user must be logged in
167
168                return "requestUserCode";
169        }
170
171        @PreAuthorize("hasRole('ROLE_USER')")
172        @RequestMapping(value = "/" + USER_URL + "/verify", method = RequestMethod.POST)
173        public String readUserCode(@RequestParam("user_code") String userCode, ModelMap model, HttpSession session) {
174
175                // look up the request based on the user code
176                DeviceCode dc = deviceCodeService.lookUpByUserCode(userCode);
177
178                // we couldn't find the device code
179                if (dc == null) {
180                        model.addAttribute("error", "noUserCode");
181                        return "requestUserCode";
182                }
183
184                // make sure the code hasn't expired yet
185                if (dc.getExpiration() != null && dc.getExpiration().before(new Date())) {
186                        model.addAttribute("error", "expiredUserCode");
187                        return "requestUserCode";
188                }
189
190                // make sure the device code hasn't already been approved
191                if (dc.isApproved()) {
192                        model.addAttribute("error", "userCodeAlreadyApproved");
193                        return "requestUserCode";
194                }
195
196                ClientDetailsEntity client = clientService.loadClientByClientId(dc.getClientId());
197
198                model.put("client", client);
199                model.put("dc", dc);
200
201                // pre-process the scopes
202                Set<SystemScope> scopes = scopeService.fromStrings(dc.getScope());
203
204                Set<SystemScope> sortedScopes = new LinkedHashSet<>(scopes.size());
205                Set<SystemScope> systemScopes = scopeService.getAll();
206
207                // sort scopes for display based on the inherent order of system scopes
208                for (SystemScope s : systemScopes) {
209                        if (scopes.contains(s)) {
210                                sortedScopes.add(s);
211                        }
212                }
213
214                // add in any scopes that aren't system scopes to the end of the list
215                sortedScopes.addAll(Sets.difference(scopes, systemScopes));
216
217                model.put("scopes", sortedScopes);
218
219                AuthorizationRequest authorizationRequest = oAuth2RequestFactory.createAuthorizationRequest(dc.getRequestParameters());
220
221                session.setAttribute("authorizationRequest", authorizationRequest);
222                session.setAttribute("deviceCode", dc);
223
224                return "approveDevice";
225        }
226
227        @PreAuthorize("hasRole('ROLE_USER')")
228        @RequestMapping(value = "/" + USER_URL + "/approve", method = RequestMethod.POST)
229        public String approveDevice(@RequestParam("user_code") String userCode, @RequestParam(value = "user_oauth_approval") Boolean approve, ModelMap model, Authentication auth, HttpSession session) {
230
231                AuthorizationRequest authorizationRequest = (AuthorizationRequest) session.getAttribute("authorizationRequest");
232                DeviceCode dc = (DeviceCode) session.getAttribute("deviceCode");
233
234                // make sure the form that was submitted is the one that we were expecting
235                if (!dc.getUserCode().equals(userCode)) {
236                        model.addAttribute("error", "userCodeMismatch");
237                        return "requestUserCode";
238                }
239
240                // make sure the code hasn't expired yet
241                if (dc.getExpiration() != null && dc.getExpiration().before(new Date())) {
242                        model.addAttribute("error", "expiredUserCode");
243                        return "requestUserCode";
244                }
245
246                ClientDetailsEntity client = clientService.loadClientByClientId(dc.getClientId());
247
248                model.put("client", client);
249
250                // user did not approve
251                if (!approve) {
252                        model.addAttribute("approved", false);
253                        return "deviceApproved";
254                }
255
256                // create an OAuth request for storage
257                OAuth2Request o2req = oAuth2RequestFactory.createOAuth2Request(authorizationRequest);
258                OAuth2Authentication o2Auth = new OAuth2Authentication(o2req, auth);
259                
260                DeviceCode approvedCode = deviceCodeService.approveDeviceCode(dc, o2Auth);
261                
262                // pre-process the scopes
263                Set<SystemScope> scopes = scopeService.fromStrings(dc.getScope());
264
265                Set<SystemScope> sortedScopes = new LinkedHashSet<>(scopes.size());
266                Set<SystemScope> systemScopes = scopeService.getAll();
267
268                // sort scopes for display based on the inherent order of system scopes
269                for (SystemScope s : systemScopes) {
270                        if (scopes.contains(s)) {
271                                sortedScopes.add(s);
272                        }
273                }
274
275                // add in any scopes that aren't system scopes to the end of the list
276                sortedScopes.addAll(Sets.difference(scopes, systemScopes));
277
278                model.put("scopes", sortedScopes);
279                model.put("approved", true);
280
281                return "deviceApproved";
282        }
283}