/*
* Copyright (C) 2013 Intel Corporation
* All rights reserved.
*/
package com.intel.mtwilson.shiro.jdbi;
import com.intel.dcsg.cpg.crypto.Sha1Digest;
import com.intel.dcsg.cpg.crypto.Sha256Digest;
import com.intel.mtwilson.shiro.*;
import com.intel.dcsg.cpg.io.UUID;
import com.intel.dcsg.cpg.net.NetUtils;
import com.intel.dcsg.cpg.rfc822.Rfc822Date;
import com.intel.dcsg.cpg.x509.X509Util;
import com.intel.mtwilson.shiro.authc.x509.Credential;
import com.intel.mtwilson.shiro.authc.x509.Fingerprint;
import com.intel.mtwilson.shiro.authc.x509.LoginCertificateId;
import com.intel.mtwilson.shiro.authc.x509.X509AuthenticationInfo;
import com.intel.mtwilson.shiro.authc.x509.X509AuthenticationToken;
import com.intel.mtwilson.shiro.jdbi.LoginDAO;
import com.intel.mtwilson.shiro.jdbi.MyJdbi;
import com.intel.mtwilson.user.management.rest.v2.model.Role;
import com.intel.mtwilson.user.management.rest.v2.model.RolePermission;
import com.intel.mtwilson.user.management.rest.v2.model.UserLoginCertificate;
import com.intel.mtwilson.user.management.rest.v2.model.User;
import java.net.SocketException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.time.DurationFormatUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
/**
* Example X509 Authorization header:
* <pre>
* Authorization: X509 fingerprint="H1PPayM0FcOHtdUhHOuZlBaeynVrYl9yJV3JqqAsMHc=", headers="X-Nonce,Date", algorithm="SHA256withRSA", signature="BRlDe76PQLkKWgG982Su+/wPdDRIOa6eKdstpxI6tPWKUod8H46yqaPSyapjufnOPuknJ6IXOBY42xSjD/Dl4Les/JciodI/4BGuThMZDPRft+hnijM2A876OX4L60J/pMW+1/s6Ar/zTofK0d4oOpGyyu2QdZ8pGMlRhUXejGEChRjBKYMpf0Z+EsTaRQqhya18G/NeqJufjx571X50JJE4UaX3MIkpiSsX+em9sCtMayvUBzfnaQDZcRG5/DDTnfsbPQaMhhOtpZ9W4xJYWH1/6BwWVT+PLRz0Ztpq5atDhZ82XEk92nwXY9hYJ/VpoBb3ZhCZUNIildEceW/TiQ=="
* </pre>
*
* @author jbuhacoff
*/
public class JdbcCertificateRealm extends AuthorizingRealm {
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(JdbcCertificateRealm.class);
private String localAddress;
public JdbcCertificateRealm() {
super();
try {
List<String> localAddresses = NetUtils.getNetworkAddressList();
if( localAddresses.isEmpty() ) {
localAddress = String.format("default-%s", getRandomNodeId());
}
else {
localAddress = localAddresses.get(0);
}
}
catch(SocketException e) {
log.debug("Cannot determine local network address", e);
localAddress = String.format("unknown-%s", getRandomNodeId());
}
}
private String getRandomNodeId() {
SecureRandom random = new SecureRandom();
String id = String.valueOf(random.nextInt());
return id;
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof X509AuthenticationToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection pc) {
if (pc == null) {
throw new AuthorizationException("Principal must be provided");
}
SimpleAuthorizationInfo authzInfo = new SimpleAuthorizationInfo();
for (String realmName : pc.getRealmNames()) {
log.debug("doGetAuthorizationInfo for realm: {}", realmName);
}
Collection<Username> usernames = pc.byType(Username.class);
for (Username username : usernames) {
log.debug("doGetAuthorizationInfo for username: {}", username.getUsername());
}
try (LoginDAO dao = MyJdbi.authz()) {
Collection<LoginCertificateId> loginCertificateIds = pc.byType(LoginCertificateId.class);
for (LoginCertificateId loginCertificateId : loginCertificateIds) {
log.debug("doGetAuthorizationInfo for login certificate id: {}", loginCertificateId.getLoginCertificateId());
List<Role> roles = dao.findRolesByUserLoginCertificateId(loginCertificateId.getLoginCertificateId());
HashSet<String> roleIds = new HashSet<>();
for (Role role : roles) {
log.debug("doGetAuthorizationInfo found role: {}", role.getRoleName());
roleIds.add(role.getId().toString());
authzInfo.addRole(role.getRoleName());
}
if (!roleIds.isEmpty()) {
List<RolePermission> permissions = dao.findRolePermissionsByCertificateRoleIds(roleIds);
for (RolePermission permission : permissions) {
log.debug("doGetAuthorizationInfo found permission: {} {} {}", permission.getPermitDomain(), permission.getPermitAction(), permission.getPermitSelection());
authzInfo.addStringPermission(String.format("%s:%s:%s", permission.getPermitDomain(), permission.getPermitAction(), permission.getPermitSelection()));
}
}
}
} catch (Exception e) {
log.debug("doGetAuthorizationInfo error", e);
throw new AuthenticationException("Internal server error", e);
}
return authzInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
X509AuthenticationToken xToken = (X509AuthenticationToken) token;
UserLoginCertificate userLoginCertificate = null;
User user = null;
if( xToken.getPrincipal() instanceof Fingerprint ) {
Fingerprint fingerprint = (Fingerprint)xToken.getPrincipal();
log.debug("doGetAuthenticationInfo for fingerprint {}", fingerprint.getHex());
try (LoginDAO dao = MyJdbi.authz()) {
// anti-replay protection: check the database to see if we have already received a request with this signature
// it should not affect legitimate clients because if they send the same logical request twice, the timestamp and
// nonce would be different which would result in a different digest and signature and thus would still have be unique
Credential credential = (Credential)xToken.getCredentials();
RequestLogEntry requestLogEntry = new RequestLogEntry();
requestLogEntry.setDigest(Base64.encodeBase64String(credential.getDigest()));
requestLogEntry.setSource(xToken.getHost());
requestLogEntry.setInstance(localAddress);
requestLogEntry.setReceived(new Date());
requestLogEntry.setContent(xToken.getSignatureInput().toString());
try {
// first, look at the date header in the request and compare to our current time; if it's earlier than
// the earliest entry in the request log, then we have to reject it in order to prevent replay attacks
// of messages that we already deleted from the request log.
// clients MUST include a date header in the request and cover it with the signature.
// clients SHOULD synchronize their clocks (at least for the requests) with our server clock to avoid rejecting messages unnecessarily
if( xToken.getSignatureInput().headers == null || !xToken.getSignatureInput().headers.containsKey("Date") ) {
log.debug("request does not include date header");
return null;
}
// second, check if the user has specified an expiration time for the request and enforce it
if( xToken.getSignatureInput().headers.containsKey("Expires") ) {
Date expires = Rfc822Date.parse(xToken.getSignatureInput().headers.get("Expires"));
if( requestLogEntry.getReceived().after(expires) ) {
log.debug("request has expired; must be rejected");
return null;
}
}
// third, ensure that the request is not earlier than the start of our anti-replay protection window (represented by the earliest request received in the request log)
Date requestDate = Rfc822Date.parse(xToken.getSignatureInput().headers.get("Date"));
RequestLogEntry earliest = dao.findRequestLogEntryByEarliestDate();
if( earliest != null && requestDate.before(earliest.getReceived()) ) {
log.debug("request date is before anti-replay window; must be rejected");
return null;
}
// fourth, try to insert the request into the log - it will fail if there is already a request with the same digest
log.debug("inserting request log entry with digest {} from source {} received at {} by {}", requestLogEntry.getDigest(), requestLogEntry.getSource(), requestLogEntry.getReceived(), requestLogEntry.getInstance());
dao.insertRequestLogEntry(requestLogEntry);
// so at this point, we have inserted a new request into the request log
// and it's protected from replay because the request digest is in the log so
// if a duplicate request comes in then it won't be able to insert (above)
}
catch(Exception e) {
log.debug("Cannot insert request log entry", e); // probably a duplicate, but could also be database connection issue
return null;
}
if( Sha256Digest.isValid(fingerprint.getBytes())) {
userLoginCertificate = dao.findUserLoginCertificateBySha256(fingerprint.getBytes());
}
else if( Sha1Digest.isValid(fingerprint.getBytes())) {
userLoginCertificate = dao.findUserLoginCertificateBySha1(fingerprint.getBytes());
}
else {
log.error("Unsupported digest length {}", fingerprint.getBytes().length);
}
if(userLoginCertificate != null && userLoginCertificate.isEnabled() ) {
user = dao.findUserById(userLoginCertificate.getUserId());
}
// xToken.
// userLoginCertificate = dao.findUserLoginCertificateByUsername(username);
} catch (Exception e) {
log.debug("doGetAuthenticationInfo error", e);
throw new AuthenticationException("Internal server error", e);
}
}
if (userLoginCertificate == null || user == null) {
return null;
}
log.debug("doGetAuthenticationInfo found user login certificate id {}", userLoginCertificate.getId());
SimplePrincipalCollection principals = new SimplePrincipalCollection();
principals.add(new UserId(userLoginCertificate.getUserId()), getName());
principals.add(new Username(user.getUsername()), getName());
principals.add(new LoginCertificateId(user.getUsername(), userLoginCertificate.getUserId(), userLoginCertificate.getId()), getName());
// should we add the Fingerprint principal? or is it enough to use LoginCertificateId ?
X509AuthenticationInfo info = new X509AuthenticationInfo();
info.setPrincipals(principals);
try {
X509Certificate decodeDerCertificate = X509Util.decodeDerCertificate(userLoginCertificate.getCertificate());
log.trace("Decoded DER certificate public key is {}", decodeDerCertificate.getPublicKey().toString());
info.setCredentials(X509Util.decodeDerCertificate(userLoginCertificate.getCertificate()));
}
catch(CertificateException e) {
throw new AuthenticationException("Invalid certificate", e);
}
return info;
}
}