Skip to content
This repository has been archived by the owner on Nov 9, 2017. It is now read-only.

Commit

Permalink
ZNTA-942 make Zanata server an OAuth 2.0 AS and RS
Browse files Browse the repository at this point in the history
Zanata server now can act as a AS(Authorization Server) and itself being
a RS (Resource Server). It uses authorization code, access token and
refresh token.

This implementation is still a WIP because client id and refresh token
are not persisted to the database. Access token authorization is
temporarily enabled for just one REST resource.
  • Loading branch information
Patrick Huang committed May 22, 2016
1 parent def2e56 commit d0a8b04
Show file tree
Hide file tree
Showing 19 changed files with 1,196 additions and 8 deletions.
11 changes: 11 additions & 0 deletions zanata-war/pom.xml
Expand Up @@ -2255,6 +2255,17 @@
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.apache.oltu.oauth2</groupId>
<artifactId>org.apache.oltu.oauth2.authzserver</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.oltu.oauth2</groupId>
<artifactId>org.apache.oltu.oauth2.resourceserver</artifactId>
<version>1.0.1</version>
</dependency>

<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
Expand Down
17 changes: 17 additions & 0 deletions zanata-war/src/main/java/org/zanata/ApplicationConfiguration.java
Expand Up @@ -29,6 +29,7 @@
import java.util.Set;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.context.Dependent;
import javax.enterprise.event.Observes;
import javax.enterprise.event.TransactionPhase;
import javax.enterprise.inject.Produces;
Expand All @@ -47,6 +48,7 @@
import javax.servlet.http.HttpSession;

import org.zanata.config.SystemPropertyConfigStore;
import org.zanata.security.annotations.AuthType;
import org.zanata.servlet.HttpRequestAndSessionHolder;
import org.zanata.servlet.annotations.ServerPath;
import org.zanata.util.DefaultLocale;
Expand Down Expand Up @@ -313,6 +315,21 @@ public boolean isSingleOpenIdProvider() {
return openIdProvider.isPresent();
}

@Produces
@AuthType
@Dependent
protected AuthenticationType authenticationType() {
if (isInternalAuth()) {
return AuthenticationType.INTERNAL;
} else if (isJaasAuth()) {
return AuthenticationType.JAAS;
} else if (isKerberosAuth()) {
return AuthenticationType.KERBEROS;
}
throw new RuntimeException(
"only supports internal, jaas, or kerberos authentication");
}

public String getOpenIdProviderUrl() {
return openIdProvider.orNull();
}
Expand Down
14 changes: 7 additions & 7 deletions zanata-war/src/main/java/org/zanata/action/LoginAction.java
Expand Up @@ -32,12 +32,14 @@

import javax.inject.Inject;
import javax.inject.Named;

