diff options
Diffstat (limited to 'odl-aaa-moon/aaa/aaa-authn-sts/src')
11 files changed, 1154 insertions, 0 deletions
diff --git a/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/Activator.java b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/Activator.java new file mode 100644 index 00000000..1bf4591d --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/Activator.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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 + */ + +package org.opendaylight.aaa.sts; + +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; +import com.google.common.collect.Lists; +import java.util.List; +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.opendaylight.aaa.api.AuthenticationService; +import org.opendaylight.aaa.api.ClaimAuth; +import org.opendaylight.aaa.api.ClientService; +import org.opendaylight.aaa.api.CredentialAuth; +import org.opendaylight.aaa.api.IdMService; +import org.opendaylight.aaa.api.TokenAuth; +import org.opendaylight.aaa.api.TokenStore; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An activator for the secure token server to inject in a + * {@link CredentialAuth} implementation. + * + * @author liemmn + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class Activator extends DependencyActivatorBase { + + private static final Logger LOG = LoggerFactory.getLogger(Activator.class); + + // Definition of several methods called in the ServiceLocator through + // Reflection + private static final String AUTHENTICATION_SERVICE_REMOVED = "authenticationServiceRemoved"; + private static final String AUTHENTICATION_SERVICE_ADDED = "authenticationServiceAdded"; + private static final String TOKEN_STORE_REMOVED = "tokenStoreRemoved"; + private static final String TOKEN_STORE_ADDED = "tokenStoreAdded"; + private static final String TOKEN_AUTH_REMOVED = "tokenAuthRemoved"; + private static final String TOKEN_AUTH_ADDED = "tokenAuthAdded"; + private static final String CLAIM_AUTH_REMOVED = "claimAuthRemoved"; + private static final String CLAIM_AUTH_ADDED = "claimAuthAdded"; + private static final String CREDENTIAL_AUTH_REMOVED = "credentialAuthRemoved"; + private static final String CREDENTIAL_AUTH_ADDED = "credentialAuthAdded"; + + // A collection of all services, which is used for closing ServiceTrackers + private ImmutableList<ServiceTracker<?, ?>> services; + + @Override + public void init(BundleContext context, DependencyManager manager) throws Exception { + + LOG.info("STS Activator initializing"); + manager.add(createComponent().setImplementation(ServiceLocator.getInstance()) + .add(createServiceDependency().setService(CredentialAuth.class) + .setRequired(true) + .setCallbacks( + CREDENTIAL_AUTH_ADDED, + CREDENTIAL_AUTH_REMOVED)) + .add(createServiceDependency().setService(ClaimAuth.class) + .setRequired(false) + .setCallbacks(CLAIM_AUTH_ADDED, + CLAIM_AUTH_REMOVED)) + .add(createServiceDependency().setService(TokenAuth.class) + .setRequired(false) + .setCallbacks(TOKEN_AUTH_ADDED, + TOKEN_AUTH_REMOVED)) + .add(createServiceDependency().setService(TokenStore.class) + .setRequired(true) + .setCallbacks(TOKEN_STORE_ADDED, + TOKEN_STORE_REMOVED)) + .add(createServiceDependency().setService(TokenStore.class) + .setRequired(true)) + .add(createServiceDependency().setService( + AuthenticationService.class) + .setRequired(true) + .setCallbacks( + AUTHENTICATION_SERVICE_ADDED, + AUTHENTICATION_SERVICE_REMOVED)) + .add(createServiceDependency().setService(IdMService.class) + .setRequired(true)) + .add(createServiceDependency().setService(ClientService.class) + .setRequired(true))); + + final Builder<ServiceTracker<?, ?>> servicesBuilder = new ImmutableList.Builder<ServiceTracker<?, ?>>(); + + // Async ServiceTrackers to track and load AAA STS bundles + final ServiceTracker<AuthenticationService, AuthenticationService> authenticationService = new ServiceTracker<>( + context, AuthenticationService.class, + new AAAServiceTrackerCustomizer<AuthenticationService>( + new Function<AuthenticationService, Void>() { + @Override + public Void apply(AuthenticationService authenticationService) { + ServiceLocator.getInstance().setAuthenticationService( + authenticationService); + return null; + } + })); + servicesBuilder.add(authenticationService); + authenticationService.open(); + + final ServiceTracker<IdMService, IdMService> idmService = new ServiceTracker<>(context, + IdMService.class, new AAAServiceTrackerCustomizer<IdMService>( + new Function<IdMService, Void>() { + @Override + public Void apply(IdMService idmService) { + ServiceLocator.getInstance().setIdmService(idmService); + return null; + } + })); + servicesBuilder.add(idmService); + idmService.open(); + + final ServiceTracker<TokenAuth, TokenAuth> tokenAuthService = new ServiceTracker<>(context, + TokenAuth.class, new AAAServiceTrackerCustomizer<TokenAuth>( + new Function<TokenAuth, Void>() { + @Override + public Void apply(TokenAuth tokenAuth) { + final List<TokenAuth> tokenAuthCollection = (List<TokenAuth>) Lists.newArrayList(tokenAuth); + ServiceLocator.getInstance().setTokenAuthCollection( + tokenAuthCollection); + return null; + } + })); + servicesBuilder.add(tokenAuthService); + tokenAuthService.open(); + + final ServiceTracker<TokenStore, TokenStore> tokenStoreService = new ServiceTracker<>( + context, TokenStore.class, new AAAServiceTrackerCustomizer<TokenStore>( + new Function<TokenStore, Void>() { + @Override + public Void apply(TokenStore tokenStore) { + ServiceLocator.getInstance().setTokenStore(tokenStore); + return null; + } + })); + servicesBuilder.add(tokenStoreService); + tokenStoreService.open(); + + final ServiceTracker<ClientService, ClientService> clientService = new ServiceTracker<>( + context, ClientService.class, new AAAServiceTrackerCustomizer<ClientService>( + new Function<ClientService, Void>() { + @Override + public Void apply(ClientService clientService) { + ServiceLocator.getInstance().setClientService(clientService); + return null; + } + })); + servicesBuilder.add(clientService); + clientService.open(); + + services = servicesBuilder.build(); + + LOG.info("STS Activator initialized; ServiceTracker may still be processing"); + } + + /** + * Wrapper for AAA generic service loading. + * + * @param <S> + */ + static final class AAAServiceTrackerCustomizer<S> implements ServiceTrackerCustomizer<S, S> { + + private Function<S, Void> callback; + + public AAAServiceTrackerCustomizer(final Function<S, Void> callback) { + this.callback = callback; + } + + @Override + public S addingService(ServiceReference<S> reference) { + S service = reference.getBundle().getBundleContext().getService(reference); + LOG.info("Unable to resolve {}", service.getClass()); + try { + callback.apply(service); + } catch (Exception e) { + LOG.error("Unable to resolve {}", service.getClass(), e); + } + return service; + } + + @Override + public void modifiedService(ServiceReference<S> reference, S service) { + } + + @Override + public void removedService(ServiceReference<S> reference, S service) { + } + } + + @Override + public void destroy(BundleContext context, DependencyManager manager) throws Exception { + + for (ServiceTracker<?, ?> serviceTracker : services) { + serviceTracker.close(); + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/AnonymousPasswordValidator.java b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/AnonymousPasswordValidator.java new file mode 100644 index 00000000..55b5b61f --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/AnonymousPasswordValidator.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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 + */ + +package org.opendaylight.aaa.sts; + +import javax.servlet.http.HttpServletRequest; +import org.apache.oltu.oauth2.common.OAuth; +import org.apache.oltu.oauth2.common.validators.AbstractValidator; + +/** + * A password validator that does not enforce client identification. + * + * @author liemmn + * + */ +public class AnonymousPasswordValidator extends AbstractValidator<HttpServletRequest> { + + public AnonymousPasswordValidator() { + requiredParams.add(OAuth.OAUTH_GRANT_TYPE); + requiredParams.add(OAuth.OAUTH_USERNAME); + requiredParams.add(OAuth.OAUTH_PASSWORD); + + enforceClientAuthentication = false; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/AnonymousRefreshTokenValidator.java b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/AnonymousRefreshTokenValidator.java new file mode 100644 index 00000000..5b50c7da --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/AnonymousRefreshTokenValidator.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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 + */ + +package org.opendaylight.aaa.sts; + +import javax.servlet.http.HttpServletRequest; +import org.apache.oltu.oauth2.common.OAuth; +import org.apache.oltu.oauth2.common.validators.AbstractValidator; + +/** + * A refresh token validator that does not enforce client identification. + * + * @author liemmn + * + */ +public class AnonymousRefreshTokenValidator extends AbstractValidator<HttpServletRequest> { + + public AnonymousRefreshTokenValidator() { + requiredParams.add(OAuth.OAUTH_GRANT_TYPE); + requiredParams.add(OAuth.OAUTH_REFRESH_TOKEN); + + enforceClientAuthentication = false; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/OAuthRequest.java b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/OAuthRequest.java new file mode 100644 index 00000000..2a2b34b6 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/OAuthRequest.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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 + */ + +package org.opendaylight.aaa.sts; + +import javax.servlet.http.HttpServletRequest; +import org.apache.oltu.oauth2.as.request.AbstractOAuthTokenRequest; +import org.apache.oltu.oauth2.as.validator.UnauthenticatedAuthorizationCodeValidator; +import org.apache.oltu.oauth2.common.exception.OAuthProblemException; +import org.apache.oltu.oauth2.common.exception.OAuthSystemException; +import org.apache.oltu.oauth2.common.message.types.GrantType; +import org.apache.oltu.oauth2.common.validators.OAuthValidator; + +/** + * OAuth request wrapper. + * + * @author liemmn + * + */ +public class OAuthRequest extends AbstractOAuthTokenRequest { + + public OAuthRequest(HttpServletRequest request) throws OAuthSystemException, + OAuthProblemException { + super(request); + } + + @Override + public OAuthValidator<HttpServletRequest> initValidator() throws OAuthProblemException, + OAuthSystemException { + validators.put(GrantType.PASSWORD.toString(), AnonymousPasswordValidator.class); + validators.put(GrantType.REFRESH_TOKEN.toString(), AnonymousRefreshTokenValidator.class); + validators.put(GrantType.AUTHORIZATION_CODE.toString(), + UnauthenticatedAuthorizationCodeValidator.class); + return super.initValidator(); + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/ServiceLocator.java b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/ServiceLocator.java new file mode 100644 index 00000000..2c1f84c3 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/ServiceLocator.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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 + */ + +package org.opendaylight.aaa.sts; + +import java.util.List; +import java.util.Vector; +import org.opendaylight.aaa.api.AuthenticationService; +import org.opendaylight.aaa.api.ClientService; +import org.opendaylight.aaa.api.CredentialAuth; +import org.opendaylight.aaa.api.IdMService; +import org.opendaylight.aaa.api.PasswordCredentials; +import org.opendaylight.aaa.api.TokenAuth; +import org.opendaylight.aaa.api.TokenStore; + +/** + * A service locator to bridge between the web world and OSGi world. + * + * @author liemmn + * + */ +public class ServiceLocator { + + private static final ServiceLocator instance = new ServiceLocator(); + + protected volatile List<TokenAuth> tokenAuthCollection = new Vector<>(); + + protected volatile CredentialAuth<PasswordCredentials> credentialAuth; + + protected volatile TokenStore tokenStore; + + protected volatile AuthenticationService authenticationService; + + protected volatile IdMService idmService; + + protected volatile ClientService clientService; + + private ServiceLocator() { + } + + public static ServiceLocator getInstance() { + return instance; + } + + /** + * Called through reflection by the sts activator. + * + * @see org.opendaylight.aaa.sts.Activator + * @param ta + */ + protected void tokenAuthAdded(TokenAuth ta) { + this.tokenAuthCollection.add(ta); + } + + /** + * Called through reflection by the sts activator. + * + * @see org.opendaylight.aaa.sts.Activator + * @param ta + */ + protected void tokenAuthRemoved(TokenAuth ta) { + this.tokenAuthCollection.remove(ta); + } + + protected void tokenStoreAdded(TokenStore ts) { + this.tokenStore = ts; + } + + protected void tokenStoreRemoved(TokenStore ts) { + this.tokenStore = null; + } + + protected void authenticationServiceAdded(AuthenticationService as) { + this.authenticationService = as; + } + + protected void authenticationServiceRemoved(AuthenticationService as) { + this.authenticationService = null; + } + + protected void credentialAuthAdded(CredentialAuth<PasswordCredentials> da) { + this.credentialAuth = da; + } + + protected void credentialAuthAddedRemoved(CredentialAuth<PasswordCredentials> da) { + this.credentialAuth = null; + } + + public List<TokenAuth> getTokenAuthCollection() { + return tokenAuthCollection; + } + + public void setTokenAuthCollection(List<TokenAuth> tokenAuthCollection) { + this.tokenAuthCollection = tokenAuthCollection; + } + + public CredentialAuth<PasswordCredentials> getCredentialAuth() { + return credentialAuth; + } + + public synchronized void setCredentialAuth(CredentialAuth<PasswordCredentials> credentialAuth) { + this.credentialAuth = credentialAuth; + } + + public TokenStore getTokenStore() { + return tokenStore; + } + + public void setTokenStore(TokenStore tokenStore) { + this.tokenStore = tokenStore; + } + + public AuthenticationService getAuthenticationService() { + return authenticationService; + } + + public void setAuthenticationService(AuthenticationService authenticationService) { + this.authenticationService = authenticationService; + } + + public IdMService getIdmService() { + return idmService; + } + + public void setIdmService(IdMService idmService) { + this.idmService = idmService; + } + + public ClientService getClientService() { + return clientService; + } + + public void setClientService(ClientService clientService) { + this.clientService = clientService; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/TokenAuthFilter.java b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/TokenAuthFilter.java new file mode 100644 index 00000000..3fa7a66c --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/TokenAuthFilter.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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 + */ + +package org.opendaylight.aaa.sts; + +import com.sun.jersey.spi.container.ContainerRequest; +import com.sun.jersey.spi.container.ContainerRequestFilter; +import java.util.List; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import org.apache.oltu.oauth2.common.exception.OAuthProblemException; +import org.apache.oltu.oauth2.common.exception.OAuthSystemException; +import org.apache.oltu.oauth2.common.message.types.ParameterStyle; +import org.apache.oltu.oauth2.rs.request.OAuthAccessResourceRequest; +import org.opendaylight.aaa.api.Authentication; +import org.opendaylight.aaa.api.AuthenticationException; +import org.opendaylight.aaa.api.TokenAuth; + +/** + * A token-based authentication filter for resource providers. + * + * Deprecated: Use <code>AAAFilter</code> instead. + * + * @author liemmn + * + */ +@Deprecated +public class TokenAuthFilter implements ContainerRequestFilter { + + private final String OPTIONS = "OPTIONS"; + private final String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers"; + private final String AUTHORIZATION = "authorization"; + + @Context + private HttpServletRequest httpRequest; + + @Override + public ContainerRequest filter(ContainerRequest request) { + + // Do the CORS check first + if (checkCORSOptionRequest(request)) { + return request; + } + + // Are we up yet? + if (ServiceLocator.getInstance().getAuthenticationService() == null) { + throw new WebApplicationException( + Response.status(Status.SERVICE_UNAVAILABLE).type(MediaType.APPLICATION_JSON) + .entity("{\"error\":\"Authentication service unavailable\"}").build()); + } + + // Are we doing authentication or not? + if (ServiceLocator.getInstance().getAuthenticationService().isAuthEnabled()) { + Map<String, List<String>> headers = request.getRequestHeaders(); + + // Go through and invoke other TokenAuth first... + List<TokenAuth> tokenAuthCollection = ServiceLocator.getInstance() + .getTokenAuthCollection(); + for (TokenAuth ta : tokenAuthCollection) { + try { + Authentication auth = ta.validate(headers); + if (auth != null) { + ServiceLocator.getInstance().getAuthenticationService().set(auth); + return request; + } + } catch (AuthenticationException ae) { + throw unauthorized(); + } + } + + // OK, last chance to validate token... + try { + OAuthAccessResourceRequest or = new OAuthAccessResourceRequest(httpRequest, + ParameterStyle.HEADER); + validate(or.getAccessToken()); + } catch (OAuthSystemException | OAuthProblemException e) { + throw unauthorized(); + } + } + + return request; + } + + /** + * CORS access control : when browser sends cross-origin request, it first + * sends the OPTIONS method with a list of access control request headers, + * which has a list of custom headers and access control method such as GET. + * POST etc. You custom header "Authorization will not be present in request + * header, instead it will be present as a value inside + * Access-Control-Request-Headers. We should not do any authorization + * against such request. for more details : + * https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS + */ + + private boolean checkCORSOptionRequest(ContainerRequest request) { + if (OPTIONS.equals(request.getMethod())) { + List<String> headerList = request.getRequestHeader(ACCESS_CONTROL_REQUEST_HEADERS); + if (headerList != null && !headerList.isEmpty()) { + String header = headerList.get(0); + if (header != null && header.toLowerCase().contains(AUTHORIZATION)) { + return true; + } + } + } + return false; + } + + // Validate an ODL token... + private Authentication validate(final String token) { + Authentication auth = ServiceLocator.getInstance().getTokenStore().get(token); + if (auth == null) { + throw unauthorized(); + } else { + ServiceLocator.getInstance().getAuthenticationService().set(auth); + } + return auth; + } + + // Houston, we got a problem! + private static final WebApplicationException unauthorized() { + ServiceLocator.getInstance().getAuthenticationService().clear(); + return new UnauthorizedException(); + } + + // A custom 401 web exception that handles http basic response as well + static final class UnauthorizedException extends WebApplicationException { + private static final long serialVersionUID = -1732363804773027793L; + static final String WWW_AUTHENTICATE = "WWW-Authenticate"; + static final Object OPENDAYLIGHT = "Basic realm=\"opendaylight\""; + private static final Response response = Response.status(Status.UNAUTHORIZED) + .header(WWW_AUTHENTICATE, OPENDAYLIGHT) + .build(); + + public UnauthorizedException() { + super(response); + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/TokenEndpoint.java b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/TokenEndpoint.java new file mode 100644 index 00000000..a456d702 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/TokenEndpoint.java @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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 + */ + +package org.opendaylight.aaa.sts; + +import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import static javax.servlet.http.HttpServletResponse.SC_CREATED; +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; +import static javax.servlet.http.HttpServletResponse.SC_NOT_IMPLEMENTED; +import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; +import static javax.servlet.http.HttpServletResponse.SC_OK; +import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.oltu.oauth2.as.issuer.OAuthIssuer; +import org.apache.oltu.oauth2.as.issuer.OAuthIssuerImpl; +import org.apache.oltu.oauth2.as.issuer.UUIDValueGenerator; +import org.apache.oltu.oauth2.as.response.OAuthASResponse; +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.GrantType; +import org.apache.oltu.oauth2.common.message.types.TokenType; +import org.opendaylight.aaa.AuthenticationBuilder; +import org.opendaylight.aaa.ClaimBuilder; +import org.opendaylight.aaa.PasswordCredentialBuilder; +import org.opendaylight.aaa.api.Authentication; +import org.opendaylight.aaa.api.AuthenticationException; +import org.opendaylight.aaa.api.Claim; +import org.opendaylight.aaa.api.PasswordCredentials; + +/** + * Secure Token Service (STS) endpoint. + * + * @author liemmn + * + */ +public class TokenEndpoint extends HttpServlet { + private static final long serialVersionUID = 8272453849539659999L; + + private static final String DOMAIN_SCOPE_REQUIRED = "Domain scope required"; + private static final String NOT_IMPLEMENTED = "not_implemented"; + private static final String UNAUTHORIZED = "unauthorized"; + + static final String TOKEN_GRANT_ENDPOINT = "/token"; + static final String TOKEN_REVOKE_ENDPOINT = "/revoke"; + static final String TOKEN_VALIDATE_ENDPOINT = "/validate"; + + private transient OAuthIssuer oi; + + @Override + public void init(ServletConfig config) throws ServletException { + oi = new OAuthIssuerImpl(new UUIDValueGenerator()); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + try { + if (req.getServletPath().equals(TOKEN_GRANT_ENDPOINT)) { + createAccessToken(req, resp); + } else if (req.getServletPath().equals(TOKEN_REVOKE_ENDPOINT)) { + deleteAccessToken(req, resp); + } else if (req.getServletPath().equals(TOKEN_VALIDATE_ENDPOINT)) { + validateToken(req, resp); + } + } catch (AuthenticationException e) { + error(resp, SC_UNAUTHORIZED, e.getMessage()); + } catch (OAuthProblemException oe) { + error(resp, oe); + } catch (Exception e) { + error(resp, e); + } + } + + private void validateToken(HttpServletRequest req, HttpServletResponse resp) + throws IOException, OAuthSystemException { + String token = req.getReader().readLine(); + if (token != null) { + Authentication authn = ServiceLocator.getInstance().getTokenStore().get(token.trim()); + if (authn == null) { + throw new AuthenticationException(UNAUTHORIZED); + } else { + ServiceLocator.getInstance().getAuthenticationService().set(authn); + resp.setStatus(SC_OK); + } + } else { + throw new AuthenticationException(UNAUTHORIZED); + } + } + + // Delete an access token + private void deleteAccessToken(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + String token = req.getReader().readLine(); + if (token != null) { + if (ServiceLocator.getInstance().getTokenStore().delete(token.trim())) { + resp.setStatus(SC_NO_CONTENT); + } else { + throw new AuthenticationException(UNAUTHORIZED); + } + } else { + throw new AuthenticationException(UNAUTHORIZED); + } + } + + // Create an access token + private void createAccessToken(HttpServletRequest req, HttpServletResponse resp) + throws OAuthSystemException, OAuthProblemException, IOException { + Claim claim = null; + String clientId = null; + + OAuthRequest oauthRequest = new OAuthRequest(req); + // Any client credentials? + clientId = oauthRequest.getClientId(); + if (clientId != null) { + ServiceLocator.getInstance().getClientService() + .validate(clientId, oauthRequest.getClientSecret()); + } + + // Credential request... + if (oauthRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals(GrantType.PASSWORD.toString())) { + String domain = oauthRequest.getScopes().iterator().next(); + PasswordCredentials pc = new PasswordCredentialBuilder().setUserName( + oauthRequest.getUsername()).setPassword(oauthRequest.getPassword()) + .setDomain(domain).build(); + if (!oauthRequest.getScopes().isEmpty()) { + claim = ServiceLocator.getInstance().getCredentialAuth().authenticate(pc); + } + } else if (oauthRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals( + GrantType.REFRESH_TOKEN.toString())) { + // Refresh token... + String token = oauthRequest.getRefreshToken(); + if (!oauthRequest.getScopes().isEmpty()) { + String domain = oauthRequest.getScopes().iterator().next(); + // Authenticate... + Authentication auth = ServiceLocator.getInstance().getTokenStore().get(token); + if (auth != null && domain != null) { + List<String> roles = ServiceLocator.getInstance().getIdmService() + .listRoles(auth.userId(), domain); + if (!roles.isEmpty()) { + ClaimBuilder cb = new ClaimBuilder(auth); + cb.setDomain(domain); // scope domain + // Add roles for the scoped domain + for (String role : roles) { + cb.addRole(role); + } + claim = cb.build(); + } + } + } else { + error(resp, SC_BAD_REQUEST, DOMAIN_SCOPE_REQUIRED); + } + } else { + // Support authorization code later... + error(resp, SC_NOT_IMPLEMENTED, NOT_IMPLEMENTED); + } + + // Respond with OAuth token + oauthAccessTokenResponse(resp, claim, clientId); + } + + // Build OAuth access token response from the given claim + private void oauthAccessTokenResponse(HttpServletResponse resp, Claim claim, String clientId) + throws OAuthSystemException, IOException { + if (claim == null) { + throw new AuthenticationException(UNAUTHORIZED); + } + String token = oi.accessToken(); + + // Cache this token... + Authentication auth = new AuthenticationBuilder(new ClaimBuilder(claim).setClientId( + clientId).build()).setExpiration(tokenExpiration()).build(); + ServiceLocator.getInstance().getTokenStore().put(token, auth); + + OAuthResponse r = OAuthASResponse.tokenResponse(SC_CREATED).setAccessToken(token) + .setTokenType(TokenType.BEARER.toString()) + .setExpiresIn(Long.toString(auth.expiration())) + .buildJSONMessage(); + write(resp, r); + } + + // Token expiration + private long tokenExpiration() { + return ServiceLocator.getInstance().getTokenStore().tokenExpiration(); + } + + // Emit an error OAuthResponse with the given HTTP code + private void error(HttpServletResponse resp, int httpCode, String error) { + try { + OAuthResponse r = OAuthResponse.errorResponse(httpCode).setError(error) + .buildJSONMessage(); + write(resp, r); + } catch (Exception e1) { + // Nothing to do here + } + } + + // Emit an error OAuthResponse for the given OAuth-related exception + private void error(HttpServletResponse resp, OAuthProblemException e) { + try { + OAuthResponse r = OAuthResponse.errorResponse(SC_BAD_REQUEST).error(e) + .buildJSONMessage(); + write(resp, r); + } catch (Exception e1) { + // Nothing to do here + } + } + + // Emit an error OAuthResponse for the given generic exception + private void error(HttpServletResponse resp, Exception e) { + try { + OAuthResponse r = OAuthResponse.errorResponse(SC_INTERNAL_SERVER_ERROR) + .setError(e.getClass().getName()) + .setErrorDescription(e.getMessage()).buildJSONMessage(); + write(resp, r); + } catch (Exception e1) { + // Nothing to do here + } + } + + // Write out an OAuthResponse + private void write(HttpServletResponse resp, OAuthResponse r) throws IOException { + resp.setStatus(r.getResponseStatus()); + PrintWriter pw = resp.getWriter(); + pw.print(r.getBody()); + pw.flush(); + pw.close(); + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-sts/src/main/resources/WEB-INF/web.xml b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/resources/WEB-INF/web.xml new file mode 100644 index 00000000..83a9fa51 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/resources/WEB-INF/web.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> +<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" + version="3.0"> + + <servlet> + <servlet-name>STS</servlet-name> + <servlet-class>org.opendaylight.aaa.sts.TokenEndpoint</servlet-class> + <load-on-startup>1</load-on-startup> + </servlet> + <servlet-mapping> + <servlet-name>STS</servlet-name> + <url-pattern>/token</url-pattern> + </servlet-mapping> + <servlet-mapping> + <servlet-name>STS</servlet-name> + <url-pattern>/revoke</url-pattern> + </servlet-mapping> + <servlet-mapping> + <servlet-name>STS</servlet-name> + <url-pattern>/validate</url-pattern> + </servlet-mapping> +</web-app> diff --git a/odl-aaa-moon/aaa/aaa-authn-sts/src/test/java/org/opendaylight/aaa/sts/RestFixture.java b/odl-aaa-moon/aaa/aaa-authn-sts/src/test/java/org/opendaylight/aaa/sts/RestFixture.java new file mode 100644 index 00000000..0f806d91 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-sts/src/test/java/org/opendaylight/aaa/sts/RestFixture.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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 + */ + +package org.opendaylight.aaa.sts; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; + +/** + * Fixture for testing RESTful stuff. + * + * @author liemmn + * + */ +@Path("test") +public class RestFixture { + + @Context + private HttpServletRequest httpRequest; + + @GET + @Produces("text/plain") + public String msg() { + return "ok"; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-sts/src/test/java/org/opendaylight/aaa/sts/TokenAuthTest.java b/odl-aaa-moon/aaa/aaa-authn-sts/src/test/java/org/opendaylight/aaa/sts/TokenAuthTest.java new file mode 100644 index 00000000..7f888455 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-sts/src/test/java/org/opendaylight/aaa/sts/TokenAuthTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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 + */ + +package org.opendaylight.aaa.sts; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.anyMap; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.UniformInterfaceException; +import com.sun.jersey.test.framework.JerseyTest; +import com.sun.jersey.test.framework.WebAppDescriptor; +import org.junit.BeforeClass; +import org.junit.Test; +import org.opendaylight.aaa.AuthenticationBuilder; +import org.opendaylight.aaa.ClaimBuilder; +import org.opendaylight.aaa.api.Authentication; +import org.opendaylight.aaa.api.AuthenticationService; +import org.opendaylight.aaa.api.TokenAuth; +import org.opendaylight.aaa.api.TokenStore; +import org.opendaylight.aaa.sts.TokenAuthFilter.UnauthorizedException; + +public class TokenAuthTest extends JerseyTest { + + private static final String RS_PACKAGES = "org.opendaylight.aaa.sts"; + private static final String JERSEY_FILTERS = "com.sun.jersey.spi.container.ContainerRequestFilters"; + private static final String AUTH_FILTERS = TokenAuthFilter.class.getName(); + + private static Authentication auth = new AuthenticationBuilder(new ClaimBuilder().setUserId( + "1234").setUser("Bob").addRole("admin").addRole("user").setDomain("tenantX").build()).setExpiration( + System.currentTimeMillis() + 1000).build(); + + private static final String GOOD_TOKEN = "9b01b7cf-8a49-346d-8c47-6a61193e2b60"; + private static final String BAD_TOKEN = "9b01b7cf-8a49-346d-8c47-6a611badbeef"; + + public TokenAuthTest() throws Exception { + super(new WebAppDescriptor.Builder(RS_PACKAGES).initParam(JERSEY_FILTERS, AUTH_FILTERS) + .build()); + } + + @BeforeClass + public static void init() { + ServiceLocator.getInstance().setAuthenticationService(mock(AuthenticationService.class)); + ServiceLocator.getInstance().setTokenStore(mock(TokenStore.class)); + when(ServiceLocator.getInstance().getTokenStore().get(GOOD_TOKEN)).thenReturn(auth); + when(ServiceLocator.getInstance().getTokenStore().get(BAD_TOKEN)).thenReturn(null); + when(ServiceLocator.getInstance().getAuthenticationService().isAuthEnabled()).thenReturn( + Boolean.TRUE); + } + + @Test() + public void testGetUnauthorized() { + try { + resource().path("test").get(String.class); + fail("Shoulda failed with 401!"); + } catch (UniformInterfaceException e) { + ClientResponse resp = e.getResponse(); + assertEquals(401, resp.getStatus()); + assertTrue(resp.getHeaders().get(UnauthorizedException.WWW_AUTHENTICATE) + .contains(UnauthorizedException.OPENDAYLIGHT)); + } + } + + @Test + public void testGet() { + String resp = resource().path("test").header("Authorization", "Bearer " + GOOD_TOKEN) + .get(String.class); + assertEquals("ok", resp); + } + + @SuppressWarnings("unchecked") + @Test + public void testGetWithValidator() { + try { + // Mock a laxed tokenauth... + TokenAuth ta = mock(TokenAuth.class); + when(ta.validate(anyMap())).thenReturn(auth); + ServiceLocator.getInstance().getTokenAuthCollection().add(ta); + testGet(); + } finally { + ServiceLocator.getInstance().getTokenAuthCollection().clear(); + } + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authn-sts/src/test/java/org/opendaylight/aaa/sts/TokenEndpointTest.java b/odl-aaa-moon/aaa/aaa-authn-sts/src/test/java/org/opendaylight/aaa/sts/TokenEndpointTest.java new file mode 100644 index 00000000..06dd6302 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-sts/src/test/java/org/opendaylight/aaa/sts/TokenEndpointTest.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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 + */ + +package org.opendaylight.aaa.sts; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import org.eclipse.jetty.testing.HttpTester; +import org.eclipse.jetty.testing.ServletTester; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.opendaylight.aaa.AuthenticationBuilder; +import org.opendaylight.aaa.ClaimBuilder; +import org.opendaylight.aaa.api.AuthenticationService; +import org.opendaylight.aaa.api.Claim; +import org.opendaylight.aaa.api.ClientService; +import org.opendaylight.aaa.api.CredentialAuth; +import org.opendaylight.aaa.api.IdMService; +import org.opendaylight.aaa.api.PasswordCredentials; +import org.opendaylight.aaa.api.TokenAuth; +import org.opendaylight.aaa.api.TokenStore; + +/** + * A unit test for token endpoint. + * + * @author liemmn + * + */ +public class TokenEndpointTest { + private static final long TOKEN_TIMEOUT_SECS = 10; + private static final String CONTEXT = "/oauth2"; + private static final String DIRECT_AUTH = "grant_type=password&username=admin&password=admin&scope=pepsi&client_id=dlux&client_secret=secrete"; + private static final String REFRESH_TOKEN = "grant_type=refresh_token&refresh_token=whateverisgood&scope=pepsi"; + + private static final Claim claim = new ClaimBuilder().setUser("bob").setUserId("1234") + .addRole("admin").build(); + private final static ServletTester server = new ServletTester(); + + @BeforeClass + public static void init() throws Exception { + // Set up server + server.setContextPath(CONTEXT); + + // Add our servlet under test + server.addServlet(TokenEndpoint.class, "/revoke"); + server.addServlet(TokenEndpoint.class, "/token"); + + // Let's do dis + server.start(); + } + + @AfterClass + public static void shutdown() throws Exception { + server.stop(); + } + + @Before + public void setup() { + mockServiceLocator(); + when(ServiceLocator.getInstance().getTokenStore().tokenExpiration()).thenReturn( + TOKEN_TIMEOUT_SECS); + } + + @After + public void teardown() { + ServiceLocator.getInstance().getTokenAuthCollection().clear(); + } + + @Test + public void testCreateToken401() throws Exception { + HttpTester req = new HttpTester(); + req.setMethod("POST"); + req.setHeader("Content-Type", "application/x-www-form-urlencoded"); + req.setContent(DIRECT_AUTH); + req.setURI(CONTEXT + TokenEndpoint.TOKEN_GRANT_ENDPOINT); + req.setVersion("HTTP/1.0"); + + HttpTester resp = new HttpTester(); + resp.parse(server.getResponses(req.generate())); + assertEquals(401, resp.getStatus()); + } + + @Test + public void testCreateTokenWithPassword() throws Exception { + when( + ServiceLocator.getInstance().getCredentialAuth() + .authenticate(any(PasswordCredentials.class))).thenReturn(claim); + + HttpTester req = new HttpTester(); + req.setMethod("POST"); + req.setHeader("Content-Type", "application/x-www-form-urlencoded"); + req.setContent(DIRECT_AUTH); + req.setURI(CONTEXT + TokenEndpoint.TOKEN_GRANT_ENDPOINT); + req.setVersion("HTTP/1.0"); + + HttpTester resp = new HttpTester(); + resp.parse(server.getResponses(req.generate())); + assertEquals(201, resp.getStatus()); + assertTrue(resp.getContent().contains("expires_in\":10")); + assertTrue(resp.getContent().contains("Bearer")); + } + + @Test + public void testCreateTokenWithRefreshToken() throws Exception { + when(ServiceLocator.getInstance().getTokenStore().get(anyString())).thenReturn( + new AuthenticationBuilder(claim).build()); + when(ServiceLocator.getInstance().getIdmService().listRoles(anyString(), anyString())).thenReturn( + Arrays.asList("admin", "user")); + + HttpTester req = new HttpTester(); + req.setMethod("POST"); + req.setHeader("Content-Type", "application/x-www-form-urlencoded"); + req.setContent(REFRESH_TOKEN); + req.setURI(CONTEXT + TokenEndpoint.TOKEN_GRANT_ENDPOINT); + req.setVersion("HTTP/1.0"); + + HttpTester resp = new HttpTester(); + resp.parse(server.getResponses(req.generate())); + assertEquals(201, resp.getStatus()); + assertTrue(resp.getContent().contains("expires_in\":10")); + assertTrue(resp.getContent().contains("Bearer")); + } + + @Test + public void testDeleteToken() throws Exception { + when(ServiceLocator.getInstance().getTokenStore().delete("token_to_be_deleted")).thenReturn( + true); + + HttpTester req = new HttpTester(); + req.setMethod("POST"); + req.setHeader("Content-Type", "application/x-www-form-urlencoded"); + req.setContent("token_to_be_deleted"); + req.setURI(CONTEXT + TokenEndpoint.TOKEN_REVOKE_ENDPOINT); + req.setVersion("HTTP/1.0"); + + HttpTester resp = new HttpTester(); + resp.parse(server.getResponses(req.generate())); + assertEquals(204, resp.getStatus()); + } + + @SuppressWarnings("unchecked") + private static void mockServiceLocator() { + ServiceLocator.getInstance().setClientService(mock(ClientService.class)); + ServiceLocator.getInstance().setIdmService(mock(IdMService.class)); + ServiceLocator.getInstance().setAuthenticationService(mock(AuthenticationService.class)); + ServiceLocator.getInstance().setTokenStore(mock(TokenStore.class)); + ServiceLocator.getInstance().setCredentialAuth(mock(CredentialAuth.class)); + ServiceLocator.getInstance().getTokenAuthCollection().add(mock(TokenAuth.class)); + } +} |