JsonPathJwtConverter.java

/*
 * Copyright 2020-2026 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.bremersee.spring.security.oauth2.server.resource.authentication;

import static org.springframework.util.ObjectUtils.isEmpty;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import org.bremersee.spring.security.core.Group;
import org.bremersee.spring.security.core.NormalizedGroup;
import org.bremersee.spring.security.core.NormalizedUser;
import org.jspecify.annotations.NonNull;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.util.Assert;

/**
 * The json path jwt converter.
 *
 * @author Christian Bremer
 */
@Getter(AccessLevel.PROTECTED)
@ToString(callSuper = true)
@EqualsAndHashCode
public class JsonPathJwtConverter implements Converter<Jwt, AbstractAuthenticationToken> {

  private final JsonPathJwtProperties properties;

  /**
   * Instantiates a new Json path jwt converter.
   *
   * @param properties the properties
   */
  public JsonPathJwtConverter(JsonPathJwtProperties properties) {
    Assert.notNull(properties, "Properties must not be null.");
    this.properties = properties;
  }

  @NonNull
  @Override
  public NormalizedJwtAuthenticationToken convert(@NonNull Jwt source) {
    JsonPathJwtParser parser = new JsonPathJwtParser(source);
    return new NormalizedJwtAuthenticationToken(
        source,
        new NormalizedUser(
            getUsername(source, parser),
            getFirstName(parser),
            getLastName(parser),
            getEmail(parser)),
        getGrantedAuthorities(parser),
        getGroups(parser));
  }

  /**
   * Gets groups.
   *
   * @param parser the parser
   * @return the groups
   */
  protected Collection<? extends GrantedAuthority> getGrantedAuthorities(JsonPathJwtParser parser) {
    Stream<String> values = properties.isRolesValueList()
        ? getAuthoritiesFromList(parser)
        : getAuthoritiesFromValue(parser);
    Set<GrantedAuthority> authorities = values.map(SimpleGrantedAuthority::new)
        .collect(Collectors.toSet());
    return properties.getAuthoritiesMapper().mapAuthorities(authorities);
  }

  /**
   * Gets authorities from list.
   *
   * @param parser the parser
   * @return the authorities from list
   */
  protected Stream<String> getAuthoritiesFromList(JsonPathJwtParser parser) {
    //noinspection unchecked
    return Stream.ofNullable(properties.getRolesJsonPath())
        .map(path -> parser.read(path, List.class))
        .filter(Objects::nonNull)
        .map(list -> (List<String>) list)
        .flatMap(Collection::stream);
  }

  /**
   * Gets authorities from value.
   *
   * @param parser the parser
   * @return the authorities from value
   */
  protected Stream<String> getAuthoritiesFromValue(JsonPathJwtParser parser) {
    return Stream.ofNullable(properties.getRolesJsonPath())
        .filter(path -> !isEmpty(properties.getRolesValueSeparator()))
        .map(path -> parser.read(path, String.class))
        .filter(Objects::nonNull)
        .map(value -> value.split(Pattern.quote(properties.getRolesValueSeparator())))
        .flatMap(Arrays::stream)
        .map(String::valueOf);
  }

  /**
   * Gets groups.
   *
   * @param parser the parser
   * @return the groups
   */
  protected Collection<Group> getGroups(JsonPathJwtParser parser) {
    Stream<String> values = properties.isGroupsValueList()
        ? getGroupsFromList(parser)
        : getGroupsFromValue(parser);
    Set<Group> groups = values.map(NormalizedGroup::of)
        .collect(Collectors.toSet());
    return properties.getGroupsMapper().mapGroups(groups);
  }

  /**
   * Gets groups from list.
   *
   * @param parser the parser
   * @return the groups from list
   */
  protected Stream<String> getGroupsFromList(JsonPathJwtParser parser) {
    //noinspection unchecked
    return Stream.ofNullable(properties.getGroupsJsonPath())
        .map(path -> parser.read(path, List.class))
        .filter(Objects::nonNull)
        .map(list -> (List<String>) list)
        .flatMap(Collection::stream);
  }

  /**
   * Gets groups from value.
   *
   * @param parser the parser
   * @return the groups from value
   */
  protected Stream<String> getGroupsFromValue(JsonPathJwtParser parser) {
    return Stream.ofNullable(properties.getGroupsJsonPath())
        .filter(path -> !isEmpty(properties.getGroupsValueSeparator()))
        .map(path -> parser.read(path, String.class))
        .filter(Objects::nonNull)
        .map(value -> value.split(Pattern.quote(properties.getGroupsValueSeparator())))
        .flatMap(Arrays::stream)
        .map(String::valueOf);
  }

  /**
   * Gets username.
   *
   * @param source the source
   * @param parser the parser
   * @return the username
   */
  protected String getUsername(Jwt source, JsonPathJwtParser parser) {
    return Optional.ofNullable(properties.getNameJsonPath())
        .filter(jsonPath -> !jsonPath.isBlank())
        .map(jsonPath -> parser.read(jsonPath, String.class))
        .orElseGet(source::getSubject);
  }

  /**
   * Gets first name.
   *
   * @param parser the parser
   * @return the first name
   */
  protected String getFirstName(JsonPathJwtParser parser) {
    return parser.read(properties.getFirstNameJsonPath(), String.class);
  }

  /**
   * Gets last name.
   *
   * @param parser the parser
   * @return the last name
   */
  protected String getLastName(JsonPathJwtParser parser) {
    return parser.read(properties.getLastNameJsonPath(), String.class);
  }

  /**
   * Gets email.
   *
   * @param parser the parser
   * @return the email
   */
  protected String getEmail(JsonPathJwtParser parser) {
    return parser.read(properties.getEmailJsonPath(), String.class);
  }

}