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}