Implementation of OAtuth pages

This commit is contained in:
Ziver Koc 2020-11-21 18:41:09 +01:00
parent fee8180690
commit 8af91a9169
3 changed files with 579 additions and 0 deletions

View file

@ -0,0 +1,198 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2020 Ziver Koc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/*
* The MIT License (MIT)
*
* Copyright (c) 2020 Ziver Koc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package zutil.net.http.page.oauth;
import zutil.log.LogUtil;
import zutil.net.http.HttpHeader;
import zutil.net.http.HttpPage;
import zutil.net.http.HttpPrintStream;
import zutil.net.http.HttpURL;
import java.net.MalformedURLException;
import java.net.URLDecoder;
import java.util.Map;
import java.util.Random;
import java.util.logging.Logger;
/**
* 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.
*
* <pre>
* From RFC:
* +---------+ +---------------+
* | |&#62;---(D)-- Authorization Code ----&#62;| |
* | Client | and Redirection URI | Authorization |
* | | | Server |
* | |&#60;---(E)----- Access Token -------&#60;| |
* +---------+ (w/ Optional Refresh Token) +---------------+
* </pre>
*
* @see <a href="https://tools.ietf.org/html/rfc6749#section-4">RFC 6749: Chapter 4</a>
*/
public class OAuth2AuthorizationPage implements HttpPage {
private static final Logger logger = LogUtil.getLogger();
/** 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";
private Random random = new Random();
private OAuth2Registry registry;
public OAuth2AuthorizationPage(OAuth2Registry registry) {
this.registry = registry;
}
@Override
public void respond(
HttpPrintStream out,
HttpHeader headers,
Map<String, Object> session,
Map<String, String> cookie,
Map<String, String> request) throws MalformedURLException {
if (!request.containsKey("redirect_uri")) {
errorResponse(out, "Bad Request, missing property: redirect_uri");
return;
}
String clientId = request.get("client_id");
if (registry.isClientIdValid(clientId)) {
errorResponse(out, "Bad Request, missing or invalid client_id value.");
return;
}
HttpURL url = new HttpURL(URLDecoder.decode(request.get("redirect_uri")));
if (!"HTTPS".equalsIgnoreCase(url.getProtocol())) {
errorResponse(out, "Bad redirect protocol: " + url.getProtocol());
return;
}
switch (request.get("response_type")) {
case RESPONSE_TYPE_CODE:
String code = generateCode();
registry.registerAuthorizationCode(clientId, code);
url.setParameter("state", request.get("state"));
url.setParameter("code", 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 String generateCode() {
return String.valueOf(random.nextInt());
}
// ------------------------------------------------------
// Error handling
// ------------------------------------------------------
private static void errorResponse(HttpPrintStream out, String description) {
out.setResponseStatusCode(400);
out.println(description);
}
/**
* @see <a href="https://tools.ietf.org/html/rfc6749#section-4.2.2.1">RFC 6749: Chapter 4.2.2.1</a>
*
* @param out
* @param url
* @param error
* @param state
* @param 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());
}
}

View file

@ -0,0 +1,196 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2020 Ziver Koc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/*
* The MIT License (MIT)
*
* Copyright (c) 2020 Ziver Koc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package zutil.net.http.page.oauth;
import zutil.Timer;
import java.util.HashMap;
import java.util.Map;
/**
* A data class containing authentication information for individual
* clients going through the OAuth 2 process.
*/
public class OAuth2Registry {
private static final long DEFAULT_TIMEOUT = 24 * 60 * 60 * 1000; // 24h
private Map<String, ClientRegister> clientRegistry = new HashMap<>();
private boolean requireWhitelist = false;
// ------------------------------------------------------
// Whitelist methods
// ------------------------------------------------------
/**
* Set the requirement or non-requirement of pre-registered client-ids.
* If enabled then any clients starting a OAuth2 process needs to have a
* preregistered client-id value in the registry object.
*
* @param enabled if true then all requests will be required to be in whitelist
*/
public void requireWhitelisting(boolean enabled) {
requireWhitelist = enabled;
}
/**
* Register a client-id to be whitelisted. Note this function
* has no impact if requireWhitelisting is set to false.
*
* @param clientId A String ID that should be whitelisted
*/
public void addWhitelist(String clientId) {
if (!clientRegistry.containsKey(clientId)) {
clientRegistry.put(clientId, new ClientRegister());
}
}
// ------------------------------------------------------
// Validation methods
// ------------------------------------------------------
/**
* Validates a client_id value is a valid value and is in the whitelist fro approved clients.
*
* @param clientId the client_id value to validate
* @return true if the client_id is allowed to start the OAuth process.
*/
public boolean isClientIdValid(String clientId) {
if (clientId == null)
return false;
if (!requireWhitelist)
return true;
return clientRegistry.containsKey(clientId);
}
/**
* Validates that a authorization code has valid format and has been authorized and not elapsed.
*
* @param clientId the id of the requesting client
* @param code the code that should be validated
* @return true if the given code is valid otherwise false.
*/
public boolean isAuthorizationCodeValid(String clientId, String code) {
if (clientId == null || code == null)
return false;
ClientRegister reg = getClientRegistry(clientId);
if (reg != null) {
return reg.authCodes.containsKey(code) &&
!reg.authCodes.get(code).hasTimedOut();
}
return false;
}
/**
* Validates that a access token has valid format and has been authorized and not elapsed.
*
* @param clientId the id of the requesting client
* @param token the token that should be validated
* @return true if the given token is valid otherwise false.
*/
public boolean isAccessTokenValid(String clientId, String token) {
if (clientId == null || token == null)
return false;
ClientRegister reg = getClientRegistry(clientId);
if (reg != null) {
return reg.accessTokens.containsKey(token) &&
!reg.accessTokens.get(token).hasTimedOut();
}
return false;
}
// ------------------------------------------------------
// OAuth2 process methods
// ------------------------------------------------------
protected long registerAuthorizationCode(String clientId, String code) {
return registerAuthorizationCode(clientId, code, DEFAULT_TIMEOUT);
}
protected long registerAuthorizationCode(String clientId, String code, long timeoutMillis) {
ClientRegister reg = getClientRegistry(clientId);
if (reg != null) {
reg.authCodes.put(code, new Timer(timeoutMillis));
return timeoutMillis;
}
return -1;
}
protected long registerAccessToken(String clientId, String token) {
return registerAccessToken(clientId, token, DEFAULT_TIMEOUT);
}
protected long registerAccessToken(String clientId, String token, long timeoutMillis) {
ClientRegister reg = getClientRegistry(clientId);
if (reg != null) {
reg.accessTokens.put(token, new Timer(timeoutMillis));
return timeoutMillis;
}
return -1;
}
// --------------------------------------------------------------------
private ClientRegister getClientRegistry(String clientId) {
if (!requireWhitelist && !clientRegistry.containsKey(clientId))
clientRegistry.put(clientId, new ClientRegister());
return clientRegistry.get(clientId);
}
private static class ClientRegister {
Map<String, Timer> authCodes = new HashMap<>();
Map<String, Timer> accessTokens = new HashMap<>();
}
}

View file

@ -0,0 +1,185 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2020 Ziver Koc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/*
* The MIT License (MIT)
*
* Copyright (c) 2020 Ziver Koc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package zutil.net.http.page.oauth;
import java.util.Map;
import java.util.Random;
import java.util.logging.Logger;
import zutil.log.LogUtil;
import zutil.net.http.HttpHeader;
import zutil.net.http.HttpPrintStream;
import zutil.net.http.page.HttpJsonPage;
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.
*
* <pre>
* From RFC:
* +---------+ +---------------+
* | | | |
* | |&#62;--(A)- Client Authentication ---&#62;| Authorization |
* | Client | | Server |
* | |&#60;--(B)---- Access Token ---------&#60;| |
* | | | |
* +---------+ +---------------+
* </pre>
*
*
* @see <a href="https://tools.ietf.org/html/rfc6749#section-5">RFC 6749: Chapter 5</a>
*/
public class OAuth2TokenPage extends HttpJsonPage {
private static final Logger logger = LogUtil.getLogger();
/** 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";
private Random random = new Random();
private OAuth2Registry registry;
public OAuth2TokenPage(OAuth2Registry registry) {
this.registry = registry;
}
@Override
public DataNode jsonRespond(
HttpPrintStream out,
HttpHeader headers,
Map<String, Object> session,
Map<String, String> cookie,
Map<String, String> request) {
// 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 (!request.containsKey("client_id"))
return errorResponse(out, ERROR_INVALID_REQUEST , request.get("state"), "Missing mandatory parameter client_id.");
String clientId = request.get("client_id");
if (registry.isClientIdValid(clientId))
return errorResponse(out, ERROR_INVALID_CLIENT , request.get("state"), "Invalid client_id value.");
if (!registry.isAuthorizationCodeValid(clientId, request.get("code")))
return errorResponse(out, ERROR_INVALID_GRANT, request.get("state"), "Invalid authorization code value provided.");
String grantType = request.get("grant_type");
switch (grantType) {
case "authorization_code":
jsonRes.set("refresh_token", "TODO"); // TODO: implement refresh logic
break;
default:
return errorResponse(out, ERROR_UNSUPPORTED_GRANT_TYPE, request.get("state"), "Unsupported grant_type: " + request.containsKey("grant_type"));
}
String token = generateToken();
long timeoutMillis = registry.registerAccessToken(clientId, token);
jsonRes.set("access_token", token);
jsonRes.set("token_type", "bearer");
jsonRes.set("expires_in", timeoutMillis/1000);
//jsonRes.set("scope", ?);
if (request.containsKey("state")) jsonRes.set("state", request.get("state"));
return jsonRes;
}
private String generateToken() {
return String.valueOf(random.nextInt());
}
// ------------------------------------------------------
// Error handling
// ------------------------------------------------------
/**
* @see <a href="https://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749: Chapter 5.2</a>
*
* @param out
* @param error
* @param state
* @param description
* @return A DataNode containing the error response
*/
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;
}
}