From 707beb7cd3efe39b57c51a6b60ddfbce4a2bf33c Mon Sep 17 00:00:00 2001 From: Felix Lo Date: Thu, 28 Sep 2023 21:50:43 +0800 Subject: [PATCH 1/3] Additional login option with API Token ID Refer to: https://github.com/openhab/openhab-core/issues/3813 This commit comprises a few changes: 1) Login screen - password field can be emptied but username must be generated API token 2) Some housekeeping to Jaas code to remove unnecessary codes. Signed-off-by: Felix Lo --- .../internal/JaasAuthenticationProvider.java | 107 +++++++++++------- .../jaas/internal/ManagedUserLoginModule.java | 9 +- 2 files changed, 71 insertions(+), 45 deletions(-) diff --git a/bundles/org.openhab.core.auth.jaas/src/main/java/org/openhab/core/auth/jaas/internal/JaasAuthenticationProvider.java b/bundles/org.openhab.core.auth.jaas/src/main/java/org/openhab/core/auth/jaas/internal/JaasAuthenticationProvider.java index 2249031139b..e31c8222ad2 100644 --- a/bundles/org.openhab.core.auth.jaas/src/main/java/org/openhab/core/auth/jaas/internal/JaasAuthenticationProvider.java +++ b/bundles/org.openhab.core.auth.jaas/src/main/java/org/openhab/core/auth/jaas/internal/JaasAuthenticationProvider.java @@ -12,18 +12,13 @@ */ package org.openhab.core.auth.jaas.internal; -import java.io.IOException; import java.security.Principal; import java.util.Collections; import java.util.Map; import java.util.Set; import javax.security.auth.Subject; -import javax.security.auth.callback.Callback; -import javax.security.auth.callback.CallbackHandler; -import javax.security.auth.callback.NameCallback; -import javax.security.auth.callback.PasswordCallback; -import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.CredentialException; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; @@ -34,6 +29,7 @@ import org.openhab.core.auth.AuthenticationProvider; import org.openhab.core.auth.Credentials; import org.openhab.core.auth.GenericUser; +import org.openhab.core.auth.UserApiTokenCredentials; import org.openhab.core.auth.UsernamePasswordCredentials; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; @@ -53,6 +49,7 @@ @Component(configurationPid = "org.openhab.jaas") public class JaasAuthenticationProvider implements AuthenticationProvider { private static final String DEFAULT_REALM = "openhab"; + static final String API_TOKEN_PREFIX = "oh."; private @Nullable String realmName; @@ -62,55 +59,77 @@ public Authentication authenticate(final Credentials credentials) throws Authent realmName = DEFAULT_REALM; } - if (!(credentials instanceof UsernamePasswordCredentials)) { + if (!((credentials instanceof UsernamePasswordCredentials) + || (credentials instanceof UserApiTokenCredentials))) { throw new AuthenticationException("Unsupported credentials passed to provider."); } - UsernamePasswordCredentials userCredentials = (UsernamePasswordCredentials) credentials; - final String name = userCredentials.getUsername(); - final char[] password = userCredentials.getPassword().toCharArray(); + Subject subject; final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); - try { - Principal userPrincipal = new GenericUser(name); - Subject subject = new Subject(true, Set.of(userPrincipal), Collections.emptySet(), Set.of(userCredentials)); - - Thread.currentThread().setContextClassLoader(ManagedUserLoginModule.class.getClassLoader()); - LoginContext loginContext = new LoginContext(realmName, subject, new CallbackHandler() { - @Override - public void handle(@NonNullByDefault({}) Callback[] callbacks) - throws IOException, UnsupportedCallbackException { - for (Callback callback : callbacks) { - if (callback instanceof PasswordCallback passwordCallback) { - passwordCallback.setPassword(password); - } else if (callback instanceof NameCallback nameCallback) { - nameCallback.setName(name); - } else { - throw new UnsupportedCallbackException(callback); - } - } - } - }, new ManagedUserLoginConfiguration()); - loginContext.login(); - - return getAuthentication(name, loginContext.getSubject()); - } catch (LoginException e) { - String message = e.getMessage(); - throw new AuthenticationException(message != null ? message : "An unexpected LoginException occurred"); - } finally { - Thread.currentThread().setContextClassLoader(contextClassLoader); + + if (credentials instanceof UserApiTokenCredentials) { + UserApiTokenCredentials userCredentials = (UserApiTokenCredentials) credentials; + final String token = userCredentials.getApiToken(); + + subject = new Subject(false, Collections.emptySet(), Collections.emptySet(), Set.of(userCredentials)); + try { + + Thread.currentThread().setContextClassLoader(ApiTokenLoginModule.class.getClassLoader()); + LoginContext loginContext = new LoginContext(realmName, subject, null, + new ApiTokenLoginConfiguration()); + loginContext.login(); + + return getAuthentication("", loginContext.getSubject()); + } catch (LoginException e) { + String message = e.getMessage(); + throw new AuthenticationException(message != null ? message : "An unexpected LoginException occurred"); + } finally { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } + + } else { + UsernamePasswordCredentials userCredentials = (UsernamePasswordCredentials) credentials; + final String name = userCredentials.getUsername(); + final char[] password = userCredentials.getPassword().toCharArray(); + + subject = new Subject(false, Collections.emptySet(), Collections.emptySet(), Set.of(userCredentials)); + try { + + Thread.currentThread().setContextClassLoader(ManagedUserLoginModule.class.getClassLoader()); + LoginContext loginContext = new LoginContext(realmName, subject, null, + new ManagedUserLoginConfiguration()); + loginContext.login(); + return getAuthentication(name, loginContext.getSubject()); + } catch (LoginException e) { + String message = e.getMessage(); + throw new AuthenticationException(message != null ? message : "An unexpected LoginException occurred"); + } finally { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } } } - private Authentication getAuthentication(String name, Subject subject) { - return new Authentication(name, getRoles(subject.getPrincipals())); + private Authentication getAuthentication(String name, Subject subject) throws CredentialException { + String username = name; + + if (subject.getPrincipals().isEmpty()) { + throw new CredentialException("Missing logged in user information"); + } + + if (username.isBlank()) { + username = ((GenericUser) subject.getPrincipals().iterator().next()).getName(); + } + return new Authentication(username, getRoles(subject.getPrincipals())); } - private String[] getRoles(Set principals) { - String[] roles = new String[principals.size()]; + private String[] getRoles(Set principals) throws CredentialException { + GenericUser user = (GenericUser) principals.iterator().next(); + String[] roles = new String[user.getRoles().size()]; + int i = 0; - for (Principal principal : principals) { - roles[i++] = principal.getName(); + for (String role : user.getRoles()) { + roles[i++] = role; } return roles; } diff --git a/bundles/org.openhab.core.auth.jaas/src/main/java/org/openhab/core/auth/jaas/internal/ManagedUserLoginModule.java b/bundles/org.openhab.core.auth.jaas/src/main/java/org/openhab/core/auth/jaas/internal/ManagedUserLoginModule.java index c285b5a0038..190d821aeb4 100644 --- a/bundles/org.openhab.core.auth.jaas/src/main/java/org/openhab/core/auth/jaas/internal/ManagedUserLoginModule.java +++ b/bundles/org.openhab.core.auth.jaas/src/main/java/org/openhab/core/auth/jaas/internal/ManagedUserLoginModule.java @@ -12,6 +12,7 @@ */ package org.openhab.core.auth.jaas.internal; +import java.security.Principal; import java.util.Map; import javax.security.auth.Subject; @@ -19,8 +20,10 @@ import javax.security.auth.login.LoginException; import javax.security.auth.spi.LoginModule; +import org.openhab.core.auth.Authentication; import org.openhab.core.auth.AuthenticationException; import org.openhab.core.auth.Credentials; +import org.openhab.core.auth.GenericUser; import org.openhab.core.auth.UserRegistry; import org.osgi.framework.BundleContext; import org.osgi.framework.FrameworkUtil; @@ -62,7 +65,11 @@ public boolean login() throws LoginException { try { Credentials credentials = (Credentials) this.subject.getPrivateCredentials().iterator().next(); - userRegistry.authenticate(credentials); + Authentication auth = userRegistry.authenticate(credentials); + Principal userPrincipal = new GenericUser(auth.getUsername(), auth.getRoles()); + if (!this.subject.getPrincipals().contains(userPrincipal)) { + this.subject.getPrincipals().add(userPrincipal); + } return true; } catch (AuthenticationException e) { throw new LoginException(e.getMessage()); From 65adfd57fa55b1cf099b6f3dd67c830c9a90535b Mon Sep 17 00:00:00 2001 From: Felix Lo Date: Thu, 28 Sep 2023 22:08:52 +0800 Subject: [PATCH 2/3] Signed-off-by: Felix Lo --- .../internal/ApiTokenLoginConfiguration.java | 33 +++++++ .../jaas/internal/ApiTokenLoginModule.java | 99 +++++++++++++++++++ .../pages/authorize.html | 2 +- .../internal/AbstractAuthPageServlet.java | 15 ++- 4 files changed, 144 insertions(+), 5 deletions(-) create mode 100644 bundles/org.openhab.core.auth.jaas/src/main/java/org/openhab/core/auth/jaas/internal/ApiTokenLoginConfiguration.java create mode 100644 bundles/org.openhab.core.auth.jaas/src/main/java/org/openhab/core/auth/jaas/internal/ApiTokenLoginModule.java diff --git a/bundles/org.openhab.core.auth.jaas/src/main/java/org/openhab/core/auth/jaas/internal/ApiTokenLoginConfiguration.java b/bundles/org.openhab.core.auth.jaas/src/main/java/org/openhab/core/auth/jaas/internal/ApiTokenLoginConfiguration.java new file mode 100644 index 00000000000..c2774bebc6d --- /dev/null +++ b/bundles/org.openhab.core.auth.jaas/src/main/java/org/openhab/core/auth/jaas/internal/ApiTokenLoginConfiguration.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.auth.jaas.internal; + +import java.util.HashMap; + +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag; +import javax.security.auth.login.Configuration; + +/** + * Describes a JAAS configuration with the {@link ApiTokenLoginModule} as a sufficient login module. + * + * @author Felix Lo - initial contribution + */ +public class ApiTokenLoginConfiguration extends Configuration { + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + return new AppConfigurationEntry[] { new AppConfigurationEntry(ApiTokenLoginModule.class.getCanonicalName(), + LoginModuleControlFlag.SUFFICIENT, new HashMap()) }; + } +} diff --git a/bundles/org.openhab.core.auth.jaas/src/main/java/org/openhab/core/auth/jaas/internal/ApiTokenLoginModule.java b/bundles/org.openhab.core.auth.jaas/src/main/java/org/openhab/core/auth/jaas/internal/ApiTokenLoginModule.java new file mode 100644 index 00000000000..f2eb2d1c899 --- /dev/null +++ b/bundles/org.openhab.core.auth.jaas/src/main/java/org/openhab/core/auth/jaas/internal/ApiTokenLoginModule.java @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.openhab.core.auth.jaas.internal; + +import java.security.Principal; +import java.util.Map; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; + +import org.openhab.core.auth.Authentication; +import org.openhab.core.auth.AuthenticationException; +import org.openhab.core.auth.Credentials; +import org.openhab.core.auth.GenericUser; +import org.openhab.core.auth.UserRegistry; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.ServiceReference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This {@link LoginModule} delegates the authentication to a {@link UserRegistry} + * + * @author Felix Lo - initial contribution + * + **/ + +public class ApiTokenLoginModule implements LoginModule { + + private final Logger logger = LoggerFactory.getLogger(ManagedUserLoginModule.class); + + private UserRegistry userRegistry; + + private Subject subject; + private CallbackHandler callbackHandler; + + @Override + public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, + Map options) { + this.subject = subject; + this.callbackHandler = callbackHandler; + } + + @Override + public boolean login() throws LoginException { + try { + // try to get the UserRegistry instance + BundleContext bundleContext = FrameworkUtil.getBundle(UserRegistry.class).getBundleContext(); + ServiceReference serviceReference = bundleContext.getServiceReference(UserRegistry.class); + + userRegistry = bundleContext.getService(serviceReference); + } catch (Exception e) { + logger.error("Cannot initialize the ApiTokenLoginModule", e); + throw new LoginException("Authorization failed"); + } + + try { + Credentials credentials = (Credentials) this.subject.getPrivateCredentials().iterator().next(); + Authentication auth = userRegistry.authenticate(credentials); + Principal userPrincipal = new GenericUser(auth.getUsername(), auth.getRoles()); + if (!this.subject.getPrincipals().contains(userPrincipal)) { + this.subject.getPrincipals().add(userPrincipal); + } + return true; + } catch (AuthenticationException e) { + throw new LoginException(e.getMessage()); + } + } + + @Override + public boolean abort() throws LoginException { + return false; + } + + @Override + public boolean commit() throws LoginException { + + return true; + } + + @Override + public boolean logout() throws LoginException { + return false; + } +} diff --git a/bundles/org.openhab.core.io.http.auth/pages/authorize.html b/bundles/org.openhab.core.io.http.auth/pages/authorize.html index 9b24fe84ee4..102139608ac 100644 --- a/bundles/org.openhab.core.io.http.auth/pages/authorize.html +++ b/bundles/org.openhab.core.io.http.auth/pages/authorize.html @@ -105,7 +105,7 @@
- +
diff --git a/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/AbstractAuthPageServlet.java b/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/AbstractAuthPageServlet.java index ff3e18aa610..ef1acf09fcf 100644 --- a/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/AbstractAuthPageServlet.java +++ b/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/AbstractAuthPageServlet.java @@ -34,6 +34,7 @@ import org.openhab.core.auth.AuthenticationException; import org.openhab.core.auth.AuthenticationProvider; import org.openhab.core.auth.User; +import org.openhab.core.auth.UserApiTokenCredentials; import org.openhab.core.auth.UserRegistry; import org.openhab.core.auth.UsernamePasswordCredentials; import org.openhab.core.i18n.LocaleProvider; @@ -124,10 +125,16 @@ protected User login(String username, String password) throws AuthenticationExce .isAfter(Instant.now().minus(Duration.ofSeconds(authenticationFailureCount)))) { throw new AuthenticationException("Too many consecutive login attempts"); } - - // Authenticate the user with the supplied credentials - UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(username, password); - Authentication auth = authProvider.authenticate(credentials); + Authentication auth; + if (!password.isEmpty()) { + // Authenticate the user with the supplied credentials + UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(username, password); + auth = authProvider.authenticate(credentials); + } else { + // try APItoken to login + UserApiTokenCredentials credentials = new UserApiTokenCredentials(username); + auth = authProvider.authenticate(credentials); + } logger.debug("Login successful: {}", auth.getUsername()); lastAuthenticationFailure = null; authenticationFailureCount = 0; From 9196980c294bdec45ba399f3ac8571d4dfbe95ef Mon Sep 17 00:00:00 2001 From: Felix Lo Date: Thu, 28 Sep 2023 22:27:22 +0800 Subject: [PATCH 3/3] Signed-off-by: Felix Lo --- .../openhab/core/auth/jaas/internal/ApiTokenLoginModule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.core.auth.jaas/src/main/java/org/openhab/core/auth/jaas/internal/ApiTokenLoginModule.java b/bundles/org.openhab.core.auth.jaas/src/main/java/org/openhab/core/auth/jaas/internal/ApiTokenLoginModule.java index f2eb2d1c899..fa0f7130562 100644 --- a/bundles/org.openhab.core.auth.jaas/src/main/java/org/openhab/core/auth/jaas/internal/ApiTokenLoginModule.java +++ b/bundles/org.openhab.core.auth.jaas/src/main/java/org/openhab/core/auth/jaas/internal/ApiTokenLoginModule.java @@ -41,7 +41,7 @@ public class ApiTokenLoginModule implements LoginModule { - private final Logger logger = LoggerFactory.getLogger(ManagedUserLoginModule.class); + private final Logger logger = LoggerFactory.getLogger(ApiTokenLoginModule.class); private UserRegistry userRegistry;