All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.glassfish.common.util.admin.AuthTokenManager Maven / Gradle / Ivy

There is a newer version: 8.0.0-JDK17-M7
Show newest version
/*
 * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.common.util.admin;

import com.sun.enterprise.util.CULoggerInfo;
import com.sun.enterprise.util.LocalStringManagerImpl;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import jakarta.inject.Singleton;
import javax.security.auth.Subject;
import org.jvnet.hk2.annotations.Service;

/**
 * Coordinates generation and consumption of very-limited-use authentication tokens.
 * 

* Some DAS commands submit admin commands to be run elsewhere - either in * another process on the same host or, via ssh, to another host. Given that it * is already executing, the DAS command in progress has already been authenticated (if * required). Therefore we want the soon-to-be submitted commands to also * be authenticated, but we do not want to send the username and/or password * information that was used to authenticate the currently-running DAS command * to the other process for it to use. *

* Instead, the currently-running DAS command can use this service to obtain * a one-time authentication token. The DAS command then includes the token, * rather than username/password credentials, in the submitted command. *

* This service records which tokens have been given out but not yet used up. * When an admin request arrives with a token, the AdminAdapter consults this * service to see if the token is valid and, if so, the AdminAdapter * allows the request to run. *

* We allow each token to be used twice, once for retrieving the command * metadata and then the second time to execute the command. (Also see the note below.) *

* Tokens have a limited life as measured in time also. If a token is created * but not fully consumed before it expires, then this manager considers the * token invalid and removes it from the collection of known valid tokens. * * NOTE * * Commands that trigger other commands on multiple hosts - such as * start-cluster - will need to reuse the authentication token more than twice. * For such purposes the code using the token can append a "+" to the token. * When such a token is used, this manager does NOT decrement the remaining * number of uses. Rather, it only refreshes the token's expiration time. * * @author Tim Quinn */ @Service @Singleton public class AuthTokenManager { public static final String AUTH_TOKEN_OPTION_NAME = "_authtoken"; private static final String SUPPRESSED_TOKEN_OUTPUT = "????"; private final static int TOKEN_SIZE = 10; private final static long DEFAULT_TOKEN_LIFETIME = 60 * 1000; private final SecureRandom rng = new SecureRandom(); private final Map liveTokens = new HashMap(); private final static Logger logger = CULoggerInfo.getLogger(); private final static char REUSE_TOKEN_MARKER = '+'; private static final LocalStringManagerImpl localStrings = new LocalStringManagerImpl(AuthTokenManager.class); /* hex conversion stolen shamelessly from Bill's LocalPasswordImpl - maybe refactor to share later */ private static final char[] hex = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; private static class TokenInfo { private final String token; private int usesRemaining = 2; // each token is used once to get metadata, once to execute private long expiration; private final long lifetime; private final Subject subject; private TokenInfo(final Subject subject, final String value, final long lifetime) { this.subject = subject; this.token = value; this.lifetime = lifetime; this.expiration = System.currentTimeMillis() + (lifetime); } private synchronized boolean isOKTouse(final long now) { return ! isUsedUp(now); } private synchronized boolean use(final boolean isBeingReused, final long now) { if (isUsedUp(now)) { if (logger.isLoggable(Level.FINER)) { final String msg = localStrings.getLocalString("AuthTokenInvalid", "Use of auth token {2} attempted but token is invalid; usesRemaining = {0,number,integer}, expired = {1}", Integer.valueOf(usesRemaining), Boolean.toString(expiration <= now), token); logger.log(Level.FINER, msg); } return false; } if ( ! isBeingReused) { usesRemaining--; } if (logger.isLoggable(Level.FINER)) { logger.log(Level.FINER, "Use of auth token {0} OK; isBeingReused = {2}; remaining uses = {1,number,integer}", new Object[] {token, Integer.valueOf(usesRemaining), Boolean.toString(isBeingReused)}); } expiration += lifetime; return true; } private boolean isUsedUp(final long now) { return usesRemaining <= 0 || expiration <= now; } } /** * Creates a new limited use authentication token with the specified * lifetime (in ms). * @param subject the Subject to associate with this token when it is consumed * @param lifetime how long each use of the token extends its lifetime * @return auth token */ public String createToken(final Subject subject, final long lifetime) { final byte[] newToken = new byte[TOKEN_SIZE]; rng.nextBytes(newToken); final String token = toHex(newToken); liveTokens.put(token, new TokenInfo(subject, token, lifetime)); logger.log(Level.FINER, "Auth token {0} created", token); return token; } /** * Creates a new limited use authentication token with the default * lifetime. * @return auth token */ public String createToken() { return createToken(DEFAULT_TOKEN_LIFETIME); } /** * Creates a new limited use authentication token with the given Subject * and the default lifetime. * @param subject the Subject to associated with this token when it is consumed * @return */ public String createToken(final Subject subject) { return createToken(subject, DEFAULT_TOKEN_LIFETIME); } /** * Creates a new limited use authentication token with the specified * lifetime but no Subject. * @param lifetime how long each use of the token extends its lifetime * @return */ public String createToken(final long lifetime) { return createToken (new Subject(), lifetime); } /** * Locates the Subject for the specified token (if any) without consuming * the token. *

* Use this method only from authentication logic that needs to find the * token. Later command processing will consume the token if it is present. * This avoids having to force the special admin LoginModule to run even if * username/password authentication works. * * @param token the token to find * @return Subject for the token; null if the token does not exist; */ public Subject findToken(final String token) { final TokenInfo ti = findTokenInfo(token, System.currentTimeMillis()); return (ti != null ? ti.subject : null); } private TokenInfo findTokenInfo(final String token, final long now) { final int firstReuseMarker = token.indexOf(REUSE_TOKEN_MARKER); final String tokenAsRecorded = (isReusedToken(token) ? token.substring(0, firstReuseMarker) : token); final TokenInfo ti = liveTokens.get(tokenAsRecorded); if (ti == null) { logger.log(Level.WARNING, CULoggerInfo.useNonexistentToken, logger.isLoggable(Level.FINER) ? tokenAsRecorded : SUPPRESSED_TOKEN_OUTPUT); return null; } return (ti.isOKTouse(now) ? ti : null); } /** * Records the use of an authentication token by an admin request. *

* Just to make it easier for callers, the token value can have any number * of trailing reuse markers. This simplifies the code in RemoteAdminCommand * which actually sends two requests for each command: one to retrieve * metadata and one to execute the command. It might be that the command * itself might be reusing the token, in which case it will already have * appened a reuse marker to it. Then the code which sends the metadata * request can freely append the marker again without having to check if * it is already present. * * @param token the token consumed, with 0 or more cppies of the reuse marker appended * @return the Subject stored with the token when it was created; null if none was provided */ public Subject consumeToken(final String token) { Subject result = null; final long now = System.currentTimeMillis(); final TokenInfo ti = findTokenInfo(token, now); if (ti != null) { if (ti.use(isReusedToken(token), now)) { /* * We found the token info for this token and it is still valid, * so prepare to return the stored Subject. */ result = ti.subject; } } retireExpiredTokens(now); return result; } private boolean isReusedToken(final String token) { return token.indexOf(REUSE_TOKEN_MARKER) != -1; } public Subject subject(final String token) { final TokenInfo ti = liveTokens.get(token); return (ti != null) ? ti.subject : null; } public static String markTokenForReuse(final String token) { return token + REUSE_TOKEN_MARKER; } private synchronized void retireExpiredTokens(final long now) { for (Iterator> it = liveTokens.entrySet().iterator(); it.hasNext(); ) { final Map.Entry entry = it.next(); if (entry.getValue().isUsedUp(now)) { logger.log(Level.FINER, "Auth token {0} being retired during scan", entry.getValue().token); it.remove(); } } } /** * Convert the byte array to a hex string. */ private static String toHex(byte[] b) { char[] bc = new char[b.length * 2]; for (int i = 0, j = 0; i < b.length; i++) { byte bb = b[i]; bc[j++] = hex[(bb >> 4) & 0xF]; bc[j++] = hex[bb & 0xF]; } return new String(bc); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy