This repository has been archived by the owner on Nov 9, 2017. It is now read-only.
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ZNTA-942 make Zanata server an OAuth 2.0 AS and RS
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
Showing
19 changed files
with
1,196 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
119 changes: 119 additions & 0 deletions
119
zanata-war/src/main/java/org/zanata/dao/AuthorizationCodeDAO.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
181
zanata-war/src/main/java/org/zanata/rest/oauth/AuthorizedResource.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} | ||
|
||
} |
Oops, something went wrong.