import org.zanata.ApplicationConfiguration;
import org.zanata.security.AuthenticationManager;
import org.zanata.security.AuthenticationType;
import org.zanata.security.UserRedirectBean;
import org.zanata.security.ZanataCredentials;
import org.zanata.security.ZanataIdentity;
import org.zanata.security.annotations.AuthType;
import org.zanata.security.openid.FedoraOpenIdProvider;
import org.zanata.security.openid.GoogleOpenIdProvider;
import org.zanata.security.openid.OpenIdProviderType;
Expand Down Expand Up @@ -84,16 +86,14 @@ public class LoginAction implements Serializable {
@Inject
private UserRedirectBean userRedirect;

@Inject
@AuthType
private AuthenticationType authenticationType;

public String login() {
credentials.setUsername(username);
credentials.setPassword(password);
if (applicationConfiguration.isInternalAuth()) {
credentials.setAuthType(AuthenticationType.INTERNAL);
} else if (applicationConfiguration.isJaasAuth()) {
credentials.setAuthType(AuthenticationType.JAAS);
} else if (applicationConfiguration.isKerberosAuth()) {
credentials.setAuthType(AuthenticationType.KERBEROS);
}
credentials.setAuthType(authenticationType);

String loginResult;

Expand Down
119 changes: 119 additions & 0 deletions zanata-war/src/main/java/org/zanata/dao/AuthorizationCodeDAO.java
@@ -0,0 +1,119 @@
/*
* Copyright 2016, Red Hat, Inc. and individual contributors as indicated by the
* @author tags. See the copyright.txt file in the distribution for a full
* listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This software is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this software; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF
* site: http://www.fsf.org.
*/

package org.zanata.dao;

import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import javax.enterprise.context.RequestScoped;

import org.hibernate.Session;
import org.zanata.model.HAccount;
import com.google.common.base.Throwables;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Sets;

/**
* @author Patrick Huang <a href="mailto:pahuang@redhat.com">pahuang@redhat.com</a>
*/
@RequestScoped
public class AuthorizationCodeDAO extends AbstractDAOImpl<HAccount, Long> {
// FIXME do the real database
private static Cache<String, Set<String>> usernameToClientIds =
CacheBuilder.newBuilder().build();
// FIXME persist in the database
private static Set<ValidRefreshCode> validRefreshCodes = Sets.newHashSet();


public AuthorizationCodeDAO(
Session session) {
super(HAccount.class, session);
}

public AuthorizationCodeDAO() {
super(HAccount.class);
}

// TODO support client secret as well?
public void persistClientId(String username, String clientId) {
try {
Set<String> clientIds =
usernameToClientIds.get(username, Sets::<String>newHashSet);
clientIds.add(clientId);
} catch (ExecutionException e) {
throw Throwables.propagate(e);
}
}

public Optional<HAccount> getClientIdAuthorizer(String clientId) {
Optional<Map.Entry<String, Set<String>>> any =
usernameToClientIds.asMap().entrySet().stream()
.filter(entry -> entry.getValue().contains(clientId))
.findAny();

if (any.isPresent()) {
return any.flatMap(entry -> {
HAccount hAccount = new HAccount();
hAccount.setUsername(entry.getKey());
return Optional.of(hAccount);
});
}
return Optional.empty();
}

// e.g.
// HAccount (1) -> (n) client id (1) -> (1) refresh token
// potentially we may define scope later so different refresh may reference different scope combination
// explanation of access token and refresh token
// http://stackoverflow.com/questions/3487991/why-does-oauth-v2-have-both-access-and-refresh-tokens
// and this answer: http://stackoverflow.com/a/12885823
public void persistRefreshToken(HAccount hAccount, String clientId,
String refreshToken) {
ValidRefreshCode validRefreshCode =
new ValidRefreshCode(clientId,
refreshToken, hAccount.getUsername());
validRefreshCodes.add(validRefreshCode);
}

public Optional<String> getUsernameFromClientIdAndFreshToken(String clientId,
String refreshToken) {
Optional<ValidRefreshCode> first = validRefreshCodes.stream()
.filter(entry -> entry.clientId.equals(clientId) &&
entry.refreshToken.equals(refreshToken)).findFirst();
return first.flatMap(thing -> Optional.of(thing.username));
}

private static class ValidRefreshCode {
private String clientId;
private String refreshToken;
private String username;

public ValidRefreshCode(String clientId,
String refreshToken, String username) {
this.clientId = clientId;
this.refreshToken = refreshToken;
this.username = username;
}
}
}
181 changes: 181 additions & 0 deletions zanata-war/src/main/java/org/zanata/rest/oauth/AuthorizedResource.java
@@ -0,0 +1,181 @@
/*
* Copyright 2016, Red Hat, Inc. and individual contributors as indicated by the
* @author tags. See the copyright.txt file in the distribution for a full
* listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This software is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this software; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF
* site: http://www.fsf.org.
*/

package org.zanata.rest.oauth;

import java.lang.reflect.Type;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.apache.deltaspike.jpa.api.transaction.Transactional;
import org.apache.oltu.oauth2.common.OAuth;
import org.apache.oltu.oauth2.common.exception.OAuthProblemException;
import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
import org.apache.oltu.oauth2.common.message.OAuthResponse;
import org.apache.oltu.oauth2.common.message.types.ParameterStyle;
import org.apache.oltu.oauth2.rs.request.OAuthAccessResourceRequest;
import org.apache.oltu.oauth2.rs.response.OAuthRSResponse;
import org.jboss.resteasy.util.GenericType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zanata.dao.AccountDAO;
import org.zanata.dao.ProjectDAO;
import org.zanata.model.HAccount;
import org.zanata.model.HProject;
import org.zanata.rest.dto.Account;
import org.zanata.rest.dto.Project;
import org.zanata.rest.service.AccountService;
import org.zanata.rest.service.ProjectService;
import org.zanata.security.oauth.SecurityTokens;
import com.google.common.base.Strings;
import com.googlecode.totallylazy.Either;
import lombok.extern.slf4j.Slf4j;

/**
* @author Patrick Huang <a href="mailto:pahuang@redhat.com">pahuang@redhat.com</a>
*/
@RequestScoped
@Path("/oauth/authorized")
@Slf4j
@Transactional
public class AuthorizedResource {

@Inject
private SecurityTokens securityTokens;

@Context
private HttpServletRequest request;

@Inject
private ProjectDAO projectDAO;

@Inject
private AccountDAO accountDAO;


@Path("/projects")
@Produces("application/json")
@GET
public Response maintainedProjects() {
Either<String, Response> usernameOrResponse = getUsernameOrResponse();
if (usernameOrResponse.isRight()) {
return usernameOrResponse.right();
}
String username = usernameOrResponse.left();

HAccount account = accountDAO.getByUsername(username);
// TODO pahuang this is not doing paging properly and can potentially return large data set
List<HProject> maintainedProjects = projectDAO
.getProjectsForMaintainer(account.getPerson(), null, 0, 1000);

List<Project> projects = maintainedProjects.stream()
.map(hProject -> ProjectService.toResource(hProject,
MediaType.APPLICATION_JSON_TYPE))
.collect(Collectors.toList());
Type genericType = new GenericType<List<Project>>() {
}.getGenericType();
return Response.ok(new GenericEntity<>(projects, genericType)).build();
}

@Path("/myaccount")
@Produces("application/json")
@GET
public Response accountDetail() {
Either<String, Response> usernameOrResponse = getUsernameOrResponse();
if (usernameOrResponse.isRight()) {
return usernameOrResponse.right();
}
String username = usernameOrResponse.left();

HAccount account = accountDAO.getByUsername(username);
if (Strings.isNullOrEmpty(account.getApiKey())) {
accountDAO.createApiKey(account);
}
Account dto = new Account();
AccountService.transfer(account, dto);
return Response.ok(dto).build();
}

// TODO use annotation to handle all this security
private Either<String, Response> getUsernameOrResponse() {
try {
String accessToken = getAccessToken();
Optional<String> usernameOpt = checkAccess(accessToken);
if (!usernameOpt.isPresent()) {
if (securityTokens.isTokenExpired(accessToken)) {
return Either.right(buildUnauthorizedResponse("access token expired"));
}
return Either.right(buildUnauthorizedResponse("invalid access code"));
}
String username = usernameOpt.get();
return Either.left(username);

} catch (OAuthProblemException e) {
return Either.right(buildUnauthorizedResponse(e.getMessage()));
} catch (OAuthSystemException e) {
return Either.right(buildServerErrorResponse(e.getMessage()));
}
}

private Response buildUnauthorizedResponse(String message) {
OAuthResponse oauthResponse = null;
try {
oauthResponse = OAuthRSResponse
.errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
.buildHeaderMessage();
} catch (OAuthSystemException e1) {
return buildServerErrorResponse(message);
}

return Response
.status(oauthResponse.getResponseStatus()).header(OAuth.HeaderType.WWW_AUTHENTICATE, oauthResponse.getHeader(
OAuth.HeaderType.WWW_AUTHENTICATE)).build();
}

private Response buildServerErrorResponse(String message) {
return Response.serverError().entity(message).build();
}

private Optional<String> checkAccess(String accessToken) {
return securityTokens.matchAccessToken(accessToken);
}

private String getAccessToken() throws OAuthSystemException, OAuthProblemException {
// Make the OAuth Request out of this request and validate it
// Specify where you expect OAuth access token (request header, body or query string)
OAuthAccessResourceRequest oauthRequest = new
OAuthAccessResourceRequest(request, ParameterStyle.HEADER);
return oauthRequest.getAccessToken();
}

}

0 comments on commit d0a8b04

Please sign in to comment.