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

io.camunda.tasklist.webapp.security.identity.IdentityAuthentication Maven / Gradle / Ivy

/*
 * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under
 * one or more contributor license agreements. See the NOTICE file distributed
 * with this work for additional information regarding copyright ownership.
 * Licensed under the Camunda License 1.0. You may not use this file
 * except in compliance with the Camunda License 1.0.
 */
package io.camunda.tasklist.webapp.security.identity;

import io.camunda.identity.sdk.Identity;
import io.camunda.identity.sdk.authentication.AccessToken;
import io.camunda.identity.sdk.authentication.Tokens;
import io.camunda.identity.sdk.authentication.UserDetails;
import io.camunda.identity.sdk.authentication.exception.TokenDecodeException;
import io.camunda.identity.sdk.impl.rest.exception.RestException;
import io.camunda.tasklist.property.TasklistProperties;
import io.camunda.tasklist.util.SpringContextHolder;
import io.camunda.tasklist.webapp.security.ElasticsearchSessionRepository;
import io.camunda.tasklist.webapp.security.OldUsernameAware;
import io.camunda.tasklist.webapp.security.Permission;
import io.camunda.tasklist.webapp.security.tenant.TasklistTenant;
import io.camunda.tasklist.webapp.security.tenant.TenantAwareAuthentication;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.collections4.CollectionUtils;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

