/* * 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.acegisecurity.context.SecurityContextHolder; import org.acegisecurity.providers.UsernamePasswordAuthenticationToken; import org.acegisecurity.userdetails.UserDetails; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import javax.servlet.ServletException; import java.io.IOException; /** * 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}. */ public abstract class FederatedIdentity { /** * 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 = Hudson.getInstance().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 = Hudson.getInstance().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); } }