diff --git a/plugins/hal-assistant-google/src/se/hal/plugin/assistant/google/SmartHomeDaemon.java b/plugins/hal-assistant-google/src/se/hal/plugin/assistant/google/SmartHomeDaemon.java index a66715c8..38a7457c 100644 --- a/plugins/hal-assistant-google/src/se/hal/plugin/assistant/google/SmartHomeDaemon.java +++ b/plugins/hal-assistant-google/src/se/hal/plugin/assistant/google/SmartHomeDaemon.java @@ -62,7 +62,8 @@ public class SmartHomeDaemon implements HalDaemon { ); httpServer = new HttpServer(HalContext.getIntegerProperty(PARAM_PORT)); - httpServer.setPage(OAuth2AuthPage.ENDPOINT_URL, new OAuth2AuthPage(smartHome)); + httpServer.setPage(OAuth2AuthPage.ENDPOINT_URL, new OAuth2AuthPage(smartHome, + "https://oauth-redirect.googleusercontent.com/r/optimal-comfort-93608")); httpServer.setPage(OAuth2TokenPage.ENDPOINT_URL, new OAuth2TokenPage(smartHome)); httpServer.setPage(SmartHomePage.ENDPOINT_URL, new SmartHomePage(smartHome)); httpServer.start(); diff --git a/plugins/hal-assistant-google/src/se/hal/plugin/assistant/google/endpoint/OAuth2AuthPage.java b/plugins/hal-assistant-google/src/se/hal/plugin/assistant/google/endpoint/OAuth2AuthPage.java index 48d62c97..3dcbcd29 100644 --- a/plugins/hal-assistant-google/src/se/hal/plugin/assistant/google/endpoint/OAuth2AuthPage.java +++ b/plugins/hal-assistant-google/src/se/hal/plugin/assistant/google/endpoint/OAuth2AuthPage.java @@ -54,14 +54,47 @@ import java.util.Map; /** * This endpoint is the first step in the OAuth 2 procedure. * The purpose of this page is get authorization from the user to share a resource. + * + * RFC 6749: Chapter 4.1.2: + * https://tools.ietf.org/html/rfc6749 */ public class OAuth2AuthPage implements HttpPage { public static final String ENDPOINT_URL = "api/assistant/google/auth/authorize"; + /** The request is missing a required parameter, includes an invalid parameter value, includes a parameter + more than once, or is otherwise malformed. **/ + protected static final String ERROR_INVALID_REQUEST = "invalid_request"; + /** The client is not authorized to request an authorization code using this method. **/ + protected static final String ERROR_UNAUTHORIZED_CLIENT = "unauthorized_client"; + /** The resource owner or authorization server denied the request. **/ + protected static final String ERROR_ACCESS_DENIED = "access_denied"; + /** The authorization server does not support obtaining an authorization code using this method. **/ + protected static final String ERROR_UNSUPPORTED_RESP_TYPE = "unsupported_response_type"; + /** The requested scope is invalid, unknown, or malformed. **/ + protected static final String ERROR_INVALID_SCOPE = "invalid_scope"; + /** The authorization server encountered an unexpected condition that prevented it from fulfilling the request. + (This error code is needed because a 500 Internal Server Error HTTP status code cannot be returned to the client + via an HTTP redirect.) **/ + protected static final String ERROR_SERVER_ERROR = "server_error"; + /** The authorization server is currently unable to handle the request due to a temporary overloading or maintenance + of the server. (This error code is needed because a 503 Service Unavailable HTTP status code cannot be returned + to the client via an HTTP redirect.) **/ + protected static final String ERROR_TEMPORARILY_UNAVAILABLE = "temporarily_unavailable"; + + private static final String RESPONSE_TYPE_CODE = "code"; + private static final String RESPONSE_TYPE_PASSWORD = "password"; + private static final String RESPONSE_TYPE_CREDENTIALS = "client_credentials"; + protected static final String SECRET_CODE = "SUPER-SECURE-CODE"; + private String clientId; + + public OAuth2AuthPage(SmartHomeImpl smartHome) {} + public OAuth2AuthPage(SmartHomeImpl smartHome, String clientId) { + this.clientId = clientId; + } @Override @@ -73,29 +106,58 @@ public class OAuth2AuthPage implements HttpPage { Map request) throws MalformedURLException { if (!request.containsKey("redirect_uri")) { - out.setResponseStatusCode(400); - out.println("400: Bad Request, missing property: redirect_uri"); + errorResponse(out, "Bad Request, missing property: redirect_uri"); + return; + } + + if (!request.containsKey("client_id") || clientId != null && clientId.equals(request.containsKey("client_id"))) { + errorResponse(out, "Bad Request, missing or invalid client_id property."); return; } HttpURL url = new HttpURL(URLDecoder.decode(request.get("redirect_uri"), StandardCharsets.UTF_8)); if (!"HTTPS".equalsIgnoreCase(url.getProtocol())) { - out.setResponseStatusCode(400); - out.println("Bad redirect protocol: " + url.getProtocol()); + errorResponse(out, "Bad redirect protocol: " + url.getProtocol()); return; } - if ("code".equals(request.get("response_type"))) { // response types: code, password, client_credentials - url.setParameter("state", request.get("state")); - url.setParameter("code", SECRET_CODE); - - out.setResponseStatusCode(302); - out.setHeader(HttpHeader.HEADER_LOCATION, url.toString()); - } else { - out.setResponseStatusCode(400); - out.println("400: Bad Request, unsupported response_type: " + request.get("response_type")); - return; + switch (request.get("response_type")) { + case RESPONSE_TYPE_CODE: + url.setParameter("state", request.get("state")); + url.setParameter("code", SECRET_CODE); + break; + case RESPONSE_TYPE_PASSWORD: + case RESPONSE_TYPE_CREDENTIALS: + default: + errorRedirect(out, url, ERROR_INVALID_REQUEST, request.get("state"), + "unsupported response_type: " + request.get("response_type")); + return; } + + // Setup the redirect + + redirect(out, url); + } + + + private static void errorResponse(HttpPrintStream out, String description) { + out.setResponseStatusCode(400); + out.println(description); + } + + private static void errorRedirect(HttpPrintStream out, HttpURL url, String error, String state, String description) { + out.setHeader(HttpHeader.HEADER_CONTENT_TYPE, "application/x-www-form-urlencoded"); + url.setParameter("error", error); + if (description != null) url.setParameter("error_description", description); + //if (uri != null) url.setParameter("error_uri", uri); + if (state != null) url.setParameter("state", state); + + redirect(out, url); + } + + private static void redirect(HttpPrintStream out, HttpURL url) { + out.setResponseStatusCode(302); + out.setHeader(HttpHeader.HEADER_LOCATION, url.toString()); } } diff --git a/plugins/hal-assistant-google/src/se/hal/plugin/assistant/google/endpoint/OAuth2TokenPage.java b/plugins/hal-assistant-google/src/se/hal/plugin/assistant/google/endpoint/OAuth2TokenPage.java index f2d6176a..68df0d5c 100644 --- a/plugins/hal-assistant-google/src/se/hal/plugin/assistant/google/endpoint/OAuth2TokenPage.java +++ b/plugins/hal-assistant-google/src/se/hal/plugin/assistant/google/endpoint/OAuth2TokenPage.java @@ -51,15 +51,42 @@ import zutil.parser.DataNode; /** * This endpoint is the second step in the OAuth 2 procedure. * The purpose of this page is give a token that should be used for all consequent HTTP. + * + * RFC 6749: Chapter 4.1 */ public class OAuth2TokenPage extends HttpJsonPage { private static final int SECONDS_IN_DAY = 86400; public static final String ENDPOINT_URL = "api/assistant/google/auth/token"; + /** The request is missing a required parameter, includes an unsupported parameter value (other than grant type), + repeats a parameter, includes multiple credentials, utilizes more than one mechanism for authenticating the + client, or is otherwise malformed. **/ + protected static final String ERROR_INVALID_REQUEST = "invalid_request"; + /** Client authentication failed (e.g., unknown client, no client authentication included, or unsupported + authentication method). **/ + protected static final String ERROR_INVALID_CLIENT = "invalid_client"; + /** The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is + invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to + another client. **/ + protected static final String ERROR_INVALID_GRANT = "invalid_grant"; + /** The authenticated client is not authorized to use this authorization grant type. **/ + protected static final String ERROR_UNAUTHORIZED_CLIENT = "unauthorized_client"; + /** The authorization grant type is not supported by the authorization server. **/ + protected static final String ERROR_UNSUPPORTED_GRANT_TYPE = "unsupported_grant_type"; + /** The requested scope is invalid, unknown, malformed, or exceeds the scope granted by the resource owner. **/ + protected static final String ERROR_INVALID_SCOPE = "invalid_scope"; + protected final String ACCESS_TOKEN = "SUPER-SECURE-TOKEN"; + protected final String REFRESH_ACCESS_TOKEN = "SUPER-SECURE-REFRESH-TOKEN"; + + + private String clientId; public OAuth2TokenPage(SmartHomeImpl smartHome) {} + public OAuth2TokenPage(SmartHomeImpl smartHome, String clientId) { + this.clientId = clientId; + } @Override @@ -73,32 +100,48 @@ public class OAuth2TokenPage extends HttpJsonPage { // POST out.setHeader("Access-Control-Allow-Origin", "*"); + out.setHeader("Cache-Control", "no-store"); out.setHeader("Pragma", "no-cache"); DataNode jsonRes = new DataNode(DataNode.DataType.Map); - if (OAuth2AuthPage.SECRET_CODE.equals(request.get("code"))) { - jsonRes.set("refresh_token", "123refresh"); - } else { - out.setResponseStatusCode(400); - DataNode jsonErr = new DataNode(DataNode.DataType.Map); - jsonRes.set("error", "Invalid code value provided."); - return jsonErr; - } + if (!request.containsKey("client_id")) + return errorResponse(out, ERROR_INVALID_REQUEST , request.get("state"), "Missing mandatory parameter client_id."); - if ("authorization_code".equals(request.get("grant_type"))) { - jsonRes.set("refresh_token", "123refresh"); - } else { - out.setResponseStatusCode(400); - DataNode jsonErr = new DataNode(DataNode.DataType.Map); - jsonRes.set("error", "Unsupported grant_type: " + request.containsKey("grant_type")); - return jsonErr; + if (clientId != null && clientId.equals(request.containsKey("client_id"))) + return errorResponse(out, ERROR_INVALID_CLIENT , request.get("state"), "Invalid client_id provided."); + + if (!OAuth2AuthPage.SECRET_CODE.equals(request.get("code"))) + return errorResponse(out, ERROR_INVALID_GRANT, request.get("state"), "Invalid code value provided."); + + String grantType = request.get("grant_type"); + + switch (grantType) { + case "authorization_code": + jsonRes.set("refresh_token", REFRESH_ACCESS_TOKEN); + break; + default: + return errorResponse(out, ERROR_UNSUPPORTED_GRANT_TYPE, request.get("state"), "Unsupported grant_type: " + request.containsKey("grant_type")); } jsonRes.set("access_token", ACCESS_TOKEN); jsonRes.set("token_type", "bearer"); jsonRes.set("expires_in", SECONDS_IN_DAY); + //jsonRes.set("scope", SECONDS_IN_DAY); + if (request.containsKey("state")) jsonRes.set("state", request.get("state")); return jsonRes; } + + private static DataNode errorResponse(HttpPrintStream out, String error, String state, String description) { + out.setResponseStatusCode(400); + + DataNode jsonErr = new DataNode(DataNode.DataType.Map); + jsonErr.set("error", error); + if (description != null) jsonErr.set("error_description", description); + //if (uri != null) jsonErr.set("error_uri", uri); + if (state != null) jsonErr.set("state", state); + + return jsonErr; + } }