This commit is contained in:
DarioGii 2025-01-25 20:05:09 +00:00 committed by Dario Ghunney Ware
parent ace34004f9
commit 7793be6949
13 changed files with 269 additions and 204 deletions

View File

@ -184,7 +184,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
param = "error=badcredentials"; param = "error=badcredentials";
} }
String redirect_url = UrlUtils.getOrigin(request) + "/login?" + param; String redirectUrl = UrlUtils.getOrigin(request) + "/login?" + param;
// Redirect based on OAuth2 provider // Redirect based on OAuth2 provider
switch (registrationId.toLowerCase()) { switch (registrationId.toLowerCase()) {
@ -196,30 +196,30 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
+ "?client_id=" + "?client_id="
+ clientId + clientId
+ "&post_logout_redirect_uri=" + "&post_logout_redirect_uri="
+ response.encodeRedirectURL(redirect_url); + response.encodeRedirectURL(redirectUrl);
log.info("Redirecting to Keycloak logout URL: {}", logoutUrl); log.info("Redirecting to Keycloak logout URL: {}", logoutUrl);
response.sendRedirect(logoutUrl); response.sendRedirect(logoutUrl);
} }
case "github" -> { case "github" -> {
// Add GitHub specific logout URL if needed log.info(
// todo: why does the redirect go to github? shouldn't it come to Stirling PDF? "No redirect URL for GitHub. Redirecting to default logout URL: {}",
String githubLogoutUrl = "https://github.com/logout"; redirectUrl);
log.info("Redirecting to GitHub logout URL: {}", redirect_url); response.sendRedirect(redirectUrl);
response.sendRedirect(redirect_url);
} }
case "google" -> { case "google" -> {
// Add Google specific logout URL if needed // Add Google specific logout URL if needed
// String googleLogoutUrl = // String googleLogoutUrl =
// "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue=" // "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue="
// + response.encodeRedirectURL(redirect_url); // + response.encodeRedirectURL(redirectUrl);
log.info("Google does not have a specific logout URL"); log.info("Google does not have a specific logout URL");
// log.info("Redirecting to Google logout URL: " + googleLogoutUrl); // log.info("Redirecting to Google logout URL: " + googleLogoutUrl);
// response.sendRedirect(googleLogoutUrl); // response.sendRedirect(googleLogoutUrl);
} }
default -> { default -> {
String defaultRedirectUrl = request.getContextPath() + "/login?" + param; // String defaultRedirectUrl = request.getContextPath() + "/login?" +
log.info("Redirecting to default logout URL: {}", defaultRedirectUrl); // param;
response.sendRedirect(defaultRedirectUrl); log.info("Redirecting to default logout URL: {}", redirectUrl);
response.sendRedirect(redirectUrl);
} }
} }
} }

View File

