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]

Reply via email to