/* * The MIT License * * Copyright (c) 2011, CloudBees, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package jenkins.security; import hudson.Extension; import jenkins.util.SystemProperties; import hudson.Util; import hudson.model.Descriptor.FormException; import hudson.model.User; import hudson.model.UserProperty; import hudson.model.UserPropertyDescriptor; import hudson.security.ACL; import hudson.util.HttpResponses; import hudson.util.Secret; import jenkins.model.Jenkins; import net.sf.json.JSONObject; import org.jenkinsci.Symbol; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import java.io.IOException; import java.nio.charset.Charset; import java.security.MessageDigest; import java.security.SecureRandom; import javax.annotation.Nonnull; import org.apache.commons.lang.StringUtils; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; /** * Remembers the API token for this user, that can be used like a password to login. * * * @author Kohsuke Kawaguchi * @see ApiTokenFilter * @since 1.426 */ public class ApiTokenProperty extends UserProperty { private volatile Secret apiToken; /** * If enabled, shows API tokens to users with {@link Jenkins#ADMINISTER) permissions. * Disabled by default due to the security reasons. * If enabled, it restores the original Jenkins behavior (SECURITY-200). * @since TODO */ private static final boolean SHOW_TOKEN_TO_ADMINS = SystemProperties.getBoolean(ApiTokenProperty.class.getName() + ".showTokenToAdmins"); @DataBoundConstructor public ApiTokenProperty() { _changeApiToken(); } /** * We don't let the external code set the API token, * but for the initial value of the token we need to compute the seed by ourselves. */ /*package*/ ApiTokenProperty(String seed) { apiToken = Secret.fromString(seed); } /** * Gets the API token. * The method performs security checks. Only the current user and SYSTEM may see it. * Users with {@link Jenkins#ADMINISTER} may be allowed to do it using {@link #SHOW_TOKEN_TO_ADMINS}. * * @return API Token. Never null, but may be {@link Messages#ApiTokenProperty_ChangeToken_TokenIsHidden()} * if the user has no appropriate permissions. * @since TODO: the method performs security checks */ @Nonnull public String getApiToken() { return hasPermissionToSeeToken() ? getApiTokenInsecure() : Messages.ApiTokenProperty_ChangeToken_TokenIsHidden(); } @Nonnull @Restricted(NoExternalUse.class) /*package*/ String getApiTokenInsecure() { String p = apiToken.getPlainText(); if (p.equals(Util.getDigestOf(Jenkins.getInstance().getSecretKey()+":"+user.getId()))) { // if the current token is the initial value created by pre SECURITY-49 Jenkins, we can't use that. // force using the newer value apiToken = Secret.fromString(p=API_KEY_SEED.mac(user.getId())); } return Util.getDigestOf(p); } public boolean matchesPassword(String password) { String token = getApiTokenInsecure(); // String.equals isn't constant time, but this is return MessageDigest.isEqual(password.getBytes(Charset.forName("US-ASCII")), token.getBytes(Charset.forName("US-ASCII"))); } private boolean hasPermissionToSeeToken() { final Jenkins jenkins = Jenkins.getInstance(); // Administrators can do whatever they want if (SHOW_TOKEN_TO_ADMINS && jenkins.hasPermission(Jenkins.ADMINISTER)) { return true; } final User current = User.current(); if (current == null) { // Anonymous return false; } // SYSTEM user is always eligible to see tokens if (Jenkins.getAuthentication() == ACL.SYSTEM) { return true; } //TODO: replace by IdStrategy in newer Jenkins versions //return User.idStrategy().equals(user.getId(), current.getId()); return StringUtils.equals(user.getId(), current.getId()); } public void changeApiToken() throws IOException { user.checkPermission(Jenkins.ADMINISTER); _changeApiToken(); user.save(); } private void _changeApiToken() { byte[] random = new byte[16]; // 16x8=128bit worth of randomness, since we use md5 digest as the API token RANDOM.nextBytes(random); apiToken = Secret.fromString(Util.toHexString(random)); } @Override public UserProperty reconfigure(StaplerRequest req, JSONObject form) throws FormException { return this; } @Extension @Symbol("apiToken") public static final class DescriptorImpl extends UserPropertyDescriptor { public String getDisplayName() { return Messages.ApiTokenProperty_DisplayName(); } /** * When we are creating a default {@link ApiTokenProperty} for User, * we need to make sure it yields the same value for the same user, * because there's no guarantee that the property is saved. * * But we also need to make sure that an attacker won't be able to guess * the initial API token value. So we take the seed by hashing the secret + user ID. */ public ApiTokenProperty newInstance(User user) { return new ApiTokenProperty(API_KEY_SEED.mac(user.getId())); } public HttpResponse doChangeToken(@AncestorInPath User u, StaplerResponse rsp) throws IOException { ApiTokenProperty p = u.getProperty(ApiTokenProperty.class); if (p==null) { p = newInstance(u); u.addProperty(p); } else { p.changeApiToken(); } rsp.setHeader("script","document.getElementById('apiToken').value='"+p.getApiToken()+"'"); return HttpResponses.html(p.hasPermissionToSeeToken() ? Messages.ApiTokenProperty_ChangeToken_Success() : Messages.ApiTokenProperty_ChangeToken_SuccessHidden()); } } private static final SecureRandom RANDOM = new SecureRandom(); /** * We don't want an API key that's too long, so cut the length to 16 (which produces 32-letter MAC code in hexdump) */ private static final HMACConfidentialKey API_KEY_SEED = new HMACConfidentialKey(ApiTokenProperty.class,"seed",16); }