Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Auth jass] Additional login option with API Token ID #3815

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -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<String, Object>()) };
}
}
@@ -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(ApiTokenLoginModule.class);

private UserRegistry userRegistry;

private Subject subject;
private CallbackHandler callbackHandler;

@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
Map<String, ?> 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<UserRegistry> 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;
}
}
Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -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;

Expand All @@ -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<Principal> principals) {
String[] roles = new String[principals.size()];
private String[] getRoles(Set<Principal> 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;
}
Expand Down
Expand Up @@ -12,15 +12,18 @@
*/
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;
Expand Down Expand Up @@ -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());
Expand Down
Expand Up @@ -105,7 +105,7 @@
<input class="field" autocomplete="off" type="text" placeholder="{usernamePlaceholder}" name="username" required autofocus />
</div>
<div>
<input class="field" type="password" placeholder="{passwordPlaceholder}" name="password" required />
<input class="field" type="password" placeholder="{passwordPlaceholder}" name="password"/>
</div>
<div>
<input class="field" type="{newPasswordFieldType}" placeholder="{newPasswordPlaceholder}" name="new_password" />
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down