/*******************************************************************************
*
* 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);
}
}