CtrlK
BlogDocsLog inGet started
Tessl Logo

giuseppe-trisciuoglio/developer-kit

Comprehensive developer toolkit providing reusable skills for Java/Spring Boot, TypeScript/NestJS/React/Next.js, Python, PHP, AWS CloudFormation, AI/RAG, DevOps, and more.

89

Quality

89%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Risky

Do not use without reviewing

Overview
Quality
Evals
Security
Files

oauth2-integration.mdplugins/developer-kit-java/skills/spring-boot-security-jwt/references/

OAuth2 Integration with JWT

OAuth2 Client Configuration

OAuth2 Client Registration

@Configuration
@EnableWebSecurity
public class OAuth2ClientConfig {

    @Bean
    public SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http) throws Exception {
        return http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/login", "/error", "/webjars/**").permitAll()
                .anyRequest().authenticated())
            .oauth2Login(oauth2 -> oauth2
                .loginPage("/login")
                .authorizationEndpoint(authorization -> authorization
                    .baseUri("/oauth2/authorization")
                    .authorizationRequestRepository(cookieAuthorizationRequestRepository()))
                .redirectionEndpoint(redirection -> redirection
                    .baseUri("/login/oauth2/code/*"))
                .userInfoEndpoint(userInfo -> userInfo
                    .userService(customOAuth2UserService()))
                .successHandler(oAuth2AuthenticationSuccessHandler())
                .failureHandler(oAuth2AuthenticationFailureHandler()))
            .oauth2Client(withDefaults())
            .build();
    }

    @Bean
    public AuthorizationRequestRepository<AuthorizationRequest> cookieAuthorizationRequestRepository() {
        return new HttpSessionOAuth2AuthorizationRequestRepository();
    }

    @Bean
    public OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler() {
        return new OAuth2AuthenticationSuccessHandler(jwtTokenService, userRepository);
    }

    @Bean
    public OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler() {
        return new OAuth2AuthenticationFailureHandler();
    }

    @Bean
    public CustomOAuth2UserService customOAuth2UserService() {
        return new CustomOAuth2UserService();
    }
}

OAuth2 User Service

@Service
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

    private final DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
    private final UserRepository userRepository;
    private final JwtTokenService jwtTokenService;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = delegate.loadUser(userRequest);

        try {
            return processOAuth2User(userRequest, oAuth2User);
        } catch (AuthenticationException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex.getCause());
        }
    }

    private OAuth2User processOAuth2User(OAuth2UserRequest userRequest, OAuth2User oAuth2User) {
        OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(
            userRequest.getClientRegistration().getRegistrationId(),
            oAuth2User.getAttributes());

        if (StringUtils.isEmpty(oAuth2UserInfo.getEmail())) {
            throw new OAuth2AuthenticationProcessingException("Email not found from OAuth2 provider");
        }

        Optional<User> userOptional = userRepository.findByEmail(oAuth2UserInfo.getEmail());
        User user;

        if (userOptional.isPresent()) {
            user = userOptional.get();
            if (!user.getProvider().equals(AuthProvider.valueOf(userRequest.getClientRegistration().getRegistrationId()))) {
                throw new OAuth2AuthenticationProcessingException("Looks like you're signed up with " +
                    user.getProvider() + " account. Please use your " + user.getProvider() +
                    " account to login.");
            }
            user = updateExistingUser(user, oAuth2UserInfo);
        } else {
            user = registerNewUser(userRequest, oAuth2UserInfo);
        }

        return UserPrincipal.create(user, oAuth2User.getAttributes());
    }

    private User registerNewUser(OAuth2UserRequest userRequest, OAuth2UserInfo oAuth2UserInfo) {
        User user = new User();

        user.setProvider(AuthProvider.valueOf(userRequest.getClientRegistration().getRegistrationId()));
        user.setProviderId(oAuth2UserInfo.getId());
        user.setName(oAuth2UserInfo.getName());
        user.setEmail(oAuth2UserInfo.getEmail());
        user.setImageUrl(oAuth2UserInfo.getImageUrl());
        user.setEmailVerified(true);
        user.setEnabled(true);

        // Assign default role
        Role userRole = roleRepository.findByName("USER")
            .orElseThrow(() -> new IllegalStateException("Default USER role not found"));
        user.setRoles(Set.of(userRole));

        return userRepository.save(user);
    }

    private User updateExistingUser(User existingUser, OAuth2UserInfo oAuth2UserInfo) {
        existingUser.setName(oAuth2UserInfo.getName());
        existingUser.setImageUrl(oAuth2UserInfo.getImageUrl());
        return userRepository.save(existingUser);
    }
}

OAuth2 Success Handler

@Component
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private final JwtTokenService jwtTokenService;
    private final UserRepository userRepository;
    private final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException {
        OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
        OAuth2User oAuth2User = oauthToken.getPrincipal();

        // Get or create user
        String email = oAuth2User.getAttribute("email");
        User user = userRepository.findByEmail(email)
            .orElseThrow(() -> new OAuth2AuthenticationException("User not found"));

        // Generate JWT tokens
        AccessTokenResponse accessToken = jwtTokenService.generateAccessToken(user);
        RefreshTokenResponse refreshToken = refreshTokenService.createRefreshToken(user);

        // Create response
        OAuth2LoginResponse loginResponse = OAuth2LoginResponse.builder()
            .accessToken(accessToken.token())
            .tokenType("Bearer")
            .expiresIn((int) (accessToken.expiresAt() - System.currentTimeMillis()) / 1000)
            .refreshToken(refreshToken.token())
            .user(convertToUserDto(user))
            .build();

        // Clear cookies
        clearAuthenticationAttributes(request, response);

        // Return JSON response
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.getWriter().write(objectMapper.writeValueAsString(loginResponse));
    }

    protected void clearAuthenticationAttributes(HttpServletRequest request, HttpServletResponse response) {
        super.clearAuthenticationAttributes(request);
        httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
    }
}

OAuth2 Resource Server Configuration

JWT Resource Server with OAuth2

@Configuration
@EnableWebSecurity
public class OAuth2ResourceServerConfig {

    @Bean
    public SecurityFilterChain resourceServerSecurityFilterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/actuator/**").hasRole("ADMIN")
                .anyRequest().authenticated())
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .decoder(jwtDecoder())
                    .jwtAuthenticationConverter(jwtAuthenticationConverter()))
                .authenticationEntryPoint(authenticationEntryPoint()))
            .exceptionHandling(exception -> exception
                .accessDeniedHandler(accessDeniedHandler()))
            .build();
    }

    @Bean
    public JwtDecoder jwtDecoder() {
        NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder
            .withJwkSetUri(jwkSetUri())
            .build();

        // Add custom claim validation
        jwtDecoder.setClaimSetConverter(new OrganizationSubClaimAdapter());

        return jwtDecoder;
    }

    @Bean
    public Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter() {
        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(jwt ->
        {
            Collection<String> authorities = new ArrayList<>();

            // Extract scopes
            Collection<String> scopes = jwt.getClaimAsStringList("scope");
            if (scopes != null) {
                authorities.addAll(scopes.stream()
                    .map(scope -> "SCOPE_" + scope)
                    .collect(Collectors.toList()));
            }

            // Extract roles
            Collection<String> roles = jwt.getClaimAsStringList("roles");
            if (roles != null) {
                authorities.addAll(roles.stream()
                    .map(role -> "ROLE_" + role)
                    .collect(Collectors.toList()));
            }

            // Extract permissions
            Collection<String> permissions = jwt.getClaimAsStringList("permissions");
            if (permissions != null) {
                authorities.addAll(permissions);
            }

            return authorities;
        });

        return converter;
    }
}

Custom Claim Validation

@Component
public class JwtAudienceValidator implements OAuth2TokenValidator<Jwt> {

    private final String audience;

    public JwtAudienceValidator(String audience) {
        this.audience = audience;
    }

    @Override
    public OAuth2TokenValidatorResult validate(Jwt jwt) {
        List<String> audiences = jwt.getAudience();
        if (audiences == null || audiences.isEmpty()) {
            return OAuth2TokenValidatorResult.failure(
                new OAuth2Error("invalid_token", "Missing audience claim", null));
        }

        if (audiences.stream().noneMatch(aud -> aud.equals(audience))) {
            return OAuth2TokenValidatorResult.failure(
                new OAuth2Error("invalid_token", "Invalid audience", null));
        }

        return OAuth2TokenValidatorResult.success();
    }
}

@Component
public class CustomJwtValidator implements OAuth2TokenValidator<Jwt> {

    private final JwtIssuerValidator issuerValidator;
    private final JwtTimestampValidator timestampValidator;
    private final JwtAudienceValidator audienceValidator;

    public CustomJwtValidator(@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") String issuerUri,
                             @Value("${jwt.audience}") String audience) {
        this.issuerValidator = new JwtIssuerValidator(issuerUri);
        this.timestampValidator = new JwtTimestampValidator(Duration.ofSeconds(30));
        this.audienceValidator = new JwtAudienceValidator(audience);
    }

    @Override
    public OAuth2TokenValidatorResult validate(Jwt jwt) {
        OAuth2TokenValidatorResult result = issuerValidator.validate(jwt);
        if (!result.hasErrors()) {
            result = timestampValidator.validate(jwt);
        }
        if (!result.hasErrors()) {
            result = audienceValidator.validate(jwt);
        }
        return result;
    }
}

Multi-Provider OAuth2 Support

OAuth2 Provider Factory

@Component
public class OAuth2UserInfoFactory {

    public static OAuth2UserInfo getOAuth2UserInfo(String registrationId, Map<String, Object> attributes) {
        if (registrationId.equalsIgnoreCase(AuthProvider.google.toString())) {
            return new GoogleOAuth2UserInfo(attributes);
        } else if (registrationId.equalsIgnoreCase(AuthProvider.facebook.toString())) {
            return new FacebookOAuth2UserInfo(attributes);
        } else if (registrationId.equalsIgnoreCase(AuthProvider.github.toString())) {
            return new GithubOAuth2UserInfo(attributes);
        } else if (registrationId.equalsIgnoreCase(AuthProvider.linkedin.toString())) {
            return new LinkedInOAuth2UserInfo(attributes);
        } else if (registrationId.equalsIgnoreCase(AuthProvider.microsoft.toString())) {
            return new MicrosoftOAuth2UserInfo(attributes);
        } else {
            throw new OAuth2AuthenticationProcessingException("Sorry! Login with " + registrationId + " is not supported yet.");
        }
    }
}

public abstract class OAuth2UserInfo {
    protected Map<String, Object> attributes;

    public OAuth2UserInfo(Map<String, Object> attributes) {
        this.attributes = attributes;
    }

    public abstract String getId();
    public abstract String getName();
    public abstract String getEmail();
    public abstract String getImageUrl();

    public Map<String, Object> getAttributes() {
        return attributes;
    }
}

public class GoogleOAuth2UserInfo extends OAuth2UserInfo {

    public GoogleOAuth2UserInfo(Map<String, Object> attributes) {
        super(attributes);
    }

    @Override
    public String getId() {
        return (String) attributes.get("sub");
    }

    @Override
    public String getName() {
        return (String) attributes.get("name");
    }

    @Override
    public String getEmail() {
        return (String) attributes.get("email");
    }

    @Override
    public String getImageUrl() {
        return (String) attributes.get("picture");
    }
}

public class FacebookOAuth2UserInfo extends OAuth2UserInfo {

    public FacebookOAuth2UserInfo(Map<String, Object> attributes) {
        super(attributes);
    }

    @Override
    public String getId() {
        return (String) attributes.get("id");
    }

    @Override
    public String getName() {
        return (String) attributes.get("name");
    }

    @Override
    public String getEmail() {
        return (String) attributes.get("email");
    }

    @Override
    public String getImageUrl() {
        if (attributes.containsKey("picture")) {
            Map<String, Object> pictureObj = (Map<String, Object>) attributes.get("picture");
            if (pictureObj.containsKey("data")) {
                Map<String, Object> dataObj = (Map<String, Object>) pictureObj.get("data");
                return (String) dataObj.get("url");
            }
        }
        return null;
    }
}

OAuth2 Client Properties Configuration

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: ${OAUTH2_GOOGLE_CLIENT_ID}
            client-secret: ${OAUTH2_GOOGLE_CLIENT_SECRET}
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            scope:
              - email
              - profile
          facebook:
            client-id: ${OAUTH2_FACEBOOK_CLIENT_ID}
            client-secret: ${OAUTH2_FACEBOOK_CLIENT_SECRET}
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            scope:
              - email
              - public_profile
          github:
            client-id: ${OAUTH2_GITHUB_CLIENT_ID}
            client-secret: ${OAUTH2_GITHUB_CLIENT_SECRET}
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            scope:
              - user:email
              - read:user
          linkedin:
            client-id: ${OAUTH2_LINKEDIN_CLIENT_ID}
            client-secret: ${OAUTH2_LINKEDIN_CLIENT_SECRET}
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            scope:
              - r_emailaddress
              - r_liteprofile
          microsoft:
            client-id: ${OAUTH2_MICROSOFT_CLIENT_ID}
            client-secret: ${OAUTH2_MICROSOFT_CLIENT_SECRET}
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            scope:
              - openid
              - email
              - profile
        provider:
          facebook:
            authorization-uri: https://www.facebook.com/v3.0/dialog/oauth
            token-uri: https://graph.facebook.com/v3.0/oauth/access_token
            user-info-uri: https://graph.facebook.com/v3.0/me?fields=id,name,email,picture
          linkedin:
            authorization-uri: https://www.linkedin.com/oauth/v2/authorization
            token-uri: https://www.linkedin.com/oauth/v2/accessToken
            user-info-uri: https://api.linkedin.com/v2/people/~:(id,firstName,lastName,emailAddress,profilePicture(displayImage~:playableStreams))

OAuth2 Token Exchange

Token Exchange Service

@Service
public class TokenExchangeService {

    private final RestTemplate restTemplate;
    private final OAuth2AuthorizedClientService authorizedClientService;

    public TokenResponse exchangeToken(String accessToken, String targetAudience) {
        // Build token exchange request
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange");
        params.add("subject_token", accessToken);
        params.add("subject_token_type", "urn:ietf:params:oauth:token-type:access_token");
        params.add("audience", targetAudience);
        params.add("requested_token_type", "urn:ietf:params:oauth:token-type:access_token");

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        headers.setBasicAuth(clientId, clientSecret);

        HttpEntity<MultiValueMap<String, String>> request =
            new HttpEntity<>(params, headers);

        try {
            ResponseEntity<TokenResponse> response = restTemplate.postForEntity(
                tokenEndpoint, request, TokenResponse.class);

            return response.getBody();

        } catch (RestClientException e) {
            log.error("Token exchange failed", e);
            throw new TokenExchangeException("Failed to exchange token", e);
        }
    }

    @Cacheable(value = "exchangedTokens", key = "#accessToken + ':' + #targetAudience")
    public TokenResponse getCachedExchangeToken(String accessToken, String targetAudience) {
        return exchangeToken(accessToken, targetAudience);
    }
}

Delegated Authorization

@Service
public class DelegatedAuthorizationService {

    public String createDelegatedToken(String userToken, String delegateTo, List<String> scopes) {
        // Validate user token
        TokenValidationResult validation = tokenValidator.validate(userToken);
        if (!validation.isValid()) {
            throw new InvalidTokenException("Invalid user token");
        }

        // Check delegation permissions
        if (!hasDelegationPermission(validation.getUserId(), delegateTo, scopes)) {
            throw new InsufficientScopeException("Insufficient delegation permissions");
        }

        // Create delegated token
        JwtClaimsSet claims = JwtClaimsSet.builder()
            .issuer("delegation-service")
            .subject(validation.getUserId())
            .audience(List.of(delegateTo))
            .issuedAt(Instant.now())
            .expiresAt(Instant.now().plus(1, ChronoUnit.HOURS))
            .claim("delegated_from", validation.getUserId())
            .claim("scopes", scopes)
            .claim("type", "delegated")
            .build();

        return jwtEncoder.encode(JwtEncoderParameters.from(claims))
            .getTokenValue();
    }

    private boolean hasDelegationPermission(String userId, String delegateTo, List<String> scopes) {
        // Check if user has permission to delegate specified scopes to target service
        return delegationPermissionRepository
            .existsByUserIdAndDelegateToAndScopes(userId, delegateTo, scopes);
    }
}

OAuth2 Security Events

OAuth2 Event Tracking

@Component
@Slf4j
public class OAuth2EventTracker {

    @EventListener
    @Async
    public void handleOAuth2AuthenticationSuccess(OAuth2AuthenticationSuccessEvent event) {
        OAuth2User user = event.getAuthentication().getPrincipal();
        String provider = event.getAuthentication().getAuthorizedClientRegistrationId();

        OAuth2LogEntry logEntry = OAuth2LogEntry.builder()
            .eventType("OAUTH2_SUCCESS")
            .provider(provider)
            .userId(user.getAttribute("id"))
            .email(user.getAttribute("email"))
            .timestamp(Instant.now())
            .clientIp(getClientIp())
            .userAgent(getUserAgent())
            .build();

        auditLogService.save(logEntry);
    }

    @EventListener
    @Async
    public void handleOAuth2AuthenticationFailure(OAuth2AuthenticationFailureEvent event) {
        OAuth2LogEntry logEntry = OAuth2LogEntry.builder()
            .eventType("OAUTH2_FAILURE")
            .provider(event.getAuthorizedClientRegistrationId())
            .errorMessage(event.getException().getMessage())
            .timestamp(Instant.now())
            .clientIp(getClientIp())
            .userAgent(getUserAgent())
            .build();

        auditLogService.save(logEntry);
    }

    @EventListener
    @Async
    public void handleOAuth2AuthorizationRequest(OAuth2AuthorizationRequestEvent event) {
        OAuth2AuthorizationRequest request = event.getAuthorizationRequest();

        OAuth2LogEntry logEntry = OAuth2LogEntry.builder()
            .eventType("OAUTH2_REQUEST")
            .provider(event.getClientRegistrationId())
            .clientId(request.getClientId())
            .scopes(request.getScopes())
            .redirectUri(request.getRedirectUri())
            .state(request.getState())
            .timestamp(Instant.now())
            .clientIp(getClientIp())
            .build();

        auditLogService.save(logEntry);
    }
}

OAuth2 Client Registration API

@RestController
@RequestMapping("/api/oauth2")
@PreAuthorize("hasRole('ADMIN')")
public class OAuth2RegistrationController {

    private final ClientRegistrationRepository clientRegistrationRepository;

    @PostMapping("/clients")
    public ResponseEntity<ClientRegistration> registerClient(
            @Valid @RequestBody OAuth2ClientRegistrationRequest request) {

        // Validate client registration
        validateClientRegistration(request);

        // Create client registration
        ClientRegistration registration = ClientRegistration.withRegistrationId(request.getClientId())
            .clientId(request.getClientId())
            .clientSecret(request.getClientSecret())
            .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .redirectUri("{baseUrl}/{action}/oauth2/code/{registrationId}")
            .scope(request.getScopes().toArray(new String[0]))
            .authorizationUri(request.getAuthorizationUri())
            .tokenUri(request.getTokenUri())
            .userInfoUri(request.getUserInfoUri())
            .userNameAttributeName(request.getUserNameAttribute())
            .clientName(request.getClientName())
            .build();

        // Save to repository (if using a custom implementation)
        clientRegistrationRepository.save(registration);

        return ResponseEntity.status(HttpStatus.CREATED).body(registration);
    }

    @GetMapping("/clients/{registrationId}")
    public ResponseEntity<ClientRegistration> getClient(
            @PathVariable String registrationId) {

        return clientRegistrationRepository.findByRegistrationId(registrationId)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }
}

plugins

developer-kit-java

skills

README.md

CHANGELOG.md

context7.json

CONTRIBUTING.md

README_CN.md

README_ES.md

README_IT.md

README.md

tessl.json

tile.json