All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.interledger.spsp.server.auth.IlpOverHttpAuthenticationProvider Maven / Gradle / Ivy

Go to download

An Interledger SPSP server with a stateless STREAM receiver that publishes events.

The newest version!
package org.interledger.spsp.server.auth;

import org.interledger.connector.accounts.AccountId;
import org.interledger.connector.accounts.AccountNotFoundProblem;
import org.interledger.crypto.Decryptor;
import org.interledger.crypto.EncryptedSecret;
import org.interledger.link.http.IlpOverHttpLinkSettings;
import org.interledger.link.http.IncomingLinkSettings;
import org.interledger.link.http.SimpleAuthSettings;
import org.interledger.spsp.server.model.ParentAccountSettings;
import org.interledger.spsp.server.model.SpspServerSettings;

import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.spring.security.api.authentication.PreAuthenticatedAuthenticationJsonWebToken;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalListener;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.hash.HashCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;

import java.util.Base64;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

/**
 * 

An {@link AuthenticationProvider} that implements the Authentication profiles defined in IL-RFC-35.

* *

This implementation treats shared-secrets as sensitive values because they are an actual authentication * credential and can be used to generate new authentication tokens. Thus, this implementation attempts to reduce the * amount of time (to as short of a window of time as possible) that this information is stored in-memory in unencrypted * form.

* *

To do this, authentication decisions for any particular token are cached in-memory for a small duration. However, * if there is no authentication decision in the cache, then this provider will delegate to an underlying implementation * that will actually load the credentials for an incoming connection (currently these are stored in encrypted format on * each account) and then decrypt that value in order to verify an incoming token. The authentication decision is then * cached per the above. In this way, the actual shared-secret is only ever kept in-memory briefly during a particular * token verification, but is otherwise discarded from memory quickly.

* *

Note that the cache expiry of an authentication decision will be extended after every request that uses the same * token, so a cache expiry will not occur until after X minutes have elapsed with no requests using a given * token).

* * @see "https://github.com/sappenin/java-ilpv4-connector/issues/457" * @deprecated Remove this once the version of this in `java-ilp-connector` is extracted per #457. */ @Deprecated @SuppressWarnings("UnstableApiUsage") public class IlpOverHttpAuthenticationProvider implements AuthenticationProvider { private static final String AUTH_DECISIONS_CACHE_NAME = "ilpOverHttpAuthenticationDecisionsCache"; private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final Decryptor decryptor; // See Javadoc above for how this is used. private final Cache authenticationDecisions; private final ParentAccountSettings parentAccountSettings; public IlpOverHttpAuthenticationProvider( final Supplier spspServerSettingsSupplier, final Decryptor decryptor, // final AccountSettingsRepository accountSettingsRepository, // final LinkSettingsFactory linkSettingsFactory, // final CacheMetricsCollector cacheMetrics final ParentAccountSettings parentAccountSettings ) { this.parentAccountSettings = parentAccountSettings; //this.accountSettingsRepository = accountSettingsRepository; //this.linkSettingsFactory = linkSettingsFactory; Objects.requireNonNull(spspServerSettingsSupplier); this.decryptor = Objects.requireNonNull(decryptor); authenticationDecisions = Caffeine.newBuilder() .recordStats() // Publish stats to prometheus .maximumSize(5000) // Expire after this duration, which will correspond to the last incoming request from the peer. // TODO: This value should be configurable and match the server-global token expiry. .expireAfterAccess(30, TimeUnit.MINUTES) .removalListener((RemovalListener) (authenticationRequest, authenticationDecision, cause) -> logger.debug("Removing IlpOverHttp AuthenticationDecision from Cache for Principal: {}", authenticationDecision.getPrincipal())) .build(); // TODO: FIXME //Objects.requireNonNull(cacheMetrics).addCache(AUTH_DECISIONS_CACHE_NAME, authenticationDecisions); } private static AuthenticationDecision notAuthenticated() { // Cache this result and return it if an exception was encountered. Note that the authenticationRequest // always returns false for isAuthenticated, which is what we want here. return AuthenticationDecision.builder() .credentialHmac(HashCode.fromBytes(new byte[32])) .isAuthenticated(false) .build(); } private static Optional getSimpleCredentials(byte[] token) { String tokenString = new String(token); int tokenIndex = tokenString.lastIndexOf(":"); if (tokenIndex > 0) { return Optional.of(SimpleCredentials.builder() .principal(AccountId.of(tokenString.substring(0, tokenIndex).trim())) .authToken(Base64.getDecoder().decode(tokenString.substring(tokenIndex + 1).trim())) .build()); } return Optional.empty(); } private static boolean isSimple(byte[] token) { return new String(token).indexOf(":") > 0; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { try { if (authentication instanceof BearerAuthentication) { AuthenticationDecision result = authenticateBearer((BearerAuthentication) authentication); if (result.isAuthenticated()) { return result; } else { throw new BadCredentialsException("Authentication failed for principal: " + result.getPrincipal()); } } else { logger.debug("Unsupported authentication type: " + authentication.getClass()); return null; } } catch (BadCredentialsException e) { throw e; } catch (Exception e) { if (e.getCause() != null && BadCredentialsException.class.isAssignableFrom(e.getCause().getClass())) { throw e; } else { throw new BadCredentialsException("Unable to validate token due to system error", e); } } } private AuthenticationDecision authenticateBearer(BearerAuthentication bearerAuth) { return authenticationDecisions.get(bearerAuth.hmacSha256(), (request) -> isSimple(bearerAuth.getBearerToken()) ? authenticateAsSimple(bearerAuth) : authenticateAsJwt(bearerAuth)); } @Override public boolean supports(Class authentication) { return BearerAuthentication.class.isAssignableFrom(authentication); } /** * Decrypts the shared-secret from {@link IncomingLinkSettings} and returns it as a * byte-array. * * @param authPrincipal An {@link AccountId} to decrypt a shared secret for. * @param incomingLinkSettings A {@link IncomingLinkSettings} to use while decrypting a shared secret. * * @return The actual underlying shared-secret. */ @VisibleForTesting protected final EncryptedSecret getIncomingSecret( final AccountId authPrincipal, final IncomingLinkSettings incomingLinkSettings ) { Objects.requireNonNull(authPrincipal); Objects.requireNonNull(incomingLinkSettings); final IlpOverHttpLinkSettings ilpOverHttpLinkSettings = IlpOverHttpLinkSettings .fromCustomSettings(parentAccountSettings.customSettings()).build(); return ilpOverHttpLinkSettings.incomingLinkSettings() .flatMap(IncomingLinkSettings::simpleAuthSettings) .map(SimpleAuthSettings::authToken) .map(EncryptedSecret::fromEncodedValue) .orElseThrow(() -> new BadCredentialsException(String.format("No account found for `%s`", authPrincipal))); } private AuthenticationDecision authenticateAsJwt(BearerAuthentication pendingAuth) { try { PreAuthenticatedAuthenticationJsonWebToken jwt = PreAuthenticatedAuthenticationJsonWebToken.usingToken(new String(pendingAuth.getBearerToken())); if (jwt == null) { throw new JWTDecodeException("jwt decoded to null. Value: " + new String(pendingAuth.getBearerToken())); } AccountId accountId = AccountId.of(jwt.getPrincipal().toString()); EncryptedSecret encryptedSecret = getIncomingSecret(accountId); return decryptor.withDecrypted(encryptedSecret, decryptedSecret -> { Authentication authResult = new JwtHs256AuthenticationProvider(decryptedSecret) .authenticate(jwt); logger.debug("authenticationProvider returned with an AuthResult: {}", authResult.isAuthenticated()); return AuthenticationDecision.builder() .principal(accountId) .isAuthenticated(authResult.isAuthenticated()) .credentialHmac(pendingAuth.hmacSha256()) .build(); }); } catch (AccountNotFoundProblem | JWTDecodeException e) { // All other exceptions should be thrown! logger.debug(e.getMessage(), e); } return notAuthenticated(); } private AuthenticationDecision authenticateAsSimple(BearerAuthentication authentication) { try { SimpleCredentials simpleCredentials = getSimpleCredentials(authentication.getBearerToken()) .orElseThrow(() -> new BadCredentialsException("invalid simple auth credentials")); EncryptedSecret encryptedSecret = getIncomingSecret(simpleCredentials.getPrincipal()); boolean isAuthenticated = decryptor.isEqualDecrypted(encryptedSecret, simpleCredentials.getAuthToken()); return AuthenticationDecision.builder() .principal(simpleCredentials.getPrincipal()) .credentialHmac(authentication.hmacSha256()) .isAuthenticated(isAuthenticated) .build(); } catch (AccountNotFoundProblem e) { // All other exceptions should be thrown! logger.debug(e.getMessage(), e); } return notAuthenticated(); } private EncryptedSecret getIncomingSecret(AccountId accountId) { final IlpOverHttpLinkSettings ilpOverHttpLinkSettings = IlpOverHttpLinkSettings .fromCustomSettings(parentAccountSettings.customSettings()).build(); return getIncomingSecret(accountId, ilpOverHttpLinkSettings.incomingLinkSettings().get()); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy