package org.ovirt.engine.core.sso.utils;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateFactory;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import javax.net.ssl.TrustManagerFactory;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.util.InetAddressUtils;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.codehaus.jackson.map.DeserializationConfig.Feature;
import org.codehaus.jackson.map.ObjectMapper;
import org.ovirt.engine.api.extensions.ExtMap;
import org.ovirt.engine.api.extensions.aaa.Authn;
import org.ovirt.engine.api.extensions.aaa.Authz;
import org.ovirt.engine.core.uutils.crypto.EnvelopeEncryptDecrypt;
import org.ovirt.engine.core.uutils.crypto.EnvelopePBE;
import org.ovirt.engine.core.uutils.net.HttpClientBuilder;
import org.ovirt.engine.core.uutils.net.URLBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
public class SsoUtils {
private static Logger log = LoggerFactory.getLogger(SsoUtils.class);
private static SecureRandom secureRandom;
static {
try {
secureRandom = SecureRandom.getInstance("SHA1PRNG");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
// We need to create an HTTP client for each SSO client, as they may have different SSL configuration
// parameters. They will be stored in this map, indexed by client id.
private static final Map<String, CloseableHttpClient> CLIENTS = new HashMap<>();
static {
// Remember to close the clients when going down:
Runtime.getRuntime().addShutdownHook(
new Thread(() -> {
CLIENTS.values().forEach(IOUtils::closeQuietly);
CLIENTS.clear();
})
);
}
public static boolean isUserAuthenticated(HttpServletRequest request) {
return getSsoSession(request).getStatus() == SsoSession.Status.authenticated;
}
public static void redirectToModule(HttpServletRequest request,
HttpServletResponse response)
throws IOException {
log.debug("Entered redirectToModule");
try {
SsoSession ssoSession = getSsoSession(request);
URLBuilder redirectUrl = new URLBuilder(getRedirectUrl(request))
.addParameter("code", ssoSession.getAuthorizationCode());
String appUrl = ssoSession.getAppUrl();
if (StringUtils.isNotEmpty(appUrl)) {
redirectUrl.addParameter("app_url", appUrl);
}
String state = ssoSession.getState();
if (StringUtils.isNotEmpty(state)) {
redirectUrl.addParameter("state", state);
}
response.sendRedirect(redirectUrl.build());
log.debug("Redirecting back to module: {}", redirectUrl);
} catch (Exception ex) {
log.error("Error redirecting back to module: {}", ex.getMessage());
log.debug("Exception", ex);
throw new RuntimeException(ex);
} finally {
getSsoSession(request).cleanup();
}
}
public static String getRedirectUrl(HttpServletRequest request) throws Exception {
String uri = getSsoSession(request, true).getRedirectUri();
return StringUtils.isEmpty(uri) ?
new URLBuilder(getSsoContext(request).getEngineUrl(), "/oauth2-callback").build() : uri;
}
public static void redirectToErrorPage(HttpServletRequest request, HttpServletResponse response, Exception ex) {
log.error(ex.getMessage());
log.debug("Exception", ex);
redirectToErrorPageImpl(request,
response,
new OAuthException(SsoConstants.ERR_CODE_SERVER_ERROR, ex.getMessage(), ex));
}
public static void redirectToErrorPage(HttpServletRequest request,
HttpServletResponse response,
OAuthException ex) {
log.error("OAuthException {}: {}", ex.getCode(), ex.getMessage());
log.debug("Exception", ex);
redirectToErrorPageImpl(request, response, ex);
}
private static void redirectToErrorPageImpl(
HttpServletRequest request,
HttpServletResponse response,
OAuthException ex) {
log.debug("Entered redirectToErrorPage");
SsoSession ssoSession = null;
try {
ssoSession = getSsoSession(request, true);
if (ssoSession.getStatus() != SsoSession.Status.authenticated) {
ssoSession.setStatus(SsoSession.Status.unauthenticated);
}
String redirectUrl = new URLBuilder(getRedirectUrl(request))
.addParameter("error_code", ex.getCode())
.addParameter("error", ex.getMessage()).build();
response.sendRedirect(redirectUrl);
log.debug("Redirecting back to module: {}", redirectUrl);
} catch (Exception e) {
log.error("Error redirecting to error page: {}", e.getMessage());
log.debug("Exception", e);
throw new RuntimeException(ex);
} finally {
if (ssoSession != null) {
ssoSession.cleanup();
}
}
}
public static String generateAuthorizationToken() {
String ssoTokenId;
try {
byte[] s = new byte[64];
SecureRandom.getInstance("SHA1PRNG").nextBytes(s);
ssoTokenId = new Base64(0, new byte[0], true).encodeToString(s);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
return ssoTokenId;
}
public static String getJson(Object obj) throws IOException {
ObjectMapper mapper = new ObjectMapper().configure(Feature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.enableDefaultTyping(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE);
mapper.getSerializationConfig().addMixInAnnotations(ExtMap.class, JsonExtMapMixIn.class);
return mapper.writeValueAsString(obj);
}
public static String[] getClientIdClientSecret(HttpServletRequest request) throws Exception {
String[] retVal = new String[2];
retVal[0] = request.getParameter(SsoConstants.HTTP_PARAM_CLIENT_ID);
retVal[1] = request.getParameter(SsoConstants.HTTP_PARAM_CLIENT_SECRET);
if (StringUtils.isEmpty(retVal[0]) && StringUtils.isEmpty(retVal[1])) {
retVal = getClientIdClientSecretFromHeader(request);
}
if (StringUtils.isEmpty(retVal[0])) {
throw new OAuthException(SsoConstants.ERR_CODE_INVALID_REQUEST,
String.format(SsoConstants.ERR_CODE_INVALID_REQUEST_MSG, SsoConstants.HTTP_PARAM_CLIENT_ID));
}
if (StringUtils.isEmpty(retVal[1])) {
throw new OAuthException(SsoConstants.ERR_CODE_INVALID_REQUEST,
String.format(SsoConstants.ERR_CODE_INVALID_REQUEST_MSG, SsoConstants.HTTP_PARAM_CLIENT_SECRET));
}
return retVal;
}
public static String getClientId(HttpServletRequest request) {
String clientId = null;
String[] retVal = getClientIdClientSecretFromHeader(request);
if (retVal != null &&
StringUtils.isNotEmpty(retVal[0]) &&
getSsoContext(request).getClienInfo(retVal[0]) != null) {
clientId = retVal[0];
}
return clientId;
}
public static String[] getClientIdClientSecretFromHeader(HttpServletRequest request) {
String[] retVal = new String[2];
String header = request.getHeader(SsoConstants.HEADER_AUTHORIZATION);
if (StringUtils.isNotEmpty(header) && header.startsWith("Basic")) {
String[] creds = new String(
Base64.decodeBase64(header.substring("Basic".length())),
StandardCharsets.UTF_8
).split(":", 2);
if (creds.length == 2) {
retVal = creds;
}
}
return retVal;
}
public static String getFormParameter(HttpServletRequest request, String paramName)
throws UnsupportedEncodingException {
String value = request.getParameter(paramName);
return value == null ? null : new String(value.getBytes(StandardCharsets.ISO_8859_1));
}
public static String getRequestParameter(HttpServletRequest request, String paramName) throws Exception {
String value = request.getParameter(paramName);
if (value == null) {
throw new OAuthException(SsoConstants.ERR_CODE_INVALID_REQUEST,
String.format(SsoConstants.ERR_CODE_INVALID_REQUEST_MSG, paramName));
}
return value;
}
public static String getRequestParameters(HttpServletRequest request) {
StringBuilder value = new StringBuilder("");
try {
Enumeration<String> paramNames = request.getParameterNames();
String paramName;
while (paramNames.hasMoreElements()) {
paramName = paramNames.nextElement();
value.append(String.format("%s = %s, ", paramName, getRequestParameter(request, paramName)));
}
} catch (Exception ex) {
log.debug("Unable to get parameters from request");
}
return value.toString();
}
public static String getRequestParameter(HttpServletRequest request, String paramName, String defaultValue) {
String value;
try {
value = getRequestParameter(request, paramName);
} catch (Exception ex) {
log.debug("Parameter {} not found request, using default value", paramName);
value = defaultValue;
}
return value;
}
public static String getScopeRequestParameter(HttpServletRequest request, String defaultValue) {
return resolveScopeWithDependencies(getSsoContext(request),
getRequestParameter(request, SsoConstants.HTTP_PARAM_SCOPE, defaultValue));
}
public static String resolveScopeWithDependencies(SsoContext context, String scopes) {
Set<String> scopesSet = new TreeSet<>();
for (String scope : scopeAsList(scopes)) {
scopesSet.add(scope);
scopesSet.addAll(context.getScopeDependencies(scope));
}
return StringUtils.join(scopesSet, " ");
}
public static SsoContext getSsoContext(HttpServletRequest request) {
return (SsoContext) request.getServletContext().getAttribute(SsoConstants.OVIRT_SSO_CONTEXT);
}
public static SsoContext getSsoContext(ServletContext ctx) {
return (SsoContext) ctx.getAttribute(SsoConstants.OVIRT_SSO_CONTEXT);
}
public static SsoSession getSsoSessionFromRequest(HttpServletRequest request, String token) {
return getSsoSession(request, null, token, false);
}
public static SsoSession getSsoSession(HttpServletRequest request, String token, boolean mustExist) {
return getSsoSession(request, null, token, mustExist);
}
public static SsoSession getSsoSession(
HttpServletRequest request,
String clientId,
String token,
boolean mustExist) {
TokenCleanupUtility.cleanupExpiredTokens(request.getServletContext());
SsoContext ssoContext = getSsoContext(request);
SsoSession ssoSession = null;
if (StringUtils.isNotEmpty(token)) {
ssoSession = getSsoContext(request).getSsoSession(token);
if (ssoSession != null) {
ssoSession.touch();
}
}
if (mustExist && ssoSession == null) {
throw new OAuthException(SsoConstants.ERR_CODE_INVALID_GRANT,
ssoContext.getLocalizationUtils().localize(
SsoConstants.APP_ERROR_INVALID_GRANT,
(Locale) request.getAttribute(SsoConstants.LOCALE)));
}
if (StringUtils.isNotEmpty(clientId) &&
StringUtils.isNotEmpty(ssoSession.getClientId()) &&
!ssoSession.getClientId().equals(clientId)) {
throw new OAuthException(SsoConstants.ERR_CODE_UNAUTHORIZED_CLIENT,
SsoConstants.ERR_CODE_UNAUTHORIZED_CLIENT);
}
return ssoSession;
}
public static SsoSession getSsoSession(HttpServletRequest request) {
SsoContext ssoContext = getSsoContext(request);
SsoSession ssoSession = request.getSession(false) == null ?
null : (SsoSession) request.getSession().getAttribute(SsoConstants.OVIRT_SSO_SESSION);
// If the session has expired, attempt to extract the session from SsoContext persisted session
if (ssoSession == null) {
try {
ssoSession = getSsoContext(request).getSsoSessionById(
getFormParameter(request, "sessionIdToken"));
// If the server is restarted the session will be missing from SsoContext
if (ssoSession == null) {
throw new OAuthException(SsoConstants.ERR_CODE_INVALID_GRANT,
ssoContext.getLocalizationUtils().localize(
SsoConstants.APP_ERROR_SESSION_EXPIRED,
(Locale) request.getAttribute(SsoConstants.LOCALE)));
}
HttpSession session = request.getSession(true);
session.setAttribute(SsoConstants.OVIRT_SSO_SESSION, ssoSession);
ssoSession.setHttpSession(session);
} catch (UnsupportedEncodingException ex) {
throw new OAuthException(SsoConstants.ERR_CODE_SERVER_ERROR,
ssoContext.getLocalizationUtils().localize(
SsoConstants.APP_ERROR_UNABLE_TO_DECODE_SESSION_ID_TOKEN,
(Locale) request.getAttribute(SsoConstants.LOCALE)));
}
}
return ssoSession;
}
public static String generateIdToken() throws NoSuchAlgorithmException {
byte[] s = new byte[8];
secureRandom.nextBytes(s);
return new Base64(0, new byte[0], true).encodeToString(s);
}
public static SsoSession getSsoSession(HttpServletRequest request, boolean mustExist)
throws UnsupportedEncodingException {
SsoSession ssoSession = request.getSession(false) == null ?
null : (SsoSession) request.getSession().getAttribute(SsoConstants.OVIRT_SSO_SESSION);
if ((ssoSession == null || StringUtils.isEmpty(ssoSession.getClientId())) && mustExist) {
ssoSession = ssoSession == null ? new SsoSession() : ssoSession;
ssoSession.setAppUrl(getRequestParameter(request, SsoConstants.HTTP_PARAM_APP_URL, ""));
ssoSession.setClientId(getClientId(request));
ssoSession.setScope(getScopeRequestParameter(request, ""));
ssoSession.setRedirectUri(request.getParameter(SsoConstants.HTTP_PARAM_REDIRECT_URI));
}
return ssoSession;
}
public static Credentials getUserCredentialsFromHeader(HttpServletRequest request) {
String header = request.getHeader(SsoConstants.HEADER_AUTHORIZATION);
Credentials credentials = null;
if (StringUtils.isNotEmpty(header)) {
String[] creds = new String(
Base64.decodeBase64(header.substring("Basic".length())),
StandardCharsets.UTF_8
).split(":", 2);
if (creds.length == 2) {
credentials = translateUser(creds[0], creds[1], getSsoContext(request));
}
}
return credentials;
}
public static boolean areCredentialsValid(HttpServletRequest request, Credentials credentials)
throws AuthenticationException {
return areCredentialsValid(request, credentials, false);
}
public static boolean areCredentialsValid(HttpServletRequest request,
Credentials credentials,
boolean isInteractiveAuth) throws AuthenticationException {
SsoContext ssoContext = getSsoContext(request);
if (StringUtils.isEmpty(credentials.getUsername())) {
throw new AuthenticationException(ssoContext.getLocalizationUtils().localize(
isInteractiveAuth ? SsoConstants.APP_ERROR_NO_USER_NAME_IN_CREDENTIALS_INTERACTIVE_AUTH
: SsoConstants.APP_ERROR_NO_USER_NAME_IN_CREDENTIALS,
(Locale) request.getAttribute(SsoConstants.LOCALE)));
}
if (!credentials.isProfileValid()) {
throw new AuthenticationException(ssoContext.getLocalizationUtils().localize(
SsoConstants.APP_ERROR_NO_VALID_PROFILE_IN_CREDENTIALS,
(Locale) request.getAttribute(SsoConstants.LOCALE)));
}
if (StringUtils.isEmpty(credentials.getProfile())) {
throw new AuthenticationException(ssoContext.getLocalizationUtils().localize(
SsoConstants.APP_ERROR_NO_PROFILE_IN_CREDENTIALS,
(Locale) request.getAttribute(SsoConstants.LOCALE)));
}
return true;
}
public static Credentials translateUser(String user, String password, SsoContext ssoContext) {
Credentials credentials = new Credentials();
String username = user;
int separator = user.lastIndexOf("@");
if (separator != -1) {
username = user.substring(0, separator);
String profile = user.substring(separator + 1);
if (StringUtils.isNotEmpty(profile)) {
credentials.setProfile(profile);
credentials.setProfileValid(ssoContext.getSsoProfiles().contains(profile));
}
}
credentials.setUsername(username);
credentials.setPassword(password);
return credentials;
}
public static String getUserId(ExtMap principalRecord) {
String principal = principalRecord.get(Authz.PrincipalRecord.PRINCIPAL);
return principal != null ? principal : principalRecord.get(Authz.PrincipalRecord.NAME);
}
public static void persistUserPassword(
HttpServletRequest request,
SsoSession ssoSession,
String password) {
try {
if (ssoSession.getScopeAsList().contains("ovirt-ext=token:password-access") &&
password != null &&
StringUtils.isNotEmpty(ssoSession.getClientId())) {
ssoSession.setPassword(encrypt(request.getServletContext(), ssoSession.getClientId(), password));
}
} catch (Exception ex) {
log.error("Unable to encrypt password: {}", ex.getMessage());
log.debug("Exception", ex);
}
}
public static SsoSession persistAuthInfoInContextWithToken(
HttpServletRequest request,
String password,
String profileName,
ExtMap authRecord,
ExtMap principalRecord) throws Exception {
String validTo = authRecord.get(Authn.AuthRecord.VALID_TO);
String authCode = generateAuthorizationToken();
String accessToken = generateAuthorizationToken();
SsoSession ssoSession = getSsoSession(request, true);
ssoSession.setAccessToken(accessToken);
ssoSession.setAuthorizationCode(authCode);
request.setAttribute(SsoConstants.HTTP_REQ_ATTR_ACCESS_TOKEN, accessToken);
ssoSession.setActive(true);
ssoSession.setAuthRecord(authRecord);
ssoSession.setAutheticatedCredentials(ssoSession.getTempCredentials());
getSsoContext(request).registerSsoSession(ssoSession);
ssoSession.setPrincipalRecord(principalRecord);
ssoSession.setProfile(profileName);
ssoSession.setStatus(SsoSession.Status.authenticated);
ssoSession.setTempCredentials(null);
ssoSession.setUserId(getUserId(principalRecord));
try {
ssoSession.setValidTo(validTo == null ?
Long.MAX_VALUE : new SimpleDateFormat("yyyyMMddHHmmssZ").parse(validTo).getTime());
} catch (Exception ex) {
log.error("Unable to parse Auth Record valid_to value: {}", ex.getMessage());
log.debug("Exception", ex);
}
persistUserPassword(request, ssoSession, password);
ssoSession.touch();
return ssoSession;
}
public static void validateClientAcceptHeader(HttpServletRequest request) {
String acceptHeader = request.getHeader("Accept");
if (StringUtils.isEmpty(acceptHeader) || !acceptHeader.equals("application/json")) {
throw new OAuthException(SsoConstants.ERR_CODE_INVALID_REQUEST,
String.format(SsoConstants.ERR_CODE_INVALID_REQUEST_MSG, "Accept Header"));
}
}
public static void validateClientRequest(
HttpServletRequest request,
String clientId,
String clientSecret,
String scope,
String redirectUri) {
try {
SsoContext ssoContext = getSsoContext(request);
ClientInfo clientInfo = ssoContext.getClienInfo(clientId);
if (clientInfo == null) {
throw new OAuthException(SsoConstants.ERR_CODE_UNAUTHORIZED_CLIENT,
SsoConstants.ERR_CODE_UNAUTHORIZED_CLIENT_MSG);
}
if (!clientInfo.isTrusted()) {
throw new OAuthException(SsoConstants.ERR_CODE_ACCESS_DENIED,
SsoConstants.ERR_CODE_ACCESS_DENIED_MSG);
}
if (StringUtils.isNotEmpty(clientSecret) && !EnvelopePBE.check(clientInfo.getClientSecret(), clientSecret)) {
throw new OAuthException(SsoConstants.ERR_CODE_INVALID_REQUEST,
String.format(SsoConstants.ERR_CODE_INVALID_REQUEST_MSG, SsoConstants.HTTP_PARAM_CLIENT_SECRET));
}
if (StringUtils.isNotEmpty(scope)) {
validateScope(clientInfo.getScope(), scope);
}
if (StringUtils.isNotEmpty(redirectUri) &&
ssoContext.getSsoLocalConfig().getBoolean("SSO_CALLBACK_PREFIX_CHECK")) {
List<String> allowedPrefixes = new ArrayList<>(scopeAsList(clientInfo.getCallbackPrefix()));
scopeAsList(ssoContext.getSsoLocalConfig().getProperty("SSO_ALTERNATE_ENGINE_FQDNS"))
.forEach(fqdn -> allowedPrefixes.add(String.format("https://%s", fqdn)));
boolean isValidUri = false;
for (String allowedPrefix : allowedPrefixes) {
if (redirectUri.toLowerCase().startsWith(allowedPrefix.toLowerCase())) {
isValidUri = true;
break;
}
}
if (!isValidUri) {
throw new OAuthException(SsoConstants.ERR_CODE_UNAUTHORIZED_CLIENT,
SsoConstants.ERR_CODE_UNAUTHORIZED_CLIENT_MSG);
}
}
} catch (OAuthException ex) {
throw ex;
} catch (Exception ex) {
log.error("Internal Server Error: {}", ex.getMessage());
log.debug("Exception", ex);
throw new OAuthException(SsoConstants.ERR_CODE_SERVER_ERROR, ex.getMessage());
}
}
public static void validateRequestScope(HttpServletRequest req, String token, String scope) {
if (StringUtils.isNotEmpty(scope)) {
SsoSession ssoSession = getSsoSessionFromRequest(req, token);
if (ssoSession != null && ssoSession.getScope() != null) {
validateScope(ssoSession.getScopeAsList(), scope);
}
}
}
public static void validateScope(List<String> scope, String requestScope) {
List<String> strippedScope = strippedScopeAsList(scope);
List<String> requestedScope = strippedScopeAsList(scopeAsList(requestScope));
if (!strippedScope.containsAll(requestedScope)) {
throw new OAuthException(SsoConstants.ERR_CODE_INVALID_SCOPE,
String.format(SsoConstants.ERR_CODE_INVALID_SCOPE_MSG, requestedScope));
}
}
public static void sendJsonDataWithMessage(
HttpServletResponse response,
String errorCode,
Exception ex) throws IOException {
sendJsonDataWithMessage(response, new OAuthException(errorCode, ex.getMessage(), ex));
}
public static void sendJsonDataWithMessage(HttpServletResponse response, OAuthException ex) throws IOException {
sendJsonDataWithMessage(response, ex, false);
}
public static void sendJsonDataWithMessage(
HttpServletResponse response,
OAuthException ex,
boolean isValidateRequest) throws IOException {
if (isValidateRequest) {
log.debug("OAuthException {}: {}", ex.getCode(), ex.getMessage());
} else {
log.error("OAuthException {}: {}", ex.getCode(), ex.getMessage());
}
log.debug("Exception", ex);
Map<String, Object> errorData = new HashMap<>();
errorData.put(SsoConstants.ERROR_CODE, ex.getCode());
errorData.put(SsoConstants.ERROR, ex.getMessage());
sendJsonData(response, errorData);
}
public static void sendJsonData(HttpServletResponse response, Map<String, Object> payload) throws IOException {
try (OutputStream os = response.getOutputStream()) {
Map<String, Object> ovirtData = (Map<String, Object>) payload.get("ovirt");
if (ovirtData != null) {
Collection<ExtMap> groupIds = (Collection<ExtMap>) ovirtData.get("group_ids");
if (groupIds != null) {
ovirtData.put("group_ids", prepareGroupMembershipsForJson(groupIds));
}
}
String jsonPayload = getJson(payload);
response.setContentType("application/json");
byte[] jsonPayloadBytes = jsonPayload.getBytes(StandardCharsets.UTF_8.name());
response.setContentLength(jsonPayloadBytes.length);
os.write(jsonPayloadBytes);
log.trace("Sending json data {}", jsonPayload);
}
}
public static List<String> strippedScopeAsList(List<String> scope) {
List<String> scopes = new ArrayList<>();
String[] tokens;
for (String s : scope) {
tokens = s.split("=", 3);
if (tokens.length == 1) {
scopes.add(tokens[0]);
} else if (!tokens[1].equals("auth:identity")) {
scopes.add(tokens[0] + "=" + tokens[1]);
}
}
return scopes;
}
public static List<String> scopeAsList(String scope) {
return StringUtils.isEmpty(scope) ? Collections.emptyList() : Arrays.asList(scope.trim().split("\\s *"));
}
public static String encrypt(ServletContext ctx, String clientId, String rawText) throws Exception {
ClientInfo clientInfo = getSsoContext(ctx).getClienInfo(clientId);
try (InputStream in = new FileInputStream(clientInfo.getCertificateLocation())) {
return EnvelopeEncryptDecrypt.encrypt(
"AES/OFB/PKCS5Padding",
256,
CertificateFactory.getInstance("X.509").generateCertificate(in),
100,
rawText.getBytes(StandardCharsets.UTF_8));
}
}
public static void notifyClientsOfLogoutEvent(
SsoContext ssoContext,
Set<String> clientIdsForToken,
String token) throws Exception {
if (clientIdsForToken != null) {
for (String clientId : clientIdsForToken) {
notifyClientOfLogoutEvent(ssoContext, clientId, token);
}
}
}
private static void notifyClientOfLogoutEvent(
SsoContext ssoContext,
String clientId,
String token) throws Exception {
ClientInfo clientInfo = ssoContext.getClienInfo(clientId);
String url = clientInfo.getClientNotificationCallback();
if (StringUtils.isNotEmpty(url)) {
HttpPost request = createPost(url);
List<BasicNameValuePair> form = new ArrayList<>(3);
form.add(new BasicNameValuePair("event", "logout"));
form.add(new BasicNameValuePair("token", token));
form.add(new BasicNameValuePair("token_type", "bearer"));
request.setEntity(new UrlEncodedFormEntity(form, StandardCharsets.UTF_8));
execute(request, ssoContext, clientId);
}
}
private static HttpPost createPost(String url) throws Exception {
HttpPost request = new HttpPost();
request.setURI(new URI(url));
request.setHeader("Accept", "application/json");
return request;
}
private static void execute(HttpUriRequest request, SsoContext ssoContext, String clientId) throws Exception {
// Get or create the HTTP client corresponding to the given SSO client:
CloseableHttpClient client;
synchronized (CLIENTS) {
client = CLIENTS.get(clientId);
if (client == null) {
client = createClient(ssoContext, clientId);
CLIENTS.put(clientId, client);
}
}
// Execute the request and discard completely the response:
try (CloseableHttpResponse response = client.execute(request)) {
EntityUtils.consumeQuietly(response.getEntity());
}
}
private static CloseableHttpClient createClient(SsoContext ssoContext, String clientId) throws Exception {
SsoLocalConfig config = ssoContext.getSsoLocalConfig();
ClientInfo clientInfo = ssoContext.getClienInfo(clientId);
return new HttpClientBuilder()
.setSslProtocol(clientInfo.getNotificationCallbackProtocol())
.setPoolSize(config.getInteger("SSO_CALLBACK_CLIENT_POOL_SIZE"))
.setReadTimeout(config.getInteger("SSO_CALLBACK_READ_TIMEOUT"))
.setConnectTimeout(config.getInteger("SSO_CALLBACK_CONNECT_TIMEOUT"))
.setRetryCount(config.getInteger("SSO_CALLBACK_CONNECTION_RETRY_COUNT"))
.setTrustManagerAlgorithm(TrustManagerFactory.getDefaultAlgorithm())
.setTrustStore(config.getProperty("ENGINE_HTTPS_PKI_TRUST_STORE"))
.setTrustStorePassword(config.getProperty("ENGINE_HTTPS_PKI_TRUST_STORE_PASSWORD"))
.setTrustStoreType(config.getProperty("ENGINE_HTTPS_PKI_TRUST_STORE_TYPE"))
.setValidateAfterInactivity(config.getInteger("SSO_CALLBACK_CONNECTION_VALIDATE_AFTER_INACTIVITY"))
.setVerifyChain(clientInfo.isNotificationCallbackVerifyChain())
.setVerifyHost(clientInfo.isNotificationCallbackVerifyHost())
.build();
}
/**
* Currently jackson doesn't provide a way how to serialize graphs with cyclic references between nodes, which
* may happen if those cyclic dependencies exists among nested groups which is a user member of. So in order to
* serialize to JSON successfully we do the following:
* 1. If a principal is a direct member of a group, than put into group record key
* {@code Authz.PrincipalRecord.PRINCIPAL}
* 2. Change group memberships to contain only IDs of groups and not full group records by changing list in
* {@code Authz.GroupRecord.GROUPS} from {@code Collection<ExtMap>} to {@code Collection<String>}
* 3. Return all referenced group records as a set
* The whole process needs to be reversed on engine side, see
* {@code org.ovirt.engine.core.aaa.SsoOAuthServiceUtils.processGroupMembershipsFromJson()}
*/
public static Collection<ExtMap> prepareGroupMembershipsForJson(Collection<ExtMap> groupRecords) {
Map<String, ExtMap> resolvedGroups = new HashMap<>();
for (ExtMap origRecord : groupRecords) {
if (!resolvedGroups.containsKey(origRecord.<String>get(Authz.GroupRecord.ID))) {
ExtMap groupRecord = new ExtMap(origRecord);
groupRecord.put(Authz.PrincipalRecord.PRINCIPAL, "");
resolvedGroups.put(groupRecord.get(Authz.GroupRecord.ID), groupRecord);
groupRecord.put(
Authz.GroupRecord.GROUPS,
processGroupMemberships(
groupRecord.get(
Authz.GroupRecord.GROUPS,
Collections.emptyList()),
resolvedGroups
));
}
}
return new ArrayList<>(resolvedGroups.values());
}
private static SecureRandom random = new SecureRandom();
private static byte[] sharedSecret = new byte[32];
static {
random.nextBytes(sharedSecret);
}
public static String createJWT(HttpServletRequest request, SsoSession ssoSession, String clientId)
throws NoSuchAlgorithmException, JOSEException {
String serverName = request.getServerName();
String issuer = String.format("%s://%s:%s",
request.getScheme(),
InetAddressUtils.isIPv6Address(serverName) ? String.format("[%s]", serverName) : serverName,
request.getServerPort());
// Compose the JWT claims set
JWTClaimsSet jwtClaims = new JWTClaimsSet.Builder()
.jwtID(ssoSession.getPrincipalRecord().get(Authz.PrincipalRecord.ID))
.issueTime(new Date(System.currentTimeMillis()))
.issuer(issuer)
.subject(String.format("%s@%s", ssoSession.getUserId(), ssoSession.getProfile()))
.audience(clientId)
.claim("sub", String.format("%s@%s", ssoSession.getUserId(), ssoSession.getProfile()))
.claim("preferred_username", String.format("%s@%s", ssoSession.getUserId(), ssoSession.getProfile()))
.claim("email", ssoSession.getPrincipalRecord().<String>get(Authz.PrincipalRecord.EMAIL))
.claim("name", ssoSession.getPrincipalRecord().<String>get(Authz.PrincipalRecord.FIRST_NAME))
.build();
// Create HMAC signer
JWSSigner signer = new MACSigner(sharedSecret);
SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), jwtClaims);
signedJWT.sign(signer);
return signedJWT.serialize();
}
private static Set<String> processGroupMemberships(
Collection<ExtMap> memberships,
Map<String, ExtMap> resolvedGroups) {
Set<String> membershipIds = new HashSet<>();
for (ExtMap origRecord : memberships) {
ExtMap groupRecord = new ExtMap(origRecord);
membershipIds.add(groupRecord.get(Authz.GroupRecord.ID));
if (!resolvedGroups.containsKey(groupRecord.<String>get(Authz.GroupRecord.ID))) {
resolvedGroups.put(groupRecord.get(Authz.GroupRecord.ID), groupRecord);
groupRecord.put(
Authz.GroupRecord.GROUPS,
processGroupMemberships(
groupRecord.get(
Authz.GroupRecord.GROUPS,
Collections.emptyList()),
resolvedGroups
));
}
}
return membershipIds;
}
}