mirror of
https://github.com/danieldemus/openhab-core.git
synced 2025-01-10 13:21:53 +01:00
Implementation of a JWT-based OAuth2 flow for the admin API (#1389)
* Initial implementation of a JWT-based OAuth2 flow for the admin API Implements #1388. Signed-off-by: Yannick Schaus <github@schaus.net>
This commit is contained in:
parent
a06eb598d7
commit
fe4e276b68
@ -315,6 +315,14 @@
|
||||
<version>1.3.23_2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- jose4j -->
|
||||
<dependency>
|
||||
<groupId>org.bitbucket.b_c</groupId>
|
||||
<artifactId>jose4j</artifactId>
|
||||
<version>0.7.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
@ -896,6 +896,14 @@
|
||||
<version>0.5.8</version>
|
||||
<!-- <scope>compile</scope> -->
|
||||
</dependency>
|
||||
|
||||
<!-- jose4j -->
|
||||
<dependency>
|
||||
<groupId>org.bitbucket.b_c</groupId>
|
||||
<artifactId>jose4j</artifactId>
|
||||
<version>0.7.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
@ -14,6 +14,7 @@ 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;
|
||||
|
||||
@ -30,6 +31,7 @@ import org.openhab.core.auth.Authentication;
|
||||
import org.openhab.core.auth.AuthenticationException;
|
||||
import org.openhab.core.auth.AuthenticationProvider;
|
||||
import org.openhab.core.auth.Credentials;
|
||||
import org.openhab.core.auth.GenericUser;
|
||||
import org.openhab.core.auth.UsernamePasswordCredentials;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
@ -44,16 +46,18 @@ import org.osgi.service.component.annotations.Modified;
|
||||
*
|
||||
* @author Łukasz Dywicki - Initial contribution
|
||||
* @author Kai Kreuzer - Removed ManagedService and used DS configuration instead
|
||||
* @author Yannick Schaus - provides a configuration with the ManagedUserLoginModule as a sufficient login module
|
||||
*/
|
||||
@Component(configurationPid = "org.openhab.jaas")
|
||||
public class JaasAuthenticationProvider implements AuthenticationProvider {
|
||||
private final String DEFAULT_REALM = "openhab";
|
||||
|
||||
private String realmName;
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(final Credentials credentials) throws AuthenticationException {
|
||||
if (realmName == null) { // configuration is not yet ready or set
|
||||
return null;
|
||||
realmName = DEFAULT_REALM;
|
||||
}
|
||||
|
||||
if (!(credentials instanceof UsernamePasswordCredentials)) {
|
||||
@ -64,8 +68,14 @@ public class JaasAuthenticationProvider implements AuthenticationProvider {
|
||||
final String name = userCredentials.getUsername();
|
||||
final char[] password = userCredentials.getPassword().toCharArray();
|
||||
|
||||
final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
|
||||
try {
|
||||
LoginContext loginContext = new LoginContext(realmName, new CallbackHandler() {
|
||||
|
||||
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(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
|
||||
for (Callback callback : callbacks) {
|
||||
@ -78,12 +88,14 @@ public class JaasAuthenticationProvider implements AuthenticationProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}, new ManagedUserLoginConfiguration());
|
||||
loginContext.login();
|
||||
|
||||
return getAuthentication(name, loginContext.getSubject());
|
||||
} catch (LoginException e) {
|
||||
throw new AuthenticationException("Could not obtain authentication over login context", e);
|
||||
throw new AuthenticationException(e.getMessage());
|
||||
} finally {
|
||||
Thread.currentThread().setContextClassLoader(contextClassLoader);
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,7 +124,7 @@ public class JaasAuthenticationProvider implements AuthenticationProvider {
|
||||
@Modified
|
||||
protected void modified(Map<String, Object> properties) {
|
||||
if (properties == null) {
|
||||
realmName = null;
|
||||
realmName = DEFAULT_REALM;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -124,8 +136,8 @@ public class JaasAuthenticationProvider implements AuthenticationProvider {
|
||||
realmName = propertyValue.toString();
|
||||
}
|
||||
} else {
|
||||
// value could be unset, we should reset it value
|
||||
realmName = null;
|
||||
// value could be unset, we should reset its value
|
||||
realmName = DEFAULT_REALM;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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 ManagedUserLoginModule} as a sufficient login module.
|
||||
*
|
||||
* @author Yannick Schaus - initial contribution
|
||||
*/
|
||||
public class ManagedUserLoginConfiguration extends Configuration {
|
||||
|
||||
@Override
|
||||
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
|
||||
return new AppConfigurationEntry[] { new AppConfigurationEntry(ManagedUserLoginModule.class.getCanonicalName(),
|
||||
LoginModuleControlFlag.SUFFICIENT, new HashMap<String, Object>()) };
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.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.AuthenticationException;
|
||||
import org.openhab.core.auth.Credentials;
|
||||
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 Yannick Schaus - initial contribution
|
||||
*/
|
||||
public class ManagedUserLoginModule implements LoginModule {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ManagedUserLoginModule.class);
|
||||
|
||||
private UserRegistry userRegistry;
|
||||
|
||||
private Subject subject;
|
||||
|
||||
@Override
|
||||
public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
|
||||
Map<String, ?> options) {
|
||||
this.subject = subject;
|
||||
}
|
||||
|
||||
@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 ManagedLoginModule", e);
|
||||
throw new LoginException("Authorization failed");
|
||||
}
|
||||
|
||||
try {
|
||||
Credentials credentials = (Credentials) this.subject.getPrivateCredentials().iterator().next();
|
||||
userRegistry.authenticate(credentials);
|
||||
return true;
|
||||
} catch (AuthenticationException e) {
|
||||
throw new LoginException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean commit() throws LoginException {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean abort() throws LoginException {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean logout() throws LoginException {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -22,6 +22,7 @@ import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.annotation.security.RolesAllowed;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
@ -37,6 +38,7 @@ import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import org.openhab.core.auth.Role;
|
||||
import org.openhab.core.automation.Action;
|
||||
import org.openhab.core.automation.Condition;
|
||||
import org.openhab.core.automation.Module;
|
||||
@ -84,6 +86,7 @@ import io.swagger.annotations.ResponseHeader;
|
||||
@Path("rules")
|
||||
@Api("rules")
|
||||
@Component
|
||||
@RolesAllowed({ Role.ADMIN })
|
||||
public class RuleResource implements RESTResource {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(RuleResource.class);
|
||||
@ -272,6 +275,7 @@ public class RuleResource implements RESTResource {
|
||||
}
|
||||
|
||||
@POST
|
||||
@RolesAllowed({ Role.USER, Role.ADMIN })
|
||||
@Path("/{ruleUID}/runnow")
|
||||
@Consumes(MediaType.TEXT_PLAIN)
|
||||
@ApiOperation(value = "Executes actions of the rule.")
|
||||
|
77
bundles/org.openhab.core.io.http.auth/pages/authorize.html
Normal file
77
bundles/org.openhab.core.io.http.auth/pages/authorize.html
Normal file
File diff suppressed because one or more lines are too long
@ -26,5 +26,29 @@
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>build-helper-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>add-resource</goal>
|
||||
</goals>
|
||||
<phase>generate-resources</phase>
|
||||
<configuration>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>pages</directory>
|
||||
<targetPath>pages</targetPath>
|
||||
</resource>
|
||||
</resources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
@ -0,0 +1,345 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.io.http.auth.internal;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URL;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
|
||||
import org.apache.commons.httpclient.HttpStatus;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.auth.Authentication;
|
||||
import org.openhab.core.auth.AuthenticationException;
|
||||
import org.openhab.core.auth.AuthenticationProvider;
|
||||
import org.openhab.core.auth.ManagedUser;
|
||||
import org.openhab.core.auth.PendingToken;
|
||||
import org.openhab.core.auth.Role;
|
||||
import org.openhab.core.auth.User;
|
||||
import org.openhab.core.auth.UserRegistry;
|
||||
import org.openhab.core.auth.UsernamePasswordCredentials;
|
||||
import org.osgi.framework.BundleContext;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Deactivate;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.osgi.service.http.HttpService;
|
||||
import org.osgi.service.http.NamespaceException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* A servlet serving the authorization page part of the OAuth2 authorization code flow.
|
||||
*
|
||||
* The page can register the first administrator account when there are no users yet in the {@link UserRegistry}, and
|
||||
* authenticates the user otherwise. It also presents the scope that is about to be granted to the client, so the user
|
||||
* can review what kind of access is being authorized. If successful, it redirects the client back to the URI which was
|
||||
* specified and creates an authorization code stored for later in the user's profile.
|
||||
*
|
||||
* @author Yannick Schaus - initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(immediate = true)
|
||||
public class AuthorizePageServlet extends HttpServlet {
|
||||
|
||||
private static final long serialVersionUID = 5340598701104679843L;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AuthorizePageServlet.class);
|
||||
|
||||
private HashMap<String, Instant> csrfTokens = new HashMap<>();
|
||||
|
||||
private HttpService httpService;
|
||||
private UserRegistry userRegistry;
|
||||
private AuthenticationProvider authProvider;
|
||||
@Nullable
|
||||
private Instant lastAuthenticationFailure;
|
||||
private int authenticationFailureCount = 0;
|
||||
|
||||
private String pageTemplate;
|
||||
|
||||
@Activate
|
||||
public AuthorizePageServlet(BundleContext bundleContext, @Reference HttpService httpService,
|
||||
@Reference UserRegistry userRegistry, @Reference AuthenticationProvider authProvider) {
|
||||
this.httpService = httpService;
|
||||
this.userRegistry = userRegistry;
|
||||
this.authProvider = authProvider;
|
||||
|
||||
pageTemplate = "";
|
||||
try {
|
||||
URL resource = bundleContext.getBundle().getResource("pages/authorize.html");
|
||||
if (resource != null) {
|
||||
try {
|
||||
pageTemplate = IOUtils.toString(resource.openStream());
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
httpService.registerServlet("/auth", this, null, null);
|
||||
}
|
||||
} catch (NamespaceException | ServletException e) {
|
||||
logger.error("Error during authorization page registration: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp)
|
||||
throws ServletException, IOException {
|
||||
if (req != null && resp != null) {
|
||||
Map<String, String[]> params = req.getParameterMap();
|
||||
|
||||
try {
|
||||
String message = "";
|
||||
String scope = (params.containsKey("scope")) ? params.get("scope")[0] : "";
|
||||
String clientId = (params.containsKey("client_id")) ? params.get("client_id")[0] : "";
|
||||
|
||||
// Basic sanity check
|
||||
if (scope.contains("<") || clientId.contains("<")) {
|
||||
throw new IllegalArgumentException("invalid_request");
|
||||
}
|
||||
|
||||
// TODO: i18n
|
||||
if (isSignupMode()) {
|
||||
message = "Create a first administrator account to continue.";
|
||||
} else {
|
||||
message = String.format("Sign in to grant <b>%s</b> access to <b>%s</b>:", scope, clientId);
|
||||
}
|
||||
resp.setContentType("text/html;charset=UTF-8");
|
||||
resp.getWriter().append(getPageBody(params, message));
|
||||
resp.getWriter().close();
|
||||
} catch (Exception e) {
|
||||
resp.setContentType("text/plain;charset=UTF-8");
|
||||
resp.getWriter().append(e.getMessage());
|
||||
resp.getWriter().close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPost(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp)
|
||||
throws ServletException, IOException {
|
||||
if (req != null && resp != null) {
|
||||
Map<String, String[]> params = req.getParameterMap();
|
||||
try {
|
||||
if (!params.containsKey(("username"))) {
|
||||
throw new AuthenticationException("no username");
|
||||
}
|
||||
if (!params.containsKey(("password"))) {
|
||||
throw new AuthenticationException("no password");
|
||||
}
|
||||
if (!params.containsKey("csrf_token") || !csrfTokens.containsKey(params.get("csrf_token")[0])) {
|
||||
throw new AuthenticationException("CSRF check failed");
|
||||
}
|
||||
if (!params.containsKey(("redirect_uri"))) {
|
||||
throw new IllegalArgumentException("invalid_request");
|
||||
}
|
||||
if (!params.containsKey(("response_type"))) {
|
||||
throw new IllegalArgumentException("unsupported_response_type");
|
||||
}
|
||||
if (!params.containsKey(("client_id"))) {
|
||||
throw new IllegalArgumentException("unauthorized_client");
|
||||
}
|
||||
if (!params.containsKey(("scope"))) {
|
||||
throw new IllegalArgumentException("invalid_scope");
|
||||
}
|
||||
|
||||
csrfTokens.remove(params.get("csrf_token")[0]);
|
||||
|
||||
String baseRedirectUri = params.get("redirect_uri")[0];
|
||||
String responseType = params.get("response_type")[0];
|
||||
String clientId = params.get("redirect_uri")[0];
|
||||
String scope = params.get("scope")[0];
|
||||
|
||||
if (!("code".equals(responseType))) {
|
||||
throw new AuthenticationException("unsupported_response_type");
|
||||
}
|
||||
|
||||
if (!clientId.equals(baseRedirectUri)) {
|
||||
throw new IllegalArgumentException("unauthorized_client");
|
||||
}
|
||||
|
||||
String username = params.get("username")[0];
|
||||
String password = params.get("password")[0];
|
||||
|
||||
User user;
|
||||
if (isSignupMode()) {
|
||||
// Create a first administrator account with the supplied credentials
|
||||
|
||||
// first verify the password confirmation and bail out if necessary
|
||||
if (!params.containsKey("password_repeat") || !password.equals(params.get("password_repeat")[0])) {
|
||||
resp.setContentType("text/html;charset=UTF-8");
|
||||
// TODO: i18n
|
||||
resp.getWriter().append(getPageBody(params, "Passwords don't match, please try again."));
|
||||
resp.getWriter().close();
|
||||
return;
|
||||
}
|
||||
|
||||
user = userRegistry.register(username, password, Set.of(Role.ADMIN));
|
||||
logger.info("First user account created: {}", username);
|
||||
} else {
|
||||
// Enforce a dynamic cooldown period after a failed authentication attempt: the number of
|
||||
// consecutive failures in seconds
|
||||
if (lastAuthenticationFailure != null && lastAuthenticationFailure
|
||||
.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);
|
||||
logger.debug("Login successful: {}", auth.getUsername());
|
||||
lastAuthenticationFailure = null;
|
||||
authenticationFailureCount = 0;
|
||||
user = userRegistry.get(auth.getUsername());
|
||||
}
|
||||
|
||||
String authorizationCode = UUID.randomUUID().toString().replace("-", "");
|
||||
|
||||
if (user instanceof ManagedUser) {
|
||||
String codeChallenge = (params.containsKey("code_challenge")) ? params.get("code_challenge")[0]
|
||||
: null;
|
||||
String codeChallengeMethod = (params.containsKey("code_challenge_method"))
|
||||
? params.get("code_challenge_method")[0]
|
||||
: null;
|
||||
ManagedUser managedUser = (ManagedUser) user;
|
||||
PendingToken pendingToken = new PendingToken(authorizationCode, clientId, baseRedirectUri, scope,
|
||||
codeChallenge, codeChallengeMethod);
|
||||
managedUser.setPendingToken(pendingToken);
|
||||
userRegistry.update(managedUser);
|
||||
}
|
||||
|
||||
String state = params.containsKey("state") ? params.get("state")[0] : null;
|
||||
resp.addHeader(HttpHeaders.LOCATION, getRedirectUri(baseRedirectUri, authorizationCode, null, state));
|
||||
resp.setStatus(HttpStatus.SC_MOVED_TEMPORARILY);
|
||||
} catch (AuthenticationException e) {
|
||||
lastAuthenticationFailure = Instant.now();
|
||||
authenticationFailureCount += 1;
|
||||
resp.setContentType("text/html;charset=UTF-8");
|
||||
logger.warn("Authentication failed: {}", e.getMessage());
|
||||
resp.getWriter().append(getPageBody(params, "Please try again.")); // TODO: i18n
|
||||
resp.getWriter().close();
|
||||
} catch (IllegalArgumentException e) {
|
||||
@Nullable
|
||||
String baseRedirectUri = params.containsKey("redirect_uri") ? params.get("redirect_uri")[0] : null;
|
||||
@Nullable
|
||||
String state = params.containsKey("state") ? params.get("state")[0] : null;
|
||||
if (baseRedirectUri != null) {
|
||||
resp.addHeader(HttpHeaders.LOCATION, getRedirectUri(baseRedirectUri, null, e.getMessage(), state));
|
||||
resp.setStatus(HttpStatus.SC_MOVED_TEMPORARILY);
|
||||
} else {
|
||||
resp.setContentType("text/plain;charset=UTF-8");
|
||||
resp.getWriter().append(e.getMessage());
|
||||
resp.getWriter().close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getPageBody(Map<String, String[]> params, String message) {
|
||||
String responseBody = pageTemplate.replace("{form_fields}", getFormFields(params));
|
||||
String repeatPasswordFieldType = (isSignupMode()) ? "password" : "hidden";
|
||||
String buttonLabel = (isSignupMode()) ? "Create Account" : "Sign In"; // TODO: i18n
|
||||
responseBody = responseBody.replace("{message}", message);
|
||||
responseBody = responseBody.replace("{repeatPasswordFieldType}", repeatPasswordFieldType);
|
||||
responseBody = responseBody.replace("{buttonLabel}", buttonLabel);
|
||||
return responseBody;
|
||||
}
|
||||
|
||||
private String getFormFields(Map<String, String[]> params) {
|
||||
String hiddenFormFields = "";
|
||||
|
||||
if (!params.containsKey(("redirect_uri"))) {
|
||||
throw new IllegalArgumentException("invalid_request");
|
||||
}
|
||||
if (!params.containsKey(("response_type"))) {
|
||||
throw new IllegalArgumentException("unsupported_response_type");
|
||||
}
|
||||
if (!params.containsKey(("client_id"))) {
|
||||
throw new IllegalArgumentException("unauthorized_client");
|
||||
}
|
||||
if (!params.containsKey(("scope"))) {
|
||||
throw new IllegalArgumentException("invalid_scope");
|
||||
}
|
||||
String csrfToken = addCsrfToken();
|
||||
String redirectUri = params.get("redirect_uri")[0];
|
||||
String responseType = params.get("response_type")[0];
|
||||
String clientId = params.get("client_id")[0];
|
||||
String scope = params.get("scope")[0];
|
||||
String state = (params.containsKey("state")) ? params.get("state")[0] : null;
|
||||
String codeChallenge = (params.containsKey("code_challenge")) ? params.get("code_challenge")[0] : null;
|
||||
String codeChallengeMethod = (params.containsKey("code_challenge_method"))
|
||||
? params.get("code_challenge_method")[0]
|
||||
: null;
|
||||
hiddenFormFields += "<input type=\"hidden\" name=\"csrf_token\" value=\"" + csrfToken + "\">";
|
||||
hiddenFormFields += "<input type=\"hidden\" name=\"redirect_uri\" value=\"" + redirectUri + "\">";
|
||||
hiddenFormFields += "<input type=\"hidden\" name=\"response_type\" value=\"" + responseType + "\">";
|
||||
hiddenFormFields += "<input type=\"hidden\" name=\"client_id\" value=\"" + clientId + "\">";
|
||||
hiddenFormFields += "<input type=\"hidden\" name=\"scope\" value=\"" + scope + "\">";
|
||||
if (state != null) {
|
||||
hiddenFormFields += "<input type=\"hidden\" name=\"state\" value=\"" + state + "\">";
|
||||
}
|
||||
if (codeChallenge != null && codeChallengeMethod != null) {
|
||||
hiddenFormFields += "<input type=\"hidden\" name=\"code_challenge\" value=\"" + codeChallenge + "\">";
|
||||
hiddenFormFields += "<input type=\"hidden\" name=\"code_challenge_method\" value=\"" + codeChallengeMethod
|
||||
+ "\">";
|
||||
}
|
||||
|
||||
return hiddenFormFields;
|
||||
}
|
||||
|
||||
private String getRedirectUri(String baseRedirectUri, @Nullable String authorizationCode, @Nullable String error,
|
||||
@Nullable String state) {
|
||||
String redirectUri = baseRedirectUri;
|
||||
|
||||
if (authorizationCode != null) {
|
||||
redirectUri += "?code=" + authorizationCode;
|
||||
} else if (error != null) {
|
||||
redirectUri += "?error=" + error;
|
||||
}
|
||||
|
||||
if (state != null) {
|
||||
redirectUri += "&state=" + state;
|
||||
}
|
||||
|
||||
return redirectUri;
|
||||
}
|
||||
|
||||
private String addCsrfToken() {
|
||||
String csrfToken = UUID.randomUUID().toString().replace("-", "");
|
||||
csrfTokens.put(csrfToken, Instant.now());
|
||||
// remove old tokens (created earlier than 10 minutes ago) - this gives users a 10-minute window to sign in
|
||||
csrfTokens.entrySet().removeIf(e -> e.getValue().isBefore(Instant.now().minus(Duration.ofMinutes(10))));
|
||||
return csrfToken;
|
||||
}
|
||||
|
||||
private boolean isSignupMode() {
|
||||
return userRegistry.getAll().isEmpty();
|
||||
}
|
||||
|
||||
@Deactivate
|
||||
public void deactivate() {
|
||||
httpService.unregister("/auth");
|
||||
}
|
||||
}
|
2
bundles/org.openhab.core.io.rest.auth/bnd.bnd
Normal file
2
bundles/org.openhab.core.io.rest.auth/bnd.bnd
Normal file
@ -0,0 +1,2 @@
|
||||
Bundle-SymbolicName: ${project.artifactId}
|
||||
Bundle-Activator: org.openhab.core.io.rest.auth.internal.Activator
|
@ -14,4 +14,22 @@
|
||||
|
||||
<name>openHAB Core :: Bundles :: Authentication Support for the REST Interface</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.openhab.core.bundles</groupId>
|
||||
<artifactId>org.openhab.core.auth.jaas</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.core.bundles</groupId>
|
||||
<artifactId>org.openhab.core.io.rest</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.core.bundles</groupId>
|
||||
<artifactId>org.openhab.core.config.core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.io.rest.auth.internal;
|
||||
|
||||
import org.osgi.framework.BundleActivator;
|
||||
import org.osgi.framework.BundleContext;
|
||||
import org.osgi.framework.ServiceRegistration;
|
||||
|
||||
/**
|
||||
* Registers dynamic features contained in this bundle.
|
||||
*
|
||||
* @author Yannick Schaus - initial contribution
|
||||
*/
|
||||
public class Activator implements BundleActivator {
|
||||
private static Activator instance;
|
||||
|
||||
private ServiceRegistration rolesAllowedDynamicFeatureRegistration;
|
||||
|
||||
public static Activator getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(BundleContext context) throws Exception {
|
||||
rolesAllowedDynamicFeatureRegistration = context.registerService(RolesAllowedDynamicFeatureImpl.class.getName(),
|
||||
new RolesAllowedDynamicFeatureImpl(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop(BundleContext context) throws Exception {
|
||||
instance = null;
|
||||
if (rolesAllowedDynamicFeatureRegistration != null) {
|
||||
rolesAllowedDynamicFeatureRegistration.unregister();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.io.rest.auth.internal;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.annotation.Priority;
|
||||
import javax.security.sasl.AuthenticationException;
|
||||
import javax.ws.rs.NotAuthorizedException;
|
||||
import javax.ws.rs.Priorities;
|
||||
import javax.ws.rs.container.ContainerRequestContext;
|
||||
import javax.ws.rs.container.ContainerRequestFilter;
|
||||
import javax.ws.rs.container.PreMatching;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.SecurityContext;
|
||||
import javax.ws.rs.ext.Provider;
|
||||
|
||||
import org.openhab.core.auth.Authentication;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* This filter is responsible for parsing a token provided with a request, and hydrating a {@link SecurityContext} from
|
||||
* the claims contained in the token.
|
||||
*
|
||||
* @author Yannick Schaus - initial contribution
|
||||
*/
|
||||
@PreMatching
|
||||
@Priority(Priorities.AUTHENTICATION)
|
||||
@Provider
|
||||
@Component(immediate = true, service = AuthFilter.class)
|
||||
public class AuthFilter implements ContainerRequestFilter {
|
||||
private static final String COOKIE_AUTH_HEADER = "X-OPENHAB-AUTH-HEADER";
|
||||
private static final String ALT_AUTH_HEADER = "X-OPENHAB-TOKEN";
|
||||
|
||||
@Reference
|
||||
private JwtHelper jwtHelper;
|
||||
|
||||
@Override
|
||||
public void filter(ContainerRequestContext requestContext) throws IOException {
|
||||
try {
|
||||
String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
|
||||
|
||||
if (authHeader != null) {
|
||||
String[] authParts = authHeader.split(" ");
|
||||
if (authParts.length == 2) {
|
||||
if ("Bearer".equals(authParts[0])) {
|
||||
Authentication auth = jwtHelper.verifyAndParseJwtAccessToken(authParts[1]);
|
||||
requestContext.setSecurityContext(new JwtSecurityContext(auth));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (requestContext.getCookies().containsKey(COOKIE_AUTH_HEADER)) {
|
||||
String altTokenHeader = requestContext.getHeaderString(ALT_AUTH_HEADER);
|
||||
if (altTokenHeader != null) {
|
||||
Authentication auth = jwtHelper.verifyAndParseJwtAccessToken(altTokenHeader);
|
||||
requestContext.setSecurityContext(new JwtSecurityContext(auth));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// support the api_key query parameter of the Swagger UI
|
||||
if (requestContext.getUriInfo().getRequestUri().toString().contains("api_key=")) {
|
||||
String apiKey = requestContext.getUriInfo().getQueryParameters(true).getFirst("api_key");
|
||||
if (apiKey != null) {
|
||||
Authentication auth = jwtHelper.verifyAndParseJwtAccessToken(apiKey);
|
||||
requestContext.setSecurityContext(new JwtSecurityContext(auth));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (AuthenticationException e) {
|
||||
throw new NotAuthorizedException("Invalid token");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.io.rest.auth.internal;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.security.sasl.AuthenticationException;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.jose4j.jwa.AlgorithmConstraints.ConstraintType;
|
||||
import org.jose4j.jwk.JsonWebKey;
|
||||
import org.jose4j.jwk.JsonWebKey.OutputControlLevel;
|
||||
import org.jose4j.jwk.RsaJsonWebKey;
|
||||
import org.jose4j.jwk.RsaJwkGenerator;
|
||||
import org.jose4j.jws.AlgorithmIdentifiers;
|
||||
import org.jose4j.jws.JsonWebSignature;
|
||||
import org.jose4j.jwt.JwtClaims;
|
||||
import org.jose4j.jwt.consumer.JwtConsumer;
|
||||
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
|
||||
import org.jose4j.lang.JoseException;
|
||||
import org.openhab.core.auth.Authentication;
|
||||
import org.openhab.core.auth.User;
|
||||
import org.openhab.core.config.core.ConfigConstants;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This class helps with JWT tokens' building, signing, verifying and parsing.
|
||||
*
|
||||
* @author Yannick Schaus - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(immediate = true, service = JwtHelper.class)
|
||||
public class JwtHelper {
|
||||
private final Logger logger = LoggerFactory.getLogger(JwtHelper.class);
|
||||
|
||||
private static final String KEY_FILE_PATH = ConfigConstants.getUserDataFolder() + File.separator + "secrets"
|
||||
+ File.separator + "rsa_json_web_key.json";
|
||||
|
||||
private static final String ISSUER_NAME = "openhab";
|
||||
private static final String AUDIENCE = "openhab";
|
||||
|
||||
private RsaJsonWebKey jwtWebKey;
|
||||
|
||||
public JwtHelper() {
|
||||
try {
|
||||
jwtWebKey = loadOrGenerateKey();
|
||||
} catch (Exception e) {
|
||||
logger.error("Error while initializing the JWT helper", e);
|
||||
throw new RuntimeException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private RsaJsonWebKey generateNewKey() throws JoseException, FileNotFoundException, IOException {
|
||||
RsaJsonWebKey newKey = RsaJwkGenerator.generateJwk(2048);
|
||||
|
||||
File file = new File(KEY_FILE_PATH);
|
||||
file.getParentFile().mkdirs();
|
||||
|
||||
String keyJson = newKey.toJson(OutputControlLevel.INCLUDE_PRIVATE);
|
||||
|
||||
IOUtils.write(keyJson, new FileOutputStream(file));
|
||||
return newKey;
|
||||
}
|
||||
|
||||
private RsaJsonWebKey loadOrGenerateKey() throws FileNotFoundException, JoseException, IOException {
|
||||
try {
|
||||
List<String> lines = IOUtils.readLines(new FileInputStream(KEY_FILE_PATH));
|
||||
return (RsaJsonWebKey) JsonWebKey.Factory.newJwk(lines.get(0));
|
||||
} catch (IOException | JoseException e) {
|
||||
RsaJsonWebKey key = generateNewKey();
|
||||
logger.debug("Created JWT signature key in {}", KEY_FILE_PATH);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new access token.
|
||||
*
|
||||
* @param user the user (subject) to build the token, it will also add the roles as claims
|
||||
* @param clientId the client ID the token is for
|
||||
* @param scope the scope the token is valid for
|
||||
* @param tokenLifetime the lifetime of the token in minutes before it expires
|
||||
*
|
||||
* @return a base64-encoded signed JWT token to be passed as a bearer token in API requests
|
||||
*/
|
||||
public String getJwtAccessToken(User user, String clientId, String scope, int tokenLifetime) {
|
||||
try {
|
||||
JwtClaims jwtClaims = new JwtClaims();
|
||||
jwtClaims.setIssuer(ISSUER_NAME);
|
||||
jwtClaims.setAudience(AUDIENCE);
|
||||
jwtClaims.setExpirationTimeMinutesInTheFuture(tokenLifetime);
|
||||
jwtClaims.setGeneratedJwtId();
|
||||
jwtClaims.setIssuedAtToNow();
|
||||
jwtClaims.setNotBeforeMinutesInThePast(2);
|
||||
jwtClaims.setSubject(user.getName());
|
||||
jwtClaims.setClaim("client_id", clientId);
|
||||
jwtClaims.setClaim("scope", scope);
|
||||
jwtClaims.setStringListClaim("role",
|
||||
new ArrayList<>((user.getRoles() != null) ? user.getRoles() : Collections.emptySet()));
|
||||
|
||||
JsonWebSignature jws = new JsonWebSignature();
|
||||
jws.setPayload(jwtClaims.toJson());
|
||||
jws.setKey(jwtWebKey.getPrivateKey());
|
||||
jws.setKeyIdHeaderValue(jwtWebKey.getKeyId());
|
||||
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
|
||||
String jwt = jws.getCompactSerialization();
|
||||
|
||||
return jwt;
|
||||
} catch (Exception e) {
|
||||
logger.error("Error while writing JWT token", e);
|
||||
throw new RuntimeException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs verifications on a JWT token, then parses it into a {@link AuthenticationException} instance
|
||||
*
|
||||
* @param jwt the base64-encoded JWT token from the request
|
||||
* @return the {@link Authentication} derived from the information in the token
|
||||
* @throws AuthenticationException
|
||||
*/
|
||||
public Authentication verifyAndParseJwtAccessToken(String jwt) throws AuthenticationException {
|
||||
JwtConsumer jwtConsumer = new JwtConsumerBuilder().setRequireExpirationTime().setAllowedClockSkewInSeconds(30)
|
||||
.setRequireSubject().setExpectedIssuer(ISSUER_NAME).setExpectedAudience(AUDIENCE)
|
||||
.setVerificationKey(jwtWebKey.getKey())
|
||||
.setJwsAlgorithmConstraints(ConstraintType.WHITELIST, AlgorithmIdentifiers.RSA_USING_SHA256).build();
|
||||
|
||||
try {
|
||||
JwtClaims jwtClaims = jwtConsumer.processToClaims(jwt);
|
||||
String username = jwtClaims.getSubject();
|
||||
List<String> roles = jwtClaims.getStringListClaimValue("role");
|
||||
Authentication auth = new Authentication(username, roles.toArray(new String[roles.size()]));
|
||||
return auth;
|
||||
} catch (Exception e) {
|
||||
logger.error("Error while processing JWT token", e);
|
||||
throw new AuthenticationException(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.io.rest.auth.internal;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
import javax.ws.rs.core.SecurityContext;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.auth.Authentication;
|
||||
import org.openhab.core.auth.GenericUser;
|
||||
|
||||
/**
|
||||
* This {@link SecurityContext} contains information about a user, roles and authorizations granted to a client as
|
||||
* parsed from the contents of a JSON Web Token
|
||||
*
|
||||
* @author Yannick Schaus - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class JwtSecurityContext implements SecurityContext {
|
||||
|
||||
Authentication authentication;
|
||||
|
||||
public JwtSecurityContext(Authentication authentication) {
|
||||
this.authentication = authentication;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Principal getUserPrincipal() {
|
||||
return new GenericUser(authentication.getUsername());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUserInRole(@Nullable String role) {
|
||||
return authentication.getRoles().contains(role);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSecure() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthenticationScheme() {
|
||||
return "JWT";
|
||||
}
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.io.rest.auth.internal;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.annotation.Priority;
|
||||
import javax.annotation.security.DenyAll;
|
||||
import javax.annotation.security.PermitAll;
|
||||
import javax.annotation.security.RolesAllowed;
|
||||
import javax.ws.rs.ForbiddenException;
|
||||
import javax.ws.rs.NotAuthorizedException;
|
||||
import javax.ws.rs.Priorities;
|
||||
import javax.ws.rs.container.ContainerRequestContext;
|
||||
import javax.ws.rs.container.ContainerRequestFilter;
|
||||
import javax.ws.rs.container.DynamicFeature;
|
||||
import javax.ws.rs.container.ResourceInfo;
|
||||
import javax.ws.rs.core.FeatureContext;
|
||||
import javax.ws.rs.ext.Provider;
|
||||
|
||||
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature;
|
||||
import org.glassfish.jersey.server.model.AnnotatedMethod;
|
||||
import org.openhab.core.auth.Role;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* A {@link DynamicFeature} supporting the {@code javax.annotation.security.RolesAllowed},
|
||||
* {@code javax.annotation.security.PermitAll} and {@code javax.annotation.security.DenyAll}
|
||||
* on resource methods and sub-resource methods.
|
||||
*
|
||||
* Ported from {@link RolesAllowedDynamicFeature} with modifications.
|
||||
*
|
||||
* @author Paul Sandoz - initial contribution
|
||||
* @author Martin Matula - initial contribution
|
||||
* @author Yannick Schaus - port to openHAB with modifications
|
||||
*/
|
||||
@Provider
|
||||
public class RolesAllowedDynamicFeatureImpl implements DynamicFeature {
|
||||
private final Logger logger = LoggerFactory.getLogger(RolesAllowedDynamicFeatureImpl.class);
|
||||
|
||||
@Override
|
||||
public void configure(ResourceInfo resourceInfo, FeatureContext configuration) {
|
||||
final AnnotatedMethod am = new AnnotatedMethod(resourceInfo.getResourceMethod());
|
||||
try {
|
||||
// DenyAll on the method take precedence over RolesAllowed and PermitAll
|
||||
if (am.isAnnotationPresent(DenyAll.class)) {
|
||||
configuration.register(new RolesAllowedRequestFilter());
|
||||
return;
|
||||
}
|
||||
|
||||
// RolesAllowed on the method takes precedence over PermitAll
|
||||
Optional<Annotation> ra = Arrays.stream(am.getAnnotations())
|
||||
.filter(a -> a.annotationType().getName().equals(RolesAllowed.class.getName())).findFirst();
|
||||
if (ra.isPresent()) {
|
||||
configuration.register(new RolesAllowedRequestFilter(((RolesAllowed) ra.get()).value()));
|
||||
return;
|
||||
}
|
||||
|
||||
// PermitAll takes precedence over RolesAllowed on the class
|
||||
if (am.isAnnotationPresent(PermitAll.class)) {
|
||||
// Do nothing.
|
||||
return;
|
||||
}
|
||||
|
||||
// DenyAll can't be attached to classes
|
||||
|
||||
// RolesAllowed on the class takes precedence over PermitAll
|
||||
ra = Arrays.stream(resourceInfo.getResourceClass().getAnnotations())
|
||||
.filter(a -> a.annotationType().getName().equals(RolesAllowed.class.getName())).findFirst();
|
||||
if (ra.isPresent()) {
|
||||
configuration.register(new RolesAllowedRequestFilter(((RolesAllowed) ra.get()).value()));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Error while configuring the roles", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Priority(Priorities.AUTHORIZATION) // authorization filter - should go after any authentication filters
|
||||
private static class RolesAllowedRequestFilter implements ContainerRequestFilter {
|
||||
|
||||
private final boolean denyAll;
|
||||
private final String[] rolesAllowed;
|
||||
|
||||
RolesAllowedRequestFilter() {
|
||||
this.denyAll = true;
|
||||
this.rolesAllowed = null;
|
||||
}
|
||||
|
||||
RolesAllowedRequestFilter(final String[] rolesAllowed) {
|
||||
this.denyAll = false;
|
||||
this.rolesAllowed = (rolesAllowed != null) ? rolesAllowed : new String[] {};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void filter(final ContainerRequestContext requestContext) throws IOException {
|
||||
if (!denyAll) {
|
||||
// TODO: temporarily, until the complete authorization story is implemented, we consider operations
|
||||
// allowed for user roles to be permitted unrestricted (even to unauthenticated users)
|
||||
if (Arrays.asList(rolesAllowed).contains(Role.USER)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rolesAllowed.length > 0 && !isAuthenticated(requestContext)) {
|
||||
throw new NotAuthorizedException("User is not authenticated");
|
||||
}
|
||||
|
||||
for (final String role : rolesAllowed) {
|
||||
if (requestContext.getSecurityContext().isUserInRole(role)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new ForbiddenException("User is authenticated but doesn't have access to this resource");
|
||||
}
|
||||
|
||||
private static boolean isAuthenticated(final ContainerRequestContext requestContext) {
|
||||
return requestContext.getSecurityContext().getUserPrincipal() != null;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.io.rest.auth.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.auth.AuthenticationException;
|
||||
|
||||
/**
|
||||
* An exception when the token endpoint encounters an error and must return an error response, according to RFC 6749
|
||||
* Section 5.2.
|
||||
*
|
||||
* {@linkplain https://tools.ietf.org/html/rfc6749#section-5.2}
|
||||
*
|
||||
* @author Yannick Schaus - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class TokenEndpointException extends AuthenticationException {
|
||||
private static final long serialVersionUID = 610324537843397832L;
|
||||
|
||||
/**
|
||||
* Represents the error types which are supported in token issuing error responses.
|
||||
*
|
||||
* @author Yannick Schaus - initial contribution
|
||||
*/
|
||||
public enum ErrorType {
|
||||
INVALID_REQUEST("invalid_request"),
|
||||
INVALID_GRANT("invalid_grant"),
|
||||
INVALID_CLIENT("invalid_client"),
|
||||
INVALID_SCOPE("invalid_scope"),
|
||||
UNAUTHORIZED_CLIENT("unauthorized_client"),
|
||||
UNSUPPORTED_GRANT_TYPE("unsupported_grant_type");
|
||||
|
||||
private String error;
|
||||
|
||||
ErrorType(String error) {
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public String getError() {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@link TokenEndpointException} for the specified error type.
|
||||
*
|
||||
* @param errorType the error type
|
||||
*/
|
||||
public TokenEndpointException(ErrorType errorType) {
|
||||
super(errorType.getError());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a {@link TokenResponseErrorDTO} representing the exception
|
||||
*
|
||||
* @return the error response object
|
||||
*/
|
||||
public TokenResponseErrorDTO getErrorDTO() {
|
||||
return new TokenResponseErrorDTO(getMessage());
|
||||
}
|
||||
}
|
@ -0,0 +1,345 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.io.rest.auth.internal;
|
||||
|
||||
import java.net.URI;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.CookieParam;
|
||||
import javax.ws.rs.FormParam;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.NotAuthorizedException;
|
||||
import javax.ws.rs.NotFoundException;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.Cookie;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.NewCookie;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.ResponseBuilder;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
import javax.ws.rs.core.SecurityContext;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import org.jose4j.base64url.Base64Url;
|
||||
import org.openhab.core.auth.ManagedUser;
|
||||
import org.openhab.core.auth.PendingToken;
|
||||
import org.openhab.core.auth.User;
|
||||
import org.openhab.core.auth.UserRegistry;
|
||||
import org.openhab.core.auth.UserSession;
|
||||
import org.openhab.core.io.rest.RESTResource;
|
||||
import org.openhab.core.io.rest.Stream2JSONInputStream;
|
||||
import org.openhab.core.io.rest.auth.internal.TokenEndpointException.ErrorType;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import io.swagger.annotations.ApiResponse;
|
||||
import io.swagger.annotations.ApiResponses;
|
||||
|
||||
/**
|
||||
* This class is used to issue JWT tokens to clients.
|
||||
*
|
||||
* @author Yannick Schaus - Initial contribution
|
||||
*/
|
||||
@Path(TokenResource.PATH_AUTH)
|
||||
@Api(value = TokenResource.PATH_AUTH)
|
||||
@Component(service = { RESTResource.class, TokenResource.class })
|
||||
public class TokenResource implements RESTResource {
|
||||
private final Logger logger = LoggerFactory.getLogger(TokenResource.class);
|
||||
|
||||
/** The URI path to this resource */
|
||||
public static final String PATH_AUTH = "auth";
|
||||
|
||||
/** The name of the HTTP-only cookie holding the session ID */
|
||||
public static final String SESSIONID_COOKIE_NAME = "X-OPENHAB-SESSIONID";
|
||||
|
||||
/** The default lifetime of tokens in minutes before they expire */
|
||||
public static final int TOKEN_LIFETIME = 60;
|
||||
|
||||
@Context
|
||||
private UriInfo uriInfo;
|
||||
|
||||
private UserRegistry userRegistry;
|
||||
private JwtHelper jwtHelper;
|
||||
|
||||
@Activate
|
||||
public TokenResource(final @Reference UserRegistry userRegistry, final @Reference JwtHelper jwtHelper) {
|
||||
this.userRegistry = userRegistry;
|
||||
this.jwtHelper = jwtHelper;
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/token")
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Consumes({ MediaType.APPLICATION_FORM_URLENCODED })
|
||||
@ApiOperation(value = "Get access and refresh tokens.")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response getToken(@FormParam("grant_type") String grantType, @FormParam("code") String code,
|
||||
@FormParam("redirect_uri") String redirectUri, @FormParam("client_id") String clientId,
|
||||
@FormParam("refresh_token") String refreshToken, @FormParam("code_verifier") String codeVerifier,
|
||||
@QueryParam("useCookie") boolean useCookie, @CookieParam(SESSIONID_COOKIE_NAME) Cookie sessionCookie) {
|
||||
try {
|
||||
switch (grantType) {
|
||||
case "authorization_code":
|
||||
return processAuthorizationCodeGrant(code, redirectUri, clientId, codeVerifier, useCookie);
|
||||
|
||||
case "refresh_token":
|
||||
return processRefreshTokenGrant(clientId, refreshToken, sessionCookie);
|
||||
|
||||
default:
|
||||
throw new TokenEndpointException(ErrorType.UNSUPPORTED_GRANT_TYPE);
|
||||
}
|
||||
} catch (TokenEndpointException e) {
|
||||
logger.warn("Token issuing failed: {}", e.getMessage());
|
||||
return Response.status(Status.BAD_REQUEST).entity(e.getErrorDTO()).build();
|
||||
} catch (Exception e) {
|
||||
logger.error("Error while authenticating", e);
|
||||
return Response.status(Status.BAD_REQUEST).build();
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/sessions")
|
||||
@ApiOperation(value = "List the sessions associated to the authenticated user.")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK", response = UserSessionDTO.class) })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
public Response getSessions(@Context SecurityContext securityContext) {
|
||||
if (securityContext.getUserPrincipal() == null) {
|
||||
throw new NotAuthorizedException("User not authenticated");
|
||||
}
|
||||
|
||||
ManagedUser user = (ManagedUser) userRegistry.get(securityContext.getUserPrincipal().getName());
|
||||
if (user == null) {
|
||||
throw new NotFoundException("User not found");
|
||||
}
|
||||
|
||||
Stream<UserSessionDTO> sessions = user.getSessions().stream().map(this::toUserSessionDTO);
|
||||
return Response.ok(new Stream2JSONInputStream(sessions)).build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/logout")
|
||||
@Consumes({ MediaType.APPLICATION_FORM_URLENCODED })
|
||||
@ApiOperation(value = "Delete the session associated with a refresh token.")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response deleteSession(@FormParam("refresh_token") String refreshToken, @FormParam("id") String id,
|
||||
@Context SecurityContext securityContext) {
|
||||
if (securityContext.getUserPrincipal() == null) {
|
||||
throw new NotAuthorizedException("User not authenticated");
|
||||
}
|
||||
|
||||
ManagedUser user = (ManagedUser) userRegistry.get(securityContext.getUserPrincipal().getName());
|
||||
if (user == null) {
|
||||
throw new NotFoundException("User not found");
|
||||
}
|
||||
|
||||
Optional<UserSession> session;
|
||||
if (refreshToken != null) {
|
||||
session = user.getSessions().stream().filter(s -> s.getRefreshToken().equals(refreshToken)).findAny();
|
||||
} else if (id != null) {
|
||||
session = user.getSessions().stream().filter(s -> s.getSessionId().startsWith(id + "-")).findAny();
|
||||
} else {
|
||||
throw new IllegalArgumentException("no refresh_token or id specified");
|
||||
}
|
||||
if (session.isEmpty()) {
|
||||
throw new NotFoundException("Session not found");
|
||||
}
|
||||
|
||||
ResponseBuilder response = Response.ok();
|
||||
|
||||
if (session.get().hasSessionCookie()) {
|
||||
URI domainUri;
|
||||
try {
|
||||
domainUri = new URI(session.get().getRedirectUri());
|
||||
NewCookie newCookie = new NewCookie(SESSIONID_COOKIE_NAME, "", "/", domainUri.getHost(), null, 0, false,
|
||||
true);
|
||||
response.cookie(newCookie);
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
|
||||
user.getSessions().remove(session.get());
|
||||
userRegistry.update(user);
|
||||
|
||||
return response.build();
|
||||
}
|
||||
|
||||
private UserSessionDTO toUserSessionDTO(UserSession session) {
|
||||
// we only divulge the prefix of the session ID to the client (otherwise an XSS attacker may find the
|
||||
// session ID for a stolen refresh token easily by using the sessions endpoint).
|
||||
return new UserSessionDTO(session.getSessionId().split("-")[0], session.getCreatedTime(),
|
||||
session.getLastRefreshTime(), session.getClientId(), session.getScope());
|
||||
}
|
||||
|
||||
private Response processAuthorizationCodeGrant(String code, String redirectUri, String clientId,
|
||||
String codeVerifier, boolean useCookie) throws TokenEndpointException, NoSuchAlgorithmException {
|
||||
// find an user with the authorization code pending
|
||||
Optional<User> user = userRegistry.getAll().stream().filter(u -> ((ManagedUser) u).getPendingToken() != null
|
||||
&& ((ManagedUser) u).getPendingToken().getAuthorizationCode().equals(code)).findAny();
|
||||
|
||||
if (!user.isPresent()) {
|
||||
logger.warn("Couldn't find a user with the provided authentication code pending");
|
||||
throw new TokenEndpointException(ErrorType.INVALID_GRANT);
|
||||
}
|
||||
|
||||
ManagedUser managedUser = (ManagedUser) user.get();
|
||||
PendingToken pendingToken = managedUser.getPendingToken();
|
||||
if (pendingToken == null) {
|
||||
throw new TokenEndpointException(ErrorType.INVALID_GRANT);
|
||||
}
|
||||
if (!pendingToken.getClientId().equals(clientId)) {
|
||||
logger.warn("client_id '{}' doesn't match pending token information '{}'", clientId,
|
||||
pendingToken.getClientId());
|
||||
throw new TokenEndpointException(ErrorType.INVALID_GRANT);
|
||||
}
|
||||
if (!pendingToken.getRedirectUri().equals(redirectUri)) {
|
||||
logger.warn("redirect_uri '{}' doesn't match pending token information '{}'", redirectUri,
|
||||
pendingToken.getRedirectUri());
|
||||
throw new TokenEndpointException(ErrorType.INVALID_GRANT);
|
||||
}
|
||||
|
||||
// create a new session ID and refresh token
|
||||
String sessionId = UUID.randomUUID().toString();
|
||||
String newRefreshToken = UUID.randomUUID().toString().replace("-", "");
|
||||
String scope = pendingToken.getScope();
|
||||
|
||||
// if there is PKCE information in the pending token, check that first
|
||||
String codeChallengeMethod = pendingToken.getCodeChallengeMethod();
|
||||
if (codeChallengeMethod != null) {
|
||||
String codeChallenge = pendingToken.getCodeChallenge();
|
||||
if (codeChallenge == null || codeVerifier == null) {
|
||||
logger.warn("the PKCE code challenge or code verifier information is missing");
|
||||
throw new TokenEndpointException(ErrorType.INVALID_GRANT);
|
||||
}
|
||||
switch (codeChallengeMethod) {
|
||||
case "plain":
|
||||
if (!codeVerifier.equals(codeChallenge)) {
|
||||
logger.warn("PKCE verification failed");
|
||||
throw new TokenEndpointException(ErrorType.INVALID_GRANT);
|
||||
}
|
||||
break;
|
||||
case "S256":
|
||||
MessageDigest sha256Digest = MessageDigest.getInstance("SHA-256");
|
||||
String computedCodeChallenge = Base64Url.encode(sha256Digest.digest(codeVerifier.getBytes()));
|
||||
if (!computedCodeChallenge.equals(codeChallenge)) {
|
||||
logger.warn("PKCE verification failed");
|
||||
throw new TokenEndpointException(ErrorType.INVALID_GRANT);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
logger.warn("PKCE transformation algorithm '{}' not supported", codeChallengeMethod);
|
||||
throw new TokenEndpointException(ErrorType.INVALID_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
// create an access token
|
||||
String accessToken = jwtHelper.getJwtAccessToken(managedUser, clientId, scope, TOKEN_LIFETIME);
|
||||
|
||||
UserSession newSession = new UserSession(sessionId, newRefreshToken, clientId, redirectUri, scope);
|
||||
|
||||
ResponseBuilder response = Response.ok(
|
||||
new TokenResponseDTO(accessToken, "bearer", TOKEN_LIFETIME * 60, newRefreshToken, scope, managedUser));
|
||||
|
||||
// if the client has requested an http-only cookie for the session, set it
|
||||
if (useCookie) {
|
||||
try {
|
||||
// this feature is only available for root redirect URIs: the targeted client is the main
|
||||
// UI; even though the cookie will be set for the entire domain (i.e. no path) so that
|
||||
// other servlets can make use of it
|
||||
URI domainUri = new URI(redirectUri);
|
||||
if (!("".equals(domainUri.getPath()) || "/".equals(domainUri.getPath()))) {
|
||||
throw new IllegalArgumentException(
|
||||
"Will not honor the request to set a session cookie for this client, because it's only allowed for root redirect URIs");
|
||||
}
|
||||
NewCookie newCookie = new NewCookie(SESSIONID_COOKIE_NAME, sessionId, "/", domainUri.getHost(), null,
|
||||
2147483647, false, true);
|
||||
response.cookie(newCookie);
|
||||
|
||||
// also mark the session as supported by a cookie
|
||||
newSession.setSessionCookie(true);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Error while setting a session cookie: {}", e.getMessage());
|
||||
throw new TokenEndpointException(ErrorType.UNAUTHORIZED_CLIENT);
|
||||
}
|
||||
}
|
||||
|
||||
// add the new session to the user profile and clear the pending token information
|
||||
managedUser.getSessions().add(newSession);
|
||||
managedUser.setPendingToken(null);
|
||||
userRegistry.update(managedUser);
|
||||
|
||||
return response.build();
|
||||
}
|
||||
|
||||
private Response processRefreshTokenGrant(String clientId, String refreshToken, Cookie sessionCookie)
|
||||
throws TokenEndpointException {
|
||||
if (refreshToken == null) {
|
||||
throw new TokenEndpointException(ErrorType.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
// find an user associated with the provided refresh token
|
||||
Optional<User> refreshTokenUser = userRegistry.getAll().stream().filter(
|
||||
u -> ((ManagedUser) u).getSessions().stream().anyMatch(s -> refreshToken.equals(s.getRefreshToken())))
|
||||
.findAny();
|
||||
|
||||
if (!refreshTokenUser.isPresent()) {
|
||||
logger.warn("Couldn't find a user with a session matching the provided refresh_token");
|
||||
throw new TokenEndpointException(ErrorType.INVALID_GRANT);
|
||||
}
|
||||
|
||||
// get the session from the refresh token
|
||||
ManagedUser refreshTokenManagedUser = (ManagedUser) refreshTokenUser.get();
|
||||
UserSession session = refreshTokenManagedUser.getSessions().stream()
|
||||
.filter(s -> s.getRefreshToken().equals(refreshToken)).findAny().get();
|
||||
|
||||
// if the cookie flag is present on the session, verify that the cookie is present and corresponds
|
||||
// to this session
|
||||
if (session.hasSessionCookie()) {
|
||||
if (sessionCookie == null || !sessionCookie.getValue().equals(session.getSessionId())) {
|
||||
logger.warn("Not refreshing token for session {} of user {}, missing or invalid session cookie",
|
||||
session.getSessionId(), refreshTokenManagedUser.getName());
|
||||
throw new TokenEndpointException(ErrorType.INVALID_GRANT);
|
||||
}
|
||||
}
|
||||
|
||||
// issue a new access token
|
||||
String refreshedAccessToken = jwtHelper.getJwtAccessToken(refreshTokenManagedUser, clientId, session.getScope(),
|
||||
TOKEN_LIFETIME);
|
||||
|
||||
logger.debug("Refreshing session {} of user {}", session.getSessionId(), refreshTokenManagedUser.getName());
|
||||
|
||||
ResponseBuilder refreshResponse = Response.ok(new TokenResponseDTO(refreshedAccessToken, "bearer",
|
||||
TOKEN_LIFETIME * 60, refreshToken, session.getScope(), refreshTokenManagedUser));
|
||||
|
||||
// update the last refresh time of the session in the user's profile
|
||||
session.setLastRefreshTime(new Date());
|
||||
userRegistry.update(refreshTokenManagedUser);
|
||||
|
||||
return refreshResponse.build();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.io.rest.auth.internal;
|
||||
|
||||
import org.openhab.core.auth.User;
|
||||
|
||||
/**
|
||||
* A DTO object for a successful token endpoint response, as per RFC 6749, Section 5.1.
|
||||
*
|
||||
* {@linkplain https://tools.ietf.org/html/rfc6749#section-5.1}
|
||||
*
|
||||
* @author Yannick Schaus - initial contribution
|
||||
*/
|
||||
public class TokenResponseDTO {
|
||||
public String access_token;
|
||||
public String token_type;
|
||||
public Integer expires_in;
|
||||
public String refresh_token;
|
||||
public String scope;
|
||||
public UserDTO user;
|
||||
|
||||
/**
|
||||
* Builds a successful response containing token information.
|
||||
*
|
||||
* @param access_token the access token
|
||||
* @param token_type the type of the token, normally "bearer"
|
||||
* @param expires_in the expiration time of the access token in seconds
|
||||
* @param refresh_token the refresh token which can be used to get additional tokens
|
||||
* @param scope the request scope
|
||||
* @param user the user object, an additional parameter not part of the specification
|
||||
*/
|
||||
public TokenResponseDTO(String access_token, String token_type, Integer expires_in, String refresh_token,
|
||||
String scope, User user) {
|
||||
super();
|
||||
this.access_token = access_token;
|
||||
this.token_type = token_type;
|
||||
this.expires_in = expires_in;
|
||||
this.refresh_token = refresh_token;
|
||||
this.scope = scope;
|
||||
this.user = new UserDTO(user);
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.io.rest.auth.internal;
|
||||
|
||||
/**
|
||||
* A DTO object for an unsuccessful token endpoint response, as per RFC 6749, Section 5.2.
|
||||
*
|
||||
* {@linkplain https://tools.ietf.org/html/rfc6749#section-5.2}
|
||||
*
|
||||
* @author Yannick Schaus - initial contribution
|
||||
*/
|
||||
public class TokenResponseErrorDTO {
|
||||
public String error;
|
||||
public String error_description;
|
||||
public String error_uri;
|
||||
|
||||
/**
|
||||
* Builds a token endpoint response for a specific error
|
||||
*
|
||||
* @param the error
|
||||
*/
|
||||
public TokenResponseErrorDTO(String error) {
|
||||
this.error = error;
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.io.rest.auth.internal;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.openhab.core.auth.User;
|
||||
|
||||
/**
|
||||
* A DTO representing a {@link User}.
|
||||
*
|
||||
* @author Yannick Schaus - initial contribution
|
||||
*/
|
||||
public class UserDTO {
|
||||
String name;
|
||||
Collection<String> roles;
|
||||
|
||||
public UserDTO(User user) {
|
||||
super();
|
||||
this.name = user.getName();
|
||||
this.roles = user.getRoles();
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.io.rest.auth.internal;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* A DTO representing a user session, without the sensible information.
|
||||
*
|
||||
* @author Yannick Schaus - initial contribution
|
||||
*/
|
||||
public class UserSessionDTO {
|
||||
String sessionId;
|
||||
Date createdTime;
|
||||
Date lastRefreshTime;
|
||||
String clientId;
|
||||
String scope;
|
||||
|
||||
public UserSessionDTO(String sessionId, Date createdTime, Date lastRefreshTime, String clientId, String scope) {
|
||||
super();
|
||||
this.sessionId = sessionId;
|
||||
this.createdTime = createdTime;
|
||||
this.lastRefreshTime = lastRefreshTime;
|
||||
this.clientId = clientId;
|
||||
this.scope = scope;
|
||||
}
|
||||
}
|
@ -62,7 +62,6 @@ import io.swagger.annotations.ApiResponses;
|
||||
* @author Yannick Schaus - Added filters to getAll
|
||||
*/
|
||||
@Path(ItemChannelLinkResource.PATH_LINKS)
|
||||
@RolesAllowed({ Role.ADMIN })
|
||||
@Api(value = ItemChannelLinkResource.PATH_LINKS)
|
||||
@Component(service = { RESTResource.class, ItemChannelLinkResource.class })
|
||||
public class ItemChannelLinkResource implements RESTResource {
|
||||
@ -115,6 +114,7 @@ public class ItemChannelLinkResource implements RESTResource {
|
||||
}
|
||||
|
||||
@PUT
|
||||
@RolesAllowed({ Role.ADMIN })
|
||||
@Path("/{itemName}/{channelUID}")
|
||||
@ApiOperation(value = "Links item to a channel.")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK"),
|
||||
@ -147,6 +147,7 @@ public class ItemChannelLinkResource implements RESTResource {
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@RolesAllowed({ Role.ADMIN })
|
||||
@Path("/{itemName}/{channelUID}")
|
||||
@ApiOperation(value = "Unlinks item from a channel.")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK"),
|
||||
|
@ -15,6 +15,7 @@ package org.openhab.core.io.rest.ui.internal;
|
||||
import java.security.InvalidParameterException;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.annotation.security.RolesAllowed;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
@ -28,6 +29,7 @@ import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import org.openhab.core.auth.Role;
|
||||
import org.openhab.core.io.rest.RESTResource;
|
||||
import org.openhab.core.io.rest.Stream2JSONInputStream;
|
||||
import org.openhab.core.io.rest.ui.TileDTO;
|
||||
@ -107,6 +109,7 @@ public class UIResource implements RESTResource {
|
||||
}
|
||||
|
||||
@POST
|
||||
@RolesAllowed({ Role.ADMIN })
|
||||
@Path("/components/{namespace}")
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@ApiOperation(value = "Add an UI component in the specified namespace.")
|
||||
@ -119,6 +122,7 @@ public class UIResource implements RESTResource {
|
||||
}
|
||||
|
||||
@PUT
|
||||
@RolesAllowed({ Role.ADMIN })
|
||||
@Path("/components/{namespace}/{componentUID}")
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@ApiOperation(value = "Update a specific UI component in the specified namespace.")
|
||||
@ -141,6 +145,7 @@ public class UIResource implements RESTResource {
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@RolesAllowed({ Role.ADMIN })
|
||||
@Path("/components/{namespace}/{componentUID}")
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@ApiOperation(value = "Remove a specific UI component in the specified namespace.")
|
||||
|
@ -15,6 +15,11 @@
|
||||
<name>openHAB Core :: Bundles :: Karaf Integration</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.openhab.core.bundles</groupId>
|
||||
<artifactId>org.openhab.core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.core.bundles</groupId>
|
||||
<artifactId>org.openhab.core.boot</artifactId>
|
||||
@ -39,6 +44,12 @@
|
||||
<version>${karaf.compile.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.karaf.jaas</groupId>
|
||||
<artifactId>org.apache.karaf.jaas.modules</artifactId>
|
||||
<version>${karaf.compile.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.karaf.internal.jaas;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.karaf.jaas.boot.principal.GroupPrincipal;
|
||||
import org.apache.karaf.jaas.boot.principal.RolePrincipal;
|
||||
import org.apache.karaf.jaas.boot.principal.UserPrincipal;
|
||||
import org.apache.karaf.jaas.modules.BackingEngine;
|
||||
import org.openhab.core.auth.ManagedUser;
|
||||
import org.openhab.core.auth.Role;
|
||||
import org.openhab.core.auth.User;
|
||||
import org.openhab.core.auth.UserRegistry;
|
||||
|
||||
/**
|
||||
* A Karaf backing engine for the {@link UserRegistry}
|
||||
*
|
||||
* @author Yannick Schaus - initial contribution
|
||||
*/
|
||||
public class ManagedUserBackingEngine implements BackingEngine {
|
||||
|
||||
UserRegistry userRegistry;
|
||||
|
||||
public ManagedUserBackingEngine(UserRegistry userRegistry) {
|
||||
this.userRegistry = userRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addUser(String username, String password) {
|
||||
userRegistry.register(username, password, new HashSet<String>(Set.of(Role.USER)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteUser(String username) {
|
||||
userRegistry.remove(username);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UserPrincipal> listUsers() {
|
||||
return userRegistry.getAll().stream().map(u -> new UserPrincipal(u.getName())).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserPrincipal lookupUser(String username) {
|
||||
User user = userRegistry.get(username);
|
||||
if (user != null) {
|
||||
return new UserPrincipal(user.getName());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GroupPrincipal> listGroups(UserPrincipal user) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<GroupPrincipal, String> listGroups() {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addGroup(String username, String group) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createGroup(String group) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteGroup(String username, String group) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RolePrincipal> listRoles(Principal principal) {
|
||||
User user = userRegistry.get(principal.getName());
|
||||
if (user != null) {
|
||||
return user.getRoles().stream().map(r -> new RolePrincipal(r)).collect(Collectors.toList());
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addRole(String username, String role) {
|
||||
User user = userRegistry.get(username);
|
||||
if (user instanceof ManagedUser) {
|
||||
ManagedUser managedUser = (ManagedUser) user;
|
||||
managedUser.getRoles().add(role);
|
||||
userRegistry.update(managedUser);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteRole(String username, String role) {
|
||||
User user = userRegistry.get(username);
|
||||
if (user instanceof ManagedUser) {
|
||||
ManagedUser managedUser = (ManagedUser) user;
|
||||
managedUser.getRoles().remove(role);
|
||||
userRegistry.update(managedUser);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addGroupRole(String group, String role) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteGroupRole(String group, String role) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.karaf.internal.jaas;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.apache.karaf.jaas.modules.BackingEngine;
|
||||
import org.apache.karaf.jaas.modules.BackingEngineFactory;
|
||||
import org.openhab.core.auth.UserRegistry;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* A Karaf backing engine factory for the {@link UserRegistry}
|
||||
*
|
||||
* @author Yannick Schaus - initial contribution
|
||||
*/
|
||||
@Singleton
|
||||
@Component(service = BackingEngineFactory.class)
|
||||
public class ManagedUserBackingEngineFactory implements BackingEngineFactory {
|
||||
|
||||
UserRegistry userRegistry;
|
||||
|
||||
@Activate
|
||||
public ManagedUserBackingEngineFactory(@Reference UserRegistry userRegistry) {
|
||||
this.userRegistry = userRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModuleClass() {
|
||||
return ManagedUserRealm.MODULE_CLASS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BackingEngine build(Map<String, ?> options) {
|
||||
return new ManagedUserBackingEngine(userRegistry);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.karaf.internal.jaas;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
import javax.security.auth.login.AppConfigurationEntry;
|
||||
import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag;
|
||||
import javax.security.auth.spi.LoginModule;
|
||||
|
||||
import org.apache.karaf.jaas.boot.ProxyLoginModule;
|
||||
import org.apache.karaf.jaas.config.JaasRealm;
|
||||
import org.apache.karaf.shell.api.action.lifecycle.Service;
|
||||
import org.openhab.core.auth.UserRegistry;
|
||||
import org.osgi.framework.BundleContext;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* A JAAS realm description for the {@link UserRegistry} based login module.
|
||||
*
|
||||
* @author Yannick Schaus - initial contribution
|
||||
*/
|
||||
@Singleton
|
||||
@Component(service = JaasRealm.class)
|
||||
@Service
|
||||
public class ManagedUserRealm implements JaasRealm {
|
||||
|
||||
public static final String REALM_NAME = "openhab";
|
||||
public static final String MODULE_CLASS = "org.openhab.core.auth.jaas.internal.ManagedUserLoginModule";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ManagedUserRealm.class);
|
||||
|
||||
BundleContext bundleContext;
|
||||
|
||||
@Activate
|
||||
public ManagedUserRealm(BundleContext bundleContext, @Reference LoginModule loginModule) {
|
||||
this.bundleContext = bundleContext;
|
||||
logger.debug("Using login module {} for the openhab realm", loginModule.getClass().getCanonicalName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return REALM_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRank() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AppConfigurationEntry[] getEntries() {
|
||||
Map<String, Object> options = new HashMap<>();
|
||||
options.put(ProxyLoginModule.PROPERTY_MODULE, MODULE_CLASS);
|
||||
|
||||
return new AppConfigurationEntry[] {
|
||||
new AppConfigurationEntry(MODULE_CLASS, LoginModuleControlFlag.SUFFICIENT, options) };
|
||||
}
|
||||
|
||||
}
|
@ -12,12 +12,15 @@
|
||||
*/
|
||||
package org.openhab.core.auth;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Realizations of this type are responsible for checking validity of various credentials and giving back authentication
|
||||
* which defines access scope for authenticated user or system.
|
||||
*
|
||||
* @author Łukasz Dywicki - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface AuthenticationProvider {
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Represents a generic {@link User} with a set of roles
|
||||
*
|
||||
* @author Yannick Schaus - initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GenericUser implements User {
|
||||
|
||||
private String name;
|
||||
private Set<String> roles;
|
||||
|
||||
/**
|
||||
* Constructs a user attributed with a set of roles.
|
||||
*
|
||||
* @param name the username (account name)
|
||||
* @param roles the roles attributed to this user
|
||||
*/
|
||||
public GenericUser(String name, Set<String> roles) {
|
||||
this.name = name;
|
||||
this.roles = roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a user with no roles.
|
||||
*
|
||||
* @param name the username (account name)
|
||||
*/
|
||||
public GenericUser(String name) {
|
||||
this(name, new HashSet<>());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getUID() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getRoles() {
|
||||
return roles;
|
||||
}
|
||||
}
|
@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* A {@link User} sourced from a managed {@link UserProvider}.
|
||||
*
|
||||
* @author Yannick Schaus - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ManagedUser implements User {
|
||||
|
||||
private String name;
|
||||
private String passwordHash;
|
||||
private String passwordSalt;
|
||||
private Set<String> roles = new HashSet<>();
|
||||
private @Nullable PendingToken pendingToken = null;
|
||||
private List<UserSession> sessions = new ArrayList<>();
|
||||
private List<UserApiToken> apiTokens = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Constructs a user with a password hash & salt provided by the caller.
|
||||
*
|
||||
* @param name the username (account name)
|
||||
* @param passwordSalt the salt to compute the password hash
|
||||
* @param passwordHash the result of the hashing of the salted password
|
||||
*/
|
||||
public ManagedUser(String name, String passwordSalt, String passwordHash) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.passwordSalt = passwordSalt;
|
||||
this.passwordHash = passwordHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the password hash.
|
||||
*
|
||||
* @return the password hash
|
||||
*/
|
||||
public String getPasswordHash() {
|
||||
return passwordHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the password salt.
|
||||
*
|
||||
* @return the password salt
|
||||
*/
|
||||
public String getPasswordSalt() {
|
||||
return passwordSalt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alters the user's account name
|
||||
*
|
||||
* @param name the new account name
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUID() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getRoles() {
|
||||
return roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alters the user's set of roles.
|
||||
*
|
||||
* @param roles the new roles
|
||||
*/
|
||||
public void setRoles(Set<String> roles) {
|
||||
this.roles = roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the pending token information for this user, if any.
|
||||
*
|
||||
* @return the pending token information or null if there is none
|
||||
*/
|
||||
public @Nullable PendingToken getPendingToken() {
|
||||
return pendingToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or clears the pending token information for this user.
|
||||
*
|
||||
* @param pendingToken the pending token information or null to clear it
|
||||
*/
|
||||
public void setPendingToken(@Nullable PendingToken pendingToken) {
|
||||
this.pendingToken = pendingToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current persistent sessions for this user.
|
||||
*
|
||||
* @return the list of sessions
|
||||
*/
|
||||
public List<UserSession> getSessions() {
|
||||
return sessions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the list of sessions by a new one.
|
||||
*
|
||||
* @param sessions the new list of sessions
|
||||
*/
|
||||
public void setSessions(List<UserSession> sessions) {
|
||||
this.sessions = sessions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the long-term API tokens for this user
|
||||
*
|
||||
* @return the API tokens
|
||||
*/
|
||||
public List<UserApiToken> getApiTokens() {
|
||||
return apiTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the list of API tokens by a new one.
|
||||
*
|
||||
* @param apiTokens the new API tokens
|
||||
*/
|
||||
public void setApiTokens(List<UserApiToken> apiTokens) {
|
||||
this.apiTokens = apiTokens;
|
||||
}
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The pending information used in a OAuth2 authorization flow, set after the user has authorized the client to access
|
||||
* resources, and has been redirected to the callback URI with an authorization code. The information will be used when
|
||||
* it calls the token endpoint to exchange the authorization code for a access token and a refresh token.
|
||||
*
|
||||
* The authorization code for a token is sensible information while it is valid, therefore the client is supposed to
|
||||
* call the token endpoint to perform the exchange it immediately after receiving it. The information should remain in
|
||||
* the @link {@link ManagedUser} profile for a limited time only.
|
||||
*
|
||||
* Additionally, and optionally, information about the code challenge as specified by PKCE (RFC 7636) can be stored
|
||||
* along with the code.
|
||||
*
|
||||
* @author Yannick Schaus - initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PendingToken {
|
||||
private String authorizationCode;
|
||||
private String clientId;
|
||||
private String redirectUri;
|
||||
private String scope;
|
||||
|
||||
@Nullable
|
||||
private String codeChallenge;
|
||||
@Nullable
|
||||
private String codeChallengeMethod;
|
||||
|
||||
/**
|
||||
* Constructs a pending token.
|
||||
*
|
||||
* @param authorizationCode the authorization code provided to the client
|
||||
* @param clientId the client ID making the request
|
||||
* @param redirectUri the provided redirect URI
|
||||
* @param scope the requested scopes
|
||||
* @param codeChallenge the code challenge (optional)
|
||||
* @param codeChallengeMethod the code challenge method (optional)
|
||||
*/
|
||||
public PendingToken(String authorizationCode, String clientId, String redirectUri, String scope,
|
||||
@Nullable String codeChallenge, @Nullable String codeChallengeMethod) {
|
||||
super();
|
||||
this.authorizationCode = authorizationCode;
|
||||
this.clientId = clientId;
|
||||
this.redirectUri = redirectUri;
|
||||
this.scope = scope;
|
||||
this.codeChallenge = codeChallenge;
|
||||
this.codeChallengeMethod = codeChallengeMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the authorization code provided to the client.
|
||||
*
|
||||
* @return the authorization code
|
||||
*/
|
||||
public String getAuthorizationCode() {
|
||||
return authorizationCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ID of the client requesting the upcoming token.
|
||||
*
|
||||
* @return the client ID
|
||||
*/
|
||||
public String getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the redirect URI of the client requesting the upcoming token.
|
||||
*
|
||||
* @return the redirect URI
|
||||
*/
|
||||
public String getRedirectUri() {
|
||||
return redirectUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the requested scopes for the upcoming token.
|
||||
*
|
||||
* @return the requested scopes
|
||||
*/
|
||||
public String getScope() {
|
||||
return scope;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the code challenge
|
||||
*
|
||||
* @return the code challenge
|
||||
*/
|
||||
public @Nullable String getCodeChallenge() {
|
||||
return codeChallenge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the code challenge method
|
||||
*
|
||||
* @return the code challenge method
|
||||
*/
|
||||
public @Nullable String getCodeChallengeMethod() {
|
||||
return codeChallengeMethod;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.common.registry.Identifiable;
|
||||
|
||||
/**
|
||||
* A user represents an individual, physical person using the system.
|
||||
*
|
||||
* @author Yannick Schaus - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface User extends Principal, Identifiable<String> {
|
||||
|
||||
/**
|
||||
* Gets the roles attributed to the user.
|
||||
*
|
||||
* @see Role
|
||||
* @return role attributed to the user
|
||||
*/
|
||||
public Set<String> getRoles();
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* An API token represents long-term credentials generated by an user, giving the bearer access to the API on behalf of
|
||||
* this user for a certain scope.
|
||||
*
|
||||
* @author Yannick Schaus - initial contribution
|
||||
*
|
||||
*/
|
||||
public class UserApiToken {
|
||||
String name;
|
||||
String apiToken;
|
||||
Date createdTime;
|
||||
String scope;
|
||||
|
||||
/**
|
||||
* Constructs an API token.
|
||||
*
|
||||
* @param name the name of the token, for identification purposes
|
||||
* @param apiToken the token
|
||||
* @param scope the scope this token is valid for
|
||||
*/
|
||||
public UserApiToken(String name, String apiToken, String scope) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.apiToken = apiToken;
|
||||
this.createdTime = new Date();
|
||||
this.scope = scope;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name identifying the token
|
||||
*
|
||||
* @return the API token
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the API token which can be passed in requests as a "Bearer" token in the Authorization HTTP header.
|
||||
*
|
||||
* @return the API token
|
||||
*/
|
||||
public String getApiToken() {
|
||||
return apiToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the time when this token was created.
|
||||
*
|
||||
* @return the date of creation
|
||||
*/
|
||||
public Date getCreatedTime() {
|
||||
return createdTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the scope this token is valid for.
|
||||
*
|
||||
* @return the scope
|
||||
*/
|
||||
public String getScope() {
|
||||
return scope;
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.common.registry.Provider;
|
||||
|
||||
/**
|
||||
* A interface for a {@link Provider} of {@link User} entities
|
||||
*
|
||||
* @author Yannick Schaus - initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface UserProvider extends Provider<User> {
|
||||
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.common.registry.Registry;
|
||||
|
||||
/**
|
||||
* An interface for a generic {@link Registry} of {@link User} entities. User registries can also be used as
|
||||
* {@link AuthenticationProvider}.
|
||||
*
|
||||
* @author Yannick Schaus - initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface UserRegistry extends Registry<User, String>, AuthenticationProvider {
|
||||
|
||||
/**
|
||||
* Adds a new {@link User} in this registry. The implementation receives the clear text credentials and is
|
||||
* responsible for their secure storage (for instance by hashing the password), then return the newly created
|
||||
* {@link User} instance.
|
||||
*
|
||||
* @param username the username of the new user
|
||||
* @param password the user password
|
||||
* @param roles the roles attributed to the new user
|
||||
*
|
||||
* @return the new registered {@link User} instance
|
||||
*/
|
||||
public User register(String username, String password, Set<String> roles);
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* A persistent session for a {@link ManagedUser}, which holds a refresh token used by a client to get short-lived
|
||||
* access tokens for API requests authorization.
|
||||
*
|
||||
* @author Yannick Schaus - initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class UserSession {
|
||||
String sessionId;
|
||||
String refreshToken;
|
||||
Date createdTime;
|
||||
Date lastRefreshTime;
|
||||
String clientId;
|
||||
String redirectUri;
|
||||
String scope;
|
||||
boolean sessionCookie;
|
||||
|
||||
/**
|
||||
* Constructs a new session.
|
||||
*
|
||||
* @param sessionId an unique ID for the session
|
||||
* @param refreshToken the refresh token associated to the session
|
||||
* @param clientId the client ID associated to the session
|
||||
* @param redirectUri the callback URI provided when the client was authorized by the user
|
||||
* @param scope the granted scope provided when the client was authorized by the user
|
||||
*/
|
||||
public UserSession(String sessionId, String refreshToken, String clientId, String redirectUri, String scope) {
|
||||
super();
|
||||
this.sessionId = sessionId;
|
||||
this.refreshToken = refreshToken;
|
||||
this.createdTime = new Date();
|
||||
this.lastRefreshTime = new Date();
|
||||
this.clientId = clientId;
|
||||
this.redirectUri = redirectUri;
|
||||
this.scope = scope;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ID of the session.
|
||||
*
|
||||
* @return the session ID
|
||||
*/
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the refresh token for the session.
|
||||
*
|
||||
* @return the refresh token
|
||||
*/
|
||||
public String getRefreshToken() {
|
||||
return refreshToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the creation time of the session.
|
||||
*
|
||||
* @return the creation time
|
||||
*/
|
||||
public Date getCreatedTime() {
|
||||
return createdTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the time when the refresh token was last used to get a new access token.
|
||||
*
|
||||
* @return the last refresh time
|
||||
*/
|
||||
public Date getLastRefreshTime() {
|
||||
return lastRefreshTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the time when the refresh token was last used to get a new access token.
|
||||
*
|
||||
* @param lastRefreshTime the last refresh time
|
||||
*/
|
||||
public void setLastRefreshTime(Date lastRefreshTime) {
|
||||
this.lastRefreshTime = lastRefreshTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the scope requested when authorizing this session.
|
||||
*
|
||||
* @return the session scope
|
||||
*/
|
||||
public String getScope() {
|
||||
return scope;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ID of the client this session was created for
|
||||
*
|
||||
* @return the client ID
|
||||
*/
|
||||
public String getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the redirect URI which was used to perform the authorization flow.
|
||||
*
|
||||
* @return the redirect URI
|
||||
*/
|
||||
public String getRedirectUri() {
|
||||
return redirectUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies whether this session is supported by a session cookie, to mitigate the impact of refresh token
|
||||
* leakage.
|
||||
*
|
||||
* @return whether or not a cookie has been set
|
||||
*/
|
||||
public boolean hasSessionCookie() {
|
||||
return sessionCookie;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the session cookie flag for this session.
|
||||
*
|
||||
* @param sessionCookie the cookie flag
|
||||
*/
|
||||
public void setSessionCookie(boolean sessionCookie) {
|
||||
this.sessionCookie = sessionCookie;
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.internal.auth;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.auth.ManagedUser;
|
||||
import org.openhab.core.auth.User;
|
||||
import org.openhab.core.common.registry.DefaultAbstractManagedProvider;
|
||||
import org.openhab.core.common.registry.ManagedProvider;
|
||||
import org.openhab.core.storage.StorageService;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* A {@link ManagedProvider} for {@link ManagedUser} entities
|
||||
*
|
||||
* @author Yannick Schaus - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = ManagedUserProvider.class, immediate = true)
|
||||
public class ManagedUserProvider extends DefaultAbstractManagedProvider<User, String> {
|
||||
|
||||
@Activate
|
||||
public ManagedUserProvider(final @Reference StorageService storageService) {
|
||||
super(storageService);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getStorageName() {
|
||||
return "users";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String keyToString(String key) {
|
||||
return key;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.internal.auth;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.auth.Authentication;
|
||||
import org.openhab.core.auth.AuthenticationException;
|
||||
import org.openhab.core.auth.Credentials;
|
||||
import org.openhab.core.auth.ManagedUser;
|
||||
import org.openhab.core.auth.User;
|
||||
import org.openhab.core.auth.UserProvider;
|
||||
import org.openhab.core.auth.UserRegistry;
|
||||
import org.openhab.core.auth.UsernamePasswordCredentials;
|
||||
import org.openhab.core.common.registry.AbstractRegistry;
|
||||
import org.osgi.framework.BundleContext;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Deactivate;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.osgi.service.component.annotations.ReferenceCardinality;
|
||||
import org.osgi.service.component.annotations.ReferencePolicy;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The implementation of a {@link UserRegistry} for {@link ManagedUser} entities.
|
||||
*
|
||||
* @author Yannick Schaus - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = UserRegistry.class, immediate = true)
|
||||
public class UserRegistryImpl extends AbstractRegistry<User, String, UserProvider> implements UserRegistry {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(UserRegistryImpl.class);
|
||||
|
||||
private static final int ITERATIONS = 65536;
|
||||
private static final int KEY_LENGTH = 512;
|
||||
private static final String ALGORITHM = "PBKDF2WithHmacSHA512";
|
||||
private static final SecureRandom RAND = new SecureRandom();
|
||||
|
||||
@Activate
|
||||
public UserRegistryImpl(BundleContext context, Map<String, Object> properties) {
|
||||
super(UserProvider.class);
|
||||
super.activate(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deactivate
|
||||
protected void deactivate() {
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
|
||||
protected void setManagedProvider(ManagedUserProvider managedProvider) {
|
||||
super.setManagedProvider(managedProvider);
|
||||
super.addProvider(managedProvider);
|
||||
}
|
||||
|
||||
protected void unsetManagedProvider(ManagedUserProvider managedProvider) {
|
||||
super.unsetManagedProvider(managedProvider);
|
||||
super.removeProvider(managedProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User register(String username, String password, Set<String> roles) {
|
||||
String passwordSalt = generateSalt(KEY_LENGTH / 8).get();
|
||||
String passwordHash = hashPassword(password, passwordSalt).get();
|
||||
ManagedUser user = new ManagedUser(username, passwordSalt, passwordHash);
|
||||
user.setRoles(new HashSet<>(roles));
|
||||
super.add(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
private Optional<String> generateSalt(final int length) {
|
||||
if (length < 1) {
|
||||
logger.error("error in generateSalt: length must be > 0");
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
byte[] salt = new byte[length];
|
||||
RAND.nextBytes(salt);
|
||||
|
||||
return Optional.of(Base64.getEncoder().encodeToString(salt));
|
||||
}
|
||||
|
||||
private Optional<String> hashPassword(String password, String salt) {
|
||||
char[] chars = password.toCharArray();
|
||||
byte[] bytes = salt.getBytes();
|
||||
|
||||
PBEKeySpec spec = new PBEKeySpec(chars, bytes, ITERATIONS, KEY_LENGTH);
|
||||
|
||||
Arrays.fill(chars, Character.MIN_VALUE);
|
||||
|
||||
try {
|
||||
SecretKeyFactory fac = SecretKeyFactory.getInstance(ALGORITHM);
|
||||
byte[] securePassword = fac.generateSecret(spec).getEncoded();
|
||||
return Optional.of(Base64.getEncoder().encodeToString(securePassword));
|
||||
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
|
||||
logger.error("Exception encountered in hashPassword", e);
|
||||
return Optional.empty();
|
||||
} finally {
|
||||
spec.clearPassword();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Credentials credentials) throws AuthenticationException {
|
||||
UsernamePasswordCredentials usernamePasswordCreds = (UsernamePasswordCredentials) credentials;
|
||||
User user = this.get(usernamePasswordCreds.getUsername());
|
||||
if (user == null) {
|
||||
throw new AuthenticationException("User not found: " + usernamePasswordCreds.getUsername());
|
||||
}
|
||||
if (!(user instanceof ManagedUser)) {
|
||||
throw new AuthenticationException("User is not managed: " + usernamePasswordCreds.getUsername());
|
||||
}
|
||||
|
||||
ManagedUser managedUser = (ManagedUser) user;
|
||||
String hashedPassword = hashPassword(usernamePasswordCreds.getPassword(), managedUser.getPasswordSalt()).get();
|
||||
if (!hashedPassword.equals(managedUser.getPasswordHash())) {
|
||||
throw new AuthenticationException("Wrong password for user " + usernamePasswordCreds.getUsername());
|
||||
}
|
||||
|
||||
Authentication authentication = new Authentication(managedUser.getName());
|
||||
return authentication;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<? extends Credentials> type) {
|
||||
return (UsernamePasswordCredentials.class.isAssignableFrom(type));
|
||||
}
|
||||
|
||||
}
|
@ -138,6 +138,7 @@
|
||||
|
||||
<feature name="openhab-core-io-http-auth" version="${project.version}">
|
||||
<feature>openhab-core-base</feature>
|
||||
<feature>openhab-core-auth-jaas</feature>
|
||||
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.io.http.auth/${project.version}</bundle>
|
||||
</feature>
|
||||
|
||||
@ -153,7 +154,10 @@
|
||||
|
||||
<feature name="openhab-core-io-rest-auth" version="${project.version}">
|
||||
<feature>openhab-core-base</feature>
|
||||
<feature>openhab-core-auth-jaas</feature>
|
||||
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.io.rest.auth/${project.version}</bundle>
|
||||
<requirement>openhab.tp;filter:="(feature=jose4j)"</requirement>
|
||||
<feature dependency="true">openhab.tp-jose4j</feature>
|
||||
</feature>
|
||||
|
||||
<feature name="openhab-core-io-rest-log" version="${project.version}">
|
||||
|
@ -176,6 +176,11 @@
|
||||
<bundle>mvn:org.jmdns/jmdns/3.5.5</bundle>
|
||||
</feature>
|
||||
|
||||
<feature name="openhab.tp-jose4j" description="jose4j JWT library" version="${project.version}">
|
||||
<capability>openhab.tp;feature=jose4j;version=0.7.0</capability>
|
||||
<bundle>mvn:org.bitbucket.b_c/jose4j/0.7.0</bundle>
|
||||
</feature>
|
||||
|
||||
<feature name="openhab.tp-jupnp" description="UPnP/DLNA library for Java" version="${project.version}">
|
||||
<capability>openhab.tp;feature=jupnp;version=2.5.2</capability>
|
||||
<feature dependency="true">http</feature>
|
||||
|
Loading…
Reference in New Issue
Block a user