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

org.springframework.security.core.token.KeyBasedPersistenceTokenService Maven / Gradle / Ivy

There is a newer version: 6.2.4
Show newest version
/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.security.core.token;

import java.security.SecureRandom;
import java.util.Base64;
import java.util.Date;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.crypto.codec.Hex;
import org.springframework.security.crypto.codec.Utf8;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * Basic implementation of {@link TokenService} that is compatible with clusters and
 * across machine restarts, without requiring database persistence.
 *
 * 

* Keys are produced in the format: *

* *

* Base64(creationTime + ":" + hex(pseudoRandomNumber) + ":" + extendedInformation + ":" + * Sha512Hex(creationTime + ":" + hex(pseudoRandomNumber) + ":" + extendedInformation + * ":" + serverSecret) ) *

* *

* In the above, creationTime, tokenKey and * extendedInformation are equal to that stored in {@link Token}. The * Sha512Hex includes the same payload, plus a serverSecret. *

* *

* The serverSecret varies every millisecond. It relies on two static * server-side secrets. The first is a password, and the second is a server integer. Both * of these must remain the same for any issued keys to subsequently be recognised. The * applicable serverSecret in any millisecond is computed by * password + ":" + (creationTime % serverInteger). * This approach further obfuscates the actual server secret and renders attempts to * compute the server secret more limited in usefulness (as any false tokens would be * forced to have a creationTime equal to the computed hash). Recall that * framework features depending on token services should reject tokens that are relatively * old in any event. *

* *

* A further consideration of this class is the requirement for cryptographically strong * pseudo-random numbers. To this end, the use of {@link SecureRandomFactoryBean} is * recommended to inject the property. *

* *

* This implementation uses UTF-8 encoding internally for string manipulation. *

* * @author Ben Alex * */ public class KeyBasedPersistenceTokenService implements TokenService, InitializingBean { private int pseudoRandomNumberBytes = 32; private String serverSecret; private Integer serverInteger; private SecureRandom secureRandom; @Override public Token allocateToken(String extendedInformation) { Assert.notNull(extendedInformation, "Must provided non-null extendedInformation (but it can be empty)"); long creationTime = new Date().getTime(); String serverSecret = computeServerSecretApplicableAt(creationTime); String pseudoRandomNumber = generatePseudoRandomNumber(); String content = creationTime + ":" + pseudoRandomNumber + ":" + extendedInformation; String key = computeKey(serverSecret, content); return new DefaultToken(key, creationTime, extendedInformation); } private String computeKey(String serverSecret, String content) { String sha512Hex = Sha512DigestUtils.shaHex(content + ":" + serverSecret); String keyPayload = content + ":" + sha512Hex; return Utf8.decode(Base64.getEncoder().encode(Utf8.encode(keyPayload))); } @Override public Token verifyToken(String key) { if (key == null || "".equals(key)) { return null; } String[] tokens = StringUtils .delimitedListToStringArray(Utf8.decode(Base64.getDecoder().decode(Utf8.encode(key))), ":"); Assert.isTrue(tokens.length >= 4, () -> "Expected 4 or more tokens but found " + tokens.length); long creationTime; try { creationTime = Long.decode(tokens[0]); } catch (NumberFormatException ex) { throw new IllegalArgumentException("Expected number but found " + tokens[0]); } String serverSecret = computeServerSecretApplicableAt(creationTime); String pseudoRandomNumber = tokens[1]; // Permit extendedInfo to itself contain ":" characters StringBuilder extendedInfo = new StringBuilder(); for (int i = 2; i < tokens.length - 1; i++) { if (i > 2) { extendedInfo.append(":"); } extendedInfo.append(tokens[i]); } String sha1Hex = tokens[tokens.length - 1]; // Verification String content = creationTime + ":" + pseudoRandomNumber + ":" + extendedInfo.toString(); String expectedSha512Hex = Sha512DigestUtils.shaHex(content + ":" + serverSecret); Assert.isTrue(expectedSha512Hex.equals(sha1Hex), "Key verification failure"); return new DefaultToken(key, creationTime, extendedInfo.toString()); } /** * @return a pseduo random number (hex encoded) */ private String generatePseudoRandomNumber() { byte[] randomBytes = new byte[this.pseudoRandomNumberBytes]; this.secureRandom.nextBytes(randomBytes); return new String(Hex.encode(randomBytes)); } private String computeServerSecretApplicableAt(long time) { return this.serverSecret + ":" + new Long(time % this.serverInteger).intValue(); } /** * @param serverSecret the new secret, which can contain a ":" if desired (never being * sent to the client) */ public void setServerSecret(String serverSecret) { this.serverSecret = serverSecret; } public void setSecureRandom(SecureRandom secureRandom) { this.secureRandom = secureRandom; } /** * @param pseudoRandomNumberBytes changes the number of bytes issued (must be >= 0; * defaults to 256) */ public void setPseudoRandomNumberBytes(int pseudoRandomNumberBytes) { Assert.isTrue(pseudoRandomNumberBytes >= 0, "Must have a positive pseudo random number bit size"); this.pseudoRandomNumberBytes = pseudoRandomNumberBytes; } public void setServerInteger(Integer serverInteger) { this.serverInteger = serverInteger; } @Override public void afterPropertiesSet() { Assert.hasText(this.serverSecret, "Server secret required"); Assert.notNull(this.serverInteger, "Server integer required"); Assert.notNull(this.secureRandom, "SecureRandom instance required"); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy