blob: 465e370a7c153d45f6d6ec52ca04b2571729a7d8 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.knox.gateway.services.token.impl;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PublicKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.security.auth.Subject;
import org.apache.knox.gateway.GatewayResources;
import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.i18n.resources.ResourcesFactory;
import org.apache.knox.gateway.services.Service;
import org.apache.knox.gateway.services.ServiceLifecycleException;
import org.apache.knox.gateway.services.security.AliasService;
import org.apache.knox.gateway.services.security.AliasServiceException;
import org.apache.knox.gateway.services.security.KeystoreService;
import org.apache.knox.gateway.services.security.KeystoreServiceException;
import org.apache.knox.gateway.services.security.token.JWTokenAuthority;
import org.apache.knox.gateway.services.security.token.TokenServiceException;
import org.apache.knox.gateway.services.security.token.impl.JWT;
import org.apache.knox.gateway.services.security.token.impl.JWTToken;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.jwk.source.RemoteJWKSet;
import com.nimbusds.jose.proc.BadJOSEException;
import com.nimbusds.jose.proc.JWSKeySelector;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import com.nimbusds.jwt.proc.JWTClaimsSetVerifier;
public class DefaultTokenAuthorityService implements JWTokenAuthority, Service {
private static final GatewayResources RESOURCES = ResourcesFactory.get(GatewayResources.class);
private static final Set<String> SUPPORTED_SIG_ALGS = new HashSet<>();
private AliasService as;
private KeystoreService ks;
private GatewayConfig config;
private char[] cachedSigningKeyPassphrase;
private RSAPrivateKey signingKey;
static {
// Only standard RSA signature algorithms are accepted
// https://tools.ietf.org/html/rfc7518
SUPPORTED_SIG_ALGS.add("RS256");
SUPPORTED_SIG_ALGS.add("RS384");
SUPPORTED_SIG_ALGS.add("RS512");
SUPPORTED_SIG_ALGS.add("PS256");
SUPPORTED_SIG_ALGS.add("PS384");
SUPPORTED_SIG_ALGS.add("PS512");
}
public void setKeystoreService(KeystoreService ks) {
this.ks = ks;
}
public void setAliasService(AliasService as) {
this.as = as;
}
@Override
public JWT issueToken(Subject subject, String algorithm) throws TokenServiceException {
Principal p = (Principal) subject.getPrincipals().toArray()[0];
return issueToken(p, algorithm);
}
@Override
public JWT issueToken(Principal p, String algorithm) throws TokenServiceException {
return issueToken(p, null, algorithm);
}
@Override
public JWT issueToken(Principal p, String algorithm, long expires) throws TokenServiceException {
return issueToken(p, (String)null, algorithm, expires);
}
@Override
public JWT issueToken(Principal p, String audience, String algorithm)
throws TokenServiceException {
return issueToken(p, audience, algorithm, -1);
}
@Override
public JWT issueToken(Principal p, String audience, String algorithm, long expires)
throws TokenServiceException {
List<String> audiences = null;
if (audience != null) {
audiences = new ArrayList<>();
audiences.add(audience);
}
return issueToken(p, audiences, algorithm, expires);
}
@Override
public JWT issueToken(Principal p, List<String> audiences, String algorithm, long expires)
throws TokenServiceException {
return issueToken(p, audiences, algorithm, expires, null, null, null);
}
private RSAPrivateKey getSigningKey(final String signingKeystoreName,
final String signingKeystoreAlias,
final char[] signingKeystorePassphrase)
throws KeystoreServiceException, TokenServiceException {
if (signingKeystorePassphrase != null) {
return (RSAPrivateKey) ks.getSigningKey(signingKeystoreName,
getSigningKeyAlias(signingKeystoreAlias),
getSigningKeyPassphrase(signingKeystorePassphrase));
}
return signingKey;
}
@Override
public JWT issueToken(Principal p, List<String> audiences, String algorithm, long expires,
String signingKeystoreName, String signingKeystoreAlias, char[] signingKeystorePassphrase)
throws TokenServiceException {
String[] claimArray = new String[4];
claimArray[0] = "KNOXSSO";
claimArray[1] = p.getName();
claimArray[2] = null;
if (expires == -1) {
claimArray[3] = null;
}
else {
claimArray[3] = String.valueOf(expires);
}
JWT token;
if (SUPPORTED_SIG_ALGS.contains(algorithm)) {
token = new JWTToken(algorithm, claimArray, audiences);
try {
RSAPrivateKey key = getSigningKey(signingKeystoreName, signingKeystoreAlias, signingKeystorePassphrase);
// allowWeakKey to not break existing 1024 bit certificates
JWSSigner signer = new RSASSASigner(key, true);
token.sign(signer);
} catch (KeystoreServiceException e) {
throw new TokenServiceException(e);
}
}
else {
throw new TokenServiceException("Cannot issue token - Unsupported algorithm");
}
return token;
}
private char[] getSigningKeyPassphrase(char[] signingKeyPassphrase) {
return (signingKeyPassphrase != null) ? signingKeyPassphrase : cachedSigningKeyPassphrase;
}
private String getSigningKeyAlias() {
String alias = config.getSigningKeyAlias();
return (alias == null) ? GatewayConfig.DEFAULT_SIGNING_KEY_ALIAS : alias;
}
private String getSigningKeyAlias(String signingKeystoreAlias) {
if(signingKeystoreAlias != null) {
return signingKeystoreAlias;
}
// Fallback to defaults
return getSigningKeyAlias();
}
@Override
public boolean verifyToken(JWT token)
throws TokenServiceException {
return verifyToken(token, null);
}
@Override
public boolean verifyToken(JWT token, RSAPublicKey publicKey)
throws TokenServiceException {
boolean rc;
PublicKey key;
try {
if (publicKey == null) {
key = ks.getSigningKeystore().getCertificate(getSigningKeyAlias()).getPublicKey();
}
else {
key = publicKey;
}
JWSVerifier verifier = new RSASSAVerifier((RSAPublicKey) key);
// TODO: interrogate the token for issuer claim in order to determine the public key to use for verification
// consider jwk for specifying the key too
rc = token.verify(verifier);
} catch (KeyStoreException | KeystoreServiceException e) {
throw new TokenServiceException("Cannot verify token.", e);
}
return rc;
}
@Override
public boolean verifyToken(JWT token, String jwksurl, String algorithm) throws TokenServiceException {
boolean verified = false;
try {
if (algorithm != null && jwksurl != null) {
JWSAlgorithm expectedJWSAlg = JWSAlgorithm.parse(algorithm);
JWKSource<SecurityContext> keySource = new RemoteJWKSet<>(new URL(jwksurl));
JWSKeySelector<SecurityContext> keySelector = new JWSVerificationKeySelector<>(expectedJWSAlg, keySource);
// Create a JWT processor for the access tokens
ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
jwtProcessor.setJWSKeySelector(keySelector);
JWTClaimsSetVerifier<SecurityContext> claimsVerifier = new DefaultJWTClaimsVerifier<>();
jwtProcessor.setJWTClaimsSetVerifier(claimsVerifier);
// Process the token
SecurityContext ctx = null; // optional context parameter, not required here
jwtProcessor.process(token.toString(), ctx);
verified = true;
}
} catch (BadJOSEException | JOSEException | ParseException | MalformedURLException e) {
throw new TokenServiceException("Cannot verify token.", e);
}
return verified;
}
@Override
public void init(GatewayConfig config, Map<String, String> options)
throws ServiceLifecycleException {
if (as == null || ks == null) {
throw new ServiceLifecycleException("Alias or Keystore service is not set");
}
this.config = config;
}
@Override
public void start() throws ServiceLifecycleException {
// Ensure that the default signing keystore is available
KeyStore keystore;
try {
keystore = ks.getSigningKeystore();
if (keystore == null) {
throw new ServiceLifecycleException(RESOURCES.signingKeystoreNotAvailable(config.getSigningKeystorePath()));
}
} catch (KeystoreServiceException e) {
throw new ServiceLifecycleException(RESOURCES.signingKeystoreNotAvailable(config.getSigningKeystorePath()), e);
}
// Ensure that the password for the signing key is available
try {
cachedSigningKeyPassphrase = as.getSigningKeyPassphrase();
if (cachedSigningKeyPassphrase == null) {
throw new ServiceLifecycleException(RESOURCES.signingKeyPassphraseNotAvailable(config.getSigningKeyPassphraseAlias()));
}
} catch (AliasServiceException e) {
throw new ServiceLifecycleException(RESOURCES.signingKeyPassphraseNotAvailable(config.getSigningKeyPassphraseAlias()), e);
}
String signingKeyAlias = getSigningKeyAlias();
// Ensure that the public signing keys is available
try {
Certificate certificate = keystore.getCertificate(signingKeyAlias);
if(certificate == null) {
throw new ServiceLifecycleException(RESOURCES.publicSigningKeyNotFound(signingKeyAlias));
}
PublicKey publicKey = certificate.getPublicKey();
if (publicKey == null) {
throw new ServiceLifecycleException(RESOURCES.publicSigningKeyNotFound(signingKeyAlias));
}
else if (! (publicKey instanceof RSAPublicKey)) {
throw new ServiceLifecycleException(RESOURCES.publicSigningKeyWrongType(signingKeyAlias));
}
} catch (KeyStoreException e) {
throw new ServiceLifecycleException(RESOURCES.publicSigningKeyNotFound(signingKeyAlias), e);
}
// Ensure that the private signing keys is available
try {
Key key = keystore.getKey(signingKeyAlias, cachedSigningKeyPassphrase);
if (key == null) {
throw new ServiceLifecycleException(RESOURCES.privateSigningKeyNotFound(signingKeyAlias));
}
else if (! (key instanceof RSAPrivateKey)) {
throw new ServiceLifecycleException(RESOURCES.privateSigningKeyWrongType(signingKeyAlias));
}
signingKey = (RSAPrivateKey) key;
} catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) {
throw new ServiceLifecycleException(RESOURCES.privateSigningKeyNotFound(signingKeyAlias), e);
}
}
@Override
public void stop() throws ServiceLifecycleException {
}
}