blob: a1f4df049eb66ef8bec352f0ee074ba1b6a20a2f [file] [log] [blame]
// Copyright 2013 Google Inc. All Rights Reserved.
//
// 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
//
// http://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 com.google.enterprise.adaptor.googleauthn;
import com.sun.net.httpserver.HttpContext;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.google.enterprise.adaptor.AbstractAdaptor;
import com.google.enterprise.adaptor.AdaptorContext;
import com.google.enterprise.adaptor.AuthnAdaptor;
import com.google.enterprise.adaptor.AuthnIdentity;
import com.google.enterprise.adaptor.Config;
import com.google.enterprise.adaptor.DocIdPusher;
import com.google.enterprise.adaptor.HttpExchanges;
import com.google.enterprise.adaptor.Request;
import com.google.enterprise.adaptor.Response;
import com.google.enterprise.adaptor.Session;
import com.google.gdata.client.authn.oauth.GoogleOAuthParameters;
import com.google.gdata.client.authn.oauth.OAuthException;
import com.google.gdata.client.authn.oauth.OAuthHmacSha1Signer;
import com.google.gdata.client.authn.oauth.OAuthParameters;
import com.google.gdata.client.authn.oauth.OAuthSigner;
import com.google.gdata.data.Link;
import com.google.gdata.util.AuthenticationException;
import com.google.gdata.util.ServiceException;
import com.google.gdata.client.appsforyourdomain.AppsGroupsService;
import com.google.gdata.data.appsforyourdomain.generic.GenericEntry;
import com.google.gdata.data.appsforyourdomain.generic.GenericFeed;
import org.openid4java.OpenIDException;
import org.openid4java.consumer.ConsumerManager;
import org.openid4java.consumer.VerificationResult;
import org.openid4java.discovery.DiscoveryException;
import org.openid4java.discovery.DiscoveryInformation;
import org.openid4java.message.AuthRequest;
import org.openid4java.message.Message;
import org.openid4java.message.MessageException;
import org.openid4java.message.ParameterList;
import org.openid4java.message.ax.AxMessage;
import org.openid4java.message.ax.FetchRequest;
import org.openid4java.message.ax.FetchResponse;
import java.io.IOException;
import java.net.*;
import java.nio.charset.Charset;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
/** Adaptor that authenticates users with Google. */
public class GoogleAuthnAdaptor extends AbstractAdaptor
implements AuthnAdaptor {
private static final String PROGRAM_NAME = "GoogleAuthnAdaptor/v0.1";
private static final String SESSION_DATA = "authndata";
private static final Logger log
= Logger.getLogger(GoogleAuthnAdaptor.class.getName());
private AdaptorContext context;
private HttpContext responseContext;
private List<DiscoveryInformation> discoveries;
private String consumerKey;
private String consumerSecret;
private String domain;
@Override
public void initConfig(Config config) {
config.addKey("google-authn.consumerKey", null);
config.addKey("google-authn.consumerSecret", null);
config.addKey("google-authn.domain", null);
}
@Override
public void init(AdaptorContext context) throws IOException {
this.context = context;
Config config = context.getConfig();
consumerKey = config.getValue("google-authn.consumerKey");
consumerSecret = context.getSensitiveValueDecoder().decodeValue(
config.getValue("google-authn.consumerSecret"));
domain = config.getValue("google-authn.domain");
log.log(Level.CONFIG, "google-authn.consumerKey: {0}", consumerKey);
log.log(Level.CONFIG, "google-authn.domain: {0}", domain);
try {
@SuppressWarnings("unchecked")
List<DiscoveryInformation> discoveries = new ConsumerManager()
.discover("https://www.google.com/accounts/o8/id");
this.discoveries = discoveries;
} catch (DiscoveryException ex) {
throw new IOException(ex);
}
if (discoveries.isEmpty()) {
throw new RuntimeException("Could not discover openid endpoint");
}
responseContext = context.createHttpContext(
"/google-response", new ResponseHandler());
}
@Override
public void destroy() {
discoveries = null;
responseContext.getServer().removeContext(responseContext);
responseContext = null;
}
@Override
public void authenticateUser(HttpExchange ex, Callback callback)
throws IOException {
ConsumerManager manager = new ConsumerManager();
DiscoveryInformation discovered = manager.associate(discoveries);
URI requestUri = HttpExchanges.getRequestUri(ex);
URI returnUri = requestUri.resolve(responseContext.getPath());
AuthRequest request;
try {
request = manager.authenticate(discovered, returnUri.toASCIIString());
} catch (OpenIDException e) {
log.log(Level.WARNING, "Authn failed: OpenIDException", e);
callback.userAuthenticated(ex, null);
return;
}
FetchRequest fetch = FetchRequest.createFetchRequest();
try {
fetch.addAttribute("email", "http://axschema.org/contact/email", true);
request.addExtension(fetch);
} catch (MessageException e) {
log.log(Level.WARNING, "Authn failed: MessageException", e);
callback.userAuthenticated(ex, null);
return;
}
Session session = context.getUserSession(ex, true);
session.setAttribute(SESSION_DATA,
new SessionData(manager, discovered, callback));
HttpExchanges.sendRedirect(ex, URI.create(request.getDestinationUrl(true)));
}
@Override
public void getDocIds(DocIdPusher pusher) {}
@Override
public void getDocContent(Request request, Response response)
throws IOException {
response.respondNotFound();
}
public static void main(String[] args) throws Exception {
AbstractAdaptor.main(new GoogleAuthnAdaptor(), args);
}
private static Map<String, String[]> convertParameterListsToArrays(
Map<String, List<String>> params) {
Map<String, String[]> newMap = new HashMap<String, String[]>();
String[] zeroArray = new String[0];
for (Map.Entry<String, List<String>> me : params.entrySet()) {
newMap.put(me.getKey(), me.getValue().toArray(zeroArray));
}
return newMap;
}
private List<String> getAllGroups(String username) throws IOException {
// Username known to be valid and trusted.
String userDomain = username.split("@", 2)[1];
AppsGroupsService groupService;
try {
groupService = new AppsGroupsService(userDomain, PROGRAM_NAME);
} catch (AuthenticationException ex) {
throw new IOException("Failed to create groups service", ex);
}
GoogleOAuthParameters oauthParameters = getOAuthParameters();
try {
groupService.setOAuthCredentials(oauthParameters, getOAuthSigner());
} catch (OAuthException e) {
throw new IOException("Failed to set provisioning credentials", e);
}
try {
log.log(Level.FINE, "Getting group entries for {0}", username);
ArrayList<String> groups = new ArrayList<String>();
GenericFeed groupsFeed = groupService.retrieveGroups(username, false);
while (groupsFeed != null) {
for (GenericEntry entry : groupsFeed.getEntries()) {
// Normalize to always lower case (even though we haven't seen
// anything other than lower case).
groups.add(entry.getProperty("groupId").toLowerCase(Locale.ENGLISH));
}
Link nextPage = groupsFeed.getNextLink();
if (nextPage == null) {
groupsFeed = null;
} else {
groupsFeed = groupService.getFeed(
new URL(nextPage.getHref()), GenericFeed.class);
}
}
log.log(Level.FINE, "group count: {0}", groups.size());
log.log(Level.FINER, "all groups: {0}", groups);
return groups;
} catch (ServiceException se) {
throw new IOException("failed to get groups", se);
}
}
private GoogleOAuthParameters getOAuthParameters() {
GoogleOAuthParameters oauthParameters = new GoogleOAuthParameters();
oauthParameters.setOAuthConsumerKey(consumerKey);
oauthParameters.setOAuthConsumerSecret(consumerSecret);
oauthParameters.setOAuthType(OAuthParameters.OAuthType.TWO_LEGGED_OAUTH);
return oauthParameters;
}
private static OAuthSigner getOAuthSigner() {
return new OAuthHmacSha1Signer();
}
private static class SessionData {
private final ConsumerManager manager;
private final DiscoveryInformation discovered;
private final Callback callback;
public SessionData(ConsumerManager manager, DiscoveryInformation discovered,
Callback callback) {
this.manager = manager;
this.discovered = discovered;
this.callback = callback;
}
}
private class ResponseHandler implements HttpHandler {
@Override
public void handle(HttpExchange ex) throws IOException {
Session session = context.getUserSession(ex, false);
if (session == null) {
log.log(Level.WARNING, "Authn failed: Could not find user's session");
// TODO(ejona): Translate.
HttpExchanges.respond(ex, HttpURLConnection.HTTP_INTERNAL_ERROR,
"text/plain",
"Could not find user's session".getBytes(Charset.forName("UTF-8")));
return;
}
SessionData sessionData
= (SessionData) session.removeAttribute(SESSION_DATA);
if (sessionData == null) {
log.log(Level.WARNING, "Authn failed: Could not find session data");
// TODO(ejona): Translate.
HttpExchanges.respond(ex, HttpURLConnection.HTTP_INTERNAL_ERROR,
"text/plain",
"Could not find session data".getBytes(Charset.forName("UTF-8")));
return;
}
ConsumerManager manager = sessionData.manager;
DiscoveryInformation discovered = sessionData.discovered;
Callback callback = sessionData.callback;
URI requestUri = HttpExchanges.getRequestUri(ex);
@SuppressWarnings("unchecked")
Map<String, List<String>> params
= manager.extractQueryParams(requestUri.toURL());
ParameterList openidResp = new ParameterList(
convertParameterListsToArrays(params));
// TODO(ejona): compute requestUri directly from the exchange
VerificationResult verification;
try {
verification = manager.verify(
requestUri.toASCIIString(), openidResp, discovered);
} catch (OpenIDException e) {
log.log(Level.WARNING, "Authn failed: OpenIDException", e);
callback.userAuthenticated(ex, null);
return;
}
if (verification.getVerifiedId() == null) {
if (Message.OPENID2_NS.equals(verification.getAuthResponse()
.getParameterValue("openid.ns"))
&& Message.MODE_CANCEL.equals(verification.getAuthResponse()
.getParameterValue("openid.mode"))) {
log.log(Level.WARNING, "Authn failed: user canceled");
} else {
log.log(Level.WARNING, "Authn failed: verification failed");
}
callback.userAuthenticated(ex, null);
return;
}
Message response = verification.getAuthResponse();
FetchResponse ax;
try {
ax = (FetchResponse) response.getExtension(AxMessage.OPENID_NS_AX);
} catch (MessageException e) {
log.log(Level.WARNING, "Authn failed: MessageException", e);
callback.userAuthenticated(ex, null);
return;
}
if (ax == null) {
log.log(Level.WARNING, "Authn failed: No ax extension");
callback.userAuthenticated(ex, null);
return;
}
final String email = ax.getAttributeValue("email");
if (email == null) {
log.log(Level.WARNING, "Authn failed: No email attribute");
callback.userAuthenticated(ex, null);
return;
}
log.log(Level.FINE, "User {0} authenticated", email);
String[] parts = email.split("@", 2);
if (parts.length != 2) {
log.log(Level.WARNING,
"Authn failed: Could not determine user's domain: {0}", email);
callback.userAuthenticated(ex, null);
return;
}
if (!domain.equals(parts[1])) {
log.log(Level.WARNING,
"Authn failed: User {0} has domain {1} which is not the expected "
+ "domain {2}", new Object[] {email, parts[1], domain});
callback.userAuthenticated(ex, null);
return;
}
final Set<String> groups;
try {
groups = Collections.unmodifiableSet(
new HashSet<String>(getAllGroups(email)));
} catch (IOException e) {
log.log(Level.WARNING, "Authn failed: Error getting groups", e);
callback.userAuthenticated(ex, null);
return;
}
AuthnIdentity identity = new AuthnIdentity() {
@Override
public String getUsername() {
return email;
}
@Override
public String getPassword() {
return null;
}
@Override
public Set<String> getGroups() {
return groups;
}
};
callback.userAuthenticated(ex, identity);
}
}
}