GspeyanHov commented on issue #13948: URL: https://github.com/apache/superset/issues/13948#issuecomment-2483387849
Hi dear, but how I could be able to use it?. In my case my main application is java, spring boot application, database is postgresql. User login's into my application via oauth providers like gitlab and google. Now in my case it is more suitable to redirect already authenticated user into superset server. Superset runs in docker. I have created my own superset image and it runs in docker-compose. Its more suitable that my main java server is as sso oauth provider for superset server. I have tried different ways but nothing, I receive 308 or 302 and can not authenticate user against superset. Your any help and support is very important for me and I will be very glad. There is not any example integrating superset with java application. superset runs in docker http://127.0.0.1:8088, java application runs in idea http://127.0.0.1:8080. I also provide my implementations, please help. docker-compose.yml version: '3' services: postgres: image: library/postgres:15.1-alpine ports: - 5432:5432 environment: - POSTGRES_USER=${DB_USERNAME} - POSTGRES_PASSWORD=${DB_PASSWORD} - POSTGRES_DB=${DB_NAME} volumes: - db-data:/var/lib/postgresql/data - ./docker-data/postgres/init:/docker-entrypoint-initdb.d/ accounting: depends_on: - postgres build: context: ./ dockerfile: docker-data/accounting/Dockerfile image: "${IMAGE_NAME}:${IMAGE_VERSION:-latest}" ports: - 8080:8080 environment: - DB_HOST=${DB_HOST} - DB_USERNAME=${DB_USERNAME} - DB_PASSWORD=${DB_PASSWORD} - DB_NAME=${DB_NAME} - DB_SCHEMA=${DB_SCHEMA} redis: image: redis:7 container_name: superset_cache restart: unless-stopped ports: - 6379:6379 volumes: - redis:/data superset: image: gspeyanhov/accounting-superset:latest depends_on: - postgres - redis command: > /bin/bash -c "pip install --upgrade pip && pip install authlib && pip install apache-superset[cors] && pip install Babel && superset fab create-admin \ --username admin \ --firstname Admin \ --lastname User \ --email [email protected] \ --password admin && superset db upgrade && superset init && superset run -p 8088 --with-threads --reload --debugger --host=0.0.0.0" environment: - DATABASE_URL=postgresql+psycopg2://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${D_NAME} - REDIS_HOST=${REDIS_HOST:-redis} - REDIS_PORT=${REDIS_PORT:-6379} - SUPERSET_SECRET_KEY=${SUPERSET_SECRET_KEY} ports: - 8088:8088 # external_links: # - host volumes: - ./superset/superset_config.py:/app/pythonpath/superset_config.py - ./accounting/src/main/resources/static/images/image.png:/app/superset/static/assets/images/image.png - ./accounting/src/main/resources/static/images/favicon.png:/app/superset/static/assets/images/favicon.png - ./superset/custom_sso_security_manager.py:/app/pythonpath/custom_sso_security_manager.py volumes: db-data: redis: superset-config.py # Application Name and Icon APP_NAME = "custom name" APP_ICON = "/static/assets/images/image.png" FAVICONS = [{"href": "/static/assets/images/image.png"}] #Babel config for translations LANGUAGES = { 'en': {'flag': 'us', 'name': 'English'}, 'ru': {'flag': 'ru', 'name': 'Russian'} } # Feature Flags FEATURE_FLAGS = { "EMBEDDED_SUPERSET": True, "ENABLE_TEMPLATE_PROCESSING": True, } DEBUG = True PUBLIC_ROLE_LIKE_GAMMA = True ENABLE_PROXY_FIX = True ENABLE_CORS = True SUPERSET_WORKERS = 4 # CORS Options CORS_OPTIONS = { "supports_credentials": True, "expose_headers": "*", "resources": "*", "origins": ["http://localhost:8080/api"], "methods": ["GET", "POST", "OPTIONS"], "allow_headers": "*" } # Cross-Site Request Forgery (CSRF) Protection WTF_CSRF_ENABLED = False # OAuth Authentication Configuration from flask_appbuilder.security.manager import AUTH_OAUTH from custom_sso_security_manager import CustomSsoSecurityManager SESSION_PROTECTION = "basic" AUTH_TYPE = AUTH_OAUTH ALLOW_LOCAL_LOGIN = True OAUTH_PROVIDERS = [ { 'name': 'SUU', 'token_key': 'access_token', 'remote_app': { 'client_id': 'her I paste the client id used in swagger for testers and developers for (regression)', 'token_key': 'access_token', 'client_secret': 'neither superset's secret_key nor token secret used in java application is not working', 'access_token_url': 'http://localhost:8080/api/auth/token', # Main server's OAuth token URL 'authorize_url': 'http://localhost:8080/api/auth/login', # Main server's OAuth authorization URL 'api_base_url': 'http://localhost:8080/api', # Base URL for the API that returns user info 'client_kwargs': { 'scope': 'user:email', }, }, 'oauth_user_info_url': 'http://localhost:8080/api/auth/userInfo', 'oauth_user_info_method': 'GET', 'oauth_user_info_headers': {'Authorization': 'Bearer {access_token}'}, }, ] AUTH_USER_REGISTRY = True # Authentication Role Mapping AUTH_ROLES_MAPPING = { "superset_users": ["Gamma", "Alpha"], "superset_admins": ["Admin"], } AUTH_ROLES_SYNC_AT_LOGIN = True AUTH_USER_REGISTRATION = True AUTH_USER_REGISTRATION_ROLE = "Public" CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager # Content Security Policy # TALISMAN_ENABLED = True # TALISMAN_CONFIG = { # "content_security_policy": { # "base-uri": ["'self'"], # "frame-ancestors": ["'self'", "*"], # "default-src": ["'self'"], # "img-src": ["'self'", "blob:", "data:", "https://apachesuperset.gateway.scarf.sh", "https://static.scarf.sh/"], # "worker-src": ["'self'", "blob:"], # "connect-src": ["'self'", "https://api.mapbox.com", "https://events.mapbox.com"], # "object-src": "'none'", # "style-src": ["'self'", "'unsafe-inline'"], # "script-src": ["'self'", "'strict-dynamic'"], # }, # "content_security_policy_nonce_in": ["script-src"], # "force_https": False, # "session_cookie_secure": False, # } # Redis Cache Configurations CACHE_CONFIG = { "CACHE_TYPE": "redis", "CACHE_DEFAULT_TIMEOUT": 300, "CACHE_KEY_PREFIX": "superset_", "CACHE_REDIS_HOST": "redis", "CACHE_REDIS_PORT": "6379", } # Mapbox API Key MAPBOX_API_KEY = '' SESSION_COOKIE_SAMESITE = 'None' Also I have created SupersetAuth entity and have flyway migration, so releavant the database table and I have created foreign key with my users table in order to connect with it. CREATE TABLE if not exists superset_auth ( id BIGSERIAL NOT NULL, token VARCHAR(255) NOT NULL, username VARCHAR(255) NOT NULL, consumed BOOLEAN DEFAULT FALSE, creation_time TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP, expiration_time TIMESTAMP(6), consumed_time TIMESTAMP(6), user_id BIGINT, CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES users(id), PRIMARY KEY (id) ); also repository, service, controller classes package ru.codeinside.accounting.superset.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import ru.codeinside.accounting.superset.entity.SupersetAuth; import java.util.List; import java.util.Optional; @Repository public interface SupersetAuthRepository extends JpaRepository<SupersetAuth, Long> { Optional<SupersetAuth> findByTokenAndConsumed(String token, Boolean consumed); List<SupersetAuth> findByConsumed(Boolean consumed); } package ru.codeinside.accounting.superset.service.impl; import io.jsonwebtoken.Claims; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import ru.codeinside.accounting.entity.User; import ru.codeinside.accounting.repository.UserRepository; import ru.codeinside.accounting.superset.dto.SupersetAuthRequestDto; import ru.codeinside.accounting.superset.dto.SupersetAuthResponseDto; import ru.codeinside.accounting.superset.entity.SupersetAuth; import ru.codeinside.accounting.superset.repository.SupersetAuthRepository; import ru.codeinside.accounting.superset.service.SupersetAuthService; import ru.codeinside.accounting.util.JwtUtils; import java.time.LocalDateTime; import java.util.Map; import java.util.Optional; @Service @RequiredArgsConstructor @Slf4j public class SupersetAuthServiceImpl implements SupersetAuthService { private final SupersetAuthRepository supersetAuthRepository; private final UserRepository userRepository; @Override public Optional<SupersetAuthResponseDto> getValidToken(SupersetAuthRequestDto authRequest) { Optional<SupersetAuth> tokenRecord = supersetAuthRepository.findByTokenAndConsumed(authRequest.getToken(), false); if (tokenRecord.isPresent() && tokenRecord.get().getExpirationTime().isAfter(LocalDateTime.now())) { SupersetAuth auth = tokenRecord.get(); SupersetAuthResponseDto response = SupersetAuthResponseDto.builder() .id(auth.getId()) .token(auth.getToken()) .username(auth.getUsername()) .consumed(auth.getConsumed()) .creationTime(auth.getCreationTime()) .expirationTime(auth.getExpirationTime()) .consumedTime(auth.getConsumedTime()) .build(); consumeToken(response); return Optional.of(response); } return Optional.empty(); } @Override public void consumeToken(SupersetAuthResponseDto tokenRecord) { Optional<SupersetAuth> recordOptional = supersetAuthRepository.findById(tokenRecord.getId()); if (recordOptional.isPresent()) { SupersetAuth token = recordOptional.get(); token.setConsumed(true); token.setConsumedTime(LocalDateTime.now()); supersetAuthRepository.save(token); log.info("Token marked as consumed: {}", token); } else { log.error("Token record not found for id: {}", tokenRecord.getId()); } } @Override public void saveNewToken(SupersetAuthRequestDto authRequest) { String token = authRequest.getToken(); Map<String, String> attributes = JwtUtils.decodeJWTBody(token); if (attributes == null || !attributes.containsKey("username")) { if (attributes != null && attributes.containsKey("sub")) { attributes.put("username", attributes.get("sub")); } else { throw new IllegalArgumentException("Cannot extract username from token or fallback"); } } String username = attributes.get("username"); Optional<SupersetAuth> existingToken = supersetAuthRepository.findByTokenAndConsumed(token, false); if (existingToken.isEmpty() || existingToken.get().getExpirationTime().isBefore(LocalDateTime.now())) { supersetAuthRepository.findByConsumed(false) .forEach(tokenRecord -> { tokenRecord.setConsumed(true); tokenRecord.setConsumedTime(LocalDateTime.now()); supersetAuthRepository.save(tokenRecord); }); Optional<User> userOptional = userRepository.findFirstByUsername(username); if (userOptional.isEmpty()) { throw new IllegalArgumentException("User not found"); } User user = userOptional.get(); SupersetAuth newToken = SupersetAuth.builder() .username(username) .user(user) .token(token) .creationTime(LocalDateTime.now()) .expirationTime(LocalDateTime.now().plusHours(1)) .consumed(false) .build(); supersetAuthRepository.save(newToken); } } } package ru.codeinside.accounting.superset.controller.api; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import javax.servlet.http.HttpServletRequest; @Api(tags = { "Аутентификация суперсет" }) @RequestMapping("/superset") public interface SupersetAuthApi { @GetMapping("/welcome") @ApiOperation(value = "Аутентификация пользователя", response = ResponseEntity.class) @ApiResponses(value = { @ApiResponse(code = 200, message = "Пользователь успешно аутентифицирован"), @ApiResponse(code = 401, message = "Неверный или истекший токен"), @ApiResponse(code = 500, message = "Ошибка при создании нового токена") }) ResponseEntity<String> authenticateUser(@ApiParam HttpServletRequest request); } package ru.codeinside.accounting.superset.controller; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; import ru.codeinside.accounting.superset.controller.api.SupersetAuthApi; import ru.codeinside.accounting.superset.dto.SupersetAuthRequestDto; import ru.codeinside.accounting.superset.dto.SupersetAuthResponseDto; import ru.codeinside.accounting.superset.service.SupersetAuthService; import javax.servlet.http.HttpServletRequest; import java.net.URI; import java.util.Collections; import java.util.Optional; @Log4j2 @RestController @RequiredArgsConstructor public class SupersetAuthController implements SupersetAuthApi { private final SupersetAuthService supersetAuthService; private final RestTemplate restTemplate; @Value("${superset.base-uri}") private String supersetBaseUrl; @Override public ResponseEntity<String> authenticateUser(HttpServletRequest request) { String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); } if (token == null) { log.error("Authorization token is missing"); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Authorization token is missing"); } SupersetAuthRequestDto authRequest = SupersetAuthRequestDto.builder() .username(request.getRemoteUser() != null ? request.getRemoteUser() : "unknown") .token(token) .build(); Optional<SupersetAuthResponseDto> tokenRecord = supersetAuthService.getValidToken(authRequest); if (tokenRecord.isPresent()) { supersetAuthService.consumeToken(tokenRecord.get()); log.info("Token consumed successfully"); } else { log.info("Token not found or invalid. Saving new token..."); supersetAuthService.saveNewToken(authRequest); log.info("New token saved."); } // Step 2: Send request to /api/login with token URI loginUri = UriComponentsBuilder.fromUriString(supersetBaseUrl + "/login") .queryParam("token", token) .build(Collections.emptyMap()); HttpHeaders loginHeaders = new HttpHeaders(); HttpEntity<String> loginEntity = new HttpEntity<>(loginHeaders); ResponseEntity<Void> loginResponse; try { loginResponse = restTemplate.exchange(loginUri, HttpMethod.GET, loginEntity, Void.class); } catch (Exception e) { log.error("Error during login process: {}", e.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Login process failed."); } // if (loginResponse.getStatusCode() != HttpStatus.FOUND) { // log.warn("Login failed with status: {}", loginResponse.getStatusCode()); // return ResponseEntity.status(loginResponse.getStatusCode()).body("Login failed."); // } // Successful login. Proceed to forward request to Superset welcome page. String supersetPath = request.getRequestURI().substring(request.getContextPath().length()); URI targetUri = UriComponentsBuilder.fromHttpUrl(supersetBaseUrl) .path(supersetPath) .query(request.getQueryString()) .build(Collections.emptyMap()); HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + token); HttpEntity<String> entity = new HttpEntity<>(headers); try { ResponseEntity<String> response = restTemplate.exchange(targetUri, HttpMethod.GET, entity, String.class); log.info("Request forwarded to Superset with response status: {}", response.getStatusCode()); if (response.getStatusCode() == HttpStatus.OK) { return ResponseEntity.status(HttpStatus.FOUND) .header(HttpHeaders.LOCATION, buildSupersetWelcomeUrl(token)) .build(); } log.warn("Error during user authentication: {}. Response: {}", response.getStatusCode(), response.getBody()); return ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders()).body(response.getBody()); } catch (Exception e) { log.error("Error forwarding request to Superset: {}", e.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred while processing the request."); } } private String buildSupersetWelcomeUrl(String token) { return UriComponentsBuilder.fromHttpUrl(supersetBaseUrl) .path("/superset/welcome") .queryParam("token", token) .build() .toUriString(); } } -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
