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: 7.2024.1.Alpha1
Show newest version
/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright (c) 2010-2012 Oracle and/or its affiliates. All rights reserved.
 * 
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 * 
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 * 
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 * 
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */
// Portions Copyright [2019-2021] Payara Foundation and/or affiliates

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 static final int TOKEN_SIZE = 10; private static final long DEFAULT_TOKEN_LIFETIME = 60 * 1000; private static final long TOKEN_EXPIRATION_IN_MS = 360 * 1000; private final SecureRandom rng = new SecureRandom(); private final Map liveTokens = new HashMap(); private static final Logger logger = CULoggerInfo.getLogger(); private static final 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}", 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, 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 && 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