2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.freeboxos.internal.api.rest;
15 import static javax.xml.bind.DatatypeConverter.printHexBinary;
17 import java.security.InvalidKeyException;
18 import java.security.NoSuchAlgorithmException;
20 import java.util.Optional;
22 import javax.crypto.Mac;
23 import javax.crypto.spec.SecretKeySpec;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.freeboxos.internal.api.FreeboxException;
28 import org.openhab.binding.freeboxos.internal.api.Response;
29 import org.osgi.framework.Bundle;
30 import org.osgi.framework.FrameworkUtil;
33 * The {@link LoginManager} is the Java class used to handle api requests related to session handling and login
35 * @author Gaƫl L'hopital - Initial contribution
38 public class LoginManager extends RestManager {
39 private static final Bundle BUNDLE = FrameworkUtil.getBundle(LoginManager.class);
40 private static final String APP_ID = BUNDLE.getSymbolicName();
41 private static final String ALGORITHM = "HmacSHA1";
42 private static final String PATH = "login";
43 private static final String SESSION = "session";
44 private static final String AUTHORIZE_ACTION = "authorize";
45 private static final String LOGOUT = "logout";
47 private static enum Status {
48 PENDING, // the user has not confirmed the autorization request yet
49 TIMEOUT, // the user did not confirmed the authorization within the given time
50 GRANTED, // the app_token is valid and can be used to open a session
51 DENIED, // the user denied the authorization request
52 UNKNOWN; // the app_token is invalid or has been revoked
55 private static record AuthorizationStatus(Status status, boolean loggedIn, String challenge,
56 @Nullable String passwordSalt, boolean passwordSet) {
59 private static class AuthStatus extends Response<AuthorizationStatus> {
62 private static record Authorization(String appToken, String trackId) {
65 private static class AuthResponse extends Response<Authorization> {
68 public static enum Permission {
87 public static record Session(Map<LoginManager.Permission, @Nullable Boolean> permissions,
88 @Nullable String sessionToken) {
89 protected boolean hasPermission(LoginManager.Permission checked) {
90 return Boolean.TRUE.equals(permissions.get(checked));
94 private static class SessionResponse extends Response<Session> {
97 private static record AuthorizeData(String appId, String appName, String appVersion, String deviceName) {
98 AuthorizeData(String appId, Bundle bundle) {
99 this(appId, bundle.getHeaders().get("Bundle-Name"), bundle.getVersion().toString(),
100 bundle.getHeaders().get("Bundle-Vendor"));
104 private static record OpenSessionData(String appId, String password) {
107 private final Mac mac;
108 private Optional<Authorization> authorize = Optional.empty();
110 public LoginManager(FreeboxOsSession session) throws FreeboxException {
111 super(session, LoginManager.Permission.NONE, session.getUriBuilder().path(PATH));
113 this.mac = Mac.getInstance(ALGORITHM);
114 } catch (NoSuchAlgorithmException e) {
115 throw new IllegalArgumentException(e);
119 public Session openSession(String appToken) throws FreeboxException {
120 AuthorizationStatus authorization = getSingle(AuthStatus.class);
123 // Initialize mac with the signing key
124 mac.init(new SecretKeySpec(appToken.getBytes(), mac.getAlgorithm()));
125 // Compute the hmac on input data bytes
126 byte[] rawHmac = mac.doFinal(authorization.challenge().getBytes());
127 // Convert raw bytes to Hex
128 String password = printHexBinary(rawHmac).toLowerCase();
129 return post(new OpenSessionData(APP_ID, password), SessionResponse.class, SESSION);
130 } catch (InvalidKeyException e) {
131 throw new IllegalArgumentException(e);
135 public void closeSession() throws FreeboxException {
139 public String checkGrantStatus() throws FreeboxException {
140 if (authorize.isEmpty()) {
141 authorize = Optional.of(post(new AuthorizeData(APP_ID, BUNDLE), AuthResponse.class, AUTHORIZE_ACTION));
144 return switch (getSingle(AuthStatus.class, AUTHORIZE_ACTION, authorize.get().trackId).status()) {
147 String appToken = authorize.get().appToken;
148 authorize = Optional.empty();
151 case TIMEOUT -> throw new FreeboxException("Unable to grant session, delay expired");
152 case DENIED -> throw new FreeboxException("Unable to grant session, access was denied");
153 case UNKNOWN -> throw new FreeboxException("Unable to grant session");