@ -1,6 +1,7 @@
package stirling.software.SPDF.config.security.oauth2; package stirling.software.SPDF.config.security.oauth2;
import static org.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE; import static org.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE;
import static stirling.software.SPDF.utils.validation.Validator.*;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
@ -27,9 +28,7 @@ import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2; import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client; import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
import stirling.software.SPDF.model.provider.GithubProvider; import stirling.software.SPDF.model.provider.Provider;
import stirling.software.SPDF.model.provider.GoogleProvider;
import stirling.software.SPDF.model.provider.KeycloakProvider;
@Slf4j @Slf4j
@Configuration @Configuration
@ -68,10 +67,9 @@ public class OAuth2Configuration {
return Optional.empty(); return Optional.empty();
} }
GoogleProvider google = Provider google = applicationProperties.getSecurity().getOauth2().getClient().getGoogle();
applicationProperties.getSecurity().getOauth2().getClient().getGoogle();
return google != null && google.isSettingsValid() return validateSettings(google)
? Optional.of( ? Optional.of(
ClientRegistration.withRegistrationId(google.getName()) ClientRegistration.withRegistrationId(google.getName())
.clientId(google.getClientId()) .clientId(google.getClientId())
@ -79,7 +77,7 @@ public class OAuth2Configuration {
.scope(google.getScopes()) .scope(google.getScopes())
.authorizationUri(google.getAuthorizationUri()) .authorizationUri(google.getAuthorizationUri())
.tokenUri(google.getTokenUri()) .tokenUri(google.getTokenUri())
.userInfoUri(google.getUserinfoUri()) .userInfoUri(google.getUserInfoUri())
.userNameAttributeName(google.getUseAsUsername()) .userNameAttributeName(google.getUseAsUsername())
.clientName(google.getClientName()) .clientName(google.getClientName())
.redirectUri(REDIRECT_URI_PATH + google.getName()) .redirectUri(REDIRECT_URI_PATH + google.getName())
@ -93,10 +91,10 @@ public class OAuth2Configuration {
return Optional.empty(); return Optional.empty();
} }
KeycloakProvider keycloak = Provider keycloak =
applicationProperties.getSecurity().getOauth2().getClient().getKeycloak(); applicationProperties.getSecurity().getOauth2().getClient().getKeycloak();
return keycloak != null && keycloak.isSettingsValid() return validateSettings(keycloak)
? Optional.of( ? Optional.of(
ClientRegistrations.fromIssuerLocation(keycloak.getIssuer()) ClientRegistrations.fromIssuerLocation(keycloak.getIssuer())
.registrationId(keycloak.getName()) .registrationId(keycloak.getName())
@ -114,10 +112,9 @@ public class OAuth2Configuration {
return Optional.empty(); return Optional.empty();
} }
GithubProvider github = Provider github = applicationProperties.getSecurity().getOauth2().getClient().getGithub();
applicationProperties.getSecurity().getOauth2().getClient().getGithub();
return github != null && github.isSettingsValid() return validateSettings(github)
? Optional.of( ? Optional.of(
ClientRegistration.withRegistrationId(github.getName()) ClientRegistration.withRegistrationId(github.getName())
.clientId(github.getClientId()) .clientId(github.getClientId())
@ -125,7 +122,7 @@ public class OAuth2Configuration {
.scope(github.getScopes()) .scope(github.getScopes())
.authorizationUri(github.getAuthorizationUri()) .authorizationUri(github.getAuthorizationUri())
.tokenUri(github.getTokenUri()) .tokenUri(github.getTokenUri())
.userInfoUri(github.getUserinfoUri()) .userInfoUri(github.getUserInfoUri())
.userNameAttributeName(github.getUseAsUsername()) .userNameAttributeName(github.getUseAsUsername())
.clientName(github.getClientName()) .clientName(github.getClientName())
.redirectUri(REDIRECT_URI_PATH + github.getName()) .redirectUri(REDIRECT_URI_PATH + github.getName())
@ -180,6 +177,7 @@ public class OAuth2Configuration {
This following function is to grant Authorities to the OAUTH2 user from the values stored in the database. This following function is to grant Authorities to the OAUTH2 user from the values stored in the database.
This is required for the internal; 'hasRole()' function to give out the correct role. This is required for the internal; 'hasRole()' function to give out the correct role.
*/ */
@Bean @Bean
@ConditionalOnProperty( @ConditionalOnProperty(
value = "security.oauth2.enabled", value = "security.oauth2.enabled",

View File

@ -24,24 +24,17 @@ import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
@Configuration @Configuration
@Slf4j @Slf4j
@ConditionalOnProperty( @ConditionalOnProperty(value = "security.saml2.enabled", havingValue = "true")
value = "security.saml2.enabled",
havingValue = "true",
matchIfMissing = false)
public class SAML2Configuration { public class SAML2Configuration {
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
public SAML2Configuration(ApplicationProperties applicationProperties) { public SAML2Configuration(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties; this.applicationProperties = applicationProperties;
} }
@Bean @Bean
@ConditionalOnProperty( @ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
name = "security.saml2.enabled",
havingValue = "true",
matchIfMissing = false)
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception { public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
SAML2 samlConf = applicationProperties.getSecurity().getSaml2(); SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert()); X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
@ -71,10 +64,7 @@ public class SAML2Configuration {
} }
@Bean @Bean
@ConditionalOnProperty( @ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
name = "security.saml2.enabled",
havingValue = "true",
matchIfMissing = false)
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver( public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) { RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
OpenSaml4AuthenticationRequestResolver resolver = OpenSaml4AuthenticationRequestResolver resolver =

View File

@ -1,5 +1,7 @@
package stirling.software.SPDF.controller.web; package stirling.software.SPDF.controller.web;
import static stirling.software.SPDF.utils.validation.Validator.validateSettings;
import java.time.Instant; import java.time.Instant;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.*; import java.util.*;
@ -27,7 +29,7 @@ import stirling.software.SPDF.model.ApplicationProperties.Security;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2; import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client; import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2; import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.provider.GithubProvider; import stirling.software.SPDF.model.provider.GitHubProvider;
import stirling.software.SPDF.model.provider.GoogleProvider; import stirling.software.SPDF.model.provider.GoogleProvider;
import stirling.software.SPDF.model.provider.KeycloakProvider; import stirling.software.SPDF.model.provider.KeycloakProvider;
import stirling.software.SPDF.repository.UserRepository; import stirling.software.SPDF.repository.UserRepository;
@ -60,28 +62,37 @@ public class AccountWebController {
if (authentication != null && authentication.isAuthenticated()) { if (authentication != null && authentication.isAuthenticated()) {
return "redirect:/"; return "redirect:/";
} }
Map<String, String> providerList = new HashMap<>(); Map<String, String> providerList = new HashMap<>();
Security securityProps = applicationProperties.getSecurity(); Security securityProps = applicationProperties.getSecurity();
OAUTH2 oauth = securityProps.getOauth2(); OAUTH2 oauth = securityProps.getOauth2();
if (oauth != null) { if (oauth != null) {
if (oauth.getEnabled()) { if (oauth.getEnabled()) {
if (oauth.isSettingsValid()) { if (oauth.isSettingsValid()) {
providerList.put(OAUTH_2_AUTHORIZATION + "oidc", oauth.getProvider()); providerList.put(OAUTH_2_AUTHORIZATION + "oidc", oauth.getProvider());
} }
Client client = oauth.getClient(); Client client = oauth.getClient();
if (client != null) { if (client != null) {
GoogleProvider google = client.getGoogle(); GoogleProvider google = client.getGoogle();
if (google.isSettingsValid()) {
if (validateSettings(google)) {
providerList.put( providerList.put(
OAUTH_2_AUTHORIZATION + google.getName(), google.getClientName()); OAUTH_2_AUTHORIZATION + google.getName(), google.getClientName());
} }
GithubProvider github = client.getGithub();
if (github.isSettingsValid()) { GitHubProvider github = client.getGithub();
if (validateSettings(github)) {
providerList.put( providerList.put(
OAUTH_2_AUTHORIZATION + github.getName(), github.getClientName()); OAUTH_2_AUTHORIZATION + github.getName(), github.getClientName());
} }
KeycloakProvider keycloak = client.getKeycloak(); KeycloakProvider keycloak = client.getKeycloak();
if (keycloak.isSettingsValid()) {
if (validateSettings(keycloak)) {
providerList.put( providerList.put(
OAUTH_2_AUTHORIZATION + keycloak.getName(), OAUTH_2_AUTHORIZATION + keycloak.getName(),
keycloak.getClientName()); keycloak.getClientName());
@ -89,101 +100,74 @@ public class AccountWebController {
} }
} }
} }
SAML2 saml2 = securityProps.getSaml2(); SAML2 saml2 = securityProps.getSaml2();
if (securityProps.isSaml2Activ() if (securityProps.isSaml2Activ()
&& applicationProperties.getSystem().getEnableAlphaFunctionality()) { && applicationProperties.getSystem().getEnableAlphaFunctionality()) {
providerList.put("/saml2/authenticate/" + saml2.getRegistrationId(), "SAML 2"); providerList.put("/saml2/authenticate/" + saml2.getRegistrationId(), "SAML 2");
} }
// Remove any null keys/values from the providerList // Remove any null keys/values from the providerList
providerList providerList
.entrySet() .entrySet()
.removeIf(entry -> entry.getKey() == null || entry.getValue() == null); .removeIf(entry -> entry.getKey() == null || entry.getValue() == null);
model.addAttribute("providerlist", providerList); model.addAttribute("providerlist", providerList);
model.addAttribute("loginMethod", securityProps.getLoginMethod()); model.addAttribute("loginMethod", securityProps.getLoginMethod());
boolean altLogin = !providerList.isEmpty() ? securityProps.isAltLogin() : false; boolean altLogin = !providerList.isEmpty() ? securityProps.isAltLogin() : false;
model.addAttribute("altLogin", altLogin); model.addAttribute("altLogin", altLogin);
model.addAttribute("currentPage", "login"); model.addAttribute("currentPage", "login");
String error = request.getParameter("error"); String error = request.getParameter("error");
if (error != null) { if (error != null) {
switch (error) { switch (error) {
case "badcredentials": case "badcredentials" -> error = "login.invalid";
error = "login.invalid"; case "locked" -> error = "login.locked";
break; case "oauth2AuthenticationError" -> error = "userAlreadyExistsOAuthMessage";
case "locked":
error = "login.locked";
break;
case "oauth2AuthenticationError":
error = "userAlreadyExistsOAuthMessage";
break;
default:
break;
} }
model.addAttribute("error", error); model.addAttribute("error", error);
} }
String erroroauth = request.getParameter("erroroauth"); String erroroauth = request.getParameter("erroroauth");
if (erroroauth != null) { if (erroroauth != null) {
switch (erroroauth) { switch (erroroauth) {
case "oauth2AutoCreateDisabled": case "oauth2AutoCreateDisabled" -> erroroauth = "login.oauth2AutoCreateDisabled";
erroroauth = "login.oauth2AutoCreateDisabled"; case "invalidUsername" -> erroroauth = "login.invalid";
break; case "userAlreadyExistsWeb" -> erroroauth = "userAlreadyExistsWebMessage";
case "invalidUsername": case "oauth2AuthenticationErrorWeb" -> erroroauth = "login.oauth2InvalidUserType";
erroroauth = "login.invalid"; case "invalid_token_response" -> erroroauth = "login.oauth2InvalidTokenResponse";
break; case "authorization_request_not_found" ->
case "userAlreadyExistsWeb":
erroroauth = "userAlreadyExistsWebMessage";
break;
case "oauth2AuthenticationErrorWeb":
erroroauth = "login.oauth2InvalidUserType";
break;
case "invalid_token_response":
erroroauth = "login.oauth2InvalidTokenResponse";
break;
case "authorization_request_not_found":
erroroauth = "login.oauth2RequestNotFound"; erroroauth = "login.oauth2RequestNotFound";
break; case "access_denied" -> erroroauth = "login.oauth2AccessDenied";
case "access_denied": case "invalid_user_info_response" ->
erroroauth = "login.oauth2AccessDenied";
break;
case "invalid_user_info_response":
erroroauth = "login.oauth2InvalidUserInfoResponse"; erroroauth = "login.oauth2InvalidUserInfoResponse";
break; case "invalid_request" -> erroroauth = "login.oauth2invalidRequest";
case "invalid_request": case "invalid_id_token" -> erroroauth = "login.oauth2InvalidIdToken";
erroroauth = "login.oauth2invalidRequest"; case "oauth2_admin_blocked_user" -> erroroauth = "login.oauth2AdminBlockedUser";
break; case "userIsDisabled" -> erroroauth = "login.userIsDisabled";
case "invalid_id_token": case "invalid_destination" -> erroroauth = "login.invalid_destination";
erroroauth = "login.oauth2InvalidIdToken"; case "relying_party_registration_not_found" ->
break;
case "oauth2_admin_blocked_user":
erroroauth = "login.oauth2AdminBlockedUser";
break;
case "userIsDisabled":
erroroauth = "login.userIsDisabled";
break;
case "invalid_destination":
erroroauth = "login.invalid_destination";
break;
case "relying_party_registration_not_found":
erroroauth = "login.relyingPartyRegistrationNotFound"; erroroauth = "login.relyingPartyRegistrationNotFound";
break;
// Valid InResponseTo was not available from the validation context, unable to // Valid InResponseTo was not available from the validation context, unable to
// evaluate // evaluate
case "invalid_in_response_to": case "invalid_in_response_to" -> erroroauth = "login.invalid_in_response_to";
erroroauth = "login.invalid_in_response_to"; case "not_authentication_provider_found" ->
break;
case "not_authentication_provider_found":
erroroauth = "login.not_authentication_provider_found"; erroroauth = "login.not_authentication_provider_found";
break;
default:
break;
} }
model.addAttribute("erroroauth", erroroauth); model.addAttribute("erroroauth", erroroauth);
} }
if (request.getParameter("messageType") != null) { if (request.getParameter("messageType") != null) {
model.addAttribute("messageType", "changedCredsMessage"); model.addAttribute("messageType", "changedCredsMessage");
} }
if (request.getParameter("logout") != null) { if (request.getParameter("logout") != null) {
model.addAttribute("logoutMessage", "You have been logged out."); model.addAttribute("logoutMessage", "You have been logged out.");
} }
return "login"; return "login";
} }

View File

@ -34,7 +34,7 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.InstallationPathConfig; import stirling.software.SPDF.config.InstallationPathConfig;
import stirling.software.SPDF.config.YamlPropertySourceFactory; import stirling.software.SPDF.config.YamlPropertySourceFactory;
import stirling.software.SPDF.model.exception.UnsupportedProviderException; import stirling.software.SPDF.model.exception.UnsupportedProviderException;
import stirling.software.SPDF.model.provider.GithubProvider; import stirling.software.SPDF.model.provider.GitHubProvider;
import stirling.software.SPDF.model.provider.GoogleProvider; import stirling.software.SPDF.model.provider.GoogleProvider;
import stirling.software.SPDF.model.provider.KeycloakProvider; import stirling.software.SPDF.model.provider.KeycloakProvider;
import stirling.software.SPDF.model.provider.Provider; import stirling.software.SPDF.model.provider.Provider;
@ -253,7 +253,7 @@ public class ApplicationProperties {
@Data @Data
public static class Client { public static class Client {
private GoogleProvider google = new GoogleProvider(); private GoogleProvider google = new GoogleProvider();
private GithubProvider github = new GithubProvider(); private GitHubProvider github = new GitHubProvider();
private KeycloakProvider keycloak = new KeycloakProvider(); private KeycloakProvider keycloak = new KeycloakProvider();
public Provider get(String registrationId) throws UnsupportedProviderException { public Provider get(String registrationId) throws UnsupportedProviderException {

View File

@ -6,7 +6,7 @@ import java.util.Collection;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@NoArgsConstructor @NoArgsConstructor
public class GithubProvider extends Provider { public class GitHubProvider extends Provider {
private static final String NAME = "github"; private static final String NAME = "github";
private static final String CLIENT_NAME = "GitHub"; private static final String CLIENT_NAME = "GitHub";
@ -14,51 +14,67 @@ public class GithubProvider extends Provider {
private static final String TOKEN_URI = "https://github.com/login/oauth/access_token"; private static final String TOKEN_URI = "https://github.com/login/oauth/access_token";
private static final String USER_INFO_URI = "https://api.github.com/user"; private static final String USER_INFO_URI = "https://api.github.com/user";
private String clientId; public GitHubProvider(String clientId, String clientSecret, String useAsUsername) {
private String clientSecret; super(
private Collection<String> scopes = new ArrayList<>(); null,
private String useAsUsername = "login"; NAME,
CLIENT_NAME,
public GithubProvider( clientId,
String clientId, String clientSecret, Collection<String> scopes, String useAsUsername) { clientSecret,
super(null, NAME, CLIENT_NAME, clientId, clientSecret, scopes, useAsUsername); new ArrayList<>(),
this.clientId = clientId; useAsUsername != null ? useAsUsername : "login",
this.clientSecret = clientSecret; AUTHORIZATION_URI,
this.scopes = scopes; TOKEN_URI,
this.useAsUsername = useAsUsername; USER_INFO_URI);
} }
@Override
public String getAuthorizationUri() { public String getAuthorizationUri() {
return AUTHORIZATION_URI; return AUTHORIZATION_URI;
} }
@Override
public String getTokenUri() { public String getTokenUri() {
return TOKEN_URI; return TOKEN_URI;
} }
public String getUserinfoUri() { @Override
public String getUserInfoUri() {
return USER_INFO_URI; return USER_INFO_URI;
} }
@Override
public String getName() {
return NAME;
}
@Override
public String getClientName() {
return CLIENT_NAME;
}
@Override @Override
public Collection<String> getScopes() { public Collection<String> getScopes() {
Collection<String> scopes = super.getScopes();
if (scopes == null || scopes.isEmpty()) { if (scopes == null || scopes.isEmpty()) {
scopes = new ArrayList<>(); scopes = new ArrayList<>();
scopes.add("read:user"); scopes.add("read:user");
} }
return scopes; return scopes;
} }
@Override @Override
public String toString() { public String toString() {
return "GitHub [clientId=" return "GitHub [clientId="
+ clientId + getClientId()
+ ", clientSecret=" + ", clientSecret="
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL") + (getClientSecret() != null && !getClientSecret().isEmpty() ? "MASKED" : "NULL")
+ ", scopes=" + ", scopes="
+ scopes + getScopes()
+ ", useAsUsername=" + ", useAsUsername="
+ useAsUsername + getUseAsUsername()
+ "]"; + "]";
} }
} }

View File

@ -15,18 +15,18 @@ public class GoogleProvider extends Provider {
private static final String USER_INFO_URI = private static final String USER_INFO_URI =
"https://www.googleapis.com/oauth2/v3/userinfo?alt=json"; "https://www.googleapis.com/oauth2/v3/userinfo?alt=json";
private String clientId; public GoogleProvider(String clientId, String clientSecret, String useAsUsername) {
private String clientSecret; super(
private Collection<String> scopes = new ArrayList<>(); null,
private String useAsUsername = "email"; NAME,
CLIENT_NAME,
public GoogleProvider( clientId,
String clientId, String clientSecret, Collection<String> scopes, String useAsUsername) { clientSecret,
super(null, NAME, CLIENT_NAME, clientId, clientSecret, scopes, useAsUsername); new ArrayList<>(),
this.clientId = clientId; useAsUsername,
this.clientSecret = clientSecret; AUTHORIZATION_URI,
this.scopes = scopes; TOKEN_URI,
this.useAsUsername = useAsUsername; USER_INFO_URI);
} }
public String getAuthorizationUri() { public String getAuthorizationUri() {
@ -41,26 +41,39 @@ public class GoogleProvider extends Provider {
return USER_INFO_URI; return USER_INFO_URI;
} }
@Override
public String getName() {
return NAME;
}
@Override
public String getClientName() {
return CLIENT_NAME;
}
@Override @Override
public Collection<String> getScopes() { public Collection<String> getScopes() {
Collection<String> scopes = super.getScopes();
if (scopes == null || scopes.isEmpty()) { if (scopes == null || scopes.isEmpty()) {
scopes = new ArrayList<>(); scopes = new ArrayList<>();
scopes.add("https://www.googleapis.com/auth/userinfo.email"); scopes.add("https://www.googleapis.com/auth/userinfo.email");
scopes.add("https://www.googleapis.com/auth/userinfo.profile"); scopes.add("https://www.googleapis.com/auth/userinfo.profile");
} }
return scopes; return scopes;
} }
@Override @Override
public String toString() { public String toString() {
return "Google [clientId=" return "Google [clientId="
+ clientId + getClientId()
+ ", clientSecret=" + ", clientSecret="
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL") + (getClientSecret() != null && !getClientSecret().isEmpty() ? "MASKED" : "NULL")
+ ", scopes=" + ", scopes="
+ scopes + getScopes()
+ ", useAsUsername=" + ", useAsUsername="
+ useAsUsername + getUseAsUsername()
+ "]"; + "]";
} }
} }

View File

@ -11,24 +11,29 @@ public class KeycloakProvider extends Provider {
private static final String NAME = "keycloak"; private static final String NAME = "keycloak";
private static final String CLIENT_NAME = "Keycloak"; private static final String CLIENT_NAME = "Keycloak";
private String issuer;
private String clientId;
private String clientSecret;
private Collection<String> scopes;
private String useAsUsername = "email";
public KeycloakProvider( public KeycloakProvider(
String issuer, String issuer, String clientId, String clientSecret, String useAsUsername) {
String clientId, super(
String clientSecret, issuer,
Collection<String> scopes, NAME,
String useAsUsername) { CLIENT_NAME,
super(issuer, NAME, CLIENT_NAME, clientId, clientSecret, scopes, useAsUsername); clientId,
this.useAsUsername = useAsUsername; clientSecret,
this.issuer = issuer; new ArrayList<>(),
this.clientId = clientId; useAsUsername,
this.clientSecret = clientSecret; null,
this.scopes = scopes; null,
null);
}
@Override
public String getName() {
return NAME;
}
@Override
public String getClientName() {
return CLIENT_NAME;
} }
@Override @Override
@ -47,15 +52,15 @@ public class KeycloakProvider extends Provider {
@Override @Override
public String toString() { public String toString() {
return "Keycloak [issuer=" return "Keycloak [issuer="
+ issuer + getIssuer()
+ ", clientId=" + ", clientId="
+ clientId + getClientId()
+ ", clientSecret=" + ", clientSecret="
+ (clientSecret != null && !clientSecret.isBlank() ? "MASKED" : "NULL") + (getClientSecret() != null && !getClientSecret().isBlank() ? "MASKED" : "NULL")
+ ", scopes=" + ", scopes="
+ scopes + getScopes()
+ ", useAsUsername=" + ", useAsUsername="
+ useAsUsername + getUseAsUsername()
+ "]"; + "]";
} }
} }

View File

@ -18,6 +18,9 @@ public abstract class Provider {
private String clientSecret; private String clientSecret;
private Collection<String> scopes; private Collection<String> scopes;
private String useAsUsername; private String useAsUsername;
private String authorizationUri;
private String tokenUri;
private String userInfoUri;
public Provider( public Provider(
String issuer, String issuer,
@ -26,7 +29,10 @@ public abstract class Provider {
String clientId, String clientId,
String clientSecret, String clientSecret,
Collection<String> scopes, Collection<String> scopes,
String useAsUsername) { String useAsUsername,
String authorizationUri,
String tokenUri,
String userInfoUri) {
this.issuer = issuer; this.issuer = issuer;
this.name = name; this.name = name;
this.clientName = clientName; this.clientName = clientName;
@ -34,28 +40,15 @@ public abstract class Provider {
this.clientSecret = clientSecret; this.clientSecret = clientSecret;
this.scopes = scopes; this.scopes = scopes;
this.useAsUsername = !useAsUsername.isBlank() ? useAsUsername : "email"; this.useAsUsername = !useAsUsername.isBlank() ? useAsUsername : "email";
} this.authorizationUri = authorizationUri;
this.tokenUri = tokenUri;
// todo: why are we passing name here if it's not used? this.userInfoUri = userInfoUri;
// todo: use util class/method
public boolean isSettingsValid() {
return isValid(this.getIssuer(), "issuer")
&& isValid(this.getClientId(), "clientId")
&& isValid(this.getClientSecret(), "clientSecret")
&& isValid(this.getScopes(), "scopes")
&& isValid(this.getUseAsUsername(), "useAsUsername");
}
private boolean isValid(String value, String name) {
return value != null && !value.isBlank();
}
private boolean isValid(Collection<String> value, String name) {
return value != null && !value.isEmpty();
} }
public void setScopes(String scopes) { public void setScopes(String scopes) {
if (scopes != null && !scopes.isBlank()) {
this.scopes = this.scopes =
Arrays.stream(scopes.split(",")).map(String::trim).collect(Collectors.toList()); Arrays.stream(scopes.split(",")).map(String::trim).collect(Collectors.toList());
} }
}
} }

View File

@ -1,11 +0,0 @@
package stirling.software.SPDF.utils.validation;
import java.util.Collection;
public class CollectionValidator implements Validator<Collection<String>> {
@Override
public boolean validate(Collection<String> input, String path) {
return input != null && !input.isEmpty();
}
}

View File

@ -1,9 +0,0 @@
package stirling.software.SPDF.utils.validation;
public class StringValidator implements Validator<String> {
@Override
public boolean validate(String input, String path) {
return input != null && !input.isBlank();
}
}

View File

@ -1,6 +1,36 @@
package stirling.software.SPDF.utils.validation; package stirling.software.SPDF.utils.validation;
public interface Validator<T> { import java.util.Collection;
boolean validate(T input, String path); import stirling.software.SPDF.model.provider.Provider;
public class Validator {
public static boolean validateSettings(Provider provider) {
if (provider == null) {
return false;
}
if (isStringEmpty(provider.getClientId())) {
return false;
}
if (isStringEmpty(provider.getClientSecret())) {
return false;
}
if (isCollectionEmpty(provider.getScopes())) {
return false;
}
return !isStringEmpty(provider.getUseAsUsername());
}
private static boolean isStringEmpty(String input) {
return input == null || input.isBlank();
}
private static boolean isCollectionEmpty(Collection<String> input) {
return input == null || input.isEmpty();
}
} }

View File

@ -0,0 +1,56 @@
package stirling.software.SPDF.utils.validation;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.junit.jupiter.MockitoExtension;
import stirling.software.SPDF.model.provider.GitHubProvider;
import stirling.software.SPDF.model.provider.GoogleProvider;
import stirling.software.SPDF.model.provider.KeycloakProvider;
import stirling.software.SPDF.model.provider.Provider;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class ValidatorTest {
@Test
void testSuccessfulValidation() {
var provider = mock(GitHubProvider.class);
when(provider.getClientId()).thenReturn("clientId");
when(provider.getClientSecret()).thenReturn("clientSecret");
when(provider.getScopes()).thenReturn(List.of("read:user"));
when(provider.getUseAsUsername()).thenReturn("email");
assertTrue(Validator.validateSettings(provider));
}
@ParameterizedTest
@MethodSource("providerParams")
void testUnsuccessfulValidation(Provider provider) {
assertFalse(Validator.validateSettings(provider));
}
public static Stream<Arguments> providerParams() {
var generic = new GitHubProvider(null, "clientSecret", " ");
var google = new GoogleProvider(null, "clientSecret", "email");
var github = new GitHubProvider("clientId", "", "email");
var keycloak = new KeycloakProvider("issuer", "clientId", "clientSecret", " ");
return Stream.of(
Arguments.of(generic),
Arguments.of(google),
Arguments.of(github),
Arguments.of(keycloak)
);
}
}