diff --git a/.github/workflows/push-with-v-tag.yml b/.github/workflows/push-with-v-tag.yml index f1cdac09..ad02421a 100644 --- a/.github/workflows/push-with-v-tag.yml +++ b/.github/workflows/push-with-v-tag.yml @@ -28,6 +28,9 @@ jobs: GPG_SECRET_KEY: ${{ secrets.GPG_SECRET_KEY }} GPG_OWNERTRUST: ${{ secrets.GPG_OWNERTRUST }} + - name: Step 4 - Build client + run: ./scripts/build_client.sh + - name: Get the tag name run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV diff --git a/pom.xml b/pom.xml index 851f318e..d43dd05c 100644 --- a/pom.xml +++ b/pom.xml @@ -52,10 +52,10 @@ 3.2.5 0.0.11 5.3.0 - 2.4.0 - 2.2.20 - 2.15.1 - 2.15.1 + 2.5.0 + 2.2.22 + 2.17.1 + 2.17.1 22.0.4 3.2.5 3.2.5 @@ -73,8 +73,8 @@ 3.0.1 3.3.0 3.2.0 - 3.1.1 - 3.1.0 + 3.1.2 + 3.2.4 3.6.0 diff --git a/sts-e2e-tests/src/test/java/de/adorsys/sts/tests/e2e/tokenexchange/TokenExchangeControllerJpaTest.java b/sts-e2e-tests/src/test/java/de/adorsys/sts/tests/e2e/tokenexchange/TokenExchangeControllerJpaTest.java index 8a69e715..2db3fb34 100644 --- a/sts-e2e-tests/src/test/java/de/adorsys/sts/tests/e2e/tokenexchange/TokenExchangeControllerJpaTest.java +++ b/sts-e2e-tests/src/test/java/de/adorsys/sts/tests/e2e/tokenexchange/TokenExchangeControllerJpaTest.java @@ -7,7 +7,6 @@ import de.adorsys.sts.tests.config.WithTokenExchangeConfig; import de.adorsys.sts.tests.config.WithoutWebSecurityConfig; import de.adorsys.sts.token.tokenexchange.TokenExchangeConstants; -import de.adorsys.sts.token.tokenexchange.server.TokenExchangeRestController; import lombok.SneakyThrows; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -38,7 +37,7 @@ void tokenExchangeTest() { clock.setInstant(Instant.ofEpochMilli(1516239022000L)); - mvc.perform(post(TokenExchangeRestController.DEFAULT_PATH) + mvc.perform(post("/token/token-exchange") .accept(MediaType.APPLICATION_JSON_VALUE) .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) .param("grant_type", TokenExchangeConstants.TOKEN_EXCHANGE_OAUTH_GRANT_TYPE) diff --git a/sts-example/src/main/java/de/adorsys/sts/example/config/SecurityConfiguration.java b/sts-example/src/main/java/de/adorsys/sts/example/config/SecurityConfiguration.java index b070dc29..a8910a08 100644 --- a/sts-example/src/main/java/de/adorsys/sts/example/config/SecurityConfiguration.java +++ b/sts-example/src/main/java/de/adorsys/sts/example/config/SecurityConfiguration.java @@ -34,6 +34,7 @@ protected SecurityFilterChain securityFilterChain(HttpSecurity http, TokenAuthen .requestMatchers(HttpMethod.POST, "/login" ).permitAll() + .requestMatchers("/error").permitAll() .anyRequest().authenticated() ; diff --git a/sts-spring/pom.xml b/sts-spring/pom.xml index 6b341aed..75267eb3 100644 --- a/sts-spring/pom.xml +++ b/sts-spring/pom.xml @@ -76,7 +76,7 @@ com.fasterxml.jackson.module jackson-module-jakarta-xmlbind-annotations - 2.15.0 + ${jackson.version} diff --git a/sts-spring/src/main/java/de/adorsys/sts/filter/JWTAuthenticationFilter.java b/sts-spring/src/main/java/de/adorsys/sts/filter/JWTAuthenticationFilter.java index e8dee3fc..18d79628 100644 --- a/sts-spring/src/main/java/de/adorsys/sts/filter/JWTAuthenticationFilter.java +++ b/sts-spring/src/main/java/de/adorsys/sts/filter/JWTAuthenticationFilter.java @@ -1,31 +1,29 @@ package de.adorsys.sts.filter; import de.adorsys.sts.token.authentication.TokenAuthenticationService; +import jakarta.annotation.Nonnull; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import jakarta.servlet.http.HttpServletResponse; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.filter.GenericFilterBean; +import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; -public class JWTAuthenticationFilter extends GenericFilterBean { - private static final Logger logger = LoggerFactory.getLogger(JWTAuthenticationFilter.class); - - private TokenAuthenticationService tokenAuthenticationService; +@Slf4j +@RequiredArgsConstructor +public class JWTAuthenticationFilter extends OncePerRequestFilter { - public JWTAuthenticationFilter(TokenAuthenticationService tokenAuthenticationService) { - this.tokenAuthenticationService = tokenAuthenticationService; - } + private final TokenAuthenticationService tokenAuthenticationService; @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) - throws IOException, ServletException { + public void doFilterInternal(@Nonnull HttpServletRequest request, @Nonnull HttpServletResponse response, @Nonnull FilterChain filterChain) + throws ServletException, IOException { if (logger.isTraceEnabled()) logger.trace("doFilter start"); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); @@ -33,7 +31,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha if (logger.isDebugEnabled()) logger.debug("Authentication is null. Try to get authentication from request..."); - authentication = tokenAuthenticationService.getAuthentication((HttpServletRequest) request); + authentication = tokenAuthenticationService.getAuthentication(request); SecurityContextHolder.getContext().setAuthentication(authentication); } diff --git a/sts-spring/src/main/java/de/adorsys/sts/secretserver/SecretServerRestController.java b/sts-spring/src/main/java/de/adorsys/sts/secretserver/SecretServerRestController.java index 97b08213..a98d85d1 100644 --- a/sts-spring/src/main/java/de/adorsys/sts/secretserver/SecretServerRestController.java +++ b/sts-spring/src/main/java/de/adorsys/sts/secretserver/SecretServerRestController.java @@ -1,6 +1,7 @@ package de.adorsys.sts.secretserver; import de.adorsys.sts.common.config.TokenResource; +import de.adorsys.sts.token.tokenexchange.TokenExchangeService; import de.adorsys.sts.token.tokenexchange.server.TokenExchangeController; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.RequestMapping; @@ -11,4 +12,7 @@ @TokenResource @RequestMapping("${sts.secret-server.endpoint:/secret-server/token-exchange}") public class SecretServerRestController extends TokenExchangeController { + public SecretServerRestController(TokenExchangeService tokenExchangeService) { + super(tokenExchangeService); + } } diff --git a/sts-spring/src/main/java/de/adorsys/sts/token/authentication/LoggingAuthServer.java b/sts-spring/src/main/java/de/adorsys/sts/token/authentication/LoggingAuthServer.java index 1775455f..25bffa10 100644 --- a/sts-spring/src/main/java/de/adorsys/sts/token/authentication/LoggingAuthServer.java +++ b/sts-spring/src/main/java/de/adorsys/sts/token/authentication/LoggingAuthServer.java @@ -2,9 +2,11 @@ import com.nimbusds.jose.jwk.JWK; import de.adorsys.sts.tokenauth.AuthServer; +import lombok.extern.slf4j.Slf4j; import java.util.List; +@Slf4j public class LoggingAuthServer extends AuthServer { public LoggingAuthServer(String name, String issUrl, String jwksUrl, int refreshIntervalSeconds, String keyCloakUrl) { @@ -14,5 +16,9 @@ public LoggingAuthServer(String name, String issUrl, String jwksUrl, int refresh @Override protected void onJsonWebKeySetRetrieved(List jwks) { super.onJsonWebKeySetRetrieved(jwks); + + if(log.isDebugEnabled()) { + log.debug("Retrieved remote JWKS: {}", jwks); + } } } diff --git a/sts-spring/src/main/java/de/adorsys/sts/token/tokenexchange/server/TokenExchangeController.java b/sts-spring/src/main/java/de/adorsys/sts/token/tokenexchange/server/TokenExchangeController.java index 696871e6..57da3403 100644 --- a/sts-spring/src/main/java/de/adorsys/sts/token/tokenexchange/server/TokenExchangeController.java +++ b/sts-spring/src/main/java/de/adorsys/sts/token/tokenexchange/server/TokenExchangeController.java @@ -3,131 +3,73 @@ import de.adorsys.sts.ResponseUtils; import de.adorsys.sts.token.InvalidParameterException; import de.adorsys.sts.token.MissingParameterException; +import de.adorsys.sts.token.api.TokenRequestForm; import de.adorsys.sts.token.api.TokenResponse; -import de.adorsys.sts.token.tokenexchange.TokenExchangeConstants; import de.adorsys.sts.token.tokenexchange.TokenExchangeRequest; import de.adorsys.sts.token.tokenexchange.TokenExchangeService; import de.adorsys.sts.token.tokenexchange.TokenValidationException; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.headers.Header; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; +import jakarta.servlet.http.HttpServletRequest; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestParam; - -import jakarta.servlet.http.HttpServletRequest; +import org.springframework.web.bind.annotation.RequestBody; +@Slf4j +@RequiredArgsConstructor(access = AccessLevel.PROTECTED) public class TokenExchangeController { - private static final Logger logger = LoggerFactory.getLogger(TokenExchangeController.class); - - @Autowired - private TokenExchangeService tokenExchangeService; + private final TokenExchangeService tokenExchangeService; @PostMapping(consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE}) @Operation(summary = "Exchange Token", description = "Create an access or refresh token given a valide subject token.", responses = { @ApiResponse(responseCode = "200", description = "Ok", content = @Content(mediaType = "application/json", schema = @Schema(implementation = TokenResponse.class))), @ApiResponse(responseCode = "400", description = "Bad request", headers = @Header(name = "error", description = "invalid request")) }) - public ResponseEntity tokenExchange( - @Parameter( - name = "grant_type", - description = "Indicates that a token exchange is being performed.", - required = true, - example = TokenExchangeConstants.TOKEN_EXCHANGE_OAUTH_GRANT_TYPE) - @RequestParam( - value = "grant_type", - defaultValue = TokenExchangeConstants.TOKEN_EXCHANGE_OAUTH_GRANT_TYPE - ) String grantType, - - @Parameter( - name = "resource", - description = "Indicates the physical location of the target service or resource where the client intends to use the requested security token. This enables the authorization server to apply policy as appropriate for the target, such as determining the type and content of the token to be issued or if and how the token is to be encrypted.", - example = "http://localhost:8080/multibanking-service") - @RequestParam(name = "resource", required = false) String[] resources, - - @Parameter( - name = "audience", - description = "The logical name of the target service where the client intends to use the requested security token. This serves a purpose similar to the resource parameter, but with the client providing a logical name rather than a physical location.", - example = "http://localhost:8080/multibanking-service") - @RequestParam(name = "audience", required = false) String[] audiences, - - @Parameter( - name = "scope", - description = "A list of space-delimited, case-sensitive strings that allow the client to specify the desired scope of the requested security token in the context of the service or resource where the token will be used.", - example = "user banking") - @RequestParam(name = "scope", required = false) String scope, - - @Parameter( - name = "requested_token_type", - description = "An identifier for the type of the requested security token. If the requested type is unspecified, the issued token type is at the discretion of the authorization server and may be dictated by knowledge of the requirements of the service or resource indicated by the resource or audience parameter. This can be urn:ietf:params:oauth:token-type:jwt or urn:ietf:params:oauth:token-type:saml.", - required = false, - example = TokenExchangeConstants.JWT_OAUTH_TOKEN_TYPE) - @RequestParam(name = "requested_token_type", required = false) String requestedTokenType, - - @Parameter( - name = "subject_token", - description = "A security token that represents the identity of the party on behalf of whom the request is being made. Typically, the subject of this token will be the subject of the security token issued in response to this request.", - required = true, - example = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJNYXhNdXN0ZXJtYW4iLCJyb2xlIjoiVVNFUiIsImV4cCI6MTQ5NTM5MTAxM30.mN9eFMnEuYgh_KCULI8Gpm1X49wWaA67Ps1M7EFV0BQ") - @RequestParam("subject_token") String subjectToken, + public ResponseEntity tokenExchange(@RequestBody @ModelAttribute TokenRequestForm tokenRequestForm, HttpServletRequest servletRequest) { + if (log.isTraceEnabled()) log.trace("POST tokenExchange started..."); - @Parameter( - name = "subject_token_type", - description = "An identifier for the type of the requested security token. If the requested type is unspecified, the issued token type is at the discretion of the authorization server and may be dictated by knowledge of the requirements of the service or resource indicated by the resource or audience parameter. This can be urn:ietf:params:oauth:token-type:jwt or urn:ietf:params:oauth:token-type:saml. This can be urn:ietf:params:oauth:token-type:access_token or urn:ietf:params:oauth:token-type:refresh_token.", - required = true, - example = TokenExchangeConstants.JWT_OAUTH_TOKEN_TYPE) - @RequestParam(value = "subject_token_type", defaultValue = TokenExchangeConstants.JWT_OAUTH_TOKEN_TYPE) String subjectTokenType, - - @Parameter( - name = "actor_token", - description = "A security token that represents the identity of the acting party. Typically this will be the party that is authorized to use the requested security token and act on behalf of the subject.", - required = false, - example = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJNYXhNdXN0ZXJtYW4iLCJyb2xlIjoiVVNFUiIsImV4cCI6MTQ5NTM5MTAxM30.mN9eFMnEuYgh_KCULI8Gpm1X49wWaA67Ps1M7EFV0BQ") - @RequestParam(name = "actor_token", required = false) String actorToken, - - @Parameter( - name = "actor_token_type", - description = "An identifier for the type of the requested security token. If the requested type is unspecified, the issued token type is at the discretion of the authorization server and may be dictated by knowledge of the requirements of the service or resource indicated by the resource or audience parameter. This can be urn:ietf:params:oauth:token-type:jwt or urn:ietf:params:oauth:token-type:saml. This can be urn:ietf:params:oauth:token-type:access_token or urn:ietf:params:oauth:token-type:refresh_token.", - required = false, - example = TokenExchangeConstants.JWT_OAUTH_TOKEN_TYPE) - @RequestParam(name = "actor_token_type", required = false) String actorTokenType, - HttpServletRequest servletRequest - ) { - if (logger.isTraceEnabled()) logger.trace("POST tokenExchange started..."); - - TokenExchangeRequest tokenExchange = TokenExchangeRequest.builder() - .grantType(grantType) - .resources(resources) - .subjectToken(subjectToken) - .subjectTokenType(subjectTokenType) - .actorToken(actorToken) - .actorTokenType(actorTokenType) - .issuer(ResponseUtils.getIssuer(servletRequest)) - .scope(scope) - .requestedTokenType(requestedTokenType) - .audiences(audiences) - .build(); + TokenExchangeRequest tokenExchange = getTokenExchangeRequest(tokenRequestForm, servletRequest); + String errorMessage = ""; try { TokenResponse tokenResponse = tokenExchangeService.exchangeToken(tokenExchange); return ResponseEntity.ok(tokenResponse); } catch (InvalidParameterException e) { + errorMessage = e.getMessage(); return ResponseUtils.invalidParam(e.getMessage()); } catch (MissingParameterException e) { + errorMessage = e.getMessage(); return ResponseUtils.missingParam(e.getMessage()); } catch (TokenValidationException e) { + errorMessage = e.getMessage(); ResponseEntity errorData = ResponseUtils.invalidParam(e.getMessage()); return ResponseEntity.badRequest().body(errorData); } finally { - if (logger.isTraceEnabled()) logger.trace("POST tokenExchange finished."); + if (log.isTraceEnabled()) log.trace("POST tokenExchange finished: {}", errorMessage); } } + + private static TokenExchangeRequest getTokenExchangeRequest(TokenRequestForm tokenRequestForm, HttpServletRequest servletRequest) { + return TokenExchangeRequest.builder() + .grantType(tokenRequestForm.getGrantType()) + .resources(tokenRequestForm.getResources()) + .subjectToken(tokenRequestForm.getSubjectToken()) + .subjectTokenType(tokenRequestForm.getSubjectTokenType()) + .actorToken(tokenRequestForm.getActorToken()) + .actorTokenType(tokenRequestForm.getActorTokenType()) + .issuer(ResponseUtils.getIssuer(servletRequest)) + .scope(tokenRequestForm.getScope()) + .requestedTokenType(tokenRequestForm.getRequestedTokenType()) + .audiences(tokenRequestForm.getAudiences()) + .build(); + } } diff --git a/sts-spring/src/main/java/de/adorsys/sts/token/tokenexchange/server/TokenExchangeRestController.java b/sts-spring/src/main/java/de/adorsys/sts/token/tokenexchange/server/TokenExchangeRestController.java index 8b3dca7c..16824270 100644 --- a/sts-spring/src/main/java/de/adorsys/sts/token/tokenexchange/server/TokenExchangeRestController.java +++ b/sts-spring/src/main/java/de/adorsys/sts/token/tokenexchange/server/TokenExchangeRestController.java @@ -1,6 +1,7 @@ package de.adorsys.sts.token.tokenexchange.server; import de.adorsys.sts.common.config.TokenResource; +import de.adorsys.sts.token.tokenexchange.TokenExchangeService; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -8,8 +9,10 @@ @RestController @Tag(name = "Token Exchange", description = "Token exchange, token degradation endpoint") @TokenResource -@RequestMapping(path = TokenExchangeRestController.DEFAULT_PATH) +@RequestMapping(path = "/token/token-exchange") public class TokenExchangeRestController extends TokenExchangeController { - public static final String DEFAULT_PATH = "/token/token-exchange"; + public TokenExchangeRestController(TokenExchangeService tokenExchangeService) { + super(tokenExchangeService); + } } diff --git a/sts-token/src/main/java/de/adorsys/sts/token/api/TokenRequestForm.java b/sts-token/src/main/java/de/adorsys/sts/token/api/TokenRequestForm.java new file mode 100644 index 00000000..c7487a77 --- /dev/null +++ b/sts-token/src/main/java/de/adorsys/sts/token/api/TokenRequestForm.java @@ -0,0 +1,60 @@ +package de.adorsys.sts.token.api; + +import de.adorsys.sts.token.tokenexchange.TokenExchangeConstants; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.beans.ConstructorProperties; + +@Getter +@AllArgsConstructor(onConstructor_ = @ConstructorProperties({"grant_type", "resource", "audience", "scope", "requested_token_type", "subject_token", "subject_token_type", "actor_token", "actor_token_type"})) +@Schema(description = "Carries request form properties of a token-exchange request", + requiredProperties = {"grant_type", "subject_token", "subject_token_type"}) +public class TokenRequestForm { + + @Schema(name = "grant_type", + description = "Indicates that a token exchange is being performed.", + example = TokenExchangeConstants.TOKEN_EXCHANGE_OAUTH_GRANT_TYPE) + private String grantType; + + @Schema(name = "resource", + description = "Indicates the physical location of the target service or resource where the client intends to use the requested security token. This enables the authorization server to apply policy as appropriate for the target, such as determining the type and content of the token to be issued or if and how the token is to be encrypted.", + example = "http://localhost:8080/multibanking-service") + private String[] resources; + + @Schema(name = "audience", + description = "The logical name of the target service where the client intends to use the requested security token. This serves a purpose similar to the resource parameter, but with the client providing a logical name rather than a physical location.", + example = "http://localhost:8080/multibanking-service") + private String[] audiences; + + @Schema(name = "scope", + description = "A list of space-delimited, case-sensitive strings that allow the client to specify the desired scope of the requested security token in the context of the service or resource where the token will be used.", + example = "user banking") + private String scope; + + @Schema(name = "requested_token_type", + description = "An identifier for the type of the requested security token. If the requested type is unspecified, the issued token type is at the discretion of the authorization server and may be dictated by knowledge of the requirements of the service or resource indicated by the resource or audience parameter. This can be urn:ietf:params:oauth:token-type:jwt or urn:ietf:params:oauth:token-type:saml.", + example = TokenExchangeConstants.JWT_OAUTH_TOKEN_TYPE) + private String requestedTokenType; + + @Schema(name = "subject_token", + description = "A security token that represents the identity of the party on behalf of whom the request is being made. Typically, the subject of this token will be the subject of the security token issued in response to this request.", + example = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJNYXhNdXN0ZXJtYW4iLCJyb2xlIjoiVVNFUiIsImV4cCI6MTQ5NTM5MTAxM30.mN9eFMnEuYgh_KCULI8Gpm1X49wWaA67Ps1M7EFV0BQ") + private String subjectToken; + + @Schema(name = "subject_token_type", + description = "An identifier for the type of the requested security token. If the requested type is unspecified, the issued token type is at the discretion of the authorization server and may be dictated by knowledge of the requirements of the service or resource indicated by the resource or audience parameter. This can be urn:ietf:params:oauth:token-type:jwt or urn:ietf:params:oauth:token-type:saml. This can be urn:ietf:params:oauth:token-type:access_token or urn:ietf:params:oauth:token-type:refresh_token.", + example = TokenExchangeConstants.JWT_OAUTH_TOKEN_TYPE) + private String subjectTokenType; + + @Schema(name = "actor_token", + description = "A security token that represents the identity of the acting party. Typically this will be the party that is authorized to use the requested security token and act on behalf of the subject.", + example = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJNYXhNdXN0ZXJtYW4iLCJyb2xlIjoiVVNFUiIsImV4cCI6MTQ5NTM5MTAxM30.mN9eFMnEuYgh_KCULI8Gpm1X49wWaA67Ps1M7EFV0BQ") + private String actorToken; + + @Schema(name = "actor_token_type", + description = "An identifier for the type of the requested security token. If the requested type is unspecified, the issued token type is at the discretion of the authorization server and may be dictated by knowledge of the requirements of the service or resource indicated by the resource or audience parameter. This can be urn:ietf:params:oauth:token-type:jwt or urn:ietf:params:oauth:token-type:saml. This can be urn:ietf:params:oauth:token-type:access_token or urn:ietf:params:oauth:token-type:refresh_token.", + example = TokenExchangeConstants.JWT_OAUTH_TOKEN_TYPE) + private String actorTokenType; +}