/*
* oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text.
*
* Copyright (c) 2014, Gluu
*/
package org.xdi.oxauth.model.registration;
import org.apache.commons.lang.StringUtils;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.jboss.resteasy.client.ClientRequest;
import org.jboss.resteasy.client.ClientResponse;
import org.slf4j.Logger;
import org.xdi.oxauth.model.common.SubjectType;
import org.xdi.oxauth.model.configuration.AppConfiguration;
import org.xdi.oxauth.model.error.ErrorResponseFactory;
import org.xdi.oxauth.model.register.ApplicationType;
import org.xdi.oxauth.model.register.RegisterErrorResponseType;
import org.xdi.oxauth.model.util.URLPatternList;
import org.xdi.oxauth.model.util.Util;
import org.xdi.oxauth.util.ServerUtil;
import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.inject.Named;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import java.net.ConnectException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Validates the parameters received for the register web service.
*
* @author Javier Rojas Blum
* @version April 19, 2017
*/
@Stateless
@Named
public class RegisterParamsValidator {
@Inject
private Logger log;
@Inject
private AppConfiguration appConfiguration;
private static final String HTTP = "http";
private static final String HTTPS = "https";
private static final String LOCALHOST = "localhost";
private static final String LOOPBACK = "127.0.0.1";
/**
* Validates the parameters for a register request.
*
* @param applicationType The Application Type: native or web.
* @param subjectType The subject_type requested for responses to this Client.
* @param redirectUris Space-separated list of redirect URIs.
* @param sectorIdentifierUrl A HTTPS scheme URL to be used in calculating Pseudonymous Identifiers by the OP.
* The URL contains a file with a single JSON array of redirect_uri values.
* @return Whether the parameters of client register is valid or not.
*/
public boolean validateParamsClientRegister(ApplicationType applicationType, SubjectType subjectType,
List<String> redirectUris, String sectorIdentifierUrl) {
boolean valid = applicationType != null && redirectUris != null && !redirectUris.isEmpty();
if (subjectType == null || !appConfiguration.getSubjectTypesSupported().contains(subjectType.toString())) {
log.debug("Parameter subject_type is not valid.");
valid = false;
}
return valid;
}
/**
* Validates the parameters for a client read request.
*
* @param clientId Unique Client identifier.
* @param accessToken Access Token obtained out of band to authorize the registrant.
* @return Whether the parameters of client read is valid or not.
*/
public boolean validateParamsClientRead(String clientId, String accessToken) {
return StringUtils.isNotBlank(clientId) && StringUtils.isNotBlank(accessToken);
}
/**
* @param applicationType The Application Type: native or web.
* @param subjectType Subject Type requested for responses to this Client.
* @param redirectUris Redirection URI values used by the Client.
* @param sectorIdentifierUrl A HTTPS scheme URL to be used in calculating Pseudonymous Identifiers by the OP.
* The URL contains a file with a single JSON array of redirect_uri values.
* @return Whether the Redirect URI parameters are valid or not.
*/
public boolean validateRedirectUris(ApplicationType applicationType, SubjectType subjectType,
List<String> redirectUris, String sectorIdentifierUrl) {
boolean valid = true;
Set<String> redirectUriHosts = new HashSet<String>();
try {
if (redirectUris != null && !redirectUris.isEmpty()) {
for (String redirectUri : redirectUris) {
if (redirectUri == null || redirectUri.contains("#")) {
valid = false;
} else {
URI uri = new URI(redirectUri);
redirectUriHosts.add(uri.getHost());
switch (applicationType) {
case WEB:
if (HTTP.equalsIgnoreCase(uri.getScheme())) {
if (!LOCALHOST.equalsIgnoreCase(uri.getHost()) && !LOOPBACK.equalsIgnoreCase(uri.getHost())) {
log.error("Invalid protocol for redirect_uri: " +
redirectUri +
" (only https protocol is allowed for application_type=web or localhost/127.0.0.1 for http)");
valid = false;
}
}
break;
case NATIVE:
// to conform "OAuth 2.0 for Native Apps" https://tools.ietf.org/html/draft-wdenniss-oauth-native-apps-00
// we allow registration with custom schema for native apps.
// if (!HTTP.equalsIgnoreCase(uri.getScheme())) {
// valid = false;
// } else if (!LOCALHOST.equalsIgnoreCase(uri.getHost())) {
// valid = false;
// }
break;
}
}
}
} else {
valid = false;
}
} catch (URISyntaxException e) {
valid = false;
}
/*
* Providers that use pairwise sub (subject) values SHOULD utilize the sector_identifier_uri value
* provided in the Subject Identifier calculation for pairwise identifiers.
*
* If the Client has not provided a value for sector_identifier_uri in Dynamic Client Registration,
* the Sector Identifier used for pairwise identifier calculation is the host component of the
* registered redirect_uri.
*
* If there are multiple hostnames in the registered redirect_uris, the Client MUST register a
* sector_identifier_uri.
*/
if (subjectType != null && subjectType.equals(SubjectType.PAIRWISE) && StringUtils.isBlank(sectorIdentifierUrl)) {
if (redirectUriHosts.size() > 1) {
valid = false;
}
}
// Validate Sector Identifier URL
if (valid && StringUtils.isNotBlank(sectorIdentifierUrl)) {
try {
URI uri = new URI(sectorIdentifierUrl);
if (!HTTPS.equalsIgnoreCase(uri.getScheme())) {
valid = false;
}
ClientRequest clientRequest = new ClientRequest(sectorIdentifierUrl);
clientRequest.setHttpMethod(HttpMethod.GET);
ClientResponse<String> clientResponse = clientRequest.get(String.class);
int status = clientResponse.getStatus();
if (status == 200) {
String entity = clientResponse.getEntity(String.class);
JSONArray sectorIdentifierJsonArray = new JSONArray(entity);
valid = Util.asList(sectorIdentifierJsonArray).containsAll(redirectUris);
}
} catch (URISyntaxException e) {
log.trace(e.getMessage(), e);
valid = false;
} catch (UnknownHostException e) {
log.trace(e.getMessage(), e);
valid = false;
} catch (ConnectException e) {
log.trace(e.getMessage(), e);
valid = false;
} catch (JSONException e) {
log.trace(e.getMessage(), e);
valid = false;
} catch (Exception e) {
log.trace(e.getMessage(), e);
valid = false;
}
}
// Validate Redirect Uris checking the white list and black list
if (valid) {
valid = checkWhiteListRedirectUris(redirectUris) && checkBlackListRedirectUris(redirectUris);
}
return valid;
}
/**
* All the Redirect Uris must match to return true.
*/
private boolean checkWhiteListRedirectUris(List<String> redirectUris) {
boolean valid = true;
List<String> whiteList = appConfiguration.getClientWhiteList();
URLPatternList urlPatternList = new URLPatternList(whiteList);
for (String redirectUri : redirectUris) {
valid &= urlPatternList.isUrlListed(redirectUri);
}
return valid;
}
/**
* None of the Redirect Uris must match to return true.
*/
private boolean checkBlackListRedirectUris(List<String> redirectUris) {
boolean valid = true;
List<String> blackList = appConfiguration.getClientBlackList();
URLPatternList urlPatternList = new URLPatternList(blackList);
for (String redirectUri : redirectUris) {
valid &= !urlPatternList.isUrlListed(redirectUri);
}
return valid;
}
public void validateLogoutUri(List<String> logoutUris, List<String> redirectUris, ErrorResponseFactory errorResponseFactory) {
if (logoutUris == null || logoutUris.isEmpty()) { // logout uri is optional so null or empty list is valid
return;
}
for (String logoutUri : logoutUris) {
validateLogoutUri(logoutUri, redirectUris, errorResponseFactory);
}
}
public void validateLogoutUri(String logoutUri, List<String> redirectUris, ErrorResponseFactory errorResponseFactory) {
if (Util.isNullOrEmpty(logoutUri)) { // logout uri is optional so null or empty string is valid
return;
}
// preconditions
if (redirectUris == null || redirectUris.isEmpty()) {
log.error("Preconditions of logout uri validation are failed.");
throwInvalidLogoutUri(errorResponseFactory);
return;
}
try {
Set<String> redirectUriHosts = collectUriHosts(redirectUris);
URI uri = new URI(logoutUri);
if (!redirectUriHosts.contains(uri.getHost())) {
log.error("logout uri host is not within redirect_uris, logout_uri: {}, redirect_uris: {}", logoutUri, redirectUris);
throwInvalidLogoutUri(errorResponseFactory);
return;
}
if (!HTTPS.equalsIgnoreCase(uri.getScheme())) {
log.error("logout uri schema is not https, logout_uri: {}", logoutUri);
throwInvalidLogoutUri(errorResponseFactory);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
throwInvalidLogoutUri(errorResponseFactory);
}
}
private void throwInvalidLogoutUri(ErrorResponseFactory errorResponseFactory) throws WebApplicationException {
throw new WebApplicationException(
Response.status(Response.Status.BAD_REQUEST.getStatusCode()).
entity(errorResponseFactory.getErrorAsJson(RegisterErrorResponseType.INVALID_LOGOUT_URI)).
cacheControl(ServerUtil.cacheControl(true, false)).
header("Pragma", "no-cache").
build());
}
private Set<String> collectUriHosts(List<String> uriList) throws URISyntaxException {
Set<String> hosts = new HashSet<String>();
for (String redirectUri : uriList) {
URI uri = new URI(redirectUri);
hosts.add(uri.getHost());
}
return hosts;
}
}