From a5b41151691d136e5f12f11b7e7cd0679222a137 Mon Sep 17 00:00:00 2001 From: Justin Richer Date: Thu, 9 Mar 2017 20:08:47 -0500 Subject: [PATCH] functioning device code flow --- .../oauth2/service/DeviceCodeService.java | 5 +- .../src/main/resources/db/hsql/clients.sql | 1 + .../webapp/WEB-INF/application-context.xml | 14 + .../src/main/webapp/WEB-INF/authz-config.xml | 1 + .../webapp/WEB-INF/views/approveDevice.jsp | 286 ++++++++++++++++++ .../webapp/WEB-INF/views/deviceApproved.jsp | 33 ++ .../webapp/WEB-INF/views/requestUserCode.jsp | 48 +++ .../resources/js/locale/en/messages.json | 9 +- .../AuthorizationPendingException.java | 50 +++ .../oauth2/token/DeviceTokenGranter.java | 19 +- .../org/mitre/oauth2/web/DeviceEndpoint.java | 42 ++- 11 files changed, 493 insertions(+), 15 deletions(-) create mode 100644 openid-connect-server-webapp/src/main/webapp/WEB-INF/views/approveDevice.jsp create mode 100644 openid-connect-server-webapp/src/main/webapp/WEB-INF/views/deviceApproved.jsp create mode 100644 openid-connect-server-webapp/src/main/webapp/WEB-INF/views/requestUserCode.jsp create mode 100644 openid-connect-server/src/main/java/org/mitre/oauth2/exception/AuthorizationPendingException.java diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/service/DeviceCodeService.java b/openid-connect-common/src/main/java/org/mitre/oauth2/service/DeviceCodeService.java index ed354744f..443bb674b 100644 --- a/openid-connect-common/src/main/java/org/mitre/oauth2/service/DeviceCodeService.java +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/service/DeviceCodeService.java @@ -19,6 +19,8 @@ package org.mitre.oauth2.service; import org.mitre.oauth2.model.DeviceCode; import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.OAuth2Request; /** * @author jricher @@ -39,8 +41,9 @@ public interface DeviceCodeService { /** * @param dc + * @param o2Auth */ - public DeviceCode approveDeviceCode(DeviceCode dc); + public DeviceCode approveDeviceCode(DeviceCode dc, OAuth2Authentication o2Auth); /** * @param deviceCode diff --git a/openid-connect-server-webapp/src/main/resources/db/hsql/clients.sql b/openid-connect-server-webapp/src/main/resources/db/hsql/clients.sql index 7e7a8a53d..1410f7bd1 100644 --- a/openid-connect-server-webapp/src/main/resources/db/hsql/clients.sql +++ b/openid-connect-server-webapp/src/main/resources/db/hsql/clients.sql @@ -28,6 +28,7 @@ INSERT INTO client_redirect_uri_TEMP (owner_id, redirect_uri) VALUES INSERT INTO client_grant_type_TEMP (owner_id, grant_type) VALUES ('client', 'authorization_code'), ('client', 'urn:ietf:params:oauth:grant_type:redelegate'), + ('client', 'urn:ietf:params:oauth:grant-type:device_code'), ('client', 'implicit'), ('client', 'refresh_token'); diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/application-context.xml b/openid-connect-server-webapp/src/main/webapp/WEB-INF/application-context.xml index bdbf0c52c..49cb8d3e9 100644 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/application-context.xml +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/application-context.xml @@ -138,6 +138,20 @@ + + + + + + + + + + + diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/approveDevice.jsp b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/approveDevice.jsp new file mode 100644 index 000000000..bcc6882ca --- /dev/null +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/approveDevice.jsp @@ -0,0 +1,286 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> +<%@ page import="org.springframework.security.core.AuthenticationException"%> +<%@ page import="org.springframework.security.oauth2.common.exceptions.UnapprovedClientAuthenticationException"%> +<%@ page import="org.springframework.security.web.WebAttributes"%> +<%@ taglib prefix="authz" uri="http://www.springframework.org/security/tags"%> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> +<%@ taglib prefix="o" tagdir="/WEB-INF/tags"%> +<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> + + + + +
+ <% if (session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION) != null && !(session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION) instanceof UnapprovedClientAuthenticationException)) { %> +
+ × + +

+ (<%= ((AuthenticationException) session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION)).getMessage() %>) +

+
+ <% } %> + + +
+

  + + + + + + + + +

+ +
+ +
+
+ + + + +
+

+ + + + +

+
+
+ + +
"> +

+ : +

+ +

+ +

+

+ + + + + + + + + + + +

+
+
+
+
+ + +
    +
  • + +
  • +
+ + +
+ +
+ + +
+ +
+
+
    + +
  • : ">
  • +
    + +
  • : ">
  • +
    + +
  • : ">
  • +
    + +
  • :
  • +
    +
+
+
+
+
+ + +
+ +
+
+ +
+
+
+ : + + +
+

+ : +

+

+ +

+
+
+ + + + + + + + + + + + + + + + + +
  • + : + +
  • +
    + +
    + " + > + + + + + + + + + +
    + +
    + +
    +

    + + " + + + + + + + "? +

    + + + + + + +   + +
    + + + +
    + + + diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/deviceApproved.jsp b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/deviceApproved.jsp new file mode 100644 index 000000000..a31898fe9 --- /dev/null +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/deviceApproved.jsp @@ -0,0 +1,33 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> +<%@ page import="org.springframework.security.core.AuthenticationException"%> +<%@ page import="org.springframework.security.oauth2.common.exceptions.UnapprovedClientAuthenticationException"%> +<%@ page import="org.springframework.security.web.WebAttributes"%> +<%@ taglib prefix="authz" uri="http://www.springframework.org/security/tags"%> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> +<%@ taglib prefix="o" tagdir="/WEB-INF/tags"%> +<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> + + + + +
    + +
    +

    + + + + + + + + +

    + +
    + +
    +
    + diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/requestUserCode.jsp b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/requestUserCode.jsp new file mode 100644 index 000000000..ec80d9b5d --- /dev/null +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/requestUserCode.jsp @@ -0,0 +1,48 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> +<%@ page import="org.springframework.security.core.AuthenticationException"%> +<%@ page import="org.springframework.security.oauth2.common.exceptions.UnapprovedClientAuthenticationException"%> +<%@ page import="org.springframework.security.web.WebAttributes"%> +<%@ taglib prefix="authz" uri="http://www.springframework.org/security/tags"%> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> +<%@ taglib prefix="o" tagdir="/WEB-INF/tags"%> +<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> + + + + +
    + +
    +

      + + + + + + + + + +

    + +
    + +
    +
    + + + + + +   + +
    +
    + +
    + +
    +
    + diff --git a/openid-connect-server-webapp/src/main/webapp/resources/js/locale/en/messages.json b/openid-connect-server-webapp/src/main/webapp/resources/js/locale/en/messages.json index 1a0c7aed6..99329298c 100644 --- a/openid-connect-server-webapp/src/main/webapp/resources/js/locale/en/messages.json +++ b/openid-connect-server-webapp/src/main/webapp/resources/js/locale/en/messages.json @@ -487,6 +487,13 @@ "password": "Password", "login-button": "Login", "error": "The system was unable to log you in. Please try again." + }, + "device": { + "request_code": { + "title": "Enter Code", + "header": "Enter code for ", + "description": "Enter the code displayed on your device into the box below and press submit", + "submit": "Submit" + } } - } \ No newline at end of file diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/exception/AuthorizationPendingException.java b/openid-connect-server/src/main/java/org/mitre/oauth2/exception/AuthorizationPendingException.java new file mode 100644 index 000000000..e2ea2f71b --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/exception/AuthorizationPendingException.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright 2017 The MITRE Corporation + * and the MIT Internet Trust Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ + +package org.mitre.oauth2.exception; + +import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; + +/** + * @author jricher + * + */ +public class AuthorizationPendingException extends OAuth2Exception { + + /** + * @param msg + */ + public AuthorizationPendingException(String msg) { + super(msg); + } + + /** + * + */ + private static final long serialVersionUID = -7078098692596870940L; + + /* (non-Javadoc) + * @see org.springframework.security.oauth2.common.exceptions.OAuth2Exception#getOAuth2ErrorCode() + */ + @Override + public String getOAuth2ErrorCode() { + return "authorization_pending"; + } + + + +} diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/token/DeviceTokenGranter.java b/openid-connect-server/src/main/java/org/mitre/oauth2/token/DeviceTokenGranter.java index 5fd7fbd34..f8252cab7 100644 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/token/DeviceTokenGranter.java +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/token/DeviceTokenGranter.java @@ -17,6 +17,7 @@ package org.mitre.oauth2.token; +import org.mitre.oauth2.exception.AuthorizationPendingException; import org.mitre.oauth2.model.DeviceCode; import org.mitre.oauth2.service.DeviceCodeService; import org.mitre.oauth2.web.DeviceEndpoint; @@ -29,6 +30,7 @@ import org.springframework.security.oauth2.provider.OAuth2RequestFactory; import org.springframework.security.oauth2.provider.TokenRequest; import org.springframework.security.oauth2.provider.token.AbstractTokenGranter; import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices; +import org.springframework.stereotype.Component; /** * Implements https://tools.ietf.org/html/draft-ietf-oauth-device-flow @@ -38,6 +40,7 @@ import org.springframework.security.oauth2.provider.token.AuthorizationServerTok * @author jricher * */ +@Component("deviceTokenGranter") public class DeviceTokenGranter extends AbstractTokenGranter { public static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code"; @@ -67,12 +70,20 @@ public class DeviceTokenGranter extends AbstractTokenGranter { DeviceCode dc = deviceCodeService.consumeDeviceCode(deviceCode, client); if (dc != null) { - // inherit the (approved) scopes from the original request - tokenRequest.setScope(dc.getScope()); - OAuth2Authentication auth = new OAuth2Authentication(getRequestFactory().createOAuth2Request(client, tokenRequest), dc.getAuthenticationHolder().getUserAuth()); + if (dc.isApproved()) { - return auth; + // inherit the (approved) scopes from the original request + tokenRequest.setScope(dc.getScope()); + + OAuth2Authentication auth = new OAuth2Authentication(getRequestFactory().createOAuth2Request(client, tokenRequest), dc.getAuthenticationHolder().getUserAuth()); + + return auth; + } else { + + // still waiting for approval + throw new AuthorizationPendingException("Authorization pending for code " + deviceCode); + } } else { throw new InvalidGrantException("Invalid device code: " + deviceCode); } diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/web/DeviceEndpoint.java b/openid-connect-server/src/main/java/org/mitre/oauth2/web/DeviceEndpoint.java index 18e83a167..975f0b073 100644 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/web/DeviceEndpoint.java +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/web/DeviceEndpoint.java @@ -23,6 +23,8 @@ import java.util.Map; import java.util.Set; import java.util.UUID; +import javax.servlet.http.HttpSession; + import org.mitre.oauth2.model.ClientDetailsEntity; import org.mitre.oauth2.model.DeviceCode; import org.mitre.oauth2.model.SystemScope; @@ -40,10 +42,15 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.common.exceptions.InvalidClientException; import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; import org.springframework.security.oauth2.common.util.OAuth2Utils; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; +import org.springframework.security.oauth2.provider.AuthorizationRequest; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.OAuth2Request; +import org.springframework.security.oauth2.provider.OAuth2RequestFactory; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.RequestMapping; @@ -64,7 +71,8 @@ import com.google.common.collect.Sets; @Controller public class DeviceEndpoint { - public static final String URL = "/device"; + public static final String URL = "device"; + public static final String USER_URL = "device-user"; public static final Logger logger = LoggerFactory.getLogger(DeviceEndpoint.class); @@ -80,9 +88,12 @@ public class DeviceEndpoint { @Autowired private DeviceCodeService deviceCodeService; + @Autowired + private OAuth2RequestFactory oAuth2RequestFactory; + private RandomValueStringGenerator randomGenerator = new RandomValueStringGenerator(); - @RequestMapping(value = URL, method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @RequestMapping(value = "/" + URL, method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public String requestDeviceCode(@RequestParam("client_id") String clientId, @RequestParam(name="scope", required=false) String scope, Map parameters, ModelMap model) { ClientDetailsEntity client; @@ -150,7 +161,7 @@ public class DeviceEndpoint { } @PreAuthorize("hasRole('ROLE_USER')") - @RequestMapping(value = URL, method = RequestMethod.GET) + @RequestMapping(value = "/" + USER_URL, method = RequestMethod.GET) public String requestUserCode(ModelMap model) { // print out a page that asks the user to enter their user code @@ -160,8 +171,8 @@ public class DeviceEndpoint { } @PreAuthorize("hasRole('ROLE_USER')") - @RequestMapping(value = URL + "/verify", method = RequestMethod.POST) - public String readUserCode(@RequestParam("userCode") String userCode, ModelMap model) { + @RequestMapping(value = "/" + USER_URL + "/verify", method = RequestMethod.POST) + public String readUserCode(@RequestParam("user_code") String userCode, ModelMap model, HttpSession session) { // look up the request based on the user code DeviceCode dc = deviceCodeService.lookUpByUserCode(userCode); @@ -189,17 +200,30 @@ public class DeviceEndpoint { model.put("scopes", sortedScopes); + AuthorizationRequest authorizationRequest = oAuth2RequestFactory.createAuthorizationRequest(dc.getRequestParameters()); + + session.setAttribute("authorizationRequest", authorizationRequest); + session.setAttribute("deviceCode", dc); + return "approveDevice"; } @PreAuthorize("hasRole('ROLE_USER')") - @RequestMapping(value = URL + "/approve", method = RequestMethod.POST) - public String approveDevice(@RequestParam("userCode") String userCode, @RequestParam(value = "approve", required = false) String approve, ModelMap model) { + @RequestMapping(value = "/" + USER_URL + "/approve", method = RequestMethod.POST) + public String approveDevice(@RequestParam("user_code") String userCode, @RequestParam(value = "user_oauth_approval") String approve, ModelMap model, Authentication auth, HttpSession session) { + AuthorizationRequest authorizationRequest = (AuthorizationRequest) session.getAttribute("authorizationRequest"); + DeviceCode dc = (DeviceCode) session.getAttribute("deviceCode"); - DeviceCode dc = deviceCodeService.lookUpByUserCode(userCode); + if (!dc.getUserCode().equals(userCode)) { + // TODO: return an error + return "error"; + } - DeviceCode approvedCode = deviceCodeService.approveDeviceCode(dc); + OAuth2Request o2req = oAuth2RequestFactory.createOAuth2Request(authorizationRequest); + OAuth2Authentication o2Auth = new OAuth2Authentication(o2req, auth); + + DeviceCode approvedCode = deviceCodeService.approveDeviceCode(dc, o2Auth); ClientDetailsEntity client = clientService.loadClientByClientId(dc.getClientId());