/******************************************************************************* * * Copyright (c) 2012, CloudBees, Inc. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * * Kohsuke Kawaguchi, Winston Prakash * *******************************************************************************/ /* * The MIT License * * Copyright (c) 2010, 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 hudson.security; import hudson.ExtensionList; import hudson.ExtensionPoint; import hudson.model.Hudson; import hudson.model.User; import hudson.model.UserProperty; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import javax.servlet.ServletException; import java.io.IOException; import java.io.Serializable; import org.eclipse.hudson.security.HudsonSecurityEntitiesHolder; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; /** * Abstraction for a login mechanism through external authenticator/identity * provider (instead of username/password.) * * <p> This extension point adds additional login mechanism for * {@link SecurityRealm}s that authenticate the user via username/password * (which typically extends from {@link AbstractPasswordBasedSecurityRealm}.) * The intended use case is protocols like OpenID, OAuth, and other SSO-like * services. * * <p> The basic abstraction is that: * * <ul> <li> The user can have (possibly multiple, possibly zero) opaque strings * to their {@linkplain User} object. Such opaque strings are called * "identifiers." Think of them as OpenID URLs, twitter account names, etc. * Identifiers are only comparable within the same {@link FederatedLoginService} * implementation. * * <li> After getting authenticated by some means, the user can add additional * identifiers to their account. Your implementation would do protocol specific * thing to verify that the user indeed owns the claimed identifier, create a * {@link FederatedIdentity} instance, then call * {@link FederatedIdentity#addToCurrentUser()} to record such association. * * <li> In the login page, instead of entering the username and password, the * user opts for authenticating via other services. Think of OpenID, OAuth, your * corporate SSO service, etc. The user proves (by your protocol specific way) * that they own some identifier, then create a {@link FederatedIdentity} * instance, and invoke {@link FederatedIdentity#signin()} to sign in that user. * * </ul> * * * <h2>Views</h2> <dl> <dt>loginFragment.jelly <dd> Injected into the login form * page, after the default "login" button but before the "create account" link. * Use this to generate a button or a link so that the user can initiate login * via your federated login service. </dl> * * <h2>URL Binding</h2> <p> Each {@link FederatedLoginService} is exposed to the * URL space via {@link Hudson#getFederatedLoginService(String)}. So for example * if your {@linkplain #getUrlName() url name} is "openid", this object gets * "/federatedLoginService/openid" as the URL. * * @author Kohsuke Kawaguchi * @since 1.394 */ public abstract class FederatedLoginService implements ExtensionPoint { /** * Returns the url name that determines where this * {@link FederatedLoginService} is mapped to in the URL space. * * <p> The object is bound to /federatedLoginService/URLNAME/. The url name * needs to be unique among all {@link FederatedLoginService}s. */ public abstract String getUrlName(); /** * Returns your implementation of {@link FederatedLoginServiceUserProperty} * that stores opaque identifiers. */ public abstract Class<? extends FederatedLoginServiceUserProperty> getUserPropertyClass(); /** * Identity information as obtained from {@link FederatedLoginService}. * Although it is discouraged to implement {@link Serializable} by an inner * class, it is too late to change it to <code>static</code>. */ public abstract class FederatedIdentity implements Serializable { private static final long serialVersionUID = 1L; /** * Gets the string representation of the identity in the form that makes * sense to the enclosing {@link FederatedLoginService}, such as full * OpenID URL. * * @return must not be null. */ public abstract String getIdentifier(); /** * Gets a short ID of this user, as a suitable candidate for * {@link User#getId()}. This should be Unix username like token. * * @return null if this information is not available. */ public abstract String getNickname(); /** * Gets a human readable full name of this user. Maps to * {@link User#getDisplayName()} * * @return null if this information is not available. */ public abstract String getFullName(); /** * Gets the e-mail address of this user, like "abc@def.com" * * @return null if this information is not available. */ public abstract String getEmailAddress(); /** * Returns a human-readable pronoun that describes this kind of * identifier. This is used for rendering UI. For example, "OpenID", * "Twitter ID", etc. */ public abstract String getPronoun(); /** * Locates the user who owns this identifier. */ public final User locateUser() { Class<? extends FederatedLoginServiceUserProperty> pt = getUserPropertyClass(); String id = getIdentifier(); for (User u : User.getAll()) { if (u.getProperty(pt).has(id)) { return u; } } return null; } /** * Call this method to authenticate the user when you confirmed (via * your protocol specific work) that the current HTTP request indeed * owns this identifier. * * <p> This method will locate the user who owns this identifier, * associate the credential with the current session. IOW, it signs in * the user. * * @throws UnclaimedIdentityException If this identifier is not claimed * by anyone. If you just let this exception propagate to the caller of * your "doXyz" method, it will either render an error page or initiate * a user registration session (provided that {@link SecurityRealm} * supports that.) */ public User signin() throws UnclaimedIdentityException { User u = locateUser(); if (u != null) { // login as this user UserDetails d = HudsonSecurityEntitiesHolder.getHudsonSecurityManager().getSecurityRealm().loadUserByUsername(u.getId()); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(d, "", d.getAuthorities()); token.setDetails(d); SecurityContextHolder.getContext().setAuthentication(token); return u; } else { // Unassociated identity. throw new UnclaimedIdentityException(this); } } /** * Your implementation will call this method to add this identifier to * the current user of an already authenticated session. * * <p> This method will record the identifier in * {@link FederatedLoginServiceUserProperty} so that in the future the * user can login to Hudson with the identifier. */ public void addToCurrentUser() throws IOException { User u = User.current(); if (u == null) { throw new IllegalStateException("Current request is unauthenticated"); } addTo(u); } /** * Adds this identity to the specified user. */ public void addTo(User u) throws IOException { FederatedLoginServiceUserProperty p = u.getProperty(getUserPropertyClass()); if (p == null) { p = (FederatedLoginServiceUserProperty) UserProperty.all().find(getUserPropertyClass()).newInstance(u); u.addProperty(p); } p.addIdentifier(getIdentifier()); } @Override public String toString() { return getIdentifier(); } } /** * Used in {@link FederatedIdentity#signin()} to indicate that the * identifier is not currently associated with anyone. */ public static class UnclaimedIdentityException extends RuntimeException implements HttpResponse { //TODO: review and check whether we can do it private public final FederatedIdentity identity; public UnclaimedIdentityException(FederatedIdentity identity) { this.identity = identity; } public FederatedIdentity getIdentity() { return identity; } public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException { SecurityRealm sr = HudsonSecurityEntitiesHolder.getHudsonSecurityManager().getSecurityRealm(); if (sr.allowsSignup()) { try { sr.commenceSignup(identity).generateResponse(req, rsp, node); return; } catch (UnsupportedOperationException e) { // fall through } } // this security realm doesn't support user registration. // just report an error req.getView(this, "error").forward(req, rsp); } } public static ExtensionList<FederatedLoginService> all() { return Hudson.getInstance().getExtensionList(FederatedLoginService.class); } }