diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..788be24434 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.git +docker/postgresql-data +docker/postgresql-16-data +docker/roller-data +it-selenium diff --git a/Dockerfile b/Dockerfile index 330b2cb381..487124e8c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,17 +23,9 @@ FROM maven:3-eclipse-temurin-17 AS builder -COPY ./docker /project/docker - -# Build Apache Roller - -WORKDIR /tmp -RUN apt-get update && apt-get install -y git -RUN git clone https://github.com/apache/roller.git -WORKDIR /tmp/roller -# change to branch/tag you prefer -RUN git checkout tags/roller-6.1.0; \ -mvn -Duser.home=/builder/home -DskipTests=true -B clean install +COPY . /project +WORKDIR /project +RUN mvn -Duser.home=/builder/home -DskipTests=true -B clean install # STAGE 2 - PACKAGE ------------------------------------------------ @@ -51,7 +43,7 @@ ARG DATABASE_JDBC_DRIVERCLASS=org.postgresql.Driver ARG DATABASE_JDBC_CONNECTIONURL=jdbc:postgresql://postgresql/rollerdb ARG DATABASE_JDBC_USERNAME=scott ARG DATABASE_JDBC_PASSWORD=tiger -ARG DATABASE_HOST=postgresql:5434 +ARG DATABASE_HOST=postgresql:5432 ENV STORAGE_ROOT ${STORAGE_ROOT} ENV DATABASE_JDBC_DRIVERCLASS ${DATABASE_JDBC_DRIVERCLASS} @@ -63,7 +55,7 @@ ENV DATABASE_HOST ${DATABASE_HOST} # install Roller WAR as ROOT.war, create data dirs WORKDIR /usr/local/roller -COPY --from=builder /tmp/roller/app/target/roller.war /usr/local/tomcat/webapps/ROOT.war +COPY --from=builder /project/app/target/roller.war /usr/local/tomcat/webapps/ROOT.war RUN mkdir -p data/mediafiles data/searchindex # download PostgreSQL and MySQL drivers plus Mail and Activation JARs @@ -78,8 +70,8 @@ RUN wget https://repo1.maven.org/maven2/org/eclipse/angus/angus-activation/2.0.2 # Add Roller entry-point and go! -COPY --from=builder /project/docker/entry-point.sh /usr/local/tomcat/bin -COPY --from=builder /project/docker/wait-for-it.sh /usr/local/tomcat/bin +COPY docker/entry-point.sh /usr/local/tomcat/bin +COPY docker/wait-for-it.sh /usr/local/tomcat/bin RUN chgrp -R 0 /usr/local/tomcat RUN chmod -R g+rw /usr/local/tomcat diff --git a/app/pom.xml b/app/pom.xml index 357120e671..e127402c0d 100644 --- a/app/pom.xml +++ b/app/pom.xml @@ -485,6 +485,24 @@ limitations under the License. + + org.springframework.security + spring-security-oauth2-client + ${spring.security.version} + + + + org.springframework.security + spring-security-oauth2-jose + ${spring.security.version} + + + + com.fasterxml.jackson.core + jackson-databind + 2.18.3 + + diff --git a/app/src/main/java/org/apache/roller/weblogger/config/AuthMethod.java b/app/src/main/java/org/apache/roller/weblogger/config/AuthMethod.java index 3c4fa58af1..1d2248b9fc 100644 --- a/app/src/main/java/org/apache/roller/weblogger/config/AuthMethod.java +++ b/app/src/main/java/org/apache/roller/weblogger/config/AuthMethod.java @@ -17,11 +17,14 @@ */ package org.apache.roller.weblogger.config; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + public enum AuthMethod { ROLLERDB("db"), LDAP("ldap"), - OPENID("openid"), - DB_OPENID("db-openid"), + OIDC("oidc"), + DB_OIDC("db-oidc"), CMA("cma"); private final String propertyName; @@ -34,7 +37,22 @@ public String getPropertyName() { return propertyName; } + private static final Log log = LogFactory.getLog(AuthMethod.class); + private static boolean warnedAboutOpenId; + public static AuthMethod getAuthMethod(String propertyName) { + // OpenID 2.0 was replaced by OIDC; accept the old property values so + // an upgraded install boots instead of failing on every request + if ("openid".equals(propertyName) || "db-openid".equals(propertyName)) { + AuthMethod replacement = "openid".equals(propertyName) ? OIDC : DB_OIDC; + if (!warnedAboutOpenId) { + warnedAboutOpenId = true; + log.warn("authentication.method=" + propertyName + " is no longer supported and is " + + "treated as " + replacement.getPropertyName() + "; update the property and " + + "configure an oidc.{id}.* provider registration"); + } + return replacement; + } for (AuthMethod test : AuthMethod.values()) { if (test.getPropertyName().equals(propertyName)) { return test; diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/core/RollerContext.java b/app/src/main/java/org/apache/roller/weblogger/ui/core/RollerContext.java index 9413bc8caf..65f11a1b0c 100644 --- a/app/src/main/java/org/apache/roller/weblogger/ui/core/RollerContext.java +++ b/app/src/main/java/org/apache/roller/weblogger/ui/core/RollerContext.java @@ -45,6 +45,7 @@ import org.apache.roller.weblogger.ui.core.plugins.UIPluginManager; import org.apache.roller.weblogger.ui.core.plugins.UIPluginManagerImpl; import org.apache.roller.weblogger.ui.core.security.AutoProvision; +import org.apache.roller.weblogger.ui.core.security.RollerClientRegistrationRepository; import org.apache.roller.weblogger.util.Reflection; import org.apache.roller.weblogger.util.cache.CacheManager; import org.apache.velocity.runtime.RuntimeSingleton; @@ -309,7 +310,10 @@ private DelegatingPasswordEncoder createPasswordEncoder() { // supported encoders encoders.put("bcrypt", new BCryptPasswordEncoder()); - encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8()); + // pbkdf2 stores only salt+hash, so its parameters must stay as they were + // when existing passwords were encoded or those passwords stop verifying. + // scrypt and argon2 encode their parameters, so they can take v5_8. + encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5()); // provided by bouncy castle dependency encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8()); encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()); @@ -340,6 +344,21 @@ private DelegatingPasswordEncoder createPasswordEncoder() { } + /** + * The OIDC client registrations declared in security.xml, or null when + * OIDC is not configured. + */ + public static RollerClientRegistrationRepository getClientRegistrationRepository() { + ApplicationContext ctx = + WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext); + try { + return ctx.getBean("clientRegistrationRepository", RollerClientRegistrationRepository.class); + } catch (NoSuchBeanDefinitionException exc) { + log.debug("No clientRegistrationRepository bean in context", exc); + return null; + } + } + /** * Flush user from any caches maintained by security system. */ diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/core/RollerSession.java b/app/src/main/java/org/apache/roller/weblogger/ui/core/RollerSession.java index 959cc3e908..f02fba54b6 100644 --- a/app/src/main/java/org/apache/roller/weblogger/ui/core/RollerSession.java +++ b/app/src/main/java/org/apache/roller/weblogger/ui/core/RollerSession.java @@ -34,6 +34,9 @@ import org.apache.roller.weblogger.business.UserManager; import org.apache.roller.weblogger.pojos.User; import org.apache.roller.weblogger.ui.core.security.AutoProvision; +import org.apache.roller.weblogger.ui.core.security.RollerOidcUserService; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; /** @@ -90,6 +93,15 @@ public static RollerSession getRollerSession(HttpServletRequest request) { UserManager umgr = WebloggerFactory.getWeblogger().getUserManager(); User user = umgr.getUserByUserName(principal.getName()); + // For OIDC-authenticated users, look up by OIDC subject + if (user == null && principal instanceof OAuth2AuthenticationToken oauthToken) { + Object oauthPrincipal = oauthToken.getPrincipal(); + if (oauthPrincipal instanceof OidcUser oidcUser) { + String oidcSubject = RollerOidcUserService.toOidcSubject(oidcUser); + user = umgr.getUserByOpenIdUrl(oidcSubject); + } + } + // try one time to auto-provision, only happens if user==null // which means installation has LDAP enabled in security.xml if (user == null && WebloggerConfig.getBooleanProperty("users.ldap.autoProvision.enabled")) { diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/core/security/RollerClientRegistrationRepository.java b/app/src/main/java/org/apache/roller/weblogger/ui/core/security/RollerClientRegistrationRepository.java new file mode 100644 index 0000000000..d84db0cba1 --- /dev/null +++ b/app/src/main/java/org/apache/roller/weblogger/ui/core/security/RollerClientRegistrationRepository.java @@ -0,0 +1,235 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. For additional information regarding + * copyright in this work, please see the NOTICE file in the top level + * directory of this distribution. + */ +package org.apache.roller.weblogger.ui.core.security; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.roller.weblogger.config.AuthMethod; +import org.apache.roller.weblogger.config.WebloggerConfig; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.ClientRegistrations; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; + +/** + * Builds OAuth2/OIDC client registrations from Roller properties. + * + *

