forked from openhab/openhab-core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[REST Auth] API tokens & openhab:users console command (openhab#1735)
This adds API tokens as a new credential type. Their format is: `oh.<name>.<random chars>` The "oh." prefix is used to tell them apart from a JWT access token, because they're both used as a Bearer authorization scheme, but there is no semantic value attached to any of the other parts. They are stored hashed in the user's profile, and can be listed, added or removed managed with the new `openhab:users` console command. Currently the scopes are still not checked, but ultimately they could be, for instance a scope of e.g. `user admin.items` would mean that the API token can be used to perform user operations like retrieving info or sending a command, _and_ managing the items, but nothing else - even if the user has more permissions because of their role (which will of course still be checked). Tokens are normally passed in the Authorization header with the Bearer scheme, or the X-OPENHAB-TOKEN header, like access tokens. As a special exception, API tokens can also be used with the Basic authorization scheme, **even if the allowBasicAuth** option is not enabled in the "API Security" service, because there's no additional security risk in allowing that. In that case, the token should be passed as the username and the password MUST be empty. In short, this means that all these curl commands will work: - `curl -H 'Authorization: Bearer <token>' http://localhost:8080/rest/inbox` - `curl -H 'X-OPENHAB-TOKEN: <token>' http://localhost:8080/rest/inbox` - `curl -u '<token>[:]' http://localhost:8080/rest/inbox` - `curl http://<token>@localhost:8080/rest/inbox` 2 REST API operations were adding to the AuthResource, to allow authenticated users to list their tokens or remove (revoke) one. Self-service for creating a token or changing the password is more sensitive so these should be handled with a servlet and pages devoid of any JavaScript instead of REST API calls, therefore for now they'll have to be done with the console. This also fixes regressions introduced with openhab#1713 - the operations annotated with @RolesAllowed({ Role.USER }) only were not authorized for administrators anymore. * Generate a unique salt for each token Reusing the password salt is bad practice, and changing the password changes the salt as well which makes all tokens invalid. Put the salt in the same field as the hash (concatenated with a separator) to avoid modifying the JSON DB schema. * Fix API token authentication, make scope available to security context The X-OPENHAB-TOKEN header now has priority over the Authorization header to credentials, if both are set. * Add self-service pages to change password & create new API token Signed-off-by: Yannick Schaus <github@schaus.net> GitOrigin-RevId: 8b52cab
- Loading branch information
Showing
20 changed files
with
1,431 additions
and
245 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
126 changes: 126 additions & 0 deletions
126
...use.core.auth.core/src/test/java/org/openhab/core/internal/auth/UserRegistryImplTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
/** | ||
* 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 static org.junit.jupiter.api.Assertions.*; | ||
import static org.mockito.ArgumentMatchers.*; | ||
import static org.mockito.Mockito.*; | ||
|
||
import java.util.Map; | ||
import java.util.Set; | ||
import java.util.UUID; | ||
|
||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.extension.ExtendWith; | ||
import org.mockito.ArgumentCaptor; | ||
import org.mockito.Mock; | ||
import org.mockito.junit.jupiter.MockitoExtension; | ||
import org.openhab.core.auth.ManagedUser; | ||
import org.openhab.core.auth.User; | ||
import org.openhab.core.auth.UserApiTokenCredentials; | ||
import org.openhab.core.auth.UserSession; | ||
import org.openhab.core.auth.UsernamePasswordCredentials; | ||
import org.osgi.framework.BundleContext; | ||
import org.osgi.framework.ServiceEvent; | ||
import org.osgi.framework.ServiceListener; | ||
import org.osgi.framework.ServiceReference; | ||
|
||
/** | ||
* @author Yannick Schaus - Initial contribution | ||
*/ | ||
@ExtendWith(MockitoExtension.class) | ||
public class UserRegistryImplTest { | ||
|
||
@SuppressWarnings("rawtypes") | ||
private @Mock ServiceReference managedProviderRef; | ||
private @Mock BundleContext bundleContext; | ||
private @Mock ManagedUserProvider managedProvider; | ||
|
||
private UserRegistryImpl registry; | ||
private ServiceListener providerTracker; | ||
|
||
@BeforeEach | ||
@SuppressWarnings("unchecked") | ||
public void setup() throws Exception { | ||
when(bundleContext.getService(same(managedProviderRef))).thenReturn(managedProvider); | ||
|
||
registry = new UserRegistryImpl(bundleContext, Map.of()); | ||
registry.setManagedProvider(managedProvider); | ||
registry.waitForCompletedAsyncActivationTasks(); | ||
|
||
ArgumentCaptor<ServiceListener> captor = ArgumentCaptor.forClass(ServiceListener.class); | ||
verify(bundleContext).addServiceListener(captor.capture(), any()); | ||
providerTracker = captor.getValue(); | ||
providerTracker.serviceChanged(new ServiceEvent(ServiceEvent.REGISTERED, managedProviderRef)); | ||
} | ||
|
||
@Test | ||
public void testGetEmpty() throws Exception { | ||
User res = registry.get("none"); | ||
assertNull(res); | ||
} | ||
|
||
@Test | ||
public void testUserManagement() throws Exception { | ||
User user = registry.register("username", "password", Set.of("administrator")); | ||
registry.added(managedProvider, user); | ||
assertNotNull(user); | ||
registry.authenticate(new UsernamePasswordCredentials("username", "password")); | ||
registry.changePassword(user, "password2"); | ||
registry.authenticate(new UsernamePasswordCredentials("username", "password2")); | ||
registry.remove(user.getName()); | ||
registry.removed(managedProvider, user); | ||
user = registry.get("username"); | ||
assertNull(user); | ||
} | ||
|
||
@Test | ||
public void testSessions() throws Exception { | ||
ManagedUser user = (ManagedUser) registry.register("username", "password", Set.of("administrator")); | ||
registry.added(managedProvider, user); | ||
assertNotNull(user); | ||
UserSession session1 = new UserSession(UUID.randomUUID().toString(), "s1", "urn:test", "urn:test", "scope"); | ||
UserSession session2 = new UserSession(UUID.randomUUID().toString(), "s2", "urn:test", "urn:test", "scope2"); | ||
UserSession session3 = new UserSession(UUID.randomUUID().toString(), "s3", "urn:test", "urn:test", "scope3"); | ||
registry.addUserSession(user, session1); | ||
registry.addUserSession(user, session2); | ||
registry.addUserSession(user, session3); | ||
assertEquals(user.getSessions().size(), 3); | ||
registry.removeUserSession(user, session3); | ||
assertEquals(user.getSessions().size(), 2); | ||
registry.clearSessions(user); | ||
assertEquals(user.getSessions().size(), 0); | ||
} | ||
|
||
@Test | ||
public void testApiTokens() throws Exception { | ||
ManagedUser user = (ManagedUser) registry.register("username", "password", Set.of("administrator")); | ||
registry.added(managedProvider, user); | ||
assertNotNull(user); | ||
String token1 = registry.addUserApiToken(user, "token1", "scope1"); | ||
String token2 = registry.addUserApiToken(user, "token2", "scope2"); | ||
String token3 = registry.addUserApiToken(user, "token3", "scope3"); | ||
assertEquals(user.getApiTokens().size(), 3); | ||
registry.authenticate(new UserApiTokenCredentials(token1)); | ||
registry.authenticate(new UserApiTokenCredentials(token2)); | ||
registry.authenticate(new UserApiTokenCredentials(token3)); | ||
registry.removeUserApiToken(user, | ||
user.getApiTokens().stream().filter(t -> t.getName().equals("token1")).findAny().get()); | ||
registry.removeUserApiToken(user, | ||
user.getApiTokens().stream().filter(t -> t.getName().equals("token2")).findAny().get()); | ||
registry.removeUserApiToken(user, | ||
user.getApiTokens().stream().filter(t -> t.getName().equals("token3")).findAny().get()); | ||
assertEquals(user.getApiTokens().size(), 0); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.