From 49c93977d1d4d6658f70ff31cd86ed20939cc31a Mon Sep 17 00:00:00 2001 From: RubenModamioGarcia Date: Wed, 30 Oct 2024 08:25:54 +0100 Subject: [PATCH] - Added functionality to redirect to the home page when clicking the logo in the login page. - Added a timeout that redirects to the home page when the QR code expires in the login page. --- CHANGELOG.md | 5 ++ build.gradle | 2 +- .../vcverifier/config/CacheStoreConfig.java | 7 +- .../vcverifier/config/ClientLoaderConfig.java | 15 ++-- .../config/properties/SecurityProperties.java | 20 ++++- .../controller/LoginQrController.java | 7 +- .../es/in2/vcverifier/model/ClientData.java | 1 + .../security/AuthorizationServerConfig.java | 2 +- .../CustomAuthorizationRequestConverter.java | 25 +++++- src/main/resources/application-dev.yaml | 4 + src/main/resources/application-local.yaml | 4 + src/main/resources/application-prod.yaml | 4 + src/main/resources/application-test.yaml | 4 + src/main/resources/application.yaml | 4 + src/main/resources/templates/login.html | 85 +++++++++++++------ .../controller/LoginQrControllerTest.java | 15 +++- ...stomAuthorizationRequestConverterTest.java | 11 ++- 17 files changed, 170 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5d693b..0d24464 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v1.0.2] +### Fixed +- Added functionality to redirect to the home page when clicking the logo in the login page. +- Added a timeout that redirects to the home page when the QR code expires in the login page. + ## [v1.0.1] ### Fixed - Fixed registration button link on the login qr page. diff --git a/build.gradle b/build.gradle index 77eec9b..0e920d4 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ plugins { } group = 'es.in2' -version = '1.0.1' +version = '1.0.2' java { toolchain { diff --git a/src/main/java/es/in2/vcverifier/config/CacheStoreConfig.java b/src/main/java/es/in2/vcverifier/config/CacheStoreConfig.java index e72a876..6a9d5a8 100644 --- a/src/main/java/es/in2/vcverifier/config/CacheStoreConfig.java +++ b/src/main/java/es/in2/vcverifier/config/CacheStoreConfig.java @@ -1,5 +1,6 @@ package es.in2.vcverifier.config; +import es.in2.vcverifier.config.properties.SecurityProperties; import es.in2.vcverifier.model.AuthorizationCodeData; import es.in2.vcverifier.model.AuthorizationRequestJWT; import lombok.RequiredArgsConstructor; @@ -7,6 +8,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import java.time.temporal.ChronoUnit; import java.util.HashSet; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -15,9 +17,12 @@ @RequiredArgsConstructor public class CacheStoreConfig { + private final SecurityProperties securityProperties; @Bean public CacheStore cacheStoreForAuthorizationRequestJWT() { - return new CacheStore<>(10, TimeUnit.MINUTES); + return new CacheStore<>( + Long.parseLong(securityProperties.loginCode().expirationProperties().expiration()), + TimeUnit.of(ChronoUnit.valueOf(securityProperties.token().accessToken().cronUnit()))); } @Bean diff --git a/src/main/java/es/in2/vcverifier/config/ClientLoaderConfig.java b/src/main/java/es/in2/vcverifier/config/ClientLoaderConfig.java index 5b22f6d..80c4fff 100644 --- a/src/main/java/es/in2/vcverifier/config/ClientLoaderConfig.java +++ b/src/main/java/es/in2/vcverifier/config/ClientLoaderConfig.java @@ -42,7 +42,15 @@ private List loadClients() { List registeredClients = new ArrayList<>(); // Convertir cada ClientData a RegisteredClient y agregarlo a la lista for (ClientData clientData : clientsYamlData.clients()) { - RegisteredClient.Builder registeredClientBuilder = RegisteredClient.withId(UUID.randomUUID().toString()).clientId(clientData.clientId()).clientAuthenticationMethods(authMethods -> clientData.clientAuthenticationMethods().forEach(method -> authMethods.add(new ClientAuthenticationMethod(method)))).authorizationGrantTypes(grantTypes -> clientData.authorizationGrantTypes().forEach(grantType -> grantTypes.add(new AuthorizationGrantType(grantType)))).redirectUris(uris -> uris.addAll(clientData.redirectUris())).postLogoutRedirectUris(uris -> uris.addAll(clientData.postLogoutRedirectUris())).scopes(scopes -> scopes.addAll(clientData.scopes())); + RegisteredClient.Builder registeredClientBuilder = RegisteredClient + .withId(UUID.randomUUID().toString()) + .clientId(clientData.clientId()) + .clientAuthenticationMethods(authMethods -> clientData.clientAuthenticationMethods().forEach(method -> authMethods.add(new ClientAuthenticationMethod(method)))) + .authorizationGrantTypes(grantTypes -> clientData.authorizationGrantTypes().forEach(grantType -> grantTypes.add(new AuthorizationGrantType(grantType)))) + .redirectUris(uris -> uris.addAll(clientData.redirectUris())) + .postLogoutRedirectUris(uris -> uris.addAll(clientData.postLogoutRedirectUris())) + .scopes(scopes -> scopes.addAll(clientData.scopes())) + .clientName(clientData.domain()); if (clientData.clientSecret() != null && !clientData.clientSecret().isBlank()) { registeredClientBuilder.clientSecret(clientData.clientSecret()); @@ -67,7 +75,4 @@ private List loadClients() { throw new ClientLoadingException("Error loading clients from Yaml", e); } } - -} - - +} \ No newline at end of file diff --git a/src/main/java/es/in2/vcverifier/config/properties/SecurityProperties.java b/src/main/java/es/in2/vcverifier/config/properties/SecurityProperties.java index 573ea74..8adc962 100644 --- a/src/main/java/es/in2/vcverifier/config/properties/SecurityProperties.java +++ b/src/main/java/es/in2/vcverifier/config/properties/SecurityProperties.java @@ -7,24 +7,36 @@ import java.util.Optional; @ConfigurationProperties(prefix = "security") -public record SecurityProperties(String authorizationServer, @NestedConfigurationProperty TokenProperties token) { +public record SecurityProperties(String authorizationServer, @NestedConfigurationProperty TokenProperties token, + @NestedConfigurationProperty LoginCodeProperties loginCode) { @ConstructorBinding - public SecurityProperties(String authorizationServer, TokenProperties token) { + public SecurityProperties(String authorizationServer, TokenProperties token, LoginCodeProperties loginCode) { this.authorizationServer = authorizationServer; this.token = Optional.ofNullable(token).orElse(new TokenProperties(null)); + this.loginCode = Optional.ofNullable(loginCode).orElse(new LoginCodeProperties(null)); } public record TokenProperties(@NestedConfigurationProperty AccessTokenProperties accessToken) { @ConstructorBinding public TokenProperties(AccessTokenProperties accessToken) { - this.accessToken = Optional.ofNullable(accessToken).orElse(new AccessTokenProperties(null, null)); + this.accessToken = Optional.ofNullable(accessToken).orElse(new AccessTokenProperties("30", "MINUTES")); } public record AccessTokenProperties(String expiration, String cronUnit) { } - } + public record LoginCodeProperties(@NestedConfigurationProperty ExpirationProperties expirationProperties) { + + @ConstructorBinding + public LoginCodeProperties(ExpirationProperties expirationProperties) { + this.expirationProperties = Optional.ofNullable(expirationProperties).orElse(new ExpirationProperties("5", "MINUTES")); + } + + public record ExpirationProperties(String expiration, String cronUnit) { + } + } } + diff --git a/src/main/java/es/in2/vcverifier/controller/LoginQrController.java b/src/main/java/es/in2/vcverifier/controller/LoginQrController.java index 4fd51c3..f0d1ab4 100644 --- a/src/main/java/es/in2/vcverifier/controller/LoginQrController.java +++ b/src/main/java/es/in2/vcverifier/controller/LoginQrController.java @@ -1,5 +1,6 @@ package es.in2.vcverifier.controller; +import es.in2.vcverifier.config.properties.SecurityProperties; import es.in2.vcverifier.config.properties.VerifierUiLoginUrisProperties; import es.in2.vcverifier.exception.QRCodeGenerationException; import lombok.RequiredArgsConstructor; @@ -19,10 +20,11 @@ public class LoginQrController { private final VerifierUiLoginUrisProperties verifierUiLoginUrisProperties; + private final SecurityProperties securityProperties; @GetMapping("/login") @ResponseStatus(HttpStatus.OK) - public String showQrLogin(@RequestParam("authRequest") String authRequest, @RequestParam("state") String state, Model model) { + public String showQrLogin(@RequestParam("authRequest") String authRequest, @RequestParam("state") String state, Model model, @RequestParam("homeUri") String homeUri) { try { // Generar la imagen QR en base64 String qrImageBase64 = generateQRCodeImageBase64(authRequest); @@ -31,9 +33,12 @@ public String showQrLogin(@RequestParam("authRequest") String authRequest, @Requ model.addAttribute("authRequest", authRequest); // Pasar el sessionId al modelo model.addAttribute("state", state); + model.addAttribute("homeUri", homeUri); model.addAttribute("onboardingUri", verifierUiLoginUrisProperties.onboardingUri()); model.addAttribute("supportUri", verifierUiLoginUrisProperties.supportUri()); model.addAttribute("walletUri", verifierUiLoginUrisProperties.walletUri()); + model.addAttribute("cronUnit", securityProperties.loginCode().expirationProperties().cronUnit()); + model.addAttribute("expiration", securityProperties.loginCode().expirationProperties().expiration()); } catch (Exception e) { throw new QRCodeGenerationException(e.getMessage()); } diff --git a/src/main/java/es/in2/vcverifier/model/ClientData.java b/src/main/java/es/in2/vcverifier/model/ClientData.java index 1c1c291..f7b3f5d 100644 --- a/src/main/java/es/in2/vcverifier/model/ClientData.java +++ b/src/main/java/es/in2/vcverifier/model/ClientData.java @@ -4,6 +4,7 @@ public record ClientData( String id, + String domain, String clientId, String clientSecret, List redirectUris, diff --git a/src/main/java/es/in2/vcverifier/security/AuthorizationServerConfig.java b/src/main/java/es/in2/vcverifier/security/AuthorizationServerConfig.java index 4bc3dfa..61db530 100644 --- a/src/main/java/es/in2/vcverifier/security/AuthorizationServerConfig.java +++ b/src/main/java/es/in2/vcverifier/security/AuthorizationServerConfig.java @@ -67,7 +67,7 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h // Adds an AuthenticationConverter (pre-processor) used when attempting to extract // an OAuth2 authorization request (or consent) from HttpServletRequest to an instance // of OAuth2AuthorizationCodeRequestAuthenticationToken or OAuth2AuthorizationConsentAuthenticationToken. - .authorizationRequestConverter(new CustomAuthorizationRequestConverter(didService,jwtService,cryptoComponent,cacheStoreForAuthorizationRequestJWT,cacheStoreForOAuth2AuthorizationRequest,securityProperties)) + .authorizationRequestConverter(new CustomAuthorizationRequestConverter(didService,jwtService,cryptoComponent,cacheStoreForAuthorizationRequestJWT,cacheStoreForOAuth2AuthorizationRequest,securityProperties,registeredClientRepository)) .errorResponseHandler(new CustomErrorResponseHandler()) ) .tokenEndpoint(tokenEndpoint -> diff --git a/src/main/java/es/in2/vcverifier/security/filters/CustomAuthorizationRequestConverter.java b/src/main/java/es/in2/vcverifier/security/filters/CustomAuthorizationRequestConverter.java index 1162832..4193d13 100644 --- a/src/main/java/es/in2/vcverifier/security/filters/CustomAuthorizationRequestConverter.java +++ b/src/main/java/es/in2/vcverifier/security/filters/CustomAuthorizationRequestConverter.java @@ -2,9 +2,9 @@ import com.nimbusds.jose.JWSObject; import com.nimbusds.jwt.JWTClaimsSet; +import es.in2.vcverifier.component.CryptoComponent; import es.in2.vcverifier.config.CacheStore; import es.in2.vcverifier.config.properties.SecurityProperties; -import es.in2.vcverifier.component.CryptoComponent; import es.in2.vcverifier.exception.JWTParsingException; import es.in2.vcverifier.exception.RequestMismatchException; import es.in2.vcverifier.exception.RequestObjectRetrievalException; @@ -19,9 +19,13 @@ import org.json.JSONException; import org.json.JSONObject; import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.web.authentication.AuthenticationConverter; import java.io.IOException; @@ -50,6 +54,7 @@ public class CustomAuthorizationRequestConverter implements AuthenticationConver private final CacheStore cacheStoreForAuthorizationRequestJWT; private final CacheStore cacheStoreForOAuth2AuthorizationRequest; private final SecurityProperties securityProperties; + private final RegisteredClientRepository registeredClientRepository; /** * The Authorization Request MUST be signed by the Client, and MUST use the request_uri parameter which enables @@ -68,9 +73,17 @@ public Authentication convert(HttpServletRequest request) { String clientId = request.getParameter(CLIENT_ID); // client_id parameter String state = request.getParameter("state"); String scope = request.getParameter(SCOPE); + + RegisteredClient registeredClient = registeredClientRepository.findByClientId(clientId); + + if (registeredClient == null) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT); + } + if (clientId == null) { throw new IllegalArgumentException("Client ID is required."); } + // Case 1: JWT needs to be retrieved via "request_uri" if (requestUri != null) { log.info("Retrieving JWT from request_uri: " + requestUri); @@ -117,10 +130,15 @@ public Authentication convert(HttpServletRequest request) { .authRequest(signedAuthRequest) .build() ); + + // This is used to allow the user te return to the application if the user wants to cancel the login + String homeUri = registeredClient.getClientName(); + String authRequest = generateOpenId4VpUrl(nonce); - String redirectUrl = String.format("/login?authRequest=%s&state=%s", + String redirectUrl = String.format("/login?authRequest=%s&state=%s&homeUri=%s", URLEncoder.encode(authRequest, StandardCharsets.UTF_8), - URLEncoder.encode(state, StandardCharsets.UTF_8)); + URLEncoder.encode(state, StandardCharsets.UTF_8), + URLEncoder.encode(homeUri, StandardCharsets.UTF_8)); OAuth2Error error = new OAuth2Error("custom_error", "Redirection required", redirectUrl); throw new OAuth2AuthorizationCodeRequestAuthenticationException(error,null); } catch (ParseException e) { @@ -138,6 +156,7 @@ private boolean validateOAuth2Parameters(HttpServletRequest request, JWSObject j String jwtResponseType = jwtClaims.optString(RESPONSE_TYPE); String jwtClientId = jwtClaims.optString(CLIENT_ID); String jwtScope = jwtClaims.optString(SCOPE); + // Ensure that required OAuth 2.0 parameters match those in the JWT return requestResponseType.equals(jwtResponseType) && requestClientId.equals(jwtClientId) diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml index 8792b4d..7cf4a37 100644 --- a/src/main/resources/application-dev.yaml +++ b/src/main/resources/application-dev.yaml @@ -44,6 +44,10 @@ management: security: authorizationServer: + loginCode: + expirationProperties: + expiration: + cronUnit: token: accessToken: cronUnit: diff --git a/src/main/resources/application-local.yaml b/src/main/resources/application-local.yaml index 7031c6f..a251b7d 100644 --- a/src/main/resources/application-local.yaml +++ b/src/main/resources/application-local.yaml @@ -47,6 +47,10 @@ management: security: authorizationServer: "https://verifier.dome-marketplace-lcl.org" + loginCode: + expirationProperties: + expiration: "5" + cronUnit: "MINUTES" token: accessToken: expiration: "1" diff --git a/src/main/resources/application-prod.yaml b/src/main/resources/application-prod.yaml index 8792b4d..7cf4a37 100644 --- a/src/main/resources/application-prod.yaml +++ b/src/main/resources/application-prod.yaml @@ -44,6 +44,10 @@ management: security: authorizationServer: + loginCode: + expirationProperties: + expiration: + cronUnit: token: accessToken: cronUnit: diff --git a/src/main/resources/application-test.yaml b/src/main/resources/application-test.yaml index 8792b4d..7cf4a37 100644 --- a/src/main/resources/application-test.yaml +++ b/src/main/resources/application-test.yaml @@ -44,6 +44,10 @@ management: security: authorizationServer: + loginCode: + expirationProperties: + expiration: + cronUnit: token: accessToken: cronUnit: diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 34442a5..bcc6300 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -47,6 +47,10 @@ management: security: authorizationServer: "https://verifier.dome-marketplace-lcl.org" + loginCode: + expirationProperties: + expiration: "5" + cronUnit: "MINUTES" token: accessToken: expiration: "5" diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html index 0102897..207b328 100644 --- a/src/main/resources/templates/login.html +++ b/src/main/resources/templates/login.html @@ -6,11 +6,11 @@ - + - +