public class IdentityAuthentication extends AbstractAuthenticationToken
    implements OldUsernameAware, TenantAwareAuthentication {

  private static final Logger LOGGER = LoggerFactory.getLogger(IdentityAuthentication.class);
  private Tokens tokens;
  private String id;
  private String name;
  private String userDisplayName;
  private List permissions;
  private String subject;
  private Date expires;
  private Date refreshTokenExpiresAt;
  private volatile List tenants = Collections.emptyList();
  private IdentityAuthorization authorization;
  private List groups;

  public IdentityAuthentication() {
    super(null);
  }

  @Override
  public String getCredentials() {
    return tokens.getAccessToken();
  }

  @Override
  public Object getPrincipal() {
    return subject;
  }

  @Override
  public List getTenants() {
    if (CollectionUtils.isEmpty(tenants)) {
      synchronized (this) {
        if (CollectionUtils.isEmpty(tenants)) {
          retrieveTenants();
        }
      }
    }
    return tenants;
  }

  private void retrieveTenants() {
    if (getTasklistProperties().getMultiTenancy().isEnabled()) {
      try {
        final var accessToken = tokens.getAccessToken();
        final var identityTenants = getIdentity().tenants().forToken(accessToken);

        if (CollectionUtils.isNotEmpty(identityTenants)) {
          tenants =
              identityTenants.stream()
                  .map((t) -> new TasklistTenant(t.getTenantId(), t.getName()))
                  .sorted(TENANT_NAMES_COMPARATOR)
                  .toList();
        } else {
          tenants = List.of();
        }
      } catch (RestException ex) {
        LOGGER.warn("Unable to retrieve tenants from Identity. Error: " + ex.getMessage(), ex);
        tenants = List.of();
      }
    } else {
      tenants = List.of();
    }
  }

  public Tokens getTokens() {
    return tokens;
  }

  private boolean hasExpired() {
    return expires == null || expires.before(new Date());
  }

  private boolean hasRefreshTokenExpired() {
    try {
      LOGGER.info("Refresh token will expire at {}", refreshTokenExpiresAt);
      return refreshTokenExpiresAt == null || refreshTokenExpiresAt.before(new Date());
    } catch (final TokenDecodeException e) {
      LOGGER.info(
          "Refresh token is not a JWT and expire date can not be determined. Error message: {}",
          e.getMessage());
      return false;
    }
  }

  @Override
  public String getName() {
    return name;
  }

  @Override
  public boolean isAuthenticated() {
    if (hasExpired()) {
      LOGGER.info("Access token is expired");
      if (hasRefreshTokenExpired()) {
        LOGGER.info("No refresh token available. Authentication is invalid.");
        setAuthenticated(false);
        getIdentity().authentication().revokeToken(tokens.getRefreshToken());
        return false;
      } else {
        try {
          LOGGER.info("Get a new access token by using refresh token");
          renewAccessToken();
        } catch (Exception e) {
          LOGGER.error("Renewing access token failed with exception", e);
          setAuthenticated(false);
        }
      }
    }
    return super.isAuthenticated();
  }

  public String getId() {
    return id;
  }

  public List getPermissions() {
    return permissions.stream()
        .map(PermissionConverter.getInstance()::convert)
        .collect(Collectors.toList());
  }

  public void authenticate(final Tokens tokens) {
    if (tokens != null) {
      this.tokens = tokens;
    }
    final AccessToken accessToken =
        getIdentity().authentication().verifyToken(this.tokens.getAccessToken());
    final UserDetails userDetails = accessToken.getUserDetails();
    id = userDetails.getId();
    name = retrieveName(userDetails);
    userDisplayName = retrieveUserDisplayName();
    permissions = accessToken.getPermissions();
    if (!getPermissions().contains(Permission.READ)) {
      throw new InsufficientAuthenticationException("No read permissions");
    }

    try {
      final TasklistProperties props = getTasklistProperties();
      if (props.getIdentity().isResourcePermissionsEnabled()) {
        authorization =
            new IdentityAuthorization(
                getIdentity().authorizations().forToken(this.tokens.getAccessToken()));
      }
    } catch (io.camunda.identity.sdk.exception.InvalidConfigurationException ice) {
      LOGGER.debug(
          "Base URL is not provided so it's not possible to get authorizations from Identity");
    } catch (Exception e) {
      LOGGER.debug("Identity and Tasklist misconfiguration.");
    }
    subject = accessToken.getToken().getSubject();
    expires = accessToken.getToken().getExpiresAt();
    groups = accessToken.getUserDetails().getGroups();
    if (!isPolling()) {
      try {
        refreshTokenExpiresAt =
            getIdentity().authentication().decodeJWT(this.tokens.getRefreshToken()).getExpiresAt();
      } catch (TokenDecodeException e) {
        LOGGER.error(
            "Unable to decode refresh token {} with exception: {}",
            this.tokens.getRefreshToken(),
            e.getMessage());
      }
    }
    if (!hasExpired()) {
      setAuthenticated(true);
    } else {
      setAuthenticated(false);
    }
  }

  @NotNull
  private static TasklistProperties getTasklistProperties() {
    return SpringContextHolder.getBean(TasklistProperties.class);
  }

  private String retrieveName(final UserDetails userDetails) {
    return userDetails.getUsername().orElse(userDetails.getId());
  }

  private void renewAccessToken() {
    authenticate(renewTokens(tokens.getRefreshToken()));
  }

  private Tokens renewTokens(final String refreshToken) {
    return IdentityService.requestWithRetry(
        () -> getIdentity().authentication().renewToken(refreshToken));
  }

  private Identity getIdentity() {
    return SpringContextHolder.getBean(Identity.class);
  }

  public IdentityAuthentication setExpires(final Date expires) {
    this.expires = expires;
    return this;
  }

  public IdentityAuthentication setPermissions(final List permissions) {
    this.permissions = permissions;
    return this;
  }

  public String getUserDisplayName() {
    return userDisplayName;
  }

  private String retrieveUserDisplayName() {
    return getIdentity()
        .authentication()
        .verifyToken(this.tokens.getAccessToken())
        .getUserDetails()
        .getName()
        .orElse(this.name);
  }

  public IdentityAuthentication setUserDisplayName(String userDisplayName) {
    this.userDisplayName = userDisplayName;
    return this;
  }

  @Override
  public String getOldName() {
    return getId();
  }

  public IdentityAuthorization getAuthorizations() {
    return authorization;
  }

  public IdentityAuthentication setAuthorizations(IdentityAuthorization authorization) {
    this.authorization = authorization;
    return this;
  }

  public List getGroups() {
    return groups;
  }

  public IdentityAuthentication setGroups(List groups) {
    this.groups = groups;
    return this;
  }

  private boolean isPolling() {
    final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
    if (requestAttributes instanceof ServletRequestAttributes) {
      return RequestContextHolder.getRequestAttributes() != null
          && Boolean.TRUE.equals(
              Boolean.parseBoolean(
                  ((ServletRequestAttributes) requestAttributes)
                      .getRequest()
                      .getHeader(ElasticsearchSessionRepository.POLLING_HEADER)));
    } else {
      return false;
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy