/* Copyright (c) 2011 Danish Maritime Authority.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.maritimecloud.mms.server.security.impl;
import com.typesafe.config.Config;
import net.maritimecloud.mms.server.security.AuthenticationToken;
import net.maritimecloud.mms.server.security.AuthenticationTokenHandler;
import javax.naming.ldap.LdapName;
import javax.servlet.http.HttpServletRequest;
import java.security.cert.X509Certificate;
/**
* Implementation of the {@code AuthenticationTokenHandler} interface that attempts
* to extract the subject-dn principal of the X.509 client certificate
* <p/>
* If the "subject-dn-header" configuration option is set, then it is assumed that a proxy SSL-server
* (e.g. nginx) has already validated the client certificate and stamped the subject-DN in the given request header.
* <p/>
* If the "principal-rdn-attr" configuration option is set, then the given RDN attribute (e.g. "CN") will be extracted
* from the certificate and used as the principal.
*/
@SuppressWarnings("unused")
public class ClientCertAuthenticationTokenHandler implements AuthenticationTokenHandler {
private Config conf;
/** {@inheritDoc} */
@Override
public AuthenticationToken resolveAuthenticationToken(HttpServletRequest request) {
String rdnAttr = conf.hasPath("principal-rdn-attr") ? conf.getString("principal-rdn-attr") : null;
if (conf.hasPath("subject-dn-header")) {
// Check if the client certificate has already been validated by an SSL proxy (e.g. nginx)
// and the subject-dn stamped into a request header
String subjectDnHeader = request.getHeader(conf.getString("subject-dn-header"));
if (subjectDnHeader != null && subjectDnHeader.trim().length() > 0) {
return new SubjectDnAuthenticationToken(subjectDnHeader, rdnAttr);
}
} else {
// Returns the subject-dn principal of the X.509 client certificate
X509Certificate[] certs = getCertificates(request);
if (certs != null && certs.length > 0) {
return new X509CertificateAuthenticationToken(certs[0], rdnAttr);
}
}
// No principal resolved
return null;
}
/**
* Returns the certificates used for client SSL authentication
* @param request the servlet request
* @return the certificates used for client SSL authentication
*/
public X509Certificate[] getCertificates(HttpServletRequest request) {
return (X509Certificate[])request.getAttribute("javax.servlet.request.X509Certificate");
}
/** {@inheritDoc} */
@Override
public void init(Config conf) {
this.conf = conf;
}
/** {@inheritDoc} */
@Override
public Config getConf() {
return conf;
}
/**
* Base class for the certificate authentication tokens
*/
public abstract static class BaseCertificateAuthenticationToken implements AuthenticationToken {
String rdnAttr;
/** No-arg constructor */
public BaseCertificateAuthenticationToken() {
}
/**
* Constructor
* @param rdnAttr the RDN attribute name
*/
public BaseCertificateAuthenticationToken(String rdnAttr) {
this.rdnAttr = rdnAttr;
}
/**
* Extracts the configured RDN attribute from the given DN.
* If {@code rndAttr} is undefined or the attribute is not
* part of the DN, then the DN is returned.
*
* @param dn the principal DN
* @return the extracted RDN attribute value
*/
protected String getPrincipal(String dn) {
if (rdnAttr == null) {
return dn;
}
try {
return new LdapName(dn).getRdns().stream()
.filter(rdn -> rdn.getType().equalsIgnoreCase(rdnAttr))
.map(rdn -> rdn.getValue().toString())
.findFirst()
.orElse(dn);
} catch (Exception e) {
// Fall back to returning the DN
return dn;
}
}
public String getRdnAttr() {
return rdnAttr;
}
public void setRdnAttr(String rdnAttr) {
this.rdnAttr = rdnAttr;
}
}
/**
* An X.509 certificate authentication token
*/
public static class X509CertificateAuthenticationToken extends BaseCertificateAuthenticationToken {
private X509Certificate cert;
/** No-arg constructor */
public X509CertificateAuthenticationToken() {
}
/**
* Constructor
* @param cert the certificate
*/
public X509CertificateAuthenticationToken(X509Certificate cert, String rdnAttr) {
super(rdnAttr);
this.cert = cert;
}
/** {@inheritDoc} */
@Override
public Object getPrincipal() {
return getPrincipal(cert.getSubjectDN().getName());
}
/** {@inheritDoc} */
@Override
public Object getCredentials() {
return null;
}
public X509Certificate getCert() {
return cert;
}
public void setCert(X509Certificate cert) {
this.cert = cert;
}
}
/**
* A certificate subject DN authentication token.
* <p/>
* Used when a front-end SSL-proxy has handled the client certificate validation and
* stamped the subject DN into a request header.
*/
public static class SubjectDnAuthenticationToken extends BaseCertificateAuthenticationToken {
private String subjectDn;
/** No-arg constructor */
public SubjectDnAuthenticationToken() {
}
/**
* Constructor
* @param subjectDn the certificate subject DN
*/
public SubjectDnAuthenticationToken(String subjectDn, String rdnAttr) {
super(rdnAttr);
this.subjectDn = subjectDn;
}
/** {@inheritDoc} */
@Override
public Object getPrincipal() {
return getPrincipal(subjectDn);
}
/** {@inheritDoc} */
@Override
public Object getCredentials() {
return null;
}
public String getSubjectDn() {
return subjectDn;
}
public void setSubjectDn(String subjectDn) {
this.subjectDn = subjectDn;
}
}
}