No registrations are served unless {@code authentication.method} is + * {@code oidc} or {@code db-oidc}, so configuring providers under another + * method does not open the {@code /oauth2/authorization/*} endpoints. + * + *

OIDC discovery is deferred until first access so the identity provider + * does not need to be reachable during application startup. Each provider is + * resolved and cached independently under a discovery timeout: one unreachable + * provider does not block the others, a failed provider is retried with a + * bounded backoff instead of on every request, and concurrent requests do not + * pile onto the same discovery (an in-flight provider is simply skipped until + * its attempt finishes). + * + *

Properties follow the pattern: + *

+ * oidc.{registrationId}.client-id=...
+ * oidc.{registrationId}.client-secret=...
+ * oidc.{registrationId}.issuer-uri=...
+ * oidc.{registrationId}.client-name=...  (optional, defaults to registrationId)
+ * oidc.{registrationId}.scope=openid,profile,email  (optional)
+ * oidc.{registrationId}.client-authentication-method=none  (optional, for a
+ *     public client using PKCE; without it a client-secret is required)
+ * 
+ */ +public class RollerClientRegistrationRepository implements ClientRegistrationRepository, Iterable { + + private static final Log log = LogFactory.getLog(RollerClientRegistrationRepository.class); + private static final String PREFIX = "oidc."; + private static final long RETRY_BACKOFF_MS = 60_000; + private static final long DISCOVERY_TIMEOUT_MS = 10_000; + + private final Map resolved = new ConcurrentHashMap<>(); + private final Map failedAt = new ConcurrentHashMap<>(); + private final Map inFlight = new ConcurrentHashMap<>(); + + @Override + public ClientRegistration findByRegistrationId(String registrationId) { + if (!oidcEnabled()) { + return null; + } + ClientRegistration registration = resolved.get(registrationId); + if (registration != null) { + return registration; + } + // resolve only the requested provider, not the whole configuration + String clientId = configuredProviderIds().get(registrationId); + if (clientId != null) { + resolveProvider(registrationId, clientId); + } + return resolved.get(registrationId); + } + + @Override + public Iterator iterator() { + return getRegistrations().values().iterator(); + } + + /** Whether the configured authentication method allows OIDC login at all. */ + static boolean oidcEnabled() { + AuthMethod method = WebloggerConfig.getAuthMethod(); + return method == AuthMethod.OIDC || method == AuthMethod.DB_OIDC; + } + + /** + * The successfully resolved registrations, in configuration order. Providers + * that have not resolved yet are attempted, unless they failed within the + * retry backoff window. + */ + public Map getRegistrations() { + if (!oidcEnabled()) { + return Collections.emptyMap(); + } + + Map configured = configuredProviderIds(); + for (Map.Entry entry : configured.entrySet()) { + resolveProvider(entry.getKey(), entry.getValue()); + } + + // return in configuration order, only what resolved + Map result = new LinkedHashMap<>(); + for (String id : configured.keySet()) { + ClientRegistration registration = resolved.get(id); + if (registration != null) { + result.put(id, registration); + } + } + return Collections.unmodifiableMap(result); + } + + /** + * Attempts discovery for one provider unless it is already resolved, failed + * within the backoff window, or another thread is on it right now. + */ + private void resolveProvider(String id, String clientId) { + if (resolved.containsKey(id)) { + return; + } + Long lastFailure = failedAt.get(id); + if (lastFailure != null && System.currentTimeMillis() - lastFailure < RETRY_BACKOFF_MS) { + return; + } + if (inFlight.putIfAbsent(id, Boolean.TRUE) != null) { + return; + } + try { + // discovery has no timeout hook of its own, so bound the wait here; + // an abandoned attempt still occupies its pool thread until the + // connection gives up, but request threads stop paying for it + ClientRegistration registration = null; + try { + registration = CompletableFuture.supplyAsync(() -> buildRegistration(id, clientId)) + .get(DISCOVERY_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + log.error("OIDC discovery for provider '" + id + "' timed out after " + + DISCOVERY_TIMEOUT_MS + "ms, will retry in " + (RETRY_BACKOFF_MS / 1000) + "s"); + } catch (Exception e) { + log.error("OIDC discovery for provider '" + id + "' failed", e); + } + if (registration != null) { + resolved.put(id, registration); + failedAt.remove(id); + } else { + failedAt.put(id, System.currentTimeMillis()); + } + } finally { + inFlight.remove(id); + } + } + + /** Registration ids that have an {@code oidc..client-id} property set. */ + static Map configuredProviderIds() { + Map registrationIds = new LinkedHashMap<>(); + Enumeration keys = WebloggerConfig.keys(); + while (keys.hasMoreElements()) { + String key = (String) keys.nextElement(); + if (key.startsWith(PREFIX) && key.endsWith(".client-id")) { + String id = key.substring(PREFIX.length(), key.length() - ".client-id".length()); + String clientId = WebloggerConfig.getProperty(key); + if (clientId != null && !clientId.isBlank()) { + registrationIds.put(id, clientId); + } + } + } + return registrationIds; + } + + private ClientRegistration buildRegistration(String id, String clientId) { + String clientSecret = WebloggerConfig.getProperty(PREFIX + id + ".client-secret"); + String issuerUri = WebloggerConfig.getProperty(PREFIX + id + ".issuer-uri"); + String clientName = WebloggerConfig.getProperty(PREFIX + id + ".client-name", id); + String scopeStr = WebloggerConfig.getProperty(PREFIX + id + ".scope", "openid,profile,email"); + String clientAuthMethod = WebloggerConfig.getProperty(PREFIX + id + ".client-authentication-method"); + + if (clientId == null || clientId.isBlank() || issuerUri == null || issuerUri.isBlank()) { + log.warn("Skipping OIDC registration '" + id + "': client-id and issuer-uri are required"); + return null; + } + + boolean publicClient = "none".equalsIgnoreCase(clientAuthMethod); + if (!publicClient && (clientSecret == null || clientSecret.isBlank())) { + log.error("Skipping OIDC registration '" + id + "': no client-secret configured. Set oidc." + + id + ".client-secret, or oidc." + id + + ".client-authentication-method=none for a public client using PKCE."); + return null; + } + + try { + ClientRegistration.Builder builder = ClientRegistrations.fromIssuerLocation(issuerUri) + .registrationId(id) + .clientId(clientId) + .clientName(clientName) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .scope(Arrays.stream(scopeStr.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .toArray(String[]::new)); + + if (publicClient) { + builder.clientAuthenticationMethod(ClientAuthenticationMethod.NONE); + } else { + builder.clientSecret(clientSecret); + } + + ClientRegistration registration = builder.build(); + log.info("Registered OIDC provider: " + id + " (issuer: " + issuerUri + ")"); + return registration; + } catch (Exception e) { + log.error("Failed to configure OIDC provider '" + id + "' (issuer: " + issuerUri + + "), will retry in " + (RETRY_BACKOFF_MS / 1000) + "s", e); + return null; + } + } +} diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/core/security/RollerOidcUserService.java b/app/src/main/java/org/apache/roller/weblogger/ui/core/security/RollerOidcUserService.java new file mode 100644 index 0000000000..0784533107 --- /dev/null +++ b/app/src/main/java/org/apache/roller/weblogger/ui/core/security/RollerOidcUserService.java @@ -0,0 +1,284 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. For additional information regarding + * copyright in this work, please see the NOTICE file in the top level + * directory of this distribution. + */ +package org.apache.roller.weblogger.ui.core.security; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.roller.util.UUIDGenerator; +import org.apache.roller.weblogger.business.UserManager; +import org.apache.roller.weblogger.business.WebloggerFactory; +import org.apache.roller.weblogger.config.WebloggerConfig; +import org.apache.roller.weblogger.pojos.User; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; + +/** + * Bridges OIDC-authenticated users to Roller's user store. + * + *

The OIDC subject (formatted as {@code issuer#sub}) is matched against the + * User.openIdUrl column. Users who authenticate for the first time are + * provisioned just-in-time from their OIDC claims. Either way the returned + * OidcUser carries the Roller roles as authorities, so authorization works on + * the very first request after login. + */ +public class RollerOidcUserService implements OAuth2UserService { + + private static final Log log = LogFactory.getLog(RollerOidcUserService.class); + private final OidcUserService delegate = new OidcUserService(); + + @Override + public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { + // hiding the login buttons is not an authentication control: reject the + // whole flow server side unless the configured method allows OIDC + if (!RollerClientRegistrationRepository.oidcEnabled()) { + throw new OAuth2AuthenticationException(new OAuth2Error("oidc_not_enabled"), + "authentication.method does not allow OIDC login"); + } + return resolveUser(delegate.loadUser(userRequest)); + } + + /** + * Resolves the Roller account behind an authenticated OIDC user and returns + * a principal carrying that account's Roller roles as authorities. + */ + OidcUser resolveUser(OidcUser oidcUser) throws OAuth2AuthenticationException { + if (!WebloggerFactory.isBootstrapped()) { + throw new OAuth2AuthenticationException(new OAuth2Error("roller_not_bootstrapped"), + "Roller is not bootstrapped; cannot resolve OIDC user"); + } + + String oidcSubject = toOidcSubject(oidcUser); + + try { + UserManager umgr = WebloggerFactory.getWeblogger().getUserManager(); + User rollerUser = umgr.getUserByOpenIdUrl(oidcSubject); + + if (rollerUser == null) { + rollerUser = linkExistingUser(umgr, oidcUser, oidcSubject); + } + if (rollerUser == null) { + rollerUser = provisionUser(umgr, oidcUser, oidcSubject); + } + if (!Boolean.TRUE.equals(rollerUser.getEnabled())) { + throw new OAuth2AuthenticationException(new OAuth2Error("user_disabled"), + "Roller user is disabled: " + rollerUser.getUserName()); + } + + List authorities = new ArrayList<>(); + for (String role : umgr.getRoles(rollerUser)) { + authorities.add(new SimpleGrantedAuthority(role)); + } + return new RollerOidcUser(authorities, oidcUser.getIdToken(), oidcUser.getUserInfo(), + rollerUser.getUserName()); + + } catch (OAuth2AuthenticationException e) { + throw e; + } catch (Exception e) { + log.error("Error resolving Roller user for OIDC subject: " + oidcSubject, e); + throw new OAuth2AuthenticationException(new OAuth2Error("user_resolution_failed"), + "Could not resolve Roller user for OIDC subject: " + oidcSubject, e); + } + } + + /** + * Adopts a pre-existing Roller account whose username matches the one + * asserted by the provider, which is how database users carry over when a + * site turns on OIDC. Linking requires the provider to have verified an + * email address matching the account, so that control of an unverified + * address at the provider cannot be used to take over a Roller account. + * + * @return the linked user, or null if there is no account to adopt + */ + private User linkExistingUser(UserManager umgr, OidcUser oidcUser, String oidcSubject) throws Exception { + String username = usernameOf(oidcUser); + // include disabled accounts, otherwise a pending or deactivated user + // falls through to provisioning and dies on the username constraint + User existing = umgr.getUserByUserName(username, null); + if (existing == null) { + return null; + } + if (!Boolean.TRUE.equals(existing.getEnabled())) { + throw new OAuth2AuthenticationException(new OAuth2Error("user_disabled"), + "Roller user is disabled: " + username); + } + + String email = oidcUser.getEmail(); + boolean emailVerified = Boolean.TRUE.equals(oidcUser.getEmailVerified()) + && email != null && email.equalsIgnoreCase(existing.getEmailAddress()); + + if (!emailVerified) { + throw new OAuth2AuthenticationException(new OAuth2Error("account_link_required"), + "A Roller account named '" + username + "' already exists but is not linked to " + + oidcSubject + ". An administrator must set its federated identity, or the" + + " provider must assert a verified email address matching the account."); + } + + existing.setOpenIdUrl(oidcSubject); + umgr.saveUser(existing); + WebloggerFactory.getWeblogger().flush(); + log.info("Linked existing Roller user '" + username + "' to OIDC subject " + oidcSubject); + return existing; + } + + /** + * Creates a Roller account from the OIDC claims. The account is linked to + * the identity provider by subject, and gets a random password since it is + * never used for authentication. + */ + private User provisionUser(UserManager umgr, OidcUser oidcUser, String oidcSubject) throws Exception { + String username = usernameOf(oidcUser); + + // the identity provider decides who may sign in, so provisioning is a + // static config choice like users.ldap.autoProvision.enabled, not tied + // to the runtime form-registration toggle + if (!WebloggerConfig.getBooleanProperty("users.oidc.autoProvision.enabled")) { + throw new OAuth2AuthenticationException(new OAuth2Error("auto_provision_disabled"), + "OIDC auto-provisioning is disabled; no Roller account exists for " + oidcSubject); + } + + User user = new User(); + user.setId(UUIDGenerator.generateUUID()); + user.setUserName(username); + + String fullName = oidcUser.getFullName(); + if (fullName == null || fullName.isBlank()) { + fullName = username; + } + user.setFullName(fullName); + user.setScreenName(username); + + String email = oidcUser.getEmail(); + if (email == null || email.isBlank()) { + throw new OAuth2AuthenticationException(new OAuth2Error("missing_email"), + "OIDC provider did not supply an email address for " + username); + } + user.setEmailAddress(email); + + user.setOpenIdUrl(oidcSubject); + user.setPassword(UUIDGenerator.generateUUID()); + user.setDateCreated(new Timestamp(System.currentTimeMillis())); + user.setLocale(Locale.getDefault().toString()); + user.setTimeZone(TimeZone.getDefault().getID()); + user.setEnabled(Boolean.TRUE); + + boolean bootstrapAdmin = umgr.getUserCount() == 0 + && WebloggerConfig.getBooleanProperty("users.firstUserAdmin"); + + // grants the "editor" role, and "admin" if this is the first user + umgr.addUser(user); + + // flush before granting so the roles addUser() just created are visible + // to grantRole()'s duplicate check, which queries the database + WebloggerFactory.getWeblogger().flush(); + + Collection claimRoles = extractRoles(oidcUser); + if (claimRoles.contains("admin")) { + umgr.grantRole("admin", user); + WebloggerFactory.getWeblogger().flush(); + } else if (bootstrapAdmin + && !WebloggerConfig.getBooleanProperty("users.oidc.firstUserAdmin")) { + // users.firstUserAdmin makes the first account an administrator, + // which for auto-provisioned identities would mean whichever + // provider user reaches a fresh install first. That grant needs an + // explicit opt-in for OIDC. + umgr.revokeRole("admin", user); + WebloggerFactory.getWeblogger().flush(); + log.warn("First user '" + username + "' was auto-provisioned from OIDC and did NOT " + + "receive the admin role. To bootstrap an administrator, assert an 'admin' " + + "role claim at the provider, set users.oidc.firstUserAdmin=true, or create " + + "the account before enabling OIDC."); + } + log.info("Auto-provisioned OIDC user '" + username + "' from claims (roles: " + + claimRoles + ", subject: " + oidcSubject + ")"); + return user; + } + + private String usernameOf(OidcUser oidcUser) { + String username = oidcUser.getPreferredUsername(); + return (username == null || username.isBlank()) ? oidcUser.getSubject() : username; + } + + /** + * Reads role names from the token. Providers differ in where they put them, + * so both a flat "roles" claim and Keycloak's nested "realm_access.roles" + * are supported. + */ + @SuppressWarnings("unchecked") + private Collection extractRoles(OidcUser oidcUser) { + Object roles = oidcUser.getClaim("roles"); + if (roles instanceof Collection) { + return (Collection) roles; + } + Object realmAccess = oidcUser.getClaim("realm_access"); + if (realmAccess instanceof Map) { + Object realmRoles = ((Map) realmAccess).get("roles"); + if (realmRoles instanceof Collection) { + return (Collection) realmRoles; + } + } + return Collections.emptyList(); + } + + /** + * Formats the OIDC issuer and subject as {@code issuer#sub} for storage + * in the User.openIdUrl column. + */ + public static String toOidcSubject(OidcUser oidcUser) { + return oidcUser.getIssuer().toString() + "#" + oidcUser.getSubject(); + } + + /** + * An OidcUser named after its Roller account. Roller looks users up by + * principal name throughout (ParsedRequest, RoleAssignmentFilter, Struts + * actions), and an OidcUser's default name is the "sub" claim: an opaque + * provider ID that matches no Roller account. + */ + private static class RollerOidcUser extends DefaultOidcUser { + + private final String rollerUserName; + + RollerOidcUser(Collection authorities, + OidcIdToken idToken, OidcUserInfo userInfo, String rollerUserName) { + super(authorities, idToken, userInfo); + this.rollerUserName = rollerUserName; + } + + @Override + public String getName() { + return rollerUserName; + } + } +} diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/core/security/RollerUserDetailsService.java b/app/src/main/java/org/apache/roller/weblogger/ui/core/security/RollerUserDetailsService.java index 853cfb7429..249b8eb3ec 100644 --- a/app/src/main/java/org/apache/roller/weblogger/ui/core/security/RollerUserDetailsService.java +++ b/app/src/main/java/org/apache/roller/weblogger/ui/core/security/RollerUserDetailsService.java @@ -9,6 +9,8 @@ import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.apache.roller.weblogger.WebloggerException; +import org.apache.roller.weblogger.config.AuthMethod; +import org.apache.roller.weblogger.config.WebloggerConfig; import org.apache.roller.weblogger.business.Weblogger; import org.apache.roller.weblogger.business.WebloggerFactory; import org.apache.roller.weblogger.business.UserManager; @@ -27,6 +29,13 @@ public class RollerUserDetailsService implements UserDetailsService { */ @Override public UserDetails loadUserByUsername(String userName) { + // hiding the login form is not an authentication control: refuse + // password lookups server side when only OIDC login is configured + if (WebloggerConfig.getAuthMethod() == AuthMethod.OIDC) { + throw new UsernameNotFoundException( + "form login is disabled: authentication.method is oidc"); + } + Weblogger roller; try { roller = WebloggerFactory.getWeblogger(); diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/admin/UserEdit.java b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/admin/UserEdit.java index 70878ecf82..46338f5fe9 100644 --- a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/admin/UserEdit.java +++ b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/admin/UserEdit.java @@ -143,7 +143,7 @@ public String save() { if (!hasActionErrors()) { getBean().copyTo(user); - if (authMethod == AuthMethod.DB_OPENID) { + if (authMethod == AuthMethod.DB_OIDC) { if (StringUtils.isEmpty(user.getPassword()) && StringUtils.isEmpty(bean.getPassword()) && StringUtils.isEmpty(bean.getOpenIdUrl())) { @@ -157,8 +157,8 @@ public String save() { } // User.password does not allow null, so generate one - if (authMethod.equals(AuthMethod.OPENID) || - (authMethod.equals(AuthMethod.DB_OPENID) && !StringUtils.isEmpty(bean.getOpenIdUrl()))) { + if (authMethod.equals(AuthMethod.OIDC) || + (authMethod.equals(AuthMethod.DB_OIDC) && !StringUtils.isEmpty(bean.getOpenIdUrl()))) { String randomString = RandomStringUtils.randomAlphanumeric(255); user.resetPassword(randomString); } @@ -243,7 +243,7 @@ private void myValidate() { addError("error.add.user.badUserName"); } if ((authMethod == AuthMethod.ROLLERDB || - (authMethod == AuthMethod.DB_OPENID && StringUtils.isEmpty(getBean().getOpenIdUrl()))) + (authMethod == AuthMethod.DB_OIDC && StringUtils.isEmpty(getBean().getOpenIdUrl()))) && StringUtils.isEmpty(getBean().getPassword())) { addError("error.add.user.missingPassword"); } @@ -253,7 +253,7 @@ private void myValidate() { addError("userAdmin.error.userNotFound"); } } - if ((authMethod == AuthMethod.OPENID) && StringUtils.isEmpty(getBean().getOpenIdUrl())) { + if ((authMethod == AuthMethod.OIDC) && StringUtils.isEmpty(getBean().getOpenIdUrl())) { addError("userRegister.error.missingOpenID"); } diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/Login.java b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/Login.java index 0cd3a83860..e68558d2ee 100644 --- a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/Login.java +++ b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/Login.java @@ -18,10 +18,17 @@ package org.apache.roller.weblogger.ui.struts2.core; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + import org.apache.roller.weblogger.config.AuthMethod; import org.apache.roller.weblogger.config.WebloggerConfig; +import org.apache.roller.weblogger.ui.core.RollerContext; +import org.apache.roller.weblogger.ui.core.security.RollerClientRegistrationRepository; import org.apache.roller.weblogger.ui.struts2.util.UIAction; -import org.apache.struts2.convention.annotation.AllowedMethods; +import org.springframework.security.oauth2.client.registration.ClientRegistration; /** * Handle user logins. @@ -36,7 +43,7 @@ */ // TODO: make this work @AllowedMethods({"execute"}) public class Login extends UIAction { - + private String error = null; private AuthMethod authMethod = WebloggerConfig.getAuthMethod(); @@ -50,7 +57,7 @@ public Login() { public boolean isUserRequired() { return false; } - + // override default security, we do not require an action weblog @Override public boolean isWeblogRequired() { @@ -61,22 +68,39 @@ public String getAuthMethod() { return authMethod.name(); } + /** + * Providers to offer sign-in buttons for. Only registrations the repository + * could actually resolve are listed, so the page never advertises a provider + * whose discovery endpoint was unreachable. + */ + public List> getOidcProviders() { + List> providers = new ArrayList<>(); + RollerClientRegistrationRepository repository = RollerContext.getClientRegistrationRepository(); + if (repository == null) { + return providers; + } + for (ClientRegistration registration : repository.getRegistrations().values()) { + Map provider = new LinkedHashMap<>(); + provider.put("id", registration.getRegistrationId()); + provider.put("name", registration.getClientName()); + providers.add(provider); + } + return providers; + } + @Override public String execute() { - - // set action error message if there was login error - if(getError() != null) { - if (authMethod == AuthMethod.OPENID) { - addError("error.unmatched.openid"); - } else { - addError("error.password.mismatch"); - } + + // set action error message if there was login error; OAuth2/OIDC + // failures redirect here with error=oidc, form login with error=true + if (getError() != null) { + addError("oidc".equals(getError()) ? "error.oidc.login" : "error.password.mismatch"); } - + return SUCCESS; } - + public String getError() { return error; } @@ -84,5 +108,5 @@ public String getError() { public void setError(String error) { this.error = error; } - + } diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/Profile.java b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/Profile.java index 6f83a0ddb6..9ea0a37686 100644 --- a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/Profile.java +++ b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/Profile.java @@ -91,7 +91,7 @@ public String save() { } } - if (authMethod == AuthMethod.DB_OPENID) { + if (authMethod == AuthMethod.DB_OIDC) { if (StringUtils.isEmpty(existingUser.getPassword()) && StringUtils.isEmpty(bean.getPasswordText()) && StringUtils.isEmpty(bean.getOpenIdUrl())) { @@ -105,8 +105,8 @@ public String save() { } // User.password does not allow null, so generate one - if (authMethod.equals(AuthMethod.OPENID) || - (authMethod.equals(AuthMethod.DB_OPENID) && !StringUtils.isEmpty(bean.getOpenIdUrl()))) { + if (authMethod.equals(AuthMethod.OIDC) || + (authMethod.equals(AuthMethod.DB_OIDC) && !StringUtils.isEmpty(bean.getOpenIdUrl()))) { String randomString = RandomStringUtils.randomAlphanumeric(255); existingUser.resetPassword(randomString); } @@ -138,7 +138,7 @@ public void myValidate() { if (!StringUtils.equals(getBean().getPasswordText(), getBean().getPasswordConfirm())) { addError("userRegister.error.mismatchedPasswords"); } - if (authMethod == AuthMethod.OPENID) { + if (authMethod == AuthMethod.OIDC) { addError("userRegister.error.missingOpenID"); } } else { diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/ProfileBean.java b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/ProfileBean.java index 7e7a9703a7..b9cbf032c3 100644 --- a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/ProfileBean.java +++ b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/ProfileBean.java @@ -113,7 +113,22 @@ public String getOpenIdUrl() { public void setOpenIdUrl(String openIdUrl) { this.openIdUrl = openIdUrl; } - + + /** The issuer half of the {@code issuer#subject} federated identity. */ + public String getOidcIssuer() { + return openIdUrl == null ? null : openIdUrl.split("#", 2)[0]; + } + + /** The subject half of the {@code issuer#subject} federated identity. */ + public String getOidcSubject() { + if (openIdUrl == null) { + return null; + } + String[] parts = openIdUrl.split("#", 2); + return parts.length > 1 ? parts[1] : null; + } + + public String getPasswordText() { return passwordText; } diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/Register.java b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/Register.java index 5ddf04337e..e992248c7e 100644 --- a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/Register.java +++ b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/core/Register.java @@ -159,7 +159,7 @@ public String execute() { getBean().setScreenName(getServletRequest().getUserPrincipal().getName()); } } - + } catch (Exception ex) { log.error("Error reading SSO user data", ex); addError("error.editing.user", ex.toString()); @@ -336,7 +336,8 @@ public String activate() { public void myValidate() { // if using external auth, we don't want to error on empty password/username from HTML form. - boolean usingSSO = authMethod == AuthMethod.LDAP || authMethod == AuthMethod.CMA; + boolean usingSSO = authMethod == AuthMethod.LDAP || authMethod == AuthMethod.CMA + || authMethod == AuthMethod.OIDC; if (usingSSO) { // store an unused marker in the Roller DB for the passphrase in // the LDAP or CMA cases, as actual passwords are stored externally @@ -364,9 +365,9 @@ public void myValidate() { return; } - // User.password does not allow null, so generate one - if (getAuthMethod().equals(AuthMethod.OPENID.name()) || - (getAuthMethod().equals(AuthMethod.DB_OPENID.name()) && !StringUtils.isEmpty(getBean().getOpenIdUrl()))) { + // User.password does not allow null, so generate one for OIDC users + if (getAuthMethod().equals(AuthMethod.OIDC.name()) || + (getAuthMethod().equals(AuthMethod.DB_OIDC.name()) && !StringUtils.isEmpty(getBean().getOpenIdUrl()))) { String randomString = RandomStringUtils.randomAlphanumeric(255); getBean().setPasswordText(randomString); getBean().setPasswordConfirm(randomString); diff --git a/app/src/main/resources/ApplicationResources.properties b/app/src/main/resources/ApplicationResources.properties index 6a0abebdce..5041ab909a 100644 --- a/app/src/main/resources/ApplicationResources.properties +++ b/app/src/main/resources/ApplicationResources.properties @@ -463,6 +463,7 @@ error.upload.forbiddenFile=File {0} content-type {1} not allowed error.general=ERROR: Unexpected Exception [{0}] has been logged. error.password.mismatch=Wrong username and password combination error.unmatched.openid=Unknown or invalid OpenID URL +error.oidc.login=OIDC authentication failed error.trackback=Error sending trackback. Possible cause: incorrect \ trackback URL. {0} @@ -662,12 +663,11 @@ issued during the upgrade process: loginPage.title=Welcome to Roller loginPage.prompt=Please login -loginPage.openIdPrompt=Login with OpenID -loginPage.openIdHybridPrompt=Or with username +loginPage.oidcPrompt=Sign in with your identity provider +loginPage.dbOidcPrompt=Or sign in with username and password +loginPage.signInWith=Sign in with {0} loginPage.userName=Username loginPage.password=Password -loginPage.openID=OpenID username -loginPage.loginOpenID=Login loginPage.rememberMe=Remember Me (up to two weeks) loginPage.login=Login loginPage.reset=Reset @@ -1347,7 +1347,7 @@ userSettings.passwordConfirm=Password (Confirm) userSettings.email=Email userSettings.locale=Locale userSettings.timeZone=Timezone -userSettings.openIdUrl=OpenID URL +userSettings.openIdUrl=Federated Identity userSettings.tip.username=Usernames can''t be changed. # ----------------------------------------------------------------- Your profile (profile.jsp) @@ -1386,13 +1386,13 @@ userAdmin.newEntry=New Entry userAdmin.editEntries=Edit Entries userAdmin.manage=Manage -userAdmin.noPasswordForOpenID=Leave password field(s) blank if providing an OpenID. +userAdmin.noPasswordForOidc=Leave password field(s) blank for OIDC-authenticated users. userAdmin.tip.screenName=User''s screen name (with no HTML). userAdmin.tip.fullName=User''s full name (with no HTML). userAdmin.tip.userName=A short one-word username for the user account. \ Please limit it to simple ASCII alphanumeric characters (a-z, A-Z and 0-9), \ and do not use HTML. -userAdmin.tip.openIdUrl=Open ID identifier (in the form of a URL). +userAdmin.tip.openIdUrl=Federated identity (OIDC issuer and subject). userAdmin.tip.password=User''s password. Fill in only to change it to what you enter. userAdmin.tip.email=Valid email address needed for automated notification. userAdmin.tip.enabled=Disabled users are unable to login to Roller. @@ -1436,21 +1436,19 @@ may disable your account if he/she cannot reach you via email. userRegister.heading.authentication=How will you be authenticated? -userRegister.tip.openid.disabled=Enter a password to be used when you login \ +userRegister.tip.password.db=Enter a password to be used when you login \ and confirm that password by entering it a second time. -userRegister.tip.openid.hybrid=You can choose to login via username/password or \ -OpenID. If you choose the latter, leave \ +userRegister.tip.password.dbOidc=You can choose to login via username/password or \ +your identity provider. If you authenticated via your identity provider, leave \ the password fields blank. -userRegister.tip.openid.only=This site uses only OpenID for logins, so please \ -specify your OpenID identifier below. For more information about OpenID see \ -http://openid.net. +userRegister.tip.password.oidc=This site uses an external identity provider for logins. \ +Your account will be linked to your identity provider automatically. -userRegister.tip.password=Your password. -userSettings.tip.password=Your password. Fill-in only if you wish to change it. +userRegister.tip.password=Your password. +userSettings.tip.password=Your password. Fill-in only if you wish to change it. userRegister.tip.passwordConfirm=Confirm your password. -userRegister.tip.openIdUrl=Your OpenID identifier (in the form of a URL). userRegister.heading.locale=What are your locale and timezone settings? diff --git a/app/src/main/resources/org/apache/roller/weblogger/config/roller.properties b/app/src/main/resources/org/apache/roller/weblogger/config/roller.properties index d73e7f9ca1..221f1746a0 100644 --- a/app/src/main/resources/org/apache/roller/weblogger/config/roller.properties +++ b/app/src/main/resources/org/apache/roller/weblogger/config/roller.properties @@ -322,21 +322,14 @@ cache.salt.timeout=3600 # User management and security settings #----------------------------------------------------------------------------- -# Top-level authentication declaration for Apache Roller. Introduced in Roller 5.1, -# replaces authentication.cma.enabled, authentication.openid, and users.sso.enabled -# from earlier versions. Must be one of the following values: +# Top-level authentication declaration for Apache Roller. Introduced in Roller 5.1. +# Must be one of the following values: # db: use Roller database to store usernames and passwords # ldap: use external LDAP to authenticate (must configure Roller security.xml, # see Roller Wiki for more details) -# openid: users must use OpenID to authenticate -# db-openid: users may choose to authenticate via Roller DB or OpenID but not both. -# Trickier to implement so may not work as well as above methods, test before using. -# cma: container-managed authentication (e.g., Tomcat tomcat-users.xml file). Currently -# unusable, not implemented. -# Note that if you override this value in your roller-custom.properties file, you will also -# need to re-configure the security.xml file in the Roller WAR (under WEB-INF) to use the -# new security method -- check the comments in that latter file for instructions on -# how to do so. +# oidc: users must use an OIDC provider to authenticate (configure oidc.* properties below) +# db-oidc: users may choose to authenticate via Roller DB or OIDC provider. +# cma: container-managed authentication (e.g., Tomcat tomcat-users.xml file). authentication.method=db # Enables HTTPS for login page only @@ -345,10 +338,35 @@ securelogin.enabled=false # With this settings, all users will have HTML posts sanitized. weblogAdminsUntrusted=true -# Empty value used for passphrase in roller_user table when LDAP or CMA used; -# openid presently generates a random (long) password string instead. +# Empty value used for passphrase in roller_user table when LDAP, CMA, or OIDC used. users.passwords.externalAuthValue= +# OIDC provider configuration. To configure a provider, set all three required +# properties using the pattern oidc.{registrationId}.{property}. +# Multiple providers can be configured with different registration IDs. +# Required: client-id, client-secret, issuer-uri +# Optional: client-name (display name, defaults to registrationId), +# scope (comma-separated, defaults to openid,profile,email) +# +# Example for Keycloak: +# oidc.keycloak.client-id=roller +# oidc.keycloak.client-secret=your-client-secret +# oidc.keycloak.issuer-uri=http://localhost:9080/realms/roller +# oidc.keycloak.client-name=Keycloak + +# Create a Roller account automatically the first time someone signs in +# through an OIDC provider. Off by default, like users.ldap.autoProvision: +# without it only accounts an administrator pre-created, or that the provider +# links via a verified email matching an existing account, can sign in. +users.oidc.autoProvision.enabled=false + +# users.firstUserAdmin makes the first registered account an administrator. +# For auto-provisioned OIDC accounts that would mean whichever provider user +# reaches a fresh install first, so the grant is off unless enabled here. +# Alternatives: assert an "admin" role claim at the provider, or create the +# admin account before enabling OIDC. +users.oidc.firstUserAdmin=false + # Password security settings passwds.encryption.enabled=true passwds.encryption.algorithm=bcrypt diff --git a/app/src/main/webapp/WEB-INF/jsps/admin/UserEdit.jsp b/app/src/main/webapp/WEB-INF/jsps/admin/UserEdit.jsp index f9c65a6717..59a86fe625 100644 --- a/app/src/main/webapp/WEB-INF/jsps/admin/UserEdit.jsp +++ b/app/src/main/webapp/WEB-INF/jsps/admin/UserEdit.jsp @@ -36,8 +36,8 @@ - - + +

@@ -68,14 +68,16 @@ label="%{getText('userSettings.fullname')}" tooltip="%{getText('userAdmin.tip.fullName')}" /> - + - - + <%-- editable so an administrator can link an account to its provider + identity (issuer#subject), the fix account_link_required asks for --%> + diff --git a/app/src/main/webapp/WEB-INF/jsps/core/Login.jsp b/app/src/main/webapp/WEB-INF/jsps/core/Login.jsp index 0a3918e990..232ae2cd09 100644 --- a/app/src/main/webapp/WEB-INF/jsps/core/Login.jsp +++ b/app/src/main/webapp/WEB-INF/jsps/core/Login.jsp @@ -33,36 +33,33 @@ } %> - - - -
- + +
- +
- - - - - +
- +
"/>" onsubmit="saveUsername(this)">
- - + + @@ -102,44 +99,14 @@ \ No newline at end of file + diff --git a/app/src/main/webapp/WEB-INF/jsps/core/Profile.jsp b/app/src/main/webapp/WEB-INF/jsps/core/Profile.jsp index 2f91d013e2..4fad0f6956 100644 --- a/app/src/main/webapp/WEB-INF/jsps/core/Profile.jsp +++ b/app/src/main/webapp/WEB-INF/jsps/core/Profile.jsp @@ -19,9 +19,9 @@

- +

- +

@@ -49,7 +49,7 @@ onchange="formChanged()" onkeyup="formChanged()" name="bean.emailAddress" size="40" maxlength="40"/> - + - + + <%-- assigned by the identity provider at login, so shown read-only --%> diff --git a/app/src/main/webapp/WEB-INF/jsps/core/Register.jsp b/app/src/main/webapp/WEB-INF/jsps/core/Register.jsp index 0667e3d131..c8e8ac6174 100644 --- a/app/src/main/webapp/WEB-INF/jsps/core/Register.jsp +++ b/app/src/main/webapp/WEB-INF/jsps/core/Register.jsp @@ -69,19 +69,7 @@

- -

-
- - -

-
- - -

-
- - + - - - - -

@@ -156,22 +136,17 @@ userName = document.register['bean.userName'].value; } - if (authMethod === "ROLLERDB" || authMethod === "DB_OPENID") { + if (authMethod === "ROLLERDB" || authMethod === "DB_OIDC") { passwordText = document.register['bean.passwordText'].value; passwordConfirm = document.register['bean.passwordConfirm'].value; } - if (authMethod === "OPENID" || authMethod === "DB_OPENID") { - openIdUrl = document.register['bean.openIdUrl'].value; - } if (authMethod === "LDAP") { if (emailAddress) disabled = false; - } else if (authMethod === "ROLLERDB") { + } else if (authMethod === "ROLLERDB" || authMethod === "DB_OIDC") { if (emailAddress && userName && passwordText && passwordConfirm) disabled = false; - } else if (authMethod === "OPENID") { - if (emailAddress && openIdUrl) disabled = false; - } else if (authMethod === "DB_OPENID") { - if (emailAddress && ((passwordText && passwordConfirm) || (openIdUrl)) ) disabled = false; + } else if (authMethod === "OIDC") { + if (emailAddress && userName) disabled = false; } if (authMethod !== 'LDAP') { diff --git a/app/src/main/webapp/WEB-INF/security.xml b/app/src/main/webapp/WEB-INF/security.xml index 68a6644b23..0f416d685a 100644 --- a/app/src/main/webapp/WEB-INF/security.xml +++ b/app/src/main/webapp/WEB-INF/security.xml @@ -49,6 +49,13 @@ + + + @@ -82,27 +89,24 @@ class="org.apache.roller.weblogger.ui.core.security.RollerRememberMeAuthenticationProvider"> - + + + + + + + - - - - - - /roller-ui/register.rol - - - /roller-ui/login.rol?error=true - - - /roller-ui/login.rol?error=true - - - + + +