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

de.retest.recheck.auth.RetestAuthentication Maven / Gradle / Ivy

There is a newer version: 1.13.0
Show newest version
package de.retest.recheck.auth;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.client.utils.URLEncodedUtils;

import com.auth0.jwk.JwkException;
import com.auth0.jwk.UrlJwkProvider;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;

import kong.unirest.HttpResponse;
import kong.unirest.JsonNode;
import kong.unirest.Unirest;
import kong.unirest.json.JSONObject;
import lombok.Cleanup;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class RetestAuthentication {

	private static final String REALM = "customer";
	private static final String KEYCLOAK_URL = "https://sso.prod.cloud.retest.org/auth";
	private static final String BASE_URL = KEYCLOAK_URL + "/realms/" + REALM + "/protocol/openid-connect";
	private static final String AUTH_URL = BASE_URL + "/auth";
	private static final String TOKEN_URL = BASE_URL + "/token";
	private static final String CERTS_URL = BASE_URL + "/certs";
	private static final String LOGOUT_URL = BASE_URL + "/logout";

	private static final String OAUTH_ACCESS_TOKEN = "access_token";
	private static final String OAUTH_REFRESH_TOKEN = "refresh_token";
	private static final String OAUTH_GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code";
	private static final String OAUTH_GRANT_TYPE = "grant_type";
	private static final String OAUTH_SCOPE_OFFLINE_ACCESS = "offline_access";
	private static final String OAUTH_SCOPE = "scope";
	private static final String OAUTH_STATE = "state";
	private static final String OAUTH_REDIRECT_URI = "redirect_uri";
	private static final String OAUTH_CLIENT_ID = "client_id";
	private static final String OAUTH_RESPONSE_TYPE_CODE = "code";
	private static final String OAUTH_RESPONSE_TYPE = "response_type";

	private static final String PUBLIC_KEY_ID = "cXdlj_AlGVf-TbXyauXYM2XairgNUahzgOXHAuAxAmQ";

	private DecodedJWT accessToken;
	private final AuthenticationHandler handler;
	private final String client;
	private final JWTVerifier verifier;

	public RetestAuthentication( final AuthenticationHandler handler, final String client ) {
		this.handler = handler;
		this.client = client;
		verifier = getJwtVerifier();
	}

	private JWTVerifier getJwtVerifier() {
		try {
			final UrlJwkProvider provider = new UrlJwkProvider( URI.create( CERTS_URL ).toURL() );
			final PublicKey publicKey = provider.get( PUBLIC_KEY_ID ).getPublicKey();
			return JWT.require( Algorithm.RSA256( (RSAPublicKey) publicKey, null ) ).acceptLeeway( 3 ).build();
		} catch ( final JwkException | MalformedURLException e ) {
			throw new RuntimeException( "Error accessing keycloak JWK information", e );
		}
	}

	public void authenticate() {
		if ( handler.getOfflineToken() != null ) {
			refreshTokens();
		} else {
			log.info( "No active token found, initiating authentication" );
			login();
		}
	}

	private void login() {
		try {
			final CallbackListener callback = new CallbackListener();
			callback.start();

			final String redirectUri = "http://localhost:" + callback.server.getLocalPort();
			final String state = UUID.randomUUID().toString();

			final URIBuilder builder = new URIBuilder( AUTH_URL );
			builder.addParameter( OAUTH_RESPONSE_TYPE, OAUTH_RESPONSE_TYPE_CODE );
			builder.addParameter( OAUTH_CLIENT_ID, client );
			builder.addParameter( OAUTH_REDIRECT_URI, redirectUri );
			builder.addParameter( OAUTH_STATE, state );
			builder.addParameter( OAUTH_SCOPE, OAUTH_SCOPE_OFFLINE_ACCESS );

			final URI loginUri = URI.create( builder.build().toString() );
			handler.showWebLoginUri( loginUri );

			callback.join();

			if ( loginSuccessful( state, callback.result ) ) {
				final TokenBundle bundle = accessCodeToToken( callback.result.getCode(), redirectUri );
				accessToken = verifier.verify( bundle.accessToken );
				handler.loginPerformed( bundle.refreshToken );
			} else {
				handler.loginFailed( retrieveError( callback.result ) );
			}

		} catch ( final InterruptedException | IOException | URISyntaxException e ) {
			log.error( "Error during authentication", e );
			Thread.currentThread().interrupt();
		}

	}

	private Throwable retrieveError( final KeycloakResult result ) {
		final IOException exception = result.getErrorException();
		final String error = result.getError();
		if ( exception != null ) {
			return exception;
		}
		if ( !StringUtils.isEmpty( error ) ) {
			return new RuntimeException( error );
		}
		return new RuntimeException( "Error during login" );
	}

	private boolean loginSuccessful( final String state, final KeycloakResult result ) {
		return state.equals( result.getState() ) && result.getError() == null && result.getErrorException() == null;
	}

	private TokenBundle accessCodeToToken( final String code, final String redirectUri ) {
		final TokenBundle bundle = new TokenBundle();

		final HttpResponse response = Unirest.post( TOKEN_URL ) //
				.field( OAUTH_GRANT_TYPE, OAUTH_GRANT_TYPE_AUTHORIZATION_CODE ) //
				.field( OAUTH_RESPONSE_TYPE_CODE, code ) //
				.field( OAUTH_CLIENT_ID, client ) //
				.field( OAUTH_REDIRECT_URI, redirectUri ) //
				.asJson();

		if ( response.isSuccess() ) {
			final JSONObject object = response.getBody().getObject();
			bundle.setAccessToken( object.getString( OAUTH_ACCESS_TOKEN ) );
			bundle.setRefreshToken( object.getString( OAUTH_REFRESH_TOKEN ) );
		}

		return bundle;
	}

	@Data
	private static class TokenBundle {
		private String accessToken;
		private String refreshToken;
	}

	public void logout() {
		final String offlineToken = handler.getOfflineToken();
		if ( offlineToken != null ) {
			final HttpResponse response = Unirest.post( LOGOUT_URL ) //
					.field( OAUTH_REFRESH_TOKEN, handler.getOfflineToken() ) //
					.field( OAUTH_CLIENT_ID, client ) //
					.asJson();

			if ( response.isSuccess() ) {
				handler.logoutPerformed();
			} else {
				handler.logoutFailed( new RuntimeException( response.getStatusText() ) );
			}

		} else {
			log.error( "No offline token provided" );
		}
	}

	private void refreshTokens() {
		final Optional refreshedToken = refreshAccessToken();
		if ( refreshedToken.isPresent() ) {
			accessToken = refreshedToken.get();
		} else {
			login();
		}
	}

	public DecodedJWT getAccessToken() {
		if ( !isAccessTokenValid() ) {
			refreshTokens();
		}
		return accessToken;
	}

	private Optional refreshAccessToken() {
		final HttpResponse response = Unirest.post( TOKEN_URL ) //
				.field( OAUTH_GRANT_TYPE, OAUTH_REFRESH_TOKEN ) //
				.field( OAUTH_REFRESH_TOKEN, handler.getOfflineToken() ) //
				.field( OAUTH_CLIENT_ID, client ) //
				.asJson();

		if ( response.isSuccess() ) {
			final JSONObject object = response.getBody().getObject();
			try {
				return Optional.of( verifier.verify( object.getString( OAUTH_ACCESS_TOKEN ) ) );
			} catch ( final Exception e ) {
				log.error( "Error verifying access token: {}", e.getMessage() );
				log.debug( "Details: ", e );
			}
		}
		log.error( "Error retrieving access token: {}", response.getStatusText() );
		return Optional.empty();
	}

	private boolean isAccessTokenValid() {
		try {
			final DecodedJWT verify = verifier.verify( accessToken );
			return accessToken != null && verify != null;
		} catch ( final JWTVerificationException exception ) {
			log.info( "Current token is invalid, requesting new one" );
		}
		return false;
	}

	static KeycloakResult getRequestParameters( final String request ) {
		final String url = "http://localhost/" + request.split( " " )[1];
		final Map parameters = URLEncodedUtils.parse( URI.create( url ), StandardCharsets.UTF_8 ) //
				.stream() //
				.collect( Collectors.toMap( NameValuePair::getName, NameValuePair::getValue ) );

		return KeycloakResult.builder() //
				.code( parameters.get( OAUTH_RESPONSE_TYPE_CODE ) ) //
				.error( parameters.get( "error" ) ) //
				.errorDescription( parameters.get( "error-description" ) ) //
				.state( parameters.get( OAUTH_STATE ) ) //
				.build();
	}

	private class CallbackListener extends Thread {

		private final ServerSocket server;
		private KeycloakResult result;

		public CallbackListener() throws IOException {
			server = new ServerSocket( 0 );
		}

		@Override
		public void run() {
			try ( Socket socket = server.accept() ) {
				@Cleanup
				final BufferedReader br = new BufferedReader( new InputStreamReader( socket.getInputStream() ) );
				final String request = br.readLine();

				result = getRequestParameters( request );

				@Cleanup
				final OutputStreamWriter out = new OutputStreamWriter( socket.getOutputStream() );
				@Cleanup
				final PrintWriter writer = new PrintWriter( out );

				if ( result.getError() == null ) {
					writer.println( "HTTP/1.1 302 Found" );
					writer.println( "Location: " + TOKEN_URL.replace( "/token", "/delegated" ) );

				} else {
					writer.println( "HTTP/1.1 302 Found" );
					writer.println( "Location: " + TOKEN_URL.replace( "/token", "/delegated?error=true" ) );

				}
			} catch ( final IOException e ) {
				log.error( "Error during communication with {}", KEYCLOAK_URL, e );
			}
		}

	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy